Files
flatrender/src/components/sections/SecondsCalculator.tsx
T
soroush.asadi 4f04f6bf75
CI/CD / CI · Web (tsc) (push) Successful in 1m21s
CI/CD / Deploy · full stack (push) Failing after 20s
feat(render+templates): Remotion engine, 16 branded templates (incl. 3D), seconds pricing, coming-soon
Render engine
- Add Remotion (code-based) as a 2nd render engine alongside After Effects.
  node-agent dispatches on Job.Engine; RunRemotion maps bindings -> --props,
  renders native then ffmpeg-scales to the quality tier (aspect-preserving).
- content.projects.render_engine + render_remotion_comp (migration 32);
  render-svc claim resolves engine and routes (skips .aep for Remotion).
- Admin TemplatesAdmin gains an engine picker + Remotion composition id field.

Template pack (services/remotion)
- 16 branded, Persian (Vazirmatn), color- and text-editable templates, each in
  3 aspects (16:9 / 1:1 / 9:16): LogoMotion, Opener, InstaPromo, YouTubeIntro,
  Slideshow, HappyBirthday, SalePromo, QuoteCard, EventInvite, Countdown,
  GlitterReveal (editable logo image), NowruzGreeting (animated characters),
  and 4 cinematic 3D templates via @remotion/three (Hero3D, Nowruz3D,
  Birthday3D, Promo3D) with reflections + bloom/DOF/vignette.
- scripts/seed_remotion_templates.py seeds containers/projects/scenes/colors.

Pricing
- Rewrite /pricing to the seconds-based model (charge = length x resolution),
  data-driven from /v1/plans, Toman, broker checkout.

Coming-soon
- Persian experimental-build overlay on all pages (launch date + countdown).

Fixes
- middleware matcher bypasses all static asset paths; catalog mapping passes
  cover image + preview video so real thumbnails render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:52:52 +03:30

121 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import {
RESOLUTION_ORDER,
renderSecondsCost,
type SecondsPlan,
} from "@/lib/plans-catalog";
interface Props {
plans: SecondsPlan[];
}
/**
* Interactive "how many seconds do I need" helper. The user picks a video length
* and resolution; we show the per-render cost (length × resolution multiplier)
* and how many such videos each paid plan's monthly seconds would cover.
*/
export function SecondsCalculator({ plans }: Props) {
const t = useTranslations("pricing");
const [length, setLength] = useState(15);
const [resolution, setResolution] = useState("720p");
const cost = useMemo(
() => renderSecondsCost(length, resolution),
[length, resolution]
);
const paidPlans = plans.filter((p) => p.priceTomans > 0);
return (
<div className="rounded-2xl border border-neutral-200 bg-white p-6 shadow-sm sm:p-8">
<h3 className="font-heading text-xl font-bold text-neutral-900">
{t("calcTitle")}
</h3>
<p className="mt-1 text-sm text-neutral-500">{t("calcDesc")}</p>
<div className="mt-6 grid gap-6 sm:grid-cols-2">
{/* Length */}
<div>
<label className="mb-2 flex items-center justify-between text-sm font-medium text-neutral-700">
<span>{t("calcLength")}</span>
<span className="font-bold text-neutral-900">
{length} {t("calcSecondsUnit")}
</span>
</label>
<input
type="range"
min={1}
max={120}
value={length}
onChange={(e) => setLength(Number(e.target.value))}
className="w-full accent-indigo-600"
/>
</div>
{/* Resolution */}
<div>
<label className="mb-2 block text-sm font-medium text-neutral-700">
{t("calcResolution")}
</label>
<div className="flex flex-wrap gap-1.5">
{RESOLUTION_ORDER.map((r) => (
<button
key={r}
type="button"
onClick={() => setResolution(r)}
className={`rounded-lg border px-2.5 py-1.5 text-xs font-medium transition ${
resolution === r
? "border-indigo-600 bg-indigo-600 text-white"
: "border-neutral-200 bg-white text-neutral-600 hover:border-neutral-300"
}`}
>
{r}
</button>
))}
</div>
</div>
</div>
{/* Cost */}
<div className="mt-6 flex flex-wrap items-end justify-between gap-4 rounded-xl bg-neutral-50 p-5">
<div>
<p className="text-sm text-neutral-500">{t("calcCost")}</p>
<p className="mt-1 text-3xl font-extrabold text-neutral-900">
{cost}{" "}
<span className="text-base font-medium text-neutral-500">
{t("calcSecondsUnit")}
</span>
</p>
</div>
{paidPlans.length > 0 && (
<div className="text-end">
<p className="mb-1 text-sm text-neutral-500">
{t("calcRendersWith")}
</p>
<div className="flex flex-wrap justify-end gap-2">
{paidPlans.map((p) => (
<span
key={p.id}
className="rounded-lg border border-neutral-200 bg-white px-2.5 py-1 text-xs text-neutral-700"
>
<span className="font-semibold text-neutral-900">
{p.name}
</span>
{": "}
{t("calcVideosFmt", {
count: Math.floor(p.secondsCharge / Math.max(cost, 1)),
})}
</span>
))}
</div>
</div>
)}
</div>
</div>
);
}