omgkit 2.1.1 → 2.2.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 (50) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/SKILL_STANDARDS.md +743 -0
  3. package/plugin/skills/databases/mongodb/SKILL.md +797 -28
  4. package/plugin/skills/databases/prisma/SKILL.md +776 -30
  5. package/plugin/skills/databases/redis/SKILL.md +885 -25
  6. package/plugin/skills/devops/aws/SKILL.md +686 -28
  7. package/plugin/skills/devops/github-actions/SKILL.md +684 -29
  8. package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
  9. package/plugin/skills/frameworks/django/SKILL.md +920 -20
  10. package/plugin/skills/frameworks/express/SKILL.md +1361 -35
  11. package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
  12. package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
  13. package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
  14. package/plugin/skills/frameworks/rails/SKILL.md +594 -28
  15. package/plugin/skills/frameworks/spring/SKILL.md +528 -35
  16. package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
  17. package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
  18. package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
  19. package/plugin/skills/frontend/responsive/SKILL.md +847 -21
  20. package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
  21. package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
  22. package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
  23. package/plugin/skills/languages/javascript/SKILL.md +935 -31
  24. package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
  25. package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
  26. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
  27. package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
  28. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
  29. package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
  30. package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
  31. package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
  32. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
  33. package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
  34. package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
  35. package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
  36. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
  37. package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
  38. package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
  39. package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
  40. package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
  41. package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
  42. package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
  43. package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
  44. package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
  45. package/plugin/skills/security/better-auth/SKILL.md +1065 -28
  46. package/plugin/skills/security/oauth/SKILL.md +968 -31
  47. package/plugin/skills/security/owasp/SKILL.md +894 -33
  48. package/plugin/skills/testing/playwright/SKILL.md +764 -38
  49. package/plugin/skills/testing/pytest/SKILL.md +873 -36
  50. package/plugin/skills/testing/vitest/SKILL.md +980 -35
@@ -1,62 +1,1331 @@
1
1
  ---
2
2
  name: vue
3
- description: Vue.js development. Use for Vue 3 projects, Composition API, Pinia.
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
4
15
  ---
5
16
 
6
- # Vue.js Skill
17
+ # Vue.js
7
18
 
8
- ## Composition API
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.
20
+
21
+ ## Purpose
22
+
23
+ Build reactive, maintainable Vue applications with confidence:
24
+
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
32
+
33
+ ## Features
34
+
35
+ ### 1. Component Architecture with TypeScript
9
36
 
10
- ### Component
11
37
  ```vue
38
+ <!-- src/components/UserCard.vue -->
12
39
  <script setup lang="ts">
13
- import { ref, computed } from 'vue';
40
+ import { computed, ref, watch, onMounted } from 'vue';
41
+ import type { User, UserRole } from '@/types';
14
42
 
15
- const count = ref(0);
16
- const doubled = computed(() => count.value * 2);
43
+ // Props with TypeScript
44
+ interface Props {
45
+ user: User;
46
+ showActions?: boolean;
47
+ variant?: 'default' | 'compact' | 'detailed';
48
+ }
49
+
50
+ const props = withDefaults(defineProps<Props>(), {
51
+ showActions: true,
52
+ variant: 'default',
53
+ });
17
54
 
18
- function increment() {
19
- count.value++;
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;
20
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
+ });
101
+
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
+ });
21
120
  </script>
22
121
 
23
122
  <template>
24
- <button @click="increment">{{ count }} ({{ doubled }})</button>
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>
147
+ </div>
148
+
149
+ <slot name="footer" :user="user" :selected="isSelected" />
150
+ </div>
25
151
  </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>
26
178
  ```
27
179
 
28
- ### Composable
180
+ ### 2. Composables (Reusable Logic)
181
+
29
182
  ```typescript
30
- // composables/useUser.ts
31
- export function useUser(id: Ref<string>) {
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;
195
+
32
196
  const user = ref<User | null>(null);
33
- const loading = ref(true);
197
+ const loading = ref(false);
198
+ const error = ref<Error | null>(null);
199
+
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
+ }
34
210
 
35
- watch(id, async (newId) => {
36
211
  loading.value = true;
37
- user.value = await fetchUser(newId);
38
- loading.value = false;
39
- }, { immediate: true });
212
+ error.value = null;
40
213
 
41
- return { user, loading };
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
+ try {
229
+ user.value = await userService.update(userId.value, data);
230
+ } catch (e) {
231
+ error.value = e as Error;
232
+ throw e;
233
+ } finally {
234
+ loading.value = false;
235
+ }
236
+ }
237
+
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
+
251
+
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
+ };
42
356
  }
43
357
  ```
44
358
 
45
- ### Pinia Store
359
+ ### 3. Pinia State Management
360
+
46
361
  ```typescript
362
+ // src/stores/user.store.ts
363
+ import { defineStore } from 'pinia';
364
+ 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
+
47
369
  export const useUserStore = defineStore('user', () => {
370
+ // State
48
371
  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
+ );
49
382
 
50
- async function login(email: string, password: string) {
51
- user.value = await api.login(email, password);
383
+ // Actions
384
+ async function login(credentials: LoginCredentials) {
385
+ loading.value = true;
386
+ error.value = null;
387
+
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
+ }
427
+ }
428
+
429
+ function logout() {
430
+ user.value = null;
431
+ token.value = null;
432
+ }
433
+
434
+ function $reset() {
435
+ user.value = null;
436
+ token.value = null;
437
+ loading.value = false;
438
+ error.value = null;
52
439
  }
53
440
 
54
- return { user, login };
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
+ };
55
458
  });
459
+
460
+
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
+ }
514
+
515
+ return {
516
+ notifications,
517
+ add,
518
+ remove,
519
+ success,
520
+ error,
521
+ warning,
522
+ info,
523
+ };
524
+ });
525
+ ```
526
+
527
+ ### 4. Vue Router Configuration
528
+
529
+ ```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
+ },
583
+ {
584
+ path: '/:pathMatch(.*)*',
585
+ name: 'not-found',
586
+ component: () => import('@/views/NotFoundView.vue'),
587
+ },
588
+ ];
589
+
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) => {
602
+ 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
+ if (to.meta.requiresAuth && !userStore.isAuthenticated) {
611
+ return next({ name: 'login', query: { redirect: to.fullPath } });
612
+ }
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
+ });
629
+
630
+ export default router;
631
+ ```
632
+
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>
1154
+
1155
+ <div v-if="loading" class="loading">Loading...</div>
1156
+
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>
1166
+
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
+ ```
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>
56
1295
  ```
57
1296
 
58
1297
  ## Best Practices
59
- - Use Composition API
60
- - Use `<script setup>`
61
- - Create composables for logic
62
- - Use Pinia for state
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/)