start-vibing-stacks 2.1.1 → 2.3.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 +4 -6
- package/dist/index.js +63 -2
- package/dist/scanner.d.ts +12 -0
- package/dist/scanner.js +480 -0
- package/dist/setup.js +29 -0
- package/dist/types.d.ts +20 -0
- package/dist/ui.js +1 -1
- package/package.json +1 -1
- package/stacks/_shared/hooks/user-prompt-submit.ts +26 -2
- 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/php/skills/api-security/SKILL.md +431 -0
- 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-php.md +108 -29
package/dist/setup.js
CHANGED
|
@@ -138,6 +138,35 @@ export async function setupProject(projectDir, config, options = {}) {
|
|
|
138
138
|
writeFileSync(gitignorePath, gitignore.trimEnd() + '\n\n# Claude Code local preferences\nCLAUDE.local.md\n');
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
+
// 11c. Save standards review results
|
|
142
|
+
if (config.standardsReview) {
|
|
143
|
+
const reviewPath = join(claudeDir, 'config', 'standards-review.json');
|
|
144
|
+
writeFileSync(reviewPath, JSON.stringify(config.standardsReview, null, 2));
|
|
145
|
+
if (config.standardsReview.status === 'adapted' && config.standardsReview.patterns.length > 0) {
|
|
146
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
147
|
+
if (existsSync(claudeMdPath)) {
|
|
148
|
+
let claudeContent = readFileSync(claudeMdPath, 'utf8');
|
|
149
|
+
const categories = new Map();
|
|
150
|
+
for (const p of config.standardsReview.patterns) {
|
|
151
|
+
const list = categories.get(p.category) || [];
|
|
152
|
+
list.push(p.name);
|
|
153
|
+
categories.set(p.category, list);
|
|
154
|
+
}
|
|
155
|
+
let standardsSection = '\n\n## Project Standards (imported)\n\n';
|
|
156
|
+
standardsSection += `> Scanned from: ${config.standardsReview.sources.join(', ')}\n\n`;
|
|
157
|
+
for (const [category, items] of categories) {
|
|
158
|
+
standardsSection += `### ${category.charAt(0).toUpperCase() + category.slice(1)}\n\n`;
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
standardsSection += `- ${item}\n`;
|
|
161
|
+
}
|
|
162
|
+
standardsSection += '\n';
|
|
163
|
+
}
|
|
164
|
+
claudeContent += standardsSection;
|
|
165
|
+
writeFileSync(claudeMdPath, claudeContent);
|
|
166
|
+
}
|
|
167
|
+
spinner.text = `Imported ${config.standardsReview.patterns.length} project standards`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
141
170
|
// 12. Copy commands
|
|
142
171
|
const sharedCommandsDir = join(PACKAGE_ROOT, 'stacks', '_shared', 'commands');
|
|
143
172
|
if (existsSync(sharedCommandsDir)) {
|
package/dist/types.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface FrameworkOption {
|
|
|
31
31
|
name: string;
|
|
32
32
|
icon: string;
|
|
33
33
|
detectFiles?: string[];
|
|
34
|
+
skills?: string[];
|
|
34
35
|
extra?: Record<string, unknown>;
|
|
35
36
|
}
|
|
36
37
|
export interface DatabaseOption {
|
|
@@ -42,6 +43,7 @@ export interface FrontendOption {
|
|
|
42
43
|
id: string;
|
|
43
44
|
name: string;
|
|
44
45
|
icon: string;
|
|
46
|
+
frameworks?: string[];
|
|
45
47
|
}
|
|
46
48
|
export interface DeployTarget {
|
|
47
49
|
id: string;
|
|
@@ -74,6 +76,7 @@ export interface ProjectConfig {
|
|
|
74
76
|
domains: Record<string, {
|
|
75
77
|
patterns: string[];
|
|
76
78
|
}>;
|
|
79
|
+
standardsReview?: StandardsReview;
|
|
77
80
|
}
|
|
78
81
|
export interface DetectionResult {
|
|
79
82
|
files: string[];
|
|
@@ -86,3 +89,20 @@ export interface DetectionResult {
|
|
|
86
89
|
hasClaudeMd: boolean;
|
|
87
90
|
hasGit: boolean;
|
|
88
91
|
}
|
|
92
|
+
export interface PatternMatch {
|
|
93
|
+
category: string;
|
|
94
|
+
name: string;
|
|
95
|
+
confidence: number;
|
|
96
|
+
detail?: string;
|
|
97
|
+
}
|
|
98
|
+
export interface ScanResult {
|
|
99
|
+
source: string;
|
|
100
|
+
patterns: PatternMatch[];
|
|
101
|
+
}
|
|
102
|
+
export interface StandardsReview {
|
|
103
|
+
status: 'adapted' | 'defaults' | 'pending';
|
|
104
|
+
scannedAt: string;
|
|
105
|
+
sources: string[];
|
|
106
|
+
patterns: PatternMatch[];
|
|
107
|
+
userChoice: 'adapt' | 'defaults' | 'pending';
|
|
108
|
+
}
|
package/dist/ui.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Start Vibing Stacks — Terminal UI
|
|
3
3
|
*/
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
const VERSION = '2.
|
|
5
|
+
const VERSION = '2.2.0';
|
|
6
6
|
const gradient = (text) => {
|
|
7
7
|
const colors = [chalk.hex('#FF6B6B'), chalk.hex('#FF8E53'), chalk.hex('#FFBD2E'), chalk.hex('#48BB78'), chalk.hex('#4299E1'), chalk.hex('#9F7AEA')];
|
|
8
8
|
return text.split('').map((c, i) => colors[i % colors.length](c)).join('');
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import { join } from 'path';
|
|
|
11
11
|
|
|
12
12
|
const PROJECT_DIR = process.env['CLAUDE_PROJECT_DIR'] || process.cwd();
|
|
13
13
|
const ACTIVE_PROJECT = join(PROJECT_DIR, '.claude', 'config', 'active-project.json');
|
|
14
|
+
const STANDARDS_REVIEW = join(PROJECT_DIR, '.claude', 'config', 'standards-review.json');
|
|
14
15
|
|
|
15
16
|
let stackName = 'Unknown';
|
|
16
17
|
let qualityCmd = 'Run quality gates';
|
|
@@ -18,7 +19,6 @@ try {
|
|
|
18
19
|
if (existsSync(ACTIVE_PROJECT)) {
|
|
19
20
|
const config = JSON.parse(readFileSync(ACTIVE_PROJECT, 'utf8'));
|
|
20
21
|
stackName = config.stack || 'Unknown';
|
|
21
|
-
// Read quality gates command from stack config
|
|
22
22
|
const stackConfig = join(PROJECT_DIR, '.claude', 'config', 'quality-gates.json');
|
|
23
23
|
if (existsSync(stackConfig)) {
|
|
24
24
|
const gates = JSON.parse(readFileSync(stackConfig, 'utf8'));
|
|
@@ -27,6 +27,30 @@ try {
|
|
|
27
27
|
}
|
|
28
28
|
} catch {}
|
|
29
29
|
|
|
30
|
+
interface ReviewFile {
|
|
31
|
+
status: string;
|
|
32
|
+
sources?: string[];
|
|
33
|
+
patterns?: { category: string; name: string }[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let standardsContext = '';
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(STANDARDS_REVIEW)) {
|
|
39
|
+
standardsContext = `\n\nSTANDARDS REVIEW NEEDED: No standards-review.json found. ` +
|
|
40
|
+
`This project may have existing coding standards (.cursorrules, composer.json configs). ` +
|
|
41
|
+
`Ask the user: "I noticed this project hasn't been scanned for existing standards. ` +
|
|
42
|
+
`Would you like me to review your codebase patterns and adapt my behavior, ` +
|
|
43
|
+
`or should I use the default standards?"`;
|
|
44
|
+
} else {
|
|
45
|
+
const review: ReviewFile = JSON.parse(readFileSync(STANDARDS_REVIEW, 'utf8'));
|
|
46
|
+
if (review.status === 'adapted' && review.patterns && review.patterns.length > 0) {
|
|
47
|
+
const patternList = review.patterns.map(p => `- [${p.category}] ${p.name}`).join('\n');
|
|
48
|
+
standardsContext = `\n\nPROJECT STANDARDS (scanned from ${(review.sources || []).join(', ')}):\n${patternList}\n` +
|
|
49
|
+
`Follow these project-specific patterns. They take priority over generic defaults.`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
|
|
30
54
|
async function main(): Promise<void> {
|
|
31
55
|
let hookInput: any = {};
|
|
32
56
|
try {
|
|
@@ -65,7 +89,7 @@ async function main(): Promise<void> {
|
|
|
65
89
|
a. "## Last Change" (date: ${today}, branch, summary)
|
|
66
90
|
b. Update ALL affected rule/flow sections
|
|
67
91
|
|
|
68
|
-
6. Run stop-validator before finishing
|
|
92
|
+
6. Run stop-validator before finishing.${standardsContext}`;
|
|
69
93
|
|
|
70
94
|
console.log(JSON.stringify({ continue: true, systemMessage }));
|
|
71
95
|
process.exit(0);
|
|
@@ -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 |
|