@jjlmoya/utils-pets 1.1.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 (41) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +48 -0
  3. package/src/category/i18n/es.ts +48 -0
  4. package/src/category/i18n/fr.ts +48 -0
  5. package/src/category/index.ts +13 -0
  6. package/src/category/seo.astro +14 -0
  7. package/src/components/PreviewNavSidebar.astro +115 -0
  8. package/src/components/PreviewToolbar.astro +142 -0
  9. package/src/data.ts +15 -0
  10. package/src/env.d.ts +4 -0
  11. package/src/index.ts +20 -0
  12. package/src/layouts/PreviewLayout.astro +116 -0
  13. package/src/pages/[locale]/[slug].astro +148 -0
  14. package/src/pages/[locale].astro +270 -0
  15. package/src/pages/index.astro +3 -0
  16. package/src/tests/faq_count.test.ts +23 -0
  17. package/src/tests/mocks/astro_mock.js +1 -0
  18. package/src/tests/seo_length.test.ts +54 -0
  19. package/src/tests/tool_validation.test.ts +139 -0
  20. package/src/tool/petAge/bibliography.astro +14 -0
  21. package/src/tool/petAge/component.astro +267 -0
  22. package/src/tool/petAge/handlers.ts +142 -0
  23. package/src/tool/petAge/i18n/en.ts +160 -0
  24. package/src/tool/petAge/i18n/es.ts +160 -0
  25. package/src/tool/petAge/i18n/fr.ts +160 -0
  26. package/src/tool/petAge/index.ts +64 -0
  27. package/src/tool/petAge/logic.ts +59 -0
  28. package/src/tool/petAge/seo.astro +61 -0
  29. package/src/tool/petAge/style.css +649 -0
  30. package/src/tool/petRation/bibliography.astro +14 -0
  31. package/src/tool/petRation/component.astro +181 -0
  32. package/src/tool/petRation/handlers.ts +139 -0
  33. package/src/tool/petRation/i18n/en.ts +161 -0
  34. package/src/tool/petRation/i18n/es.ts +161 -0
  35. package/src/tool/petRation/i18n/fr.ts +161 -0
  36. package/src/tool/petRation/index.ts +63 -0
  37. package/src/tool/petRation/logic.ts +44 -0
  38. package/src/tool/petRation/seo.astro +61 -0
  39. package/src/tool/petRation/style.css +427 -0
  40. package/src/tools.ts +13 -0
  41. package/src/types.ts +71 -0
@@ -0,0 +1,427 @@
1
+ .pet-ration-calculator-final {
2
+ position: relative;
3
+ max-width: 650px;
4
+ margin: 2rem auto;
5
+ background: var(--color-surface, #fff);
6
+ border-radius: 2rem;
7
+ padding: 2.5rem;
8
+ box-shadow: 0 20px 50px -15px rgba(0, 0, 0, 0.08);
9
+ color: var(--color-text, #334155);
10
+ border: 1px solid var(--color-border, #f1f5f9);
11
+ min-height: 400px;
12
+ }
13
+
14
+ .calc-loader {
15
+ position: absolute;
16
+ top: 0;
17
+ left: 0;
18
+ width: 100%;
19
+ height: 100%;
20
+ background: var(--color-surface, #fff);
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ border-radius: 2rem;
25
+ z-index: 50;
26
+ transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s;
27
+ }
28
+
29
+ .theme-dark .pet-ration-calculator-final {
30
+ background: var(--color-surface-dark, #1e293b);
31
+ color: var(--color-text-dark, #e2e8f0);
32
+ border-color: var(--color-border-dark, #334155);
33
+ box-shadow: 0 20px 50px -15px rgba(0, 0, 0, 0.3);
34
+ }
35
+
36
+ .theme-dark .calc-loader {
37
+ background: var(--color-surface-dark, #1e293b);
38
+ }
39
+
40
+ .calc-loader.hidden {
41
+ opacity: 0;
42
+ visibility: hidden;
43
+ pointer-events: none;
44
+ }
45
+
46
+ .loader-spinner {
47
+ width: 3rem;
48
+ height: 3rem;
49
+ color: var(--color-accent, #f59e0b);
50
+ animation: spin-pulse 2s infinite linear;
51
+ }
52
+
53
+ @keyframes spin-pulse {
54
+ from { transform: rotate(0deg); }
55
+ to { transform: rotate(360deg); }
56
+ }
57
+
58
+ .calc-container {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 3rem;
62
+ animation: fade-in-up 0.6s cubic-bezier(0.4, 0, 0.2, 1);
63
+ }
64
+
65
+ @keyframes fade-in-up {
66
+ from {
67
+ opacity: 0;
68
+ transform: translateY(10px);
69
+ }
70
+ to {
71
+ opacity: 1;
72
+ transform: translateY(0);
73
+ }
74
+ }
75
+
76
+ .section-title {
77
+ font-size: 1.1rem;
78
+ font-weight: 950;
79
+ color: var(--color-heading, #0f172a);
80
+ margin-bottom: 2rem;
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 0.75rem;
84
+ text-transform: uppercase;
85
+ letter-spacing: 0.05em;
86
+ }
87
+
88
+ .theme-dark .section-title {
89
+ color: var(--color-heading-dark, #f8fafc);
90
+ }
91
+
92
+ .calc-section {
93
+ border-bottom: 1px dashed var(--color-border-dashed, #e2e8f0);
94
+ padding-bottom: 2rem;
95
+ }
96
+
97
+ .calc-section:last-of-type {
98
+ border-bottom: none;
99
+ }
100
+
101
+ .theme-dark .calc-section {
102
+ border-bottom-color: var(--color-border-dark, #334155);
103
+ }
104
+
105
+ .input-item {
106
+ margin-bottom: 2rem;
107
+ }
108
+
109
+ .item-label {
110
+ display: block;
111
+ font-size: 0.75rem;
112
+ font-weight: 800;
113
+ text-transform: uppercase;
114
+ color: var(--color-label, #94a3b8);
115
+ margin-bottom: 1rem;
116
+ letter-spacing: 0.1em;
117
+ }
118
+
119
+ .species-selector-v2 {
120
+ display: flex;
121
+ gap: 1rem;
122
+ }
123
+
124
+ .species-selector-v2 input {
125
+ display: none;
126
+ }
127
+
128
+ .btn-species {
129
+ flex: 1;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ gap: 0.5rem;
134
+ padding: 1.25rem 0.75rem;
135
+ background: var(--color-btn-bg, #f8fafc);
136
+ border: 2px solid var(--color-btn-border, #e2e8f0);
137
+ border-radius: 1rem;
138
+ cursor: pointer;
139
+ font-weight: 800;
140
+ font-size: 1rem;
141
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
142
+ }
143
+
144
+ .theme-dark .btn-species {
145
+ background: var(--color-btn-bg-dark, #0f172a);
146
+ border-color: var(--color-btn-border-dark, #334155);
147
+ }
148
+
149
+ .icon-v2 {
150
+ width: 1.5rem;
151
+ height: 1.5rem;
152
+ }
153
+
154
+ .species-selector-v2 input:checked + .btn-species {
155
+ background: var(--color-surface, #fff);
156
+ border-color: var(--color-accent, #f59e0b);
157
+ color: var(--color-accent, #f59e0b);
158
+ box-shadow: 0 4px 15px rgba(245, 158, 11, 0.15);
159
+ transform: translateY(-2px);
160
+ }
161
+
162
+ .theme-dark .species-selector-v2 input:checked + .btn-species {
163
+ background: var(--color-surface-elevated-dark, #1e293b);
164
+ }
165
+
166
+ .segmented-v2 {
167
+ display: flex;
168
+ background: var(--color-btn-bg, #f8fafc);
169
+ padding: 6px;
170
+ border-radius: 1rem;
171
+ border: 1px solid var(--color-btn-border, #e2e8f0);
172
+ }
173
+
174
+ .theme-dark .segmented-v2 {
175
+ background: var(--color-btn-bg-dark, #0f172a);
176
+ border-color: var(--color-btn-border-dark, #334155);
177
+ }
178
+
179
+ .segmented-v2 input {
180
+ display: none;
181
+ }
182
+
183
+ .segmented-v2 label {
184
+ flex: 1;
185
+ text-align: center;
186
+ padding: 0.85rem;
187
+ border-radius: 0.75rem;
188
+ cursor: pointer;
189
+ font-weight: 700;
190
+ font-size: 0.85rem;
191
+ transition: all 0.2s;
192
+ color: var(--color-muted, #64748b);
193
+ }
194
+
195
+ .segmented-v2 input:checked + label {
196
+ background: var(--color-accent, #f59e0b);
197
+ color: var(--color-on-accent, #fff);
198
+ box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
199
+ }
200
+
201
+ .val-badge {
202
+ font-weight: 950;
203
+ color: var(--color-accent, #f59e0b);
204
+ font-size: 1.25rem;
205
+ }
206
+
207
+ .label-row-v2 {
208
+ display: flex;
209
+ justify-content: space-between;
210
+ }
211
+
212
+ .range-v2 {
213
+ -webkit-appearance: none;
214
+ appearance: none;
215
+ width: 100%;
216
+ height: 8px;
217
+ background: var(--color-range-track, #e2e8f0);
218
+ border-radius: 10px;
219
+ }
220
+
221
+ .theme-dark .range-v2 {
222
+ background: var(--color-range-track-dark, #334155);
223
+ }
224
+
225
+ .range-v2::-webkit-slider-thumb {
226
+ -webkit-appearance: none;
227
+ appearance: none;
228
+ width: 26px;
229
+ height: 26px;
230
+ background: var(--color-surface, #fff);
231
+ border: 6px solid var(--color-accent, #f59e0b);
232
+ border-radius: 50%;
233
+ cursor: pointer;
234
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
235
+ }
236
+
237
+ .ratio-presets {
238
+ display: grid;
239
+ grid-template-columns: repeat(2, 1fr);
240
+ gap: 0.75rem;
241
+ }
242
+
243
+ .ratio-presets input {
244
+ display: none;
245
+ }
246
+
247
+ .ratio-presets label {
248
+ padding: 0.85rem;
249
+ text-align: center;
250
+ background: var(--color-btn-bg, #f8fafc);
251
+ border: 1px solid var(--color-btn-border, #e2e8f0);
252
+ border-radius: 0.75rem;
253
+ cursor: pointer;
254
+ font-weight: 700;
255
+ font-size: 0.85rem;
256
+ transition: all 0.2s;
257
+ }
258
+
259
+ .theme-dark .ratio-presets label {
260
+ background: var(--color-btn-bg-dark, #0f172a);
261
+ border-color: var(--color-btn-border-dark, #334155);
262
+ }
263
+
264
+ .ratio-presets input:checked + label {
265
+ background: var(--color-surface, #fff);
266
+ border-color: var(--color-accent, #f59e0b);
267
+ color: var(--color-accent, #f59e0b);
268
+ box-shadow: 0 4px 10px rgba(245, 158, 11, 0.1);
269
+ }
270
+
271
+ .theme-dark .ratio-presets input:checked + label {
272
+ background: var(--color-surface-elevated-dark, #1e293b);
273
+ }
274
+
275
+ .custom-ratio-link {
276
+ display: block;
277
+ text-align: right;
278
+ font-size: 0.75rem;
279
+ color: var(--color-label, #94a3b8);
280
+ margin-top: 1rem;
281
+ cursor: pointer;
282
+ text-decoration: underline;
283
+ }
284
+
285
+ .manual-slider-box {
286
+ margin-top: 2rem;
287
+ padding: 1.5rem;
288
+ background: var(--color-btn-bg, #f8fafc);
289
+ border-radius: 1rem;
290
+ border: 1px solid var(--color-btn-border, #e2e8f0);
291
+ }
292
+
293
+ .theme-dark .manual-slider-box {
294
+ background: var(--color-btn-bg-dark, #0f172a);
295
+ border-color: var(--color-btn-border-dark, #334155);
296
+ }
297
+
298
+ .ratio-labels {
299
+ display: flex;
300
+ justify-content: space-between;
301
+ font-size: 0.8rem;
302
+ margin-top: 1rem;
303
+ font-weight: 700;
304
+ }
305
+
306
+ .results-v2 {
307
+ background: var(--color-results-bg, #0f172a);
308
+ border-radius: 2rem;
309
+ padding: 2.5rem;
310
+ color: var(--color-results-text, #fff);
311
+ box-shadow: 0 20px 40px -10px rgba(15, 23, 42, 0.3);
312
+ }
313
+
314
+ .theme-dark .results-v2 {
315
+ background: var(--color-accent, #f59e0b);
316
+ color: var(--color-results-text-dark, #0f172a);
317
+ box-shadow: 0 20px 40px -10px rgba(245, 158, 11, 0.2);
318
+ }
319
+
320
+ .res-cards-v2 {
321
+ display: flex;
322
+ gap: 1.5rem;
323
+ margin-bottom: 2rem;
324
+ }
325
+
326
+ .res-card-v2 {
327
+ flex: 1;
328
+ display: flex;
329
+ flex-direction: column;
330
+ align-items: center;
331
+ border-left: 4px solid var(--color-accent, #f59e0b);
332
+ padding: 0.5rem 1.5rem;
333
+ }
334
+
335
+ .theme-dark .res-card-v2 {
336
+ border-left-color: var(--color-results-bg, #0f172a);
337
+ }
338
+
339
+ .res-tag {
340
+ font-size: 0.8rem;
341
+ font-weight: 900;
342
+ text-transform: uppercase;
343
+ color: var(--color-accent, #f59e0b);
344
+ margin-bottom: 0.25rem;
345
+ align-self: flex-start;
346
+ }
347
+
348
+ .theme-dark .res-tag {
349
+ color: var(--color-results-bg, #0f172a);
350
+ opacity: 0.7;
351
+ }
352
+
353
+ .res-num {
354
+ font-size: 4rem;
355
+ font-weight: 950;
356
+ line-height: 1;
357
+ align-self: flex-start;
358
+ }
359
+
360
+ .res-num small {
361
+ font-size: 1.25rem;
362
+ margin-left: 4px;
363
+ opacity: 0.6;
364
+ }
365
+
366
+ .res-footer-v2 {
367
+ display: flex;
368
+ justify-content: space-between;
369
+ align-items: center;
370
+ padding-top: 1.5rem;
371
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
372
+ }
373
+
374
+ .theme-dark .res-footer-v2 {
375
+ border-top-color: rgba(0, 0, 0, 0.1);
376
+ }
377
+
378
+ .total-info {
379
+ font-weight: 800;
380
+ display: flex;
381
+ align-items: center;
382
+ gap: 0.5rem;
383
+ }
384
+
385
+ .total-info strong {
386
+ font-size: 1.4rem;
387
+ }
388
+
389
+ .footer-icon {
390
+ width: 1.5rem;
391
+ height: 1.5rem;
392
+ }
393
+
394
+ .status-box-v2 {
395
+ background: var(--color-accent, #f59e0b);
396
+ color: var(--color-on-accent, #fff);
397
+ padding: 0.4rem 1.25rem;
398
+ border-radius: 2rem;
399
+ font-size: 0.85rem;
400
+ font-weight: 900;
401
+ }
402
+
403
+ .theme-dark .status-box-v2 {
404
+ background: var(--color-results-bg, #0f172a);
405
+ color: var(--color-results-text, #fff);
406
+ }
407
+
408
+ @media (max-width: 600px) {
409
+ .pet-ration-calculator-final {
410
+ padding: 1.5rem;
411
+ margin: 1rem;
412
+ }
413
+
414
+ .species-selector-v2 {
415
+ flex-direction: column;
416
+ }
417
+
418
+ .res-cards-v2 {
419
+ flex-direction: column;
420
+ }
421
+
422
+ .res-footer-v2 {
423
+ flex-direction: column;
424
+ gap: 1.5rem;
425
+ text-align: center;
426
+ }
427
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { PET_AGE_TOOL } from './tool/petAge';
2
+ import { PET_RATION_TOOL } from './tool/petRation';
3
+ import type { ToolDefinition } from './types';
4
+
5
+ export const ALL_TOOLS: ToolDefinition[] = [
6
+ PET_AGE_TOOL,
7
+ PET_RATION_TOOL,
8
+ ];
9
+
10
+ export {
11
+ PET_AGE_TOOL,
12
+ PET_RATION_TOOL,
13
+ };
package/src/types.ts ADDED
@@ -0,0 +1,71 @@
1
+ import type { SEOSection } from '@jjlmoya/utils-shared';
2
+ import type { WithContext, Thing } from 'schema-dts';
3
+
4
+ export type { SEOSection };
5
+
6
+ export type KnownLocale =
7
+ | 'ar' | 'da' | 'de' | 'en' | 'es' | 'fi'
8
+ | 'fr' | 'it' | 'ja' | 'ko' | 'nb' | 'nl'
9
+ | 'pl' | 'pt' | 'ru' | 'sv' | 'tr' | 'zh';
10
+
11
+ export interface FAQItem {
12
+ question: string;
13
+ answer: string;
14
+ }
15
+
16
+ export interface BibliographyEntry {
17
+ name: string;
18
+ url: string;
19
+ }
20
+
21
+ export interface HowToStep {
22
+ name: string;
23
+ text: string;
24
+ }
25
+
26
+ export interface ToolLocaleContent<TUI extends Record<string, string> = Record<string, string>> {
27
+ slug: string;
28
+ title: string;
29
+ description: string;
30
+ ui: TUI;
31
+ seo: SEOSection[];
32
+ faqTitle?: string;
33
+ faq: FAQItem[];
34
+ bibliographyTitle?: string;
35
+ bibliography: BibliographyEntry[];
36
+ howTo: HowToStep[];
37
+ schemas: WithContext<Thing>[];
38
+ }
39
+
40
+ export interface CategoryLocaleContent {
41
+ slug: string;
42
+ title: string;
43
+ description: string;
44
+ seo: SEOSection[];
45
+ }
46
+
47
+ export type LocaleLoader<T> = () => Promise<T>;
48
+
49
+ export type LocaleMap<T> = Partial<Record<KnownLocale, LocaleLoader<T>>>;
50
+
51
+ export interface PetToolEntry<TUI extends Record<string, string> = Record<string, string>> {
52
+ id: string;
53
+ icons: {
54
+ bg: string;
55
+ fg: string;
56
+ };
57
+ i18n: LocaleMap<ToolLocaleContent<TUI>>;
58
+ }
59
+
60
+ export interface ToolDefinition {
61
+ entry: PetToolEntry;
62
+ Component: unknown;
63
+ SEOComponent?: unknown;
64
+ BibliographyComponent?: unknown;
65
+ }
66
+
67
+ export interface PetCategoryEntry {
68
+ icon: string;
69
+ tools: PetToolEntry[];
70
+ i18n: LocaleMap<CategoryLocaleContent>;
71
+ }