start-vibing-stacks 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/detector.js +23 -11
- package/dist/index.js +78 -5
- package/dist/scanner.d.ts +12 -0
- package/dist/scanner.js +501 -0
- package/dist/setup.js +46 -1
- package/dist/types.d.ts +24 -0
- package/dist/ui.js +6 -5
- package/package.json +1 -1
- package/stacks/_shared/config/security-rules.json +27 -5
- package/stacks/_shared/hooks/user-prompt-submit.ts +26 -2
- package/stacks/frontend/react/skills/preline-ui/SKILL.md +31 -35
- package/stacks/frontend/react/skills/react-standards/SKILL.md +20 -20
- package/stacks/frontend/react/skills/react-ui-patterns/SKILL.md +78 -42
- package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +1 -1
- package/stacks/frontend/react/skills/zod-validation/SKILL.md +84 -18
- package/stacks/frontend/react-inertia/skills/inertia-react/SKILL.md +342 -0
- package/stacks/frontend/react-inertia/skills/react-standards/SKILL.md +267 -0
- package/stacks/nodejs/skills/nextjs-app-router/SKILL.md +101 -0
- package/stacks/nodejs/stack.json +43 -121
- package/stacks/php/skills/laravel-octane/SKILL.md +155 -53
- package/stacks/php/skills/laravel-patterns/SKILL.md +244 -39
- package/stacks/php/skills/php-patterns/SKILL.md +113 -53
- package/stacks/php/skills/security-scan-php/SKILL.md +161 -43
- package/stacks/php/stack.json +19 -6
- package/templates/CLAUDE-nodejs.md +323 -0
- package/templates/CLAUDE-php.md +233 -33
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Inertia.js + React Integration
|
|
2
|
+
|
|
3
|
+
## Architecture Overview
|
|
4
|
+
|
|
5
|
+
Inertia.js acts as a bridge between Laravel (backend) and React (frontend). There is NO separate API layer — controllers return Inertia responses that render React page components with server-side data as props.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Request Flow:
|
|
9
|
+
Browser → Laravel Router → Controller → Inertia::render('Page', $props)
|
|
10
|
+
↓
|
|
11
|
+
HandleInertiaRequests (middleware)
|
|
12
|
+
↓
|
|
13
|
+
InertiaShare (shared props: auth, translations, menu)
|
|
14
|
+
↓
|
|
15
|
+
React Page Component (receives all props)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Backend: Middleware
|
|
19
|
+
|
|
20
|
+
### HandleInertiaRequests
|
|
21
|
+
|
|
22
|
+
The Inertia middleware merges shared props (auth, translations, flash messages) into every response:
|
|
23
|
+
|
|
24
|
+
```php
|
|
25
|
+
namespace App\Http\Middleware;
|
|
26
|
+
|
|
27
|
+
use Inertia\Middleware;
|
|
28
|
+
use App\Support\InertiaShare;
|
|
29
|
+
|
|
30
|
+
class HandleInertiaRequests extends Middleware
|
|
31
|
+
{
|
|
32
|
+
protected $rootView = 'app';
|
|
33
|
+
|
|
34
|
+
public function version(Request $request): ?string
|
|
35
|
+
{
|
|
36
|
+
return parent::version($request);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public function share(Request $request): array
|
|
40
|
+
{
|
|
41
|
+
return array_merge(
|
|
42
|
+
parent::share($request),
|
|
43
|
+
InertiaShare::getProps($request),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Rules:**
|
|
50
|
+
- Keep `share()` lean — delegate to `InertiaShare` helper
|
|
51
|
+
- `parent::share()` provides validation errors automatically
|
|
52
|
+
- `version()` triggers full page reload on asset changes
|
|
53
|
+
|
|
54
|
+
### InertiaShare Support Class
|
|
55
|
+
|
|
56
|
+
Centralize all shared props in `App\Support\InertiaShare`:
|
|
57
|
+
|
|
58
|
+
```php
|
|
59
|
+
namespace App\Support;
|
|
60
|
+
|
|
61
|
+
class InertiaShare
|
|
62
|
+
{
|
|
63
|
+
public static function getProps(Request $request): array
|
|
64
|
+
{
|
|
65
|
+
$locale = App::getLocale();
|
|
66
|
+
|
|
67
|
+
return [
|
|
68
|
+
'auth' => [
|
|
69
|
+
'user' => $request->user() ? [
|
|
70
|
+
'id' => $request->user()->id,
|
|
71
|
+
'name' => $request->user()->name,
|
|
72
|
+
'role' => $request->user()->role,
|
|
73
|
+
'email' => $request->user()->email,
|
|
74
|
+
'timezone' => $request->user()->timezone,
|
|
75
|
+
] : null,
|
|
76
|
+
],
|
|
77
|
+
'locale' => $locale,
|
|
78
|
+
'translations' => static::getTranslations(
|
|
79
|
+
$locale,
|
|
80
|
+
$request->route()->uri,
|
|
81
|
+
),
|
|
82
|
+
'flash' => $request->hasSession() ? [
|
|
83
|
+
'success' => Session::get('success'),
|
|
84
|
+
'error' => Session::get('error'),
|
|
85
|
+
] : [],
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Rules:**
|
|
92
|
+
- Auth data: expose only necessary fields (never passwords, tokens)
|
|
93
|
+
- Translations loaded on-demand per page (not all at once)
|
|
94
|
+
- Flash messages via Laravel session
|
|
95
|
+
- Menu structure loaded from DB, filtered by permissions, cached per user
|
|
96
|
+
|
|
97
|
+
## Backend: Controllers with Inertia
|
|
98
|
+
|
|
99
|
+
Controllers render React page components via `Inertia::render()`:
|
|
100
|
+
|
|
101
|
+
```php
|
|
102
|
+
use Inertia\Inertia;
|
|
103
|
+
use Inertia\Response as InertiaResponse;
|
|
104
|
+
|
|
105
|
+
class DashboardController extends Controller
|
|
106
|
+
{
|
|
107
|
+
public function __construct(
|
|
108
|
+
private readonly DashboardService $service,
|
|
109
|
+
) {}
|
|
110
|
+
|
|
111
|
+
public function index(Request $request): InertiaResponse
|
|
112
|
+
{
|
|
113
|
+
return Inertia::render('Dashboard/Index', [
|
|
114
|
+
'stats' => $this->service->getStats($request->user()),
|
|
115
|
+
'recentOrders' => fn () => OrderResource::collection(
|
|
116
|
+
$request->user()->orders()->latest()->limit(10)->get()
|
|
117
|
+
),
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public function show(Order $order): InertiaResponse
|
|
122
|
+
{
|
|
123
|
+
return Inertia::render('Orders/Show', [
|
|
124
|
+
'order' => OrderResource::make($order->load('items')),
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Rules:**
|
|
131
|
+
- Return type: `Inertia\Response` (not `JsonResponse`)
|
|
132
|
+
- Page component path maps to: `resources/js/Pages/{path}`
|
|
133
|
+
- Use lazy props with `fn ()` for data not needed on first render
|
|
134
|
+
- Use API Resources to format complex data before passing as props
|
|
135
|
+
- Keep controller thin — delegate to services
|
|
136
|
+
|
|
137
|
+
### Inertia Redirects
|
|
138
|
+
|
|
139
|
+
```php
|
|
140
|
+
// After mutations, redirect (Inertia handles SPA navigation)
|
|
141
|
+
public function store(StoreOrderRequest $request): RedirectResponse
|
|
142
|
+
{
|
|
143
|
+
$order = $this->service->create($request->validated());
|
|
144
|
+
|
|
145
|
+
return redirect()
|
|
146
|
+
->route('orders.show', $order)
|
|
147
|
+
->with('success', __('orders.created'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public function destroy(Order $order): RedirectResponse
|
|
151
|
+
{
|
|
152
|
+
$this->service->delete($order);
|
|
153
|
+
|
|
154
|
+
return redirect()
|
|
155
|
+
->route('orders.index')
|
|
156
|
+
->with('success', __('orders.deleted'));
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Rule:** POST/PUT/DELETE actions return `redirect()` with flash messages, never `Inertia::render()`.
|
|
161
|
+
|
|
162
|
+
## Backend: Translations On-Demand
|
|
163
|
+
|
|
164
|
+
Translations are loaded per-page via a config file that maps routes to translation files:
|
|
165
|
+
|
|
166
|
+
```php
|
|
167
|
+
// config/translations_inertia.php
|
|
168
|
+
return [
|
|
169
|
+
'global' => ['common', 'errors', 'validation'],
|
|
170
|
+
|
|
171
|
+
'pages' => [
|
|
172
|
+
'dashboard' => ['dashboard'],
|
|
173
|
+
'orders/*' => ['orders', 'products'],
|
|
174
|
+
'settings/*' => ['settings'],
|
|
175
|
+
],
|
|
176
|
+
];
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The `InertiaShare::getTranslations()` method:
|
|
180
|
+
1. Loads global translation files (always sent)
|
|
181
|
+
2. Loads page-specific files based on route URI
|
|
182
|
+
3. Merges PHP files (`lang/{locale}/*.php`) + JSON file (`lang/{locale}.json`)
|
|
183
|
+
4. Caches result per locale + page combination
|
|
184
|
+
|
|
185
|
+
**Rules:**
|
|
186
|
+
- Global files: `common`, `errors`, `validation` (always loaded)
|
|
187
|
+
- Page files: only load what the page needs
|
|
188
|
+
- Cache invalidation: clear on deploy (`php artisan cache:clear`)
|
|
189
|
+
- Store translations in `lang/en/*.php` and `lang/pt/*.php`
|
|
190
|
+
|
|
191
|
+
## Frontend: Translation Helper
|
|
192
|
+
|
|
193
|
+
The `__()` function resolves translation keys from Inertia shared props:
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
// resources/js/Utils/translate.js
|
|
197
|
+
import { usePage } from '@inertiajs/react';
|
|
198
|
+
|
|
199
|
+
export default function __(key, replacements = {}, pageProps = null) {
|
|
200
|
+
const propsSource = pageProps ? { props: pageProps } : usePage();
|
|
201
|
+
const translations = propsSource.props.translations || {};
|
|
202
|
+
|
|
203
|
+
let translation = key.split('.').reduce((obj, part) => {
|
|
204
|
+
return obj && typeof obj[part] !== 'undefined' ? obj[part] : null;
|
|
205
|
+
}, translations);
|
|
206
|
+
|
|
207
|
+
if (translation === null) {
|
|
208
|
+
return key;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (typeof translation === 'string' && Object.keys(replacements).length > 0) {
|
|
212
|
+
Object.keys(replacements).forEach((placeholder) => {
|
|
213
|
+
translation = translation.replace(
|
|
214
|
+
new RegExp(`:${placeholder}`, 'g'),
|
|
215
|
+
replacements[placeholder],
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return translation;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Usage in React Components
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
import __ from '@/Utils/translate';
|
|
228
|
+
|
|
229
|
+
// CORRECT: Define translations as CONST before hooks
|
|
230
|
+
const LABELS = {
|
|
231
|
+
title: __('dashboard.title'),
|
|
232
|
+
welcome: __('dashboard.welcome', { name: 'User' }),
|
|
233
|
+
save: __('common.save'),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export default function Dashboard({ stats }) {
|
|
237
|
+
const [loading, setLoading] = useState(false);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div>
|
|
241
|
+
<h1>{LABELS.title}</h1>
|
|
242
|
+
<p>{LABELS.welcome}</p>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// WRONG: Calling __() inside JSX (React Hook violation)
|
|
248
|
+
return <h1>{__('dashboard.title')}</h1>; // NEVER
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Rules:**
|
|
252
|
+
- ALWAYS define translations as `CONST` at the top of the component, BEFORE hooks
|
|
253
|
+
- NEVER call `__()` inside JSX or render methods
|
|
254
|
+
- New strings must be added to both `lang/en/*.php` and `lang/pt/*.php`
|
|
255
|
+
- Error strings centralized in `lang/*/errors.php`
|
|
256
|
+
- Use replacements for dynamic values: `__('greeting', { name: userName })`
|
|
257
|
+
|
|
258
|
+
## Frontend: Page Component Structure
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
resources/js/
|
|
262
|
+
├── Pages/
|
|
263
|
+
│ ├── Dashboard/
|
|
264
|
+
│ │ └── Index.jsx
|
|
265
|
+
│ ├── Orders/
|
|
266
|
+
│ │ ├── Index.jsx
|
|
267
|
+
│ │ ├── Show.jsx
|
|
268
|
+
│ │ └── _components/ # Page-specific components
|
|
269
|
+
│ │ ├── OrderTable.jsx
|
|
270
|
+
│ │ └── OrderFilters.jsx
|
|
271
|
+
│ ├── Auth/
|
|
272
|
+
│ │ ├── Login.jsx
|
|
273
|
+
│ │ └── Register.jsx
|
|
274
|
+
│ └── Users/
|
|
275
|
+
│ └── Admin/
|
|
276
|
+
│ └── Dashboard.jsx
|
|
277
|
+
├── Components/
|
|
278
|
+
│ ├── UI/ # Reusable UI primitives
|
|
279
|
+
│ ├── Layout/ # Header, Sidebar, Footer
|
|
280
|
+
│ └── Shared/ # Cross-feature components
|
|
281
|
+
├── Icons/
|
|
282
|
+
│ ├── index.js # Barrel export
|
|
283
|
+
│ ├── CheckIcon.svg
|
|
284
|
+
│ └── AlertIcon.svg
|
|
285
|
+
├── Layouts/
|
|
286
|
+
│ ├── AuthenticatedLayout.jsx
|
|
287
|
+
│ └── GuestLayout.jsx
|
|
288
|
+
└── Utils/
|
|
289
|
+
└── translate.js # __() helper
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Rules:**
|
|
293
|
+
- Pages map 1:1 to `Inertia::render('Path/Component')`
|
|
294
|
+
- Page-specific components in `_components/` folder
|
|
295
|
+
- Shared components in `Components/`
|
|
296
|
+
- Icons as separate `.svg` files, imported with `?react` suffix
|
|
297
|
+
|
|
298
|
+
## Frontend: Inertia Hooks
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import { usePage, useForm, router, Link } from '@inertiajs/react';
|
|
302
|
+
|
|
303
|
+
// Access shared props
|
|
304
|
+
const { auth, flash, locale } = usePage().props;
|
|
305
|
+
|
|
306
|
+
// Form handling with Inertia
|
|
307
|
+
const { data, setData, post, processing, errors } = useForm({
|
|
308
|
+
name: '',
|
|
309
|
+
email: '',
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const handleSubmit = (e) => {
|
|
313
|
+
e.preventDefault();
|
|
314
|
+
post(route('users.store'));
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Programmatic navigation
|
|
318
|
+
router.visit(route('dashboard'));
|
|
319
|
+
router.reload({ only: ['stats'] }); // Partial reload
|
|
320
|
+
|
|
321
|
+
// Links (SPA navigation, no full page reload)
|
|
322
|
+
<Link href={route('orders.index')}>Orders</Link>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Rules:**
|
|
326
|
+
- Use `useForm` for all form submissions (handles CSRF, errors, loading state)
|
|
327
|
+
- Use `router.reload({ only: [...] })` for partial page updates
|
|
328
|
+
- Use `<Link>` instead of `<a>` for SPA navigation
|
|
329
|
+
- Access shared props via `usePage().props`
|
|
330
|
+
- `processing` boolean from `useForm` for button loading states
|
|
331
|
+
|
|
332
|
+
## Forbidden Patterns
|
|
333
|
+
|
|
334
|
+
| Pattern | Reason | Use Instead |
|
|
335
|
+
|---------|--------|-------------|
|
|
336
|
+
| `fetch()` / `axios` for page data | Bypasses Inertia | `Inertia::render()` with props |
|
|
337
|
+
| `__()` inside JSX | React Hook violation | CONST at top of component |
|
|
338
|
+
| Inline SVGs in JSX | Bloats components | SVG files with `?react` import |
|
|
339
|
+
| `<a href>` for internal links | Full page reload | `<Link href>` |
|
|
340
|
+
| `window.location` for navigation | Full page reload | `router.visit()` |
|
|
341
|
+
| `Inertia::render()` after POST | Breaks Inertia protocol | `redirect()->route()` |
|
|
342
|
+
| Loading all translations globally | Performance waste | On-demand per page route |
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# React 19+ Standards (with Inertia.js)
|
|
2
|
+
|
|
3
|
+
## Version Requirements
|
|
4
|
+
|
|
5
|
+
- **ReactJS >= 19** — MANDATORY
|
|
6
|
+
- **TailwindCSS >= 4** — MANDATORY
|
|
7
|
+
- **Inertia.js >= 2** — MANDATORY
|
|
8
|
+
|
|
9
|
+
## Translation Pattern (via Inertia shared props)
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import __ from '@/Utils/translate';
|
|
13
|
+
|
|
14
|
+
// CORRECT: Translations as CONST at the top, BEFORE hooks
|
|
15
|
+
const LABELS = {
|
|
16
|
+
title: __('dashboard.title'),
|
|
17
|
+
save: __('common.save'),
|
|
18
|
+
cancel: __('common.cancel'),
|
|
19
|
+
errorRequired: __('errors.field_required'),
|
|
20
|
+
welcome: __('dashboard.welcome', { name: 'User' }),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default function Dashboard() {
|
|
24
|
+
const [data, setData] = useState(null);
|
|
25
|
+
|
|
26
|
+
return <h1>{LABELS.title}</h1>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// WRONG: __() inside JSX (Hook violation — usePage() is called internally)
|
|
30
|
+
return <h1>{__('dashboard.title')}</h1>; // NEVER
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Rules:**
|
|
34
|
+
- Translations in `CONST` variables before state hooks
|
|
35
|
+
- New strings must be added to `lang/en/*.php` AND `lang/pt/*.php`
|
|
36
|
+
- Error strings centralized in `lang/*/errors.php`
|
|
37
|
+
- Use replacements for dynamic values: `__('key', { name: value })`
|
|
38
|
+
|
|
39
|
+
## Debug Logging
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
const ENABLE_DASHBOARD_DEBUG = false;
|
|
43
|
+
|
|
44
|
+
const debugLog = (...args: unknown[]) => {
|
|
45
|
+
if (ENABLE_DASHBOARD_DEBUG) console.log('[Dashboard]', ...args);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default function Dashboard() {
|
|
49
|
+
debugLog('Rendering with data:', data);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Rule:** Never leave raw `console.log`. Always use controlled debug pattern.
|
|
54
|
+
|
|
55
|
+
## TailwindCSS Class Organization
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// CORRECT: Classes as CONST — clean JSX
|
|
59
|
+
const STYLES = {
|
|
60
|
+
container: 'flex flex-col gap-4 p-6 bg-white rounded-lg shadow-sm',
|
|
61
|
+
title: 'text-2xl font-bold text-gray-900',
|
|
62
|
+
button: 'px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition',
|
|
63
|
+
grid: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default function Dashboard() {
|
|
67
|
+
return (
|
|
68
|
+
<div className={STYLES.container}>
|
|
69
|
+
<h1 className={STYLES.title}>{LABELS.title}</h1>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// WRONG: Inline class soup
|
|
75
|
+
<div className="flex flex-col gap-4 p-6 bg-white rounded-lg shadow-sm"> // NEVER
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## SVG Icons
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// CORRECT: Separate files, import with ?react
|
|
82
|
+
import CheckIcon from '@/Icons/CheckIcon.svg?react';
|
|
83
|
+
import { CheckIcon, AlertIcon } from '@/Icons';
|
|
84
|
+
|
|
85
|
+
// WRONG: Inline SVG (bloats JSX)
|
|
86
|
+
<svg viewBox="0 0 24 24">...</svg> // NEVER
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Structure:**
|
|
90
|
+
```
|
|
91
|
+
resources/js/Icons/
|
|
92
|
+
├── index.js # Barrel export
|
|
93
|
+
├── CheckIcon.svg
|
|
94
|
+
├── AlertIcon.svg
|
|
95
|
+
└── SpinnerIcon.svg
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Inertia.js Hooks & Navigation
|
|
99
|
+
|
|
100
|
+
### Accessing Shared Props
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { usePage } from '@inertiajs/react';
|
|
104
|
+
|
|
105
|
+
export default function Header() {
|
|
106
|
+
const { auth, locale, flash } = usePage().props;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<nav>
|
|
110
|
+
<span>{auth.user?.name}</span>
|
|
111
|
+
{flash.success && <Alert>{flash.success}</Alert>}
|
|
112
|
+
</nav>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Forms with useForm
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { useForm } from '@inertiajs/react';
|
|
121
|
+
|
|
122
|
+
export default function CreateOrder() {
|
|
123
|
+
const { data, setData, post, processing, errors } = useForm({
|
|
124
|
+
product_id: '',
|
|
125
|
+
quantity: 1,
|
|
126
|
+
notes: '',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const handleSubmit = (e) => {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
post(route('orders.store'));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<form onSubmit={handleSubmit}>
|
|
136
|
+
<Input
|
|
137
|
+
value={data.product_id}
|
|
138
|
+
onChange={(e) => setData('product_id', e.target.value)}
|
|
139
|
+
error={errors.product_id}
|
|
140
|
+
/>
|
|
141
|
+
<Button type="submit" loading={processing}>
|
|
142
|
+
{processing ? <LoadingSpinner /> : LABELS.save}
|
|
143
|
+
</Button>
|
|
144
|
+
</form>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Rules:**
|
|
150
|
+
- Use `useForm` for ALL form submissions (handles CSRF, errors, loading)
|
|
151
|
+
- `processing` boolean for button loading states
|
|
152
|
+
- `errors` object maps to Form Request validation errors
|
|
153
|
+
- Never use `fetch()` or `axios` for form submissions
|
|
154
|
+
|
|
155
|
+
### Navigation
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { Link, router } from '@inertiajs/react';
|
|
159
|
+
|
|
160
|
+
// SPA links (no full page reload)
|
|
161
|
+
<Link href={route('orders.index')}>Orders</Link>
|
|
162
|
+
|
|
163
|
+
// Programmatic navigation
|
|
164
|
+
router.visit(route('dashboard'));
|
|
165
|
+
|
|
166
|
+
// Partial reload (only refresh specific props)
|
|
167
|
+
router.reload({ only: ['stats', 'recentOrders'] });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Loading States
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
// CORRECT: Always show loading feedback
|
|
174
|
+
export default function DataTable() {
|
|
175
|
+
const [loading, setLoading] = useState(true);
|
|
176
|
+
|
|
177
|
+
if (loading) {
|
|
178
|
+
return <SectionLoader />;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return <Table data={data} />;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Button loading (from useForm)
|
|
185
|
+
<Button onClick={handleSave} disabled={processing}>
|
|
186
|
+
{processing ? <LoadingSpinner /> : LABELS.save}
|
|
187
|
+
</Button>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Rule:** Every data-heavy section needs a loading state.
|
|
191
|
+
|
|
192
|
+
## Modal Data Flow
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
interface EditModalProps {
|
|
196
|
+
item: Item;
|
|
197
|
+
isOpen: boolean;
|
|
198
|
+
onClose: () => void;
|
|
199
|
+
onUpdated: () => void;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function EditModal({ item, isOpen, onClose, onUpdated }: EditModalProps) {
|
|
203
|
+
const { data, setData, put, processing } = useForm({
|
|
204
|
+
name: item.name,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const handleSave = (e) => {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
put(route('items.update', item.id), {
|
|
210
|
+
onSuccess: () => {
|
|
211
|
+
onUpdated();
|
|
212
|
+
onClose();
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Parent: use router.reload for refresh after modal mutation
|
|
219
|
+
<EditModal
|
|
220
|
+
item={selectedItem}
|
|
221
|
+
isOpen={showModal}
|
|
222
|
+
onClose={() => setShowModal(false)}
|
|
223
|
+
onUpdated={() => router.reload({ only: ['items'] })}
|
|
224
|
+
/>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Third-Party Libraries (Charts)
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
// CORRECT: Let React handle re-rendering
|
|
231
|
+
{chartData && (
|
|
232
|
+
<ApexChart
|
|
233
|
+
key={JSON.stringify(chartData)}
|
|
234
|
+
options={chartOptions}
|
|
235
|
+
series={chartData}
|
|
236
|
+
type="area"
|
|
237
|
+
/>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
// WRONG: Manual DOM manipulation
|
|
241
|
+
const chartRef = useRef(null);
|
|
242
|
+
chartRef.current.updateSeries(newData); // NEVER
|
|
243
|
+
|
|
244
|
+
// Memoize expensive computations
|
|
245
|
+
const processedData = useMemo(() => {
|
|
246
|
+
return heavyTransform(rawData);
|
|
247
|
+
}, [rawData]);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Rules:**
|
|
251
|
+
- No `useRef` for updating third-party components
|
|
252
|
+
- Conditional rendering: `data && <Component />`
|
|
253
|
+
- `useMemo` for expensive computations
|
|
254
|
+
- Loading states before rendering charts
|
|
255
|
+
|
|
256
|
+
## Forbidden Patterns
|
|
257
|
+
|
|
258
|
+
| Pattern | Reason | Use Instead |
|
|
259
|
+
|---------|--------|-------------|
|
|
260
|
+
| `fetch()` / `axios` for pages | Bypasses Inertia | `Inertia::render()` props |
|
|
261
|
+
| `__()` inside JSX | Hook violation | CONST at top |
|
|
262
|
+
| Inline SVGs | Bloats components | SVG files + `?react` |
|
|
263
|
+
| `<a href>` for internal links | Full reload | `<Link href>` |
|
|
264
|
+
| `window.location` | Full reload | `router.visit()` |
|
|
265
|
+
| Raw `console.log` | Uncontrolled | Debug constant pattern |
|
|
266
|
+
| Inline Tailwind soup | Unreadable | STYLES const object |
|
|
267
|
+
| `axios.post()` for forms | No CSRF/errors | `useForm().post()` |
|