From 42a5b7a796b759ffa00815d361f7d56128cd8663 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 11:14:46 -0300 Subject: [PATCH] feat: add functionality to embed uploaded images with BBCode or HTML (#296) --- apps/server/src/modules/file/controller.ts | 45 ++++++ apps/server/src/modules/file/routes.ts | 23 +++ apps/web/messages/ar-SA.json | 16 +- apps/web/messages/de-DE.json | 16 +- apps/web/messages/en-US.json | 16 +- apps/web/messages/es-ES.json | 16 +- apps/web/messages/fr-FR.json | 16 +- apps/web/messages/hi-IN.json | 16 +- apps/web/messages/it-IT.json | 16 +- apps/web/messages/ja-JP.json | 16 +- apps/web/messages/ko-KR.json | 16 +- apps/web/messages/nl-NL.json | 16 +- apps/web/messages/pl-PL.json | 16 +- apps/web/messages/pt-BR.json | 16 +- apps/web/messages/ru-RU.json | 16 +- apps/web/messages/tr-TR.json | 16 +- apps/web/messages/zh-CN.json | 16 +- apps/web/src/app/e/[id]/route.ts | 71 ++++++++ .../components/files/embed-code-display.tsx | 151 ++++++++++++++++++ .../src/components/files/media-embed-link.tsx | 72 +++++++++ .../components/modals/file-preview-modal.tsx | 17 ++ 21 files changed, 604 insertions(+), 15 deletions(-) create mode 100644 apps/web/src/app/e/[id]/route.ts create mode 100644 apps/web/src/components/files/embed-code-display.tsx create mode 100644 apps/web/src/components/files/media-embed-link.tsx diff --git a/apps/server/src/modules/file/controller.ts b/apps/server/src/modules/file/controller.ts index b3bad03..e4bccec 100644 --- a/apps/server/src/modules/file/controller.ts +++ b/apps/server/src/modules/file/controller.ts @@ -585,6 +585,51 @@ export class FileController { } } + async embedFile(request: FastifyRequest, reply: FastifyReply) { + try { + const { id } = request.params as { id: string }; + + if (!id) { + return reply.status(400).send({ error: "File ID is required." }); + } + + const fileRecord = await prisma.file.findUnique({ where: { id } }); + + if (!fileRecord) { + return reply.status(404).send({ error: "File not found." }); + } + + const extension = fileRecord.extension.toLowerCase(); + const imageExts = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico", "avif"]; + const videoExts = ["mp4", "webm", "ogg", "mov", "avi", "mkv", "flv", "wmv"]; + const audioExts = ["mp3", "wav", "ogg", "m4a", "flac", "aac", "wma"]; + + const isMedia = imageExts.includes(extension) || videoExts.includes(extension) || audioExts.includes(extension); + + if (!isMedia) { + return reply.status(403).send({ + error: "Embed is only allowed for images, videos, and audio files.", + }); + } + + const storageProvider = (this.fileService as any).storageProvider; + const filePath = storageProvider.getFilePath(fileRecord.objectName); + + const contentType = getContentType(fileRecord.name); + const fileName = fileRecord.name; + + reply.header("Content-Type", contentType); + reply.header("Content-Disposition", `inline; filename="${encodeURIComponent(fileName)}"`); + reply.header("Cache-Control", "public, max-age=31536000"); // Cache por 1 ano + + const stream = fs.createReadStream(filePath); + return reply.send(stream); + } catch (error) { + console.error("Error in embedFile:", error); + return reply.status(500).send({ error: "Internal server error." }); + } + } + private async getAllUserFilesRecursively(userId: string): Promise { const rootFiles = await prisma.file.findMany({ where: { userId, folderId: null }, diff --git a/apps/server/src/modules/file/routes.ts b/apps/server/src/modules/file/routes.ts index e8c67a5..e6a5eba 100644 --- a/apps/server/src/modules/file/routes.ts +++ b/apps/server/src/modules/file/routes.ts @@ -131,6 +131,29 @@ export async function fileRoutes(app: FastifyInstance) { fileController.getDownloadUrl.bind(fileController) ); + app.get( + "/embed/:id", + { + schema: { + tags: ["File"], + operationId: "embedFile", + summary: "Embed File (Public Access)", + description: + "Returns a media file (image/video/audio) for public embedding without authentication. Only works for media files.", + params: z.object({ + id: z.string().min(1, "File ID is required").describe("The file ID"), + }), + response: { + 400: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message - not a media file") }), + 404: z.object({ error: z.string().describe("Error message") }), + 500: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + fileController.embedFile.bind(fileController) + ); + app.get( "/files/download", { diff --git a/apps/web/messages/ar-SA.json b/apps/web/messages/ar-SA.json index ff1fdc0..0f7a10f 100644 --- a/apps/web/messages/ar-SA.json +++ b/apps/web/messages/ar-SA.json @@ -150,7 +150,9 @@ "move": "نقل", "rename": "إعادة تسمية", "search": "بحث", - "share": "مشاركة" + "share": "مشاركة", + "copied": "تم النسخ", + "copy": "نسخ" }, "createShare": { "title": "إنشاء مشاركة", @@ -1933,5 +1935,17 @@ "passwordRequired": "كلمة المرور مطلوبة", "nameRequired": "الاسم مطلوب", "required": "هذا الحقل مطلوب" + }, + "embedCode": { + "title": "تضمين الصورة", + "description": "استخدم هذه الأكواد لتضمين هذه الصورة في المنتديات أو المواقع الإلكترونية أو المنصات الأخرى", + "tabs": { + "directLink": "رابط مباشر", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "عنوان URL مباشر لملف الصورة", + "htmlDescription": "استخدم هذا الكود لتضمين الصورة في صفحات HTML", + "bbcodeDescription": "استخدم هذا الكود لتضمين الصورة في المنتديات التي تدعم BBCode" } } \ No newline at end of file diff --git a/apps/web/messages/de-DE.json b/apps/web/messages/de-DE.json index ce541b8..576a730 100644 --- a/apps/web/messages/de-DE.json +++ b/apps/web/messages/de-DE.json @@ -150,7 +150,9 @@ "move": "Verschieben", "rename": "Umbenennen", "search": "Suchen", - "share": "Teilen" + "share": "Teilen", + "copied": "Kopiert", + "copy": "Kopieren" }, "createShare": { "title": "Freigabe Erstellen", @@ -1931,5 +1933,17 @@ "passwordRequired": "Passwort ist erforderlich", "nameRequired": "Name ist erforderlich", "required": "Dieses Feld ist erforderlich" + }, + "embedCode": { + "title": "Bild einbetten", + "description": "Verwenden Sie diese Codes, um dieses Bild in Foren, Websites oder anderen Plattformen einzubetten", + "tabs": { + "directLink": "Direkter Link", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Direkte URL zur Bilddatei", + "htmlDescription": "Verwenden Sie diesen Code, um das Bild in HTML-Seiten einzubetten", + "bbcodeDescription": "Verwenden Sie diesen Code, um das Bild in Foren einzubetten, die BBCode unterstützen" } } \ No newline at end of file diff --git a/apps/web/messages/en-US.json b/apps/web/messages/en-US.json index 6a5c37d..087cf76 100644 --- a/apps/web/messages/en-US.json +++ b/apps/web/messages/en-US.json @@ -150,7 +150,9 @@ "rename": "Rename", "move": "Move", "share": "Share", - "search": "Search" + "search": "Search", + "copy": "Copy", + "copied": "Copied" }, "createShare": { "title": "Create Share", @@ -1882,6 +1884,18 @@ "userr": "User" } }, + "embedCode": { + "title": "Embed Image", + "description": "Use these codes to embed this image in forums, websites, or other platforms", + "tabs": { + "directLink": "Direct Link", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Direct URL to the image file", + "htmlDescription": "Use this code to embed the image in HTML pages", + "bbcodeDescription": "Use this code to embed the image in forums that support BBCode" + }, "validation": { "firstNameRequired": "First name is required", "lastNameRequired": "Last name is required", diff --git a/apps/web/messages/es-ES.json b/apps/web/messages/es-ES.json index 56c076e..5d01b3b 100644 --- a/apps/web/messages/es-ES.json +++ b/apps/web/messages/es-ES.json @@ -150,7 +150,9 @@ "move": "Mover", "rename": "Renombrar", "search": "Buscar", - "share": "Compartir" + "share": "Compartir", + "copied": "Copiado", + "copy": "Copiar" }, "createShare": { "title": "Crear Compartir", @@ -1931,5 +1933,17 @@ "passwordRequired": "Se requiere la contraseña", "nameRequired": "El nombre es obligatorio", "required": "Este campo es obligatorio" + }, + "embedCode": { + "title": "Insertar imagen", + "description": "Utiliza estos códigos para insertar esta imagen en foros, sitios web u otras plataformas", + "tabs": { + "directLink": "Enlace directo", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "URL directa al archivo de imagen", + "htmlDescription": "Utiliza este código para insertar la imagen en páginas HTML", + "bbcodeDescription": "Utiliza este código para insertar la imagen en foros que admiten BBCode" } } \ No newline at end of file diff --git a/apps/web/messages/fr-FR.json b/apps/web/messages/fr-FR.json index 3ed504b..1cb2277 100644 --- a/apps/web/messages/fr-FR.json +++ b/apps/web/messages/fr-FR.json @@ -150,7 +150,9 @@ "move": "Déplacer", "rename": "Renommer", "search": "Rechercher", - "share": "Partager" + "share": "Partager", + "copied": "Copié", + "copy": "Copier" }, "createShare": { "title": "Créer un Partage", @@ -1931,5 +1933,17 @@ "passwordRequired": "Le mot de passe est requis", "nameRequired": "Nome é obrigatório", "required": "Este campo é obrigatório" + }, + "embedCode": { + "title": "Intégrer l'image", + "description": "Utilisez ces codes pour intégrer cette image dans des forums, sites web ou autres plateformes", + "tabs": { + "directLink": "Lien direct", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "URL directe vers le fichier image", + "htmlDescription": "Utilisez ce code pour intégrer l'image dans des pages HTML", + "bbcodeDescription": "Utilisez ce code pour intégrer l'image dans des forums prenant en charge BBCode" } } \ No newline at end of file diff --git a/apps/web/messages/hi-IN.json b/apps/web/messages/hi-IN.json index 72cc2ba..fa16cc4 100644 --- a/apps/web/messages/hi-IN.json +++ b/apps/web/messages/hi-IN.json @@ -150,7 +150,9 @@ "move": "स्थानांतरित करें", "rename": "नाम बदलें", "search": "खोजें", - "share": "साझा करें" + "share": "साझा करें", + "copied": "कॉपी किया गया", + "copy": "कॉपी करें" }, "createShare": { "title": "साझाकरण बनाएं", @@ -1931,5 +1933,17 @@ "passwordRequired": "पासवर्ड आवश्यक है", "nameRequired": "नाम आवश्यक है", "required": "यह फ़ील्ड आवश्यक है" + }, + "embedCode": { + "title": "छवि एम्बेड करें", + "description": "इस छवि को मंचों, वेबसाइटों या अन्य प्लेटफार्मों में एम्बेड करने के लिए इन कोड का उपयोग करें", + "tabs": { + "directLink": "सीधा लिंक", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "छवि फ़ाइल का सीधा URL", + "htmlDescription": "HTML पेजों में छवि एम्बेड करने के लिए इस कोड का उपयोग करें", + "bbcodeDescription": "BBCode का समर्थन करने वाले मंचों में छवि एम्बेड करने के लिए इस कोड का उपयोग करें" } } \ No newline at end of file diff --git a/apps/web/messages/it-IT.json b/apps/web/messages/it-IT.json index e8d327e..7056fef 100644 --- a/apps/web/messages/it-IT.json +++ b/apps/web/messages/it-IT.json @@ -150,7 +150,9 @@ "move": "Sposta", "rename": "Rinomina", "search": "Cerca", - "share": "Condividi" + "share": "Condividi", + "copied": "Copiato", + "copy": "Copia" }, "createShare": { "title": "Crea Condivisione", @@ -1931,5 +1933,17 @@ "passwordMinLength": "La password deve contenere almeno 6 caratteri", "nameRequired": "Il nome è obbligatorio", "required": "Questo campo è obbligatorio" + }, + "embedCode": { + "title": "Incorpora immagine", + "description": "Usa questi codici per incorporare questa immagine in forum, siti web o altre piattaforme", + "tabs": { + "directLink": "Link diretto", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "URL diretto al file immagine", + "htmlDescription": "Usa questo codice per incorporare l'immagine nelle pagine HTML", + "bbcodeDescription": "Usa questo codice per incorporare l'immagine nei forum che supportano BBCode" } } \ No newline at end of file diff --git a/apps/web/messages/ja-JP.json b/apps/web/messages/ja-JP.json index 5973b06..096c425 100644 --- a/apps/web/messages/ja-JP.json +++ b/apps/web/messages/ja-JP.json @@ -150,7 +150,9 @@ "move": "移動", "rename": "名前を変更", "search": "検索", - "share": "共有" + "share": "共有", + "copied": "コピーしました", + "copy": "コピー" }, "createShare": { "title": "共有を作成", @@ -1931,5 +1933,17 @@ "passwordRequired": "パスワードは必須です", "nameRequired": "名前は必須です", "required": "このフィールドは必須です" + }, + "embedCode": { + "title": "画像を埋め込む", + "description": "これらのコードを使用して、この画像をフォーラム、ウェブサイト、またはその他のプラットフォームに埋め込みます", + "tabs": { + "directLink": "直接リンク", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "画像ファイルへの直接URL", + "htmlDescription": "このコードを使用してHTMLページに画像を埋め込みます", + "bbcodeDescription": "BBCodeをサポートするフォーラムに画像を埋め込むには、このコードを使用します" } } \ No newline at end of file diff --git a/apps/web/messages/ko-KR.json b/apps/web/messages/ko-KR.json index 0e377c9..a274947 100644 --- a/apps/web/messages/ko-KR.json +++ b/apps/web/messages/ko-KR.json @@ -150,7 +150,9 @@ "move": "이동", "rename": "이름 변경", "search": "검색", - "share": "공유" + "share": "공유", + "copied": "복사됨", + "copy": "복사" }, "createShare": { "title": "공유 생성", @@ -1931,5 +1933,17 @@ "passwordRequired": "비밀번호는 필수입니다", "nameRequired": "이름은 필수입니다", "required": "이 필드는 필수입니다" + }, + "embedCode": { + "title": "이미지 삽입", + "description": "이 코드를 사용하여 포럼, 웹사이트 또는 기타 플랫폼에 이 이미지를 삽입하세요", + "tabs": { + "directLink": "직접 링크", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "이미지 파일에 대한 직접 URL", + "htmlDescription": "이 코드를 사용하여 HTML 페이지에 이미지를 삽입하세요", + "bbcodeDescription": "BBCode를 지원하는 포럼에 이미지를 삽입하려면 이 코드를 사용하세요" } } \ No newline at end of file diff --git a/apps/web/messages/nl-NL.json b/apps/web/messages/nl-NL.json index 30f9102..5e2faab 100644 --- a/apps/web/messages/nl-NL.json +++ b/apps/web/messages/nl-NL.json @@ -150,7 +150,9 @@ "move": "Verplaatsen", "rename": "Hernoemen", "search": "Zoeken", - "share": "Delen" + "share": "Delen", + "copied": "Gekopieerd", + "copy": "Kopiëren" }, "createShare": { "title": "Delen Maken", @@ -1931,5 +1933,17 @@ "passwordMinLength": "Wachtwoord moet minimaal 6 tekens bevatten", "nameRequired": "Naam is verplicht", "required": "Dit veld is verplicht" + }, + "embedCode": { + "title": "Afbeelding insluiten", + "description": "Gebruik deze codes om deze afbeelding in te sluiten in forums, websites of andere platforms", + "tabs": { + "directLink": "Directe link", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Directe URL naar het afbeeldingsbestand", + "htmlDescription": "Gebruik deze code om de afbeelding in te sluiten in HTML-pagina's", + "bbcodeDescription": "Gebruik deze code om de afbeelding in te sluiten in forums die BBCode ondersteunen" } } \ No newline at end of file diff --git a/apps/web/messages/pl-PL.json b/apps/web/messages/pl-PL.json index 1d16b5f..5fc4c14 100644 --- a/apps/web/messages/pl-PL.json +++ b/apps/web/messages/pl-PL.json @@ -150,7 +150,9 @@ "move": "Przenieś", "rename": "Zmień nazwę", "search": "Szukaj", - "share": "Udostępnij" + "share": "Udostępnij", + "copied": "Skopiowano", + "copy": "Kopiuj" }, "createShare": { "title": "Utwórz Udostępnienie", @@ -1931,5 +1933,17 @@ "passwordMinLength": "Hasło musi mieć co najmniej 6 znaków", "nameRequired": "Nazwa jest wymagana", "required": "To pole jest wymagane" + }, + "embedCode": { + "title": "Osadź obraz", + "description": "Użyj tych kodów, aby osadzić ten obraz na forach, stronach internetowych lub innych platformach", + "tabs": { + "directLink": "Link bezpośredni", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Bezpośredni adres URL pliku obrazu", + "htmlDescription": "Użyj tego kodu, aby osadzić obraz na stronach HTML", + "bbcodeDescription": "Użyj tego kodu, aby osadzić obraz na forach obsługujących BBCode" } } \ No newline at end of file diff --git a/apps/web/messages/pt-BR.json b/apps/web/messages/pt-BR.json index eadacc4..732bb73 100644 --- a/apps/web/messages/pt-BR.json +++ b/apps/web/messages/pt-BR.json @@ -150,7 +150,9 @@ "move": "Mover", "rename": "Renomear", "search": "Pesquisar", - "share": "Compartilhar" + "share": "Compartilhar", + "copied": "Copiado", + "copy": "Copiar" }, "createShare": { "title": "Criar compartilhamento", @@ -1932,5 +1934,17 @@ "lastNameRequired": "O sobrenome é necessário", "usernameLength": "O nome de usuário deve ter pelo menos 3 caracteres", "usernameSpaces": "O nome de usuário não pode conter espaços" + }, + "embedCode": { + "title": "Incorporar imagem", + "description": "Use estes códigos para incorporar esta imagem em fóruns, sites ou outras plataformas", + "tabs": { + "directLink": "Link direto", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "URL direto para o arquivo de imagem", + "htmlDescription": "Use este código para incorporar a imagem em páginas HTML", + "bbcodeDescription": "Use este código para incorporar a imagem em fóruns que suportam BBCode" } } \ No newline at end of file diff --git a/apps/web/messages/ru-RU.json b/apps/web/messages/ru-RU.json index 307e14c..6e9503f 100644 --- a/apps/web/messages/ru-RU.json +++ b/apps/web/messages/ru-RU.json @@ -150,7 +150,9 @@ "move": "Переместить", "rename": "Переименовать", "search": "Поиск", - "share": "Поделиться" + "share": "Поделиться", + "copied": "Скопировано", + "copy": "Копировать" }, "createShare": { "title": "Создать общий доступ", @@ -1931,5 +1933,17 @@ "passwordRequired": "Требуется пароль", "nameRequired": "Требуется имя", "required": "Это поле обязательно" + }, + "embedCode": { + "title": "Встроить изображение", + "description": "Используйте эти коды для встраивания этого изображения на форумах, веб-сайтах или других платформах", + "tabs": { + "directLink": "Прямая ссылка", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Прямой URL-адрес файла изображения", + "htmlDescription": "Используйте этот код для встраивания изображения в HTML-страницы", + "bbcodeDescription": "Используйте этот код для встраивания изображения на форумах, поддерживающих BBCode" } } \ No newline at end of file diff --git a/apps/web/messages/tr-TR.json b/apps/web/messages/tr-TR.json index d8cff94..234f93b 100644 --- a/apps/web/messages/tr-TR.json +++ b/apps/web/messages/tr-TR.json @@ -150,7 +150,9 @@ "move": "Taşı", "rename": "Yeniden Adlandır", "search": "Ara", - "share": "Paylaş" + "share": "Paylaş", + "copied": "Kopyalandı", + "copy": "Kopyala" }, "createShare": { "title": "Paylaşım Oluştur", @@ -1931,5 +1933,17 @@ "passwordRequired": "Şifre gerekli", "nameRequired": "İsim gereklidir", "required": "Bu alan zorunludur" + }, + "embedCode": { + "title": "Resmi Yerleştir", + "description": "Bu görüntüyü forumlara, web sitelerine veya diğer platformlara yerleştirmek için bu kodları kullanın", + "tabs": { + "directLink": "Doğrudan Bağlantı", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "Resim dosyasının doğrudan URL'si", + "htmlDescription": "Resmi HTML sayfalarına yerleştirmek için bu kodu kullanın", + "bbcodeDescription": "BBCode destekleyen forumlara resmi yerleştirmek için bu kodu kullanın" } } \ No newline at end of file diff --git a/apps/web/messages/zh-CN.json b/apps/web/messages/zh-CN.json index 1e44794..1dc439d 100644 --- a/apps/web/messages/zh-CN.json +++ b/apps/web/messages/zh-CN.json @@ -150,7 +150,9 @@ "move": "移动", "rename": "重命名", "search": "搜索", - "share": "分享" + "share": "分享", + "copied": "已复制", + "copy": "复制" }, "createShare": { "title": "创建分享", @@ -1931,5 +1933,17 @@ "passwordRequired": "密码为必填项", "nameRequired": "名称为必填项", "required": "此字段为必填项" + }, + "embedCode": { + "title": "嵌入图片", + "description": "使用这些代码将此图片嵌入到论坛、网站或其他平台中", + "tabs": { + "directLink": "直接链接", + "html": "HTML", + "bbcode": "BBCode" + }, + "directLinkDescription": "图片文件的直接URL", + "htmlDescription": "使用此代码将图片嵌入HTML页面", + "bbcodeDescription": "使用此代码将图片嵌入支持BBCode的论坛" } } \ No newline at end of file diff --git a/apps/web/src/app/e/[id]/route.ts b/apps/web/src/app/e/[id]/route.ts new file mode 100644 index 0000000..ed21e3b --- /dev/null +++ b/apps/web/src/app/e/[id]/route.ts @@ -0,0 +1,71 @@ +import { NextRequest, NextResponse } from "next/server"; + +const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:3333"; + +/** + * Short public embed endpoint: /e/{id} + * No authentication required + * Only works for media files (images, videos, audio) + */ +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + + if (!id) { + return new NextResponse(JSON.stringify({ error: "File ID is required" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + const url = `${API_BASE_URL}/embed/${id}`; + + try { + const apiRes = await fetch(url, { + method: "GET", + redirect: "manual", + }); + + if (!apiRes.ok) { + const errorText = await apiRes.text(); + return new NextResponse(errorText, { + status: apiRes.status, + headers: { + "Content-Type": "application/json", + }, + }); + } + + const blob = await apiRes.blob(); + + const contentType = apiRes.headers.get("content-type") || "application/octet-stream"; + const contentDisposition = apiRes.headers.get("content-disposition"); + const cacheControl = apiRes.headers.get("cache-control"); + + const res = new NextResponse(blob, { + status: apiRes.status, + headers: { + "Content-Type": contentType, + }, + }); + + if (contentDisposition) { + res.headers.set("Content-Disposition", contentDisposition); + } + + if (cacheControl) { + res.headers.set("Cache-Control", cacheControl); + } else { + res.headers.set("Cache-Control", "public, max-age=31536000"); + } + + return res; + } catch (error) { + console.error("Error proxying embed request:", error); + return new NextResponse(JSON.stringify({ error: "Failed to fetch file" }), { + status: 500, + headers: { + "Content-Type": "application/json", + }, + }); + } +} diff --git a/apps/web/src/components/files/embed-code-display.tsx b/apps/web/src/components/files/embed-code-display.tsx new file mode 100644 index 0000000..794674f --- /dev/null +++ b/apps/web/src/components/files/embed-code-display.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { IconCheck, IconCopy } from "@tabler/icons-react"; +import { useTranslations } from "next-intl"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +interface EmbedCodeDisplayProps { + imageUrl: string; + fileName: string; + fileId: string; +} + +export function EmbedCodeDisplay({ imageUrl, fileName, fileId }: EmbedCodeDisplayProps) { + const t = useTranslations(); + const [copiedType, setCopiedType] = useState(null); + const [fullUrl, setFullUrl] = useState(""); + + useEffect(() => { + if (typeof window !== "undefined") { + const origin = window.location.origin; + const embedUrl = `${origin}/e/${fileId}`; + setFullUrl(embedUrl); + } + }, [fileId]); + + const directLink = fullUrl || imageUrl; + const htmlCode = `${fileName}`; + const bbCode = `[img]${directLink}[/img]`; + + const copyToClipboard = async (text: string, type: string) => { + try { + await navigator.clipboard.writeText(text); + setCopiedType(type); + setTimeout(() => setCopiedType(null), 2000); + } catch (error) { + console.error("Failed to copy:", error); + } + }; + + return ( + + +
+
+ +

{t("embedCode.description")}

+
+ + + + + {t("embedCode.tabs.directLink")} + + + {t("embedCode.tabs.html")} + + + {t("embedCode.tabs.bbcode")} + + + + +
+ + +
+

{t("embedCode.directLinkDescription")}

+
+ + +
+ + +
+

{t("embedCode.htmlDescription")}

+
+ + +
+ + +
+

{t("embedCode.bbcodeDescription")}

+
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/files/media-embed-link.tsx b/apps/web/src/components/files/media-embed-link.tsx new file mode 100644 index 0000000..d95c476 --- /dev/null +++ b/apps/web/src/components/files/media-embed-link.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { IconCheck, IconCopy } from "@tabler/icons-react"; +import { useTranslations } from "next-intl"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; + +interface MediaEmbedLinkProps { + fileId: string; +} + +export function MediaEmbedLink({ fileId }: MediaEmbedLinkProps) { + const t = useTranslations(); + const [copied, setCopied] = useState(false); + const [embedUrl, setEmbedUrl] = useState(""); + + useEffect(() => { + if (typeof window !== "undefined") { + const origin = window.location.origin; + const url = `${origin}/e/${fileId}`; + setEmbedUrl(url); + } + }, [fileId]); + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(embedUrl); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + console.error("Failed to copy:", error); + } + }; + + return ( + + +
+
+ +

{t("embedCode.directLinkDescription")}

+
+ +
+ + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/modals/file-preview-modal.tsx b/apps/web/src/components/modals/file-preview-modal.tsx index b00e6f6..72bfdad 100644 --- a/apps/web/src/components/modals/file-preview-modal.tsx +++ b/apps/web/src/components/modals/file-preview-modal.tsx @@ -3,6 +3,8 @@ import { IconDownload } from "@tabler/icons-react"; import { useTranslations } from "next-intl"; +import { EmbedCodeDisplay } from "@/components/files/embed-code-display"; +import { MediaEmbedLink } from "@/components/files/media-embed-link"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -14,6 +16,7 @@ import { } from "@/components/ui/dialog"; import { useFilePreview } from "@/hooks/use-file-preview"; import { getFileIcon } from "@/utils/file-icons"; +import { getFileType } from "@/utils/file-types"; import { FilePreviewRenderer } from "./previews"; interface FilePreviewModalProps { @@ -39,6 +42,10 @@ export function FilePreviewModal({ }: FilePreviewModalProps) { const t = useTranslations(); const previewState = useFilePreview({ file, isOpen, isReverseShare, sharePassword }); + const fileType = getFileType(file.name); + const isImage = fileType === "image"; + const isVideo = fileType === "video"; + const isAudio = fileType === "audio"; return ( @@ -67,6 +74,16 @@ export function FilePreviewModal({ description={file.description} onDownload={previewState.handleDownload} /> + {isImage && previewState.previewUrl && !previewState.isLoading && file.id && ( +
+ +
+ )} + {(isVideo || isAudio) && !previewState.isLoading && file.id && ( +
+ +
+ )}