mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
7 files changed
+133
-10
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 509 KiB |
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { ChevronLeft, ChevronRight, Send, Facebook, Instagram, Heart, MessageCircle, Share2, Bookmark } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Send, Facebook, Instagram, Heart, MessageCircle, Share2, Bookmark } from 'lucide-react';
|
||||||
import type { Media } from '@shared/schema';
|
import type { Media } from '@shared/schema';
|
||||||
|
import { removeEmojis } from '@shared/emoji';
|
||||||
|
|
||||||
interface PreviewModalProps {
|
interface PreviewModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -227,8 +228,10 @@ export function PreviewModal({
|
|||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) return [text];
|
if (!ctx) return [text];
|
||||||
|
|
||||||
|
// Remove emojis for better text rendering
|
||||||
|
const cleanText = removeEmojis(text);
|
||||||
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
|
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
|
||||||
const words = text.split(' ');
|
const words = cleanText.split(' ');
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
let currentLine = '';
|
let currentLine = '';
|
||||||
|
|
||||||
@@ -237,8 +240,27 @@ export function PreviewModal({
|
|||||||
const metrics = ctx.measureText(testLine);
|
const metrics = ctx.measureText(testLine);
|
||||||
|
|
||||||
if (metrics.width > maxWidth && currentLine) {
|
if (metrics.width > maxWidth && currentLine) {
|
||||||
|
// Current line would exceed width, push it and start new line
|
||||||
lines.push(currentLine);
|
lines.push(currentLine);
|
||||||
currentLine = word;
|
currentLine = word;
|
||||||
|
|
||||||
|
// Check if the word itself is too long for a single line
|
||||||
|
const wordMetrics = ctx.measureText(word);
|
||||||
|
if (wordMetrics.width > maxWidth) {
|
||||||
|
// Word is too long, need to break it into chunks
|
||||||
|
const chunks = breakLongWord(ctx, word, maxWidth);
|
||||||
|
for (let i = 0; i < chunks.length - 1; i++) {
|
||||||
|
lines.push(chunks[i]);
|
||||||
|
}
|
||||||
|
currentLine = chunks[chunks.length - 1];
|
||||||
|
}
|
||||||
|
} else if (metrics.width > maxWidth && !currentLine) {
|
||||||
|
// First word is too long, need to break it
|
||||||
|
const chunks = breakLongWord(ctx, word, maxWidth);
|
||||||
|
for (let i = 0; i < chunks.length - 1; i++) {
|
||||||
|
lines.push(chunks[i]);
|
||||||
|
}
|
||||||
|
currentLine = chunks[chunks.length - 1];
|
||||||
} else {
|
} else {
|
||||||
currentLine = testLine;
|
currentLine = testLine;
|
||||||
}
|
}
|
||||||
@@ -251,6 +273,33 @@ export function PreviewModal({
|
|||||||
return lines;
|
return lines;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breaks a long word into chunks that fit within maxWidth
|
||||||
|
*/
|
||||||
|
const breakLongWord = (ctx: CanvasRenderingContext2D, word: string, maxWidth: number): string[] => {
|
||||||
|
const chunks: string[] = [];
|
||||||
|
let currentChunk = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < word.length; i++) {
|
||||||
|
const char = word[i];
|
||||||
|
const testChunk = currentChunk + char;
|
||||||
|
const metrics = ctx.measureText(testChunk);
|
||||||
|
|
||||||
|
if (metrics.width > maxWidth && currentChunk) {
|
||||||
|
chunks.push(currentChunk);
|
||||||
|
currentChunk = char;
|
||||||
|
} else {
|
||||||
|
currentChunk = testChunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChunk) {
|
||||||
|
chunks.push(currentChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks.length > 0 ? chunks : [word];
|
||||||
|
};
|
||||||
|
|
||||||
const calculateAdaptiveFontSize = (text: string, containerWidth: number, containerHeight: number): { fontSize: number; lines: string[]; isTruncated: boolean } => {
|
const calculateAdaptiveFontSize = (text: string, containerWidth: number, containerHeight: number): { fontSize: number; lines: string[]; isTruncated: boolean } => {
|
||||||
const MIN_FONT_SIZE = 12;
|
const MIN_FONT_SIZE = 12;
|
||||||
const MAX_FONT_SIZE = 72;
|
const MAX_FONT_SIZE = 72;
|
||||||
|
|||||||
Generated
+10
-3
@@ -58,6 +58,7 @@
|
|||||||
"drizzle-orm": "^0.39.1",
|
"drizzle-orm": "^0.39.1",
|
||||||
"drizzle-zod": "^0.7.0",
|
"drizzle-zod": "^0.7.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"emoji-regex": "^10.6.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
@@ -5341,9 +5342,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "9.2.2",
|
"version": "10.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
@@ -8293,6 +8294,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width/node_modules/emoji-regex": {
|
||||||
|
"version": "9.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
"drizzle-orm": "^0.39.1",
|
"drizzle-orm": "^0.39.1",
|
||||||
"drizzle-zod": "^0.7.0",
|
"drizzle-zod": "^0.7.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"emoji-regex": "^10.6.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
|
|||||||
+4
-4
@@ -1247,9 +1247,9 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
|
|
||||||
// Calculate token expiration date (90 days from now)
|
// Calculate token expiration date (60 days from now)
|
||||||
const tokenExpiresAt = new Date();
|
const tokenExpiresAt = new Date();
|
||||||
tokenExpiresAt.setDate(tokenExpiresAt.getDate() + 90);
|
tokenExpiresAt.setDate(tokenExpiresAt.getDate() + 60);
|
||||||
|
|
||||||
const pageData = insertSocialPageSchema.parse({
|
const pageData = insertSocialPageSchema.parse({
|
||||||
...req.body,
|
...req.body,
|
||||||
@@ -1277,10 +1277,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
let pageData = insertSocialPageSchema.partial().parse(req.body);
|
let pageData = insertSocialPageSchema.partial().parse(req.body);
|
||||||
|
|
||||||
// If accessToken is being updated, recalculate expiration date (90 days from now)
|
// If accessToken is being updated, recalculate expiration date (60 days from now)
|
||||||
if (pageData.accessToken) {
|
if (pageData.accessToken) {
|
||||||
const tokenExpiresAt = new Date();
|
const tokenExpiresAt = new Date();
|
||||||
tokenExpiresAt.setDate(tokenExpiresAt.getDate() + 90);
|
tokenExpiresAt.setDate(tokenExpiresAt.getDate() + 60);
|
||||||
pageData = { ...pageData, tokenExpiresAt };
|
pageData = { ...pageData, tokenExpiresAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { randomUUID } from "crypto";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { createCanvas, loadImage, registerFont } from "canvas";
|
import { createCanvas, loadImage, registerFont } from "canvas";
|
||||||
|
import { removeEmojis } from "@shared/emoji";
|
||||||
|
|
||||||
interface CropDimensions {
|
interface CropDimensions {
|
||||||
width: number;
|
width: number;
|
||||||
@@ -117,6 +118,9 @@ export class ImageProcessor {
|
|||||||
const PADDING = 40;
|
const PADDING = 40;
|
||||||
const LINE_HEIGHT_MULTIPLIER = 1.3;
|
const LINE_HEIGHT_MULTIPLIER = 1.3;
|
||||||
|
|
||||||
|
// Remove emojis from text as canvas doesn't support them
|
||||||
|
const cleanText = removeEmojis(text);
|
||||||
|
|
||||||
const canvas = createCanvas(STORY_WIDTH, STORY_HEIGHT);
|
const canvas = createCanvas(STORY_WIDTH, STORY_HEIGHT);
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
@@ -161,7 +165,7 @@ export class ImageProcessor {
|
|||||||
while (fontSize >= MIN_FONT_SIZE) {
|
while (fontSize >= MIN_FONT_SIZE) {
|
||||||
ctx.font = `bold ${fontSize}px "DejaVu Sans", Arial, sans-serif`;
|
ctx.font = `bold ${fontSize}px "DejaVu Sans", Arial, sans-serif`;
|
||||||
|
|
||||||
lines = this.wrapText(ctx, text, maxTextWidth);
|
lines = this.wrapText(ctx, cleanText, maxTextWidth);
|
||||||
const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER;
|
const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER;
|
||||||
totalTextHeight = lines.length * lineHeight;
|
totalTextHeight = lines.length * lineHeight;
|
||||||
|
|
||||||
@@ -175,7 +179,7 @@ export class ImageProcessor {
|
|||||||
if (fontSize < MIN_FONT_SIZE) {
|
if (fontSize < MIN_FONT_SIZE) {
|
||||||
fontSize = MIN_FONT_SIZE;
|
fontSize = MIN_FONT_SIZE;
|
||||||
ctx.font = `bold ${fontSize}px "DejaVu Sans", Arial, sans-serif`;
|
ctx.font = `bold ${fontSize}px "DejaVu Sans", Arial, sans-serif`;
|
||||||
lines = this.wrapText(ctx, text, maxTextWidth);
|
lines = this.wrapText(ctx, cleanText, maxTextWidth);
|
||||||
const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER;
|
const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER;
|
||||||
totalTextHeight = lines.length * lineHeight;
|
totalTextHeight = lines.length * lineHeight;
|
||||||
|
|
||||||
@@ -214,8 +218,27 @@ export class ImageProcessor {
|
|||||||
const metrics = ctx.measureText(testLine);
|
const metrics = ctx.measureText(testLine);
|
||||||
|
|
||||||
if (metrics.width > maxWidth && currentLine) {
|
if (metrics.width > maxWidth && currentLine) {
|
||||||
|
// Current line would exceed width, push it and start new line
|
||||||
lines.push(currentLine);
|
lines.push(currentLine);
|
||||||
currentLine = word;
|
currentLine = word;
|
||||||
|
|
||||||
|
// Check if the word itself is too long for a single line
|
||||||
|
const wordMetrics = ctx.measureText(word);
|
||||||
|
if (wordMetrics.width > maxWidth) {
|
||||||
|
// Word is too long, need to break it into chunks
|
||||||
|
const chunks = this.breakLongWord(ctx, word, maxWidth);
|
||||||
|
for (let i = 0; i < chunks.length - 1; i++) {
|
||||||
|
lines.push(chunks[i]);
|
||||||
|
}
|
||||||
|
currentLine = chunks[chunks.length - 1];
|
||||||
|
}
|
||||||
|
} else if (metrics.width > maxWidth && !currentLine) {
|
||||||
|
// First word is too long, need to break it
|
||||||
|
const chunks = this.breakLongWord(ctx, word, maxWidth);
|
||||||
|
for (let i = 0; i < chunks.length - 1; i++) {
|
||||||
|
lines.push(chunks[i]);
|
||||||
|
}
|
||||||
|
currentLine = chunks[chunks.length - 1];
|
||||||
} else {
|
} else {
|
||||||
currentLine = testLine;
|
currentLine = testLine;
|
||||||
}
|
}
|
||||||
@@ -227,6 +250,33 @@ export class ImageProcessor {
|
|||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breaks a long word into chunks that fit within maxWidth
|
||||||
|
*/
|
||||||
|
private breakLongWord(ctx: any, word: string, maxWidth: number): string[] {
|
||||||
|
const chunks: string[] = [];
|
||||||
|
let currentChunk = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < word.length; i++) {
|
||||||
|
const char = word[i];
|
||||||
|
const testChunk = currentChunk + char;
|
||||||
|
const metrics = ctx.measureText(testChunk);
|
||||||
|
|
||||||
|
if (metrics.width > maxWidth && currentChunk) {
|
||||||
|
chunks.push(currentChunk);
|
||||||
|
currentChunk = char;
|
||||||
|
} else {
|
||||||
|
currentChunk = testChunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChunk) {
|
||||||
|
chunks.push(currentChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks.length > 0 ? chunks : [word];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const imageProcessor = new ImageProcessor();
|
export const imageProcessor = new ImageProcessor();
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import emojiRegex from 'emoji-regex';
|
||||||
|
|
||||||
|
// Cached regex instance for performance
|
||||||
|
const emojiPattern = emojiRegex();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes emojis from text while preserving other Unicode characters
|
||||||
|
* Uses the emoji-regex package which correctly identifies emojis without
|
||||||
|
* removing legitimate astral-plane characters (CJK Extension B, etc.)
|
||||||
|
*/
|
||||||
|
export function removeEmojis(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(emojiPattern, '')
|
||||||
|
.replace(/\s+/g, ' ') // Normalize whitespace
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user