diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py
index 82a8887..7b0c8eb 100644
--- a/backend/app/api/admin.py
+++ b/backend/app/api/admin.py
@@ -14,17 +14,43 @@ from ..schemas import (
BeforeAfterOut,
BrandIn,
BrandOut,
+ ContactSectionIn,
+ ContactSectionOut,
ContactMessageOut,
FaqIn,
FaqOut,
+ FooterIn,
+ FooterOut,
+ GalleryImageIn,
+ GalleryImageOut,
+ HeroIn,
+ HeroOut,
+ InvestmentModelIn,
+ InvestmentModelOut,
LoginIn,
MessageReadIn,
+ NavItemIn,
+ NavItemOut,
+ NewsletterSectionIn,
+ NewsletterOut,
+ OfficeIn,
+ OfficeOut,
+ PageHeroIn,
+ PageHeroOut,
+ ProcessStepIn,
+ ProcessStepOut,
ProjectIn,
ProjectOut,
+ SectionHeadingIn,
+ SectionHeadingOut,
ServiceIn,
ServiceOut,
StatIn,
StatOut,
+ StatusLabelIn,
+ StatusLabelOut,
+ StoryIn,
+ StoryOut,
SubscriberOut,
TeamMemberIn,
TeamMemberOut,
@@ -107,6 +133,11 @@ def register_crud(path: str, model, out_schema, create_schema) -> None:
# CRUD محتوا
register_crud("brand", models.Brand, BrandOut, BrandIn)
+register_crud("nav", models.NavItem, NavItemOut, NavItemIn)
+register_crud("hero", models.Hero, HeroOut, HeroIn)
+register_crud("story", models.StorySection, StoryOut, StoryIn)
+register_crud("section-headings", models.SectionHeadingContent, SectionHeadingOut, SectionHeadingIn)
+register_crud("page-heroes", models.PageHeroContent, PageHeroOut, PageHeroIn)
register_crud("services", models.Service, ServiceOut, ServiceIn)
register_crud("projects", models.Project, ProjectOut, ProjectIn)
register_crud("articles", models.Article, ArticleOut, ArticleIn)
@@ -117,6 +148,14 @@ register_crud("timeline", models.TimelineItem, TimelineItemOut, TimelineItemIn)
register_crud("stats", models.Stat, StatOut, StatIn)
register_crud("why-us", models.WhyUsItem, WhyUsItemOut, WhyUsItemIn)
register_crud("before-after", models.BeforeAfter, BeforeAfterOut, BeforeAfterIn)
+register_crud("investment-models", models.InvestmentModel, InvestmentModelOut, InvestmentModelIn)
+register_crud("process-steps", models.ProcessStep, ProcessStepOut, ProcessStepIn)
+register_crud("offices", models.Office, OfficeOut, OfficeIn)
+register_crud("gallery", models.GalleryImage, GalleryImageOut, GalleryImageIn)
+register_crud("status-labels", models.StatusLabel, StatusLabelOut, StatusLabelIn)
+register_crud("contact-section", models.ContactSectionContent, ContactSectionOut, ContactSectionIn)
+register_crud("newsletter", models.NewsletterSectionContent, NewsletterOut, NewsletterSectionIn)
+register_crud("footer", models.FooterContent, FooterOut, FooterIn)
# پیامهای تماس
diff --git a/backend/app/api/public.py b/backend/app/api/public.py
index 895111a..c070bda 100644
--- a/backend/app/api/public.py
+++ b/backend/app/api/public.py
@@ -109,6 +109,11 @@ def get_site(db: Session = Depends(get_db)):
"""همه محتوای سایت در یک درخواست — مناسب برای بارگذاری اولیه فرانتاند."""
return {
"brand": db.query(models.Brand).first(),
+ "nav": db.query(models.NavItem).order_by(models.NavItem.id).all(),
+ "hero": db.query(models.Hero).first(),
+ "story": db.query(models.StorySection).first(),
+ "section_headings": db.query(models.SectionHeadingContent).order_by(models.SectionHeadingContent.id).all(),
+ "page_heroes": db.query(models.PageHeroContent).order_by(models.PageHeroContent.id).all(),
"services": db.query(models.Service).all(),
"projects": db.query(models.Project).order_by(models.Project.id).all(),
"articles": db.query(models.Article).order_by(models.Article.id).all(),
@@ -119,4 +124,12 @@ def get_site(db: Session = Depends(get_db)):
"stats": db.query(models.Stat).all(),
"why_us": db.query(models.WhyUsItem).all(),
"before_after": db.query(models.BeforeAfter).first(),
+ "investment_models": db.query(models.InvestmentModel).order_by(models.InvestmentModel.id).all(),
+ "process_steps": db.query(models.ProcessStep).order_by(models.ProcessStep.id).all(),
+ "offices": db.query(models.Office).order_by(models.Office.id).all(),
+ "gallery": db.query(models.GalleryImage).order_by(models.GalleryImage.id).all(),
+ "status_labels": db.query(models.StatusLabel).order_by(models.StatusLabel.id).all(),
+ "contact_section": db.query(models.ContactSectionContent).first(),
+ "newsletter": db.query(models.NewsletterSectionContent).first(),
+ "footer": db.query(models.FooterContent).first(),
}
diff --git a/backend/app/models.py b/backend/app/models.py
index d8b3d02..3a9d97d 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -139,6 +139,179 @@ class BeforeAfter(Base):
after_label: Mapped[str] = mapped_column(String(128), default="")
+class NavItem(Base):
+ __tablename__ = "nav_items"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ label: Mapped[str] = mapped_column(String(64))
+ href: Mapped[str] = mapped_column(String(128))
+
+
+class Hero(Base):
+ """محتوای هیرو صفحه اصلی — رکورد یگانه."""
+
+ __tablename__ = "hero"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ kicker: Mapped[str] = mapped_column(Text, default="")
+ title: Mapped[str] = mapped_column(Text, default="")
+ title_highlight: Mapped[str] = mapped_column(String(128), default="")
+ subtitle: Mapped[str] = mapped_column(Text, default="")
+ badge_value: Mapped[str] = mapped_column(String(32), default="")
+ badge_label: Mapped[str] = mapped_column(Text, default="")
+ cta_primary_label: Mapped[str] = mapped_column(String(128), default="")
+ cta_primary_href: Mapped[str] = mapped_column(String(128), default="")
+ cta_secondary_label: Mapped[str] = mapped_column(String(128), default="")
+ cta_secondary_href: Mapped[str] = mapped_column(String(128), default="")
+ scroll_label: Mapped[str] = mapped_column(String(64), default="")
+
+
+class StorySection(Base):
+ """بخش «درباره ما» در خانه و صفحه درباره — رکورد یگانه."""
+
+ __tablename__ = "story"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ title: Mapped[str] = mapped_column(String(256), default="")
+ paragraphs: Mapped[list] = mapped_column(JSON, default=list)
+ quote: Mapped[str] = mapped_column(Text, default="")
+ quote_author: Mapped[str] = mapped_column(Text, default="")
+ stat1_value: Mapped[str] = mapped_column(String(32), default="")
+ stat1_label: Mapped[str] = mapped_column(String(128), default="")
+ stat2_value: Mapped[str] = mapped_column(String(32), default="")
+ stat2_label: Mapped[str] = mapped_column(String(128), default="")
+ link_label: Mapped[str] = mapped_column(String(128), default="")
+ link_href: Mapped[str] = mapped_column(String(128), default="")
+ bento: Mapped[list] = mapped_column(JSON, default=list) # [{src, alt}]
+
+
+class SectionHeadingContent(Base):
+ """تیتر و توضیح بخشها — فرانتاند بر اساس key آن را پیدا میکند."""
+
+ __tablename__ = "section_headings"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ key: Mapped[str] = mapped_column(String(64), unique=True, index=True)
+ label: Mapped[str] = mapped_column(String(64), default="")
+ title: Mapped[str] = mapped_column(String(256), default="")
+ lead: Mapped[str] = mapped_column(Text, default="")
+ image: Mapped[str] = mapped_column(Text, default="")
+ cta_label: Mapped[str] = mapped_column(String(128), default="")
+ cta_href: Mapped[str] = mapped_column(String(128), default="")
+
+
+class PageHeroContent(Base):
+ """هیرو بالای هر صفحه — key: about | services | projects | investment | blog | faq | contact."""
+
+ __tablename__ = "page_heroes"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ key: Mapped[str] = mapped_column(String(32), unique=True, index=True)
+ title: Mapped[str] = mapped_column(String(256), default="")
+ lead: Mapped[str] = mapped_column(Text, default="")
+ image: Mapped[str] = mapped_column(Text, default="")
+
+
+class InvestmentModel(Base):
+ __tablename__ = "investment_models"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ title: Mapped[str] = mapped_column(String(256))
+ desc: Mapped[str] = mapped_column(Text, default="")
+ points: Mapped[list] = mapped_column(JSON, default=list)
+
+
+class ProcessStep(Base):
+ __tablename__ = "process_steps"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ step: Mapped[str] = mapped_column(String(16), default="")
+ title: Mapped[str] = mapped_column(String(256))
+ desc: Mapped[str] = mapped_column(Text, default="")
+
+
+class Office(Base):
+ __tablename__ = "offices"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ city: Mapped[str] = mapped_column(String(64))
+ address: Mapped[str] = mapped_column(Text, default="")
+ hours: Mapped[str] = mapped_column(String(128), default="")
+
+
+class GalleryImage(Base):
+ __tablename__ = "gallery_images"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ src: Mapped[str] = mapped_column(Text, default="")
+ alt: Mapped[str] = mapped_column(Text, default="")
+
+
+class StatusLabel(Base):
+ """برچسب فارسی وضعیت پروژه — key: done | progress."""
+
+ __tablename__ = "status_labels"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ key: Mapped[str] = mapped_column(String(16), unique=True, index=True)
+ label: Mapped[str] = mapped_column(String(64))
+
+
+class ContactSectionContent(Base):
+ """بخش فرم تماس — رکورد یگانه."""
+
+ __tablename__ = "contact_section"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ label: Mapped[str] = mapped_column(String(64), default="")
+ title: Mapped[str] = mapped_column(String(256), default="")
+ lead: Mapped[str] = mapped_column(Text, default="")
+ address_label: Mapped[str] = mapped_column(String(64), default="")
+ phone_label: Mapped[str] = mapped_column(String(64), default="")
+ email_label: Mapped[str] = mapped_column(String(64), default="")
+ name_label: Mapped[str] = mapped_column(String(64), default="")
+ name_placeholder: Mapped[str] = mapped_column(String(128), default="")
+ phone_field_label: Mapped[str] = mapped_column(String(64), default="")
+ phone_placeholder: Mapped[str] = mapped_column(String(64), default="")
+ note_label: Mapped[str] = mapped_column(String(64), default="")
+ note_optional_label: Mapped[str] = mapped_column(String(64), default="")
+ note_placeholder: Mapped[str] = mapped_column(String(256), default="")
+ submit_label: Mapped[str] = mapped_column(String(128), default="")
+ sending_label: Mapped[str] = mapped_column(String(64), default="")
+ success_title: Mapped[str] = mapped_column(String(256), default="")
+ success_text: Mapped[str] = mapped_column(Text, default="")
+ success_button_label: Mapped[str] = mapped_column(String(128), default="")
+
+
+class NewsletterSectionContent(Base):
+ """بخش خبرنامه — رکورد یگانه."""
+
+ __tablename__ = "newsletter"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ title: Mapped[str] = mapped_column(String(256), default="")
+ lead: Mapped[str] = mapped_column(Text, default="")
+ placeholder: Mapped[str] = mapped_column(String(128), default="")
+ button_label: Mapped[str] = mapped_column(String(64), default="")
+ sending_label: Mapped[str] = mapped_column(String(64), default="")
+ success_text: Mapped[str] = mapped_column(String(256), default="")
+
+
+class FooterContent(Base):
+ """متنهای فوتر — رکورد یگانه."""
+
+ __tablename__ = "footer"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ about_text: Mapped[str] = mapped_column(Text, default="")
+ pages_heading: Mapped[str] = mapped_column(String(64), default="")
+ services_heading: Mapped[str] = mapped_column(String(64), default="")
+ contact_heading: Mapped[str] = mapped_column(String(64), default="")
+ copyright: Mapped[str] = mapped_column(String(256), default="")
+ tagline: Mapped[str] = mapped_column(String(256), default="")
+ socials: Mapped[list] = mapped_column(JSON, default=list) # [{label, href, icon}]
+
+
class ContactMessage(Base):
__tablename__ = "contact_messages"
diff --git a/backend/app/schemas.py b/backend/app/schemas.py
index d9815db..ad5c098 100644
--- a/backend/app/schemas.py
+++ b/backend/app/schemas.py
@@ -112,6 +112,137 @@ class BeforeAfterOut(ORMModel):
after_label: str
+class NavItemOut(ORMModel):
+ id: int
+ label: str
+ href: str
+
+
+class HeroOut(ORMModel):
+ id: int
+ kicker: str
+ title: str
+ title_highlight: str
+ subtitle: str
+ badge_value: str
+ badge_label: str
+ cta_primary_label: str
+ cta_primary_href: str
+ cta_secondary_label: str
+ cta_secondary_href: str
+ scroll_label: str
+
+
+class StoryOut(ORMModel):
+ id: int
+ title: str
+ paragraphs: list[str]
+ quote: str
+ quote_author: str
+ stat1_value: str
+ stat1_label: str
+ stat2_value: str
+ stat2_label: str
+ link_label: str
+ link_href: str
+ bento: list[dict]
+
+
+class SectionHeadingOut(ORMModel):
+ id: int
+ key: str
+ label: str
+ title: str
+ lead: str
+ image: str
+ cta_label: str
+ cta_href: str
+
+
+class PageHeroOut(ORMModel):
+ id: int
+ key: str
+ title: str
+ lead: str
+ image: str
+
+
+class InvestmentModelOut(ORMModel):
+ id: int
+ title: str
+ desc: str
+ points: list[str]
+
+
+class ProcessStepOut(ORMModel):
+ id: int
+ step: str
+ title: str
+ desc: str
+
+
+class OfficeOut(ORMModel):
+ id: int
+ city: str
+ address: str
+ hours: str
+
+
+class GalleryImageOut(ORMModel):
+ id: int
+ src: str
+ alt: str
+
+
+class StatusLabelOut(ORMModel):
+ id: int
+ key: str
+ label: str
+
+
+class ContactSectionOut(ORMModel):
+ id: int
+ label: str
+ title: str
+ lead: str
+ address_label: str
+ phone_label: str
+ email_label: str
+ name_label: str
+ name_placeholder: str
+ phone_field_label: str
+ phone_placeholder: str
+ note_label: str
+ note_optional_label: str
+ note_placeholder: str
+ submit_label: str
+ sending_label: str
+ success_title: str
+ success_text: str
+ success_button_label: str
+
+
+class NewsletterOut(ORMModel):
+ id: int
+ title: str
+ lead: str
+ placeholder: str
+ button_label: str
+ sending_label: str
+ success_text: str
+
+
+class FooterOut(ORMModel):
+ id: int
+ about_text: str
+ pages_heading: str
+ services_heading: str
+ contact_heading: str
+ copyright: str
+ tagline: str
+ socials: list[dict]
+
+
class ContactMessageOut(ORMModel):
id: int
name: str
@@ -242,5 +373,123 @@ class BeforeAfterIn(BaseModel):
after_label: str = ""
+class NavItemIn(BaseModel):
+ label: str = Field(min_length=1, max_length=64)
+ href: str = Field(min_length=1, max_length=128)
+
+
+class HeroIn(BaseModel):
+ kicker: str = ""
+ title: str = ""
+ title_highlight: str = ""
+ subtitle: str = ""
+ badge_value: str = ""
+ badge_label: str = ""
+ cta_primary_label: str = ""
+ cta_primary_href: str = ""
+ cta_secondary_label: str = ""
+ cta_secondary_href: str = ""
+ scroll_label: str = ""
+
+
+class StoryIn(BaseModel):
+ title: str = ""
+ paragraphs: list[str] = []
+ quote: str = ""
+ quote_author: str = ""
+ stat1_value: str = ""
+ stat1_label: str = ""
+ stat2_value: str = ""
+ stat2_label: str = ""
+ link_label: str = ""
+ link_href: str = ""
+ bento: list[dict] = []
+
+
+class SectionHeadingIn(BaseModel):
+ key: str = Field(min_length=1, max_length=64)
+ label: str = ""
+ title: str = ""
+ lead: str = ""
+ image: str = ""
+ cta_label: str = ""
+ cta_href: str = ""
+
+
+class PageHeroIn(BaseModel):
+ key: str = Field(min_length=1, max_length=32)
+ title: str = ""
+ lead: str = ""
+ image: str = ""
+
+
+class InvestmentModelIn(BaseModel):
+ title: str = Field(min_length=1, max_length=256)
+ desc: str = ""
+ points: list[str] = []
+
+
+class ProcessStepIn(BaseModel):
+ step: str = ""
+ title: str = Field(min_length=1, max_length=256)
+ desc: str = ""
+
+
+class OfficeIn(BaseModel):
+ city: str = Field(min_length=1, max_length=64)
+ address: str = ""
+ hours: str = ""
+
+
+class GalleryImageIn(BaseModel):
+ src: str = ""
+ alt: str = ""
+
+
+class StatusLabelIn(BaseModel):
+ key: str = Field(min_length=1, max_length=16)
+ label: str = Field(min_length=1, max_length=64)
+
+
+class ContactSectionIn(BaseModel):
+ label: str = ""
+ title: str = ""
+ lead: str = ""
+ address_label: str = ""
+ phone_label: str = ""
+ email_label: str = ""
+ name_label: str = ""
+ name_placeholder: str = ""
+ phone_field_label: str = ""
+ phone_placeholder: str = ""
+ note_label: str = ""
+ note_optional_label: str = ""
+ note_placeholder: str = ""
+ submit_label: str = ""
+ sending_label: str = ""
+ success_title: str = ""
+ success_text: str = ""
+ success_button_label: str = ""
+
+
+class NewsletterSectionIn(BaseModel):
+ title: str = ""
+ lead: str = ""
+ placeholder: str = ""
+ button_label: str = ""
+ sending_label: str = ""
+ success_text: str = ""
+
+
+class FooterIn(BaseModel):
+ about_text: str = ""
+ pages_heading: str = ""
+ services_heading: str = ""
+ contact_heading: str = ""
+ copyright: str = ""
+ tagline: str = ""
+ socials: list[dict] = []
+
+
class MessageReadIn(BaseModel):
is_read: bool = True
diff --git a/backend/app/seed.py b/backend/app/seed.py
index 545f561..059358e 100644
--- a/backend/app/seed.py
+++ b/backend/app/seed.py
@@ -1,17 +1,49 @@
"""اسکریپت seed دیتابیس.
اجرا:
- python -m app.seed # فقط اگر دیتابیس خالی باشد
+ python -m app.seed # فقط جدولهای خالی پر میشوند
python -m app.seed --force # حذف کامل دادهها و ساخت مجدد
+
+پر کردن هر جدول مستقل است: روی یک دیتابیس موجود، فقط جدولهای جدید یا خالی
+seed میشوند و دادههای قبلی دستنخورده میمانند.
"""
import argparse
+from typing import Any
from . import models, seed_data
from .auth import hash_password
from .config import settings
from .database import Base, SessionLocal, engine
+# (مدل، دادههای اولیه، برچسب گزارش)
+CONTENT: list[tuple[Any, list[dict], str]] = [
+ (models.Brand, [seed_data.BRAND], "برند"),
+ (models.Hero, [seed_data.HERO], "هیرو صفحه اصلی"),
+ (models.StorySection, [seed_data.STORY], "داستان ما"),
+ (models.SectionHeadingContent, seed_data.SECTION_HEADINGS, "تیتر بخشها"),
+ (models.PageHeroContent, seed_data.PAGE_HEROES, "هیرو صفحات"),
+ (models.NavItem, seed_data.NAV_ITEMS, "منوی ناوبری"),
+ (models.InvestmentModel, seed_data.INVESTMENT_MODELS, "مدلهای سرمایهگذاری"),
+ (models.ProcessStep, seed_data.PROCESS_STEPS, "مراحل فرآیند کار"),
+ (models.Office, seed_data.OFFICES, "دفاتر"),
+ (models.GalleryImage, seed_data.GALLERY_IMAGES, "گالری تصاویر"),
+ (models.StatusLabel, seed_data.STATUS_LABELS, "برچسب وضعیت پروژه"),
+ (models.ContactSectionContent, [seed_data.CONTACT_SECTION], "بخش تماس"),
+ (models.NewsletterSectionContent, [seed_data.NEWSLETTER], "خبرنامه"),
+ (models.FooterContent, [seed_data.FOOTER], "فوتر"),
+ (models.Service, seed_data.SERVICES, "خدمات"),
+ (models.Stat, seed_data.STATS, "آمار"),
+ (models.Project, seed_data.PROJECTS, "پروژهها"),
+ (models.Article, seed_data.ARTICLES, "مقالات"),
+ (models.Testimonial, seed_data.TESTIMONIALS, "نظرات"),
+ (models.Faq, seed_data.FAQS, "سوالات متداول"),
+ (models.TeamMember, seed_data.TEAM, "تیم"),
+ (models.TimelineItem, seed_data.TIMELINE, "تایملاین"),
+ (models.WhyUsItem, seed_data.WHY, "چرا آرامش"),
+ (models.BeforeAfter, [seed_data.BEFORE_AFTER], "قبل/بعد"),
+]
+
def seed(force: bool = False) -> None:
db = SessionLocal()
@@ -22,31 +54,11 @@ def seed(force: bool = False) -> None:
Base.metadata.create_all(bind=engine)
- if not force and db.query(models.Service).count() > 0:
- print("دیتابیس قبلاً seed شده است. برای بازنشانی کامل: python -m app.seed --force")
- return
-
- db.add(models.Brand(**seed_data.BRAND))
-
- for item in seed_data.SERVICES:
- db.add(models.Service(**item))
- for item in seed_data.STATS:
- db.add(models.Stat(**item))
- for item in seed_data.PROJECTS:
- db.add(models.Project(**item))
- for item in seed_data.ARTICLES:
- db.add(models.Article(**item))
- for item in seed_data.TESTIMONIALS:
- db.add(models.Testimonial(**item))
- for item in seed_data.FAQS:
- db.add(models.Faq(**item))
- for item in seed_data.TEAM:
- db.add(models.TeamMember(**item))
- for item in seed_data.TIMELINE:
- db.add(models.TimelineItem(**item))
- for item in seed_data.WHY:
- db.add(models.WhyUsItem(**item))
- db.add(models.BeforeAfter(**seed_data.BEFORE_AFTER))
+ for model, rows, label in CONTENT:
+ if db.query(model).count() > 0:
+ continue
+ for row in rows:
+ db.add(model(**row))
if db.query(models.AdminUser).count() == 0:
db.add(
@@ -59,19 +71,8 @@ def seed(force: bool = False) -> None:
db.commit()
- counts = {
- "خدمات": db.query(models.Service).count(),
- "پروژهها": db.query(models.Project).count(),
- "مقالات": db.query(models.Article).count(),
- "نظرات": db.query(models.Testimonial).count(),
- "سوالات متداول": db.query(models.Faq).count(),
- "تیم": db.query(models.TeamMember).count(),
- "تایملاین": db.query(models.TimelineItem).count(),
- "آمار": db.query(models.Stat).count(),
- "چرا آرامش": db.query(models.WhyUsItem).count(),
- }
- for label, count in counts.items():
- print(f" {label}: {count} رکورد")
+ for model, _, label in CONTENT:
+ print(f" {label}: {db.query(model).count()} رکورد")
print("✔ seed با موفقیت انجام شد.")
finally:
db.close()
diff --git a/backend/app/seed_data.py b/backend/app/seed_data.py
index 1d7d7d8..4e739cd 100644
--- a/backend/app/seed_data.py
+++ b/backend/app/seed_data.py
@@ -289,3 +289,165 @@ FAQS = [
{"question": "خدمات پس از تحویل شامل چه مواردی میشود؟", "answer": "پنج سال پشتیبانی فنی تأسیسات مکانیکی و برقی، بازدید دورهای سالانه و تضمین اجرای نما و عایقکاری."},
{"question": "پروژهها در چه شهرهایی اجرا میشوند؟", "answer": "در حال حاضر تهران، اصفهان و سواحل شمالی کشور مناطق اصلی فعالیت ما هستند و پروژهای نیز در جزیره کیش در دست ساخت است."},
]
+
+# ---------- محتوای قبلاً هاردکدشده در فرانتاند (الان در دیتابیس) ----------
+
+NAV_ITEMS = [
+ {"label": "صفحه اصلی", "href": "/"},
+ {"label": "درباره ما", "href": "/about"},
+ {"label": "خدمات", "href": "/services"},
+ {"label": "پروژهها", "href": "/projects"},
+ {"label": "سرمایهگذاری", "href": "/investment"},
+ {"label": "وبلاگ", "href": "/blog"},
+ {"label": "سوالات متداول", "href": "/faq"},
+ {"label": "تماس با ما", "href": "/contact"},
+]
+
+HERO = {
+ "kicker": "از سال ۱۳۸۳ — تهران، اصفهان، سواحل شمال",
+ "title": "ساختمانی که آرامش را معماری میکند",
+ "title_highlight": "معماری",
+ "subtitle": "طراحی، ساخت و سرمایهگذاری در پروژههای مسکونی و تجاری لوکس؛ با استانداردی که در جزئیات دیده میشود و در سالها دوام میآورد.",
+ "badge_value": "۴۷",
+ "badge_label": "پروژه تحویلشده در بیستویک سال فعالیت حرفهای",
+ "cta_primary_label": "مشاهده پروژهها",
+ "cta_primary_href": "/projects",
+ "cta_secondary_label": "مشاوره سرمایهگذاری",
+ "cta_secondary_href": "/contact",
+ "scroll_label": "اسکرول کنید",
+}
+
+STORY = {
+ "title": "درباره ی ما",
+ "paragraphs": [
+ "گروه ساختمانی آرامش با یک دفتر کوچک معماری آغاز شد و امروز یکی از مجموعههای شناختهشده ساختوساز لوکس در ایران است. در تمام این سالها یک اصل تغییر نکرده است: ساختمان باید برای زندگی ساخته شود، نه برای فروش سریع.",
+ "هر پروژه با مطالعه زمین، نور، اقلیم و الگوی زندگی ساکنان آغاز میشود؛ سپس تیم طراحی، سازه و اجرا در یک فرآیند واحد آن را تا تحویل کلید پیش میبرند.",
+ ],
+ "quote": "«ساختمان برای ما فقط آجر و بتن نیست؛ بلکه تعهدی است به کیفیت زندگی ساکنان. ما فضاهایی میسازیم که در گذر زمان ارزش و هویت خود را حفظ کنند.»",
+ "quote_author": "کامران آرامش — مدیرعامل شرکت ساختمانی آرامش",
+ "stat1_value": "۹۴٪",
+ "stat1_label": "تحویل بهموقع پروژهها",
+ "stat2_value": "۵ سال",
+ "stat2_label": "پشتیبانی پس از تحویل",
+ "link_label": "داستان کامل ما",
+ "link_href": "/about",
+ "bento": [
+ {"src": IMAGES[0], "alt": "نمای برج مسکونی آرامش در تهران"},
+ {"src": IMAGES[1], "alt": "آتریوم مرکزی مجتمع تجاری پارسه"},
+ {"src": IMAGES[2], "alt": "ویلاهای ساختهشده در سواحل شمال"},
+ {"src": IMAGES[3], "alt": "نمای شبانه برج دوقلوی نیاوران"},
+ ],
+}
+
+SECTION_HEADINGS = [
+ {"key": "services", "title": "خدمات", "lead": "از نخستین خط روی کاغذ تا تحویل کلید، مدیریت سرمایه و طراحی محوطه؛ همه مراحل در یک مجموعه و زیر یک نظام کنترل کیفیت."},
+ {"key": "featured_projects", "title": "پروژههای شاخص", "lead": "کارهایی که بیشترین چیز را درباره استاندارد ما میگویند؛ چه آنهایی که تحویل شدهاند و چه آنهایی که همین امروز در حال ساختاند.", "cta_label": "همه پروژهها", "cta_href": "/projects"},
+ {"key": "gallery", "title": "گالری تصاویر", "lead": "قابهایی از کارگاهها و پروژههای تحویلشده گروه ساختمانی آرامش."},
+ {"key": "timeline", "title": "مسیر ما", "lead": "از یک دفتر چهارنفره تا ۴۷ پروژه. خط زمان پروژههای ما را ببینید."},
+ {"key": "why_us", "title": "چرا گروه ساختمانی آرامش", "lead": "شش دلیلی که کارفرمایان و سرمایهگذاران بار دوم هم به ما برمیگردند.", "image": IMAGES[3]},
+ {"key": "testimonials", "title": "نظرات مشتریان"},
+ {"key": "team", "title": "تیم ما", "lead": "کسانی که تصمیمهای اصلی هر پروژه را میگیرند و پاسخگوی آن هستند."},
+ {"key": "articles", "label": "وبلاگ", "title": "آخرین مقالات", "lead": "یادداشتهای تیم معماری، اجرا و سرمایهگذاری گروه ساختمانی آرامش درباره آنچه در کارگاه و بازار میگذرد.", "cta_label": "همه مقالات", "cta_href": "/blog"},
+ {"key": "faq", "title": "سوالات متداول", "lead": "پاسخ کوتاه به پرتکرارترین پرسشهای سرمایهگذاران و خریداران."},
+ {"key": "investment_models", "title": "مدلهای همکاری", "lead": "بسته به میزان سرمایه، افق زمانی و سطح ریسکی که میپذیرید، یکی از این سه مسیر مناسب شماست."},
+ {"key": "process", "title": "فرآیند کار ما", "lead": "چهار مرحله شفاف، از نخستین جلسه تا تحویل کلید."},
+]
+
+PAGE_HEROES = [
+ {"key": "about", "title": "درباره ما", "lead": "گروه ساختمانی آرامش مجموعهای از معماران، مهندسان سازه و مدیران پروژه است که از سال ۱۳۸۳ در حوزه ساختوساز لوکس فعالیت میکند.", "image": IMAGES[3]},
+ {"key": "services", "title": "خدمات", "lead": "هشت حوزه تخصصی که چرخه کامل یک پروژه ساختمانی را از ایده تا بهرهبرداری پوشش میدهند.", "image": IMAGES[1]},
+ {"key": "projects", "title": "پروژهها", "lead": "مجموعهای از برجهای مسکونی، مراکز تجاری و ویلاهای اختصاصی؛ از پروژههای تحویلشده تا کارگاههای فعال امروز.", "image": IMAGES[0]},
+ {"key": "investment", "title": "سرمایهگذاری", "lead": "سه مسیر روشن برای ورود به بازار ساختوساز لوکس، با ساختار مالی شفاف و گزارشدهی منظم.", "image": IMAGES[4]},
+ {"key": "blog", "title": "وبلاگ", "lead": "یادداشتهای تیم معماری، اجرا و سرمایهگذاری گروه ساختمانی آرامش درباره آنچه در کارگاه و بازار میگذرد.", "image": IMAGES[2]},
+ {"key": "faq", "title": "سوالات متداول", "lead": "پیش از تماس، شاید پاسخ پرسش شما همینجا باشد. اگر نبود، خوشحال میشویم مستقیم صحبت کنیم.", "image": IMAGES[1]},
+ {"key": "contact", "title": "تماس با ما", "lead": "برای مشاوره ساخت، سرمایهگذاری یا بازدید از پروژهها، تیم ما در سه شهر آماده پاسخگویی است.", "image": IMAGES[1]},
+]
+
+INVESTMENT_MODELS = [
+ {
+ "title": "مشارکت در ساخت",
+ "desc": "مالک زمین و گروه ساختمانی آرامش در سود پروژه شریک میشوند؛ اجرا، مجوزها و فروش بر عهده ماست.",
+ "points": ["ارزیابی رایگان زمین", "قرارداد شفاف با ضمانت اجرا", "گزارش ماهانه پیشرفت"],
+ },
+ {
+ "title": "صندوق پروژهمحور",
+ "desc": "سرمایهگذاری روی یک پروژه مشخص با واحدهای قابل خرید و بازدهی محاسبهشده در پایان دوره.",
+ "points": ["حداقل دوره ۲۴ ماه", "گزارش ارزشگذاری فصلی", "امکان انتقال واحد سرمایه"],
+ },
+ {
+ "title": "پیشخرید واحد",
+ "desc": "خرید واحد مسکونی یا تجاری در مراحل ابتدایی ساخت با قیمت پیش از تکمیل و پرداخت مرحلهای.",
+ "points": ["پرداخت مرحلهای متناسب با پیشرفت", "امکان شخصیسازی پلان", "تضمین زمان تحویل"],
+ },
+]
+
+PROCESS_STEPS = [
+ {"step": "۰۱", "title": "مطالعه و امکانسنجی", "desc": "بررسی زمین، ضوابط شهری و تحلیل بازدهی اقتصادی پروژه."},
+ {"step": "۰۲", "title": "طراحی و فاز اجرایی", "desc": "کانسپت معماری، طراحی سازه و تأسیسات و تهیه نقشههای اجرایی."},
+ {"step": "۰۳", "title": "اجرا و کنترل کیفیت", "desc": "ساخت با نظارت مستمر، گزارش ماهانه پیشرفت و کنترل هزینه."},
+ {"step": "۰۴", "title": "تحویل و پشتیبانی", "desc": "تحویل کلید، آموزش بهرهبرداری و پنج سال پشتیبانی فنی."},
+]
+
+OFFICES = [
+ {"city": "تهران", "address": "خیابان ولیعصر، برج آرامش، طبقه هجدهم", "hours": "شنبه تا چهارشنبه، ۹ تا ۱۸"},
+ {"city": "اصفهان", "address": "خیابان چهارباغ بالا، مجتمع پارسه، طبقه ششم", "hours": "شنبه تا چهارشنبه، ۹ تا ۱۷"},
+ {"city": "نوشهر", "address": "بلوار دریا، دفتر پروژه دریاکنار", "hours": "شنبه تا پنجشنبه، ۱۰ تا ۱۶"},
+]
+
+GALLERY_IMAGES = [
+ {"src": IMAGES[0], "alt": "تصویری از پروژههای گروه ساختمانی آرامش، شماره ۱"},
+ {"src": IMAGES[1], "alt": "تصویری از پروژههای گروه ساختمانی آرامش، شماره ۲"},
+ {"src": IMAGES[2], "alt": "تصویری از پروژههای گروه ساختمانی آرامش، شماره ۳"},
+ {"src": IMAGES[3], "alt": "تصویری از پروژههای گروه ساختمانی آرامش، شماره ۴"},
+ {"src": IMAGES[4], "alt": "تصویری از پروژههای گروه ساختمانی آرامش، شماره ۵"},
+ {"src": IMAGES[1], "alt": "تصویری از پروژههای گروه ساختمانی آرامش، شماره ۶"},
+]
+
+STATUS_LABELS = [
+ {"key": "done", "label": "انجام شده"},
+ {"key": "progress", "label": "در حال انجام"},
+]
+
+CONTACT_SECTION = {
+ "label": "تماس با ما",
+ "title": "پروژهتان را با ما در میان بگذارید",
+ "lead": "شمارهتان را بگذارید؛ کارشناس ما در کمتر از یک روز کاری برای یک جلسه مشاوره رایگان تماس میگیرد.",
+ "address_label": "دفتر مرکزی",
+ "phone_label": "تلفن",
+ "email_label": "ایمیل",
+ "name_label": "نام",
+ "name_placeholder": "نام و نام خانوادگی",
+ "phone_field_label": "شماره تلفن",
+ "phone_placeholder": "09xx xxx xxxx",
+ "note_label": "توضیحات",
+ "note_optional_label": "(اختیاری)",
+ "note_placeholder": "درباره پروژه یا درخواست خود بنویسید…",
+ "submit_label": "ارسال درخواست مشاوره",
+ "sending_label": "در حال ارسال…",
+ "success_title": "درخواست شما ثبت شد",
+ "success_text": "از اعتماد شما سپاسگزاریم. کارشناسان ما بهزودی با شما تماس میگیرند.",
+ "success_button_label": "ثبت درخواست جدید",
+}
+
+NEWSLETTER = {
+ "title": "خبرنامه گروه ساختمانی آرامش",
+ "lead": "هر ماه یک ایمیل: پروژههای جدید، فرصتهای سرمایهگذاری و یادداشتهای تیم معماری.",
+ "placeholder": "نشانی ایمیل شما",
+ "button_label": "عضویت",
+ "sending_label": "در حال ارسال…",
+ "success_text": "عضویت شما ثبت شد.",
+}
+
+FOOTER = {
+ "about_text": "گروه ساختمانی آرامش از سال ۱۳۸۳ ساختمانهایی میسازد که قرار است چند نسل در آنها زندگی کنند؛ معماری آرام، اجرای دقیق و سرمایهگذاری شفاف.",
+ "pages_heading": "صفحات",
+ "services_heading": "خدمات",
+ "contact_heading": "ارتباط با ما",
+ "copyright": "© ۱۴۰۳ گروه ساختمانی آرامش — تمامی حقوق محفوظ است.",
+ "tagline": "طراحی و ساخت با دقتِ معماری",
+ "socials": [
+ {"label": "اینستاگرام", "href": "#", "icon": "instagram"},
+ {"label": "لینکدین", "href": "#", "icon": "linkedin"},
+ {"label": "تلگرام", "href": "#", "icon": "telegram"},
+ ],
+}
diff --git a/frontend/index.html b/frontend/index.html
index 23badd1..6523b88 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,10 +1,10 @@
-
+
- Aramesh Building Group Luxury Website
+ گروه ساختمانی آرامش
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 6187972..dce1d9a 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -57,6 +57,10 @@ function AnimatedRoutes() {
function AppShell() {
const { data, error, reload } = useSiteData();
+ useEffect(() => {
+ if (data?.brand.name) document.title = data.brand.name;
+ }, [data?.brand.name]);
+
return (
diff --git a/frontend/src/api.tsx b/frontend/src/api.tsx
index c4a8e25..3b6749d 100644
--- a/frontend/src/api.tsx
+++ b/frontend/src/api.tsx
@@ -32,8 +32,109 @@ export type BeforeAfterInfo = {
after_label: string
}
+export type NavItem = { id: number; label: string; href: string }
+
+export type HeroContent = {
+ id: number
+ kicker: string
+ title: string
+ title_highlight: string
+ subtitle: string
+ badge_value: string
+ badge_label: string
+ cta_primary_label: string
+ cta_primary_href: string
+ cta_secondary_label: string
+ cta_secondary_href: string
+ scroll_label: string
+}
+
+export type StoryContent = {
+ id: number
+ title: string
+ paragraphs: string[]
+ quote: string
+ quote_author: string
+ stat1_value: string
+ stat1_label: string
+ stat2_value: string
+ stat2_label: string
+ link_label: string
+ link_href: string
+ bento: { src: string; alt: string }[]
+}
+
+export type SectionHeadingContent = {
+ id: number
+ key: string
+ label: string
+ title: string
+ lead: string
+ image: string
+ cta_label: string
+ cta_href: string
+}
+
+export type PageHeroContent = { id: number; key: string; title: string; lead: string; image: string }
+
+export type InvestmentModelItem = { id: number; title: string; desc: string; points: string[] }
+export type ProcessStepItem = { id: number; step: string; title: string; desc: string }
+export type OfficeItem = { id: number; city: string; address: string; hours: string }
+export type GalleryImageItem = { id: number; src: string; alt: string }
+export type StatusLabelItem = { id: number; key: string; label: string }
+
+export type ContactSectionContent = {
+ id: number
+ label: string
+ title: string
+ lead: string
+ address_label: string
+ phone_label: string
+ email_label: string
+ name_label: string
+ name_placeholder: string
+ phone_field_label: string
+ phone_placeholder: string
+ note_label: string
+ note_optional_label: string
+ note_placeholder: string
+ submit_label: string
+ sending_label: string
+ success_title: string
+ success_text: string
+ success_button_label: string
+}
+
+export type NewsletterContent = {
+ id: number
+ title: string
+ lead: string
+ placeholder: string
+ button_label: string
+ sending_label: string
+ success_text: string
+}
+
+export type SocialItem = { label: string; href: string; icon: string }
+
+export type FooterContent = {
+ id: number
+ about_text: string
+ pages_heading: string
+ services_heading: string
+ contact_heading: string
+ copyright: string
+ tagline: string
+ socials: SocialItem[]
+}
+
export interface SiteData {
brand: BrandInfo
+ nav: NavItem[]
+ hero: HeroContent
+ story: StoryContent
+ section_headings: SectionHeadingContent[]
+ page_heroes: PageHeroContent[]
services: Service[]
projects: Project[]
articles: Article[]
@@ -44,6 +145,14 @@ export interface SiteData {
stats: StatItem[]
why_us: WhyUsItem[]
before_after: BeforeAfterInfo
+ investment_models: InvestmentModelItem[]
+ process_steps: ProcessStepItem[]
+ offices: OfficeItem[]
+ gallery: GalleryImageItem[]
+ status_labels: StatusLabelItem[]
+ contact_section: ContactSectionContent
+ newsletter: NewsletterContent
+ footer: FooterContent
}
interface SiteContextValue {
@@ -68,7 +177,7 @@ export function SiteProvider({ children }: { children: ReactNode }) {
})
.then((json) => {
if (!cancelled) {
- if (json.brand && json.before_after) setData(json)
+ if (json.brand && json.hero && json.footer && json.nav) setData(json)
else setError(true)
}
})
@@ -103,3 +212,34 @@ export function useSite(): SiteData {
}
return data
}
+
+const EMPTY_HEADING: SectionHeadingContent = {
+ id: 0,
+ key: '',
+ label: '',
+ title: '',
+ lead: '',
+ image: '',
+ cta_label: '',
+ cta_href: '',
+}
+
+/** تیتر و توضیح یک بخش بر اساس key تعریفشده در پنل مدیریت */
+export function useSectionHeading(key: string): SectionHeadingContent {
+ const { data } = useContext(SiteContext)
+ return data?.section_headings.find((s) => s.key === key) ?? { ...EMPTY_HEADING, key }
+}
+
+/** هیرو بالای صفحه بر اساس key تعریفشده در پنل مدیریت */
+export function usePageHero(key: string): PageHeroContent {
+ const { data } = useContext(SiteContext)
+ return (
+ data?.page_heroes.find((p) => p.key === key) ?? { id: 0, key, title: '', lead: '', image: '' }
+ )
+}
+
+/** برچسب فارسی وضعیت پروژه (done | progress) از پنل مدیریت */
+export function useStatusLabel(key: string): string {
+ const { data } = useContext(SiteContext)
+ return data?.status_labels.find((s) => s.key === key)?.label ?? key
+}
diff --git a/frontend/src/components/ContactSection.tsx b/frontend/src/components/ContactSection.tsx
index 3084330..fd7e10c 100644
--- a/frontend/src/components/ContactSection.tsx
+++ b/frontend/src/components/ContactSection.tsx
@@ -7,7 +7,7 @@ import { useSite } from '../api'
import { API_URL } from '../config'
export function ContactSection() {
- const { brand } = useSite()
+ const { brand, contact_section: content } = useSite()
const [form, setForm] = useState({ name: '', phone: '', note: '' })
const [sent, setSent] = useState(false)
const [sending, setSending] = useState(false)
@@ -96,12 +96,12 @@ export function ContactSection() {
- تماس با ما
+ {content.label}
- شمارهتان را بگذارید؛ کارشناس ما در کمتر از یک روز کاری برای یک جلسه مشاوره رایگان تماس میگیرد.
+ {content.lead}
@@ -110,14 +110,14 @@ export function ContactSection() {
-
دفتر مرکزی
+
{content.address_label}
{brand.address}
-
تلفن
+
{content.phone_label}
) : (
diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx
index 25ced7c..fd831f0 100644
--- a/frontend/src/components/Footer.tsx
+++ b/frontend/src/components/Footer.tsx
@@ -1,11 +1,16 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { InstagramIcon, LinkedinIcon, MailIcon, MapPinIcon, PhoneIcon, SendIcon } from 'lucide-react';
-import { NAV } from '../data/site';
import { useSite } from '../api';
+const SOCIAL_ICONS: Record> = {
+ instagram: InstagramIcon,
+ linkedin: LinkedinIcon,
+ telegram: SendIcon,
+};
+
export function Footer() {
- const { brand, services } = useSite();
+ const { brand, services, nav, footer } = useSite();
return (
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index 9ec7e23..0bae11d 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -1,12 +1,10 @@
import React, { useEffect, useState } from "react";
import { Link, NavLink, useLocation } from "react-router-dom";
import { MenuIcon, PhoneIcon, XIcon } from "lucide-react";
-import { NAV } from "../data/site";
import { useSite } from "../api";
-import { use } from "framer-motion/m";
export function Header() {
- const { brand } = useSite();
+ const { brand, nav } = useSite();
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const { pathname } = useLocation();
@@ -46,10 +44,10 @@ export function Header() {