diff --git a/messages/en.json b/messages/en.json index 2ab30b5..74b041d 100644 --- a/messages/en.json +++ b/messages/en.json @@ -376,6 +376,7 @@ "categories": "Categories", "tags": "Tags", "fonts": "Fonts", + "homePage": "Home Page", "blogs": "Blog", "learn": "Tutorials", "pages": "Pages", diff --git a/messages/fa.json b/messages/fa.json index 407ac1a..64d9d58 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -376,6 +376,7 @@ "categories": "دسته‌بندی‌ها", "tags": "برچسب‌ها", "fonts": "فونت‌ها", + "homePage": "صفحهٔ اصلی", "blogs": "بلاگ", "learn": "آموزش‌ها", "pages": "برگه‌ها", diff --git a/src/app/[locale]/admin/home/page.tsx b/src/app/[locale]/admin/home/page.tsx new file mode 100644 index 0000000..2b0d845 --- /dev/null +++ b/src/app/[locale]/admin/home/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { HomeSectionsManager } from "@/components/admin/HomeSectionsManager"; + +export default function Page() { + return ; +} diff --git a/src/app/[locale]/admin/layout.tsx b/src/app/[locale]/admin/layout.tsx index a319798..07e3bfb 100644 --- a/src/app/[locale]/admin/layout.tsx +++ b/src/app/[locale]/admin/layout.tsx @@ -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") }, diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 7b492e4..f5d22ee 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -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 { const t = await getTranslations("auto.appPage"); @@ -20,21 +25,48 @@ export async function generateMetadata(): Promise { }); } +/** 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 ; + case "slides": + return ; + case "products": + return ; + case "templates": + return ; + case "howItWorks": + return ; + case "pricing": + return ; + case "testimonials": + return ; + case "faq": + return ; + case "events": + return ; + 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 (
- - - - - - - + {sections + .filter((s) => s.enabled) + .map((s) => ( +
{renderSection(s, projects.items)}
+ ))}
); } diff --git a/src/components/admin/HomeSectionsManager.tsx b/src/components/admin/HomeSectionsManager.tsx new file mode 100644 index 0000000..4460bc5 --- /dev/null +++ b/src/components/admin/HomeSectionsManager.tsx @@ -0,0 +1,239 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +import { FileUploadField } from "@/components/admin/FileUploadField"; +import { + HOME_LAYOUT_KEY, + mergeLayout, + type HomeSection, +} from "@/lib/home-layout"; + +const card = "rounded-xl border border-[#1e2235] bg-[#0f1120]"; +const btn = "rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500 disabled:opacity-50"; +const ghost = "rounded-lg border border-[#262b40] px-3 py-1.5 text-xs text-gray-300 hover:bg-[#161a2e] disabled:opacity-40"; +const inp = "w-full rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"; +const lbl = "mb-1 block text-[11px] font-medium text-gray-500"; + +type FieldDef = { key: string; label: string; localized?: boolean; type?: "text" | "textarea" | "url" | "image" }; + +// Editable content per section type. Localized fields store _fa / _en. +const SECTION_META: Record = { + hero: { + label: "بخش اصلی (Hero)", + fields: [ + { key: "title", label: "عنوان", localized: true }, + { key: "description", label: "توضیح", localized: true, type: "textarea" }, + { key: "ctaLabel", label: "متن دکمهٔ اصلی", localized: true }, + { key: "ctaHref", label: "لینک دکمهٔ اصلی", type: "url" }, + { key: "browseLabel", label: "متن دکمهٔ دوم", localized: true }, + { key: "browseHref", label: "لینک دکمهٔ دوم", type: "url" }, + ], + }, + slides: { label: "اسلایدها", note: "محتوای اسلایدها در بخش «اسلایدها» مدیریت می‌شود.", fields: [] }, + products: { label: "محصولات", fields: [{ key: "heading", label: "عنوان", localized: true }] }, + templates: { label: "گالری قالب‌ها", fields: [{ key: "heading", label: "عنوان", localized: true }] }, + howItWorks: { + label: "چطور کار می‌کند", + fields: [ + { key: "heading", label: "عنوان", localized: true }, + { key: "subtitle", label: "زیرعنوان", localized: true }, + ], + }, + pricing: { label: "تعرفه‌ها", fields: [{ key: "heading", label: "عنوان", localized: true }] }, + testimonials: { label: "نظرات کاربران", fields: [{ key: "heading", label: "عنوان", localized: true }] }, + faq: { + label: "سوالات متداول", + fields: [ + { key: "heading", label: "عنوان", localized: true }, + { key: "subtitle", label: "زیرعنوان", localized: true }, + ], + }, + events: { label: "بنر رویدادها", note: "محتوای رویدادها در بخش «رویدادهای صفحه اصلی» مدیریت می‌شود.", fields: [] }, +}; + +const metaFor = (key: string) => SECTION_META[key] ?? { label: key, fields: [] }; + +export function HomeSectionsManager() { + const [sections, setSections] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [open, setOpen] = useState(null); + const [msg, setMsg] = useState(null); + const [error, setError] = useState(null); + + const reload = useCallback(async () => { + setLoading(true); + setError(null); + try { + const res = await fetch("/api/admin/resource/settings/all", { cache: "no-store" }); + const data = await res.json(); + const rows: Array<{ key: string; value: string }> = Array.isArray(data) ? data : data?.items ?? []; + const row = rows.find((r) => r.key === HOME_LAYOUT_KEY); + let parsed: { sections: HomeSection[] } | null = null; + if (row?.value) { + try { + const v = typeof row.value === "string" ? JSON.parse(row.value) : row.value; + if (v && Array.isArray(v.sections)) parsed = v; + } catch { + parsed = null; + } + } + setSections(mergeLayout(parsed)); + } catch { + setError("بارگذاری چیدمان ناموفق بود"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + reload(); + }, [reload]); + + const move = (i: number, dir: -1 | 1) => { + setSections((prev) => { + const next = [...prev]; + const j = i + dir; + if (j < 0 || j >= next.length) return prev; + [next[i], next[j]] = [next[j], next[i]]; + return next; + }); + }; + + const toggle = (key: string) => + setSections((prev) => prev.map((s) => (s.key === key ? { ...s, enabled: !s.enabled } : s))); + + const setConfig = (key: string, field: string, value: string) => + setSections((prev) => + prev.map((s) => (s.key === key ? { ...s, config: { ...(s.config ?? {}), [field]: value } } : s)), + ); + + const save = async () => { + setSaving(true); + setError(null); + setMsg(null); + try { + const payload = { sections: sections.map((s, i) => ({ ...s, sort: i })) }; + const res = await fetch("/api/admin/resource/settings", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + key: HOME_LAYOUT_KEY, + value: JSON.stringify(payload), + description: "چیدمان و محتوای صفحهٔ اصلی", + is_secret: false, + }), + }); + if (!res.ok) { + const d = await res.json().catch(() => null); + throw new Error(d?.error ?? "ذخیره‌سازی ناموفق بود"); + } + setMsg("ذخیره شد — صفحهٔ اصلی به‌روزرسانی شد."); + } catch (e) { + setError(e instanceof Error ? e.message : "ذخیره‌سازی ناموفق بود"); + } finally { + setSaving(false); + } + }; + + const fieldInput = (s: HomeSection, f: FieldDef, suffix: "" | "_fa" | "_en") => { + const ck = `${f.key}${suffix}`; + const val = String(s.config?.[ck] ?? ""); + if (f.type === "image") { + return setConfig(s.key, ck, url)} accept="image/*" />; + } + if (f.type === "textarea") { + return ( +