@jjlmoya/utils-tools 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 +63 -0
- package/src/category/i18n/en.ts +172 -0
- package/src/category/i18n/es.ts +172 -0
- package/src/category/i18n/fr.ts +172 -0
- package/src/category/index.ts +23 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +11 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +90 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/locale_completeness.test.ts +42 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/schemas_fulfillment.test.ts +23 -0
- package/src/tests/seo_length.test.ts +23 -0
- package/src/tests/title_quality.test.ts +56 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/date-diff-calculator/bibliography.astro +14 -0
- package/src/tool/date-diff-calculator/component.astro +370 -0
- package/src/tool/date-diff-calculator/i18n/en.ts +132 -0
- package/src/tool/date-diff-calculator/i18n/es.ts +132 -0
- package/src/tool/date-diff-calculator/i18n/fr.ts +132 -0
- package/src/tool/date-diff-calculator/index.ts +22 -0
- package/src/tool/date-diff-calculator/seo.astro +14 -0
- package/src/tool/date-diff-calculator/ui.ts +17 -0
- package/src/tool/drive-direct-link/bibliography.astro +14 -0
- package/src/tool/drive-direct-link/component.astro +280 -0
- package/src/tool/drive-direct-link/i18n/en.ts +118 -0
- package/src/tool/drive-direct-link/i18n/es.ts +118 -0
- package/src/tool/drive-direct-link/i18n/fr.ts +118 -0
- package/src/tool/drive-direct-link/index.ts +22 -0
- package/src/tool/drive-direct-link/seo.astro +14 -0
- package/src/tool/drive-direct-link/ui.ts +10 -0
- package/src/tool/email-list-cleaner/bibliography.astro +14 -0
- package/src/tool/email-list-cleaner/component.astro +375 -0
- package/src/tool/email-list-cleaner/i18n/en.ts +140 -0
- package/src/tool/email-list-cleaner/i18n/es.ts +140 -0
- package/src/tool/email-list-cleaner/i18n/fr.ts +140 -0
- package/src/tool/email-list-cleaner/index.ts +22 -0
- package/src/tool/email-list-cleaner/seo.astro +14 -0
- package/src/tool/email-list-cleaner/ui.ts +15 -0
- package/src/tool/env-badge-spain/bibliography.astro +14 -0
- package/src/tool/env-badge-spain/component.astro +303 -0
- package/src/tool/env-badge-spain/components/BadgeForm.astro +243 -0
- package/src/tool/env-badge-spain/components/BadgeResult.astro +151 -0
- package/src/tool/env-badge-spain/i18n/en.ts +153 -0
- package/src/tool/env-badge-spain/i18n/es.ts +153 -0
- package/src/tool/env-badge-spain/i18n/fr.ts +153 -0
- package/src/tool/env-badge-spain/index.ts +22 -0
- package/src/tool/env-badge-spain/seo.astro +14 -0
- package/src/tool/env-badge-spain/ui.ts +53 -0
- package/src/tool/morse-beacon/bibliography.astro +14 -0
- package/src/tool/morse-beacon/component.astro +534 -0
- package/src/tool/morse-beacon/i18n/en.ts +157 -0
- package/src/tool/morse-beacon/i18n/es.ts +157 -0
- package/src/tool/morse-beacon/i18n/fr.ts +157 -0
- package/src/tool/morse-beacon/index.ts +22 -0
- package/src/tool/morse-beacon/logic/MorseEngine.ts +124 -0
- package/src/tool/morse-beacon/seo.astro +14 -0
- package/src/tool/morse-beacon/ui.ts +18 -0
- package/src/tool/password-generator/bibliography.astro +14 -0
- package/src/tool/password-generator/component.astro +259 -0
- package/src/tool/password-generator/components/Config.astro +227 -0
- package/src/tool/password-generator/components/Display.astro +147 -0
- package/src/tool/password-generator/components/Strength.astro +70 -0
- package/src/tool/password-generator/i18n/en.ts +166 -0
- package/src/tool/password-generator/i18n/es.ts +166 -0
- package/src/tool/password-generator/i18n/fr.ts +166 -0
- package/src/tool/password-generator/index.ts +22 -0
- package/src/tool/password-generator/seo.astro +14 -0
- package/src/tool/password-generator/ui.ts +16 -0
- package/src/tool/routes/bibliography.astro +14 -0
- package/src/tool/routes/component.astro +543 -0
- package/src/tool/routes/i18n/en.ts +157 -0
- package/src/tool/routes/i18n/es.ts +157 -0
- package/src/tool/routes/i18n/fr.ts +157 -0
- package/src/tool/routes/index.ts +22 -0
- package/src/tool/routes/logic/GeocodingService.ts +60 -0
- package/src/tool/routes/logic/RouteManager.ts +192 -0
- package/src/tool/routes/logic/RouteService.ts +66 -0
- package/src/tool/routes/seo.astro +14 -0
- package/src/tool/routes/ui.ts +16 -0
- package/src/tool/rule-of-three/bibliography.astro +14 -0
- package/src/tool/rule-of-three/component.astro +369 -0
- package/src/tool/rule-of-three/i18n/en.ts +171 -0
- package/src/tool/rule-of-three/i18n/es.ts +171 -0
- package/src/tool/rule-of-three/i18n/fr.ts +171 -0
- package/src/tool/rule-of-three/index.ts +22 -0
- package/src/tool/rule-of-three/seo.astro +14 -0
- package/src/tool/rule-of-three/ui.ts +13 -0
- package/src/tool/seo-content-optimizer/bibliography.astro +14 -0
- package/src/tool/seo-content-optimizer/component.astro +552 -0
- package/src/tool/seo-content-optimizer/i18n/en.ts +136 -0
- package/src/tool/seo-content-optimizer/i18n/es.ts +136 -0
- package/src/tool/seo-content-optimizer/i18n/fr.ts +136 -0
- package/src/tool/seo-content-optimizer/index.ts +22 -0
- package/src/tool/seo-content-optimizer/seo.astro +14 -0
- package/src/tool/seo-content-optimizer/ui.ts +29 -0
- package/src/tool/speed-reader/bibliography.astro +14 -0
- package/src/tool/speed-reader/component.astro +586 -0
- package/src/tool/speed-reader/i18n/en.ts +152 -0
- package/src/tool/speed-reader/i18n/es.ts +152 -0
- package/src/tool/speed-reader/i18n/fr.ts +152 -0
- package/src/tool/speed-reader/index.ts +22 -0
- package/src/tool/speed-reader/logic/RSVPEngine.ts +106 -0
- package/src/tool/speed-reader/seo.astro +14 -0
- package/src/tool/speed-reader/ui.ts +14 -0
- package/src/tool/text-pixel-calculator/bibliography.astro +14 -0
- package/src/tool/text-pixel-calculator/component.astro +315 -0
- package/src/tool/text-pixel-calculator/components/Editor.astro +240 -0
- package/src/tool/text-pixel-calculator/components/Preview.astro +155 -0
- package/src/tool/text-pixel-calculator/components/Stats.astro +87 -0
- package/src/tool/text-pixel-calculator/i18n/en.ts +133 -0
- package/src/tool/text-pixel-calculator/i18n/es.ts +133 -0
- package/src/tool/text-pixel-calculator/i18n/fr.ts +133 -0
- package/src/tool/text-pixel-calculator/index.ts +22 -0
- package/src/tool/text-pixel-calculator/seo.astro +14 -0
- package/src/tool/text-pixel-calculator/ui.ts +15 -0
- package/src/tool/whatsapp-link/bibliography.astro +14 -0
- package/src/tool/whatsapp-link/component.astro +455 -0
- package/src/tool/whatsapp-link/i18n/en.ts +128 -0
- package/src/tool/whatsapp-link/i18n/es.ts +128 -0
- package/src/tool/whatsapp-link/i18n/fr.ts +128 -0
- package/src/tool/whatsapp-link/index.ts +22 -0
- package/src/tool/whatsapp-link/seo.astro +14 -0
- package/src/tool/whatsapp-link/ui.ts +15 -0
- package/src/tools.ts +15 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { WhatsappLinkUI } from './ui';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
const t = (ui ?? {}) as WhatsappLinkUI;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="wl-root" data-ui={JSON.stringify(t)}>
|
|
13
|
+
<div class="wl-form">
|
|
14
|
+
<div class="wl-field">
|
|
15
|
+
<label class="wl-label" for="wl-phone">{t.phoneLabel}</label>
|
|
16
|
+
<div class="wl-phone-row">
|
|
17
|
+
<select id="wl-prefix" class="wl-prefix-select" aria-label="Country code">
|
|
18
|
+
<option value="34">🇪🇸 +34</option>
|
|
19
|
+
<option value="33">🇫🇷 +33</option>
|
|
20
|
+
<option value="44">🇬🇧 +44</option>
|
|
21
|
+
<option value="1">🇺🇸 +1</option>
|
|
22
|
+
<option value="52">🇲🇽 +52</option>
|
|
23
|
+
<option value="54">🇦🇷 +54</option>
|
|
24
|
+
<option value="55">🇧🇷 +55</option>
|
|
25
|
+
<option value="56">🇨🇱 +56</option>
|
|
26
|
+
<option value="57">🇨🇴 +57</option>
|
|
27
|
+
<option value="51">🇵🇪 +51</option>
|
|
28
|
+
<option value="49">🇩🇪 +49</option>
|
|
29
|
+
<option value="39">🇮🇹 +39</option>
|
|
30
|
+
<option value="351">🇵🇹 +351</option>
|
|
31
|
+
<option value="31">🇳🇱 +31</option>
|
|
32
|
+
<option value="32">🇧🇪 +32</option>
|
|
33
|
+
<option value="41">🇨🇭 +41</option>
|
|
34
|
+
<option value="48">🇵🇱 +48</option>
|
|
35
|
+
<option value="91">🇮🇳 +91</option>
|
|
36
|
+
<option value="966">🇸🇦 +966</option>
|
|
37
|
+
<option value="971">🇦🇪 +971</option>
|
|
38
|
+
<option value="20">🇪🇬 +20</option>
|
|
39
|
+
<option value="27">🇿🇦 +27</option>
|
|
40
|
+
<option value="234">🇳🇬 +234</option>
|
|
41
|
+
</select>
|
|
42
|
+
<input
|
|
43
|
+
type="tel"
|
|
44
|
+
id="wl-phone"
|
|
45
|
+
class="wl-phone-input"
|
|
46
|
+
placeholder={t.phonePlaceholder}
|
|
47
|
+
inputmode="tel"
|
|
48
|
+
autocomplete="tel-national"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
<span class="wl-error" id="wl-error" role="alert" aria-live="polite"></span>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="wl-field">
|
|
55
|
+
<label class="wl-label" for="wl-message">{t.messageLabel}</label>
|
|
56
|
+
<textarea
|
|
57
|
+
id="wl-message"
|
|
58
|
+
class="wl-textarea"
|
|
59
|
+
placeholder={t.messagePlaceholder}
|
|
60
|
+
rows="3"
|
|
61
|
+
></textarea>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<button type="button" id="wl-generate" class="wl-generate-btn">
|
|
65
|
+
<svg class="wl-btn-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
66
|
+
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
|
|
67
|
+
</svg>
|
|
68
|
+
{t.generateBtn}
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="wl-result" id="wl-result" style="display: none">
|
|
73
|
+
<label class="wl-result-label">{t.resultLabel}</label>
|
|
74
|
+
<div class="wl-result-row">
|
|
75
|
+
<div class="wl-link-display" id="wl-link" role="textbox" aria-readonly="true" tabindex="0"></div>
|
|
76
|
+
<div class="wl-result-actions">
|
|
77
|
+
<button type="button" id="wl-copy" class="wl-action-btn" title={t.copyTitle}>
|
|
78
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
79
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
80
|
+
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
|
|
81
|
+
</svg>
|
|
82
|
+
</button>
|
|
83
|
+
<a
|
|
84
|
+
id="wl-test"
|
|
85
|
+
class="wl-action-btn wl-test-link"
|
|
86
|
+
href="#"
|
|
87
|
+
target="_blank"
|
|
88
|
+
rel="noopener noreferrer"
|
|
89
|
+
title={t.testBtn}
|
|
90
|
+
>
|
|
91
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
92
|
+
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
|
|
93
|
+
<polyline points="15 3 21 3 21 9"></polyline>
|
|
94
|
+
<line x1="10" y1="14" x2="21" y2="3"></line>
|
|
95
|
+
</svg>
|
|
96
|
+
</a>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="wl-qr-wrap">
|
|
100
|
+
<canvas id="wl-qr-canvas" class="wl-qr-canvas"></canvas>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<style>
|
|
106
|
+
.wl-root {
|
|
107
|
+
--wl-green: #25d366;
|
|
108
|
+
--wl-green-dark: #128c7e;
|
|
109
|
+
--wl-green-hover: #1da851;
|
|
110
|
+
--wl-green-focus: rgba(37, 211, 102, 0.15);
|
|
111
|
+
--wl-panel-bg: #fff;
|
|
112
|
+
--wl-panel-border: #e2e8f0;
|
|
113
|
+
--wl-field-bg: #f8fafc;
|
|
114
|
+
--wl-field-border: #e2e8f0;
|
|
115
|
+
--wl-field-focus: #25d366;
|
|
116
|
+
--wl-text-main: #1e293b;
|
|
117
|
+
--wl-text-label: #475569;
|
|
118
|
+
--wl-text-hint: #94a3b8;
|
|
119
|
+
--wl-text-error: #ef4444;
|
|
120
|
+
--wl-result-bg: #f0fdf4;
|
|
121
|
+
--wl-result-border: #bbf7d0;
|
|
122
|
+
--wl-link-color: #065f46;
|
|
123
|
+
--wl-action-bg: #fff;
|
|
124
|
+
--wl-action-border: #d1fae5;
|
|
125
|
+
--wl-action-color: #059669;
|
|
126
|
+
--wl-action-hover: #d1fae5;
|
|
127
|
+
--wl-btn-shadow: rgba(37, 211, 102, 0.35);
|
|
128
|
+
|
|
129
|
+
width: 100%;
|
|
130
|
+
max-width: 42rem;
|
|
131
|
+
margin: 0 auto;
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
gap: 1.25rem;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
:global(.theme-dark) .wl-root {
|
|
138
|
+
--wl-panel-bg: #0f172a;
|
|
139
|
+
--wl-panel-border: #1e293b;
|
|
140
|
+
--wl-field-bg: rgba(30, 41, 59, 0.5);
|
|
141
|
+
--wl-field-border: #334155;
|
|
142
|
+
--wl-text-main: #e2e8f0;
|
|
143
|
+
--wl-text-label: #94a3b8;
|
|
144
|
+
--wl-text-hint: #64748b;
|
|
145
|
+
--wl-result-bg: rgba(6, 78, 59, 0.2);
|
|
146
|
+
--wl-result-border: #065f46;
|
|
147
|
+
--wl-link-color: #6ee7b7;
|
|
148
|
+
--wl-action-bg: rgba(6, 78, 59, 0.3);
|
|
149
|
+
--wl-action-border: #065f46;
|
|
150
|
+
--wl-action-color: #34d399;
|
|
151
|
+
--wl-action-hover: rgba(6, 78, 59, 0.5);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.wl-form {
|
|
155
|
+
background: var(--wl-panel-bg);
|
|
156
|
+
border: 1px solid var(--wl-panel-border);
|
|
157
|
+
border-radius: 1.25rem;
|
|
158
|
+
padding: 1.5rem;
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
gap: 1.25rem;
|
|
162
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.04);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.wl-field {
|
|
166
|
+
display: flex;
|
|
167
|
+
flex-direction: column;
|
|
168
|
+
gap: 0.5rem;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.wl-label {
|
|
172
|
+
font-size: 0.875rem;
|
|
173
|
+
font-weight: 700;
|
|
174
|
+
color: var(--wl-text-label);
|
|
175
|
+
letter-spacing: 0.02em;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.wl-phone-row {
|
|
179
|
+
display: flex;
|
|
180
|
+
gap: 0.5rem;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.wl-prefix-select {
|
|
184
|
+
flex-shrink: 0;
|
|
185
|
+
width: 7.5rem;
|
|
186
|
+
background: var(--wl-field-bg);
|
|
187
|
+
border: 1px solid var(--wl-field-border);
|
|
188
|
+
border-radius: 0.75rem;
|
|
189
|
+
padding: 0.75rem 0.625rem;
|
|
190
|
+
font-size: 0.875rem;
|
|
191
|
+
color: var(--wl-text-main);
|
|
192
|
+
outline: none;
|
|
193
|
+
cursor: pointer;
|
|
194
|
+
transition: border-color 0.2s;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.wl-prefix-select:focus {
|
|
198
|
+
border-color: var(--wl-field-focus);
|
|
199
|
+
box-shadow: 0 0 0 3px var(--wl-green-focus);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.wl-phone-input {
|
|
203
|
+
flex: 1;
|
|
204
|
+
background: var(--wl-field-bg);
|
|
205
|
+
border: 1px solid var(--wl-field-border);
|
|
206
|
+
border-radius: 0.75rem;
|
|
207
|
+
padding: 0.75rem 1rem;
|
|
208
|
+
font-size: 0.875rem;
|
|
209
|
+
color: var(--wl-text-main);
|
|
210
|
+
outline: none;
|
|
211
|
+
transition: border-color 0.2s;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.wl-phone-input::placeholder {
|
|
215
|
+
color: var(--wl-text-hint);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.wl-phone-input:focus {
|
|
219
|
+
border-color: var(--wl-field-focus);
|
|
220
|
+
box-shadow: 0 0 0 3px var(--wl-green-focus);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.wl-error {
|
|
224
|
+
font-size: 0.8rem;
|
|
225
|
+
color: var(--wl-text-error);
|
|
226
|
+
min-height: 1.1rem;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.wl-textarea {
|
|
230
|
+
width: 100%;
|
|
231
|
+
background: var(--wl-field-bg);
|
|
232
|
+
border: 1px solid var(--wl-field-border);
|
|
233
|
+
border-radius: 0.75rem;
|
|
234
|
+
padding: 0.75rem 1rem;
|
|
235
|
+
font-size: 0.875rem;
|
|
236
|
+
color: var(--wl-text-main);
|
|
237
|
+
line-height: 1.6;
|
|
238
|
+
resize: vertical;
|
|
239
|
+
outline: none;
|
|
240
|
+
transition: border-color 0.2s;
|
|
241
|
+
box-sizing: border-box;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.wl-textarea::placeholder {
|
|
245
|
+
color: var(--wl-text-hint);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.wl-textarea:focus {
|
|
249
|
+
border-color: var(--wl-field-focus);
|
|
250
|
+
box-shadow: 0 0 0 3px var(--wl-green-focus);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.wl-generate-btn {
|
|
254
|
+
display: flex;
|
|
255
|
+
align-items: center;
|
|
256
|
+
justify-content: center;
|
|
257
|
+
gap: 0.625rem;
|
|
258
|
+
width: 100%;
|
|
259
|
+
background: var(--wl-green);
|
|
260
|
+
color: #fff;
|
|
261
|
+
border: none;
|
|
262
|
+
border-radius: 0.875rem;
|
|
263
|
+
padding: 0.875rem 1.5rem;
|
|
264
|
+
font-size: 1rem;
|
|
265
|
+
font-weight: 700;
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
box-shadow: 0 4px 14px var(--wl-btn-shadow);
|
|
268
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.wl-generate-btn:hover {
|
|
272
|
+
background: var(--wl-green-hover);
|
|
273
|
+
transform: translateY(-1px);
|
|
274
|
+
box-shadow: 0 6px 20px var(--wl-btn-shadow);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.wl-generate-btn:active {
|
|
278
|
+
transform: translateY(0);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.wl-btn-icon {
|
|
282
|
+
width: 1.25rem;
|
|
283
|
+
height: 1.25rem;
|
|
284
|
+
flex-shrink: 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.wl-result {
|
|
288
|
+
background: var(--wl-result-bg);
|
|
289
|
+
border: 1px solid var(--wl-result-border);
|
|
290
|
+
border-radius: 1.25rem;
|
|
291
|
+
padding: 1.25rem 1.5rem;
|
|
292
|
+
display: flex;
|
|
293
|
+
flex-direction: column;
|
|
294
|
+
gap: 0.75rem;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.wl-result-label {
|
|
298
|
+
font-size: 0.75rem;
|
|
299
|
+
font-weight: 700;
|
|
300
|
+
text-transform: uppercase;
|
|
301
|
+
letter-spacing: 0.08em;
|
|
302
|
+
color: var(--wl-green-dark);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
:global(.theme-dark) .wl-result-label {
|
|
306
|
+
color: var(--wl-green);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.wl-result-row {
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
gap: 0.625rem;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.wl-link-display {
|
|
316
|
+
flex: 1;
|
|
317
|
+
font-size: 0.8rem;
|
|
318
|
+
font-weight: 500;
|
|
319
|
+
color: var(--wl-link-color);
|
|
320
|
+
word-break: break-all;
|
|
321
|
+
line-height: 1.5;
|
|
322
|
+
padding: 0.5rem 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.wl-result-actions {
|
|
326
|
+
display: flex;
|
|
327
|
+
gap: 0.5rem;
|
|
328
|
+
flex-shrink: 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.wl-action-btn {
|
|
332
|
+
display: flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
justify-content: center;
|
|
335
|
+
width: 2.5rem;
|
|
336
|
+
height: 2.5rem;
|
|
337
|
+
background: var(--wl-action-bg);
|
|
338
|
+
border: 1px solid var(--wl-action-border);
|
|
339
|
+
border-radius: 0.625rem;
|
|
340
|
+
color: var(--wl-action-color);
|
|
341
|
+
cursor: pointer;
|
|
342
|
+
text-decoration: none;
|
|
343
|
+
transition: background 0.15s, transform 0.15s;
|
|
344
|
+
flex-shrink: 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.wl-action-btn:hover {
|
|
348
|
+
background: var(--wl-action-hover);
|
|
349
|
+
transform: scale(1.05);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.wl-action-btn svg {
|
|
353
|
+
width: 1.125rem;
|
|
354
|
+
height: 1.125rem;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.wl-qr-wrap {
|
|
358
|
+
display: flex;
|
|
359
|
+
justify-content: center;
|
|
360
|
+
padding-top: 0.75rem;
|
|
361
|
+
border-top: 1px solid var(--wl-result-border);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.wl-qr-canvas {
|
|
365
|
+
border-radius: 0.75rem;
|
|
366
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
367
|
+
}
|
|
368
|
+
</style>
|
|
369
|
+
|
|
370
|
+
<script>
|
|
371
|
+
import type { WhatsappLinkUI } from './ui';
|
|
372
|
+
|
|
373
|
+
const root = document.querySelector('.wl-root') as HTMLElement;
|
|
374
|
+
const t = JSON.parse(root?.dataset.ui ?? '{}') as WhatsappLinkUI;
|
|
375
|
+
|
|
376
|
+
const prefixSelect = document.getElementById('wl-prefix') as HTMLSelectElement;
|
|
377
|
+
|
|
378
|
+
if (t.defaultPrefix) prefixSelect.value = t.defaultPrefix;
|
|
379
|
+
const phoneInput = document.getElementById('wl-phone') as HTMLInputElement;
|
|
380
|
+
const messageInput = document.getElementById('wl-message') as HTMLTextAreaElement;
|
|
381
|
+
const generateBtn = document.getElementById('wl-generate') as HTMLButtonElement;
|
|
382
|
+
const resultSection = document.getElementById('wl-result') as HTMLDivElement;
|
|
383
|
+
const linkDisplay = document.getElementById('wl-link') as HTMLDivElement;
|
|
384
|
+
const copyBtn = document.getElementById('wl-copy') as HTMLButtonElement;
|
|
385
|
+
const testLink = document.getElementById('wl-test') as HTMLAnchorElement;
|
|
386
|
+
const qrCanvas = document.getElementById('wl-qr-canvas') as HTMLCanvasElement;
|
|
387
|
+
const errorSpan = document.getElementById('wl-error') as HTMLSpanElement;
|
|
388
|
+
|
|
389
|
+
let currentUrl = '';
|
|
390
|
+
|
|
391
|
+
function cleanPhone(raw: string): string {
|
|
392
|
+
return raw.replace(/\D/g, '');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function buildUrl(prefix: string, phone: string, message: string): string {
|
|
396
|
+
const base = `https://wa.me/${prefix}${phone}`;
|
|
397
|
+
if (message.trim()) {
|
|
398
|
+
return `${base}?text=${encodeURIComponent(message.trim())}`;
|
|
399
|
+
}
|
|
400
|
+
return base;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function showError(msg: string) {
|
|
404
|
+
errorSpan.textContent = msg;
|
|
405
|
+
phoneInput.style.borderColor = 'var(--wl-text-error)';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function clearError() {
|
|
409
|
+
errorSpan.textContent = '';
|
|
410
|
+
phoneInput.style.borderColor = '';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function generate() {
|
|
414
|
+
const prefix = prefixSelect.value;
|
|
415
|
+
const phone = cleanPhone(phoneInput.value);
|
|
416
|
+
const message = messageInput.value;
|
|
417
|
+
|
|
418
|
+
if (phone.length < 5) {
|
|
419
|
+
showError(t.errorPhone);
|
|
420
|
+
resultSection.style.display = 'none';
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
clearError();
|
|
425
|
+
currentUrl = buildUrl(prefix, phone, message);
|
|
426
|
+
linkDisplay.textContent = currentUrl;
|
|
427
|
+
testLink.href = currentUrl;
|
|
428
|
+
resultSection.style.display = 'flex';
|
|
429
|
+
|
|
430
|
+
const qrcode = await import('qrcode');
|
|
431
|
+
await qrcode.toCanvas(qrCanvas, currentUrl, {
|
|
432
|
+
width: 256,
|
|
433
|
+
margin: 2,
|
|
434
|
+
color: { dark: '#000000', light: '#ffffff' },
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
generateBtn.addEventListener('click', generate);
|
|
439
|
+
|
|
440
|
+
phoneInput.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
441
|
+
if (e.key === 'Enter') { generate(); }
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
copyBtn.addEventListener('click', async () => {
|
|
445
|
+
if (!currentUrl) return;
|
|
446
|
+
await navigator.clipboard.writeText(currentUrl);
|
|
447
|
+
const prev = copyBtn.title;
|
|
448
|
+
copyBtn.title = t.copyDone;
|
|
449
|
+
copyBtn.setAttribute('aria-label', t.copyDone);
|
|
450
|
+
setTimeout(() => {
|
|
451
|
+
copyBtn.title = prev;
|
|
452
|
+
copyBtn.setAttribute('aria-label', prev);
|
|
453
|
+
}, 2000);
|
|
454
|
+
});
|
|
455
|
+
</script>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
|
+
import type { WhatsappLinkUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const faqData = [
|
|
6
|
+
{
|
|
7
|
+
question: 'How do I generate a WhatsApp link?',
|
|
8
|
+
answer: 'To create your link, enter your mobile number including the country code. For example, for Spain put 34 first, for Mexico 52, followed by your local number. You can add an optional message and the tool will generate the final wa.me link ready to copy.',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
question: 'Can I add a long pre-filled message?',
|
|
12
|
+
answer: 'Yes. You can attach a message that will automatically appear in the chat box when someone clicks your link. The tool uses URL encoding to support spaces, accents and emojis without breaking the URL.',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
question: 'How do I use the generated QR code?',
|
|
16
|
+
answer: 'Once a valid link is generated, the "Show QR" button appears. The QR code contains your link URL. Right-click on it and select "Save image as" to download a clean, high-resolution file for business cards, posters or social media.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
question: 'Where does my data go?',
|
|
20
|
+
answer: 'The generator processes everything client-side, directly in your browser. Your phone number and pre-filled message are never sent to any server or stored in any database.',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const howToData = [
|
|
25
|
+
{ name: 'Select the prefix', text: 'Choose your country code from the dropdown and enter your phone number without the prefix.' },
|
|
26
|
+
{ name: 'Add an optional message', text: 'Write a pre-filled text that will appear when someone opens the link and taps Send.' },
|
|
27
|
+
{ name: 'Generate the link', text: 'Press the green button to get your direct wa.me URL ready to share.' },
|
|
28
|
+
{ name: 'Share or print the QR', text: 'Copy the link, test the chat or download the QR code for cards and printing.' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
32
|
+
'@context': 'https://schema.org',
|
|
33
|
+
'@type': 'FAQPage',
|
|
34
|
+
mainEntity: faqData.map((item) => ({
|
|
35
|
+
'@type': 'Question',
|
|
36
|
+
name: item.question,
|
|
37
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const howToSchema: WithContext<HowTo> = {
|
|
42
|
+
'@context': 'https://schema.org',
|
|
43
|
+
'@type': 'HowTo',
|
|
44
|
+
name: 'How to create a direct WhatsApp link',
|
|
45
|
+
step: howToData.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
49
|
+
'@context': 'https://schema.org',
|
|
50
|
+
'@type': 'SoftwareApplication',
|
|
51
|
+
name: 'WhatsApp Link Generator',
|
|
52
|
+
applicationCategory: 'UtilitiesApplication',
|
|
53
|
+
operatingSystem: 'Web',
|
|
54
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
55
|
+
description: 'Create direct WhatsApp chat links with pre-filled message and QR code. Free tool, no registration, 100% private.',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const ui: WhatsappLinkUI = {
|
|
59
|
+
phoneLabel: 'WhatsApp Phone Number',
|
|
60
|
+
phonePlaceholder: '600 00 00 00',
|
|
61
|
+
messageLabel: 'Opening message (optional)',
|
|
62
|
+
messagePlaceholder: 'Hi! I would like more information about your service...',
|
|
63
|
+
generateBtn: 'Generate Link',
|
|
64
|
+
resultLabel: 'Direct Link Generated',
|
|
65
|
+
copyTitle: 'Copy to clipboard',
|
|
66
|
+
copyDone: 'Copied!',
|
|
67
|
+
testBtn: 'Test Chat',
|
|
68
|
+
qrShow: 'Show QR',
|
|
69
|
+
qrHide: 'Hide QR',
|
|
70
|
+
errorPhone: 'Please enter a valid phone number.',
|
|
71
|
+
defaultPrefix: '44',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const content: ToolLocaleContent<WhatsappLinkUI> = {
|
|
75
|
+
slug: 'whatsapp-link-generator',
|
|
76
|
+
title: 'WhatsApp Link Generator with QR',
|
|
77
|
+
description: 'Create direct WhatsApp chat links with pre-filled message and QR code. Free tool, no registration, 100% private.',
|
|
78
|
+
ui,
|
|
79
|
+
faqTitle: 'Frequently Asked Questions',
|
|
80
|
+
faq: faqData,
|
|
81
|
+
howTo: howToData,
|
|
82
|
+
bibliographyTitle: 'References',
|
|
83
|
+
bibliography: [
|
|
84
|
+
{ name: 'How to use the click to chat feature — WhatsApp Help Center', url: 'https://faq.whatsapp.com/591339899867293' },
|
|
85
|
+
{ name: 'API Click to chat: parameters and formats — WhatsApp', url: 'https://faq.whatsapp.com/425559092497645' },
|
|
86
|
+
],
|
|
87
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
88
|
+
seo: [
|
|
89
|
+
{ type: 'title', level: 2, text: 'Generate short direct links for WhatsApp' },
|
|
90
|
+
{
|
|
91
|
+
type: 'paragraph',
|
|
92
|
+
html: 'Need customers or followers to contact you on WhatsApp without saving your number first? Our <strong>wa.me link creator</strong> solves this by generating a link that opens a direct chat instantly, no prior steps required.',
|
|
93
|
+
},
|
|
94
|
+
{ type: 'title', level: 3, text: 'What is the wa.me WhatsApp shortener for?' },
|
|
95
|
+
{
|
|
96
|
+
type: 'paragraph',
|
|
97
|
+
html: 'WhatsApp offers an API called "Click to Chat". Using a special URL, any user can open a new chat with you without needing to add you as a contact first, from both mobile and WhatsApp Web.',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'list',
|
|
101
|
+
items: [
|
|
102
|
+
'<strong>More conversions:</strong> A "Contact via WhatsApp" button on your shop reduces friction and increases sales.',
|
|
103
|
+
'<strong>Pre-filled message:</strong> The user just presses "Send". Example: "Hi! I came from Instagram and want to claim the offer."',
|
|
104
|
+
'<strong>Automatic QR code:</strong> Download the QR for business cards, posters or social media posts.',
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
{ type: 'title', level: 3, text: 'How does link generation work?' },
|
|
108
|
+
{
|
|
109
|
+
type: 'paragraph',
|
|
110
|
+
html: 'The tool concatenates the international prefix and the clean phone number, then appends it to the official WhatsApp API together with the message parameter converted via <em>url-encoding</em>.',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
type: 'code',
|
|
114
|
+
ariaLabel: 'WhatsApp URL format',
|
|
115
|
+
code: 'https://wa.me/34XXXXXXXXX\nhttps://wa.me/34XXXXXXXXX?text=%C2%A1Hola!%20Me%20gustar%C3%ADa...',
|
|
116
|
+
},
|
|
117
|
+
{ type: 'title', level: 3, text: 'Guaranteed privacy and local processing' },
|
|
118
|
+
{
|
|
119
|
+
type: 'paragraph',
|
|
120
|
+
html: 'All link construction happens in your browser via JavaScript. No server records, saves or reads the phone numbers or messages you enter. Your privacy is fully protected.',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'tip',
|
|
124
|
+
title: 'Importance of the international prefix',
|
|
125
|
+
html: 'For WhatsApp to route the message correctly, the country code is mandatory. For Spain it is <strong>34</strong>, followed by the 9-digit number, no spaces or + symbol. The final result would be, for example, <code>346XXXXXXXX</code>.',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|