y3-app
Version:
CLI to add your project structure
716 lines (664 loc) • 23.3 kB
JavaScript
"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'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,
};