@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,118 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
|
+
import type { DriveDirectLinkUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const faqData = [
|
|
6
|
+
{
|
|
7
|
+
question: '¿Cómo uso el conversor de descargas de Google Drive?',
|
|
8
|
+
answer: 'Copia el enlace completo de "Compartir" de cualquier archivo subido a Google Drive (con permisos públicos). Pégalo en el cuadro y pulsa "Generar Enlace Directo". Aparecerá una nueva URL que iniciará la descarga sin abrir el visor de Google.',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
question: '¿Puedo hacer la descarga directa de un archivo privado?',
|
|
12
|
+
answer: 'Si el archivo es privado o con acceso restringido, Google solicitará que inicies sesión con una cuenta autorizada. Para descargas sin registro, los permisos del archivo deben estar en "Cualquier persona con el enlace puede ver".',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
question: '¿Es seguro convertir mis links de Drive?',
|
|
16
|
+
answer: 'Sí. La conversión ocurre 100% en tu navegador de forma anónima. La herramienta no almacena tu enlace ni hace llamadas a servidores; únicamente analiza la estructura de la URL en el lado del cliente para generar la versión de descarga.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
question: '¿Qué pasa si mi archivo de Drive pesa mucho?',
|
|
20
|
+
answer: 'Para archivos superiores a aproximadamente 100 MB, Google Drive no permite la descarga instantánea directa. En su lugar mostrará una página de advertencia sobre el tamaño y el escaneo de antivirus. Es una política del servidor de Google que ninguna herramienta externa puede eludir.',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const howToData = [
|
|
25
|
+
{ name: 'Copia el enlace de compartir', text: 'En Google Drive, haz clic derecho sobre el archivo y selecciona "Obtener enlace". Asegúrate de que los permisos permiten el acceso a cualquier persona.' },
|
|
26
|
+
{ name: 'Pega el enlace en el generador', text: 'Introduce la URL completa de Drive en el campo de texto de la herramienta y pulsa el botón "Generar Enlace Directo".' },
|
|
27
|
+
{ name: 'Copia y usa el enlace generado', text: 'Copia el nuevo enlace de descarga directa y úsalo en tus correos, sitio web, newsletters o donde necesites que el archivo se descargue de inmediato.' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
31
|
+
'@context': 'https://schema.org',
|
|
32
|
+
'@type': 'FAQPage',
|
|
33
|
+
mainEntity: faqData.map((item) => ({
|
|
34
|
+
'@type': 'Question',
|
|
35
|
+
name: item.question,
|
|
36
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const howToSchema: WithContext<HowTo> = {
|
|
41
|
+
'@context': 'https://schema.org',
|
|
42
|
+
'@type': 'HowTo',
|
|
43
|
+
name: 'Cómo generar un enlace de descarga directa para Google Drive',
|
|
44
|
+
step: howToData.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
48
|
+
'@context': 'https://schema.org',
|
|
49
|
+
'@type': 'SoftwareApplication',
|
|
50
|
+
name: 'Generador de Enlace de Descarga Directa para Google Drive',
|
|
51
|
+
applicationCategory: 'UtilitiesApplication',
|
|
52
|
+
operatingSystem: 'Web',
|
|
53
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
54
|
+
description: 'Convierte cualquier enlace de compartir de Google Drive en un enlace de descarga directa en un solo clic, sin pasar por la vista previa del archivo.',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const ui: DriveDirectLinkUI = {
|
|
58
|
+
inputLabel: 'Pega tu enlace de Google Drive aquí:',
|
|
59
|
+
inputPlaceholder: 'https://drive.google.com/file/d/...',
|
|
60
|
+
generateBtn: 'Generar Enlace Directo',
|
|
61
|
+
resultLabel: 'Enlace de Descarga Directa:',
|
|
62
|
+
copyTitle: 'Copiar al portapapeles',
|
|
63
|
+
copyDone: 'Copiado',
|
|
64
|
+
testBtn: 'Testear Descarga',
|
|
65
|
+
errorMsg: 'El enlace introducido no parece ser un enlace válido de Google Drive.',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const content: ToolLocaleContent<DriveDirectLinkUI> = {
|
|
69
|
+
slug: 'enlace-descarga-directa-google-drive',
|
|
70
|
+
title: 'Generador de Enlace de Descarga Directa para Google Drive',
|
|
71
|
+
description: 'Convierte fácilmente cualquier enlace de compartir de Google Drive en un enlace de descarga directa en un solo clic, sin pasar por la vista previa del archivo, gratis.',
|
|
72
|
+
ui,
|
|
73
|
+
faqTitle: 'Preguntas Frecuentes',
|
|
74
|
+
faq: faqData,
|
|
75
|
+
howTo: howToData,
|
|
76
|
+
bibliographyTitle: 'Referencias',
|
|
77
|
+
bibliography: [
|
|
78
|
+
{ name: 'Descargar archivos de Google Drive (Google Workspace)', url: 'https://support.google.com/drive/answer/2423534' },
|
|
79
|
+
{ name: 'Google Drive API: Download Files', url: 'https://developers.google.com/drive/api/guides/manage-downloads' },
|
|
80
|
+
],
|
|
81
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
82
|
+
seo: [
|
|
83
|
+
{ type: 'title', level: 2, text: 'Conversor de enlaces de Google Drive a descarga directa' },
|
|
84
|
+
{
|
|
85
|
+
type: 'paragraph',
|
|
86
|
+
html: 'Si compartes archivos frecuentemente mediante Google Drive, sabes lo molesto que puede ser el visor de vista previa. Cuando tus usuarios hacen clic en tu enlace, primero van a una pantalla intermedia que les obliga a buscar el botón de descarga. Nuestro <strong>generador de enlaces de descarga directa para Google Drive</strong> soluciona este problema convirtiendo cualquier enlace de compartir en uno que inicia la descarga automáticamente.',
|
|
87
|
+
},
|
|
88
|
+
{ type: 'title', level: 3, text: '¿Por qué utilizar un enlace de descarga directa?' },
|
|
89
|
+
{
|
|
90
|
+
type: 'paragraph',
|
|
91
|
+
html: 'El principal beneficio es mejorar enormemente la experiencia del usuario. Al enviar un PDF, una imagen o un instalador de software, el comportamiento esperado es que al hacer clic la descarga empiece de inmediato:',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: 'list',
|
|
95
|
+
items: [
|
|
96
|
+
'<strong>Ahorro de tiempo:</strong> Descarga con un solo clic sin pasos adicionales.',
|
|
97
|
+
'<strong>Profesionalidad:</strong> Ideal para botones de descarga en sitios web, newsletters o correos a clientes.',
|
|
98
|
+
'<strong>Reducción de abandonos:</strong> Usuarios menos técnicos pueden confundirse en la vista previa de Drive sin saber dónde descargar.',
|
|
99
|
+
'<strong>Uso en HTML:</strong> Perfecto para atributos <code>href</code> o <code>src</code> en tus páginas.',
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{ type: 'title', level: 3, text: '¿Cómo funciona nuestro creador de links directos?' },
|
|
103
|
+
{
|
|
104
|
+
type: 'paragraph',
|
|
105
|
+
html: 'El proceso ocurre 100% en tu navegador. Google Drive estructura sus enlaces de compartir de dos formas principales, ambas con un identificador único de archivo. La herramienta extrae ese ID y construye una nueva URL con el parámetro de exportación nativo de Google: <code>https://drive.google.com/uc?export=download&id=TU_ID</code>.',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: 'tip',
|
|
109
|
+
title: 'Aviso sobre archivos de gran tamaño',
|
|
110
|
+
html: 'Para archivos superiores a aproximadamente 100 MB, Google Drive mostrará una página de advertencia de escaneo de antivirus antes de la descarga. Es una política inamovible de los servidores de Google que ninguna herramienta externa puede eludir.',
|
|
111
|
+
},
|
|
112
|
+
{ type: 'title', level: 3, text: 'Privacidad y seguridad garantizadas' },
|
|
113
|
+
{
|
|
114
|
+
type: 'paragraph',
|
|
115
|
+
html: 'Esta herramienta respeta por completo tu privacidad. Todo el proceso de transformación se realiza mediante JavaScript en el lado del cliente, en tu propio dispositivo. Ningún enlace, ID de archivo o información personal se envía a ningún servidor externo.',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
|
+
import type { DriveDirectLinkUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const faqData = [
|
|
6
|
+
{
|
|
7
|
+
question: 'Comment utiliser le convertisseur de téléchargement Google Drive ?',
|
|
8
|
+
answer: 'Copiez le lien de partage complet de n\'importe quel fichier stocké sur Google Drive (il doit avoir des permissions publiques). Collez-le dans le champ de l\'outil et cliquez sur "Générer le lien direct". Une nouvelle URL apparaîtra qui lancera le téléchargement sans ouvrir le visionneur de Google.',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
question: 'Puis-je générer un lien de téléchargement direct pour un fichier privé ?',
|
|
12
|
+
answer: 'Si le fichier est privé ou à accès restreint, Google vous demandera de vous connecter avec un compte autorisé. Pour des téléchargements sans inscription, les permissions du fichier doivent être réglées sur "Toute personne disposant du lien peut afficher".',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
question: 'Est-il sécurisé de convertir mes liens Drive ici ?',
|
|
16
|
+
answer: 'Oui. La conversion se fait à 100 % dans votre navigateur de manière anonyme. L\'outil ne stocke pas votre lien et n\'effectue aucun appel serveur ; il analyse uniquement la structure de l\'URL côté client pour générer la version de téléchargement.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
question: 'Que se passe-t-il si mon fichier Drive est très volumineux ?',
|
|
20
|
+
answer: 'Pour les fichiers de plus d\'environ 100 Mo, Google Drive ne permet pas le téléchargement direct instantané. Il affichera à la place une page d\'avertissement concernant le scan antivirus. Il s\'agit d\'une politique fixe des serveurs Google qu\'aucun outil externe ne peut contourner.',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const howToData = [
|
|
25
|
+
{ name: 'Copiez le lien de partage', text: 'Dans Google Drive, faites un clic droit sur le fichier et sélectionnez "Obtenir le lien". Assurez-vous que les permissions permettent l\'accès à toute personne disposant du lien.' },
|
|
26
|
+
{ name: 'Collez le lien dans le générateur', text: 'Saisissez l\'URL complète de Drive dans le champ texte de l\'outil et cliquez sur le bouton "Générer le lien direct".' },
|
|
27
|
+
{ name: 'Copiez et utilisez le lien généré', text: 'Copiez le nouveau lien de téléchargement direct et utilisez-le dans vos e-mails, sites web, newsletters ou partout où vous souhaitez que le fichier se télécharge immédiatement au clic.' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
31
|
+
'@context': 'https://schema.org',
|
|
32
|
+
'@type': 'FAQPage',
|
|
33
|
+
mainEntity: faqData.map((item) => ({
|
|
34
|
+
'@type': 'Question',
|
|
35
|
+
name: item.question,
|
|
36
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const howToSchema: WithContext<HowTo> = {
|
|
41
|
+
'@context': 'https://schema.org',
|
|
42
|
+
'@type': 'HowTo',
|
|
43
|
+
name: 'Comment générer un lien de téléchargement direct pour Google Drive',
|
|
44
|
+
step: howToData.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
48
|
+
'@context': 'https://schema.org',
|
|
49
|
+
'@type': 'SoftwareApplication',
|
|
50
|
+
name: 'Générateur de Lien de Téléchargement Direct Google Drive',
|
|
51
|
+
applicationCategory: 'UtilitiesApplication',
|
|
52
|
+
operatingSystem: 'Web',
|
|
53
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
54
|
+
description: 'Convertissez n\'importe quel lien de partage Google Drive en un lien de téléchargement direct en un clic, sans passer par la page de prévisualisation du fichier.',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const ui: DriveDirectLinkUI = {
|
|
58
|
+
inputLabel: 'Collez votre lien Google Drive ici :',
|
|
59
|
+
inputPlaceholder: 'https://drive.google.com/file/d/...',
|
|
60
|
+
generateBtn: 'Générer le lien direct',
|
|
61
|
+
resultLabel: 'Lien de téléchargement direct :',
|
|
62
|
+
copyTitle: 'Copier dans le presse-papiers',
|
|
63
|
+
copyDone: 'Copié',
|
|
64
|
+
testBtn: 'Tester le téléchargement',
|
|
65
|
+
errorMsg: 'Le lien saisi ne semble pas être un lien Google Drive valide.',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const content: ToolLocaleContent<DriveDirectLinkUI> = {
|
|
69
|
+
slug: 'lien-telechargement-direct-google-drive',
|
|
70
|
+
title: 'Générateur de Lien de Téléchargement Direct Google Drive',
|
|
71
|
+
description: 'Convertissez facilement n\'importe quel lien de partage Google Drive en un lien de téléchargement direct en un seul clic, sans passer par la prévisualisation du fichier.',
|
|
72
|
+
ui,
|
|
73
|
+
faqTitle: 'Questions Fréquentes',
|
|
74
|
+
faq: faqData,
|
|
75
|
+
howTo: howToData,
|
|
76
|
+
bibliographyTitle: 'Références',
|
|
77
|
+
bibliography: [
|
|
78
|
+
{ name: 'Télécharger des fichiers depuis Google Drive (Google Workspace)', url: 'https://support.google.com/drive/answer/2423534' },
|
|
79
|
+
{ name: 'Google Drive API : téléchargement de fichiers', url: 'https://developers.google.com/drive/api/guides/manage-downloads' },
|
|
80
|
+
],
|
|
81
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
82
|
+
seo: [
|
|
83
|
+
{ type: 'title', level: 2, text: 'Convertir des liens Google Drive en liens de téléchargement direct' },
|
|
84
|
+
{
|
|
85
|
+
type: 'paragraph',
|
|
86
|
+
html: 'Si vous partagez fréquemment des fichiers via Google Drive, vous savez combien la page de prévisualisation peut être gênante. Vos utilisateurs atterrissent sur un écran intermédiaire qui les oblige à chercher le bouton de téléchargement. Notre <strong>générateur de liens de téléchargement direct Google Drive</strong> résout ce problème en convertissant n\'importe quel lien de partage en un lien qui lance le téléchargement automatiquement.',
|
|
87
|
+
},
|
|
88
|
+
{ type: 'title', level: 3, text: 'Pourquoi utiliser un lien de téléchargement direct ?' },
|
|
89
|
+
{
|
|
90
|
+
type: 'paragraph',
|
|
91
|
+
html: 'Le principal avantage est une bien meilleure expérience utilisateur. Lorsque vous partagez un PDF, une image ou un logiciel, l\'utilisateur s\'attend à ce que le fichier commence à se télécharger immédiatement :',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: 'list',
|
|
95
|
+
items: [
|
|
96
|
+
'<strong>Gain de temps :</strong> Téléchargement en un clic sans étapes supplémentaires.',
|
|
97
|
+
'<strong>Image professionnelle :</strong> Idéal pour les boutons de téléchargement sur les sites web, newsletters ou e-mails clients.',
|
|
98
|
+
'<strong>Moins d\'abandons :</strong> Les utilisateurs moins techniques peuvent être désorientés par la prévisualisation Drive.',
|
|
99
|
+
'<strong>Compatible HTML :</strong> Parfait pour les attributs <code>href</code> ou <code>src</code> de vos pages web.',
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{ type: 'title', level: 3, text: 'Comment fonctionne le générateur de lien direct ?' },
|
|
103
|
+
{
|
|
104
|
+
type: 'paragraph',
|
|
105
|
+
html: 'Tout se passe à 100 % dans votre navigateur. Les liens de partage Google Drive contiennent un identifiant unique de fichier. L\'outil extrait cet identifiant et construit une nouvelle URL avec le paramètre d\'export natif de Google : <code>https://drive.google.com/uc?export=download&id=VOTRE_ID</code>.',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: 'tip',
|
|
109
|
+
title: 'Avertissement pour les fichiers volumineux',
|
|
110
|
+
html: 'Pour les fichiers de plus d\'environ 100 Mo, Google Drive affiche une page d\'avertissement de scan antivirus avant le téléchargement. Il s\'agit d\'une politique fixe des serveurs Google qu\'aucun outil externe ne peut contourner.',
|
|
111
|
+
},
|
|
112
|
+
{ type: 'title', level: 3, text: 'Confidentialité et sécurité garanties' },
|
|
113
|
+
{
|
|
114
|
+
type: 'paragraph',
|
|
115
|
+
html: 'Cet outil respecte totalement votre vie privée. Toute la transformation du lien est effectuée via JavaScript côté client, sur votre propre appareil. Aucun lien, identifiant de fichier ou information personnelle n\'est jamais envoyé à un serveur externe.',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ToolDefinition, ToolsToolEntry } from '../../types';
|
|
2
|
+
import type { DriveDirectLinkUI } from './ui';
|
|
3
|
+
import DriveDirectLinkComponent from './component.astro';
|
|
4
|
+
import DriveDirectLinkSEO from './seo.astro';
|
|
5
|
+
import DriveDirectLinkBibliography from './bibliography.astro';
|
|
6
|
+
|
|
7
|
+
export const driveDirectLink: ToolsToolEntry<DriveDirectLinkUI> = {
|
|
8
|
+
id: 'drive-direct-link',
|
|
9
|
+
icons: { bg: 'mdi:google-drive', fg: 'mdi:download' },
|
|
10
|
+
i18n: {
|
|
11
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
12
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
13
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const DRIVE_DIRECT_LINK_TOOL: ToolDefinition = {
|
|
18
|
+
entry: driveDirectLink,
|
|
19
|
+
Component: DriveDirectLinkComponent,
|
|
20
|
+
SEOComponent: DriveDirectLinkSEO,
|
|
21
|
+
BibliographyComponent: DriveDirectLinkBibliography,
|
|
22
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { driveDirectLink } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const content = await driveDirectLink.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { emailListCleaner } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const content = await emailListCleaner.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { EmailListCleanerUI } from './ui';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
const t = (ui ?? {}) as EmailListCleanerUI;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="elc-root" data-ui={JSON.stringify(t)}>
|
|
13
|
+
<div class="elc-card">
|
|
14
|
+
<div class="elc-field">
|
|
15
|
+
<div class="elc-label-row">
|
|
16
|
+
<label class="elc-label" for="elc-input">{t.inputLabel}</label>
|
|
17
|
+
<span class="elc-badge" id="elc-counter">0 {t.registered}</span>
|
|
18
|
+
</div>
|
|
19
|
+
<textarea
|
|
20
|
+
class="elc-textarea"
|
|
21
|
+
id="elc-input"
|
|
22
|
+
placeholder={t.inputPlaceholder}
|
|
23
|
+
spellcheck="false"
|
|
24
|
+
></textarea>
|
|
25
|
+
<div class="elc-actions">
|
|
26
|
+
<button class="elc-btn elc-btn-primary" id="elc-clean" type="button">
|
|
27
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 11V7a5 5 0 0 1 10 0v4"></path><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M12 15v3"></path></svg>
|
|
28
|
+
{t.cleanBtn}
|
|
29
|
+
</button>
|
|
30
|
+
<button class="elc-btn elc-btn-secondary" id="elc-clear" type="button">
|
|
31
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>
|
|
32
|
+
{t.clearBtn}
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="elc-results" id="elc-results" style="display: none">
|
|
38
|
+
<div class="elc-stats">
|
|
39
|
+
<div class="elc-stat">
|
|
40
|
+
<span class="elc-stat-value" id="elc-stat-original">0</span>
|
|
41
|
+
<span class="elc-stat-label">{t.statsOriginal}</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="elc-stat">
|
|
44
|
+
<span class="elc-stat-value elc-stat-removed" id="elc-stat-removed">0</span>
|
|
45
|
+
<span class="elc-stat-label">{t.statsRemoved}</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="elc-stat">
|
|
48
|
+
<span class="elc-stat-value elc-stat-clean" id="elc-stat-final">0</span>
|
|
49
|
+
<span class="elc-stat-label">{t.statsFinal}</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="elc-field">
|
|
54
|
+
<div class="elc-label-row">
|
|
55
|
+
<label class="elc-label" for="elc-output">{t.resultLabel}</label>
|
|
56
|
+
<span class="elc-badge" id="elc-result-counter">0</span>
|
|
57
|
+
</div>
|
|
58
|
+
<textarea
|
|
59
|
+
class="elc-textarea elc-textarea-result"
|
|
60
|
+
id="elc-output"
|
|
61
|
+
readonly
|
|
62
|
+
spellcheck="false"
|
|
63
|
+
></textarea>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="elc-actions">
|
|
67
|
+
<button class="elc-btn elc-btn-primary" id="elc-copy" type="button">
|
|
68
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
69
|
+
<span id="elc-copy-text">{t.copyBtn}</span>
|
|
70
|
+
</button>
|
|
71
|
+
<button class="elc-btn elc-btn-secondary" id="elc-download" type="button">
|
|
72
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
|
73
|
+
{t.downloadBtn}
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<style>
|
|
81
|
+
.elc-root {
|
|
82
|
+
--elc-indigo: #6366f1;
|
|
83
|
+
--elc-indigo-dark: #4f46e5;
|
|
84
|
+
--elc-purple: #a855f7;
|
|
85
|
+
--elc-green: #10b981;
|
|
86
|
+
--elc-rose: #f43f5e;
|
|
87
|
+
--elc-card-bg: rgba(255, 255, 255, 0.7);
|
|
88
|
+
--elc-card-border: rgba(255, 255, 255, 0.4);
|
|
89
|
+
--elc-field-bg: #fff;
|
|
90
|
+
--elc-field-border: #e2e8f0;
|
|
91
|
+
--elc-text-main: #1e293b;
|
|
92
|
+
--elc-text-label: #64748b;
|
|
93
|
+
--elc-result-bg: rgba(99, 102, 241, 0.05);
|
|
94
|
+
--elc-divider: #e2e8f0;
|
|
95
|
+
--elc-stat-bg: rgba(255, 255, 255, 0.5);
|
|
96
|
+
--elc-stat-border: rgba(255, 255, 255, 0.4);
|
|
97
|
+
|
|
98
|
+
width: 100%;
|
|
99
|
+
max-width: 860px;
|
|
100
|
+
margin: 0 auto;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
:global(.theme-dark) .elc-root {
|
|
104
|
+
--elc-card-bg: rgba(15, 23, 42, 0.6);
|
|
105
|
+
--elc-card-border: rgba(255, 255, 255, 0.1);
|
|
106
|
+
--elc-field-bg: #1e293b;
|
|
107
|
+
--elc-field-border: #334155;
|
|
108
|
+
--elc-text-main: #f1f5f9;
|
|
109
|
+
--elc-text-label: #94a3b8;
|
|
110
|
+
--elc-result-bg: rgba(99, 102, 241, 0.08);
|
|
111
|
+
--elc-divider: #334155;
|
|
112
|
+
--elc-stat-bg: rgba(30, 41, 59, 0.4);
|
|
113
|
+
--elc-stat-border: rgba(255, 255, 255, 0.1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.elc-card {
|
|
117
|
+
background: var(--elc-card-bg);
|
|
118
|
+
backdrop-filter: blur(12px);
|
|
119
|
+
border: 1px solid var(--elc-card-border);
|
|
120
|
+
border-radius: 1.5rem;
|
|
121
|
+
padding: 2.5rem;
|
|
122
|
+
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
gap: 2rem;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.elc-field {
|
|
129
|
+
display: flex;
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
gap: 0.75rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.elc-label-row {
|
|
135
|
+
display: flex;
|
|
136
|
+
justify-content: space-between;
|
|
137
|
+
align-items: center;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.elc-label {
|
|
141
|
+
font-size: 0.8125rem;
|
|
142
|
+
font-weight: 700;
|
|
143
|
+
color: var(--elc-text-label);
|
|
144
|
+
text-transform: uppercase;
|
|
145
|
+
letter-spacing: 0.05em;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.elc-badge {
|
|
149
|
+
font-size: 0.75rem;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
background: rgba(99, 102, 241, 0.1);
|
|
152
|
+
color: var(--elc-indigo);
|
|
153
|
+
padding: 0.2rem 0.65rem;
|
|
154
|
+
border-radius: 99px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.elc-textarea {
|
|
158
|
+
width: 100%;
|
|
159
|
+
min-height: 220px;
|
|
160
|
+
padding: 1rem 1.25rem;
|
|
161
|
+
border-radius: 1rem;
|
|
162
|
+
border: 2px solid var(--elc-field-border);
|
|
163
|
+
background: var(--elc-field-bg);
|
|
164
|
+
color: var(--elc-text-main);
|
|
165
|
+
font-size: 0.9375rem;
|
|
166
|
+
resize: vertical;
|
|
167
|
+
outline: none;
|
|
168
|
+
transition: border-color 0.2s;
|
|
169
|
+
box-sizing: border-box;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.elc-textarea:focus {
|
|
173
|
+
border-color: var(--elc-indigo);
|
|
174
|
+
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.elc-textarea-result {
|
|
178
|
+
min-height: 180px;
|
|
179
|
+
background: var(--elc-result-bg);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.elc-actions {
|
|
183
|
+
display: flex;
|
|
184
|
+
gap: 0.75rem;
|
|
185
|
+
flex-wrap: wrap;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.elc-btn {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: 0.5rem;
|
|
192
|
+
padding: 0.75rem 1.375rem;
|
|
193
|
+
border-radius: 0.75rem;
|
|
194
|
+
font-size: 0.9375rem;
|
|
195
|
+
font-weight: 700;
|
|
196
|
+
border: none;
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
transition: all 0.2s;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.elc-btn-primary {
|
|
202
|
+
background: linear-gradient(135deg, var(--elc-indigo), var(--elc-purple));
|
|
203
|
+
color: #fff;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.elc-btn-primary:hover {
|
|
207
|
+
transform: translateY(-2px);
|
|
208
|
+
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.4);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.elc-btn-secondary {
|
|
212
|
+
background: rgba(99, 102, 241, 0.1);
|
|
213
|
+
color: var(--elc-indigo);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.elc-btn-secondary:hover {
|
|
217
|
+
background: rgba(99, 102, 241, 0.2);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.elc-results {
|
|
221
|
+
flex-direction: column;
|
|
222
|
+
gap: 1.5rem;
|
|
223
|
+
border-top: 2px dashed var(--elc-divider);
|
|
224
|
+
padding-top: 2rem;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.elc-stats {
|
|
228
|
+
display: grid;
|
|
229
|
+
grid-template-columns: repeat(3, 1fr);
|
|
230
|
+
gap: 1rem;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.elc-stat {
|
|
234
|
+
background: var(--elc-stat-bg);
|
|
235
|
+
border: 1px solid var(--elc-stat-border);
|
|
236
|
+
border-radius: 1rem;
|
|
237
|
+
padding: 1.25rem;
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-direction: column;
|
|
240
|
+
align-items: center;
|
|
241
|
+
gap: 0.25rem;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.elc-stat-value {
|
|
245
|
+
font-size: 1.75rem;
|
|
246
|
+
font-weight: 800;
|
|
247
|
+
color: var(--elc-indigo);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.elc-stat-removed {
|
|
251
|
+
color: var(--elc-rose);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.elc-stat-clean {
|
|
255
|
+
color: var(--elc-green);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.elc-stat-label {
|
|
259
|
+
font-size: 0.75rem;
|
|
260
|
+
font-weight: 700;
|
|
261
|
+
color: var(--elc-text-label);
|
|
262
|
+
text-transform: uppercase;
|
|
263
|
+
letter-spacing: 0.04em;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@media (max-width: 640px) {
|
|
267
|
+
.elc-card {
|
|
268
|
+
padding: 1.5rem;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.elc-stats {
|
|
272
|
+
grid-template-columns: 1fr;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.elc-actions {
|
|
276
|
+
flex-direction: column;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.elc-btn {
|
|
280
|
+
justify-content: center;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
</style>
|
|
284
|
+
|
|
285
|
+
<script>
|
|
286
|
+
import type { EmailListCleanerUI } from './ui';
|
|
287
|
+
|
|
288
|
+
const root = document.querySelector('.elc-root') as HTMLElement;
|
|
289
|
+
const t = JSON.parse(root?.dataset.ui ?? '{}') as EmailListCleanerUI;
|
|
290
|
+
|
|
291
|
+
const input = document.getElementById('elc-input') as HTMLTextAreaElement;
|
|
292
|
+
const output = document.getElementById('elc-output') as HTMLTextAreaElement;
|
|
293
|
+
const results = document.getElementById('elc-results') as HTMLDivElement;
|
|
294
|
+
const counter = document.getElementById('elc-counter') as HTMLSpanElement;
|
|
295
|
+
const resultCounter = document.getElementById('elc-result-counter') as HTMLSpanElement;
|
|
296
|
+
const statOriginal = document.getElementById('elc-stat-original') as HTMLSpanElement;
|
|
297
|
+
const statRemoved = document.getElementById('elc-stat-removed') as HTMLSpanElement;
|
|
298
|
+
const statFinal = document.getElementById('elc-stat-final') as HTMLSpanElement;
|
|
299
|
+
const cleanBtn = document.getElementById('elc-clean') as HTMLButtonElement;
|
|
300
|
+
const clearBtn = document.getElementById('elc-clear') as HTMLButtonElement;
|
|
301
|
+
const copyBtn = document.getElementById('elc-copy') as HTMLButtonElement;
|
|
302
|
+
const copyText = document.getElementById('elc-copy-text') as HTMLSpanElement;
|
|
303
|
+
const downloadBtn = document.getElementById('elc-download') as HTMLButtonElement;
|
|
304
|
+
|
|
305
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
306
|
+
|
|
307
|
+
function updateCounter(): void {
|
|
308
|
+
const text = input.value.trim();
|
|
309
|
+
if (!text) {
|
|
310
|
+
counter.textContent = `0 ${t.registered}`;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const raw = text.split(/[\s,]+/).filter((e) => e !== '');
|
|
314
|
+
counter.textContent = `${raw.length.toLocaleString()} ${t.registered}`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function clean(): void {
|
|
318
|
+
const rawText = input.value.trim();
|
|
319
|
+
if (!rawText) return;
|
|
320
|
+
|
|
321
|
+
const raw = rawText.split(/[\s,]+/).filter((e) => e !== '');
|
|
322
|
+
const originalCount = raw.length;
|
|
323
|
+
const valid = raw.filter((email) => EMAIL_REGEX.test(email));
|
|
324
|
+
const unique = Array.from(new Set(valid.map((email) => email.toLowerCase())));
|
|
325
|
+
const finalCount = unique.length;
|
|
326
|
+
const removedCount = originalCount - finalCount;
|
|
327
|
+
|
|
328
|
+
output.value = unique.join('\n');
|
|
329
|
+
statOriginal.textContent = originalCount.toLocaleString();
|
|
330
|
+
statRemoved.textContent = removedCount.toLocaleString();
|
|
331
|
+
statFinal.textContent = finalCount.toLocaleString();
|
|
332
|
+
resultCounter.textContent = finalCount.toLocaleString();
|
|
333
|
+
|
|
334
|
+
results.style.display = 'flex';
|
|
335
|
+
results.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function clearAll(): void {
|
|
339
|
+
input.value = '';
|
|
340
|
+
output.value = '';
|
|
341
|
+
results.style.display = 'none';
|
|
342
|
+
updateCounter();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function copy(): Promise<void> {
|
|
346
|
+
try {
|
|
347
|
+
await navigator.clipboard.writeText(output.value);
|
|
348
|
+
copyText.textContent = t.copyDone;
|
|
349
|
+
copyBtn.style.background = '#10b981';
|
|
350
|
+
setTimeout(() => {
|
|
351
|
+
copyText.textContent = t.copyBtn;
|
|
352
|
+
copyBtn.style.background = '';
|
|
353
|
+
}, 2000);
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function download(): void {
|
|
359
|
+
const blob = new Blob([output.value], { type: 'text/plain' });
|
|
360
|
+
const url = URL.createObjectURL(blob);
|
|
361
|
+
const a = document.createElement('a');
|
|
362
|
+
a.href = url;
|
|
363
|
+
a.download = `${t.downloadFilename}-${new Date().toISOString().split('T')[0]}.txt`;
|
|
364
|
+
document.body.appendChild(a);
|
|
365
|
+
a.click();
|
|
366
|
+
document.body.removeChild(a);
|
|
367
|
+
URL.revokeObjectURL(url);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
input.addEventListener('input', updateCounter);
|
|
371
|
+
cleanBtn.addEventListener('click', clean);
|
|
372
|
+
clearBtn.addEventListener('click', clearAll);
|
|
373
|
+
copyBtn.addEventListener('click', copy);
|
|
374
|
+
downloadBtn.addEventListener('click', download);
|
|
375
|
+
</script>
|