From 38229185a73b3abc30d8ba4994540b363bc69e16 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 25 Jun 2026 09:37:34 +0330 Subject: [PATCH] feat(remotion): IG promo posts accept images AND video MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - igkit: a Media component that detects video by extension and renders a frame via OffthreadVideo (muted), else an Img — so any post slot takes images or reels. - IGProfile: the profile-page grid is now editable — 6 post media fields (was static colour placeholders); videos get a ▶ reel badge. - IGFeed: post slots now accept video too; labels say «عکس/ویدیو». Verified: a profile still with an image cell + a video cell + avatar image renders both correctly. Co-Authored-By: Claude Opus 4.8 --- .../remotion/src/scenes/blocks/IGFeed.tsx | 20 +++++++--------- .../remotion/src/scenes/blocks/IGProfile.tsx | 24 +++++++++++++------ services/remotion/src/scenes/blocks/igkit.tsx | 10 ++++++++ 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/services/remotion/src/scenes/blocks/IGFeed.tsx b/services/remotion/src/scenes/blocks/IGFeed.tsx index 3f4677b..44c7897 100644 --- a/services/remotion/src/scenes/blocks/IGFeed.tsx +++ b/services/remotion/src/scenes/blocks/IGFeed.tsx @@ -1,13 +1,11 @@ import React from "react"; -import { AbsoluteFill, Img, interpolate, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; import { FONT } from "../../lib/fonts"; import { hexToRgba } from "../../lib/anim"; -import { IgGlows } from "./igkit"; +import { IgGlows, Media, isMedia } from "./igkit"; import type { BlockProps, SceneBlock } from "../types"; const PH = ["#ff5a3c", "#7c5cff", "#16b5a0", "#ffb23c", "#ef5da8", "#3aa0ff"]; -const resolve = (u: string) => (/^https?:\/\//.test(u) ? u : staticFile(u)); -const isImg = (u: string) => !!u && (/^https?:\/\//.test(u) || u.includes("/")); const IGFeed: React.FC = ({ data, colors, L, durationInFrames }) => { const frame = useCurrentFrame(); @@ -30,7 +28,7 @@ const IGFeed: React.FC = ({ data, colors, L, durationInFrames }) => const pop = spring({ frame: frame - 6 - i * 4, fps, config: { damping: 14, stiffness: 130 } }); return (
- {isImg(p) ? :
} + {isMedia(p) ? :
}
); })} @@ -46,12 +44,12 @@ export const IGFeedBlock: SceneBlock = { component: IGFeed, fields: [ { key: "caption", label: "عنوان بخش", type: "text", default: "محتوای ما را ببینید" }, - { key: "post1", label: "پست ۱", type: "image", default: "" }, - { key: "post2", label: "پست ۲", type: "image", default: "" }, - { key: "post3", label: "پست ۳", type: "image", default: "" }, - { key: "post4", label: "پست ۴", type: "image", default: "" }, - { key: "post5", label: "پست ۵", type: "image", default: "" }, - { key: "post6", label: "پست ۶", type: "image", default: "" }, + { key: "post1", label: "پست ۱ (عکس/ویدیو)", type: "image", default: "" }, + { key: "post2", label: "پست ۲ (عکس/ویدیو)", type: "image", default: "" }, + { key: "post3", label: "پست ۳ (عکس/ویدیو)", type: "image", default: "" }, + { key: "post4", label: "پست ۴ (عکس/ویدیو)", type: "image", default: "" }, + { key: "post5", label: "پست ۵ (عکس/ویدیو)", type: "image", default: "" }, + { key: "post6", label: "پست ۶ (عکس/ویدیو)", type: "image", default: "" }, ], defaultDurationSec: 4, minDurationSec: 2, diff --git a/services/remotion/src/scenes/blocks/IGProfile.tsx b/services/remotion/src/scenes/blocks/IGProfile.tsx index 2936dd8..6616253 100644 --- a/services/remotion/src/scenes/blocks/IGProfile.tsx +++ b/services/remotion/src/scenes/blocks/IGProfile.tsx @@ -2,7 +2,7 @@ import React from "react"; import { AbsoluteFill, Img, interpolate, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion"; import { FONT } from "../../lib/fonts"; import { hexToRgba } from "../../lib/anim"; -import { IgGlows, IgWordmark, IG_GRAD, IG_BLUE } from "./igkit"; +import { IgGlows, IgWordmark, IG_GRAD, IG_BLUE, Media, isMedia, isVideo } from "./igkit"; import type { BlockProps, SceneBlock } from "../types"; const PH = ["#ff5a3c", "#7c5cff", "#16b5a0", "#ffb23c", "#ef5da8", "#3aa0ff", "#ff7a59", "#4cd4b0", "#a06bff"]; @@ -22,6 +22,7 @@ const IGProfile: React.FC = ({ data, colors, L, durationInFrames }) const u = screenW / 770; const px = (n: number) => n * u; const cell = (screenW - px(12)) / 3; + const posts = [data.post1, data.post2, data.post3, data.post4, data.post5, data.post6]; const rise = spring({ frame, fps, config: { damping: 18, stiffness: 90 } }); const phoneY = interpolate(rise, [0, 1], [height * 0.06, 0]); @@ -97,12 +98,15 @@ const IGProfile: React.FC = ({ data, colors, L, durationInFrames })
{/* posts grid */}
- {PH.map((c, i) => ( -
-
- {i % 4 === 0 ? : null} -
- ))} + {PH.map((c, i) => { + const p = posts[i]; + return ( +
+ {isMedia(p) ? :
} + {isVideo(p) || (!isMedia(p) && i % 4 === 0) ? : null} +
+ ); + })}
@@ -134,6 +138,12 @@ export const IGProfileBlock: SceneBlock = { { key: "followLabel", label: "متن دکمهٔ دنبال", type: "text", default: "دنبال کردن" }, { key: "messageLabel", label: "متن دکمهٔ پیام", type: "text", default: "پیام" }, { key: "avatar", label: "تصویر پروفایل", type: "image", default: "" }, + { key: "post1", label: "پست شبکه ۱ (عکس/ویدیو)", type: "image", default: "" }, + { key: "post2", label: "پست شبکه ۲ (عکس/ویدیو)", type: "image", default: "" }, + { key: "post3", label: "پست شبکه ۳ (عکس/ویدیو)", type: "image", default: "" }, + { key: "post4", label: "پست شبکه ۴ (عکس/ویدیو)", type: "image", default: "" }, + { key: "post5", label: "پست شبکه ۵ (عکس/ویدیو)", type: "image", default: "" }, + { key: "post6", label: "پست شبکه ۶ (عکس/ویدیو)", type: "image", default: "" }, ], defaultDurationSec: 5, minDurationSec: 3, diff --git a/services/remotion/src/scenes/blocks/igkit.tsx b/services/remotion/src/scenes/blocks/igkit.tsx index c4bf497..54f665c 100644 --- a/services/remotion/src/scenes/blocks/igkit.tsx +++ b/services/remotion/src/scenes/blocks/igkit.tsx @@ -1,7 +1,17 @@ import React from "react"; +import { Img, OffthreadVideo, staticFile } from "remotion"; import type { Layout } from "../../lib/aspect"; import { hexToRgba } from "../../lib/anim"; +/** A post slot that accepts both images AND videos (IG reels). Detects video by + * extension and renders a frame via OffthreadVideo; otherwise an Img. */ +const VIDEO_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i; +export const isMedia = (u: string) => !!u && (/^https?:\/\//.test(u) || u.includes("/")); +export const isVideo = (u: string) => VIDEO_RE.test(u); +export const mediaSrc = (u: string) => (/^https?:\/\//.test(u) ? u : staticFile(u)); +export const Media: React.FC<{ src: string; style?: React.CSSProperties }> = ({ src, style }) => + isVideo(src) ? : ; + /** Shared Instagram primitives used across the IG-promo scene blocks. */ export const IG_GRAD = "linear-gradient(45deg,#f09433,#e6683c,#dc2743,#cc2366,#bc1888)"; export const IG_BLUE = "#0095f6";