@jjlmoya/utils-social 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +23 -0
  3. package/src/category/i18n/es.ts +23 -0
  4. package/src/category/i18n/fr.ts +23 -0
  5. package/src/category/index.ts +13 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +27 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/redditFormatter/bibliography.astro +14 -0
  23. package/src/tool/redditFormatter/component.astro +246 -0
  24. package/src/tool/redditFormatter/i18n/en.ts +193 -0
  25. package/src/tool/redditFormatter/i18n/es.ts +193 -0
  26. package/src/tool/redditFormatter/i18n/fr.ts +193 -0
  27. package/src/tool/redditFormatter/index.ts +34 -0
  28. package/src/tool/redditFormatter/logic.ts +24 -0
  29. package/src/tool/redditFormatter/seo.astro +14 -0
  30. package/src/tool/redditFormatter/ui.ts +13 -0
  31. package/src/tool/safeZones/bibliography.astro +14 -0
  32. package/src/tool/safeZones/component.astro +857 -0
  33. package/src/tool/safeZones/i18n/en.ts +244 -0
  34. package/src/tool/safeZones/i18n/es.ts +244 -0
  35. package/src/tool/safeZones/i18n/fr.ts +244 -0
  36. package/src/tool/safeZones/index.ts +34 -0
  37. package/src/tool/safeZones/logic.ts +137 -0
  38. package/src/tool/safeZones/seo.astro +14 -0
  39. package/src/tool/safeZones/ui.ts +34 -0
  40. package/src/tool/socialImageResizer/bibliography.astro +14 -0
  41. package/src/tool/socialImageResizer/component.astro +498 -0
  42. package/src/tool/socialImageResizer/i18n/en.ts +211 -0
  43. package/src/tool/socialImageResizer/i18n/es.ts +211 -0
  44. package/src/tool/socialImageResizer/i18n/fr.ts +211 -0
  45. package/src/tool/socialImageResizer/index.ts +34 -0
  46. package/src/tool/socialImageResizer/logic.ts +133 -0
  47. package/src/tool/socialImageResizer/seo.astro +14 -0
  48. package/src/tool/socialImageResizer/ui.ts +7 -0
  49. package/src/tool/socialValueCalculator/bibliography.astro +14 -0
  50. package/src/tool/socialValueCalculator/component.astro +829 -0
  51. package/src/tool/socialValueCalculator/i18n/en.ts +310 -0
  52. package/src/tool/socialValueCalculator/i18n/es.ts +310 -0
  53. package/src/tool/socialValueCalculator/i18n/fr.ts +310 -0
  54. package/src/tool/socialValueCalculator/index.ts +34 -0
  55. package/src/tool/socialValueCalculator/logic.ts +60 -0
  56. package/src/tool/socialValueCalculator/seo.astro +14 -0
  57. package/src/tool/socialValueCalculator/ui.ts +31 -0
  58. package/src/tool/tinderPhotoOptimizer/bibliography.astro +14 -0
  59. package/src/tool/tinderPhotoOptimizer/component.astro +1514 -0
  60. package/src/tool/tinderPhotoOptimizer/i18n/en.ts +248 -0
  61. package/src/tool/tinderPhotoOptimizer/i18n/es.ts +248 -0
  62. package/src/tool/tinderPhotoOptimizer/i18n/fr.ts +248 -0
  63. package/src/tool/tinderPhotoOptimizer/index.ts +34 -0
  64. package/src/tool/tinderPhotoOptimizer/logic.ts +199 -0
  65. package/src/tool/tinderPhotoOptimizer/seo.astro +14 -0
  66. package/src/tool/tinderPhotoOptimizer/ui.ts +37 -0
  67. package/src/tool/youtubeThumbnail/bibliography.astro +14 -0
  68. package/src/tool/youtubeThumbnail/component.astro +419 -0
  69. package/src/tool/youtubeThumbnail/i18n/en.ts +207 -0
  70. package/src/tool/youtubeThumbnail/i18n/es.ts +207 -0
  71. package/src/tool/youtubeThumbnail/i18n/fr.ts +207 -0
  72. package/src/tool/youtubeThumbnail/index.ts +34 -0
  73. package/src/tool/youtubeThumbnail/logic.ts +7 -0
  74. package/src/tool/youtubeThumbnail/seo.astro +14 -0
  75. package/src/tool/youtubeThumbnail/ui.ts +13 -0
  76. package/src/tool/youtubeThumbnailPreviewer/bibliography.astro +14 -0
  77. package/src/tool/youtubeThumbnailPreviewer/component.astro +791 -0
  78. package/src/tool/youtubeThumbnailPreviewer/i18n/en.ts +321 -0
  79. package/src/tool/youtubeThumbnailPreviewer/i18n/es.ts +321 -0
  80. package/src/tool/youtubeThumbnailPreviewer/i18n/fr.ts +321 -0
  81. package/src/tool/youtubeThumbnailPreviewer/index.ts +34 -0
  82. package/src/tool/youtubeThumbnailPreviewer/seo.astro +14 -0
  83. package/src/tool/youtubeThumbnailPreviewer/ui.ts +19 -0
  84. package/src/tools.ts +10 -0
  85. package/src/types.ts +72 -0
@@ -0,0 +1,244 @@
1
+ import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { SafeZonesUI } from '../ui';
4
+
5
+ const slug = 'simulateur-reseaux';
6
+ const title = 'Safe Zones Réseaux Sociaux : Simulateur Vidéo TikTok, Reels et Shorts';
7
+ const description =
8
+ 'Prévisualisez vos images et vidéos avec la vraie interface des réseaux sociaux. Assurez-vous que votre contenu est toujours visible et évitez les recadrages accidentels.';
9
+
10
+ const faqData = [
11
+ {
12
+ question: "Que sont les 'Safe Zones' sur les réseaux sociaux ?",
13
+ answer:
14
+ "Ce sont les zones de l'écran où le contenu est visible sans risque d'être recouvert par des éléments d'interface, tels que le nom d'utilisateur, la description, les boutons d'interaction ou le recadrage automatique.",
15
+ },
16
+ {
17
+ question: 'Pourquoi ma vidéo TikTok est-elle coupée en haut et en bas ?',
18
+ answer:
19
+ "TikTok utilise un format 9:16. Si votre vidéo est plus large, l'application fera un zoom pour remplir l'écran, provoquant des recadrages. De plus, l'interface inférieure couvre une partie de la vidéo, les éléments importants doivent donc être centrés.",
20
+ },
21
+ {
22
+ question: "La zone sécurisée est-elle la même pour Instagram Reels et YouTube Shorts ?",
23
+ answer:
24
+ "Elles sont très similaires mais pas identiques. Les boutons de YouTube Shorts sont positionnés différemment de ceux de Reels. Notre outil permet de basculer entre les plateformes pour vérifier votre design sur chacune d'elles.",
25
+ },
26
+ {
27
+ question: "Que se passe-t-il si je place du texte dans le coin inférieur droit ?",
28
+ answer:
29
+ "Il sera probablement invisible ou très difficile à lire, car c'est là que se trouvent les icônes de profil, cœur et commentaires. Il est idéal de garder les textes et visages dans la zone centrale-supérieure (Safe Zone).",
30
+ },
31
+ ];
32
+
33
+ const howToData = [
34
+ {
35
+ name: 'Charger votre image ou vidéo',
36
+ text: "Téléchargez le fichier que vous souhaitez prévisualiser. Le traitement est local et privé — rien n'est envoyé sur internet.",
37
+ },
38
+ {
39
+ name: 'Sélectionner la plateforme',
40
+ text: "Choisissez entre TikTok, Instagram Reels ou YouTube Shorts pour activer l'overlay d'interface correspondant.",
41
+ },
42
+ {
43
+ name: 'Vérifier la lisibilité',
44
+ text: "Vérifiez si les textes, sous-titres ou éléments clés de votre marque sont couverts par les icônes du réseau social.",
45
+ },
46
+ {
47
+ name: 'Ajuster et exporter',
48
+ text: "Si quelque chose est caché, déplacez l'élément dans votre éditeur vidéo original et retestez jusqu'à ce que ce soit parfait.",
49
+ },
50
+ ];
51
+
52
+ const faqSchema: WithContext<FAQPage> = {
53
+ '@context': 'https://schema.org',
54
+ '@type': 'FAQPage',
55
+ mainEntity: faqData.map((item) => ({
56
+ '@type': 'Question',
57
+ name: item.question,
58
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
59
+ })),
60
+ };
61
+
62
+ const howToSchema: WithContext<HowToThing> = {
63
+ '@context': 'https://schema.org',
64
+ '@type': 'HowTo',
65
+ name: title,
66
+ description,
67
+ step: howToData.map((step, i) => ({
68
+ '@type': 'HowToStep',
69
+ position: i + 1,
70
+ name: step.name,
71
+ text: step.text,
72
+ })),
73
+ };
74
+
75
+ const appSchema: WithContext<SoftwareApplication> = {
76
+ '@context': 'https://schema.org',
77
+ '@type': 'SoftwareApplication',
78
+ name: title,
79
+ description,
80
+ applicationCategory: 'UtilityApplication',
81
+ operatingSystem: 'All',
82
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
83
+ inLanguage: 'fr',
84
+ };
85
+
86
+ export const content: ToolLocaleContent<SafeZonesUI> = {
87
+ slug,
88
+ title,
89
+ description,
90
+ faqTitle: 'Questions Fréquentes',
91
+ faq: faqData,
92
+ bibliographyTitle: 'Documentation Officielle des Plateformes',
93
+ bibliography: [
94
+ {
95
+ name: 'TikTok Creator Portal: Video optimization and safe zones',
96
+ url: 'https://www.tiktok.com/creators/creator-portal/en-us/foundation/video-optimization/',
97
+ },
98
+ {
99
+ name: 'Instagram Business: Best practices for Reels',
100
+ url: 'https://business.instagram.com/creators/reels-best-practices',
101
+ },
102
+ {
103
+ name: 'YouTube Help: Create YouTube Shorts',
104
+ url: 'https://support.google.com/youtube/answer/10343433',
105
+ },
106
+ {
107
+ name: 'Meta for Creators: Safe zones and aspect ratios for video',
108
+ url: 'https://www.facebook.com/business/help/103816146375741',
109
+ },
110
+ ],
111
+ howTo: howToData,
112
+ schemas: [faqSchema, howToSchema, appSchema],
113
+ seo: [
114
+ {
115
+ type: 'title',
116
+ text: 'Guide Maître des Zones Sécurisées : Optimisez vos Reels, TikToks et Shorts',
117
+ level: 2,
118
+ },
119
+ {
120
+ type: 'paragraph',
121
+ html: "Dans le monde saturé du contenu vertical, la première impression est technique. La plus grande erreur des créateurs est d'ignorer comment leur <strong>vidéo ou image</strong> interagit avec l'interface native de la plateforme. Chaque réseau social (TikTok, Instagram Reels et YouTube Shorts) superpose des couches de boutons, de textes et de profils qui peuvent annuler l'impact visuel de votre composition.",
122
+ },
123
+ {
124
+ type: 'paragraph',
125
+ html: "Cet outil vous permet de charger vos fichiers multimédias et de prévisualiser exactement quelles parties de votre cadrage seront absorbées par l'interface. Il ne s'agit pas seulement des <strong>sous-titres</strong> ; il s'agit de s'assurer que les visages, les produits ou les actions principales de votre vidéo ne se retrouvent pas enfouis sous un bouton \"J'aime\" ou une description interminable.",
126
+ },
127
+ {
128
+ type: 'grid',
129
+ columns: [
130
+ {
131
+ type: 'card',
132
+ title: "TikTok : L'interface la plus invasive",
133
+ html: "<p>TikTok possède l'interface la plus dense. Sur la droite, une colonne d'icônes occupe environ 15% de la largeur. En bas, la description peut s'étendre sur 4 ou 5 lignes si elle est longue, couvrant presque un tiers de l'écran depuis le bas.</p><ul><li><strong>Marge inférieure :</strong> Laissez libres les 250 derniers pixels pour éviter le texte de la description et le nom d'utilisateur.</li><li><strong>Marge droite :</strong> Évitez de placer du texte à moins de 100px du bord droit.</li><li><strong>Haut :</strong> Les boutons « Abonnements » et « Pour toi » occupent la bande supérieure centrale.</li></ul>",
134
+ },
135
+ {
136
+ type: 'card',
137
+ title: 'IG Reels : Le facteur du fil',
138
+ html: "<p>Contrairement à TikTok, Instagram Reels recadre la vidéo au format 4:5 lorsqu'elle est visualisée depuis le fil principal (pas depuis l'onglet Reels). Cela signifie que si vous placez des informations vitales dans les 15% supérieurs ou inférieurs de votre vidéo 9:16, elles disparaîtront dans le fil.</p><ul><li><strong>La règle du 4:5 :</strong> Gardez les titres et éléments clés dans la zone centrale pour assurer la visibilité dans le fil.</li><li><strong>Interface :</strong> Les icônes de Reels sont plus subtiles que celles de TikTok, permettant un peu plus de marge latérale.</li><li><strong>Bouton audio :</strong> Situé en bas à droite, il couvre généralement bien moins que le disque tournant de TikTok.</li></ul>",
139
+ },
140
+ ],
141
+ },
142
+ {
143
+ type: 'title',
144
+ text: 'YouTube Shorts : Attention à la droite',
145
+ level: 3,
146
+ },
147
+ {
148
+ type: 'paragraph',
149
+ html: "YouTube Shorts a une interface asymétrique. Le bouton d'abonnement est très proéminent et de couleur unie (généralement noir ou blanc), ce qui crée un point aveugle visuel important en bas à droite.",
150
+ },
151
+ {
152
+ type: 'list',
153
+ items: [
154
+ '<strong>Marge droite :</strong> Évitez de placer du texte ou des éléments importants dans les 20% inférieurs droits.',
155
+ "<strong>Marge inférieure :</strong> Laissez libres les 150 derniers pixels pour éviter le texte de la description et le nom d'utilisateur.",
156
+ "<strong>Haut :</strong> Les boutons « S'abonner » et « Plus » occupent la bande supérieure centrale.",
157
+ ],
158
+ },
159
+ {
160
+ type: 'title',
161
+ text: 'Questions Critiques sur le Design Vertical',
162
+ level: 2,
163
+ },
164
+ {
165
+ type: 'title',
166
+ text: 'Où placer les éléments clés (visages, texte, produit) ?',
167
+ level: 3,
168
+ },
169
+ {
170
+ type: 'paragraph',
171
+ html: 'La « Zone d\'Or » pour l\'action principale est le tiers central de la vidéo. Si vous filmez une personne, assurez-vous que ses yeux et sa bouche se trouvent dans cette zone. Les éléments graphiques importants doivent être légèrement décalés vers le haut par rapport au centre géométrique pour éviter la description inférieure.',
172
+ },
173
+ {
174
+ type: 'title',
175
+ text: 'Pourquoi ma vidéo est-elle floue une fois uploadée ?',
176
+ level: 3,
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: "Ce n'est souvent pas la qualité, mais le redimensionnement. Filmez toujours en 1080x1920 à 30 ou 60 fps. Évitez le 4K si vous appliquez de nombreux effets lourds, car les plateformes compressent agressivement les fichiers volumineux.",
181
+ },
182
+ {
183
+ type: 'title',
184
+ text: 'Que se passe-t-il avec les bords des téléphones ?',
185
+ level: 3,
186
+ },
187
+ {
188
+ type: 'paragraph',
189
+ html: 'Rappelez-vous que les téléphones modernes ont des « encoches » ou des trous de caméra à l\'écran. Cet outil inclut une marge de sécurité supérieure pour que votre contenu n\'entre pas en conflit avec l\'horloge ou les icônes système (batterie, signal).',
190
+ },
191
+ {
192
+ type: 'title',
193
+ text: "Anatomie d'une Vidéo Virale",
194
+ level: 2,
195
+ },
196
+ {
197
+ type: 'paragraph',
198
+ html: "Concevoir pour les réseaux sociaux n'est pas seulement esthétique — c'est de l'<strong>utilisabilité</strong>. Si un utilisateur doit faire un effort pour lire un texte parce que les icônes de la plateforme le couvrent, il passera simplement à la vidéo suivante. La rétention (Watch Time) chute brutalement en cas de friction visuelle.",
199
+ },
200
+ {
201
+ type: 'paragraph',
202
+ html: "Sur YouTube Shorts, par exemple, l'interface est asymétrique. Le bouton d'abonnement est très proéminent et de couleur unie (généralement noir ou blanc), ce qui crée un point aveugle visuel important. En utilisant ce simulateur, vous pouvez ajuster l'opacité pour comprendre comment votre palette de couleurs interagit avec les couleurs de l'UI native.",
203
+ },
204
+ {
205
+ type: 'tip',
206
+ title: 'Outil de Contrôle Qualité Créatif',
207
+ html: '<p>Conçu pour les créateurs qui ne laissent rien au hasard.</p>',
208
+ },
209
+ ],
210
+ ui: {
211
+ labelConfig: 'Configuration',
212
+ labelPlatform: 'Plateforme cible',
213
+ labelUpload: 'Charger le contenu',
214
+ uploadPrompt: 'Sélectionner une image ou vidéo',
215
+ uploadLink: 'Ou utiliser un exemple',
216
+ loadContent: 'Chargez votre contenu',
217
+ labelOpacity: "Relief d'interface",
218
+ labelMask: 'Griser les zones critiques',
219
+ labelGrid: 'Règle des tiers',
220
+ btnReset: 'Réinitialiser le canvas',
221
+ bannerTiktok: 'Simulation TikTok 9:16',
222
+ bannerReels: 'Simulation Instagram Reels 9:16',
223
+ bannerShorts: 'Simulation YouTube Shorts 9:16',
224
+ nameTiktok: 'TikTok',
225
+ nameReels: 'IG Reels',
226
+ nameShorts: 'YT Shorts',
227
+ ttFollowing: 'Abonnements',
228
+ ttForYou: 'Pour toi',
229
+ ttDescription: 'Design d\'interfaces réelles pour créateurs de contenu. #uxdesign #socialmedia',
230
+ ttAudio: 'jjlmoya - Son Original',
231
+ ttNavHome: 'Accueil',
232
+ ttNavFriends: 'Amis',
233
+ ttNavInbox: 'Boîte',
234
+ ttNavProfile: 'Profil',
235
+ reelsFollow: 'Suivre',
236
+ reelsDescription: 'Cette interface est identique à la vraie. #reels #ux #simulateur',
237
+ reelsAudio: 'Musique Tendance - Audio Original',
238
+ shortsSubscribe: "S'ABONNER",
239
+ shortsDislike: 'Je n\'aime pas',
240
+ shortsShare: 'Partager',
241
+ shortsRemix: 'Remix',
242
+ shortsDescription: 'Simulation d\'interfaces réelles pour Shorts ! #youtube #shorts',
243
+ },
244
+ };
@@ -0,0 +1,34 @@
1
+ import type { SocialToolEntry, ToolLocaleContent, ToolDefinition } from '../../types';
2
+ import SafeZonesComponent from './component.astro';
3
+ import SafeZonesSEO from './seo.astro';
4
+ import SafeZonesBibliography from './bibliography.astro';
5
+
6
+ import type { SafeZonesUI } from './ui';
7
+
8
+ export type SafeZonesLocaleContent = ToolLocaleContent<SafeZonesUI>;
9
+
10
+ import { content as es } from './i18n/es';
11
+ import { content as en } from './i18n/en';
12
+ import { content as fr } from './i18n/fr';
13
+
14
+ export const safeZones: SocialToolEntry<SafeZonesUI> = {
15
+ id: 'safe-zones',
16
+ icons: {
17
+ bg: 'mdi:cellphone',
18
+ fg: 'mdi:crop-free',
19
+ },
20
+ i18n: {
21
+ es: async () => es,
22
+ en: async () => en,
23
+ fr: async () => fr,
24
+ },
25
+ };
26
+
27
+ export { SafeZonesComponent, SafeZonesSEO, SafeZonesBibliography };
28
+
29
+ export const SAFE_ZONES_TOOL: ToolDefinition = {
30
+ entry: safeZones,
31
+ Component: SafeZonesComponent,
32
+ SEOComponent: SafeZonesSEO,
33
+ BibliographyComponent: SafeZonesBibliography,
34
+ };
@@ -0,0 +1,137 @@
1
+ export interface PlatformStrings {
2
+ ttFollowing: string;
3
+ ttForYou: string;
4
+ ttDescription: string;
5
+ ttAudio: string;
6
+ ttNavHome: string;
7
+ ttNavFriends: string;
8
+ ttNavInbox: string;
9
+ ttNavProfile: string;
10
+ reelsFollow: string;
11
+ reelsDescription: string;
12
+ reelsAudio: string;
13
+ shortsSubscribe: string;
14
+ shortsDislike: string;
15
+ shortsShare: string;
16
+ shortsRemix: string;
17
+ shortsDescription: string;
18
+ }
19
+
20
+ const COL = 'display:flex;flex-direction:column;align-items:center;gap:2px';
21
+ const STAT = (icon: string, val: string) =>
22
+ `<div style="${COL}"><iconify-icon icon="${icon}" style="font-size:36px;color:white"></iconify-icon><span style="font-size:10px;color:white;font-weight:bold;text-shadow:0 1px 3px rgba(0,0,0,.8)">${val}</span></div>`;
23
+
24
+ function buildTikTokNav(s: PlatformStrings): string {
25
+ return `<div style="position:absolute;top:40px;left:0;width:100%;display:flex;justify-content:space-between;padding:0 24px;box-sizing:border-box;align-items:center">
26
+ <iconify-icon icon="mdi:live-tv" style="font-size:24px;color:white"></iconify-icon>
27
+ <div style="display:flex;gap:16px;font-size:10px;font-weight:bold;text-transform:uppercase;letter-spacing:.1em">
28
+ <span style="color:rgba(255,255,255,.5)">${s.ttFollowing}</span>
29
+ <span style="color:white;border-bottom:2px solid white;padding-bottom:4px">${s.ttForYou}</span>
30
+ </div>
31
+ <iconify-icon icon="mdi:magnify" style="font-size:24px;color:white"></iconify-icon>
32
+ </div>`;
33
+ }
34
+
35
+ function buildTikTokActions(): string {
36
+ return `<div style="position:absolute;right:12px;bottom:96px;display:flex;flex-direction:column;gap:20px;align-items:center">
37
+ <div style="width:44px;height:44px;border-radius:50%;border:2px solid white;background:#27272a;position:relative">
38
+ <div style="position:absolute;bottom:-6px;left:50%;transform:translateX(-50%);background:#e11d48;border-radius:50%;width:16px;height:16px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:bold;color:white">+</div>
39
+ </div>
40
+ ${STAT('ph:heart-fill', '1.2M')}${STAT('ph:chat-circle-dots-fill', '4,890')}${STAT('ph:bookmark-simple-fill', '250K')}${STAT('ph:share-fat-fill', '45.2K')}
41
+ <div style="width:40px;height:40px;border-radius:50%;background:#18181b;border:8px solid #3f3f46;margin-top:8px;animation:szl-spin 3s linear infinite"></div>
42
+ </div>`;
43
+ }
44
+
45
+ function buildTikTokInfo(s: PlatformStrings): string {
46
+ return `<div style="position:absolute;left:16px;bottom:60px;right:64px">
47
+ <div style="font-weight:bold;color:white;font-size:15px;display:flex;align-items:center;gap:6px;margin-bottom:6px">
48
+ jjlmoya <iconify-icon icon="mdi:check-decagram" style="color:#60a5fa;font-size:12px"></iconify-icon>
49
+ </div>
50
+ <div style="color:rgba(255,255,255,.95);font-size:13px;line-height:1.4;margin-bottom:8px;padding-right:16px">${s.ttDescription}</div>
51
+ <div style="display:flex;align-items:center;gap:8px;color:rgba(255,255,255,.9);font-size:11px;padding:4px 12px;background:rgba(0,0,0,.3);border-radius:999px;width:fit-content;border:1px solid rgba(255,255,255,.1)">
52
+ <iconify-icon icon="mdi:music"></iconify-icon><span>${s.ttAudio}</span>
53
+ </div>
54
+ </div>`;
55
+ }
56
+
57
+ function buildTikTokUI(s: PlatformStrings): string {
58
+ return buildTikTokNav(s) + buildTikTokActions() + buildTikTokInfo(s);
59
+ }
60
+
61
+ function buildReelsActions(): string {
62
+ return `<div style="position:absolute;top:44px;left:24px;color:white;font-size:24px;font-weight:bold;display:flex;align-items:center;gap:4px">Reels <iconify-icon icon="mdi:chevron-down"></iconify-icon></div>
63
+ <div style="position:absolute;top:44px;right:24px;color:white;font-size:24px"><iconify-icon icon="mdi:camera-outline"></iconify-icon></div>
64
+ <div style="position:absolute;right:16px;bottom:56px;display:flex;flex-direction:column;gap:24px;align-items:center">
65
+ <div style="${COL}"><iconify-icon icon="ph:heart" style="font-size:30px;color:white"></iconify-icon><span style="font-size:10px;color:white;font-weight:500">102K</span></div>
66
+ <div style="${COL}"><iconify-icon icon="ph:chat-circle" style="font-size:30px;color:white"></iconify-icon><span style="font-size:10px;color:white;font-weight:500">1,245</span></div>
67
+ <div style="${COL}"><iconify-icon icon="ph:paper-plane-tilt" style="font-size:30px;color:white"></iconify-icon></div>
68
+ <iconify-icon icon="ph:dots-three-vertical-bold" style="font-size:24px;color:white"></iconify-icon>
69
+ <div style="width:28px;height:28px;border-radius:4px;border:2px solid white;overflow:hidden;background:#1e293b"></div>
70
+ </div>`;
71
+ }
72
+
73
+ function buildReelsInfo(s: PlatformStrings): string {
74
+ return `<div style="position:absolute;left:16px;bottom:24px;right:64px">
75
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
76
+ <div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#f59e0b,#e11d48,#a855f7);padding:2px">
77
+ <div style="width:100%;height:100%;border-radius:50%;background:black"></div>
78
+ </div>
79
+ <span style="font-weight:bold;color:white;font-size:13px;letter-spacing:-.5px">jjlmoya</span>
80
+ <button style="padding:4px 12px;border:2px solid rgba(255,255,255,.5);border-radius:8px;font-size:11px;font-weight:bold;color:white;background:rgba(255,255,255,.05);cursor:pointer">${s.reelsFollow}</button>
81
+ </div>
82
+ <div style="color:white;font-size:13px;line-height:1.4;margin-bottom:8px">${s.reelsDescription}</div>
83
+ <div style="display:flex;align-items:center;gap:8px;color:rgba(255,255,255,.9);font-size:10px;font-weight:500;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
84
+ <iconify-icon icon="mdi:music-note"></iconify-icon><span>${s.reelsAudio}</span>
85
+ </div>
86
+ </div>`;
87
+ }
88
+
89
+ function buildReelsUI(s: PlatformStrings): string {
90
+ return buildReelsActions() + buildReelsInfo(s);
91
+ }
92
+
93
+ function buildShortsActions(s: PlatformStrings): string {
94
+ const iconBtn = (icon: string, label: string) =>
95
+ `<div style="${COL}"><div style="padding:12px;background:rgba(255,255,255,.1);border-radius:50%"><iconify-icon icon="${icon}" style="font-size:28px;color:white"></iconify-icon></div><span style="font-size:10px;color:white;font-weight:bold;margin-top:4px;text-shadow:0 1px 3px rgba(0,0,0,.8)">${label}</span></div>`;
96
+ return `<div style="position:absolute;top:40px;left:0;width:100%;display:flex;align-items:center;justify-content:space-between;padding:0 16px;box-sizing:border-box">
97
+ <iconify-icon icon="mdi:arrow-left" style="font-size:30px;color:white"></iconify-icon>
98
+ <div style="display:flex;gap:24px;align-items:center">
99
+ <iconify-icon icon="mdi:magnify" style="font-size:30px;color:white"></iconify-icon>
100
+ <iconify-icon icon="mdi:dots-vertical" style="font-size:30px;color:white"></iconify-icon>
101
+ </div>
102
+ </div>
103
+ <div style="position:absolute;right:8px;bottom:80px;display:flex;flex-direction:column;gap:24px;align-items:center">
104
+ ${iconBtn('ph:thumbs-up-fill', '1.2M')}${iconBtn('ph:thumbs-down-fill', s.shortsDislike)}
105
+ ${iconBtn('ph:chat-text-fill', '34K')}${iconBtn('ph:share-fat-fill', s.shortsShare)}${iconBtn('ph:arrows-clockwise-bold', s.shortsRemix)}
106
+ <div style="width:44px;height:44px;border-radius:12px;background:#1e293b;border:2px solid rgba(255,255,255,.1)"></div>
107
+ </div>`;
108
+ }
109
+
110
+ function buildShortsInfo(s: PlatformStrings): string {
111
+ return `<div style="position:absolute;left:16px;bottom:32px;right:80px">
112
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
113
+ <div style="width:40px;height:40px;border-radius:50%;background:#dc2626;border:1px solid rgba(255,255,255,.2)"></div>
114
+ <span style="color:white;font-weight:900;font-size:14px;text-shadow:0 1px 8px rgba(0,0,0,.8)">@jjlmoya</span>
115
+ <button style="background:white;color:black;padding:6px 16px;border-radius:999px;font-size:12px;font-weight:900;cursor:pointer">${s.shortsSubscribe}</button>
116
+ </div>
117
+ <div style="color:white;font-size:15px;font-weight:bold;line-height:1.4;text-shadow:0 1px 8px rgba(0,0,0,.8);margin-bottom:8px">${s.shortsDescription}</div>
118
+ <div style="display:flex;align-items:center;gap:8px">
119
+ <div style="background:rgba(255,255,255,.1);padding:4px 12px;border-radius:4px;display:flex;align-items:center;gap:8px;backdrop-filter:blur(4px)">
120
+ <iconify-icon icon="mdi:flash" style="color:white"></iconify-icon>
121
+ <span style="color:white;font-size:11px;font-weight:bold">SHORT</span>
122
+ </div>
123
+ </div>
124
+ </div>`;
125
+ }
126
+
127
+ function buildShortsUI(s: PlatformStrings): string {
128
+ return buildShortsActions(s) + buildShortsInfo(s);
129
+ }
130
+
131
+ export function buildPlatformUIs(s: PlatformStrings): Record<string, string> {
132
+ return {
133
+ tiktok: buildTikTokUI(s),
134
+ reels: buildReelsUI(s),
135
+ shorts: buildShortsUI(s),
136
+ };
137
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { safeZones } 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 safeZones.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+ <SEORenderer content={{ locale: locale as string, sections: content.seo }} />
@@ -0,0 +1,34 @@
1
+ export interface SafeZonesUI extends Record<string, string> {
2
+ labelConfig: string;
3
+ labelPlatform: string;
4
+ labelUpload: string;
5
+ uploadPrompt: string;
6
+ uploadLink: string;
7
+ loadContent: string;
8
+ labelOpacity: string;
9
+ labelMask: string;
10
+ labelGrid: string;
11
+ btnReset: string;
12
+ bannerTiktok: string;
13
+ bannerReels: string;
14
+ bannerShorts: string;
15
+ nameTiktok: string;
16
+ nameReels: string;
17
+ nameShorts: string;
18
+ ttFollowing: string;
19
+ ttForYou: string;
20
+ ttDescription: string;
21
+ ttAudio: string;
22
+ ttNavHome: string;
23
+ ttNavFriends: string;
24
+ ttNavInbox: string;
25
+ ttNavProfile: string;
26
+ reelsFollow: string;
27
+ reelsDescription: string;
28
+ reelsAudio: string;
29
+ shortsSubscribe: string;
30
+ shortsDislike: string;
31
+ shortsShare: string;
32
+ shortsRemix: string;
33
+ shortsDescription: string;
34
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { socialImageResizer } 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 socialImageResizer.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}