@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.
- package/assets/fonts/Manrope/Manrope-Bold.ttf +0 -0
- package/assets/fonts/Manrope/Manrope-Regular.ttf +0 -0
- package/assets/fonts/Others/AbyssinicaSIL-Regular.ttf +0 -0
- package/assets/fonts/Others/ChirpRegular.ttf +0 -0
- package/assets/fonts/Poppins/Poppins-Bold.ttf +0 -0
- package/assets/fonts/Poppins/Poppins-Medium.ttf +0 -0
- package/assets/fonts/Poppins/Poppins-Regular.ttf +0 -0
- package/assets/placeholders/album_art.png +0 -0
- package/assets/placeholders/avatar.png +0 -0
- package/assets/placeholders/badge.jpg +0 -0
- package/assets/placeholders/badge.png +0 -0
- package/assets/placeholders/badge_2.jpg +0 -0
- package/assets/placeholders/badge_3.jpg +0 -0
- package/assets/placeholders/badge_4.jpg +0 -0
- package/assets/placeholders/badge_5.jpg +0 -0
- package/assets/placeholders/banner.jpeg +0 -0
- package/assets/placeholders/images.jpeg +0 -0
- package/index.js +153 -0
- package/package.json +34 -0
- package/src/animation-effects.js +631 -0
- package/src/cache-manager.js +258 -0
- package/src/community-banner.js +1536 -0
- package/src/constants.js +208 -0
- package/src/discord-profile.js +584 -0
- package/src/e-commerce-banner.js +1214 -0
- package/src/effects.js +355 -0
- package/src/error-handler.js +305 -0
- package/src/event-banner.js +1319 -0
- package/src/facebook-post.js +679 -0
- package/src/gradient-welcome.js +430 -0
- package/src/image-filters.js +1034 -0
- package/src/image-processor.js +1014 -0
- package/src/instagram-post.js +504 -0
- package/src/interactive-elements.js +1208 -0
- package/src/linkedin-post.js +658 -0
- package/src/marketing-banner.js +1089 -0
- package/src/minimalist-banner.js +892 -0
- package/src/modern-profile.js +755 -0
- package/src/performance-optimizer.js +216 -0
- package/src/telegram-header.js +544 -0
- package/src/test-runner.js +645 -0
- package/src/tiktok-post.js +713 -0
- package/src/twitter-header.js +604 -0
- package/src/validator.js +442 -0
- package/src/welcome-leave.js +445 -0
- package/src/whatsapp-status.js +386 -0
- package/src/youtube-thumbnail.js +681 -0
- 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
|
+
|