symfonia-ai-tools 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +489 -0
- package/bin/cli.mjs +35 -0
- package/lib/installer.mjs +495 -0
- package/lib/questions.mjs +332 -0
- package/lib/ui.mjs +76 -0
- package/lib/utils.mjs +231 -0
- package/package.json +26 -0
- package/templates/base/CLAUDE.md +34 -0
- package/templates/base/_ai/_guidelines_header.md +70 -0
- package/templates/base/_ai/context/README.md +20 -0
- package/templates/base/_ai/prompts/codereview.prompt.md +324 -0
- package/templates/base/_ai/prompts/duplicate-code-analysis.prompt.md +128 -0
- package/templates/base/_ai/prompts/figma-analysis.prompt.md +155 -0
- package/templates/base/_ai/prompts/security-review.prompt.md +46 -0
- package/templates/base/_ai/skills/README.md +80 -0
- package/templates/base/_ai/skills/TEMPLATE.md +106 -0
- package/templates/base/_ai/skills/babysit-prs/SKILL.md +105 -0
- package/templates/base/_ai/skills/debug/SKILL.md +93 -0
- package/templates/base/_ai/skills/fill-worklogs/SKILL.md +158 -0
- package/templates/base/_ai/skills/hotfix/SKILL.md +52 -0
- package/templates/base/_ai/skills/jira-task/SKILL.md +170 -0
- package/templates/base/_ai/skills/my-prs/SKILL.md +78 -0
- package/templates/base/_ai/skills/pr-dashboard/SKILL.md +43 -0
- package/templates/base/_ai/skills/pr-prepare/SKILL.md +106 -0
- package/templates/base/_ai/skills/refactor/SKILL.md +87 -0
- package/templates/base/_ai/skills/write-tests/SKILL.md +109 -0
- package/templates/base/_claude/settings.local.json +37 -0
- package/templates/base/_cursor/rules/global.mdc +7 -0
- package/templates/base/_editorconfig +18 -0
- package/templates/base/_gemini/settings.json +3 -0
- package/templates/base/_github/copilot-instructions.md +1 -0
- package/templates/base/_github/pull_request_template.md +23 -0
- package/templates/base/_gitignore +22 -0
- package/templates/base/_junie/guidelines.md +1 -0
- package/templates/base/commit-instructions.md +92 -0
- package/templates/packs/docker/_ai/instructions/docker.instructions.md +193 -0
- package/templates/packs/docker/_guidelines.md +10 -0
- package/templates/packs/docker/pack.json +8 -0
- package/templates/packs/laravel/_ai/instructions/api-resource.instructions.md +251 -0
- package/templates/packs/laravel/_ai/instructions/module.instructions.md +133 -0
- package/templates/packs/laravel/_ai/instructions/service-repository.instructions.md +215 -0
- package/templates/packs/laravel/_ai/instructions/testing.instructions.md +278 -0
- package/templates/packs/laravel/_ai/skills/migration/SKILL.md +172 -0
- package/templates/packs/laravel/_ai/skills/new-endpoint/SKILL.md +165 -0
- package/templates/packs/laravel/_ai/skills/new-module/SKILL.md +208 -0
- package/templates/packs/laravel/_ai/skills/queued-job/SKILL.md +248 -0
- package/templates/packs/laravel/_ai/skills/testing-feature/SKILL.md +196 -0
- package/templates/packs/laravel/_ai/skills/testing-manual/SKILL.md +186 -0
- package/templates/packs/laravel/_ai/skills/testing-unit/SKILL.md +200 -0
- package/templates/packs/laravel/_guidelines.md +25 -0
- package/templates/packs/laravel/pack.json +6 -0
- package/templates/packs/playwright/_ai/instructions/playwright.instructions.md +219 -0
- package/templates/packs/playwright/_ai/skills/playwright/README.md +194 -0
- package/templates/packs/playwright/_ai/skills/playwright/SKILL.md +1245 -0
- package/templates/packs/playwright/_ai/skills/playwright-codereview/SKILL.md +642 -0
- package/templates/packs/playwright/_ai/skills/playwright-record/README.md +87 -0
- package/templates/packs/playwright/_ai/skills/playwright-record/SKILL.md +564 -0
- package/templates/packs/playwright/_guidelines.md +12 -0
- package/templates/packs/playwright/pack.json +9 -0
- package/templates/packs/storybook/_ai/instructions/storybook.instructions.md +181 -0
- package/templates/packs/storybook/pack.json +6 -0
- package/templates/packs/vitest/_ai/instructions/vitest.instructions.md +688 -0
- package/templates/packs/vitest/pack.json +6 -0
- package/templates/packs/vue3/_ai/instructions/api.instructions.md +163 -0
- package/templates/packs/vue3/_ai/instructions/coding-conventions.instructions.md +160 -0
- package/templates/packs/vue3/_ai/instructions/composables.instructions.md +218 -0
- package/templates/packs/vue3/_ai/instructions/forms.instructions.md +227 -0
- package/templates/packs/vue3/_ai/instructions/store.instructions.md +504 -0
- package/templates/packs/vue3/_ai/instructions/vue.instructions.md +339 -0
- package/templates/packs/vue3/_ai/skills/api-integration/SKILL.md +195 -0
- package/templates/packs/vue3/_ai/skills/new-component/SKILL.md +133 -0
- package/templates/packs/vue3/_ai/skills/new-module/SKILL.md +177 -0
- package/templates/packs/vue3/_guidelines.md +45 -0
- package/templates/packs/vue3/pack.json +11 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*Service*.ts,**/*service*.ts,**/api/**,**/composables/useApi*"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# API & Services - Instrukcje
|
|
6
|
+
|
|
7
|
+
## Architektura warstwy API
|
|
8
|
+
|
|
9
|
+
### Composable API (singleton)
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// composables/useApi.ts
|
|
13
|
+
import axios, { type AxiosInstance, type AxiosError, type AxiosRequestConfig } from 'axios';
|
|
14
|
+
|
|
15
|
+
let apiInstance: AxiosInstance | null = null;
|
|
16
|
+
|
|
17
|
+
export function useApi() {
|
|
18
|
+
if (!apiInstance) {
|
|
19
|
+
apiInstance = axios.create({
|
|
20
|
+
baseURL: import.meta.env.VITE_API_URL || '{{API_BASE_URL}}',
|
|
21
|
+
timeout: Number(import.meta.env.VITE_API_TIMEOUT) || 30000,
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
setupInterceptors(apiInstance);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
get: <T>(url: string, config?: AxiosRequestConfig) => apiInstance!.get<T>(url, config),
|
|
30
|
+
post: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => apiInstance!.post<T>(url, data, config),
|
|
31
|
+
put: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => apiInstance!.put<T>(url, data, config),
|
|
32
|
+
patch: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => apiInstance!.patch<T>(url, data, config),
|
|
33
|
+
delete: <T>(url: string, config?: AxiosRequestConfig) => apiInstance!.delete<T>(url, config),
|
|
34
|
+
setAuthToken,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Serwisy modulowe (functional pattern)
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// modules/[feature]/services/Feature.service.ts
|
|
43
|
+
import { useApi } from '@/composables/useApi';
|
|
44
|
+
|
|
45
|
+
const api = useApi();
|
|
46
|
+
const BASE = '/features';
|
|
47
|
+
|
|
48
|
+
const FeatureService = {
|
|
49
|
+
getAll: () => api.get<IFeature[]>(BASE),
|
|
50
|
+
getById: (id: number) => api.get<IFeature>(`${BASE}/${id}`),
|
|
51
|
+
create: (data: IFeatureCreate) => api.post<IFeature>(BASE, data),
|
|
52
|
+
update: (id: number, data: IFeatureUpdate) => api.put<IFeature>(`${BASE}/${id}`, data),
|
|
53
|
+
delete: (id: number) => api.delete<void>(`${BASE}/${id}`),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default FeatureService;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Interceptory
|
|
60
|
+
|
|
61
|
+
### Request interceptor
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
function setupInterceptors(instance: AxiosInstance) {
|
|
65
|
+
instance.interceptors.request.use((config) => {
|
|
66
|
+
const token = getAuthToken();
|
|
67
|
+
if (token) {
|
|
68
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Accept-Language z i18n
|
|
72
|
+
const locale = getCurrentLocale();
|
|
73
|
+
if (locale) {
|
|
74
|
+
config.headers['Accept-Language'] = locale;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return config;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
instance.interceptors.response.use(
|
|
81
|
+
(response) => response,
|
|
82
|
+
(error: AxiosError) => handleApiError(error),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Obsluga bledow
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
function handleApiError(error: AxiosError): Promise<never> {
|
|
91
|
+
const status = error.response?.status;
|
|
92
|
+
|
|
93
|
+
switch (status) {
|
|
94
|
+
case 400: notify('error', 'Nieprawidlowe dane'); break;
|
|
95
|
+
case 401: redirectToLogin(); break;
|
|
96
|
+
case 403: notify('error', 'Brak uprawnien'); break;
|
|
97
|
+
case 404: notify('error', 'Nie znaleziono zasobu'); break;
|
|
98
|
+
case 409: notify('error', 'Konflikt danych'); break;
|
|
99
|
+
case 422: break; // Walidacja - obslugiwana przez formularz
|
|
100
|
+
case 500: notify('error', 'Blad serwera'); break;
|
|
101
|
+
default: notify('error', 'Wystapil nieoczekiwany blad');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Promise.reject(error);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Opcja returnErrors
|
|
109
|
+
|
|
110
|
+
Dla formularzy - zwraca bledy zamiast null:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// W serwisie
|
|
114
|
+
const FeatureService = {
|
|
115
|
+
create: (data: IFeatureCreate, returnErrors = false) => {
|
|
116
|
+
return api.post<IFeature>('/features', data)
|
|
117
|
+
.catch((error: AxiosError) => {
|
|
118
|
+
if (returnErrors) return error;
|
|
119
|
+
return null;
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// W uzyciu
|
|
125
|
+
const error = await FeatureService.create(formData, true);
|
|
126
|
+
if (error instanceof AxiosError && error.response?.status === 422) {
|
|
127
|
+
validationErrors.value = error.response.data.errors;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Integracja z Pinia Store
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// stores/feature.store.ts
|
|
135
|
+
export const useFeatureStore = defineStore('feature', () => {
|
|
136
|
+
const items = ref<IFeature[]>([]);
|
|
137
|
+
const loading = ref(false);
|
|
138
|
+
|
|
139
|
+
async function fetchAll() {
|
|
140
|
+
loading.value = true;
|
|
141
|
+
try {
|
|
142
|
+
const { data } = await FeatureService.getAll();
|
|
143
|
+
items.value = data;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
printError(error);
|
|
146
|
+
} finally {
|
|
147
|
+
loading.value = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { items, loading, fetchAll };
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Zasady
|
|
156
|
+
|
|
157
|
+
1. **Jeden serwis per modul** - plik `ModuleName.service.ts`
|
|
158
|
+
2. **Functional pattern** - obiekt z metodami, nie klasa
|
|
159
|
+
3. **Typowanie** - kazda metoda typuje request i response
|
|
160
|
+
4. **Nie wywoluj API bezposrednio z komponentu** - zawsze przez serwis
|
|
161
|
+
5. **Error handling** - interceptor obsluguje globalne bledy, serwis moze dodac `returnErrors`
|
|
162
|
+
6. **Zmienne srodowiskowe** - `VITE_API_URL`, `VITE_API_TIMEOUT` w `.env`
|
|
163
|
+
7. **Brak console.log** - uzyj `printError()` dla bledow
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.vue,**/*.ts,**/*.scss"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Coding Conventions - Instrukcje
|
|
6
|
+
|
|
7
|
+
## Nazewnictwo
|
|
8
|
+
|
|
9
|
+
### Pliki i katalogi
|
|
10
|
+
|
|
11
|
+
| Element | Konwencja | Przyklad |
|
|
12
|
+
|---------|-----------|----------|
|
|
13
|
+
| Komponenty Vue | PascalCase | `UserProfile.vue` |
|
|
14
|
+
| Composables | camelCase z prefixem `use` | `useUserForm.ts` |
|
|
15
|
+
| Serwisy | PascalCase + `.service.ts` | `User.service.ts` |
|
|
16
|
+
| Store | camelCase + `.store.ts` | `user.store.ts` |
|
|
17
|
+
| Typy/Interfejsy | PascalCase + `.types.ts` | `User.types.ts` |
|
|
18
|
+
| Testy | nazwa + `.test.ts` | `UserProfile.test.ts` |
|
|
19
|
+
| Stale | camelCase + `consts.ts` | `userConsts.ts` |
|
|
20
|
+
|
|
21
|
+
### Zmienne i funkcje
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// camelCase dla zmiennych i funkcji
|
|
25
|
+
const userName = ref('');
|
|
26
|
+
const isLoading = ref(false);
|
|
27
|
+
function fetchUserData() { /* ... */ }
|
|
28
|
+
|
|
29
|
+
// PascalCase dla komponentow i klas
|
|
30
|
+
import UserProfile from './UserProfile.vue';
|
|
31
|
+
|
|
32
|
+
// UPPERCASE dla stalych globalnych
|
|
33
|
+
const MAX_RETRY_COUNT = 3;
|
|
34
|
+
const API_TIMEOUT = 30000;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### TypeScript - prefixy typow
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// I - Interface
|
|
41
|
+
interface IUser {
|
|
42
|
+
id: number;
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// E - Enum
|
|
47
|
+
enum EUserRole {
|
|
48
|
+
ADMIN = 'admin',
|
|
49
|
+
USER = 'user',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// T - Type alias
|
|
53
|
+
type TUserFilter = 'active' | 'inactive' | 'all';
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### CSS klasy
|
|
57
|
+
|
|
58
|
+
```scss
|
|
59
|
+
// Single dash - NIE uzywaj BEM (__) ani double dash (--)
|
|
60
|
+
.user-profile { }
|
|
61
|
+
.user-profile-header { }
|
|
62
|
+
.user-profile-avatar { }
|
|
63
|
+
|
|
64
|
+
// ZLE:
|
|
65
|
+
.user-profile__header { } // BEM
|
|
66
|
+
.user-profile--active { } // BEM modifier
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### data-testid
|
|
70
|
+
|
|
71
|
+
```vue
|
|
72
|
+
<!-- camelCase, opisowy -->
|
|
73
|
+
<button :data-testid="'userProfileSaveButton'">Zapisz</button>
|
|
74
|
+
<input :data-testid="'userProfileEmailInput'" />
|
|
75
|
+
<div :data-testid="'userProfileCard'" />
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Komentarze i dokumentacja
|
|
79
|
+
|
|
80
|
+
### Zasady komentowania
|
|
81
|
+
|
|
82
|
+
1. **Kod ma byc samodokumentujacy** - nazwy zmiennych i funkcji powinny wyjasnic "co"
|
|
83
|
+
2. **Komentarze tylko gdy "dlaczego"** - nie opisuj co robi kod, opisz dlaczego tak a nie inaczej
|
|
84
|
+
3. **JSDoc** - tylko dla publicznego API i composables:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
/**
|
|
88
|
+
* Composable do zarzadzania formularzem uzytkownika.
|
|
89
|
+
* Obsluguje walidacje, submit i reset.
|
|
90
|
+
*/
|
|
91
|
+
export function useUserForm(userId?: number) {
|
|
92
|
+
// ...
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
4. **Zakaz komentarzy HTML** w szablonach Vue (`<!-- -->`)
|
|
97
|
+
5. **Zakaz komentarzy CSS** w stylach (`/* */`)
|
|
98
|
+
|
|
99
|
+
## Console
|
|
100
|
+
|
|
101
|
+
### BEZWZGLEDNY ZAKAZ console.log w kodzie produkcyjnym
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// ZLE - nigdy nie commituj console.log
|
|
105
|
+
console.log('user:', user);
|
|
106
|
+
console.warn('deprecated');
|
|
107
|
+
|
|
108
|
+
// DOBRZE - uzywaj dedykowanej funkcji
|
|
109
|
+
import { printError } from '@/composables/useShowError';
|
|
110
|
+
printError(error);
|
|
111
|
+
|
|
112
|
+
// LUB - jesli potrzebujesz debug info, usun przed commitem
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Struktura komponentu Vue
|
|
116
|
+
|
|
117
|
+
Kolejnosc sekcji w pliku `.vue`:
|
|
118
|
+
|
|
119
|
+
```vue
|
|
120
|
+
<template>
|
|
121
|
+
<!-- 1. Template -->
|
|
122
|
+
</template>
|
|
123
|
+
|
|
124
|
+
<script setup lang="ts">
|
|
125
|
+
// 2. Script setup
|
|
126
|
+
// Kolejnosc wewnatrz:
|
|
127
|
+
// a) importy
|
|
128
|
+
// b) interface props/emits
|
|
129
|
+
// c) defineProps / defineEmits
|
|
130
|
+
// d) inject / provide
|
|
131
|
+
// e) ref / reactive
|
|
132
|
+
// f) computed
|
|
133
|
+
// g) watch
|
|
134
|
+
// h) metody
|
|
135
|
+
// i) lifecycle hooks (onMounted, onUnmounted)
|
|
136
|
+
// j) defineExpose (jesli potrzebny)
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
<style scoped lang="scss">
|
|
140
|
+
// 3. Scoped styles
|
|
141
|
+
</style>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Code Review Checklist
|
|
145
|
+
|
|
146
|
+
Przed kazdym commitem zweryfikuj:
|
|
147
|
+
|
|
148
|
+
1. [ ] **TypeScript** - brak `any`, poprawne typy, interfejsy z prefixem `I`
|
|
149
|
+
2. [ ] **Nazewnictwo** - camelCase zmienne, PascalCase komponenty, single-dash CSS
|
|
150
|
+
3. [ ] **Console** - brak `console.log/warn/error` (uzyj `printError()`)
|
|
151
|
+
4. [ ] **Komentarze** - brak zbednych, brak HTML/CSS komentarzy
|
|
152
|
+
5. [ ] **data-testid** - camelCase, dodane do interaktywnych elementow
|
|
153
|
+
6. [ ] **Props** - typowane przez interface, domyslne wartosci przez `withDefaults`
|
|
154
|
+
7. [ ] **Emits** - typowane przez interface
|
|
155
|
+
8. [ ] **Store** - plik `.store.ts`, `defineStore` z composition syntax
|
|
156
|
+
9. [ ] **API** - przez serwis, nie bezposrednio z komponentu
|
|
157
|
+
10. [ ] **Error handling** - `printError()`, nie `console.error()`
|
|
158
|
+
11. [ ] **Cleanup** - `onUnmounted` czysci listenery, timery, subskrypcje
|
|
159
|
+
12. [ ] **Testy** - nowy kod ma testy, istniejace testy przechodza
|
|
160
|
+
13. [ ] **Style** - `scoped`, brak `font-family` (poza monospace), single-dash klasy
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/composables/**,**/use*.ts"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Composables - Instrukcje
|
|
6
|
+
|
|
7
|
+
## Konwencje
|
|
8
|
+
|
|
9
|
+
- Nazwa pliku: `use[Feature].ts` (camelCase z prefixem `use`)
|
|
10
|
+
- Lokalizacja: `{{MODULE_PATH}}composables/` lub `/src/composables/` (globalne)
|
|
11
|
+
- Eksport: named function `export function use[Feature]()`
|
|
12
|
+
- Zwraca obiekt z reaktywnymi wartosciami i metodami
|
|
13
|
+
|
|
14
|
+
## Wzorzec useAsyncDataLoader
|
|
15
|
+
|
|
16
|
+
Uniwersalny composable do operacji asynchronicznych:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// composables/useAsyncDataLoader.ts
|
|
20
|
+
import { ref, type Ref } from 'vue';
|
|
21
|
+
|
|
22
|
+
interface IAsyncDataLoaderOptions<T> {
|
|
23
|
+
onSuccess?: (data: T) => void;
|
|
24
|
+
onError?: (error: unknown) => void;
|
|
25
|
+
throwOnError?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useAsyncDataLoader<T>(
|
|
29
|
+
fetchFn: () => Promise<T>,
|
|
30
|
+
options?: IAsyncDataLoaderOptions<T>,
|
|
31
|
+
) {
|
|
32
|
+
const loading = ref(false);
|
|
33
|
+
const data: Ref<T | null> = ref(null);
|
|
34
|
+
const error: Ref<Error | null> = ref(null);
|
|
35
|
+
|
|
36
|
+
async function execute() {
|
|
37
|
+
loading.value = true;
|
|
38
|
+
error.value = null;
|
|
39
|
+
try {
|
|
40
|
+
const result = await fetchFn();
|
|
41
|
+
data.value = result;
|
|
42
|
+
options?.onSuccess?.(result);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
45
|
+
options?.onError?.(err);
|
|
46
|
+
if (options?.throwOnError) throw err;
|
|
47
|
+
} finally {
|
|
48
|
+
loading.value = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function reset() {
|
|
53
|
+
data.value = null;
|
|
54
|
+
loading.value = false;
|
|
55
|
+
error.value = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { loading, data, error, execute, reset };
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Uzycie
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// W composable modulu
|
|
66
|
+
export function useUsers() {
|
|
67
|
+
const { loading, data, execute } = useAsyncDataLoader(
|
|
68
|
+
() => UserService.getAll(),
|
|
69
|
+
{
|
|
70
|
+
onSuccess: (users) => store.setUsers(users),
|
|
71
|
+
onError: (err) => printError(err),
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return { loading, users: data, fetchUsers: execute };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// W komponencie
|
|
79
|
+
const { loading, users, fetchUsers } = useUsers();
|
|
80
|
+
onMounted(() => fetchUsers());
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Wzorzec composable z formularzem
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// composables/useFeatureForm.ts
|
|
87
|
+
export function useFeatureForm(itemId?: number) {
|
|
88
|
+
const form = reactive<IFeatureForm>({
|
|
89
|
+
name: '',
|
|
90
|
+
description: '',
|
|
91
|
+
status: 'draft',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const errors = ref<Record<string, string[]>>({});
|
|
95
|
+
const submitting = ref(false);
|
|
96
|
+
|
|
97
|
+
function resetForm() {
|
|
98
|
+
Object.assign(form, { name: '', description: '', status: 'draft' });
|
|
99
|
+
errors.value = {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function loadItem() {
|
|
103
|
+
if (!itemId) return;
|
|
104
|
+
const { data } = await FeatureService.getById(itemId);
|
|
105
|
+
Object.assign(form, data);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function submitForm() {
|
|
109
|
+
submitting.value = true;
|
|
110
|
+
errors.value = {};
|
|
111
|
+
try {
|
|
112
|
+
if (itemId) {
|
|
113
|
+
await FeatureService.update(itemId, form);
|
|
114
|
+
} else {
|
|
115
|
+
await FeatureService.create(form);
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof AxiosError && error.response?.status === 422) {
|
|
120
|
+
errors.value = error.response.data.errors;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
} finally {
|
|
124
|
+
submitting.value = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { form, errors, submitting, resetForm, loadItem, submitForm };
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Wzorzec composable z filtrowaniem
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// composables/useFeatureFilters.ts
|
|
136
|
+
export function useFeatureFilters(items: Ref<IFeature[]>) {
|
|
137
|
+
const searchQuery = ref('');
|
|
138
|
+
const statusFilter = ref<TStatus | null>(null);
|
|
139
|
+
const sortBy = ref<'name' | 'date'>('date');
|
|
140
|
+
const sortOrder = ref<'asc' | 'desc'>('desc');
|
|
141
|
+
|
|
142
|
+
const filtered = computed(() => {
|
|
143
|
+
let result = [...items.value];
|
|
144
|
+
|
|
145
|
+
if (searchQuery.value) {
|
|
146
|
+
const q = searchQuery.value.toLowerCase();
|
|
147
|
+
result = result.filter(item =>
|
|
148
|
+
item.name.toLowerCase().includes(q),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (statusFilter.value) {
|
|
153
|
+
result = result.filter(item => item.status === statusFilter.value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
result.sort((a, b) => {
|
|
157
|
+
const modifier = sortOrder.value === 'asc' ? 1 : -1;
|
|
158
|
+
return a[sortBy.value] > b[sortBy.value] ? modifier : -modifier;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return { searchQuery, statusFilter, sortBy, sortOrder, filtered };
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Wzorzec composable z localStorage
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// composables/useLocalStorage.ts
|
|
172
|
+
export function useLocalStorage<T>(key: string, defaultValue: T) {
|
|
173
|
+
const stored = localStorage.getItem(key);
|
|
174
|
+
const data = ref<T>(stored ? JSON.parse(stored) : defaultValue) as Ref<T>;
|
|
175
|
+
|
|
176
|
+
watch(data, (newValue) => {
|
|
177
|
+
localStorage.setItem(key, JSON.stringify(newValue));
|
|
178
|
+
}, { deep: true });
|
|
179
|
+
|
|
180
|
+
function remove() {
|
|
181
|
+
localStorage.removeItem(key);
|
|
182
|
+
data.value = defaultValue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { data, remove };
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Wzorzec composable z window events
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// composables/useWindowSize.ts
|
|
193
|
+
export function useWindowSize() {
|
|
194
|
+
const width = ref(window.innerWidth);
|
|
195
|
+
const height = ref(window.innerHeight);
|
|
196
|
+
|
|
197
|
+
function onResize() {
|
|
198
|
+
width.value = window.innerWidth;
|
|
199
|
+
height.value = window.innerHeight;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
onMounted(() => window.addEventListener('resize', onResize));
|
|
203
|
+
onUnmounted(() => window.removeEventListener('resize', onResize));
|
|
204
|
+
|
|
205
|
+
return { width, height };
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Zasady
|
|
210
|
+
|
|
211
|
+
1. **Zawsze czyszcz zasoby** - `onUnmounted` dla listenerow, timerow, subskrypcji
|
|
212
|
+
2. **Zwracaj reaktywne wartosci** - `ref`, `computed`, nie surowe wartosci
|
|
213
|
+
3. **Nie uzywaj `this`** - composables to funkcje, nie klasy
|
|
214
|
+
4. **Jeden composable = jedna odpowiedzialnosc** - nie mieszaj filtrowania z API calls
|
|
215
|
+
5. **Typuj parametry i return** - TypeScript musi wiedziec co zwracasz
|
|
216
|
+
6. **Nie wywoluj side-effectow w konstruktorze** - uzytkownik composable decyduje kiedy `execute()`
|
|
217
|
+
7. **`printError()` zamiast `console.error()`** - dla obslugi bledow
|
|
218
|
+
8. **Testowalnosc** - composable powinien byc latwy do przetestowania w izolacji
|