UNPKG

y3-app

Version:
716 lines (664 loc) 23.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.reactTsStructure = void 0; const docker_1 = require("../config/docker"); exports.reactTsStructure = { src: { 'index.ts': '// Barrel file for exports\n', 'App.tsx': `import { Route, BrowserRouter as Router, Routes } from "react-router"; import AuthLayout from "./components/layouts/AuthLayout/AuthLayout"; function App() { return ( <Router> <Routes> <Route path="/" element={<AuthLayout />} /> </Routes> </Router> ); } export default App;`, 'index.css': `@tailwind base; @tailwind components; @tailwind utilities;`, 'main.tsx': `import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode>, ); `, 'vite-env.d.ts': '/// <reference types="vite/client" />\n', assets: { fonts: {}, icons: {}, images: {}, }, components: { LoginForm: `import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; import type { LoginActionResponse } from '@/types/type'; import { Msg } from './ui/ErrorMessage/Msg'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faGoogle, faGithub } from '@fortawesome/free-brands-svg-icons'; export function LoginForm({ className, submitAction, isPending, state, ...props }: React.ComponentProps<'div'> & { submitAction: (formData: FormData) => void; state: LoginActionResponse; isPending: boolean; }) { return ( <div className={cn('flex flex-col gap-6', className)} {...props}> <Card className='overflow-hidden p-0'> <CardContent className='grid p-0'> <form className='p-6 md:p-8' action={submitAction}> <div className='flex flex-col gap-6'> <div className='flex flex-col items-center text-center'> <h1 className='text-2xl font-bold'>Welcome back</h1> <p className='text-muted-foreground text-balance'> Login to your Acme Inc account </p> </div> <div className='grid gap-3'> <Label htmlFor='email'>Email</Label> <Input id='email' type='email' name='email' placeholder='email@gmail.com' defaultValue={state.inputs?.email} required /> <Msg msg={state.errors?.email?.join(', ')} isError={true} /> </div> <div className='grid gap-3'> <div className='flex items-center'> <Label htmlFor='password'>Password</Label> <a href='#' className='ml-auto text-sm underline-offset-2 hover:underline' > Forgot your password? </a> </div> <Input id='password' type='password' name='password' defaultValue={state.inputs?.password} required /> <Msg msg={state.errors?.password?.join(', ')} isError={true} /> </div> <Button type='submit' className='w-full' disabled={isPending}> {isPending ? 'Logging in...' : 'Login'} </Button> {state.success ? ( <Msg msg={state.message} /> ) : ( <Msg msg={state.message} isError={true} /> )} <div className='after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t'> <span className='bg-card text-muted-foreground relative z-10 px-2'> Or continue with </span> </div> <div className=''> <Button variant='outline' type='button' className='w-full' onClick={() => { window.location.href = 'http://localhost:5000/auth/google/callback'; }} > <FontAwesomeIcon icon={faGoogle} className='h-4 w-4' /> <span className='sr-only'>Login with Google</span> </Button> <Button variant='outline' type='button' className='w-full' onClick={() => { window.location.href = 'http://localhost:5000/auth/github/callback'; }} > <FontAwesomeIcon icon={faGithub} className='h-4 w-4' /> <span className='sr-only'>Login with Github</span> </Button> </div> <div className='text-center text-sm'> Don&apos;t have an account?{' '} <a href='#' className='underline underline-offset-4'> Sign up </a> </div> </div> </form> </CardContent> </Card> <div className='text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4'> By clicking continue, you agree to our <a href='#'>Terms of Service</a>{' '} and <a href='#'>Privacy Policy</a>. </div> </div> ); } `, layouts: { AuthLayout: { 'AuthLayout.tsx': `// AuthLayout component\n // AuthLayout component import { LoginForm } from '@/components/LoginForm'; import { useActionState } from 'react'; import type { LoginActionResponse } from '@/types/type'; import { UserLoginValidation } from '@/types/schema'; import { delay } from '@/lib/utils'; function AuthLayout() { const initialState = { success: false, message: '', }; const handleLogin = async (_: LoginActionResponse, formData: FormData) => { const rawData = { email: formData.get('email') as string, password: formData.get('password') as string, }; const validatedData = UserLoginValidation.safeParse(rawData); console.log('validatedData', validatedData); if (!validatedData.success) { return { success: false, message: 'Invalid email or password', errors: validatedData.error.flatten().fieldErrors, inputs: rawData, }; } await delay(1000); return { success: true, message: 'Login successful', }; }; const [state, submitAction, isPending] = useActionState( handleLogin, initialState ); console.log('state', state); console.log('submitAction', submitAction); console.log('isPending', isPending); return ( <div className='flex min-h-screen items-center justify-center'> <LoginForm isPending={isPending} submitAction={submitAction} state={state} /> </div> ); } export default AuthLayout;`, 'index.ts': 'export * from "./AuthLayout";\n', }, MainLayout: { 'MainLayout.tsx': '// MainLayout component\n', 'index.ts': 'export * from "./MainLayout";\n', }, }, }, features: { counter: { 'Counter.tsx': `// Counter component\n import React, { useState } from 'react' import { useAppSelector, useAppDispatch } from 'app/hooks' import { decrement, increment } from './counterSlice' export function Counter() { // The 'state' arg is correctly typed as 'RootState' already const count = useAppSelector((state) => state.counter.value) const dispatch = useAppDispatch() // omit rendering logic }`, 'CounterDisplay.tsx': '// CounterDisplay component\n', 'counterSlice.ts': '// Redux counter slice\n', }, products: { components: { ProductList: { 'ProductList.tsx': '// ProductList component\n', 'index.ts': 'export * from "./ProductList";\n', }, }, hooks: { 'useProducts.tsx': '// Custom products hook\n', 'useAuth.tsx': '// Custom auth hook\n', }, services: { 'productService.ts': '// Product service\n', }, types: { 'product.ts': '// Product types\n', }, }, users: { components: { UserProfile: { 'UserProfile.tsx': '// UserProfile component\n', 'index.ts': 'export * from "./UserProfile";\n', }, }, hooks: { 'useUser.tsx': '// Custom user hook\n', }, services: { 'userService.ts': '// User service\n', }, types: { 'user.ts': '// User types\n', }, }, }, hooks: { 'index.ts': '// Barrel file for hooks\n', 'useAuth.ts': '// Auth hook\n', 'useDebounce.ts': '// Debounce hook\n', }, lib: { validations: { 'fileValidations.ts': `import { z } from 'zod'; // Constants for file validation export const FILE_SIZE_LIMITS = { DEFAULT: 10 * 1024 * 1024, // 10MB IMAGE: 5 * 1024 * 1024, // 5MB DOCUMENT: 20 * 1024 * 1024, // 20MB VIDEO: 100 * 1024 * 1024, // 100MB } as const; export const ALLOWED_FILE_TYPES = { IMAGE: [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', ], DOCUMENT: [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/plain', 'text/csv', ], VIDEO: [ 'video/mp4', 'video/mpeg', 'video/quicktime', 'video/x-msvideo', 'video/webm', ], AUDIO: ['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/mp3', 'audio/webm'], } as const; // Custom error messages export const FILE_ERROR_MESSAGES = { REQUIRED: 'File is required', EMPTY: 'File cannot be empty', SIZE_EXCEEDED: (limit: number) => \`File size must not exceed \${formatFileSize(limit)}\`, INVALID_TYPE: (allowed: string[]) => \`Invalid file type. Allowed: \${allowed.join(', ')}\`, UPLOAD_FAILED: 'File upload failed', } as const; // Utility functions export function formatFileSize(bytes: number): string { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } export function getFileExtension(filename: string): string { return filename.slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2); } export function sanitizeFilename(filename: string): string { // Remove special characters and spaces return filename .replace(/[^a-zA-Z0-9.-]/g, '_') .replace(/_{2,}/g, '_') .toLowerCase(); } // Advanced file validation schema with custom refinements export const createFileValidationSchema = (options?: { maxSize?: number; allowedTypes?: string[]; required?: boolean; }) => { const { maxSize = FILE_SIZE_LIMITS.DEFAULT, allowedTypes = [], required = true, } = options || {}; const schema = z.instanceof(File, { message: FILE_ERROR_MESSAGES.REQUIRED }); if (!required) { return schema .optional() .refine(file => !file || file.size > 0, FILE_ERROR_MESSAGES.EMPTY) .refine( file => !file || file.size <= maxSize, FILE_ERROR_MESSAGES.SIZE_EXCEEDED(maxSize) ) .refine( file => !file || allowedTypes.length === 0 || allowedTypes.includes(file.type), FILE_ERROR_MESSAGES.INVALID_TYPE(allowedTypes) ); } return schema .refine(file => file.size > 0, FILE_ERROR_MESSAGES.EMPTY) .refine( file => !file || file.size <= maxSize, FILE_ERROR_MESSAGES.SIZE_EXCEEDED(maxSize) ) .refine( file => !file || allowedTypes.length === 0 || allowedTypes.includes(file.type), FILE_ERROR_MESSAGES.INVALID_TYPE(allowedTypes) ); }; // Example usage schemas export const profileImageSchema = createFileValidationSchema({ maxSize: FILE_SIZE_LIMITS.IMAGE, allowedTypes: Array.from(ALLOWED_FILE_TYPES.IMAGE), }); export const documentUploadSchema = createFileValidationSchema({ maxSize: FILE_SIZE_LIMITS.DOCUMENT, allowedTypes: Array.from(ALLOWED_FILE_TYPES.DOCUMENT), }); export const attachmentSchema = createFileValidationSchema({ maxSize: FILE_SIZE_LIMITS.DEFAULT, allowedTypes: [...ALLOWED_FILE_TYPES.IMAGE, ...ALLOWED_FILE_TYPES.DOCUMENT], }); `, 'validation.ts': '// Validation functions\n', }, 'utils.ts': `import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export function delay(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } `, }, pages: { 'HomePage.tsx': '// HomePage component\n', 'LoginPage.tsx': '// LoginPage component\n', 'ProductPage.tsx': '// ProductPage component\n', }, routes: { 'AppRoutes.tsx': '// App routes\n', 'ProtectedRoute.tsx': '// Protected route component\n', 'index.ts': '// Barrel file for routes\n', }, store: { 'hooks.ts': `// Redux hooks import { useDispatch, useSelector } from 'react-redux'; import type { RootState, AppDispatch } from './store'; // Use throughout your app instead of plain \`useDispatch\` and \`useSelector\` export const useAppDispatch = useDispatch.withTypes<AppDispatch>(); export const useAppSelector = useSelector.withTypes<RootState>(); `, 'store.ts': `// Redux store import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; export const store = configureStore({ reducer: { counter: counterReducer, }, }); // Infer the \`RootState\` and \`AppDispatch\` types from the store itself export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; `, slices: { 'authSlice.ts': '// Auth slice\n', 'productSlice.ts': '// Product slice\n', 'counterSlice.ts': `// Counter slice\n import { createSlice } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit' import type { RootState } from '../../app/store' // Define a type for the slice state interface CounterState { value: number } // Define the initial state using that type const initialState: CounterState = { value: 0, } export const counterSlice = createSlice({ name: 'counter', // 'createSlice' will infer the state type from the 'initialState' argument initialState, reducers: { increment: (state) => { state.value += 1 }, decrement: (state) => { state.value -= 1 }, // Use the PayloadAction type to declare the contents of 'action.payload' incrementByAmount: (state, action: PayloadAction<number>) => { state.value += action.payload }, }, }) export const { increment, decrement, incrementByAmount } = counterSlice.actions // Other code such as selectors can use the imported 'RootState' type export const selectCount = (state: RootState) => state.counter.value export default counterSlice.reducer`, }, }, styles: {}, tests: { components: {}, pages: {}, utils: {}, }, types: { 'pokemon.ts': '// Pokemon types\n', 'schema.ts': `// Schema types\n import { z } from 'zod'; // File validation schemas export const FileSchemaValidation = z.object({ id: z.string().uuid().optional(), // Optional for new uploads filename: z.string().min(1, 'Filename is required'), originalName: z.string().min(1, 'Original filename is required'), mimeType: z .string() .regex(/^[a-zA-Z0-9][a-zA-Z0-9/\-+.]*$/, 'Invalid MIME type format'), size: z .number() .int('File size must be an integer') .positive('File size must be positive') .max(10 * 1024 * 1024, 'File size must not exceed 10MB'), // 10MB limit url: z.string().url('Invalid URL format').optional(), uploadedAt: z.union([z.date(), z.string().datetime()]), uploadedBy: z.string().uuid('Invalid user ID format'), }); // Specific file type validations export const ImageFileValidation = FileSchemaValidation.extend({ mimeType: z.enum( [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', ], { errorMap: () => ({ message: 'File must be an image (JPEG, PNG, GIF, WebP, or SVG)', }), } ), size: z .number() .int() .positive() .max(5 * 1024 * 1024, 'Image size must not exceed 5MB'), }); export const DocumentFileValidation = FileSchemaValidation.extend({ mimeType: z.enum( [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/plain', 'text/csv', ], { errorMap: () => ({ message: 'File must be a document (PDF, Word, Excel, TXT, or CSV)', }), } ), }); // Email attachment validation export const EmailAttachmentValidation = z.object({ id: z.string().uuid().optional(), emailId: z.string().uuid(), file: FileSchemaValidation, }); // File upload validation (for form data) export const FileUploadValidation = z.object({ file: z .instanceof(File, { message: 'Please upload a file' }) .refine(file => file.size > 0, 'File is empty') .refine( file => file.size <= 10 * 1024 * 1024, 'File size must not exceed 10MB' ), }); // Multiple file upload validation export const MultipleFileUploadValidation = z.object({ files: z .array( z .instanceof(File) .refine(file => file.size > 0, 'File is empty') .refine( file => file.size <= 10 * 1024 * 1024, 'File size must not exceed 10MB' ) ) .min(1, 'At least one file is required') .max(5, 'Maximum 5 files allowed'), }); // User validation schemas export const UserLoginValidation = z.object({ email: z.string().min(1, 'Email is required').email('Invalid email format'), password: z .string() .min(6, 'Password must be at least 6 characters') .max(20, 'Password must be at most 20 characters'), }); export const UserValidation = z.object({ id: z.string().uuid(), email: z.string().min(1, 'Email is required').email('Invalid email format'), name: z.string().optional(), createdAt: z.union([z.date(), z.string().datetime()]), updatedAt: z.union([z.date(), z.string().datetime()]), }); // Email validation schema export const EmailValidation = z.object({ id: z.string().uuid(), from: z.string().email('Invalid sender email'), to: z .array(z.string().email('Invalid recipient email')) .min(1, 'At least one recipient is required'), cc: z.array(z.string().email('Invalid CC email')).optional(), bcc: z.array(z.string().email('Invalid BCC email')).optional(), subject: z.string().min(1, 'Subject is required'), body: z.string().min(1, 'Email body is required'), attachments: z.array(EmailAttachmentValidation).optional(), sentAt: z.union([z.date(), z.string().datetime()]).optional(), receivedAt: z.union([z.date(), z.string().datetime()]).optional(), status: z.enum(['draft', 'sent', 'received', 'failed']), userId: z.string().uuid(), }); // Helper function to validate file uploads export const validateFileUpload = (file: File, allowedTypes?: string[]) => { const validation = FileUploadValidation.safeParse({ file }); if (!validation.success) { return { success: false, error: validation.error.errors[0].message }; } if (allowedTypes && !allowedTypes.includes(file.type)) { return { success: false, error: \`File type not allowed. Allowed types: \${allowedTypes.join(', ')}\`, }; } return { success: true, data: file }; };`, 'type.ts': `import { z } from 'zod'; import { FileSchemaValidation, ImageFileValidation, DocumentFileValidation, EmailAttachmentValidation, FileUploadValidation, MultipleFileUploadValidation, UserLoginValidation, UserValidation, EmailValidation, } from '@/types/schema'; // User related types export type UserLogin = z.infer<typeof UserLoginValidation>; export type User = z.infer<typeof UserValidation>; // Infer TypeScript types from Zod schemas export type FileSchema = z.infer<typeof FileSchemaValidation>; export type ImageFile = z.infer<typeof ImageFileValidation>; export type DocumentFile = z.infer<typeof DocumentFileValidation>; export type EmailAttachment = z.infer<typeof EmailAttachmentValidation>; export type FileUpload = z.infer<typeof FileUploadValidation>; export type MultipleFileUpload = z.infer<typeof MultipleFileUploadValidation>; // File/Email related types export type Email = z.infer<typeof EmailValidation>; // Response types export type LoginActionResponse = { success: boolean; message: string; errors?: { [k in keyof UserLogin]?: string[]; }; inputs?: { [k in keyof UserLogin]?: string; }; }; // Re-export the validation helper function export { validateFileUpload } from '@/types/schema'; // Re-export all validation schemas for convenience export * from '@/types/schema'; `, }, }, ...docker_1.dockerStructure, };