@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.
- package/package.json +60 -0
- package/src/category/i18n/en.ts +48 -0
- package/src/category/i18n/es.ts +48 -0
- package/src/category/i18n/fr.ts +48 -0
- package/src/category/index.ts +13 -0
- package/src/category/seo.astro +14 -0
- package/src/components/PreviewNavSidebar.astro +115 -0
- package/src/components/PreviewToolbar.astro +142 -0
- package/src/data.ts +15 -0
- package/src/env.d.ts +4 -0
- package/src/index.ts +20 -0
- package/src/layouts/PreviewLayout.astro +116 -0
- package/src/pages/[locale]/[slug].astro +148 -0
- package/src/pages/[locale].astro +270 -0
- package/src/pages/index.astro +3 -0
- package/src/tests/faq_count.test.ts +23 -0
- package/src/tests/mocks/astro_mock.js +1 -0
- package/src/tests/seo_length.test.ts +54 -0
- package/src/tests/tool_validation.test.ts +139 -0
- package/src/tool/petAge/bibliography.astro +14 -0
- package/src/tool/petAge/component.astro +267 -0
- package/src/tool/petAge/handlers.ts +142 -0
- package/src/tool/petAge/i18n/en.ts +160 -0
- package/src/tool/petAge/i18n/es.ts +160 -0
- package/src/tool/petAge/i18n/fr.ts +160 -0
- package/src/tool/petAge/index.ts +64 -0
- package/src/tool/petAge/logic.ts +59 -0
- package/src/tool/petAge/seo.astro +61 -0
- package/src/tool/petAge/style.css +649 -0
- package/src/tool/petRation/bibliography.astro +14 -0
- package/src/tool/petRation/component.astro +181 -0
- package/src/tool/petRation/handlers.ts +139 -0
- package/src/tool/petRation/i18n/en.ts +161 -0
- package/src/tool/petRation/i18n/es.ts +161 -0
- package/src/tool/petRation/i18n/fr.ts +161 -0
- package/src/tool/petRation/index.ts +63 -0
- package/src/tool/petRation/logic.ts +44 -0
- package/src/tool/petRation/seo.astro +61 -0
- package/src/tool/petRation/style.css +427 -0
- package/src/tools.ts +13 -0
- package/src/types.ts +71 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from "astro-icon/components";
|
|
3
|
+
import type { PetAgeUI } from "./index";
|
|
4
|
+
import "./style.css";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
ui: PetAgeUI;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { ui } = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div id="share-overlay" class="age-share-overlay">
|
|
14
|
+
<div class="age-share-wrap">
|
|
15
|
+
<div class="age-share-card">
|
|
16
|
+
<div class="age-share-card-header">
|
|
17
|
+
<div class="age-share-badge">{ui.humanAgeTitle}</div>
|
|
18
|
+
<div class="age-share-animal">
|
|
19
|
+
<div id="share-icon-dog" class="hidden">
|
|
20
|
+
<Icon name="mdi:dog-side" />
|
|
21
|
+
</div>
|
|
22
|
+
<div id="share-icon-cat" class="hidden"><Icon name="mdi:cat" /></div>
|
|
23
|
+
</div>
|
|
24
|
+
<h1 id="share-pet-name"></h1>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="age-share-card-body">
|
|
27
|
+
<p class="age-share-human-label">{ui.humanAgeLabel}</p>
|
|
28
|
+
<span id="share-human-age">--</span>
|
|
29
|
+
<p class="age-share-years-label">{ui.humanAgeYears}</p>
|
|
30
|
+
<div class="age-share-info-grid">
|
|
31
|
+
<div class="age-share-info-item">
|
|
32
|
+
<span class="age-share-info-label">{ui.realAgeLabel}</span>
|
|
33
|
+
<div>
|
|
34
|
+
<span id="share-chronological-age" class="age-share-info-val"
|
|
35
|
+
>--</span
|
|
36
|
+
>
|
|
37
|
+
<span class="age-share-info-val"> {ui.humanAgeYears}</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="age-share-info-item">
|
|
41
|
+
<span class="age-share-info-label">{ui.lifeStageLabel}</span>
|
|
42
|
+
<span id="share-life-stage" class="age-share-info-val"></span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="age-share-card-footer" id="share-domain"></div>
|
|
47
|
+
</div>
|
|
48
|
+
<button id="create-new-btn" class="age-share-new-btn">
|
|
49
|
+
<Icon name="mdi:refresh" />
|
|
50
|
+
<span>{ui.recalculateBtn}</span>
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div
|
|
56
|
+
id="calculator-container"
|
|
57
|
+
class="pet-age-calculator"
|
|
58
|
+
data-ui={JSON.stringify(ui)}
|
|
59
|
+
>
|
|
60
|
+
<div class="age-loader" id="ageLoader">
|
|
61
|
+
<Icon name="mdi:loading" class="age-loader-spinner" />
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="age-container">
|
|
65
|
+
<div class="age-section">
|
|
66
|
+
<h3 class="age-section-title">
|
|
67
|
+
<Icon name="mdi:paw" />
|
|
68
|
+
{ui.toolTitle}
|
|
69
|
+
</h3>
|
|
70
|
+
|
|
71
|
+
<div class="age-input-item">
|
|
72
|
+
<label class="age-label" for="pet-name">{ui.petNameLabel}</label>
|
|
73
|
+
<input
|
|
74
|
+
type="text"
|
|
75
|
+
id="pet-name"
|
|
76
|
+
placeholder={ui.petNamePlaceholder}
|
|
77
|
+
class="age-text-input"
|
|
78
|
+
autocomplete="off"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="age-input-item">
|
|
83
|
+
<label class="age-label">Animal</label>
|
|
84
|
+
<div class="age-species-selector">
|
|
85
|
+
<button class="pet-type-btn active" data-type="dog">
|
|
86
|
+
<Icon name="mdi:dog-side" class="age-icon" />
|
|
87
|
+
{ui.petTypeDog}
|
|
88
|
+
</button>
|
|
89
|
+
<button class="pet-type-btn" data-type="cat">
|
|
90
|
+
<Icon name="mdi:cat" class="age-icon" />
|
|
91
|
+
{ui.petTypeCat}
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="age-input-item" id="dog-size-selector">
|
|
97
|
+
<label class="age-label">Tamaño</label>
|
|
98
|
+
<div class="age-segmented">
|
|
99
|
+
<button class="size-btn active" data-size="small"
|
|
100
|
+
>{ui.sizeSmall}</button
|
|
101
|
+
>
|
|
102
|
+
<button class="size-btn" data-size="medium">{ui.sizeMedium}</button>
|
|
103
|
+
<button class="size-btn" data-size="large">{ui.sizeLarge}</button>
|
|
104
|
+
<button class="size-btn" data-size="giant">{ui.sizeGiant}</button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="age-section">
|
|
110
|
+
<h3 class="age-section-title">
|
|
111
|
+
<Icon name="mdi:calendar" />
|
|
112
|
+
{ui.birthYearLabel}
|
|
113
|
+
</h3>
|
|
114
|
+
<div class="age-year-wrap">
|
|
115
|
+
<input
|
|
116
|
+
type="number"
|
|
117
|
+
id="birth-year-input"
|
|
118
|
+
placeholder="2021"
|
|
119
|
+
class="age-year-input"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="age-results">
|
|
125
|
+
<div class="age-res-main">
|
|
126
|
+
<span class="age-res-label">
|
|
127
|
+
{ui.humanAgeLabel}
|
|
128
|
+
<span id="result-pet-name" class="hidden"></span>
|
|
129
|
+
</span>
|
|
130
|
+
<div class="age-res-number">
|
|
131
|
+
<span id="human-age">--</span>
|
|
132
|
+
<small>{ui.humanAgeYears}</small>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="age-res-divider"></div>
|
|
136
|
+
<div class="age-res-grid">
|
|
137
|
+
<div class="age-res-card">
|
|
138
|
+
<span class="age-res-card-label">{ui.lifeStageLabel}</span>
|
|
139
|
+
<span id="life-stage" class="age-res-card-val">---</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="age-res-card">
|
|
142
|
+
<span class="age-res-card-label">{ui.milestoneLabel}</span>
|
|
143
|
+
<span id="next-milestone" class="age-res-card-val">---</span>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="age-res-footer">
|
|
147
|
+
<button id="share-btn" class="age-share-btn">
|
|
148
|
+
<Icon name="mdi:share-variant" class="age-icon-sm" />
|
|
149
|
+
<span id="share-text">{ui.shareBtn}</span>
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<script>
|
|
157
|
+
import type { PetSpecies, DogSize } from "./logic";
|
|
158
|
+
import {
|
|
159
|
+
getElements,
|
|
160
|
+
updateResultsDisplay,
|
|
161
|
+
showShareMode,
|
|
162
|
+
validateBirthYear,
|
|
163
|
+
generateShareUrl,
|
|
164
|
+
type PetAgeState,
|
|
165
|
+
} from "./handlers";
|
|
166
|
+
|
|
167
|
+
function setupPetTypeListener(state: PetAgeState, els: Required<ReturnType<typeof getElements>>, onUpdate: () => void) {
|
|
168
|
+
els.petBtns.forEach((btn) => {
|
|
169
|
+
btn.addEventListener("click", () => {
|
|
170
|
+
state.petType = (btn.dataset.type || "dog") as PetSpecies;
|
|
171
|
+
els.petBtns.forEach((b) => b.classList.remove("active"));
|
|
172
|
+
btn.classList.add("active");
|
|
173
|
+
if (els.dogSizeSelector) {
|
|
174
|
+
els.dogSizeSelector.style.display = state.petType === "dog" ? "block" : "none";
|
|
175
|
+
}
|
|
176
|
+
onUpdate();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function setupSizeListener(state: PetAgeState, els: Required<ReturnType<typeof getElements>>, onUpdate: () => void) {
|
|
182
|
+
els.sizeBtns.forEach((btn) => {
|
|
183
|
+
btn.addEventListener("click", () => {
|
|
184
|
+
state.dogSize = (btn.dataset.size || "small") as DogSize;
|
|
185
|
+
els.sizeBtns.forEach((b) => b.classList.remove("active"));
|
|
186
|
+
btn.classList.add("active");
|
|
187
|
+
onUpdate();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function setupBirthYearListener(state: PetAgeState, birthYearInput: HTMLInputElement, currentYear: number, onUpdate: () => void) {
|
|
193
|
+
birthYearInput.addEventListener("input", (e) => {
|
|
194
|
+
const val = (e.target as HTMLInputElement).value;
|
|
195
|
+
state.birthYear = validateBirthYear(val, currentYear);
|
|
196
|
+
birthYearInput.classList.toggle("age-input-valid", state.birthYear !== null);
|
|
197
|
+
birthYearInput.classList.toggle("age-input-error", val.length === 4 && !state.birthYear);
|
|
198
|
+
onUpdate();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function setupShareListener(state: PetAgeState, els: Required<ReturnType<typeof getElements>>, birthYearInput: HTMLInputElement) {
|
|
203
|
+
els.shareBtn?.addEventListener("click", async () => {
|
|
204
|
+
if (!state.birthYear) {
|
|
205
|
+
birthYearInput.focus();
|
|
206
|
+
birthYearInput.classList.add("age-input-error");
|
|
207
|
+
setTimeout(() => birthYearInput.classList.remove("age-input-error"), 500);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const url = generateShareUrl(state);
|
|
212
|
+
try {
|
|
213
|
+
await navigator.clipboard.writeText(url);
|
|
214
|
+
if (els.shareText) els.shareText.textContent = "¡Copiado!";
|
|
215
|
+
if (els.shareBtn) els.shareBtn.classList.add("age-btn-success");
|
|
216
|
+
setTimeout(() => (window.location.href = url), 800);
|
|
217
|
+
} catch {
|
|
218
|
+
window.location.href = url;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function initPetAge() {
|
|
224
|
+
const birthYearInput = document.getElementById("birth-year-input") as HTMLInputElement;
|
|
225
|
+
const container = document.getElementById("calculator-container");
|
|
226
|
+
|
|
227
|
+
if (!birthYearInput || !container) return;
|
|
228
|
+
|
|
229
|
+
const uiData = JSON.parse(container.dataset.ui || "{}");
|
|
230
|
+
const els = getElements() as Required<ReturnType<typeof getElements>>;
|
|
231
|
+
const currentYear = new Date().getFullYear();
|
|
232
|
+
|
|
233
|
+
const state: PetAgeState = { petType: "dog", dogSize: "small", birthYear: null, petName: "" };
|
|
234
|
+
|
|
235
|
+
birthYearInput.max = currentYear.toString();
|
|
236
|
+
showShareMode(state, els, uiData, currentYear);
|
|
237
|
+
|
|
238
|
+
const shareDomain = document.getElementById("share-domain");
|
|
239
|
+
if (shareDomain) shareDomain.textContent = window.location.hostname;
|
|
240
|
+
|
|
241
|
+
const onUpdate = () => updateResultsDisplay(state, els, uiData, currentYear);
|
|
242
|
+
|
|
243
|
+
document.getElementById("create-new-btn")?.addEventListener("click", () => {
|
|
244
|
+
window.location.href = window.location.origin + window.location.pathname;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
els.petNameInput?.addEventListener("input", (e) => {
|
|
248
|
+
state.petName = (e.target as HTMLInputElement).value;
|
|
249
|
+
onUpdate();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
setupPetTypeListener(state, els, onUpdate);
|
|
253
|
+
setupSizeListener(state, els, onUpdate);
|
|
254
|
+
setupBirthYearListener(state, birthYearInput, currentYear, onUpdate);
|
|
255
|
+
setupShareListener(state, els, birthYearInput);
|
|
256
|
+
|
|
257
|
+
onUpdate();
|
|
258
|
+
setTimeout(() => els.loader?.classList.add("hidden"), 50);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (document.readyState === "loading") {
|
|
262
|
+
document.addEventListener("DOMContentLoaded", initPetAge);
|
|
263
|
+
} else {
|
|
264
|
+
initPetAge();
|
|
265
|
+
}
|
|
266
|
+
document.addEventListener("astro:page-load", initPetAge);
|
|
267
|
+
</script>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { calculatePetHumanAge, getDogStageKey, getCatStageKey, getDogMilestone, getCatMilestone } from "./logic";
|
|
2
|
+
import type { PetSpecies, DogSize } from "./logic";
|
|
3
|
+
|
|
4
|
+
export interface PetAgeState {
|
|
5
|
+
petType: PetSpecies;
|
|
6
|
+
dogSize: DogSize;
|
|
7
|
+
birthYear: number | null;
|
|
8
|
+
petName: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PetAgeElements {
|
|
12
|
+
shareOverlay: HTMLElement | null;
|
|
13
|
+
container: HTMLElement | null;
|
|
14
|
+
sharePetName: HTMLElement | null;
|
|
15
|
+
shareHumanAge: HTMLElement | null;
|
|
16
|
+
shareChronologicalAge: HTMLElement | null;
|
|
17
|
+
shareLifeStage: HTMLElement | null;
|
|
18
|
+
shareIconDog: HTMLElement | null;
|
|
19
|
+
shareIconCat: HTMLElement | null;
|
|
20
|
+
petBtns: NodeListOf<HTMLElement>;
|
|
21
|
+
sizeBtns: NodeListOf<HTMLElement>;
|
|
22
|
+
dogSizeSelector: HTMLElement | null;
|
|
23
|
+
birthYearInput: HTMLInputElement;
|
|
24
|
+
humanAgeDisplay: HTMLElement | null;
|
|
25
|
+
lifeStageDisplay: HTMLElement | null;
|
|
26
|
+
nextMilestoneDisplay: HTMLElement | null;
|
|
27
|
+
petNameInput: HTMLInputElement | null;
|
|
28
|
+
resultPetNameSpan: HTMLElement | null;
|
|
29
|
+
shareBtn: HTMLElement | null;
|
|
30
|
+
shareText: HTMLElement | null;
|
|
31
|
+
loader: HTMLElement | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getElements(): Partial<PetAgeElements> {
|
|
35
|
+
return {
|
|
36
|
+
shareOverlay: document.getElementById("share-overlay"),
|
|
37
|
+
container: document.getElementById("calculator-container"),
|
|
38
|
+
sharePetName: document.getElementById("share-pet-name"),
|
|
39
|
+
shareHumanAge: document.getElementById("share-human-age"),
|
|
40
|
+
shareChronologicalAge: document.getElementById("share-chronological-age"),
|
|
41
|
+
shareLifeStage: document.getElementById("share-life-stage"),
|
|
42
|
+
shareIconDog: document.getElementById("share-icon-dog"),
|
|
43
|
+
shareIconCat: document.getElementById("share-icon-cat"),
|
|
44
|
+
petBtns: document.querySelectorAll(".pet-type-btn") as NodeListOf<HTMLElement>,
|
|
45
|
+
sizeBtns: document.querySelectorAll(".size-btn") as NodeListOf<HTMLElement>,
|
|
46
|
+
dogSizeSelector: document.getElementById("dog-size-selector"),
|
|
47
|
+
birthYearInput: document.getElementById("birth-year-input") as HTMLInputElement,
|
|
48
|
+
humanAgeDisplay: document.getElementById("human-age"),
|
|
49
|
+
lifeStageDisplay: document.getElementById("life-stage"),
|
|
50
|
+
nextMilestoneDisplay: document.getElementById("next-milestone"),
|
|
51
|
+
petNameInput: document.getElementById("pet-name") as HTMLInputElement,
|
|
52
|
+
resultPetNameSpan: document.getElementById("result-pet-name"),
|
|
53
|
+
shareBtn: document.getElementById("share-btn"),
|
|
54
|
+
shareText: document.getElementById("share-text"),
|
|
55
|
+
loader: document.getElementById("ageLoader"),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getStageLabel(humanAge: number, petType: PetSpecies, uiData: Record<string, string>): string {
|
|
60
|
+
const key = petType === "cat" ? getCatStageKey(humanAge) : getDogStageKey(humanAge);
|
|
61
|
+
return uiData[key] || key;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getMilestoneLabel(chronologicalAge: number, petType: PetSpecies): string {
|
|
65
|
+
return petType === "cat" ? getCatMilestone(chronologicalAge) : getDogMilestone(chronologicalAge);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function updateMainDisplay(state: PetAgeState, els: Partial<PetAgeElements>, uiData: Record<string, string>, currentYear: number): void {
|
|
69
|
+
if (!state.birthYear) {
|
|
70
|
+
if (els.humanAgeDisplay) els.humanAgeDisplay.textContent = "--";
|
|
71
|
+
if (els.lifeStageDisplay) els.lifeStageDisplay.textContent = "---";
|
|
72
|
+
if (els.nextMilestoneDisplay) els.nextMilestoneDisplay.textContent = "---";
|
|
73
|
+
} else {
|
|
74
|
+
const chronologicalAge = currentYear - state.birthYear;
|
|
75
|
+
const humanAge = calculatePetHumanAge(state.petType, chronologicalAge, state.dogSize);
|
|
76
|
+
if (els.humanAgeDisplay) els.humanAgeDisplay.textContent = humanAge.toString();
|
|
77
|
+
if (els.lifeStageDisplay) els.lifeStageDisplay.textContent = getStageLabel(humanAge, state.petType, uiData);
|
|
78
|
+
if (els.nextMilestoneDisplay) els.nextMilestoneDisplay.textContent = getMilestoneLabel(chronologicalAge, state.petType);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function updateNameDisplay(state: PetAgeState, els: Partial<PetAgeElements>): void {
|
|
83
|
+
if (!els.resultPetNameSpan) return;
|
|
84
|
+
const shouldShow = state.petName.trim().length > 0;
|
|
85
|
+
els.resultPetNameSpan.classList.toggle("hidden", !shouldShow);
|
|
86
|
+
if (shouldShow) els.resultPetNameSpan.textContent = state.petName;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function updateResultsDisplay(state: PetAgeState, els: Partial<PetAgeElements>, uiData: Record<string, string>, currentYear: number): void {
|
|
90
|
+
updateMainDisplay(state, els, uiData, currentYear);
|
|
91
|
+
updateNameDisplay(state, els);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function renderShareCard(state: PetAgeState, els: Partial<PetAgeElements>, uiData: Record<string, string>, currentYear: number): void {
|
|
95
|
+
if (!state.birthYear) return;
|
|
96
|
+
|
|
97
|
+
const chronologicalAge = currentYear - state.birthYear;
|
|
98
|
+
const humanAge = calculatePetHumanAge(state.petType, chronologicalAge, state.dogSize);
|
|
99
|
+
|
|
100
|
+
if (els.sharePetName) els.sharePetName.textContent = state.petName;
|
|
101
|
+
if (els.shareHumanAge) els.shareHumanAge.textContent = humanAge.toString();
|
|
102
|
+
if (els.shareChronologicalAge) els.shareChronologicalAge.textContent = chronologicalAge.toString();
|
|
103
|
+
if (els.shareLifeStage) els.shareLifeStage.textContent = getStageLabel(humanAge, state.petType, uiData);
|
|
104
|
+
if (els.shareIconDog) els.shareIconDog.classList.toggle("hidden", state.petType !== "dog");
|
|
105
|
+
if (els.shareIconCat) els.shareIconCat.classList.toggle("hidden", state.petType !== "cat");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function loadShareParams(state: PetAgeState, uiData: Record<string, string>): boolean {
|
|
109
|
+
const params = new URLSearchParams(window.location.search);
|
|
110
|
+
if (!params.has("birthYear") || !params.has("type")) return false;
|
|
111
|
+
|
|
112
|
+
state.petName = params.get("name") || uiData.defaultPetName || "";
|
|
113
|
+
state.petType = (params.get("type") || "dog") as PetSpecies;
|
|
114
|
+
state.dogSize = (params.get("size") || "medium") as DogSize;
|
|
115
|
+
state.birthYear = parseInt(params.get("birthYear") || "0");
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setShareVisibility(show: boolean, els: Partial<PetAgeElements>): void {
|
|
120
|
+
if (els.shareOverlay) els.shareOverlay.classList.toggle("visible", show);
|
|
121
|
+
if (els.container) els.container.style.display = show ? "none" : "block";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function showShareMode(state: PetAgeState, els: Partial<PetAgeElements>, uiData: Record<string, string>, currentYear: number): void {
|
|
125
|
+
const hasShare = loadShareParams(state, uiData);
|
|
126
|
+
if (hasShare) {
|
|
127
|
+
renderShareCard(state, els, uiData, currentYear);
|
|
128
|
+
}
|
|
129
|
+
setShareVisibility(hasShare, els);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function validateBirthYear(val: string, currentYear: number): number | null {
|
|
133
|
+
const num = parseInt(val);
|
|
134
|
+
return !isNaN(num) && num > 1900 && num <= currentYear ? num : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function generateShareUrl(state: PetAgeState): string {
|
|
138
|
+
const params = new URLSearchParams({ type: state.petType, birthYear: state.birthYear!.toString() });
|
|
139
|
+
if (state.petName) params.set("name", state.petName);
|
|
140
|
+
if (state.petType === "dog") params.set("size", state.dogSize);
|
|
141
|
+
return `${window.location.origin}${window.location.pathname}?${params}`;
|
|
142
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { WithContext, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import type { PetAgeUI, PetAgeLocaleContent } from '../index';
|
|
3
|
+
|
|
4
|
+
const slug = 'pet-age-calculator';
|
|
5
|
+
const title = 'Pet Age Calculator | Dogs and Cats to Human Years';
|
|
6
|
+
const description = 'Find out your dog or cat\'s real age in human years. Forget the multiply-by-7 rule. Use our calculator based on current veterinary science.';
|
|
7
|
+
|
|
8
|
+
const ui: PetAgeUI = {
|
|
9
|
+
toolTitle: 'Age Calculator',
|
|
10
|
+
toolSubtitle: 'How many human years does your furry friend really have?',
|
|
11
|
+
petNameLabel: 'Name (Optional)',
|
|
12
|
+
petNamePlaceholder: 'E.g. Buddy',
|
|
13
|
+
petTypeDog: 'Dog',
|
|
14
|
+
petTypeCat: 'Cat',
|
|
15
|
+
sizeSmall: 'Small',
|
|
16
|
+
sizeMedium: 'Med',
|
|
17
|
+
sizeLarge: 'Large',
|
|
18
|
+
sizeGiant: 'Giant',
|
|
19
|
+
birthYearLabel: 'Birth year',
|
|
20
|
+
humanAgeLabel: 'The human age of',
|
|
21
|
+
humanAgeUnit: 'years',
|
|
22
|
+
humanAgeYears: 'years',
|
|
23
|
+
lifeStageLabel: 'Life Stage',
|
|
24
|
+
milestoneLabel: 'Next Milestone',
|
|
25
|
+
shareBtn: 'Share Result',
|
|
26
|
+
shareSuccess: 'Copied! Opening card...',
|
|
27
|
+
humanAgeTitle: 'Magic Age',
|
|
28
|
+
recalculateBtn: 'Calculate another age',
|
|
29
|
+
realAgeLabel: 'Real Age',
|
|
30
|
+
defaultPetName: 'Your pet',
|
|
31
|
+
stageDogPuppy: 'Playful Puppy',
|
|
32
|
+
stageDogTeen: 'Rebellious Teen',
|
|
33
|
+
stageDogAdult: 'Prime Adult',
|
|
34
|
+
stageDogMature: 'Distinguished Maturity',
|
|
35
|
+
stageDogSenior: 'Senior Wisdom',
|
|
36
|
+
stageCatKitten: 'Infant Curiosity',
|
|
37
|
+
stageCatYouth: 'Feline Youth',
|
|
38
|
+
stageCatReign: 'Domestic Reign',
|
|
39
|
+
stageCatVeteran: 'Pampered Veteran',
|
|
40
|
+
stageCatVenerable: 'Venerable Feline',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const faq: PetAgeLocaleContent['faq'] = [
|
|
44
|
+
{
|
|
45
|
+
question: 'Is it true that one dog year equals 7 human years?',
|
|
46
|
+
answer: 'No. That is an oversimplified rule that modern veterinary science has debunked. Dogs mature very quickly in their first two years and the process then slows down, varying a lot by weight and breed.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
question: 'Why do large dogs age faster?',
|
|
50
|
+
answer: 'Large breed dogs have a more accelerated metabolism and growth, which causes greater oxidative stress in their cells. This means their life expectancy is shorter than that of small dogs.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
question: 'How do cats age compared to humans?',
|
|
54
|
+
answer: 'A cat finishes its infancy at one month, adolescence at 6 months, and reaches adulthood (equivalent to 24 human years) at 2 years. After that, each cat year is equivalent to about 4 human years.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
question: 'At what age is a pet considered Senior?',
|
|
58
|
+
answer: 'Generally, a dog is considered senior when it has reached 75% of its expected lifespan. In cats, it is usually from 7-11 years, depending on their state of health.',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const howTo: PetAgeLocaleContent['howTo'] = [
|
|
63
|
+
{ name: 'Choose pet type', text: 'Select whether you have a dog or cat to apply the correct biological formula.' },
|
|
64
|
+
{ name: 'Enter size (dogs only)', text: 'For dogs, weight is key. Indicate if it is small (up to 10kg), medium (10-25kg), large (25-45kg) or giant (+45kg).' },
|
|
65
|
+
{ name: 'Enter birth year', text: 'Enter your pet\'s birth year to calculate their chronological age.' },
|
|
66
|
+
{ name: 'Analyse the life stage', text: 'Check the result to know if your pet is in infant, young, adult or senior stage compared to a human.' },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const bibliography: PetAgeLocaleContent['bibliography'] = [
|
|
70
|
+
{ name: 'AAHA Senior Care Guidelines for Dogs and Cats', url: 'https://www.aaha.org/wp-content/uploads/globalassets/02-guidelines/2023-aaha-senior-care-guidelines-for-dogs-and-cats/resources/2023-aaha-senior-care-guidelines-for-dogs-and-cats.pdf' },
|
|
71
|
+
{ name: 'AKC: How to Calculate Dog Years to Human Years', url: 'https://www.akc.org/expert-advice/health/how-to-calculate-dog-years-to-human-years/' },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const seo: PetAgeLocaleContent['seo'] = [
|
|
75
|
+
{
|
|
76
|
+
type: 'summary',
|
|
77
|
+
title: 'What You Need to Know About Pet Ageing',
|
|
78
|
+
items: [
|
|
79
|
+
'The "times 7" rule is a myth: dogs mature exponentially in the first 2 years.',
|
|
80
|
+
'A 1-year-old dog is equivalent to 15 human years; a 2-year-old is equivalent to 24 human years.',
|
|
81
|
+
'Large dogs age faster than small ones after 2 years.',
|
|
82
|
+
'Cats follow a more linear curve: each feline year equals 4 human years after age 2.',
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{ type: 'title', text: 'The Scientific Truth About Your Pet\'s Age', level: 2 },
|
|
86
|
+
{
|
|
87
|
+
type: 'paragraph',
|
|
88
|
+
html: 'Did you know that a 2-year-old cat already has the maturity of a 24-year-old person? Discover why the rule of multiplying by 7 is a myth. The old myth that "1 dog year equals 7 human years" is an oversimplification. The biological reality is much more fascinating: pets mature at a dizzying speed during their first two years of life, reaching sexual and physical maturity very quickly. After that, this ageing curve flattens and slows down.',
|
|
89
|
+
},
|
|
90
|
+
{ type: 'title', text: 'The Science Behind the Calculation', level: 3 },
|
|
91
|
+
{
|
|
92
|
+
type: 'paragraph',
|
|
93
|
+
html: 'This tool uses the official guidelines of the <strong>American Animal Hospital Association (AAHA)</strong> and the latest studies on canine DNA methylation to give you the most accurate estimate possible.',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: 'table',
|
|
97
|
+
headers: ['Species', 'Year 1', 'Year 2', 'Year 3+'],
|
|
98
|
+
rows: [
|
|
99
|
+
['Cat', '15 human years', '24 human years', '+4 years per year'],
|
|
100
|
+
['Small dog', '15 human years', '24 human years', '+5 years per year'],
|
|
101
|
+
['Medium dog', '15 human years', '24 human years', '+6 years per year'],
|
|
102
|
+
['Large dog', '15 human years', '24 human years', '+7 years per year'],
|
|
103
|
+
['Giant dog', '15 human years', '24 human years', '+8 years per year'],
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{ type: 'title', text: 'Life Stages: What to Expect?', level: 3 },
|
|
107
|
+
{
|
|
108
|
+
type: 'paragraph',
|
|
109
|
+
html: 'Knowing your pet\'s real age helps you understand their medical and emotional needs at each phase.',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: 'card',
|
|
113
|
+
title: 'Puppy / Junior (0 - 1 year)',
|
|
114
|
+
icon: 'mdi:star-shooting',
|
|
115
|
+
html: 'This is the stage of explosive growth. Their bones grow, their teeth change and their brain absorbs information like a sponge. Vaccination and early socialisation are critical here.',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: 'card',
|
|
119
|
+
title: 'Adult (2 - 6 years)',
|
|
120
|
+
icon: 'mdi:shield-check',
|
|
121
|
+
html: 'Physical and mental prime. Their personality is now defined. This is the golden moment to enjoy intense physical activities and maintain their ideal weight to prevent future diseases.',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: 'card',
|
|
125
|
+
title: 'Senior (+7 years)',
|
|
126
|
+
icon: 'mdi:heart-pulse',
|
|
127
|
+
html: 'Grey hairs start appearing on the muzzle and they sleep more. Metabolism slows down. From here, veterinary check-ups should be every 6 months. Watch for arthritis and dental health.',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: 'tip',
|
|
131
|
+
title: 'Veterinary Advice',
|
|
132
|
+
html: 'The calculated age is an average biological estimate. Factors such as breed, genetics and lifestyle can all significantly influence your pet\'s actual rate of ageing. Always consult your vet for personalised advice.',
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const schemas: PetAgeLocaleContent['schemas'] = [
|
|
137
|
+
{
|
|
138
|
+
'@context': 'https://schema.org',
|
|
139
|
+
'@type': 'SoftwareApplication',
|
|
140
|
+
name: title,
|
|
141
|
+
description: description,
|
|
142
|
+
applicationCategory: 'UtilityApplication',
|
|
143
|
+
operatingSystem: 'Web',
|
|
144
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
145
|
+
} as WithContext<SoftwareApplication>,
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
export const content: PetAgeLocaleContent = {
|
|
149
|
+
slug,
|
|
150
|
+
title,
|
|
151
|
+
description,
|
|
152
|
+
ui,
|
|
153
|
+
seo,
|
|
154
|
+
faqTitle: 'Frequently Asked Questions',
|
|
155
|
+
faq,
|
|
156
|
+
bibliographyTitle: 'Bibliography',
|
|
157
|
+
bibliography,
|
|
158
|
+
howTo,
|
|
159
|
+
schemas,
|
|
160
|
+
};
|