omgkit 2.2.0 → 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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/databases/mongodb/SKILL.md +60 -776
  3. package/plugin/skills/databases/prisma/SKILL.md +53 -744
  4. package/plugin/skills/databases/redis/SKILL.md +53 -860
  5. package/plugin/skills/devops/aws/SKILL.md +68 -672
  6. package/plugin/skills/devops/github-actions/SKILL.md +54 -657
  7. package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
  8. package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
  9. package/plugin/skills/frameworks/django/SKILL.md +87 -853
  10. package/plugin/skills/frameworks/express/SKILL.md +95 -1301
  11. package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
  12. package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
  13. package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
  14. package/plugin/skills/frameworks/react/SKILL.md +94 -962
  15. package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
  16. package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
  17. package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
  18. package/plugin/skills/frontend/responsive/SKILL.md +76 -799
  19. package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
  20. package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
  21. package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
  22. package/plugin/skills/languages/javascript/SKILL.md +106 -849
  23. package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
  24. package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
  25. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
  26. package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
  27. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
  28. package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
  29. package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
  30. package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
  31. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
  32. package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
  33. package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
  34. package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
  35. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
  36. package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
  37. package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
  38. package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
  39. package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
  40. package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
  41. package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
  42. package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
  43. package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
  44. package/plugin/skills/security/better-auth/SKILL.md +46 -1034
  45. package/plugin/skills/security/oauth/SKILL.md +80 -934
  46. package/plugin/skills/security/owasp/SKILL.md +78 -862
  47. package/plugin/skills/testing/playwright/SKILL.md +77 -700
  48. package/plugin/skills/testing/pytest/SKILL.md +73 -811
  49. package/plugin/skills/testing/vitest/SKILL.md +60 -920
  50. package/plugin/skills/tools/document-processing/SKILL.md +111 -838
  51. package/plugin/skills/tools/image-processing/SKILL.md +126 -659
  52. package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
  53. package/plugin/skills/tools/media-processing/SKILL.md +118 -735
  54. package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
  55. package/plugin/skills/SKILL_STANDARDS.md +0 -743
@@ -1,429 +1,118 @@
1
1
  ---
2
- name: vue
3
- description: Modern Vue 3 development with Composition API, TypeScript, Pinia, Vue Router, and testing patterns
4
- category: frameworks
5
- triggers:
6
- - vue
7
- - vue 3
8
- - vue.js
9
- - vuejs
10
- - composition api
11
- - pinia
12
- - vue router
13
- - nuxt
14
- - vite vue
2
+ name: building-vue-apps
3
+ description: Builds production Vue 3 applications with Composition API, TypeScript, Pinia, and Vue Router. Use when creating Vue.js SPAs, component libraries, or reactive web interfaces.
15
4
  ---
16
5
 
17
6
  # Vue.js
18
7
 
19
- Modern **Vue 3 development** following industry best practices. This skill covers Composition API, TypeScript integration, Pinia state management, Vue Router, component patterns, testing with Vitest, and production-ready configurations used by top engineering teams.
8
+ ## Quick Start
20
9
 
21
- ## Purpose
10
+ ```vue
11
+ <script setup lang="ts">
12
+ import { ref, computed } from 'vue';
22
13
 
23
- Build reactive, maintainable Vue applications with confidence:
14
+ const count = ref(0);
15
+ const doubled = computed(() => count.value * 2);
16
+ </script>
24
17
 
25
- - Master Composition API and `<script setup>` syntax
26
- - Implement type-safe components with TypeScript
27
- - Manage state effectively with Pinia stores
28
- - Handle routing with Vue Router
29
- - Create reusable composables
30
- - Write comprehensive tests with Vitest
31
- - Build performant applications
18
+ <template>
19
+ <button @click="count++">
20
+ Count: {{ count }} (doubled: {{ doubled }})
21
+ </button>
22
+ </template>
23
+ ```
32
24
 
33
25
  ## Features
34
26
 
35
- ### 1. Component Architecture with TypeScript
27
+ | Feature | Description | Guide |
28
+ |---------|-------------|-------|
29
+ | Composition API | Reactive state, composables, lifecycle | [COMPOSITION.md](COMPOSITION.md) |
30
+ | TypeScript | Props, emits, refs typing | [TYPESCRIPT.md](TYPESCRIPT.md) |
31
+ | Pinia | State management, stores, plugins | [PINIA.md](PINIA.md) |
32
+ | Vue Router | Navigation, guards, lazy loading | [ROUTER.md](ROUTER.md) |
33
+ | Testing | Vitest, Vue Test Utils patterns | [TESTING.md](TESTING.md) |
34
+ | Performance | Lazy loading, memoization, virtual lists | [PERFORMANCE.md](PERFORMANCE.md) |
35
+
36
+ ## Common Patterns
37
+
38
+ ### Component with Props and Emits
36
39
 
37
40
  ```vue
38
- <!-- src/components/UserCard.vue -->
39
41
  <script setup lang="ts">
40
- import { computed, ref, watch, onMounted } from 'vue';
41
- import type { User, UserRole } from '@/types';
42
-
43
- // Props with TypeScript
44
42
  interface Props {
45
- user: User;
43
+ user: { id: string; name: string; email: string };
46
44
  showActions?: boolean;
47
- variant?: 'default' | 'compact' | 'detailed';
48
45
  }
49
46
 
50
47
  const props = withDefaults(defineProps<Props>(), {
51
48
  showActions: true,
52
- variant: 'default',
53
- });
54
-
55
- // Emits with TypeScript
56
- interface Emits {
57
- (e: 'edit', user: User): void;
58
- (e: 'delete', userId: string): void;
59
- (e: 'select', user: User, selected: boolean): void;
60
- }
61
-
62
- const emit = defineEmits<Emits>();
63
-
64
- // Expose methods to parent
65
- defineExpose({
66
- focus: () => cardRef.value?.focus(),
67
- });
68
-
69
- // Refs
70
- const cardRef = ref<HTMLDivElement | null>(null);
71
- const isSelected = ref(false);
72
- const isLoading = ref(false);
73
-
74
- // Computed
75
- const fullName = computed(() =>
76
- `${props.user.firstName} ${props.user.lastName}`
77
- );
78
-
79
- const roleLabel = computed(() => {
80
- const labels: Record<UserRole, string> = {
81
- admin: 'Administrator',
82
- user: 'User',
83
- guest: 'Guest',
84
- };
85
- return labels[props.user.role];
86
- });
87
-
88
- const cardClasses = computed(() => ({
89
- 'user-card': true,
90
- [`user-card--${props.variant}`]: true,
91
- 'user-card--selected': isSelected.value,
92
- 'user-card--loading': isLoading.value,
93
- }));
94
-
95
- // Watchers
96
- watch(() => props.user.id, (newId, oldId) => {
97
- if (newId !== oldId) {
98
- isSelected.value = false;
99
- }
100
49
  });
101
50
 
102
- // Methods
103
- function handleEdit() {
104
- emit('edit', props.user);
105
- }
106
-
107
- function handleDelete() {
108
- emit('delete', props.user.id);
109
- }
110
-
111
- function toggleSelect() {
112
- isSelected.value = !isSelected.value;
113
- emit('select', props.user, isSelected.value);
114
- }
115
-
116
- // Lifecycle
117
- onMounted(() => {
118
- console.log('UserCard mounted for:', props.user.id);
119
- });
51
+ const emit = defineEmits<{
52
+ (e: 'edit', user: Props['user']): void;
53
+ (e: 'delete', id: string): void;
54
+ }>();
120
55
  </script>
121
56
 
122
57
  <template>
123
- <div
124
- ref="cardRef"
125
- :class="cardClasses"
126
- tabindex="0"
127
- @click="toggleSelect"
128
- >
129
- <div class="user-card__avatar">
130
- <img
131
- :src="user.avatarUrl || '/default-avatar.png'"
132
- :alt="fullName"
133
- />
134
- </div>
135
-
136
- <div class="user-card__content">
137
- <h3 class="user-card__name">{{ fullName }}</h3>
138
- <p class="user-card__email">{{ user.email }}</p>
139
- <span class="user-card__role" :class="`role--${user.role}`">
140
- {{ roleLabel }}
141
- </span>
142
- </div>
143
-
144
- <div v-if="showActions" class="user-card__actions">
145
- <button @click.stop="handleEdit">Edit</button>
146
- <button @click.stop="handleDelete">Delete</button>
58
+ <div class="user-card">
59
+ <h3>{{ user.name }}</h3>
60
+ <p>{{ user.email }}</p>
61
+ <div v-if="showActions">
62
+ <button @click="emit('edit', user)">Edit</button>
63
+ <button @click="emit('delete', user.id)">Delete</button>
147
64
  </div>
148
-
149
- <slot name="footer" :user="user" :selected="isSelected" />
150
65
  </div>
151
66
  </template>
152
-
153
- <style scoped>
154
- .user-card {
155
- display: flex;
156
- align-items: center;
157
- gap: 1rem;
158
- padding: 1rem;
159
- border: 1px solid var(--border-color);
160
- border-radius: 8px;
161
- transition: all 0.2s ease;
162
- }
163
-
164
- .user-card--selected {
165
- border-color: var(--primary-color);
166
- background: var(--primary-bg);
167
- }
168
-
169
- .user-card--compact {
170
- padding: 0.5rem;
171
- }
172
-
173
- .user-card--loading {
174
- opacity: 0.6;
175
- pointer-events: none;
176
- }
177
- </style>
178
67
  ```
179
68
 
180
- ### 2. Composables (Reusable Logic)
69
+ ### Composable for Reusable Logic
181
70
 
182
71
  ```typescript
183
- // src/composables/useUser.ts
184
- import { ref, computed, watch, type Ref } from 'vue';
185
- import type { User, UpdateUserData } from '@/types';
186
- import { userService } from '@/services/user.service';
187
-
188
- interface UseUserOptions {
189
- immediate?: boolean;
190
- onError?: (error: Error) => void;
191
- }
192
-
193
- export function useUser(userId: Ref<string | null>, options: UseUserOptions = {}) {
194
- const { immediate = true, onError } = options;
72
+ // composables/useFetch.ts
73
+ import { ref, watch, type Ref } from 'vue';
195
74
 
196
- const user = ref<User | null>(null);
75
+ export function useFetch<T>(url: Ref<string>) {
76
+ const data = ref<T | null>(null);
197
77
  const loading = ref(false);
198
78
  const error = ref<Error | null>(null);
199
79
 
200
- const isLoaded = computed(() => !!user.value);
201
- const fullName = computed(() =>
202
- user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
203
- );
204
-
205
- async function fetchUser() {
206
- if (!userId.value) {
207
- user.value = null;
208
- return;
209
- }
210
-
80
+ async function fetchData() {
211
81
  loading.value = true;
212
82
  error.value = null;
213
-
214
- try {
215
- user.value = await userService.getById(userId.value);
216
- } catch (e) {
217
- error.value = e as Error;
218
- onError?.(e as Error);
219
- } finally {
220
- loading.value = false;
221
- }
222
- }
223
-
224
- async function updateUser(data: UpdateUserData) {
225
- if (!userId.value || !user.value) return;
226
-
227
- loading.value = true;
228
83
  try {
229
- user.value = await userService.update(userId.value, data);
84
+ const response = await fetch(url.value);
85
+ data.value = await response.json();
230
86
  } catch (e) {
231
87
  error.value = e as Error;
232
- throw e;
233
88
  } finally {
234
89
  loading.value = false;
235
90
  }
236
91
  }
237
92
 
238
- watch(userId, fetchUser, { immediate });
239
-
240
- return {
241
- user,
242
- loading,
243
- error,
244
- isLoaded,
245
- fullName,
246
- fetchUser,
247
- updateUser,
248
- };
249
- }
250
-
93
+ watch(url, fetchData, { immediate: true });
251
94
 
252
- // src/composables/usePagination.ts
253
- import { ref, computed, watch } from 'vue';
254
-
255
- interface UsePaginationOptions {
256
- initialPage?: number;
257
- initialLimit?: number;
258
- total?: number;
259
- }
260
-
261
- export function usePagination(options: UsePaginationOptions = {}) {
262
- const page = ref(options.initialPage ?? 1);
263
- const limit = ref(options.initialLimit ?? 20);
264
- const total = ref(options.total ?? 0);
265
-
266
- const totalPages = computed(() =>
267
- Math.ceil(total.value / limit.value) || 1
268
- );
269
-
270
- const hasNextPage = computed(() => page.value < totalPages.value);
271
- const hasPrevPage = computed(() => page.value > 1);
272
-
273
- const offset = computed(() => (page.value - 1) * limit.value);
274
-
275
- function nextPage() {
276
- if (hasNextPage.value) page.value++;
277
- }
278
-
279
- function prevPage() {
280
- if (hasPrevPage.value) page.value--;
281
- }
282
-
283
- function goToPage(pageNum: number) {
284
- if (pageNum >= 1 && pageNum <= totalPages.value) {
285
- page.value = pageNum;
286
- }
287
- }
288
-
289
- function setTotal(newTotal: number) {
290
- total.value = newTotal;
291
- if (page.value > totalPages.value) {
292
- page.value = totalPages.value || 1;
293
- }
294
- }
295
-
296
- return {
297
- page,
298
- limit,
299
- total,
300
- totalPages,
301
- hasNextPage,
302
- hasPrevPage,
303
- offset,
304
- nextPage,
305
- prevPage,
306
- goToPage,
307
- setTotal,
308
- };
309
- }
310
-
311
-
312
- // src/composables/useAsync.ts
313
- import { ref, shallowRef, type Ref } from 'vue';
314
-
315
- interface UseAsyncOptions<T> {
316
- immediate?: boolean;
317
- initialData?: T;
318
- onSuccess?: (data: T) => void;
319
- onError?: (error: Error) => void;
320
- }
321
-
322
- export function useAsync<T, P extends unknown[] = []>(
323
- asyncFn: (...args: P) => Promise<T>,
324
- options: UseAsyncOptions<T> = {}
325
- ) {
326
- const { initialData, onSuccess, onError } = options;
327
-
328
- const data = shallowRef<T | undefined>(initialData) as Ref<T | undefined>;
329
- const loading = ref(false);
330
- const error = ref<Error | null>(null);
331
-
332
- async function execute(...args: P): Promise<T | undefined> {
333
- loading.value = true;
334
- error.value = null;
335
-
336
- try {
337
- const result = await asyncFn(...args);
338
- data.value = result;
339
- onSuccess?.(result);
340
- return result;
341
- } catch (e) {
342
- error.value = e as Error;
343
- onError?.(e as Error);
344
- return undefined;
345
- } finally {
346
- loading.value = false;
347
- }
348
- }
349
-
350
- return {
351
- data,
352
- loading,
353
- error,
354
- execute,
355
- };
95
+ return { data, loading, error, refetch: fetchData };
356
96
  }
357
97
  ```
358
98
 
359
- ### 3. Pinia State Management
99
+ ### Pinia Store
360
100
 
361
101
  ```typescript
362
- // src/stores/user.store.ts
102
+ // stores/user.ts
363
103
  import { defineStore } from 'pinia';
364
104
  import { ref, computed } from 'vue';
365
- import type { User, LoginCredentials, RegisterData } from '@/types';
366
- import { authService } from '@/services/auth.service';
367
- import { useStorage } from '@vueuse/core';
368
105
 
369
106
  export const useUserStore = defineStore('user', () => {
370
- // State
371
107
  const user = ref<User | null>(null);
372
- const token = useStorage<string | null>('auth_token', null);
373
- const loading = ref(false);
374
- const error = ref<string | null>(null);
375
-
376
- // Getters
377
- const isAuthenticated = computed(() => !!token.value && !!user.value);
378
- const isAdmin = computed(() => user.value?.role === 'admin');
379
- const fullName = computed(() =>
380
- user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
381
- );
108
+ const token = ref<string | null>(null);
382
109
 
383
- // Actions
384
- async function login(credentials: LoginCredentials) {
385
- loading.value = true;
386
- error.value = null;
110
+ const isAuthenticated = computed(() => !!user.value);
387
111
 
388
- try {
389
- const response = await authService.login(credentials);
390
- token.value = response.token;
391
- user.value = response.user;
392
- } catch (e) {
393
- error.value = (e as Error).message;
394
- throw e;
395
- } finally {
396
- loading.value = false;
397
- }
398
- }
399
-
400
- async function register(data: RegisterData) {
401
- loading.value = true;
402
- error.value = null;
403
-
404
- try {
405
- const response = await authService.register(data);
406
- token.value = response.token;
407
- user.value = response.user;
408
- } catch (e) {
409
- error.value = (e as Error).message;
410
- throw e;
411
- } finally {
412
- loading.value = false;
413
- }
414
- }
415
-
416
- async function fetchCurrentUser() {
417
- if (!token.value) return;
418
-
419
- loading.value = true;
420
- try {
421
- user.value = await authService.getCurrentUser();
422
- } catch (e) {
423
- logout();
424
- } finally {
425
- loading.value = false;
426
- }
112
+ async function login(credentials: { email: string; password: string }) {
113
+ const res = await authService.login(credentials);
114
+ user.value = res.user;
115
+ token.value = res.token;
427
116
  }
428
117
 
429
118
  function logout() {
@@ -431,901 +120,65 @@ export const useUserStore = defineStore('user', () => {
431
120
  token.value = null;
432
121
  }
433
122
 
434
- function $reset() {
435
- user.value = null;
436
- token.value = null;
437
- loading.value = false;
438
- error.value = null;
439
- }
440
-
441
- return {
442
- // State
443
- user,
444
- token,
445
- loading,
446
- error,
447
- // Getters
448
- isAuthenticated,
449
- isAdmin,
450
- fullName,
451
- // Actions
452
- login,
453
- register,
454
- fetchCurrentUser,
455
- logout,
456
- $reset,
457
- };
123
+ return { user, token, isAuthenticated, login, logout };
458
124
  });
125
+ ```
459
126
 
127
+ ## Workflows
460
128
 
461
- // src/stores/notification.store.ts
462
- import { defineStore } from 'pinia';
463
- import { ref } from 'vue';
464
-
465
- interface Notification {
466
- id: string;
467
- type: 'success' | 'error' | 'warning' | 'info';
468
- message: string;
469
- duration?: number;
470
- }
471
-
472
- export const useNotificationStore = defineStore('notification', () => {
473
- const notifications = ref<Notification[]>([]);
474
-
475
- function add(notification: Omit<Notification, 'id'>) {
476
- const id = crypto.randomUUID();
477
- const newNotification: Notification = {
478
- ...notification,
479
- id,
480
- duration: notification.duration ?? 5000,
481
- };
482
-
483
- notifications.value.push(newNotification);
484
-
485
- if (newNotification.duration > 0) {
486
- setTimeout(() => remove(id), newNotification.duration);
487
- }
488
-
489
- return id;
490
- }
491
-
492
- function remove(id: string) {
493
- const index = notifications.value.findIndex(n => n.id === id);
494
- if (index !== -1) {
495
- notifications.value.splice(index, 1);
496
- }
497
- }
498
-
499
- function success(message: string) {
500
- return add({ type: 'success', message });
501
- }
502
-
503
- function error(message: string) {
504
- return add({ type: 'error', message });
505
- }
506
-
507
- function warning(message: string) {
508
- return add({ type: 'warning', message });
509
- }
510
-
511
- function info(message: string) {
512
- return add({ type: 'info', message });
513
- }
129
+ ### Component Development
514
130
 
515
- return {
516
- notifications,
517
- add,
518
- remove,
519
- success,
520
- error,
521
- warning,
522
- info,
523
- };
524
- });
525
- ```
131
+ 1. Define props interface with TypeScript
132
+ 2. Define emits with typed events
133
+ 3. Use `<script setup>` syntax
134
+ 4. Create composables for reusable logic
135
+ 5. Write tests with Vitest + Vue Test Utils
526
136
 
527
- ### 4. Vue Router Configuration
137
+ ### Router Setup
528
138
 
529
139
  ```typescript
530
- // src/router/index.ts
531
- import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
532
- import { useUserStore } from '@/stores/user.store';
533
-
534
- const routes: RouteRecordRaw[] = [
535
- {
536
- path: '/',
537
- component: () => import('@/layouts/DefaultLayout.vue'),
538
- children: [
539
- {
540
- path: '',
541
- name: 'home',
542
- component: () => import('@/views/HomeView.vue'),
543
- },
544
- {
545
- path: 'dashboard',
546
- name: 'dashboard',
547
- component: () => import('@/views/DashboardView.vue'),
548
- meta: { requiresAuth: true },
549
- },
550
- {
551
- path: 'users',
552
- name: 'users',
553
- component: () => import('@/views/UsersView.vue'),
554
- meta: { requiresAuth: true, roles: ['admin'] },
555
- },
556
- {
557
- path: 'users/:id',
558
- name: 'user-detail',
559
- component: () => import('@/views/UserDetailView.vue'),
560
- props: true,
561
- meta: { requiresAuth: true },
562
- },
563
- ],
564
- },
565
- {
566
- path: '/auth',
567
- component: () => import('@/layouts/AuthLayout.vue'),
568
- children: [
569
- {
570
- path: 'login',
571
- name: 'login',
572
- component: () => import('@/views/auth/LoginView.vue'),
573
- meta: { guestOnly: true },
574
- },
575
- {
576
- path: 'register',
577
- name: 'register',
578
- component: () => import('@/views/auth/RegisterView.vue'),
579
- meta: { guestOnly: true },
580
- },
581
- ],
582
- },
140
+ const routes = [
141
+ { path: '/', component: () => import('./views/Home.vue') },
583
142
  {
584
- path: '/:pathMatch(.*)*',
585
- name: 'not-found',
586
- component: () => import('@/views/NotFoundView.vue'),
143
+ path: '/dashboard',
144
+ component: () => import('./views/Dashboard.vue'),
145
+ meta: { requiresAuth: true },
587
146
  },
588
147
  ];
589
148
 
590
- const router = createRouter({
591
- history: createWebHistory(import.meta.env.BASE_URL),
592
- routes,
593
- scrollBehavior(to, from, savedPosition) {
594
- if (savedPosition) return savedPosition;
595
- if (to.hash) return { el: to.hash, behavior: 'smooth' };
596
- return { top: 0 };
597
- },
598
- });
599
-
600
- // Navigation Guards
601
- router.beforeEach(async (to, from, next) => {
149
+ router.beforeEach((to, from, next) => {
602
150
  const userStore = useUserStore();
603
-
604
- // Initialize user if token exists but user is not loaded
605
- if (userStore.token && !userStore.user) {
606
- await userStore.fetchCurrentUser();
607
- }
608
-
609
- // Check authentication
610
151
  if (to.meta.requiresAuth && !userStore.isAuthenticated) {
611
- return next({ name: 'login', query: { redirect: to.fullPath } });
152
+ next({ path: '/login', query: { redirect: to.fullPath } });
153
+ } else {
154
+ next();
612
155
  }
613
-
614
- // Check guest-only routes
615
- if (to.meta.guestOnly && userStore.isAuthenticated) {
616
- return next({ name: 'dashboard' });
617
- }
618
-
619
- // Check role-based access
620
- const requiredRoles = to.meta.roles as string[] | undefined;
621
- if (requiredRoles && userStore.user) {
622
- if (!requiredRoles.includes(userStore.user.role)) {
623
- return next({ name: 'dashboard' });
624
- }
625
- }
626
-
627
- next();
628
156
  });
629
-
630
- export default router;
631
157
  ```
632
158
 
633
- ### 5. Form Handling with Validation
634
-
635
- ```vue
636
- <!-- src/views/auth/RegisterView.vue -->
637
- <script setup lang="ts">
638
- import { reactive, ref } from 'vue';
639
- import { useRouter } from 'vue-router';
640
- import { useUserStore } from '@/stores/user.store';
641
- import { useNotificationStore } from '@/stores/notification.store';
642
-
643
- interface FormState {
644
- email: string;
645
- password: string;
646
- confirmPassword: string;
647
- firstName: string;
648
- lastName: string;
649
- }
650
-
651
- interface FormErrors {
652
- email?: string;
653
- password?: string;
654
- confirmPassword?: string;
655
- firstName?: string;
656
- lastName?: string;
657
- }
658
-
659
- const router = useRouter();
660
- const userStore = useUserStore();
661
- const notifications = useNotificationStore();
662
-
663
- const form = reactive<FormState>({
664
- email: '',
665
- password: '',
666
- confirmPassword: '',
667
- firstName: '',
668
- lastName: '',
669
- });
670
-
671
- const errors = reactive<FormErrors>({});
672
- const submitting = ref(false);
673
-
674
- function validateEmail(email: string): string | undefined {
675
- if (!email) return 'Email is required';
676
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Invalid email format';
677
- }
678
-
679
- function validatePassword(password: string): string | undefined {
680
- if (!password) return 'Password is required';
681
- if (password.length < 8) return 'Password must be at least 8 characters';
682
- if (!/[A-Z]/.test(password)) return 'Password must contain uppercase letter';
683
- if (!/[0-9]/.test(password)) return 'Password must contain a number';
684
- }
685
-
686
- function validateForm(): boolean {
687
- errors.email = validateEmail(form.email);
688
- errors.password = validatePassword(form.password);
689
- errors.confirmPassword = form.password !== form.confirmPassword
690
- ? 'Passwords do not match'
691
- : undefined;
692
- errors.firstName = !form.firstName ? 'First name is required' : undefined;
693
- errors.lastName = !form.lastName ? 'Last name is required' : undefined;
694
-
695
- return !Object.values(errors).some(Boolean);
696
- }
697
-
698
- async function handleSubmit() {
699
- if (!validateForm()) return;
700
-
701
- submitting.value = true;
702
-
703
- try {
704
- await userStore.register({
705
- email: form.email,
706
- password: form.password,
707
- firstName: form.firstName,
708
- lastName: form.lastName,
709
- });
710
-
711
- notifications.success('Registration successful!');
712
- router.push({ name: 'dashboard' });
713
- } catch (error) {
714
- notifications.error((error as Error).message);
715
- } finally {
716
- submitting.value = false;
717
- }
718
- }
719
- </script>
720
-
721
- <template>
722
- <form @submit.prevent="handleSubmit" class="register-form">
723
- <h1>Create Account</h1>
724
-
725
- <div class="form-row">
726
- <div class="form-group">
727
- <label for="firstName">First Name</label>
728
- <input
729
- id="firstName"
730
- v-model="form.firstName"
731
- type="text"
732
- :class="{ error: errors.firstName }"
733
- />
734
- <span v-if="errors.firstName" class="error-message">
735
- {{ errors.firstName }}
736
- </span>
737
- </div>
738
-
739
- <div class="form-group">
740
- <label for="lastName">Last Name</label>
741
- <input
742
- id="lastName"
743
- v-model="form.lastName"
744
- type="text"
745
- :class="{ error: errors.lastName }"
746
- />
747
- <span v-if="errors.lastName" class="error-message">
748
- {{ errors.lastName }}
749
- </span>
750
- </div>
751
- </div>
752
-
753
- <div class="form-group">
754
- <label for="email">Email</label>
755
- <input
756
- id="email"
757
- v-model="form.email"
758
- type="email"
759
- :class="{ error: errors.email }"
760
- />
761
- <span v-if="errors.email" class="error-message">
762
- {{ errors.email }}
763
- </span>
764
- </div>
765
-
766
- <div class="form-group">
767
- <label for="password">Password</label>
768
- <input
769
- id="password"
770
- v-model="form.password"
771
- type="password"
772
- :class="{ error: errors.password }"
773
- />
774
- <span v-if="errors.password" class="error-message">
775
- {{ errors.password }}
776
- </span>
777
- </div>
778
-
779
- <div class="form-group">
780
- <label for="confirmPassword">Confirm Password</label>
781
- <input
782
- id="confirmPassword"
783
- v-model="form.confirmPassword"
784
- type="password"
785
- :class="{ error: errors.confirmPassword }"
786
- />
787
- <span v-if="errors.confirmPassword" class="error-message">
788
- {{ errors.confirmPassword }}
789
- </span>
790
- </div>
791
-
792
- <button type="submit" :disabled="submitting">
793
- {{ submitting ? 'Creating...' : 'Create Account' }}
794
- </button>
795
-
796
- <p class="login-link">
797
- Already have an account?
798
- <RouterLink :to="{ name: 'login' }">Sign in</RouterLink>
799
- </p>
800
- </form>
801
- </template>
802
- ```
803
-
804
- ### 6. Testing with Vitest
805
-
806
- ```typescript
807
- // src/components/__tests__/UserCard.spec.ts
808
- import { describe, it, expect, vi } from 'vitest';
809
- import { mount } from '@vue/test-utils';
810
- import UserCard from '@/components/UserCard.vue';
811
- import type { User } from '@/types';
812
-
813
- const mockUser: User = {
814
- id: '1',
815
- email: 'test@example.com',
816
- firstName: 'John',
817
- lastName: 'Doe',
818
- role: 'user',
819
- avatarUrl: null,
820
- createdAt: new Date().toISOString(),
821
- };
822
-
823
- describe('UserCard', () => {
824
- it('renders user information correctly', () => {
825
- const wrapper = mount(UserCard, {
826
- props: { user: mockUser },
827
- });
828
-
829
- expect(wrapper.text()).toContain('John Doe');
830
- expect(wrapper.text()).toContain('test@example.com');
831
- });
832
-
833
- it('emits edit event when edit button is clicked', async () => {
834
- const wrapper = mount(UserCard, {
835
- props: { user: mockUser },
836
- });
837
-
838
- await wrapper.find('button:first-child').trigger('click');
839
-
840
- expect(wrapper.emitted('edit')).toBeTruthy();
841
- expect(wrapper.emitted('edit')![0]).toEqual([mockUser]);
842
- });
843
-
844
- it('emits delete event with user id', async () => {
845
- const wrapper = mount(UserCard, {
846
- props: { user: mockUser },
847
- });
848
-
849
- const deleteButton = wrapper.findAll('button')[1];
850
- await deleteButton.trigger('click');
851
-
852
- expect(wrapper.emitted('delete')).toBeTruthy();
853
- expect(wrapper.emitted('delete')![0]).toEqual(['1']);
854
- });
855
-
856
- it('hides actions when showActions is false', () => {
857
- const wrapper = mount(UserCard, {
858
- props: { user: mockUser, showActions: false },
859
- });
860
-
861
- expect(wrapper.find('.user-card__actions').exists()).toBe(false);
862
- });
863
-
864
- it('toggles selection on click', async () => {
865
- const wrapper = mount(UserCard, {
866
- props: { user: mockUser },
867
- });
868
-
869
- await wrapper.trigger('click');
870
-
871
- expect(wrapper.emitted('select')).toBeTruthy();
872
- expect(wrapper.emitted('select')![0]).toEqual([mockUser, true]);
873
- expect(wrapper.classes()).toContain('user-card--selected');
874
- });
875
- });
876
-
877
-
878
- // src/stores/__tests__/user.store.spec.ts
879
- import { describe, it, expect, vi, beforeEach } from 'vitest';
880
- import { setActivePinia, createPinia } from 'pinia';
881
- import { useUserStore } from '@/stores/user.store';
882
- import { authService } from '@/services/auth.service';
883
-
884
- vi.mock('@/services/auth.service', () => ({
885
- authService: {
886
- login: vi.fn(),
887
- register: vi.fn(),
888
- getCurrentUser: vi.fn(),
889
- },
890
- }));
891
-
892
- describe('User Store', () => {
893
- beforeEach(() => {
894
- setActivePinia(createPinia());
895
- vi.clearAllMocks();
896
- });
897
-
898
- it('initial state is correct', () => {
899
- const store = useUserStore();
900
-
901
- expect(store.user).toBeNull();
902
- expect(store.isAuthenticated).toBe(false);
903
- });
904
-
905
- it('login sets user and token', async () => {
906
- const mockResponse = {
907
- user: { id: '1', email: 'test@example.com', role: 'user' },
908
- token: 'test-token',
909
- };
910
- vi.mocked(authService.login).mockResolvedValue(mockResponse);
911
-
912
- const store = useUserStore();
913
- await store.login({ email: 'test@example.com', password: 'password' });
914
-
915
- expect(store.user).toEqual(mockResponse.user);
916
- expect(store.token).toBe('test-token');
917
- expect(store.isAuthenticated).toBe(true);
918
- });
919
-
920
- it('logout clears user and token', async () => {
921
- const store = useUserStore();
922
- store.user = { id: '1', email: 'test@example.com' } as any;
923
- store.token = 'test-token';
924
-
925
- store.logout();
926
-
927
- expect(store.user).toBeNull();
928
- expect(store.token).toBeNull();
929
- expect(store.isAuthenticated).toBe(false);
930
- });
931
-
932
- it('handles login error', async () => {
933
- vi.mocked(authService.login).mockRejectedValue(new Error('Invalid credentials'));
934
-
935
- const store = useUserStore();
936
-
937
- await expect(
938
- store.login({ email: 'test@example.com', password: 'wrong' })
939
- ).rejects.toThrow('Invalid credentials');
940
-
941
- expect(store.error).toBe('Invalid credentials');
942
- });
943
- });
944
-
945
-
946
- // src/composables/__tests__/usePagination.spec.ts
947
- import { describe, it, expect } from 'vitest';
948
- import { usePagination } from '@/composables/usePagination';
949
-
950
- describe('usePagination', () => {
951
- it('initializes with default values', () => {
952
- const { page, limit, total } = usePagination();
953
-
954
- expect(page.value).toBe(1);
955
- expect(limit.value).toBe(20);
956
- expect(total.value).toBe(0);
957
- });
958
-
959
- it('calculates total pages correctly', () => {
960
- const { totalPages, setTotal } = usePagination({ initialLimit: 10 });
961
-
962
- setTotal(45);
963
-
964
- expect(totalPages.value).toBe(5);
965
- });
966
-
967
- it('navigates pages correctly', () => {
968
- const { page, nextPage, prevPage, setTotal } = usePagination();
969
- setTotal(100);
970
-
971
- nextPage();
972
- expect(page.value).toBe(2);
973
-
974
- nextPage();
975
- expect(page.value).toBe(3);
976
-
977
- prevPage();
978
- expect(page.value).toBe(2);
979
- });
980
-
981
- it('prevents navigation beyond bounds', () => {
982
- const { page, nextPage, prevPage, setTotal, limit } = usePagination();
983
- setTotal(30);
984
-
985
- prevPage();
986
- expect(page.value).toBe(1);
987
-
988
- page.value = 2;
989
- nextPage();
990
- expect(page.value).toBe(2);
991
- });
992
- });
993
- ```
994
-
995
- ### 7. API Service Layer
996
-
997
- ```typescript
998
- // src/services/api.service.ts
999
- import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';
1000
- import { useUserStore } from '@/stores/user.store';
1001
-
1002
- const api: AxiosInstance = axios.create({
1003
- baseURL: import.meta.env.VITE_API_URL,
1004
- timeout: 10000,
1005
- headers: {
1006
- 'Content-Type': 'application/json',
1007
- },
1008
- });
1009
-
1010
- // Request interceptor
1011
- api.interceptors.request.use((config) => {
1012
- const userStore = useUserStore();
1013
- if (userStore.token) {
1014
- config.headers.Authorization = `Bearer ${userStore.token}`;
1015
- }
1016
- return config;
1017
- });
1018
-
1019
- // Response interceptor
1020
- api.interceptors.response.use(
1021
- (response) => response,
1022
- (error) => {
1023
- if (error.response?.status === 401) {
1024
- const userStore = useUserStore();
1025
- userStore.logout();
1026
- window.location.href = '/auth/login';
1027
- }
1028
- return Promise.reject(error);
1029
- }
1030
- );
1031
-
1032
- export { api };
1033
-
1034
-
1035
- // src/services/user.service.ts
1036
- import { api } from './api.service';
1037
- import type { User, CreateUserData, UpdateUserData, PaginatedResponse } from '@/types';
1038
-
1039
- interface UserFilters {
1040
- search?: string;
1041
- role?: string;
1042
- page?: number;
1043
- limit?: number;
1044
- }
1045
-
1046
- export const userService = {
1047
- async getAll(filters: UserFilters = {}): Promise<PaginatedResponse<User>> {
1048
- const { data } = await api.get('/users', { params: filters });
1049
- return data;
1050
- },
1051
-
1052
- async getById(id: string): Promise<User> {
1053
- const { data } = await api.get(`/users/${id}`);
1054
- return data;
1055
- },
1056
-
1057
- async create(userData: CreateUserData): Promise<User> {
1058
- const { data } = await api.post('/users', userData);
1059
- return data;
1060
- },
1061
-
1062
- async update(id: string, userData: UpdateUserData): Promise<User> {
1063
- const { data } = await api.patch(`/users/${id}`, userData);
1064
- return data;
1065
- },
1066
-
1067
- async delete(id: string): Promise<void> {
1068
- await api.delete(`/users/${id}`);
1069
- },
1070
- };
1071
- ```
1072
-
1073
- ## Use Cases
1074
-
1075
- ### Data Table with Pagination and Filters
1076
-
1077
- ```vue
1078
- <!-- src/views/UsersView.vue -->
1079
- <script setup lang="ts">
1080
- import { ref, watch } from 'vue';
1081
- import { usePagination } from '@/composables/usePagination';
1082
- import { useAsync } from '@/composables/useAsync';
1083
- import { userService } from '@/services/user.service';
1084
- import UserCard from '@/components/UserCard.vue';
1085
- import type { User } from '@/types';
1086
-
1087
- const search = ref('');
1088
- const roleFilter = ref('');
1089
-
1090
- const {
1091
- page,
1092
- limit,
1093
- totalPages,
1094
- hasNextPage,
1095
- hasPrevPage,
1096
- nextPage,
1097
- prevPage,
1098
- setTotal,
1099
- } = usePagination({ initialLimit: 10 });
1100
-
1101
- const { data: users, loading, execute: fetchUsers } = useAsync(
1102
- () => userService.getAll({
1103
- search: search.value,
1104
- role: roleFilter.value,
1105
- page: page.value,
1106
- limit: limit.value,
1107
- })
1108
- );
1109
-
1110
- watch([search, roleFilter], () => {
1111
- page.value = 1;
1112
- fetchUsers();
1113
- });
1114
-
1115
- watch(page, fetchUsers);
1116
-
1117
- watch(users, (response) => {
1118
- if (response) setTotal(response.total);
1119
- });
1120
-
1121
- fetchUsers();
1122
-
1123
- function handleEdit(user: User) {
1124
- // Navigate to edit page
1125
- }
1126
-
1127
- function handleDelete(userId: string) {
1128
- // Confirm and delete
1129
- }
1130
- </script>
1131
-
1132
- <template>
1133
- <div class="users-view">
1134
- <header class="users-view__header">
1135
- <h1>Users</h1>
1136
- <RouterLink :to="{ name: 'user-create' }" class="btn btn--primary">
1137
- Add User
1138
- </RouterLink>
1139
- </header>
1140
-
1141
- <div class="users-view__filters">
1142
- <input
1143
- v-model="search"
1144
- type="search"
1145
- placeholder="Search users..."
1146
- />
1147
- <select v-model="roleFilter">
1148
- <option value="">All Roles</option>
1149
- <option value="admin">Admin</option>
1150
- <option value="user">User</option>
1151
- <option value="guest">Guest</option>
1152
- </select>
1153
- </div>
159
+ ## Best Practices
1154
160
 
1155
- <div v-if="loading" class="loading">Loading...</div>
161
+ | Do | Avoid |
162
+ |----|-------|
163
+ | Use Composition API with `<script setup>` | Options API in new code |
164
+ | Type props/emits with interfaces | `any` types |
165
+ | Create composables for shared logic | Duplicating reactive logic |
166
+ | Use Pinia for global state | Overusing provide/inject |
167
+ | Lazy load routes and components | Bundling everything upfront |
1156
168
 
1157
- <div v-else-if="users?.data.length" class="users-view__list">
1158
- <UserCard
1159
- v-for="user in users.data"
1160
- :key="user.id"
1161
- :user="user"
1162
- @edit="handleEdit"
1163
- @delete="handleDelete"
1164
- />
1165
- </div>
169
+ ## Project Structure
1166
170
 
1167
- <div v-else class="empty">No users found</div>
1168
-
1169
- <div class="pagination">
1170
- <button :disabled="!hasPrevPage" @click="prevPage">Previous</button>
1171
- <span>Page {{ page }} of {{ totalPages }}</span>
1172
- <button :disabled="!hasNextPage" @click="nextPage">Next</button>
1173
- </div>
1174
- </div>
1175
- </template>
1176
171
  ```
1177
-
1178
- ### Modal Component with Teleport
1179
-
1180
- ```vue
1181
- <!-- src/components/Modal.vue -->
1182
- <script setup lang="ts">
1183
- import { ref, watch, onMounted, onUnmounted } from 'vue';
1184
-
1185
- interface Props {
1186
- modelValue: boolean;
1187
- title?: string;
1188
- persistent?: boolean;
1189
- }
1190
-
1191
- const props = withDefaults(defineProps<Props>(), {
1192
- persistent: false,
1193
- });
1194
-
1195
- const emit = defineEmits<{
1196
- (e: 'update:modelValue', value: boolean): void;
1197
- (e: 'close'): void;
1198
- }>();
1199
-
1200
- const modalRef = ref<HTMLDivElement | null>(null);
1201
-
1202
- function close() {
1203
- emit('update:modelValue', false);
1204
- emit('close');
1205
- }
1206
-
1207
- function handleBackdropClick() {
1208
- if (!props.persistent) close();
1209
- }
1210
-
1211
- function handleEscape(event: KeyboardEvent) {
1212
- if (event.key === 'Escape' && props.modelValue && !props.persistent) {
1213
- close();
1214
- }
1215
- }
1216
-
1217
- onMounted(() => {
1218
- document.addEventListener('keydown', handleEscape);
1219
- });
1220
-
1221
- onUnmounted(() => {
1222
- document.removeEventListener('keydown', handleEscape);
1223
- });
1224
-
1225
- watch(() => props.modelValue, (isOpen) => {
1226
- document.body.style.overflow = isOpen ? 'hidden' : '';
1227
- });
1228
- </script>
1229
-
1230
- <template>
1231
- <Teleport to="body">
1232
- <Transition name="modal">
1233
- <div
1234
- v-if="modelValue"
1235
- class="modal-backdrop"
1236
- @click="handleBackdropClick"
1237
- >
1238
- <div
1239
- ref="modalRef"
1240
- class="modal"
1241
- role="dialog"
1242
- aria-modal="true"
1243
- @click.stop
1244
- >
1245
- <header v-if="title || $slots.header" class="modal__header">
1246
- <slot name="header">
1247
- <h2>{{ title }}</h2>
1248
- </slot>
1249
- <button class="modal__close" @click="close">&times;</button>
1250
- </header>
1251
-
1252
- <div class="modal__body">
1253
- <slot />
1254
- </div>
1255
-
1256
- <footer v-if="$slots.footer" class="modal__footer">
1257
- <slot name="footer" :close="close" />
1258
- </footer>
1259
- </div>
1260
- </div>
1261
- </Transition>
1262
- </Teleport>
1263
- </template>
1264
-
1265
- <style scoped>
1266
- .modal-backdrop {
1267
- position: fixed;
1268
- inset: 0;
1269
- background: rgba(0, 0, 0, 0.5);
1270
- display: flex;
1271
- align-items: center;
1272
- justify-content: center;
1273
- z-index: 1000;
1274
- }
1275
-
1276
- .modal {
1277
- background: white;
1278
- border-radius: 8px;
1279
- max-width: 500px;
1280
- width: 90%;
1281
- max-height: 90vh;
1282
- overflow: auto;
1283
- }
1284
-
1285
- .modal-enter-active,
1286
- .modal-leave-active {
1287
- transition: opacity 0.3s ease;
1288
- }
1289
-
1290
- .modal-enter-from,
1291
- .modal-leave-to {
1292
- opacity: 0;
1293
- }
1294
- </style>
172
+ src/
173
+ ├── App.vue
174
+ ├── main.ts
175
+ ├── components/ # Reusable components
176
+ ├── composables/ # Composition functions
177
+ ├── views/ # Page components
178
+ ├── stores/ # Pinia stores
179
+ ├── router/ # Route definitions
180
+ ├── services/ # API services
181
+ └── types/ # TypeScript types
1295
182
  ```
1296
183
 
1297
- ## Best Practices
1298
-
1299
- ### Do's
1300
-
1301
- - Use Composition API with `<script setup>`
1302
- - Use TypeScript for type safety
1303
- - Create composables for reusable logic
1304
- - Use Pinia for global state management
1305
- - Use Vue Router for navigation
1306
- - Implement proper error boundaries
1307
- - Write unit tests with Vitest
1308
- - Use lazy loading for routes
1309
- - Follow single responsibility principle
1310
- - Use provide/inject for deep prop drilling
1311
-
1312
- ### Don'ts
1313
-
1314
- - Don't use Options API in new code
1315
- - Don't mutate props directly
1316
- - Don't use `this` in Composition API
1317
- - Don't overuse global state
1318
- - Don't skip error handling
1319
- - Don't ignore TypeScript errors
1320
- - Don't use `any` type
1321
- - Don't create memory leaks in watchers
1322
- - Don't skip component testing
1323
- - Don't mix template refs with reactive refs
1324
-
1325
- ## References
1326
-
1327
- - [Vue 3 Documentation](https://vuejs.org/)
1328
- - [Pinia Documentation](https://pinia.vuejs.org/)
1329
- - [Vue Router Documentation](https://router.vuejs.org/)
1330
- - [Vitest Documentation](https://vitest.dev/)
1331
- - [VueUse Composables](https://vueuse.org/)
184
+ For detailed examples and patterns, see reference files above.