@cognima/banners 0.0.1-beta

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 (48) hide show
  1. package/assets/fonts/Manrope/Manrope-Bold.ttf +0 -0
  2. package/assets/fonts/Manrope/Manrope-Regular.ttf +0 -0
  3. package/assets/fonts/Others/AbyssinicaSIL-Regular.ttf +0 -0
  4. package/assets/fonts/Others/ChirpRegular.ttf +0 -0
  5. package/assets/fonts/Poppins/Poppins-Bold.ttf +0 -0
  6. package/assets/fonts/Poppins/Poppins-Medium.ttf +0 -0
  7. package/assets/fonts/Poppins/Poppins-Regular.ttf +0 -0
  8. package/assets/placeholders/album_art.png +0 -0
  9. package/assets/placeholders/avatar.png +0 -0
  10. package/assets/placeholders/badge.jpg +0 -0
  11. package/assets/placeholders/badge.png +0 -0
  12. package/assets/placeholders/badge_2.jpg +0 -0
  13. package/assets/placeholders/badge_3.jpg +0 -0
  14. package/assets/placeholders/badge_4.jpg +0 -0
  15. package/assets/placeholders/badge_5.jpg +0 -0
  16. package/assets/placeholders/banner.jpeg +0 -0
  17. package/assets/placeholders/images.jpeg +0 -0
  18. package/index.js +153 -0
  19. package/package.json +34 -0
  20. package/src/animation-effects.js +631 -0
  21. package/src/cache-manager.js +258 -0
  22. package/src/community-banner.js +1536 -0
  23. package/src/constants.js +208 -0
  24. package/src/discord-profile.js +584 -0
  25. package/src/e-commerce-banner.js +1214 -0
  26. package/src/effects.js +355 -0
  27. package/src/error-handler.js +305 -0
  28. package/src/event-banner.js +1319 -0
  29. package/src/facebook-post.js +679 -0
  30. package/src/gradient-welcome.js +430 -0
  31. package/src/image-filters.js +1034 -0
  32. package/src/image-processor.js +1014 -0
  33. package/src/instagram-post.js +504 -0
  34. package/src/interactive-elements.js +1208 -0
  35. package/src/linkedin-post.js +658 -0
  36. package/src/marketing-banner.js +1089 -0
  37. package/src/minimalist-banner.js +892 -0
  38. package/src/modern-profile.js +755 -0
  39. package/src/performance-optimizer.js +216 -0
  40. package/src/telegram-header.js +544 -0
  41. package/src/test-runner.js +645 -0
  42. package/src/tiktok-post.js +713 -0
  43. package/src/twitter-header.js +604 -0
  44. package/src/validator.js +442 -0
  45. package/src/welcome-leave.js +445 -0
  46. package/src/whatsapp-status.js +386 -0
  47. package/src/youtube-thumbnail.js +681 -0
  48. package/utils.js +710 -0
@@ -0,0 +1,658 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Módulo de Banner de Post do LinkedIn
5
+ *
6
+ * Este módulo gera banners no estilo de posts do LinkedIn com
7
+ * elementos visuais característicos da plataforma.
8
+ *
9
+ * @author Cognima Team (melhorado)
10
+ * @version 2.0.0
11
+ */
12
+
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+
15
+ const pureimage = require("pureimage");
16
+ const path = require("path");
17
+ const {
18
+ loadImageWithAxios,
19
+ encodeToBuffer,
20
+ roundRect,
21
+ wrapText,
22
+ registerFontIfNeeded,
23
+ isValidHexColor,
24
+ DEFAULT_FONT_FAMILY,
25
+ applyTextShadow,
26
+ clearShadow,
27
+ createLinearGradient,
28
+ hexToRgba,
29
+ formatNumber
30
+ } = require("../utils");
31
+
32
+ const {
33
+ DEFAULT_COLORS,
34
+ LAYOUT,
35
+ DEFAULT_DIMENSIONS
36
+ } = require("./constants");
37
+
38
+ /**
39
+ * @class LinkedInPost
40
+ * @classdesc Gera um banner no estilo de post do LinkedIn.
41
+ * @example const post = new LinkedInPost()
42
+ * .setName("Nome Completo")
43
+ * .setTitle("Cargo | Empresa")
44
+ * .setContent("Conteúdo do post com #hashtags")
45
+ * .setImage("imagem.jpg")
46
+ * .setLikes(500)
47
+ * .setComments(50)
48
+ * .setShares(20)
49
+ * .build();
50
+ */
51
+ module.exports = class LinkedInPost {
52
+ constructor(options) {
53
+ // Dados Principais
54
+ this.name = "Nome Completo";
55
+ this.title = "Cargo | Empresa";
56
+ this.avatar = null;
57
+ this.content = "Conteúdo do post com #hashtags";
58
+ this.image = null;
59
+ this.likes = 0;
60
+ this.comments = 0;
61
+ this.shares = 0;
62
+ this.postTime = "1h";
63
+ this.isPremium = false;
64
+ this.isCompanyPost = false;
65
+ this.companyLogo = null;
66
+ this.companyName = null;
67
+ this.hashtags = [];
68
+ this.pollOptions = null;
69
+ this.pollVotes = null;
70
+ this.pollTimeLeft = null;
71
+
72
+ // Personalização Visual
73
+ this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path };
74
+ this.theme = "light"; // light, dark
75
+ this.postType = "standard"; // standard, article, poll, document, event
76
+ this.cornerRadius = LAYOUT.cornerRadius.small;
77
+
78
+ // Configurações de Layout
79
+ this.cardWidth = DEFAULT_DIMENSIONS.post.width;
80
+ this.cardHeight = 800;
81
+ }
82
+
83
+ // --- Setters para Dados Principais ---
84
+ /**
85
+ * Define o nome completo
86
+ * @param {string} text - Nome completo
87
+ * @returns {LinkedInPost} - Instância atual para encadeamento
88
+ */
89
+ setName(text) {
90
+ if (!text || typeof text !== "string") throw new Error("O nome completo deve ser uma string não vazia.");
91
+ this.name = text;
92
+ return this;
93
+ }
94
+
95
+ /**
96
+ * Define o título/cargo
97
+ * @param {string} text - Título ou cargo
98
+ * @returns {LinkedInPost} - Instância atual para encadeamento
99
+ */
100
+ setTitle(text) {
101
+ if (!text || typeof text !== "string") throw new Error("O título deve ser uma string não vazia.");
102
+ this.title = text;
103
+ return this;
104
+ }
105
+
106
+ /**
107
+ * Define o avatar
108
+ * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do avatar
109
+ * @returns {LinkedInPost} - Instância atual para encadeamento
110
+ */
111
+ setAvatar(image) {
112
+ if (!image) throw new Error("A fonte da imagem do avatar não pode estar vazia.");
113
+ this.avatar = image;
114
+ return this;
115
+ }
116
+
117
+ /**
118
+ * Define o conteúdo do post
119
+ * @param {string} text - Texto do conteúdo
120
+ * @returns {LinkedInPost} - Instância atual para encadeamento
121
+ */
122
+ setContent(text) {
123
+ if (!text || typeof text !== "string") throw new Error("O conteúdo deve ser uma string não vazia.");
124
+ this.content = text;
125
+
126
+ // Extrai hashtags automaticamente
127
+ const hashtagRegex = /#(\w+)/g;
128
+ const matches = text.match(hashtagRegex);
129
+
130
+ if (matches) {
131
+ this.hashtags = matches.map(tag => tag.substring(1)); // Remove o # do início
132
+ }
133
+
134
+ return this;
135
+ }
136
+
137
+ /**
138
+ * Define a imagem principal do post
139
+ * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem
140
+ * @returns {LinkedInPost} - Instância atual para encadeamento
141
+ */
142
+ setImage(image) {
143
+ if (!image) throw new Error("A fonte da imagem não pode estar vazia.");
144
+ this.image = image;
145
+ return this;
146
+ }
147
+
148
+ /**
149
+ * Define o número de curtidas
150
+ * @param {number} count - Número de curtidas
151
+ * @returns {LinkedInPost} - Instância atual para encadeamento
152
+ */
153
+ setLikes(count) {
154
+ if (typeof count !== "number" || count < 0) throw new Error("O número de curtidas deve ser um número não negativo.");
155
+ this.likes = count;
156
+ return this;
157
+ }
158
+
159
+ /**
160
+ * Define o número de comentários
161
+ * @param {number} count - Número de comentários
162
+ * @returns {LinkedInPost} - Instância atual para encadeamento
163
+ */
164
+ setComments(count) {
165
+ if (typeof count !== "number" || count < 0) throw new Error("O número de comentários deve ser um número não negativo.");
166
+ this.comments = count;
167
+ return this;
168
+ }
169
+
170
+ /**
171
+ * Define o número de compartilhamentos
172
+ * @param {number} count - Número de compartilhamentos
173
+ * @returns {LinkedInPost} - Instância atual para encadeamento
174
+ */
175
+ setShares(count) {
176
+ if (typeof count !== "number" || count < 0) throw new Error("O número de compartilhamentos deve ser um número não negativo.");
177
+ this.shares = count;
178
+ return this;
179
+ }
180
+
181
+ /**
182
+ * Define o tempo de publicação
183
+ * @param {string} text - Tempo de publicação (ex: "1h", "2d", "1sem")
184
+ * @returns {LinkedInPost} - Instância atual para encadeamento
185
+ */
186
+ setPostTime(text) {
187
+ if (!text || typeof text !== "string") throw new Error("O tempo de publicação deve ser uma string não vazia.");
188
+ this.postTime = text;
189
+ return this;
190
+ }
191
+
192
+ /**
193
+ * Define se o usuário é premium
194
+ * @param {boolean} isPremium - Se o usuário é premium
195
+ * @returns {LinkedInPost} - Instância atual para encadeamento
196
+ */
197
+ setPremium(isPremium = true) {
198
+ this.isPremium = !!isPremium;
199
+ return this;
200
+ }
201
+
202
+ /**
203
+ * Define se é um post de empresa
204
+ * @param {boolean} isCompanyPost - Se é um post de empresa
205
+ * @param {string} companyName - Nome da empresa
206
+ * @param {string|Buffer|Object} companyLogo - URL, Buffer ou caminho do logo da empresa
207
+ * @returns {LinkedInPost} - Instância atual para encadeamento
208
+ */
209
+ setCompanyPost(isCompanyPost = true, companyName = null, companyLogo = null) {
210
+ this.isCompanyPost = !!isCompanyPost;
211
+
212
+ if (isCompanyPost) {
213
+ if (!companyName || typeof companyName !== "string") {
214
+ throw new Error("O nome da empresa deve ser uma string não vazia.");
215
+ }
216
+
217
+ this.companyName = companyName;
218
+ this.companyLogo = companyLogo;
219
+ }
220
+
221
+ return this;
222
+ }
223
+
224
+ /**
225
+ * Define as opções de enquete
226
+ * @param {Array<string>} options - Array de opções da enquete
227
+ * @param {Array<number>} votes - Array de votos para cada opção
228
+ * @param {string} timeLeft - Tempo restante da enquete (ex: "2 dias restantes")
229
+ * @returns {LinkedInPost} - Instância atual para encadeamento
230
+ */
231
+ setPoll(options, votes = null, timeLeft = null) {
232
+ if (!Array.isArray(options) || options.length < 2) {
233
+ throw new Error("As opções da enquete devem ser um array com pelo menos 2 itens.");
234
+ }
235
+
236
+ this.pollOptions = options;
237
+ this.pollVotes = votes;
238
+ this.pollTimeLeft = timeLeft;
239
+ this.postType = "poll";
240
+
241
+ return this;
242
+ }
243
+
244
+ // --- Setters para Personalização Visual ---
245
+ /**
246
+ * Define o tema
247
+ * @param {string} theme - Tema ('light', 'dark')
248
+ * @returns {LinkedInPost} - Instância atual para encadeamento
249
+ */
250
+ setTheme(theme) {
251
+ const validThemes = ["light", "dark"];
252
+ if (!theme || !validThemes.includes(theme.toLowerCase())) {
253
+ throw new Error(`Tema inválido. Use um dos seguintes: ${validThemes.join(", ")}`);
254
+ }
255
+
256
+ this.theme = theme.toLowerCase();
257
+ return this;
258
+ }
259
+
260
+ /**
261
+ * Define o tipo de post
262
+ * @param {string} type - Tipo de post ('standard', 'article', 'poll', 'document', 'event')
263
+ * @returns {LinkedInPost} - Instância atual para encadeamento
264
+ */
265
+ setPostType(type) {
266
+ const validTypes = ["standard", "article", "poll", "document", "event"];
267
+ if (!type || !validTypes.includes(type.toLowerCase())) {
268
+ throw new Error(`Tipo de post inválido. Use um dos seguintes: ${validTypes.join(", ")}`);
269
+ }
270
+
271
+ this.postType = type.toLowerCase();
272
+ return this;
273
+ }
274
+
275
+ /**
276
+ * Define o raio dos cantos arredondados
277
+ * @param {number} radius - Raio dos cantos em pixels
278
+ * @returns {LinkedInPost} - Instância atual para encadeamento
279
+ */
280
+ setCornerRadius(radius) {
281
+ if (typeof radius !== "number" || radius < 0) throw new Error("O raio dos cantos deve ser um número não negativo.");
282
+ this.cornerRadius = radius;
283
+ return this;
284
+ }
285
+
286
+ /**
287
+ * Define as dimensões do card
288
+ * @param {number} width - Largura do card em pixels
289
+ * @param {number} height - Altura do card em pixels
290
+ * @returns {LinkedInPost} - Instância atual para encadeamento
291
+ */
292
+ setCardDimensions(width, height) {
293
+ if (typeof width !== "number" || width < 400 || width > 1200) {
294
+ throw new Error("A largura do card deve estar entre 400 e 1200 pixels.");
295
+ }
296
+
297
+ if (typeof height !== "number" || height < 400 || height > 1200) {
298
+ throw new Error("A altura do card deve estar entre 400 e 1200 pixels.");
299
+ }
300
+
301
+ this.cardWidth = width;
302
+ this.cardHeight = height;
303
+
304
+ return this;
305
+ }
306
+
307
+ // --- Método de Construção ---
308
+ /**
309
+ * Constrói o banner e retorna um buffer de imagem
310
+ * @returns {Promise<Buffer>} - Buffer contendo a imagem do banner
311
+ */
312
+ async build() {
313
+ // --- Registro de Fonte ---
314
+ const registeredFontName = await registerFontIfNeeded(this.font);
315
+
316
+ // --- Configuração do Canvas ---
317
+ const cardWidth = this.cardWidth;
318
+ const cardHeight = this.cardHeight;
319
+ const cornerRadius = this.cornerRadius;
320
+ const padding = 20;
321
+
322
+ const canvas = pureimage.make(cardWidth, cardHeight);
323
+ const ctx = canvas.getContext("2d");
324
+
325
+ // --- Configuração de Cores com base no Tema ---
326
+ const colors = this._getThemeColors();
327
+
328
+ // --- Desenha Plano de Fundo ---
329
+ ctx.fillStyle = colors.background;
330
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
331
+
332
+ // --- Desenha Cabeçalho do Post ---
333
+ const headerHeight = 80;
334
+
335
+ // Avatar
336
+ const avatarSize = 60;
337
+ const avatarX = padding;
338
+ const avatarY = padding;
339
+
340
+ ctx.save();
341
+ ctx.beginPath();
342
+ ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
343
+ ctx.closePath();
344
+ ctx.clip();
345
+
346
+ try {
347
+ const avatarImg = await loadImageWithAxios(this.avatar || path.join(__dirname, "../assets/placeholders/avatar.png"));
348
+ ctx.drawImage(avatarImg, avatarX, avatarY, avatarSize, avatarSize);
349
+ } catch (e) {
350
+ console.error("Falha ao desenhar avatar:", e.message);
351
+
352
+ // Avatar de fallback
353
+ ctx.fillStyle = "#0A66C2";
354
+ ctx.fillRect(avatarX, avatarY, avatarSize, avatarSize);
355
+
356
+ ctx.fillStyle = "#FFFFFF";
357
+ ctx.font = `bold ${avatarSize / 3}px ${registeredFontName}-Bold`;
358
+ ctx.textAlign = "center";
359
+ ctx.textBaseline = "middle";
360
+ ctx.fillText(this.name.charAt(0).toUpperCase(), avatarX + avatarSize / 2, avatarY + avatarSize / 2);
361
+ }
362
+
363
+ ctx.restore();
364
+
365
+ // Informações do usuário
366
+ const infoX = avatarX + avatarSize + 15;
367
+ let infoY = avatarY + 5;
368
+
369
+ // Nome
370
+ ctx.fillStyle = colors.text;
371
+ ctx.font = `bold 16px ${registeredFontName}-Bold`;
372
+ ctx.textAlign = "left";
373
+ ctx.textBaseline = "top";
374
+
375
+ const nameText = this.name;
376
+ const nameWidth = ctx.measureText(nameText).width;
377
+ ctx.fillText(nameText, infoX, infoY);
378
+
379
+ // Ícone de premium (se aplicável)
380
+ if (this.isPremium) {
381
+ const premiumSize = 16;
382
+ const premiumX = infoX + nameWidth + 5;
383
+
384
+ ctx.fillStyle = "#0A66C2";
385
+ ctx.beginPath();
386
+ ctx.arc(premiumX + premiumSize / 2, infoY + premiumSize / 2, premiumSize / 2, 0, Math.PI * 2);
387
+ ctx.fill();
388
+
389
+ ctx.fillStyle = "#FFFFFF";
390
+ ctx.font = `bold 12px ${registeredFontName}-Bold`;
391
+ ctx.textAlign = "center";
392
+ ctx.fillText("in", premiumX + premiumSize / 2, infoY + premiumSize / 2);
393
+ }
394
+
395
+ // Título
396
+ infoY += 20;
397
+ ctx.fillStyle = colors.textSecondary;
398
+ ctx.font = `regular 14px ${registeredFontName}-Regular`;
399
+ ctx.textAlign = "left";
400
+ ctx.fillText(this.title, infoX, infoY);
401
+
402
+ // Tempo de publicação
403
+ infoY += 20;
404
+ ctx.fillText(this.postTime, infoX, infoY);
405
+
406
+ // Ícone de público
407
+ const publicIconX = infoX + ctx.measureText(this.postTime).width + 10;
408
+ ctx.fillText("• 🌎", publicIconX, infoY);
409
+
410
+ // Botão de seguir
411
+ const followButtonWidth = 80;
412
+ const followButtonHeight = 30;
413
+ const followButtonX = cardWidth - followButtonWidth - padding;
414
+ const followButtonY = padding + 15;
415
+
416
+ ctx.strokeStyle = "#0A66C2";
417
+ ctx.lineWidth = 1;
418
+ roundRect(ctx, followButtonX, followButtonY, followButtonWidth, followButtonHeight, followButtonHeight / 2, false, true);
419
+
420
+ ctx.fillStyle = "#0A66C2";
421
+ ctx.font = `bold 14px ${registeredFontName}-Bold`;
422
+ ctx.textAlign = "center";
423
+ ctx.textBaseline = "middle";
424
+ ctx.fillText("+ Seguir", followButtonX + followButtonWidth / 2, followButtonY + followButtonHeight / 2);
425
+
426
+ // Botão de mais opções
427
+ const moreButtonX = followButtonX - 40;
428
+ const moreButtonY = followButtonY + followButtonHeight / 2;
429
+
430
+ ctx.fillStyle = colors.textSecondary;
431
+ ctx.textAlign = "center";
432
+ ctx.fillText("•••", moreButtonX, moreButtonY);
433
+
434
+ // --- Desenha Conteúdo do Post ---
435
+ let contentY = headerHeight + padding;
436
+
437
+ // Texto do post
438
+ ctx.fillStyle = colors.text;
439
+ ctx.font = `regular 16px ${registeredFontName}-Regular`;
440
+ ctx.textAlign = "left";
441
+ ctx.textBaseline = "top";
442
+
443
+ contentY = wrapText(ctx, this.content, padding, contentY, cardWidth - padding * 2, 24, registeredFontName);
444
+ contentY += padding;
445
+
446
+ // Imagem (se fornecida)
447
+ if (this.image && this.postType !== "poll") {
448
+ try {
449
+ const imageHeight = 300;
450
+ const imageY = contentY;
451
+
452
+ ctx.save();
453
+
454
+ // Recorta a imagem com cantos arredondados
455
+ roundRect(ctx, padding, imageY, cardWidth - padding * 2, imageHeight, cornerRadius, false, false);
456
+ ctx.clip();
457
+
458
+ const img = await loadImageWithAxios(this.image);
459
+ const aspect = img.width / img.height;
460
+ const imageWidth = cardWidth - padding * 2;
461
+
462
+ // Ajusta as dimensões para manter a proporção
463
+ let drawWidth = imageWidth;
464
+ let drawHeight = imageWidth / aspect;
465
+
466
+ if (drawHeight > imageHeight) {
467
+ drawHeight = imageHeight;
468
+ drawWidth = imageHeight * aspect;
469
+ }
470
+
471
+ const offsetX = padding + (imageWidth - drawWidth) / 2;
472
+ const offsetY = imageY + (imageHeight - drawHeight) / 2;
473
+
474
+ ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
475
+
476
+ ctx.restore();
477
+
478
+ contentY = imageY + imageHeight + padding;
479
+ } catch (e) {
480
+ console.error("Falha ao desenhar imagem:", e.message);
481
+ }
482
+ }
483
+
484
+ // Enquete (se aplicável)
485
+ if (this.postType === "poll" && this.pollOptions) {
486
+ const pollY = contentY;
487
+ const pollWidth = cardWidth - padding * 2;
488
+ const optionHeight = 50;
489
+ const pollPadding = 15;
490
+ const pollHeight = this.pollOptions.length * (optionHeight + pollPadding) + pollPadding;
491
+
492
+ // Fundo da enquete
493
+ ctx.fillStyle = colors.cardBackground;
494
+ roundRect(ctx, padding, pollY, pollWidth, pollHeight, cornerRadius, true, false);
495
+
496
+ // Opções da enquete
497
+ let optionY = pollY + pollPadding;
498
+ const totalVotes = this.pollVotes ? this.pollVotes.reduce((a, b) => a + b, 0) : 0;
499
+
500
+ this.pollOptions.forEach((option, index) => {
501
+ const votePercentage = this.pollVotes && totalVotes > 0 ? (this.pollVotes[index] / totalVotes) * 100 : 0;
502
+
503
+ // Barra de progresso
504
+ ctx.fillStyle = colors.pollBackground;
505
+ roundRect(ctx, padding + pollPadding, optionY, pollWidth - pollPadding * 2, optionHeight, optionHeight / 2, true, false);
506
+
507
+ if (votePercentage > 0) {
508
+ ctx.fillStyle = "#0A66C2";
509
+ roundRect(
510
+ ctx,
511
+ padding + pollPadding,
512
+ optionY,
513
+ (pollWidth - pollPadding * 2) * (votePercentage / 100),
514
+ optionHeight,
515
+ optionHeight / 2,
516
+ true,
517
+ false
518
+ );
519
+ }
520
+
521
+ // Texto da opção
522
+ ctx.fillStyle = colors.text;
523
+ ctx.font = `regular 16px ${registeredFontName}-Regular`;
524
+ ctx.textAlign = "left";
525
+ ctx.textBaseline = "middle";
526
+ ctx.fillText(option, padding + pollPadding * 2, optionY + optionHeight / 2);
527
+
528
+ // Porcentagem (se aplicável)
529
+ if (this.pollVotes) {
530
+ ctx.fillStyle = colors.textSecondary;
531
+ ctx.font = `regular 14px ${registeredFontName}-Regular`;
532
+ ctx.textAlign = "right";
533
+ ctx.fillText(
534
+ `${Math.round(votePercentage)}%`,
535
+ cardWidth - padding - pollPadding * 2,
536
+ optionY + optionHeight / 2
537
+ );
538
+ }
539
+
540
+ optionY += optionHeight + pollPadding;
541
+ });
542
+
543
+ // Tempo restante (se fornecido)
544
+ if (this.pollTimeLeft) {
545
+ ctx.fillStyle = colors.textSecondary;
546
+ ctx.font = `regular 14px ${registeredFontName}-Regular`;
547
+ ctx.textAlign = "left";
548
+ ctx.textBaseline = "top";
549
+ ctx.fillText(this.pollTimeLeft, padding + pollPadding, optionY);
550
+ }
551
+
552
+ contentY = pollY + pollHeight + padding;
553
+ }
554
+
555
+ // Tipo de post específico (se aplicável)
556
+ if (this.postType === "article" || this.postType === "document" || this.postType === "event") {
557
+ const typeY = contentY;
558
+ const typeWidth = cardWidth - padding * 2;
559
+ const typeHeight = 30;
560
+
561
+ ctx.fillStyle = colors.cardBackground;
562
+ roundRect(ctx, padding, typeY, typeWidth, typeHeight, cornerRadius, true, false);
563
+
564
+ ctx.fillStyle = colors.textSecondary;
565
+ ctx.font = `regular 14px ${registeredFontName}-Regular`;
566
+ ctx.textAlign = "center";
567
+ ctx.textBaseline = "middle";
568
+
569
+ let typeText = "";
570
+ switch (this.postType) {
571
+ case "article":
572
+ typeText = "Artigo";
573
+ break;
574
+ case "document":
575
+ typeText = "Documento";
576
+ break;
577
+ case "event":
578
+ typeText = "Evento";
579
+ break;
580
+ }
581
+
582
+ ctx.fillText(typeText, padding + typeWidth / 2, typeY + typeHeight / 2);
583
+
584
+ contentY = typeY + typeHeight + padding;
585
+ }
586
+
587
+ // --- Desenha Barra de Interações ---
588
+ const interactionBarY = Math.max(contentY, cardHeight - 80);
589
+ const interactionBarHeight = 50;
590
+
591
+ // Linha separadora
592
+ ctx.strokeStyle = colors.separator;
593
+ ctx.lineWidth = 1;
594
+ ctx.beginPath();
595
+ ctx.moveTo(padding, interactionBarY);
596
+ ctx.lineTo(cardWidth - padding, interactionBarY);
597
+ ctx.stroke();
598
+
599
+ // Ícones de interação
600
+ const iconSpacing = (cardWidth - padding * 2) / 4;
601
+ const iconY = interactionBarY + interactionBarHeight / 2;
602
+
603
+ // Ícone de curtida
604
+ ctx.fillStyle = colors.textSecondary;
605
+ ctx.font = `regular 14px ${registeredFontName}-Regular`;
606
+ ctx.textAlign = "center";
607
+ ctx.textBaseline = "middle";
608
+
609
+ ctx.fillText(`👍 ${formatNumber(this.likes)}`, padding + iconSpacing / 2, iconY);
610
+
611
+ // Ícone de comentário
612
+ ctx.fillText(`💬 ${formatNumber(this.comments)}`, padding + iconSpacing * 1.5, iconY);
613
+
614
+ // Ícone de compartilhamento
615
+ ctx.fillText(`↗ ${formatNumber(this.shares)}`, padding + iconSpacing * 2.5, iconY);
616
+
617
+ // Ícone de envio
618
+ ctx.fillText("✉", padding + iconSpacing * 3.5, iconY);
619
+
620
+ // --- Codifica e Retorna Buffer ---
621
+ try {
622
+ return await encodeToBuffer(canvas);
623
+ } catch (err) {
624
+ console.error("Falha ao codificar o Post do LinkedIn:", err);
625
+ throw new Error("Não foi possível gerar o buffer de imagem do Post do LinkedIn.");
626
+ }
627
+ }
628
+
629
+ // --- Métodos Auxiliares Privados ---
630
+ /**
631
+ * Obtém as cores com base no tema selecionado
632
+ * @private
633
+ */
634
+ _getThemeColors() {
635
+ switch (this.theme) {
636
+ case "dark":
637
+ return {
638
+ background: "#1C1C1C",
639
+ cardBackground: "#2D2D2D",
640
+ pollBackground: "#3D3D3D",
641
+ text: "#FFFFFF",
642
+ textSecondary: "#B3B3B3",
643
+ separator: "#3D3D3D"
644
+ };
645
+ case "light":
646
+ default:
647
+ return {
648
+ background: "#FFFFFF",
649
+ cardBackground: "#F3F2EF",
650
+ pollBackground: "#E9E5DF",
651
+ text: "#000000",
652
+ textSecondary: "#666666",
653
+ separator: "#E9E5DF"
654
+ };
655
+ }
656
+ }
657
+ };
658
+