feat(home): admin-managed homepage section manager (toggle/reorder/edit)

The homepage is now driven by a `home_layout` Website Setting (jsonb) instead of a
hardcoded section stack — zero backend changes, no migration.

- lib/home-layout.ts: section catalog + saved-layout merge + locale-aware config
  reader (`<field>_fa`/`<field>_en`) + public fetchHomeLayout() (falls back to
  defaults when unset/unreachable).
- app/[locale]/page.tsx: renders ordered, enabled sections from the layout, passing
  per-section content overrides.
- sections (Hero/Products/Templates/HowItWorks/Pricing/Testimonials/FAQ): accept an
  optional `config` prop overriding heading/subtitle/CTA, locale-aware, default-safe.
- new HomeSlides + HomeEvents sections render the previously-orphaned admin Slides
  (/v1/slides) and Home Events (/v1/home-events) data.
- admin: HomeSectionsManager (toggle on/off, ↑/↓ reorder, per-section FA/EN content
  editor) at /admin/home, saved via the existing /v1/settings upsert; nav item + i18n.

Verified: a saved layout overrides Hero/Pricing headings and reorders sections;
removing it reverts to the default homepage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-12 01:21:44 +03:30
parent 1f6c35eb7c
commit b3637cf839
17 changed files with 578 additions and 45 deletions
+7
View File
@@ -0,0 +1,7 @@
"use client";
import { HomeSectionsManager } from "@/components/admin/HomeSectionsManager";
export default function Page() {
return <HomeSectionsManager />;
}
+1
View File
@@ -25,6 +25,7 @@ export default async function AdminLayout({
{
title: "محتوا",
items: [
{ href: "/admin/home", label: t("homePage") },
{ href: "/admin/categories", label: t("categories") },
{ href: "/admin/templates", label: t("templates") },
{ href: "/admin/projects", label: t("projects") },
+45 -13
View File
@@ -1,15 +1,20 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { FAQ } from "@/components/sections/FAQ";
import { Hero } from "@/components/sections/Hero";
import { HomeEvents } from "@/components/sections/HomeEvents";
import { HomeSlides } from "@/components/sections/HomeSlides";
import { HowItWorks } from "@/components/sections/HowItWorks";
import { Pricing } from "@/components/sections/Pricing";
import { ProductsShowcase } from "@/components/sections/ProductsShowcase";
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";
import { fetchProjects, type AdminProject } from "@/lib/admin-api";
import { fetchHomeLayout, type HomeSection } from "@/lib/home-layout";
export const revalidate = 30;
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations("auto.appPage");
@@ -20,21 +25,48 @@ export async function generateMetadata(): Promise<Metadata> {
});
}
/** Maps a configured section key → its component, passing admin content overrides. */
function renderSection(section: HomeSection, adminProjects: AdminProject[]) {
const config = section.config ?? {};
switch (section.key) {
case "hero":
return <Hero config={config} />;
case "slides":
return <HomeSlides />;
case "products":
return <ProductsShowcase config={config} />;
case "templates":
return <TemplateGallery adminItems={adminProjects} config={config} />;
case "howItWorks":
return <HowItWorks config={config} />;
case "pricing":
return <Pricing config={config} />;
case "testimonials":
return <Testimonials config={config} />;
case "faq":
return <FAQ config={config} />;
case "events":
return <HomeEvents />;
default:
return null;
}
}
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 });
// Layout (which sections, order, on/off, content overrides) is admin-managed via
// the `home_layout` setting; falls back to sensible defaults when unset.
const [sections, projects] = await Promise.all([
fetchHomeLayout(),
fetchProjects({ pageSize: 8 }),
]);
return (
<main>
<Hero />
<ProductsShowcase />
<TemplateGallery adminItems={adminProjects} />
<HowItWorks />
<Pricing />
<Testimonials />
<FAQ />
{sections
.filter((s) => s.enabled)
.map((s) => (
<div key={s.key}>{renderSection(s, projects.items)}</div>
))}
</main>
);
}