modules/frontend/Write-Frontend-NextJs-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

$base = Join-Path $OutputPath "frontend"
function Write-File($rel, $rawContent) {
    Write-CoreFile (Join-Path $base $rel) $rawContent
}

# ─── COMMON COMPONENTS ────────────────────────────────────
Write-File "components\common\Button.tsx" @'
'use client';
import styles from './Button.module.css';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant?: 'primary' | 'danger' | 'ghost'; loading?: boolean; }
export function Button({ children, variant = 'primary', loading, disabled, className = '', ...props }: ButtonProps) {
  return (
    <button className={`${styles.btn} ${styles[variant]} ${className}`} 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:.92rem;font-weight:600;cursor:pointer;transition:background .18s,transform .1s,opacity .18s;white-space:nowrap;}
.btn:active{transform:scale(.97);} .btn:disabled{opacity:.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,.35);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;}
@keyframes spin{to{transform:rotate(360deg);}}
'@


Write-File "components\common\Input.tsx" @'
'use client';
import styles from './Input.module.css';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { label: string; error?: 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={`${styles.input} ${error ? styles.inputError : ''}`} {...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:.83rem;font-weight:600;color:#374151;}
.input{padding:11px 14px;border:1.5px solid #d1d5db;border-radius:8px;font-size:.93rem;outline:none;background:#fff;transition:border-color .18s,box-shadow .18s;}
.input:focus{border-color:#0f3460;box-shadow:0 0 0 3px rgba(15,52,96,.08);} .inputError{border-color:#ef4444;} .error{font-size:.78rem;color:#ef4444;}
'@


Write-File "components\common\Alert.tsx" @'
'use client';
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={`${styles.alert} ${styles[type]}`}>{message}</div>;
}
'@


Write-File "components\common\Alert.module.css" @'
.alert{padding:11px 16px;border-radius:8px;font-size:.88rem;font-weight:500;animation:fadeIn .2s ease;}
@keyframes fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
.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 ──────────────────────────────────────
Write-File "components\auth\LoginForm.tsx" @'
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
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 router = useRouter();
  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: React.FormEvent) => {
    e.preventDefault();
    setError(null); setLoading(true);
    try {
      const data = await authService.login({ username, password });
      login(data.token, data.username);
      router.push('/');
    } catch (err: any) {
      setError(err.message);
    } 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"

    # Form
    $formState = if ($e -eq "Product") { " const [price, setPrice] = useState('');" } else { "" }
    $formCall = if ($e -eq "Product") { "await $lowE`Service.add(name, parseFloat(price));" } else { "await $lowE`Service.add(name);" }
    $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 { "" }

    Write-File "components\$dirE\$e`Form.tsx" @"
'use client';
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 }: { 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: React.FormEvent) => {
    e.preventDefault();
    setError(null); setSuccess(null); setLoading(true);
    try {
      $formCall
      setSuccess('$($L.uiEntityAdded -f $e)');
      setName('');
      if ("$e" === "Product") setPrice('');
      onAdded();
    } catch (err: any) {
      setError(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>
  );
}
"@


    # Table
    $tableHead = if ($e -eq "Product") { "<th>$($L.uiPrice)</th>" } else { "" }
    $tableCell = if ($e -eq "Product") { "<td>{item.price} TL</td>" } else { "" }
    Write-File "components\$dirE\$e`Table.tsx" @"
import type { $e } from '@/types';
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>
  );
}
"@


    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:.9rem;}
.table{width:100%;border-collapse:collapse;font-size:.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;}
'@

}