modules/frontend/Write-Frontend-React-Components.ps1
|
param( [string]$ProjectName, [string]$OrmMode, [string]$OutputPath, [string]$FrontendMode, [string[]]$Entities, [string]$Language = 'tr' ) . (Join-Path (Split-Path $MyInvocation.MyCommand.Path) "..\core\Utils.ps1") . (Join-Path (Split-Path $MyInvocation.MyCommand.Path) "..\core\Get-Locale.ps1") $L = Get-Locale -Language $Language $isTS = ($FrontendMode -eq "ReactTS") $ext = if ($isTS) { "tsx" } else { "jsx" } $BT = "``" $base = Join-Path $OutputPath "frontend" function Write-File($rel, $rawContent) { $content = $rawContent -replace 'BTICK', $BT Write-CoreFile (Join-Path $base $rel) $content } # ─── COMMON COMPONENTS ──────────────────────────────────── if ($isTS) { Write-File "components\common\Button.$ext" @" import { type ButtonHTMLAttributes } from 'react'; import styles from './Button.module.css'; interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { variant?: string; loading?: boolean; } export function Button({ children, variant = 'primary', loading, disabled, className = '', ...props }: ButtonProps) { return ( <button className={BTICK`${styles.btn} `${styles[variant ?? 'primary']} `${className}BTICK} disabled={disabled || loading} {...props} > {loading && <span className={styles.spinner} />} {children} </button> ); } "@ } else { Write-File "components\common\Button.$ext" @" import styles from './Button.module.css'; export function Button({ children, variant = 'primary', loading, disabled, className = '', ...props }) { return ( <button className={BTICK`${styles.btn} `${styles[variant]} `${className}BTICK} disabled={disabled || loading} {...props} > {loading && <span className={styles.spinner} />} {children} </button> ); } "@ } Write-File "components\common\Button.module.css" @' .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 11px 24px; border: none; border-radius: 8px; font-size: 0.92rem; font-weight: 600; cursor: pointer; transition: background 0.18s, transform 0.1s, opacity 0.18s; white-space: nowrap; } .btn:active { transform: scale(0.97); } .btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; } .primary { background: #0f3460; color: #fff; } .primary:hover:not(:disabled) { background: #16213e; } .danger { background: #ef4444; color: #fff; } .danger:hover:not(:disabled) { background: #dc2626; } .ghost { background: transparent; color: #6b7280; border: 1.5px solid #d1d5db; } .ghost:hover:not(:disabled) { background: #f9fafb; } .spinner { width: 14px; height: 14px; border: 2px solid rgba(255,255,255,0.35); border-top-color: #fff; border-radius: 50%; animation: spin 0.7s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } '@ if ($isTS) { Write-File "components\common\Input.$ext" @" import { type InputHTMLAttributes } from 'react'; import styles from './Input.module.css'; interface InputProps extends InputHTMLAttributes<HTMLInputElement> { label: string; error?: string; id: string; } export function Input({ label, error, id, ...props }: InputProps) { return ( <div className={styles.group}> <label htmlFor={id} className={styles.label}>{label}</label> <input id={id} className={BTICK`${styles.input} `${error ? styles.inputError : ''}BTICK} {...props} /> {error && <span className={styles.error}>{error}</span>} </div> ); } "@ } else { Write-File "components\common\Input.$ext" @" import styles from './Input.module.css'; export function Input({ label, error, id, ...props }) { return ( <div className={styles.group}> <label htmlFor={id} className={styles.label}>{label}</label> <input id={id} className={BTICK`${styles.input} `${error ? styles.inputError : ''}BTICK} {...props} /> {error && <span className={styles.error}>{error}</span>} </div> ); } "@ } Write-File "components\common\Input.module.css" @' .group { display: flex; flex-direction: column; gap: 5px; } .label { font-size: 0.83rem; font-weight: 600; color: #374151; } .input { padding: 11px 14px; border: 1.5px solid #d1d5db; border-radius: 8px; font-size: 0.93rem; outline: none; background: #fff; transition: border-color 0.18s, box-shadow 0.18s; } .input:focus { border-color: #0f3460; box-shadow: 0 0 0 3px rgba(15,52,96,0.08); } .inputError { border-color: #ef4444; } .error { font-size: 0.78rem; color: #ef4444; } '@ if ($isTS) { Write-File "components\common\Alert.$ext" @" import styles from './Alert.module.css'; interface AlertProps { message: string | null; type?: 'error' | 'success' | 'info'; } export function Alert({ message, type = 'error' }: AlertProps) { if (!message) return null; return <div className={BTICK`${styles.alert} `${styles[type]}BTICK}>{message}</div>; } "@ } else { Write-File "components\common\Alert.$ext" @" import styles from './Alert.module.css'; export function Alert({ message, type = 'error' }) { if (!message) return null; return <div className={BTICK`${styles.alert} `${styles[type]}BTICK}>{message}</div>; } "@ } Write-File "components\common\Alert.module.css" @' .alert { padding: 11px 16px; border-radius: 8px; font-size: 0.88rem; font-weight: 500; animation: fadeIn 0.2s ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; } } .error { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; } .success { background: #f0fdf4; color: #16a34a; border: 1px solid #bbf7d0; } .info { background: #eff6ff; color: #2563eb; border: 1px solid #bfdbfe; } '@ # ─── AUTH COMPONENTS ────────────────────────────────────── if ($isTS) { Write-File "components\auth\LoginForm.$ext" @" import { useState, type FormEvent } from 'react'; import { useNavigate } from 'react-router-dom'; import { Input } from '../common/Input'; import { Button } from '../common/Button'; import { Alert } from '../common/Alert'; import { authService } from '../../services/authService'; import { useAuth } from '../../context/AuthContext'; import styles from './LoginForm.module.css'; export function LoginForm() { const navigate = useNavigate(); const { login } = useAuth(); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState<string | null>(null); const [loading, setLoading] = useState(false); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setError(null); setLoading(true); try { const data = await authService.login({ username, password }); login(data.token, data.username); navigate('/'); } catch (err) { setError(err instanceof Error ? err.message : '$($L.uiLoginFailed)'); } finally { setLoading(false); } }; return ( <form className={styles.form} onSubmit={handleSubmit}> <Alert message={error} type="error" /> <Input id="username" label="$($L.uiUsername)" value={username} onChange={e => setUsername(e.target.value)} placeholder="admin" required /> <Input id="password" label="$($L.uiPassword)" type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="********" required /> <Button type="submit" loading={loading} style={{ width: '100%', marginTop: 4 }}>$($L.uiLogin)</Button> </form> ); } "@ } else { Write-File "components\auth\LoginForm.$ext" @" import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Input } from '../common/Input'; import { Button } from '../common/Button'; import { Alert } from '../common/Alert'; import { authService } from '../../services/authService'; import { useAuth } from '../../context/AuthContext'; import styles from './LoginForm.module.css'; export function LoginForm() { const navigate = useNavigate(); const { login } = useAuth(); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); setError(null); setLoading(true); try { const data = await authService.login({ username, password }); login(data.token, data.username); navigate('/'); } catch (err) { setError(err instanceof Error ? err.message : '$($L.uiLoginFailed)'); } finally { setLoading(false); } }; return ( <form className={styles.form} onSubmit={handleSubmit}> <Alert message={error} type="error" /> <Input id="username" label="$($L.uiUsername)" value={username} onChange={e => setUsername(e.target.value)} placeholder="admin" required /> <Input id="password" label="$($L.uiPassword)" type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="********" required /> <Button type="submit" loading={loading} style={{ width: '100%', marginTop: 4 }}>$($L.uiLogin)</Button> </form> ); } "@ } Write-File "components\auth\LoginForm.module.css" @' .form { display: flex; flex-direction: column; gap: 18px; } '@ # ─── ENTITY COMPONENTS ──────────────────────────────────── foreach ($e in $Entities) { if ($e -eq "User") { continue } $lowE = $e.ToLower() $dirE = $lowE + "s" $formState = if ($e -eq "Product") { " const [price, setPrice] = useState('');" } else { "" } $priceReset = if ($e -eq "Product") { "`n setPrice('');" } else { "" } $formField = if ($e -eq "Product") { "<Input id=`"eprice`" label=`"$($L.uiPrice)`" type=`"number`" value={price} onChange={e => setPrice(e.target.value)} placeholder=`"0.00`" required />" } else { "" } $tableHead = if ($e -eq "Product") { "<th>$($L.uiPrice)</th>" } else { "" } $tableCell = if ($e -eq "Product") { "<td>{item.price} TL</td>" } else { "" } if ($isTS) { $formCallTS = if ($e -eq "Product") { "await ${lowE}Service.add(name, parseFloat(price));" } else { "await ${lowE}Service.add(name);" } $typeImport = "import type { $e } from '../../types';" Write-File "components\$dirE\$e`Form.$ext" @" import { useState, type FormEvent } from 'react'; import { Input } from '../common/Input'; import { Button } from '../common/Button'; import { Alert } from '../common/Alert'; import { ${lowE}Service } from '../../services/${lowE}Service'; import styles from './$e`Form.module.css'; export function ${e}Form({ onAdded = () => {} }: { onAdded?: () => void }) { const [name, setName] = useState(''); $formState const [error, setError] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null); const [loading, setLoading] = useState(false); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setError(null); setSuccess(null); setLoading(true); try { $formCallTS setSuccess('$($L.uiEntityAdded -f $e)'); setName('');$priceReset onAdded(); } catch (err) { setError(err instanceof Error ? err.message : '$($L.uiAddFailed)'); } finally { setLoading(false); } }; return ( <form className={styles.form} onSubmit={handleSubmit}> <Alert message={error} type="error" /> <Alert message={success} type="success" /> <div className={styles.row}> <Input id="ename" label="$($L.uiEntityName -f $e)" value={name} onChange={e => setName(e.target.value)} placeholder="$e" required /> $formField <div className={styles.btnWrap}> <Button type="submit" loading={loading}>$($L.uiAdd)</Button> </div> </div> </form> ); } "@ Write-File "components\$dirE\$e`Table.$ext" @" $typeImport import styles from './${e}Table.module.css'; export function ${e}Table({ items, loading }: { items: $e[]; loading: boolean }) { if (loading) return <div className={styles.state}>$($L.uiLoading)</div>; if (!items.length) return <div className={styles.state}>$($L.uiNoData)</div>; return ( <div className={styles.wrapper}> <table className={styles.table}> <thead><tr><th>#</th><th>$($L.uiEntityName -f $e)</th>$tableHead<th>$($L.uiCreatedAt)</th></tr></thead> <tbody> {items.map((item, i) => ( <tr key={item.id}> <td>{i + 1}</td> <td>{item.name}</td> $tableCell <td>{new Date(item.createdAt).toLocaleDateString('$($L.uiDateLocale)')}</td> </tr> ))} </tbody> </table> </div> ); } "@ } else { $formCallJS = if ($e -eq "Product") { "await ${lowE}Service.add(name, parseFloat(price));" } else { "await ${lowE}Service.add(name);" } Write-File "components\$dirE\$e`Form.$ext" @" import { useState } from 'react'; import { Input } from '../common/Input'; import { Button } from '../common/Button'; import { Alert } from '../common/Alert'; import { ${lowE}Service } from '../../services/${lowE}Service'; import styles from './$e`Form.module.css'; export function ${e}Form({ onAdded }) { const [name, setName] = useState(''); $formState const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); setError(null); setSuccess(null); setLoading(true); try { $formCallJS setSuccess('$($L.uiEntityAdded -f $e)'); setName('');$priceReset onAdded(); } catch (err) { setError(err instanceof Error ? err.message : '$($L.uiAddFailed)'); } finally { setLoading(false); } }; return ( <form className={styles.form} onSubmit={handleSubmit}> <Alert message={error} type="error" /> <Alert message={success} type="success" /> <div className={styles.row}> <Input id="ename" label="$($L.uiEntityName -f $e)" value={name} onChange={e => setName(e.target.value)} placeholder="$e" required /> $formField <div className={styles.btnWrap}> <Button type="submit" loading={loading}>$($L.uiAdd)</Button> </div> </div> </form> ); } "@ Write-File "components\$dirE\$e`Table.$ext" @" import styles from './${e}Table.module.css'; export function ${e}Table({ items, loading }) { if (loading) return <div className={styles.state}>$($L.uiLoading)</div>; if (!items.length) return <div className={styles.state}>$($L.uiNoData)</div>; return ( <div className={styles.wrapper}> <table className={styles.table}> <thead><tr><th>#</th><th>$($L.uiEntityName -f $e)</th>$tableHead<th>$($L.uiCreatedAt)</th></tr></thead> <tbody> {items.map((item, i) => ( <tr key={item.id}> <td>{i + 1}</td> <td>{item.name}</td> $tableCell <td>{new Date(item.createdAt).toLocaleDateString('$($L.uiDateLocale)')}</td> </tr> ))} </tbody> </table> </div> ); } "@ } Write-File "components\$dirE\$e`Form.module.css" @' .form { display: flex; flex-direction: column; gap: 14px; } .row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; } .row > div:first-child { flex: 2; min-width: 160px; } .btnWrap { padding-bottom: 1px; } '@ Write-File "components\$dirE\$e`Table.module.css" @' .wrapper { overflow-x: auto; } .state { text-align: center; color: #9ca3af; padding: 32px 0; font-size: 0.9rem; } .table { width: 100%; border-collapse: collapse; font-size: 0.9rem; } .table thead tr { background: #f8fafc; } .table th { padding: 11px 16px; text-align: left; font-weight: 600; color: #6b7280; border-bottom: 2px solid #e5e7eb; white-space: nowrap; } .table td { padding: 11px 16px; border-bottom: 1px solid #f3f4f6; } .table tbody tr:hover { background: #f9fafb; } '@ } # ─── BARREL INDEX EXPORTS per entity folder ─────────────── $extTs = if ($isTS) { "ts" } else { "js" } foreach ($e in $Entities) { if ($e -eq "User") { continue } $dirE = $e.ToLower() + "s" Write-File "components\$dirE\index.$extTs" @" export { ${e}Form } from './${e}Form'; export { ${e}Table } from './${e}Table'; "@ } |