feat: admin API integration, LogoMark, settings page, i18n, RTL font, docs

- 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
This commit is contained in:
Soroush.Asadi
2026-05-27 09:06:51 +03:30
parent 4875e468fe
commit 36e264f3e3
27 changed files with 1275 additions and 88 deletions
+52 -9
View File
@@ -1,25 +1,68 @@
import type { Metadata } from "next";
import { SettingsBilling } from "@/components/dashboard/settings/SettingsBilling";
import { SettingsNotifications } from "@/components/dashboard/settings/SettingsNotifications";
import { SettingsProfile } from "@/components/dashboard/settings/SettingsProfile";
import { SettingsSecurity } from "@/components/dashboard/settings/SettingsSecurity";
import { createPageMetadata } from "@/lib/metadata";
import { getUserProfile } from "@/lib/profiles";
import { createClient } from "@/lib/supabase/server";
export const metadata: Metadata = createPageMetadata({
title: "Settings",
description: "Manage your CreatorStudio account and workspace preferences.",
description: "Manage your FlatRender account and workspace preferences.",
path: "/dashboard/settings",
});
export default function DashboardSettingsPage() {
export const dynamic = "force-dynamic";
export default async function DashboardSettingsPage() {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const email = user?.email ?? "";
const displayName =
typeof user?.user_metadata?.full_name === "string"
? user.user_metadata.full_name
: null;
const profile = user ? await getUserProfile(user.id) : null;
const plan = profile?.plan ?? "free";
return (
<div className="flex flex-1 flex-col">
{/* Page header */}
<header className="border-b border-gray-100 bg-white px-6 py-4">
<h1 className="font-heading text-xl font-bold text-neutral-900">
Settings
</h1>
</header>
<div className="flex-1 p-6">
<p className="text-sm text-neutral-600">
Account and workspace settings will be available here soon.
<h1 className="font-heading text-xl font-bold text-neutral-900">Settings</h1>
<p className="mt-0.5 text-sm text-neutral-500">
Manage your account, security, and notification preferences.
</p>
</header>
{/* Content */}
<div className="flex-1 p-6">
<div className="mx-auto max-w-2xl space-y-6">
<SettingsProfile email={email} displayName={displayName} />
<SettingsSecurity />
<SettingsBilling plan={plan} />
<SettingsNotifications />
{/* Danger zone */}
<div className="rounded-xl border border-red-100 bg-white p-6">
<h2 className="font-heading text-base font-semibold text-red-600">Danger zone</h2>
<p className="mt-1 text-sm text-neutral-500">
Permanently delete your account and all your projects. This cannot be undone.
</p>
<button
type="button"
className="mt-4 rounded-lg border border-red-200 px-4 py-2 text-sm font-semibold text-red-600 transition-colors hover:bg-red-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
>
Delete account
</button>
</div>
</div>
</div>
</div>
);
+1
View File
@@ -96,6 +96,7 @@ export default async function LocaleLayout({
className={fontVars}
>
<head>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
+8 -2
View File
@@ -8,6 +8,7 @@ import { TemplateGallery } from "@/components/sections/TemplateGallery";
import { FAQ } from "@/components/sections/FAQ";
import { Testimonials } from "@/components/sections/Testimonials";
import { createPageMetadata } from "@/lib/metadata";
import { fetchProjects } from "@/lib/admin-api";
export const metadata: Metadata = createPageMetadata({
title: "Create Pro Videos & Images with AI",
@@ -16,12 +17,17 @@ export const metadata: Metadata = createPageMetadata({
path: "/",
});
export default function Home() {
export default async function Home() {
// Fetch up to 8 published projects from the admin service.
// Returns an empty array when ADMIN_API_URL is not set or the service
// is unreachable — TemplateGallery falls back to hardcoded data.
const { items: adminProjects } = await fetchProjects({ pageSize: 8 });
return (
<main>
<Hero />
<ProductsShowcase />
<TemplateGallery />
<TemplateGallery adminItems={adminProjects} />
<HowItWorks />
<Pricing />
<Testimonials />
+17 -2
View File
@@ -2,6 +2,8 @@ import type { Metadata } from "next";
import { TemplatesPageContent } from "@/components/templates/TemplatesPageContent";
import { createPageMetadata } from "@/lib/metadata";
import { fetchProjects } from "@/lib/admin-api";
import { adminProjectToCatalogTemplate } from "@/lib/video-templates-catalog";
export const metadata: Metadata = createPageMetadata({
title: "Video Templates",
@@ -10,10 +12,23 @@ export const metadata: Metadata = createPageMetadata({
path: "/templates",
});
export default function TemplatesPage() {
export default async function TemplatesPage() {
// Fetch video projects from the admin service.
// When ADMIN_API_URL is not set or the service is unreachable this returns
// an empty array → VideoTemplatesPageContent falls back to the demo catalog.
const { items: adminProjects } = await fetchProjects({
type: "video",
pageSize: 100,
});
const initialCatalog =
adminProjects.length > 0
? adminProjects.map(adminProjectToCatalogTemplate)
: undefined;
return (
<main className="min-h-screen bg-white">
<TemplatesPageContent />
<TemplatesPageContent initialCatalog={initialCatalog} />
</main>
);
}
+22
View File
@@ -68,6 +68,28 @@
}
}
/* ── RTL / Persian font override ─────────────────────────────────
Ensures Vazirmatn is used for every text element regardless of
any utility class or CSS-variable fallback chain. */
[dir="rtl"],
[dir="rtl"] body,
[dir="rtl"] h1,
[dir="rtl"] h2,
[dir="rtl"] h3,
[dir="rtl"] h4,
[dir="rtl"] h5,
[dir="rtl"] h6,
[dir="rtl"] p,
[dir="rtl"] span,
[dir="rtl"] a,
[dir="rtl"] button,
[dir="rtl"] input,
[dir="rtl"] textarea,
[dir="rtl"] select,
[dir="rtl"] label {
font-family: var(--font-vazirmatn), "Vazirmatn", sans-serif;
}
@layer utilities {
.bg-checkerboard {
background-color: #1f2937;
+73 -20
View File
@@ -2,7 +2,7 @@ import { ImageResponse } from "next/og";
export const runtime = "edge";
export const alt = "CreatorStudio — AI Video & Image Maker";
export const alt = "FlatRender — AI Video & Image Maker";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
@@ -17,41 +17,94 @@ export default function OpenGraphImage() {
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "center",
background: "linear-gradient(135deg, #1e40af 0%, #2563EB 50%, #7c3aed 100%)",
padding: "80px",
background:
"linear-gradient(135deg, #1e3a8a 0%, #2563EB 55%, #4f46e5 100%)",
padding: "80px 90px",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
<div
style={{
fontSize: 28,
fontWeight: 600,
color: "rgba(255,255,255,0.85)",
marginBottom: 16,
}}
>
CreatorStudio
{/* Logo mark row */}
<div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 40 }}>
{/* Icon */}
<div
style={{
width: 56,
height: 56,
borderRadius: 12,
background: "rgba(255,255,255,0.15)",
border: "1.5px solid rgba(255,255,255,0.3)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{/* Play triangle */}
<div
style={{
width: 0,
height: 0,
borderTop: "11px solid transparent",
borderBottom: "11px solid transparent",
borderLeft: "18px solid white",
marginLeft: 4,
}}
/>
</div>
<span
style={{ fontSize: 28, fontWeight: 700, color: "white", letterSpacing: -0.5 }}
>
FlatRender
</span>
</div>
{/* Main headline */}
<div
style={{
fontSize: 64,
fontWeight: 700,
fontSize: 62,
fontWeight: 800,
color: "white",
lineHeight: 1.1,
lineHeight: 1.08,
maxWidth: 900,
letterSpacing: -1.5,
}}
>
Create pro videos & images with AI
Create pro videos &amp; images with AI
</div>
{/* Subtitle */}
<div
style={{
fontSize: 28,
color: "rgba(255,255,255,0.9)",
marginTop: 24,
maxWidth: 800,
fontSize: 26,
color: "rgba(255,255,255,0.82)",
marginTop: 28,
maxWidth: 760,
lineHeight: 1.4,
}}
>
Templates, editors, and one-click export for every channel
</div>
{/* Bottom pill tags */}
<div style={{ display: "flex", gap: 12, marginTop: 48 }}>
{["Video Maker", "Image Maker", "AI Templates", "One-click Export"].map(
(tag) => (
<div
key={tag}
style={{
background: "rgba(255,255,255,0.15)",
border: "1px solid rgba(255,255,255,0.25)",
borderRadius: 100,
padding: "8px 20px",
fontSize: 16,
fontWeight: 600,
color: "rgba(255,255,255,0.9)",
}}
>
{tag}
</div>
)
)}
</div>
</div>
),
{ ...size }