@finema/finework-layer 0.2.50 โ 0.2.51
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/.playground/app/pages/layout-admin/test/[id]/index.vue +286 -0
- package/.playground/app/pages/layout-admin.vue +2 -2
- package/@finema finework-layer - LLM Documentation Guide.md +566 -0
- package/API_REFERENCE.md +780 -0
- package/ARCHITECTURE.md +592 -0
- package/CHANGELOG.md +6 -0
- package/COMPONENT_EXAMPLES.md +893 -0
- package/DOCUMENTATION_INDEX.md +354 -0
- package/EXAMPLES.md +597 -0
- package/QUICK_START.md +678 -0
- package/README.md +199 -33
- package/TROUBLESHOOTING.md +646 -0
- package/app/components/Button/Back.vue +1 -1
- package/app/components/Layout/Admin/Sidebar.vue +10 -3
- package/app/components/Layout/Admin/index.vue +9 -13
- package/package.json +1 -1
package/QUICK_START.md
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
# Quick Start Guide
|
|
2
|
+
|
|
3
|
+
## ๐ Getting Started
|
|
4
|
+
|
|
5
|
+
### Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js 18+ or Bun
|
|
8
|
+
- pnpm (recommended) or npm
|
|
9
|
+
- Basic knowledge of Vue 3 and Nuxt 3
|
|
10
|
+
|
|
11
|
+
### Installation
|
|
12
|
+
|
|
13
|
+
#### 1. Install the Layer
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @finema/finework-layer
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
#### 2. Extend in Your Nuxt Config
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// nuxt.config.ts
|
|
23
|
+
export default defineNuxtConfig({
|
|
24
|
+
extends: ['@finema/finework-layer']
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
#### 3. Configure Runtime Variables
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// nuxt.config.ts
|
|
32
|
+
export default defineNuxtConfig({
|
|
33
|
+
extends: ['@finema/finework-layer'],
|
|
34
|
+
|
|
35
|
+
runtimeConfig: {
|
|
36
|
+
public: {
|
|
37
|
+
baseAPI: process.env.NUXT_PUBLIC_BASE_API || 'http://localhost:8000',
|
|
38
|
+
baseAPIMock: process.env.NUXT_PUBLIC_BASE_API_MOCK || 'http://localhost:3000/api/mock'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### 4. Create Environment File
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# .env
|
|
48
|
+
NUXT_PUBLIC_BASE_API=https://api.yourapp.com
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## ๐ Your First Page
|
|
54
|
+
|
|
55
|
+
### 1. Create a Protected Page
|
|
56
|
+
|
|
57
|
+
```vue
|
|
58
|
+
<!-- pages/projects/index.vue -->
|
|
59
|
+
<template>
|
|
60
|
+
<LayoutAdmin label="Projects" :items="navItems">
|
|
61
|
+
<div class="space-y-6">
|
|
62
|
+
<h1 class="text-2xl font-bold">My Projects</h1>
|
|
63
|
+
|
|
64
|
+
<StatusBox :status="projects.status.value" :data="projects.data.value">
|
|
65
|
+
<template #default="{ data }">
|
|
66
|
+
<Card>
|
|
67
|
+
<div v-for="project in data" :key="project.id" class="p-4 border-b">
|
|
68
|
+
<h3 class="font-semibold">{{ project.name }}</h3>
|
|
69
|
+
<p class="text-gray-600">{{ project.description }}</p>
|
|
70
|
+
</div>
|
|
71
|
+
</Card>
|
|
72
|
+
</template>
|
|
73
|
+
</StatusBox>
|
|
74
|
+
</div>
|
|
75
|
+
</LayoutAdmin>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script setup lang="ts">
|
|
79
|
+
// Define page metadata
|
|
80
|
+
definePageMeta({
|
|
81
|
+
middleware: ['auth', 'permissions'],
|
|
82
|
+
accessGuard: {
|
|
83
|
+
permissions: ['pmo:USER', 'pmo:ADMIN']
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Define navigation
|
|
88
|
+
const navItems = [
|
|
89
|
+
{ label: 'Dashboard', to: '/dashboard', icon: 'i-heroicons-home' },
|
|
90
|
+
{ label: 'Projects', to: '/projects', icon: 'i-heroicons-folder' }
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
// Load data
|
|
94
|
+
interface Project {
|
|
95
|
+
id: string
|
|
96
|
+
name: string
|
|
97
|
+
description: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const projects = useListLoader<Project>({
|
|
101
|
+
method: 'GET',
|
|
102
|
+
url: '/api/projects',
|
|
103
|
+
getRequestOptions: useRequestOptions().auth
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
onMounted(() => projects.run())
|
|
107
|
+
</script>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 2. Create a Form Page
|
|
111
|
+
|
|
112
|
+
```vue
|
|
113
|
+
<!-- pages/projects/new.vue -->
|
|
114
|
+
<template>
|
|
115
|
+
<LayoutAdmin label="New Project" :items="navItems">
|
|
116
|
+
<Card>
|
|
117
|
+
<form @submit="onSubmit" class="space-y-4">
|
|
118
|
+
<FormField name="name" label="Project Name">
|
|
119
|
+
<Input v-model="form.values.name" />
|
|
120
|
+
<FormError name="name" />
|
|
121
|
+
</FormField>
|
|
122
|
+
|
|
123
|
+
<FormField name="description" label="Description">
|
|
124
|
+
<Textarea v-model="form.values.description" />
|
|
125
|
+
<FormError name="description" />
|
|
126
|
+
</FormField>
|
|
127
|
+
|
|
128
|
+
<div class="flex gap-2">
|
|
129
|
+
<Button
|
|
130
|
+
type="submit"
|
|
131
|
+
label="Create Project"
|
|
132
|
+
:loading="form.isSubmitting.value"
|
|
133
|
+
/>
|
|
134
|
+
<ButtonBack :to="routes.pmo.project.projects.to" />
|
|
135
|
+
</div>
|
|
136
|
+
</form>
|
|
137
|
+
</Card>
|
|
138
|
+
</LayoutAdmin>
|
|
139
|
+
</template>
|
|
140
|
+
|
|
141
|
+
<script setup lang="ts">
|
|
142
|
+
import * as v from 'valibot'
|
|
143
|
+
import { toTypedSchema } from '@vee-validate/valibot'
|
|
144
|
+
|
|
145
|
+
definePageMeta({
|
|
146
|
+
middleware: ['auth', 'permissions'],
|
|
147
|
+
accessGuard: {
|
|
148
|
+
permissions: ['pmo:ADMIN']
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const form = useForm({
|
|
153
|
+
validationSchema: toTypedSchema(
|
|
154
|
+
v.object({
|
|
155
|
+
name: v.pipe(
|
|
156
|
+
v.string('Name is required'),
|
|
157
|
+
v.minLength(3, 'Name must be at least 3 characters')
|
|
158
|
+
),
|
|
159
|
+
description: v.optional(v.string(), '')
|
|
160
|
+
})
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const onSubmit = form.handleSubmit(async (values) => {
|
|
165
|
+
const loader = useObjectLoader({
|
|
166
|
+
method: 'POST',
|
|
167
|
+
url: '/api/projects',
|
|
168
|
+
getRequestOptions: useRequestOptions().auth
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
await loader.run({ data: values })
|
|
172
|
+
|
|
173
|
+
if (loader.status.value.isSuccess) {
|
|
174
|
+
navigateTo(routes.pmo.project.projects.to)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const navItems = [
|
|
179
|
+
{ label: 'Projects', to: '/projects', icon: 'i-heroicons-folder' }
|
|
180
|
+
]
|
|
181
|
+
</script>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## ๐ฏ Common Tasks
|
|
187
|
+
|
|
188
|
+
### Task 1: Add a New Route
|
|
189
|
+
|
|
190
|
+
#### Step 1: Define Route Constant
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// app/constants/routes.ts (in your project)
|
|
194
|
+
export const myRoutes = {
|
|
195
|
+
...routes, // Extend existing routes
|
|
196
|
+
|
|
197
|
+
myModule: {
|
|
198
|
+
dashboard: {
|
|
199
|
+
label: 'Dashboard',
|
|
200
|
+
to: '/my-module/dashboard',
|
|
201
|
+
icon: 'i-heroicons-chart-bar'
|
|
202
|
+
},
|
|
203
|
+
settings: {
|
|
204
|
+
label: 'Settings',
|
|
205
|
+
to: '/my-module/settings',
|
|
206
|
+
icon: 'i-heroicons-cog'
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### Step 2: Create Page
|
|
213
|
+
|
|
214
|
+
```vue
|
|
215
|
+
<!-- pages/my-module/dashboard.vue -->
|
|
216
|
+
<template>
|
|
217
|
+
<LayoutAdmin label="My Module" :items="navItems">
|
|
218
|
+
<h1>Dashboard</h1>
|
|
219
|
+
</LayoutAdmin>
|
|
220
|
+
</template>
|
|
221
|
+
|
|
222
|
+
<script setup lang="ts">
|
|
223
|
+
const navItems = [
|
|
224
|
+
{ label: 'Dashboard', to: myRoutes.myModule.dashboard.to },
|
|
225
|
+
{ label: 'Settings', to: myRoutes.myModule.settings.to }
|
|
226
|
+
]
|
|
227
|
+
</script>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
### Task 2: Create a Custom Component
|
|
233
|
+
|
|
234
|
+
```vue
|
|
235
|
+
<!-- components/ProjectCard.vue -->
|
|
236
|
+
<template>
|
|
237
|
+
<Card class="hover:shadow-lg transition-shadow">
|
|
238
|
+
<div class="space-y-2">
|
|
239
|
+
<div class="flex items-center justify-between">
|
|
240
|
+
<h3 class="text-lg font-semibold">{{ project.name }}</h3>
|
|
241
|
+
<Badge :label="project.status" :color="statusColor" />
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<p class="text-gray-600">{{ project.description }}</p>
|
|
245
|
+
|
|
246
|
+
<div class="flex gap-2 pt-4">
|
|
247
|
+
<ButtonActionIcon
|
|
248
|
+
icon="ph:eye"
|
|
249
|
+
:to="`/projects/${project.id}`"
|
|
250
|
+
/>
|
|
251
|
+
<ButtonActionIcon
|
|
252
|
+
icon="ph:pencil-simple"
|
|
253
|
+
color="primary"
|
|
254
|
+
:to="`/projects/${project.id}/edit`"
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</Card>
|
|
259
|
+
</template>
|
|
260
|
+
|
|
261
|
+
<script setup lang="ts">
|
|
262
|
+
interface Project {
|
|
263
|
+
id: string
|
|
264
|
+
name: string
|
|
265
|
+
description: string
|
|
266
|
+
status: string
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const props = defineProps<{
|
|
270
|
+
project: Project
|
|
271
|
+
}>()
|
|
272
|
+
|
|
273
|
+
const statusColor = computed(() => {
|
|
274
|
+
return props.project.status === 'active' ? 'success' : 'neutral'
|
|
275
|
+
})
|
|
276
|
+
</script>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Usage:
|
|
280
|
+
|
|
281
|
+
```vue
|
|
282
|
+
<ProjectCard
|
|
283
|
+
v-for="project in projects"
|
|
284
|
+
:key="project.id"
|
|
285
|
+
:project="project"
|
|
286
|
+
/>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### Task 3: Add Permission Check
|
|
292
|
+
|
|
293
|
+
```vue
|
|
294
|
+
<template>
|
|
295
|
+
<div>
|
|
296
|
+
<!-- Show only to admins -->
|
|
297
|
+
<Button
|
|
298
|
+
v-if="canEdit"
|
|
299
|
+
label="Edit"
|
|
300
|
+
@click="handleEdit"
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
</template>
|
|
304
|
+
|
|
305
|
+
<script setup lang="ts">
|
|
306
|
+
const auth = useAuth()
|
|
307
|
+
|
|
308
|
+
const canEdit = computed(() =>
|
|
309
|
+
auth.hasPermission(UserModule.PMO, Permission.ADMIN, Permission.SUPER)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
const handleEdit = () => {
|
|
313
|
+
// Edit logic
|
|
314
|
+
}
|
|
315
|
+
</script>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### Task 4: Handle File Upload
|
|
321
|
+
|
|
322
|
+
```vue
|
|
323
|
+
<template>
|
|
324
|
+
<form @submit.prevent="handleUpload">
|
|
325
|
+
<FormField name="file" label="Upload File">
|
|
326
|
+
<input
|
|
327
|
+
type="file"
|
|
328
|
+
@change="handleFileChange"
|
|
329
|
+
accept=".pdf,.doc,.docx"
|
|
330
|
+
/>
|
|
331
|
+
</FormField>
|
|
332
|
+
|
|
333
|
+
<Button
|
|
334
|
+
type="submit"
|
|
335
|
+
label="Upload"
|
|
336
|
+
:loading="isUploading"
|
|
337
|
+
/>
|
|
338
|
+
</form>
|
|
339
|
+
</template>
|
|
340
|
+
|
|
341
|
+
<script setup lang="ts">
|
|
342
|
+
const file = ref<File | null>(null)
|
|
343
|
+
const isUploading = ref(false)
|
|
344
|
+
|
|
345
|
+
const handleFileChange = (event: Event) => {
|
|
346
|
+
const target = event.target as HTMLInputElement
|
|
347
|
+
file.value = target.files?.[0] || null
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const handleUpload = async () => {
|
|
351
|
+
if (!file.value) return
|
|
352
|
+
|
|
353
|
+
isUploading.value = true
|
|
354
|
+
|
|
355
|
+
const formData = new FormData()
|
|
356
|
+
formData.append('file', file.value)
|
|
357
|
+
|
|
358
|
+
const loader = useObjectLoader({
|
|
359
|
+
method: 'POST',
|
|
360
|
+
url: '/api/upload',
|
|
361
|
+
getRequestOptions: useRequestOptions().file
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
await loader.run({ data: formData })
|
|
365
|
+
|
|
366
|
+
isUploading.value = false
|
|
367
|
+
|
|
368
|
+
if (loader.status.value.isSuccess) {
|
|
369
|
+
console.log('Upload successful:', loader.data.value)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
</script>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
### Task 5: Implement Search & Filter
|
|
378
|
+
|
|
379
|
+
```vue
|
|
380
|
+
<template>
|
|
381
|
+
<div class="space-y-4">
|
|
382
|
+
<!-- Search Form -->
|
|
383
|
+
<Card>
|
|
384
|
+
<form @submit.prevent="handleSearch">
|
|
385
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
386
|
+
<Input
|
|
387
|
+
v-model="filters.q"
|
|
388
|
+
placeholder="Search..."
|
|
389
|
+
/>
|
|
390
|
+
|
|
391
|
+
<Select
|
|
392
|
+
v-model="filters.status"
|
|
393
|
+
:options="statusOptions"
|
|
394
|
+
/>
|
|
395
|
+
|
|
396
|
+
<div class="flex gap-2">
|
|
397
|
+
<Button type="submit" label="Search" />
|
|
398
|
+
<Button
|
|
399
|
+
type="button"
|
|
400
|
+
label="Reset"
|
|
401
|
+
variant="outline"
|
|
402
|
+
@click="handleReset"
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</form>
|
|
407
|
+
</Card>
|
|
408
|
+
|
|
409
|
+
<!-- Results -->
|
|
410
|
+
<StatusBox :status="results.status.value" :data="results.data.value">
|
|
411
|
+
<template #default="{ data }">
|
|
412
|
+
<Card>
|
|
413
|
+
<div v-for="item in data.items" :key="item.id">
|
|
414
|
+
{{ item.name }}
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<Pagination
|
|
418
|
+
v-model="page"
|
|
419
|
+
:total="data.total"
|
|
420
|
+
:per-page="perPage"
|
|
421
|
+
/>
|
|
422
|
+
</Card>
|
|
423
|
+
</template>
|
|
424
|
+
</StatusBox>
|
|
425
|
+
</div>
|
|
426
|
+
</template>
|
|
427
|
+
|
|
428
|
+
<script setup lang="ts">
|
|
429
|
+
const page = ref(1)
|
|
430
|
+
const perPage = ref(10)
|
|
431
|
+
const filters = reactive({
|
|
432
|
+
q: '',
|
|
433
|
+
status: ''
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
const results = useListLoader({
|
|
437
|
+
method: 'GET',
|
|
438
|
+
url: '/api/search',
|
|
439
|
+
getRequestOptions: useRequestOptions().auth
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
const loadResults = () => {
|
|
443
|
+
results.run({
|
|
444
|
+
params: {
|
|
445
|
+
page: page.value,
|
|
446
|
+
per_page: perPage.value,
|
|
447
|
+
...filters
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const handleSearch = () => {
|
|
453
|
+
page.value = 1
|
|
454
|
+
loadResults()
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const handleReset = () => {
|
|
458
|
+
filters.q = ''
|
|
459
|
+
filters.status = ''
|
|
460
|
+
handleSearch()
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
watch(page, () => loadResults())
|
|
464
|
+
|
|
465
|
+
onMounted(() => loadResults())
|
|
466
|
+
|
|
467
|
+
const statusOptions = [
|
|
468
|
+
{ label: 'All', value: '' },
|
|
469
|
+
{ label: 'Active', value: 'active' },
|
|
470
|
+
{ label: 'Inactive', value: 'inactive' }
|
|
471
|
+
]
|
|
472
|
+
</script>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## ๐จ Customization
|
|
478
|
+
|
|
479
|
+
### Override UI Theme
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
// app/app.config.ts
|
|
483
|
+
export default defineAppConfig({
|
|
484
|
+
core: {
|
|
485
|
+
color: '#FF6B6B', // Primary color
|
|
486
|
+
date_format: 'dd/MM/yyyy',
|
|
487
|
+
locale: 'th'
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
ui: {
|
|
491
|
+
button: {
|
|
492
|
+
defaultVariants: {
|
|
493
|
+
color: 'primary',
|
|
494
|
+
variant: 'solid'
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
card: {
|
|
499
|
+
variants: {
|
|
500
|
+
variant: {
|
|
501
|
+
custom: {
|
|
502
|
+
root: 'bg-gradient-to-r from-blue-500 to-purple-500'
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
})
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Add Custom Middleware
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
// middleware/analytics.ts
|
|
515
|
+
export default defineNuxtRouteMiddleware((to, from) => {
|
|
516
|
+
// Track page view
|
|
517
|
+
console.log('Page view:', to.path)
|
|
518
|
+
|
|
519
|
+
// Send to analytics
|
|
520
|
+
if (import.meta.client) {
|
|
521
|
+
// Your analytics code
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Use it:
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
definePageMeta({
|
|
530
|
+
middleware: ['auth', 'analytics']
|
|
531
|
+
})
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## ๐งช Testing
|
|
537
|
+
|
|
538
|
+
### Component Test
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
// tests/components/ProjectCard.test.ts
|
|
542
|
+
import { mount } from '@nuxt/test-utils'
|
|
543
|
+
import ProjectCard from '~/components/ProjectCard.vue'
|
|
544
|
+
|
|
545
|
+
describe('ProjectCard', () => {
|
|
546
|
+
it('renders project name', () => {
|
|
547
|
+
const wrapper = mount(ProjectCard, {
|
|
548
|
+
props: {
|
|
549
|
+
project: {
|
|
550
|
+
id: '1',
|
|
551
|
+
name: 'Test Project',
|
|
552
|
+
description: 'Test Description',
|
|
553
|
+
status: 'active'
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
expect(wrapper.text()).toContain('Test Project')
|
|
559
|
+
})
|
|
560
|
+
})
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Integration Test
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
// tests/pages/projects.test.ts
|
|
567
|
+
import { setup, $fetch } from '@nuxt/test-utils'
|
|
568
|
+
|
|
569
|
+
describe('Projects Page', () => {
|
|
570
|
+
await setup({
|
|
571
|
+
// Test configuration
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it('loads projects', async () => {
|
|
575
|
+
const html = await $fetch('/projects')
|
|
576
|
+
expect(html).toContain('My Projects')
|
|
577
|
+
})
|
|
578
|
+
})
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
## ๐ฆ Deployment
|
|
584
|
+
|
|
585
|
+
### Build for Production
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
pnpm build
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Preview Production Build
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
pnpm preview
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Environment Variables
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
# .env.production
|
|
601
|
+
NUXT_PUBLIC_BASE_API=https://api.production.com
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## ๐ Debugging
|
|
607
|
+
|
|
608
|
+
### Enable Nuxt DevTools
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
// nuxt.config.ts
|
|
612
|
+
export default defineNuxtConfig({
|
|
613
|
+
devtools: {
|
|
614
|
+
enabled: true
|
|
615
|
+
}
|
|
616
|
+
})
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Debug Authentication
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
const auth = useAuth()
|
|
623
|
+
|
|
624
|
+
console.log('Token:', auth.token.value)
|
|
625
|
+
console.log('User:', auth.me.value)
|
|
626
|
+
console.log('Is Authenticated:', auth.isAuthenticated.value)
|
|
627
|
+
console.log('Permissions:', auth.me.value?.access_level)
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### Debug Data Loading
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
const loader = useObjectLoader({...})
|
|
634
|
+
|
|
635
|
+
watch(() => loader.status.value, (status) => {
|
|
636
|
+
console.log('Loading:', status.isLoading)
|
|
637
|
+
console.log('Success:', status.isSuccess)
|
|
638
|
+
console.log('Error:', status.isError)
|
|
639
|
+
console.log('Data:', loader.data.value)
|
|
640
|
+
})
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## ๐ Next Steps
|
|
646
|
+
|
|
647
|
+
1. **Read the [LLM Guide](./LLM_GUIDE.md)** - Comprehensive documentation
|
|
648
|
+
2. **Check [Component Examples](./COMPONENT_EXAMPLES.md)** - Real-world examples
|
|
649
|
+
3. **Review [API Reference](./API_REFERENCE.md)** - Detailed API docs
|
|
650
|
+
4. **Study [Architecture](./ARCHITECTURE.md)** - System design
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
## ๐ก Tips
|
|
655
|
+
|
|
656
|
+
1. **Use Auto-imports** - No need to import components, composables, or constants
|
|
657
|
+
2. **Follow Existing Patterns** - Check existing code before creating new patterns
|
|
658
|
+
3. **Type Everything** - Use TypeScript for better DX
|
|
659
|
+
4. **Use StatusBox** - For consistent loading/error states
|
|
660
|
+
5. **Check Permissions** - Always validate user permissions
|
|
661
|
+
6. **Use Route Constants** - Never hardcode routes
|
|
662
|
+
7. **Leverage @finema/core** - Many utilities already exist
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## ๐ Getting Help
|
|
667
|
+
|
|
668
|
+
1. Check existing components and pages for examples
|
|
669
|
+
2. Review the documentation files
|
|
670
|
+
3. Use Nuxt DevTools for debugging
|
|
671
|
+
4. Check browser console for errors
|
|
672
|
+
5. Review network requests in DevTools
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
**Last Updated:** 2025-11-07
|
|
677
|
+
**Happy Coding! ๐**
|
|
678
|
+
|