36e264f3e3
- Wire admin API into homepage + templates page (ISR 60s, null fallback) - Add src/lib/admin-api.ts with safeFetch helper - Add adminProjectToTemplateItem + adminProjectToCatalogTemplate mappers - Add LogoMark SVG component, replace Sparkles icon in Navbar/Footer/Sidebar - Add public/favicon.svg (SVG brand mark) - Rewrite opengraph-image.tsx with FlatRender branding - Add RTL/Persian font cascade: unlayered [dir=rtl] block forces Vazirmatn - Dashboard Settings page: Profile, Security, Billing, Notifications sections - Add src/lib/supabase/client.ts browser client - Admin API: GET /me, PATCH /profile, POST /change-password endpoints - Admin API DTOs: AdminUserDto, UpdateProfileRequest, ChangePasswordRequest - Admin UI Settings page with TanStack Query + mutations - Add CLAUDE.md + README.md to both repos for new-machine onboarding - Update PROJECT_MEMORY.md with session log - Add appsettings.Development.json.example template
510 lines
13 KiB
TypeScript
510 lines
13 KiB
TypeScript
export type VideoSidebarCategoryId =
|
|
| "all"
|
|
| "animation"
|
|
| "intros"
|
|
| "editing"
|
|
| "invitation"
|
|
| "holiday"
|
|
| "slideshow"
|
|
| "presentations"
|
|
| "social"
|
|
| "ads"
|
|
| "sales"
|
|
| "music";
|
|
|
|
export type AspectRatioFilter =
|
|
| "all"
|
|
| "widescreen"
|
|
| "portrait"
|
|
| "square"
|
|
| "fourFive";
|
|
|
|
export type DurationFilter = "all" | "flexible" | "fixed";
|
|
|
|
export type RefineType = "templates" | "packs";
|
|
|
|
export interface VideoSidebarCategory {
|
|
id: VideoSidebarCategoryId;
|
|
label: string;
|
|
}
|
|
|
|
export const VIDEO_SIDEBAR_CATEGORIES: VideoSidebarCategory[] = [
|
|
{ id: "all", label: "All Templates" },
|
|
{ id: "animation", label: "Animation Videos" },
|
|
{ id: "intros", label: "Intros and Logos" },
|
|
{ id: "editing", label: "Video Editing" },
|
|
{ id: "invitation", label: "Invitation Videos" },
|
|
{ id: "holiday", label: "Holiday Videos" },
|
|
{ id: "slideshow", label: "Slideshow" },
|
|
{ id: "presentations", label: "Presentations" },
|
|
{ id: "social", label: "Social Media Videos" },
|
|
{ id: "ads", label: "Video Ad Templates" },
|
|
{ id: "sales", label: "Sales Videos" },
|
|
{ id: "music", label: "Music Visualization" },
|
|
];
|
|
|
|
export const ASPECT_RATIO_OPTIONS: {
|
|
id: AspectRatioFilter;
|
|
label: string;
|
|
}[] = [
|
|
{ id: "all", label: "All Sizes" },
|
|
{ id: "widescreen", label: "16:9" },
|
|
{ id: "portrait", label: "9:16" },
|
|
{ id: "square", label: "1:1" },
|
|
{ id: "fourFive", label: "4:5" },
|
|
];
|
|
|
|
export type TemplateDetailAspectRatio = "16:9" | "9:16";
|
|
|
|
export const TEMPLATE_STYLE_COUNT = 4;
|
|
|
|
export interface VideoCatalogTemplate {
|
|
id: string;
|
|
name: string;
|
|
videoCategory: Exclude<VideoSidebarCategoryId, "all">;
|
|
aspectRatio: Exclude<AspectRatioFilter, "all">;
|
|
aspectRatios?: readonly TemplateDetailAspectRatio[];
|
|
durationType: "flexible" | "fixed";
|
|
premium: boolean;
|
|
sceneCount: number;
|
|
supports4k: boolean;
|
|
colorChange: boolean;
|
|
scriptToVideo: boolean;
|
|
description?: string;
|
|
isNew?: boolean;
|
|
}
|
|
|
|
export function getVideoTemplateCategoryLabel(
|
|
category: Exclude<VideoSidebarCategoryId, "all">
|
|
): string {
|
|
const match = VIDEO_SIDEBAR_CATEGORIES.find((item) => item.id === category);
|
|
return match?.label ?? category;
|
|
}
|
|
|
|
export function getTemplateDetailAspectRatios(
|
|
template: VideoCatalogTemplate
|
|
): TemplateDetailAspectRatio[] {
|
|
if (template.aspectRatios && template.aspectRatios.length > 0) {
|
|
return [...template.aspectRatios];
|
|
}
|
|
return ["16:9", "9:16"];
|
|
}
|
|
|
|
export function getVideoTemplateStyleImageSrc(
|
|
templateId: string,
|
|
styleIndex: number
|
|
): string {
|
|
return `https://picsum.photos/seed/${templateId}-style${styleIndex}/240/135`;
|
|
}
|
|
|
|
export function getVideoTemplateExampleImageSrc(
|
|
templateId: string,
|
|
exampleIndex: number
|
|
): string {
|
|
return `https://picsum.photos/seed/${templateId}-example${exampleIndex}/520/325`;
|
|
}
|
|
|
|
const templatesByCategory: Record<
|
|
Exclude<VideoSidebarCategoryId, "all">,
|
|
string[]
|
|
> = {
|
|
animation: [
|
|
"Whiteboard Animation Toolkit",
|
|
"3D Explainer Video Toolkit",
|
|
"Trendy Explainer Toolkit",
|
|
"Factory of 3D Animations",
|
|
"Anime Stories Pack",
|
|
"Healthcare Explainer Toolkit",
|
|
],
|
|
intros: [
|
|
"Abstract Distortion Intro",
|
|
"Glossy Bubbles Intro",
|
|
"Neon Soundwaves Visualizer",
|
|
"Minimal Logo Reveal",
|
|
"Glitch Intro Pack",
|
|
],
|
|
editing: [
|
|
"Cinematic Color Grade",
|
|
"Quick Cut Montage",
|
|
"Documentary Style Opener",
|
|
],
|
|
invitation: [
|
|
"Wedding Invitation Slideshow",
|
|
"Birthday Party Invite",
|
|
"Corporate Event Opening",
|
|
],
|
|
holiday: [
|
|
"Christmas Greeting Card",
|
|
"New Year Countdown",
|
|
"Seasonal Sale Promo",
|
|
],
|
|
slideshow: [
|
|
"Polaroid Frames Slideshow",
|
|
"Flipping Slideshow",
|
|
"Fragmented Transitions Slideshow",
|
|
"Parallax Circles",
|
|
"Bokeh Effects Slideshow",
|
|
],
|
|
presentations: [
|
|
"Business Presentation Pack",
|
|
"Startup Pitch Deck",
|
|
"Quarterly Report Intro",
|
|
],
|
|
social: [
|
|
"Instagram Carousel",
|
|
"TikTok Hook Pack",
|
|
"Story Highlight Reel",
|
|
"LinkedIn Promo",
|
|
],
|
|
ads: [
|
|
"Product Launch Ad",
|
|
"App Promo Vertical",
|
|
"Flash Sale Countdown",
|
|
],
|
|
sales: [
|
|
"SaaS Explainer",
|
|
"Real Estate Walkthrough",
|
|
"Restaurant Promo",
|
|
],
|
|
music: [
|
|
"Audio Spectrum Visualizer",
|
|
"Vinyl Record Spin",
|
|
"Beat Sync Typography",
|
|
],
|
|
};
|
|
|
|
const aspectRatios: Exclude<AspectRatioFilter, "all">[] = [
|
|
"widescreen",
|
|
"portrait",
|
|
"square",
|
|
"fourFive",
|
|
];
|
|
|
|
function buildVideoCatalog(): VideoCatalogTemplate[] {
|
|
const items: VideoCatalogTemplate[] = [];
|
|
let index = 0;
|
|
|
|
for (const [category, names] of Object.entries(templatesByCategory)) {
|
|
const videoCategory = category as Exclude<VideoSidebarCategoryId, "all">;
|
|
for (const baseName of names) {
|
|
for (let variant = 0; variant < 2; variant += 1) {
|
|
const name = variant > 0 ? `${baseName} ${variant + 1}` : baseName;
|
|
const detailAspectRatios: TemplateDetailAspectRatio[] =
|
|
index % 3 === 0 ? ["16:9"] : ["16:9", "9:16"];
|
|
items.push({
|
|
id: `vtpl-${category}-${index}`,
|
|
name,
|
|
videoCategory,
|
|
aspectRatio: aspectRatios[index % aspectRatios.length],
|
|
aspectRatios: detailAspectRatios,
|
|
durationType: index % 3 === 0 ? "fixed" : "flexible",
|
|
premium: index % 4 === 0,
|
|
sceneCount: 5 + (index % 12) * 10,
|
|
supports4k: index % 2 === 0,
|
|
colorChange: index % 3 !== 0,
|
|
scriptToVideo: index % 5 === 0,
|
|
isNew: index < 8,
|
|
});
|
|
index += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
/** Featured presets on /studio/video/new — ids match TEMPLATE_GALLERY_ITEMS */
|
|
const ONBOARDING_PRESET_TEMPLATES: VideoCatalogTemplate[] = [
|
|
{
|
|
id: "promo-reel",
|
|
name: "Animated Inspirational Video",
|
|
videoCategory: "animation",
|
|
aspectRatio: "widescreen",
|
|
aspectRatios: ["16:9", "9:16"],
|
|
durationType: "flexible",
|
|
premium: false,
|
|
sceneCount: 12,
|
|
supports4k: true,
|
|
colorChange: true,
|
|
scriptToVideo: false,
|
|
isNew: true,
|
|
},
|
|
{
|
|
id: "product-launch",
|
|
name: "Cybersecurity Company Promo",
|
|
videoCategory: "ads",
|
|
aspectRatio: "widescreen",
|
|
aspectRatios: ["16:9", "9:16"],
|
|
durationType: "flexible",
|
|
premium: true,
|
|
sceneCount: 8,
|
|
supports4k: true,
|
|
colorChange: true,
|
|
scriptToVideo: false,
|
|
},
|
|
{
|
|
id: "brand-story",
|
|
name: "Get to Know Your Customers Day",
|
|
videoCategory: "social",
|
|
aspectRatio: "widescreen",
|
|
aspectRatios: ["16:9", "9:16"],
|
|
durationType: "flexible",
|
|
premium: false,
|
|
sceneCount: 10,
|
|
supports4k: false,
|
|
colorChange: true,
|
|
scriptToVideo: true,
|
|
},
|
|
{
|
|
id: "instagram-carousel",
|
|
name: "SEO Agency Introduction",
|
|
videoCategory: "social",
|
|
aspectRatio: "square",
|
|
aspectRatios: ["16:9", "9:16"],
|
|
durationType: "flexible",
|
|
premium: false,
|
|
sceneCount: 6,
|
|
supports4k: false,
|
|
colorChange: true,
|
|
scriptToVideo: false,
|
|
},
|
|
{
|
|
id: "tiktok-hook",
|
|
name: "Tech Startup Promo",
|
|
videoCategory: "social",
|
|
aspectRatio: "portrait",
|
|
aspectRatios: ["9:16"],
|
|
durationType: "flexible",
|
|
premium: false,
|
|
sceneCount: 5,
|
|
supports4k: false,
|
|
colorChange: true,
|
|
scriptToVideo: false,
|
|
isNew: true,
|
|
},
|
|
{
|
|
id: "pitch-deck",
|
|
name: "Corporate Explainer",
|
|
videoCategory: "presentations",
|
|
aspectRatio: "widescreen",
|
|
aspectRatios: ["16:9"],
|
|
durationType: "fixed",
|
|
premium: false,
|
|
sceneCount: 15,
|
|
supports4k: true,
|
|
colorChange: true,
|
|
scriptToVideo: false,
|
|
},
|
|
{
|
|
id: "hero-promo",
|
|
name: "Hero Product Launch",
|
|
videoCategory: "ads",
|
|
aspectRatio: "widescreen",
|
|
aspectRatios: ["16:9", "9:16"],
|
|
durationType: "flexible",
|
|
premium: true,
|
|
sceneCount: 9,
|
|
supports4k: true,
|
|
colorChange: true,
|
|
scriptToVideo: false,
|
|
},
|
|
{
|
|
id: "event-recap",
|
|
name: "Event Recap Highlight",
|
|
videoCategory: "slideshow",
|
|
aspectRatio: "widescreen",
|
|
aspectRatios: ["16:9", "9:16"],
|
|
durationType: "flexible",
|
|
premium: false,
|
|
sceneCount: 11,
|
|
supports4k: true,
|
|
colorChange: true,
|
|
scriptToVideo: false,
|
|
},
|
|
];
|
|
|
|
export const VIDEO_TEMPLATES_CATALOG = [
|
|
...ONBOARDING_PRESET_TEMPLATES,
|
|
...buildVideoCatalog(),
|
|
];
|
|
|
|
export interface VideoTemplateFilters {
|
|
search: string;
|
|
sidebarCategory: VideoSidebarCategoryId;
|
|
aspectRatio: AspectRatioFilter;
|
|
duration: DurationFilter;
|
|
premiumOnly: boolean;
|
|
supports4k: boolean;
|
|
colorChange: boolean;
|
|
scriptToVideo: boolean;
|
|
}
|
|
|
|
export function filterVideoCatalog(
|
|
templates: VideoCatalogTemplate[],
|
|
filters: VideoTemplateFilters
|
|
): VideoCatalogTemplate[] {
|
|
const query = filters.search.trim().toLowerCase();
|
|
|
|
return templates.filter((template) => {
|
|
if (
|
|
filters.sidebarCategory !== "all" &&
|
|
template.videoCategory !== filters.sidebarCategory
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
filters.aspectRatio !== "all" &&
|
|
template.aspectRatio !== filters.aspectRatio
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
filters.duration !== "all" &&
|
|
template.durationType !== filters.duration
|
|
) {
|
|
return false;
|
|
}
|
|
if (filters.premiumOnly && !template.premium) return false;
|
|
if (filters.supports4k && !template.supports4k) return false;
|
|
if (filters.colorChange && !template.colorChange) return false;
|
|
if (filters.scriptToVideo && !template.scriptToVideo) return false;
|
|
if (query && !template.name.toLowerCase().includes(query)) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
export function getVideoTemplateImageSrc(id: string): string {
|
|
return `https://picsum.photos/seed/${id}/640/360`;
|
|
}
|
|
|
|
export interface VideoTemplateSection {
|
|
id: string;
|
|
title: string;
|
|
count: number;
|
|
templates: VideoCatalogTemplate[];
|
|
}
|
|
|
|
export function buildVideoTemplateSections(
|
|
filtered: VideoCatalogTemplate[],
|
|
sidebarCategory: VideoSidebarCategoryId
|
|
): VideoTemplateSection[] {
|
|
const newlyReleased = filtered.filter((t) => t.isNew).slice(0, 8);
|
|
const sections: VideoTemplateSection[] = [];
|
|
|
|
if (newlyReleased.length > 0 && sidebarCategory === "all") {
|
|
sections.push({
|
|
id: "newly-released",
|
|
title: "Newly released",
|
|
count: newlyReleased.length,
|
|
templates: newlyReleased,
|
|
});
|
|
}
|
|
|
|
const categories =
|
|
sidebarCategory === "all"
|
|
? VIDEO_SIDEBAR_CATEGORIES.filter((c) => c.id !== "all")
|
|
: VIDEO_SIDEBAR_CATEGORIES.filter((c) => c.id === sidebarCategory);
|
|
|
|
for (const category of categories) {
|
|
const templates = filtered
|
|
.filter((t) => t.videoCategory === category.id)
|
|
.slice(0, 12);
|
|
if (templates.length === 0) continue;
|
|
sections.push({
|
|
id: category.id,
|
|
title: category.label,
|
|
count: filtered.filter((t) => t.videoCategory === category.id).length,
|
|
templates,
|
|
});
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
|
|
export function toProjectTemplate(
|
|
template: VideoCatalogTemplate
|
|
): { id: string; name: string; category: "Video" } {
|
|
return {
|
|
id: template.id,
|
|
name: template.name,
|
|
category: "Video",
|
|
};
|
|
}
|
|
|
|
// ── Admin API → catalog helpers ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* Map an admin category name (or slug) to the closest hardcoded
|
|
* VideoSidebarCategoryId. Falls back to "social" when nothing matches.
|
|
*/
|
|
export function adminCategoryNameToSidebarId(
|
|
categoryName?: string
|
|
): Exclude<VideoSidebarCategoryId, "all"> {
|
|
if (!categoryName) return "social";
|
|
const n = categoryName.toLowerCase();
|
|
if (n.includes("animat")) return "animation";
|
|
if (n.includes("intro") || n.includes("logo")) return "intros";
|
|
if (n.includes("edit")) return "editing";
|
|
if (n.includes("invit")) return "invitation";
|
|
if (
|
|
n.includes("holiday") ||
|
|
n.includes("christmas") ||
|
|
n.includes("new year")
|
|
)
|
|
return "holiday";
|
|
if (n.includes("slide")) return "slideshow";
|
|
if (
|
|
n.includes("present") ||
|
|
n.includes("pitch") ||
|
|
n.includes("deck")
|
|
)
|
|
return "presentations";
|
|
if (
|
|
n.includes("social") ||
|
|
n.includes("instagram") ||
|
|
n.includes("tiktok") ||
|
|
n.includes("reel")
|
|
)
|
|
return "social";
|
|
if (n.includes("ad") || n.includes("promo") || n.includes("ads"))
|
|
return "ads";
|
|
if (n.includes("sale") || n.includes("real estate")) return "sales";
|
|
if (n.includes("music") || n.includes("audio")) return "music";
|
|
return "social";
|
|
}
|
|
|
|
/**
|
|
* Convert a raw AdminProject (from admin-api.ts) to a VideoCatalogTemplate
|
|
* so admin-managed templates can be shown on the templates page.
|
|
*
|
|
* Import type only — do not import from admin-api in this file at runtime.
|
|
*/
|
|
export interface AdminProjectLike {
|
|
slug: string;
|
|
title: string;
|
|
description?: string;
|
|
type: "video" | "image";
|
|
categoryName?: string;
|
|
coverImageUrl?: string;
|
|
previewVideoUrl?: string;
|
|
}
|
|
|
|
export function adminProjectToCatalogTemplate(
|
|
p: AdminProjectLike
|
|
): VideoCatalogTemplate {
|
|
return {
|
|
id: p.slug,
|
|
name: p.title,
|
|
videoCategory: adminCategoryNameToSidebarId(p.categoryName),
|
|
aspectRatio: "widescreen",
|
|
durationType: "flexible",
|
|
premium: false,
|
|
sceneCount: 0,
|
|
supports4k: false,
|
|
colorChange: false,
|
|
scriptToVideo: false,
|
|
description: p.description,
|
|
isNew: true,
|
|
};
|
|
}
|