UNPKG

rnt-next

Version:

CLI para criar aplicações Next.js com configuração completa: Styled Components/Tailwind, projeto limpo/exemplos, testes opcionais, dependências adicionais e backend com Prisma/MySQL - Criado por RNT

1,929 lines (1,669 loc) 62.9 kB
import chalk from "chalk"; import fs from "fs-extra"; import path from "path"; import { execCommand } from "../utils/execCommand.js"; import { ensureFolders, writeFile } from "../utils/fileOps.js"; import { installDependencies } from "../utils/installDeps.js"; export async function createProject(config) { const { appName, cssChoice, useEmpty, installTests, installExtraDeps, installBackend, } = config; const useStyledComponents = cssChoice === "Styled Components"; const useTailwind = !useStyledComponents; const finalChoice = useStyledComponents ? "styled-components" : "tailwind"; const appPath = path.join(process.cwd(), appName); // 1. Criação do projeto Next.js let createCommand = `npx create-next-app@latest ${appName} --typescript --eslint --app --src-dir --import-alias "@/*"`; createCommand += useTailwind ? " --tailwind" : " --no-tailwind"; if (useEmpty) createCommand += " --empty"; execCommand(createCommand); if (!fs.existsSync(appPath)) { console.error(`❌ Erro: Diretório ${appPath} não foi criado`); process.exit(1); } process.chdir(appPath); // 2. Instalação de dependências let prodDependencies = [ "react-redux", "@reduxjs/toolkit", "immer", "redux@latest", "clsx", "class-variance-authority", "lucide-react", ]; if (finalChoice === "styled-components") prodDependencies.unshift("styled-components"); if (installExtraDeps) prodDependencies.push( "formik", "yup", "imask", "react-imask", "react-hot-toast", "react-loading-skeleton", "framer-motion", "react-icons" ); if (installBackend) prodDependencies.push("prisma", "@prisma/client"); let devDependencies = [ "eslint-plugin-prettier", "prettier", "eslint-config-prettier", ]; if (finalChoice === "styled-components") devDependencies.push("@types/styled-components"); if (installTests) devDependencies.push( "jest", "@testing-library/react", "@testing-library/jest-dom", "@testing-library/user-event", "jest-environment-jsdom" ); installDependencies(prodDependencies, devDependencies); // 3. Estrutura de pastas const folders = [ "src/styles", "src/lib", "src/hooks", "src/utils", "src/redux", "src/redux/slices", ".vscode", ]; if (!useEmpty) folders.push( "src/app/(private)", "src/app/(public)", "src/components/ui", "src/components/ui/Button", "src/components/ui/CartWrapper", "src/components/ui/ModalWrapper", "src/components/ui/ErrorMessage", "src/components/ui/MaskedInput", "src/components/layout", "src/components/layout/header", "src/components/layout/footer" ); if (installTests) folders.push("__tests__", "src/__tests__"); if (installBackend) folders.push("prisma"); await ensureFolders(appPath, folders); // 4. Criação de arquivos de configuração (exemplo) await writeFile( path.join(appPath, ".vscode/settings.json"), JSON.stringify( { "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true, "source.fixAll": true, }, "editor.defaultFormatter": "esbenp.prettier-vscode", "[typescriptreact]": { "editor.defaultFormatter": "vscode.typescript-language-features", }, "typescript.tsdk": "node_modules/typescript/lib", }, null, 2 ) ); await writeFile( path.join(appPath, ".prettierrc.json"), JSON.stringify( { trailingComma: "none", semi: false, singleQuote: true, printWidth: 150, arrowParens: "avoid", }, null, 2 ) ); await writeFile( path.join(appPath, ".editorconfig"), `root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true ` ); // Next.js config (sem experimental turbo) let nextConfig = `/** @type {import('next').NextConfig} */ const nextConfig = {`; if (finalChoice === "styled-components") { nextConfig += ` compiler: { styledComponents: true, },`; } nextConfig += ` images: { formats: ['image/avif', 'image/webp'], domains: ['placehold.co'], }, } module.exports = nextConfig `; await writeFile(path.join(appPath, "next.config.js"), nextConfig); // Jest config se testes foram escolhidos if (installTests) { await writeFile( path.join(appPath, "jest.config.js"), `const nextJest = require('next/jest') const createJestConfig = nextJest({ // Provide the path to your Next.js app to load next.config.js and .env files dir: './', }) // Add any custom config to be passed to Jest const customJestConfig = { setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], moduleNameMapping: { // Handle module aliases (this will be automatically configured for you based on your tsconfig.json paths) '^@/(.*)$': '<rootDir>/src/$1', }, testEnvironment: 'jest-environment-jsdom', } // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async module.exports = createJestConfig(customJestConfig) ` ); await writeFile( path.join( appPath, "jest.setup.js", `import '@testing-library/jest-dom' ` ) ); } //Cria arquivo middleware.ts na raiz do projeto await writeFile( path.join(appPath, "middleware.ts"), ` import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; /** * Middleware para controle de autenticação e redirecionamentos * * Este middleware é executado antes de cada requisição e permite: * - Verificar autenticação do usuário * - Redirecionar usuários não autenticados * - Proteger rotas privadas * - Adicionar headers customizados */ export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; // Rotas que não precisam de autenticação const publicPaths = [ '/', '/login', '/register', '/forgot-password', '/about', '/contact', '/terms', '/privacy' ]; // Rotas da API que não precisam de autenticação const publicApiPaths = [ '/api/auth/login', '/api/auth/register', '/api/auth/forgot-password', '/api/public' ]; // Rotas administrativas (requerem role específico) const adminPaths = [ '/admin', '/dashboard/admin' ]; // Verificar se é uma rota pública const isPublicPath = publicPaths.some(path => pathname === path || pathname.startsWith(\`\${path}/\`) ); const isPublicApiPath = publicApiPaths.some(path => pathname.startsWith(path) ); // Se for rota pública, permitir acesso if (isPublicPath || isPublicApiPath) { return NextResponse.next(); } // Verificar token de autenticação const token = request.cookies.get('auth-token')?.value; const userRole = request.cookies.get('user-role')?.value; // Se não há token e está tentando acessar rota privada if (!token && !isPublicPath) { const loginUrl = new URL('/login', request.url); loginUrl.searchParams.set('redirect', pathname); return NextResponse.redirect(loginUrl); } // Verificar acesso a rotas administrativas const isAdminPath = adminPaths.some(path => pathname.startsWith(path) ); if (isAdminPath && userRole !== 'admin') { return NextResponse.redirect(new URL('/unauthorized', request.url)); } // Se usuário está logado e tenta acessar login/register, redirecionar para dashboard if (token && (pathname === '/login' || pathname === '/register')) { return NextResponse.redirect(new URL('/dashboard', request.url)); } // Adicionar headers customizados const response = NextResponse.next(); // Headers de segurança response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('X-Content-Type-Options', 'nosniff'); response.headers.set('Referrer-Policy', 'origin-when-cross-origin'); // Header com informações do usuário (se autenticado) if (token) { response.headers.set('X-User-Authenticated', 'true'); if (userRole) { response.headers.set('X-User-Role', userRole); } } return response; } // Configurar em quais rotas o middleware deve ser executado export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - api/public (public API routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) * - public folder files */ '/((?!_next/static|_next/image|favicon.ico|public/).*)', ], }; ` ); //Cria arquivo types.d.ts na raiz do projeto await writeFile( path.join(appPath, "types.d.ts"), ` /** * Arquivo de tipos globais do projeto * * Adicione aqui todos os tipos TypeScript que serão utilizados * em múltiplos arquivos do projeto, incluindo: * * - Interfaces de API * - Tipos de dados do banco * - Tipos de componentes compartilhados * - Tipos de estado global (Redux) * - Tipos de formulários * - Enums e constantes tipadas */ // Exemplo: Tipos de usuário export interface User { id: string; name: string; email: string; avatar?: string; role: UserRole; createdAt: Date; updatedAt: Date; } export enum UserRole { ADMIN = 'admin', USER = 'user', MODERATOR = 'moderator' } // Exemplo: Tipos de API Response export interface ApiResponse<T = any> { success: boolean; data?: T; message?: string; errors?: string[]; } // Exemplo: Tipos de formulário export interface LoginForm { email: string; password: string; rememberMe?: boolean; } export interface RegisterForm { name: string; email: string; password: string; confirmPassword: string; } // Exemplo: Tipos de componentes export interface ButtonProps { variant?: 'primary' | 'secondary' | 'outline' | 'ghost'; size?: 'sm' | 'md' | 'lg'; loading?: boolean; disabled?: boolean; children: React.ReactNode; onClick?: () => void; } export interface ModalProps { isOpen: boolean; onClose: () => void; title?: string; children: React.ReactNode; size?: 'sm' | 'md' | 'lg' | 'xl'; } // Exemplo: Tipos de estado export interface AuthState { user: User | null; isAuthenticated: boolean; isLoading: boolean; error: string | null; } // Exemplo: Tipos de produtos (e-commerce) export interface Product { id: string; name: string; description: string; price: number; images: string[]; category: string; stock: number; featured: boolean; } export interface CartItem { product: Product; quantity: number; } export interface CartState { items: CartItem[]; total: number; isOpen: boolean; } ` ); //Cria arquivo useAppDispatch.ts dentro de src/hooks await writeFile( path.join(appPath, "src/hooks/useAppDispatch.ts"), ` import { AppDispatch, RootState } from '@/redux/store' import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' export const useAppDispatch = () => useDispatch<AppDispatch>() export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector ` ); // Criar colorUtils await writeFile( path.join(appPath, "src/utils/colorUtils.ts"), `// 🎨 COLOR UTILS - Utilitários para geração de variantes de cores HSL export function colorHSLVariants(h: number, s: number, l: number) { const clamp = (val: number) => Math.min(100, Math.max(0, val)) return { base: \`hsl(\${h}, \${s}%, \${clamp(l)}%)\`, light: \`hsl(\${h}, \${s}%, \${clamp(l + 10)}%)\`, light02: \`hsla(\${h}, \${s}%, \${clamp(l + 2)}%, 0.2)\`, light04: \`hsla(\${h}, \${s}%, \${clamp(l + 4)}%, 0.4)\`, light08: \`hsla(\${h}, \${s}%, \${clamp(l + 6)}%, 0.8)\`, light20: \`hsl(\${h}, \${s}%, \${clamp(l + 20)}%)\`, light30: \`hsl(\${h}, \${s}%, \${clamp(l + 30)}%)\`, light40: \`hsl(\${h}, \${s}%, \${clamp(l + 40)}%)\`, light50: \`hsl(\${h}, \${s}%, \${clamp(l + 50)}%)\`, dark: \`hsl(\${h}, \${s}%, \${clamp(l - 10)}%)\`, dark02: \`hsla(\${h}, \${s}%, \${clamp(l - 2)}%, 0.2)\`, dark04: \`hsla(\${h}, \${s}%, \${clamp(l - 4)}%, 0.4)\`, dark08: \`hsla(\${h}, \${s}%, \${clamp(l - 6)}%, 0.8)\`, dark20: \`hsl(\${h}, \${s}%, \${clamp(l - 20)}%)\`, dark30: \`hsl(\${h}, \${s}%, \${clamp(l - 30)}%)\`, dark40: \`hsl(\${h}, \${s}%, \${clamp(l - 40)}%)\`, dark50: \`hsl(\${h}, \${s}%, \${clamp(l - 50)}%)\` } } ` ); // Theme configuration atualizado await writeFile( path.join(appPath, "src/styles/theme.ts"), `// 🎨 ARQUIVO DE TEMA - Configurações de cores e breakpoints do projeto import { colorHSLVariants } from '@/utils/colorUtils' export const media = { pc: '@media (max-width: 1024px)', tablet: '@media (max-width: 768px)', mobile: '@media (max-width: 480px)', } export const transitions = { default: 'all 0.2s ease' } export const baseBlue = colorHSLVariants(220, 80, 50) export const baseGreen = colorHSLVariants(100, 100, 50) export const baseRed = colorHSLVariants(0, 100, 50) export const baseCyan = colorHSLVariants(180, 150, 50) export const theme = { colors: { baseBlue: baseBlue, baseGreen: baseGreen, baseRed: baseRed, baseCyan: baseCyan, primaryColor: '#011627', secondaryColor: '#023864', thirdColor: '#0d6efd', forthColor: '#E25010', textColor: '#fff', yellow: '#ffff00', yellow2: '#E1A32A', blue: '#0000FF', blue2: '#1E90FF', gray: '#666666', gray2: '#a1a1a1', orange: '#ff4500', orange2: '#ff7f50', black: '#000', red: '#FF0000', redHover: '#FF4837', error: '#AB2E46', green: '#008000', green2: '#44BD32', neonBlue: '#00FFD5 ', neonGree: '#00FF6A ' } } export const darkTheme = { colors: { primaryColor: '#13161b', secondaryColor: '#1c1f25', background: '#2F2F2F', inputColor: '#0d0e12', white: '#121212', blue: '#0d6efd', blue2: '#0000FF', red: '#FF3347', green: '#28a745', orange: '#ff4500', yellow: '#fffF00', shadow: '#000', grey: '#a1a1a1', textColor: '#f1f1f1', neon: { pink1: '#FF1493', pink2: '#FF00FF', green1: '#39FF14', green2: '#00FF7F', blue1: '#00BFFF', blue2: '#00FFFF' } } } export const lightTheme = { colors: { primaryColor: '#666666', secondaryColor: '#a1a1a1', background: '#808080', inputColor: '#f1f1f1', white: '#ffffff', blue: '#3a86ff', blue2: '#0000FF', red: '#FF0000', green: '#34d399', orange: '#ff4500', yellow: '#ffff00', shadow: '#000', grey: '#a1a1a1', textColor: '#13161b', neon: { pink1: '#FF1493', pink2: '#FF00FF', green1: '#39FF14', green2: '#00FF7F', blue1: '#00FFFF', blue2: '#00BFFF' } } } export const themeConfig = { light: lightTheme, dark: darkTheme } ` ); // Cria componente de Botão estilizado await writeFile( path.join(appPath, "src/components/ui/Button/Button.tsx"), ` "use client"; import Link from "next/link"; import React, { forwardRef } from "react"; import { ButtonContent, IconWrapper, StyledButton } from "./ButtonStyles"; type ButtonVariants = "primary" | "secondary" | "outline" | "ghost" | "danger"; type ButtonSizes = "sm" | "md" | "lg"; type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string; }; type NativeButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & { href?: undefined; }; export interface CommonButtonProps { variant?: ButtonVariants; size?: ButtonSizes; loading?: boolean; fullWidth?: boolean; leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; children?: React.ReactNode; } export type ButtonProps = CommonButtonProps & (AnchorProps | NativeButtonProps); export const Button = forwardRef< HTMLButtonElement | HTMLAnchorElement, ButtonProps >( ( { variant = "primary", size = "md", loading = false, fullWidth = false, leftIcon, rightIcon, disabled, children, ...props }, ref ) => { const isDisabled = disabled || loading; const content = ( <StyledButton as={(props as AnchorProps).href ? "a" : "button"} ref={ref as any} $variant={variant} $size={size} $loading={loading} $fullWidth={fullWidth} disabled={isDisabled} aria-disabled={isDisabled} {...(props as any)} > <ButtonContent $loading={loading}> {leftIcon && <IconWrapper>{leftIcon}</IconWrapper>} {children} {rightIcon && <IconWrapper>{rightIcon}</IconWrapper>} </ButtonContent> </StyledButton> ); if ((props as AnchorProps).href) { return ( <Link href={(props as AnchorProps).href} passHref legacyBehavior> {content} </Link> ); } return content; } ); Button.displayName = "Button"; export default Button; ` ); // Cria estilos do Botão await writeFile( path.join(appPath, "src/components/ui/Button/ButtonStyles.ts"), String.raw` import styled, { css } from 'styled-components'; interface StyledButtonProps { $variant: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'; $size: 'sm' | 'md' | 'lg'; $loading: boolean; $fullWidth: boolean; } const buttonVariants = { primary: css\` background-color: \${({ theme }) => theme.colors.primary.base}; color: \${({ theme }) => theme.colors.white}; border: 2px solid \${({ theme }) => theme.colors.primary.base}; &:hover:not(:disabled) { background-color: \${({ theme }) => theme.colors.primary.dark}; border-color: \${({ theme }) => theme.colors.primary.dark}; transform: translateY(-1px); } &:active:not(:disabled) { background-color: \${({ theme }) => theme.colors.primary.dark20}; transform: translateY(0); } \`, secondary: css\` background-color: \${({ theme }) => theme.colors.secondary.base}; color: \${({ theme }) => theme.colors.white}; border: 2px solid \${({ theme }) => theme.colors.secondary.base}; &:hover:not(:disabled) { background-color: \${({ theme }) => theme.colors.secondary.dark}; border-color: \${({ theme }) => theme.colors.secondary.dark}; transform: translateY(-1px); } &:active:not(:disabled) { background-color: \${({ theme }) => theme.colors.secondary.dark20}; transform: translateY(0); } \`, outline: css\` background-color: transparent; color: \${({ theme }) => theme.colors.primary.base}; border: 2px solid \${({ theme }) => theme.colors.primary.base}; &:hover:not(:disabled) { background-color: \${({ theme }) => theme.colors.primary.base}; color: \${({ theme }) => theme.colors.white}; transform: translateY(-1px); } &:active:not(:disabled) { background-color: \${({ theme }) => theme.colors.primary.dark}; transform: translateY(0); } \`, ghost: css\` background-color: transparent; color: \${({ theme }) => theme.colors.text.primary}; border: 2px solid transparent; &:hover:not(:disabled) { background-color: \${({ theme }) => theme.colors.gray.light40}; transform: translateY(-1px); } &:active:not(:disabled) { background-color: \${({ theme }) => theme.colors.gray.light30}; transform: translateY(0); } \`, danger: css\` background-color: \${({ theme }) => theme.colors.error.base}; color: \${({ theme }) => theme.colors.white}; border: 2px solid \${({ theme }) => theme.colors.error.base}; &:hover:not(:disabled) { background-color: \${({ theme }) => theme.colors.error.dark}; border-color: \${({ theme }) => theme.colors.error.dark}; transform: translateY(-1px); } &:active:not(:disabled) { background-color: \${({ theme }) => theme.colors.error.dark20}; transform: translateY(0); } \` }; const buttonSizes = { sm: css\` padding: 8px 16px; font-size: 14px; min-height: 36px; \`, md: css\` padding: 12px 24px; font-size: 16px; min-height: 44px; \`, lg: css\` padding: 16px 32px; font-size: 18px; min-height: 52px; \` }; export const StyledButton = styled.button<StyledButtonProps>\` display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-family: \${({ theme }) => theme.fonts.primary}; font-weight: 600; line-height: 1; text-decoration: none; text-align: center; white-space: nowrap; border-radius: \${({ theme }) => theme.borderRadius.md}; cursor: pointer; transition: all 0.2s ease-in-out; position: relative; overflow: hidden; \${({ $size }) => buttonSizes[$size]} \${({ $variant }) => buttonVariants[$variant]} \${({ $fullWidth }) => $fullWidth && css\` width: 100%; \`} &:disabled { opacity: 0.6; cursor: not-allowed; transform: none !important; } \${({ $loading }) => $loading && css\` cursor: not-allowed; &::before { content: ''; position: absolute; top: 50%; left: 50%; width: 16px; height: 16px; margin: -8px 0 0 -8px; border: 2px solid transparent; border-top: 2px solid currentColor; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } \`} &::after { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; border-radius: 50%; background: rgba(255, 255, 255, 0.3); transform: translate(-50%, -50%); transition: width 0.3s, height 0.3s; } &:active:not(:disabled)::after { width: 300px; height: 300px; } &:focus-visible { outline: 2px solid \${({ theme }) => theme.colors.primary.base}; outline-offset: 2px; } @media (max-width: \${({ theme }) => theme.breakpoints.sm}) { \${({ $size }) => $size === 'lg' && buttonSizes.md} \${({ $size }) => $size === 'md' && buttonSizes.sm} } \`; export const ButtonContent = styled.span<{ $loading: boolean }>\` display: flex; align-items: center; gap: 8px; opacity: \${({ $loading }) => $loading ? 0 : 1}; transition: opacity 0.2s ease-in-out; \`; export const IconWrapper = styled.span\` display: flex; align-items: center; justify-content: center; svg { width: 1em; height: 1em; } \`; ` ); // Cria componente de CartWrapper await writeFile( path.join(appPath, "src/components/ui/CartWrapper/CartWrapper.tsx"), ` import { AnimatePresence, motion } from "framer-motion"; import { ReactNode } from "react"; type CartWrapperProps = { isOpen: boolean; onClose: () => void; children: ReactNode; }; export const CartWrapper = ({ isOpen, onClose, children }: CartWrapperProps) => { return ( <AnimatePresence> {isOpen && ( <> <motion.div key="cart-backdrop" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.3, ease: "easeOut", opacity: { duration: 0.3, }, when: "beforeChildren", }} onClick={onClose} style={{ position: "fixed", top: 0, left: 0, width: "100vw", height: "100vh", backdropFilter: "blur(5px)", zIndex: 99, }} /> <motion.aside key="cart-panel" initial={{ x: "100%" }} animate={{ x: 0 }} exit={{ x: "100%" }} transition={{ duration: 0.3, ease: "easeInOut" }} style={{ position: "fixed", top: 0, right: 0, width: "100%", height: "100vh", display: "flex", justifyContent: "end", zIndex: 100, }} onClick={onClose} > <div onClick={(e) => e.stopPropagation()}>{children}</div> </motion.aside> </> )} </AnimatePresence> ); }; ` ); //cria componente de ErrorMessage await writeFile( path.join(appPath, "src/components/ui/ErrorMessage/ErrorMessage.tsx"), ` import { BiSolidError } from "react-icons/bi"; import { ErrorMessageContainer, ErrorMessageContent } from "./ErrorMessageStyles"; type Props = { message: string } export const ErrorMessage = ({ message }: Props) => ( <ErrorMessageContainer role="alert" aria-label="Mensagem de erro" className="container"> <ErrorMessageContent> <BiSolidError /> {message} </ErrorMessageContent> </ErrorMessageContainer> ) ` ); // cria estilo do ErrorMessage await writeFile( path.join(appPath, "src/components/ui/ErrorMessage/ErrorMessageStyles.ts"), `import { theme } from '@/styles/theme'; import { styled } from 'styled-components'; export const ErrorMessageContainer = styled.div\`\`; export const ErrorMessageContent = styled.div\` padding: 1rem; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; font-weight: 500; margin: 1rem; text-align: center; color: \${theme.colors.baseBlue.light40}; background-color: \${theme.colors.baseRed.dark08}; svg { font-size: 2rem; margin-right: 0.5rem; } \`; ` ); // Cria componente de MaskedInput await writeFile( path.join(appPath, "src/components/ui/MaskedInput/MaskedInput.tsx"), `'use client'; import React, { forwardRef, useEffect, useRef, useImperativeHandle } from 'react'; import { IMask, IMaskInput } from 'react-imask'; import styled from 'styled-components'; const StyledInputWrapper = styled.div<{ $hasError: boolean; $fullWidth: boolean }>\` display: \${({ $fullWidth }) => $fullWidth ? 'block' : 'inline-block'}; width: \${({ $fullWidth }) => $fullWidth ? '100%' : 'auto'}; position: relative; \`; const StyledInput = styled.input<{ $hasError: boolean }>\` width: 100%; padding: 12px 16px; font-size: 16px; font-family: \${({ theme }) => theme.fonts.primary}; line-height: 1.5; background-color: \${({ theme }) => theme.colors.background.secondary}; border: 2px solid \${({ theme, $hasError }) => $hasError ? theme.colors.error.base : theme.colors.gray.light20}; border-radius: \${({ theme }) => theme.borderRadius.md}; color: \${({ theme }) => theme.colors.text.primary}; transition: all 0.2s ease-in-out; &:focus { outline: none; border-color: \${({ theme, $hasError }) => $hasError ? theme.colors.error.base : theme.colors.primary.base}; box-shadow: 0 0 0 3px \${({ theme, $hasError }) => $hasError ? theme.colors.error.light40 : theme.colors.primary.light40}; } &:hover:not(:focus) { border-color: \${({ theme, $hasError }) => $hasError ? theme.colors.error.dark : theme.colors.gray.base}; } &:disabled { background-color: \${({ theme }) => theme.colors.gray.light50}; border-color: \${({ theme }) => theme.colors.gray.light30}; color: \${({ theme }) => theme.colors.text.disabled}; cursor: not-allowed; } &::placeholder { color: \${({ theme }) => theme.colors.text.placeholder}; } &::-webkit-outer-spin-button, &::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } &[type=number] { -moz-appearance: textfield; } \`; const Label = styled.label<{ $required: boolean }>\` display: block; margin-bottom: 6px; font-size: 14px; font-weight: 600; color: \${({ theme }) => theme.colors.text.primary}; \${({ $required }) => $required && \` &::after { content: ' *'; color: \${({ theme }) => theme.colors.error.base}; } \`} \`; const ErrorMessage = styled.span\` display: block; margin-top: 4px; font-size: 12px; color: \${({ theme }) => theme.colors.error.base}; \`; const HelperText = styled.span\` display: block; margin-top: 4px; font-size: 12px; color: \${({ theme }) => theme.colors.text.secondary}; \`; export interface MaskedInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> { mask: string | RegExp | Array<string | RegExp> | Function; maskOptions?: any; label?: string; required?: boolean; error?: string; helperText?: string; fullWidth?: boolean; onChange?: (value: string, unmaskedValue: string) => void; onAccept?: (value: string, mask: IMask.InputMask<any>) => void; onComplete?: (value: string, mask: IMask.InputMask<any>) => void; } export const MaskedInput = forwardRef<HTMLInputElement, MaskedInputProps>( ( { mask, maskOptions = {}, label, required = false, error, helperText, fullWidth = false, onChange, onAccept, onComplete, id, ...props }, ref ) => { const inputRef = useRef<HTMLInputElement>(null); const maskRef = useRef<IMask.InputMask<any>>(); useImperativeHandle(ref, () => inputRef.current!); const inputId = id || \`masked-input-\${Math.random().toString(36).substr(2, 9)}\`; const hasError = Boolean(error); const handleAccept = (value: string, mask: IMask.InputMask<any>) => { const unmaskedValue = mask.unmaskedValue; onChange?.(value, unmaskedValue); onAccept?.(value, mask); }; const handleComplete = (value: string, mask: IMask.InputMask<any>) => { onComplete?.(value, mask); }; return ( <StyledInputWrapper $hasError={hasError} $fullWidth={fullWidth}> {label && ( <Label htmlFor={inputId} $required={required}> {label} </Label> )} <IMaskInput {...props} id={inputId} ref={inputRef} mask={mask} {...maskOptions} onAccept={handleAccept} onComplete={handleComplete} render={(ref, props) => ( <StyledInput {...props} ref={ref} $hasError={hasError} /> )} /> {error && <ErrorMessage>{error}</ErrorMessage>} {!error && helperText && <HelperText>{helperText}</HelperText>} </StyledInputWrapper> ); } ); MaskedInput.displayName = 'MaskedInput'; export default MaskedInput; export const commonMasks = { cpf: '000.000.000-00', cnpj: '00.000.000/0000-00', phone: '(00) 00000-0000', landline: '(00) 0000-0000', cep: '00000-000', date: '00/00/0000', time: '00:00', rg: '00.000.000-0', currency: 'R$ num', percentage: 'num %', creditCard: '0000 0000 0000 0000', cvv: '000', expiryDate: '00/00' }; export const commonMaskOptions = { currency: { blocks: { num: { mask: Number, scale: 2, thousandsSeparator: '.', radix: ',', mapToRadix: ['.'], min: 0 } } }, percentage: { blocks: { num: { mask: Number, scale: 2, min: 0, max: 100 } } } }; ` ); // cria componente ModalWrapper await writeFile( path.join(appPath, "src/components/ui/ModalWrapper/ModalWrapper.tsx"), ` import { AnimatePresence, motion } from "framer-motion"; import { ReactNode } from "react"; type ModalWrapperProps = { isOpen: boolean; children: ReactNode; onClose: () => void; }; export const ModalWrapper = ({ isOpen, children, onClose }: ModalWrapperProps) => { return ( <AnimatePresence> {isOpen && ( <motion.div key="modal" initial={{ opacity: 0, y: -30 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -30 }} transition={{ duration: 0.3, ease: "easeInOut" }} style={{ position: "fixed", top: 0, left: 0, width: "100vw", height: "100vh", zIndex: 100, display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "rgba(0, 0, 0, 0.5)", backdropFilter: "blur(5px)", }} onClick={onClose} > <div onClick={(e) => e.stopPropagation()}> {children} </div> </motion.div> )} </AnimatePresence> ); }; ` ); // Criar arquivos específicos baseados na escolha if (finalChoice === "styled-components") { await createStyledComponentsFiles(appPath); } else { await createTailwindFiles(appPath); } // Middleware await writeFile( path.join(appPath, "src/middleware.ts"), `// 🔒 MIDDLEWARE - Controle de autenticação e rotas import { MiddlewareConfig, NextRequest, NextResponse } from 'next/server' const publicRoutes = [ { path: '/', whenAuthenticated: 'next' }, { path: '/sign-in', whenAuthenticated: 'redirect' }, { path: '/register', whenAuthenticated: 'redirect' }, { path: '/pricing', whenAuthenticated: 'next' } ] as const const REDIRECT_WHEN_NOT_AUTHENTICATED_ROUTE = '/sign-in' export function middleware(request: NextRequest) { const path = request.nextUrl.pathname const matchedPublicRoute = publicRoutes.find(route => route.path === path) const authToken = request.cookies.get('token') //1 - Se o usuário não estiver autenticado e o caminho da rota não for público, redireciona para a página de login if (!authToken && matchedPublicRoute) { return NextResponse.next() } //2 - se o usuario não estiver autenticado e o caminho da rota não for público, redireciona para a página de login if (!authToken && !matchedPublicRoute) { const redirectUrl = request.nextUrl.clone() redirectUrl.pathname = REDIRECT_WHEN_NOT_AUTHENTICATED_ROUTE return NextResponse.redirect(redirectUrl) } //3 - Se o usuário estiver autenticado e o caminho da rota não for público, redireciona para a página inicial if (authToken && matchedPublicRoute?.whenAuthenticated === 'redirect') { const redirectUrl = request.nextUrl.clone() redirectUrl.pathname = '/' return NextResponse.redirect(redirectUrl) } return NextResponse.next() } export const config: MiddlewareConfig = { /* * Match all request paths except for the ones starting with: * - api (API routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico, sitemap.xml, robots.txt (metadata files) */ matcher: ['/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)'] } ` ); // Providers await writeFile( path.join(appPath, "src/components/providers.tsx"), `'use client' // 🔧 PROVIDERS - Configuração de contextos globais import { Provider } from 'react-redux' import { store } from '@/redux/store' export function Providers({ children }: { children: React.ReactNode }) { return ( <Provider store={store}> {children} </Provider> ) }` ); // .env file await writeFile( path.join(appPath, ".env"), `# 🔐 VARIÁVEIS DE AMBIENTE - Configurações do projeto # Next.js NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET=your-secret-key-here # Database (se backend foi escolhido) ${ installBackend ? `DATABASE_URL="mysql://username:password@localhost:3306/database_name"` : `# DATABASE_URL="mysql://username:password@localhost:3306/database_name"` } # API Keys # API_KEY=your-api-key-here ` ); // Redux Store baseado na escolha de testes if (installTests) { await writeFile( path.join(appPath, "src/redux/store.ts"), `// 🏪 REDUX STORE - Configuração do gerenciamento de estado com preloaded state para testes import { configureStore } from '@reduxjs/toolkit' import authReducer from './slices/authSlice' export const store = configureStore({ reducer: { auth: authReducer } }) export type RootState = ReturnType<typeof store.getState> export type AppDispatch = typeof store.dispatch ` ); // AuthSlice para testes await writeFile( path.join(appPath, "src/redux/slices/authSlice.ts"), `// 🔐 AUTH SLICE - Gerenciamento de estado de autenticação import { createSlice, PayloadAction } from '@reduxjs/toolkit' interface AuthState { user: { id: string name: string email: string } | null isAuthenticated: boolean loading: boolean } const initialState: AuthState = { user: null, isAuthenticated: false, loading: false } const authSlice = createSlice({ name: 'auth', initialState, reducers: { loginStart: (state) => { state.loading = true }, loginSuccess: (state, action: PayloadAction<{ id: string; name: string; email: string }>) => { state.loading = false state.isAuthenticated = true state.user = action.payload }, loginFailure: (state) => { state.loading = false state.isAuthenticated = false state.user = null }, logout: (state) => { state.isAuthenticated = false state.user = null state.loading = false } } }) export const { loginStart, loginSuccess, loginFailure, logout } = authSlice.actions export default authSlice.reducer ` ); } else { await writeFile( path.join(appPath, "src/redux/store.ts"), `// 🏪 REDUX STORE - Configuração simples do gerenciamento de estado import { configureStore } from '@reduxjs/toolkit' import authReducer from './slices/authSlice' export const store = configureStore({ reducer: { auth: authReducer } }) export type RootState = ReturnType<typeof store.getState> export type AppDispatch = typeof store.dispatch ` ); // AuthSlice simples await writeFile( path.join(appPath, "src/redux/slices/authSlice.ts"), `// 🔐 AUTH SLICE - Gerenciamento de estado de autenticação import { createSlice, PayloadAction } from '@reduxjs/toolkit' interface AuthState { user: { id: string name: string email: string } | null isAuthenticated: boolean } const initialState: AuthState = { user: null, isAuthenticated: false } const authSlice = createSlice({ name: 'auth', initialState, reducers: { loginSuccess: (state, action: PayloadAction<{ id: string; name: string; email: string }>) => { state.isAuthenticated = true state.user = action.payload }, logout: (state) => { state.isAuthenticated = false state.user = null } } }) export const { loginSuccess, logout } = authSlice.actions export default authSlice.reducer ` ); } // Configuração do Prisma se backend foi escolhido if (installBackend) { console.log("🗄️ Configurando Prisma..."); // Executar prisma init execCommand("npx prisma init"); // Schema do Prisma com comentários await writeFile( path.join(appPath, "prisma/schema.prisma"), `// 🗄️ PRISMA SCHEMA - Configuração do banco de dados // Este arquivo define a estrutura do seu banco de dados // Configuração do gerador do Prisma Client generator client { provider = "prisma-client-js" } // Configuração da conexão com o banco de dados datasource db { provider = "mysql" url = env("DATABASE_URL") } // 👤 MODEL USER - Modelo básico de usuário model User { id String @id @default(cuid()) email String @unique name String? password String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("users") } // 📝 COMO USAR ESTE ARQUIVO: // // 1. Configure sua DATABASE_URL no arquivo .env // Exemplo: DATABASE_URL="mysql://username:password@localhost:3306/database_name" // // 2. Para criar o banco de dados e tabelas: // npx prisma db push // // 3. Para gerar o Prisma Client: // npx prisma generate // // 4. Para visualizar o banco no Prisma Studio: // npx prisma studio // // 5. Para usar um banco existente: // npx prisma db pull (puxa a estrutura do banco existente) // npx prisma generate (gera o client baseado na estrutura) // // 6. Para criar e aplicar migrations: // npx prisma migrate dev --name init // // 📚 DOCUMENTAÇÃO: https://www.prisma.io/docs ` ); // Arquivo de configuração do Prisma Client await writeFile( path.join(appPath, "src/lib/prisma.ts"), `// 🗄️ PRISMA CLIENT - Configuração da conexão com o banco de dados import { PrismaClient } from '@prisma/client' const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined } export const prisma = globalForPrisma.prisma ?? new PrismaClient() if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma ` ); } // Criar layout baseado na escolha await createLayout(appPath, finalChoice, useEmpty); // Criar arquivos de exemplo se não for projeto vazio if (!useEmpty) { await createExampleFiles(appPath, finalChoice, installTests, appName); } console.log("\n" + "=".repeat(50)); console.log("✅ PROJETO CRIADO COM SUCESSO!"); console.log("=".repeat(50)); console.log(`📁 Navegue para: cd ${appName}`); console.log("🚀 Execute: npm run dev"); console.log( `🎨 CSS: ${ finalChoice === "styled-components" ? "Styled Components" : "Tailwind CSS" }` ); console.log(`📦 Tipo: ${useEmpty ? "Projeto limpo" : "Com exemplos"}`); if (installTests) { console.log("🧪 Testes: npm test"); } if (installExtraDeps) { console.log("📚 Dependências adicionais instaladas"); } if (installBackend) { console.log("🗄️ Backend: Prisma + MySQL configurado"); console.log(" - Configure DATABASE_URL no .env"); console.log(" - Execute: npx prisma db push"); console.log(" - Execute: npx prisma generate"); } console.log("💙 Criado por RNT"); console.log("=".repeat(50)); } async function createStyledComponentsFiles(appPath) { // Global Styles para Styled Components atualizado await writeFile( path.join(appPath, "src/styles/globalStyles.tsx"), `'use client' // 🎨 GLOBAL STYLES - Estilos globais com Styled Components import styled, { createGlobalStyle } from 'styled-components'; import { theme } from './theme'; export const GlobalStyles = createGlobalStyle\` * { margin: 0; padding: 0; box-sizing: border-box; } html { scroll-behavior: smooth; } body { background-color: \${theme.colors.baseBlue.dark20}; color: \${theme.colors.baseBlue.dark50}; } .container { max-width: 1024px; margin: 0 auto; } \`; export const OverlayBlur = styled.div\` position: absolute; top: 0; left: 0; width: 100%; height: 100%; backdrop-filter: blur(5px); z-index: 100; \` export const OverlayDarck = styled.div\` position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); z-index: 10; \` export const CloseButton = styled.button\` border-radius: 50%; margin: 0; padding: 0; position: absolute; top: 0px; right: 0px; background-color: transparent; border: transparent; cursor: pointer; svg { font-size: 24px; color: \${theme.colors.baseBlue.dark20}; } &:hover { svg { color: \${theme.colors.baseBlue.light}; } } \` export const TitleH2 = styled.h2\` font-size: 24px; font-weight: 600; color: \${theme.colors.baseBlue.light30}; \` export const TitleH3 = styled.h3\` font-size: 18px; font-weight: 600; margin-bottom: 12px; color: \${theme.colors.baseBlue.dark30}; \` export const MinorTextH4 = styled.h3\` font-size: 14px; font-weight: 300; margin-bottom: 8px; color: \${theme.colors.baseBlue.dark30}; \` ` ); // Styled Components Registry await writeFile( path.join(appPath, "src/lib/styled-components-registry.tsx"), `'use client' // 🔧 STYLED COMPONENTS REGISTRY - Necessário para SSR import React, { useState } from 'react' import { useServerInsertedHTML } from 'next/navigation' import { ServerStyleSheet, StyleSheetManager } from 'styled-components' export default function StyledComponentsRegistry({ children, }: { children: React.ReactNode }) { // Only create stylesheet once with lazy initial state // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()) useServerInsertedHTML(() => { const styles = styledComponentsStyleSheet.getStyleElement() styledComponentsStyleSheet.instance.clearTag() return <>{styles}</> }) if (typeof window !== 'undefined') return <>{children}</> return ( <StyleSheetManager sheet={styledComponentsStyleSheet.instance}> {children} </StyleSheetManager> ) }` ); } async function createTailwindFiles(appPath) { // Verificar se o globals.css já existe e atualizar const globalsPath = "src/app/globals.css"; if (fs.existsSync(globalsPath)) { // Ler o conteúdo existente e adicionar customizações let existingContent = await fs.readFile(globalsPath, "utf8"); // Adicionar customizações se não existirem if (!existingContent.includes("/* RNT Custom Styles */")) { const customStyles = ` /* RNT Custom Styles */ * { margin: 0; padding: 0; box-sizing: border-box; list-style: none; } body { font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #011627; color: #fff; transition: background-color 0.3s, color 0.3s; } html { scroll-behavior: smooth; } .container { max-width: 1024px; margin: 0 auto; } `; await writeFile(globalsPath, existingContent + customStyles); } } } async function createLayout(appPath, cssChoice, useEmpty) { if (cssChoice === "styled-components") { // Layout com Styled Components await writeFile( path.join(appPath, "src/app/layout.tsx"), `import type { Metadata } from 'next' import { Inter } from 'next/font/google' import StyledComponentsRegistry from '@/lib/styled-components-registry' import { GlobalStyles } from '@/styles/globalStyles' import { Providers } from '@/components/providers'${ !useEmpty ? ` import Header from '@/components/layout/header/Header' import Footer from '@/components/layout/footer/Footer'` : "" } const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'RNT Next App', description: 'Aplicação Next.js criada com RNT CLI', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="pt-BR"> <body className={inter.className}> <StyledComponentsRegistry> <GlobalStyles /> <Providers>${ !useEmpty ? ` <Header /> {children} <Footer />` : ` {children}` } </Providers> </StyledComponentsRegistry> </body> </html> ) } ` ); } else { // Layout com Tailwind await writeFile( path.join(appPath, "src/app/layout.tsx"), `import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' import { Providers } from '@/components/providers'${ !useEmpty ? ` import Header from '@/components/layout/header/Header' import Footer from '@/components/layout/footer/Footer'` : "" } const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'RNT Next App', description: 'Aplicação Next.js criada com RNT CLI', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="pt-BR"> <body className={inter.className}> <Providers>${ !useEmpty ? ` <Header