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.title}

- شماره‌تان را بگذارید؛ کارشناس ما در کمتر از یک روز کاری برای یک جلسه مشاوره رایگان تماس می‌گیرد. + {content.lead}

@@ -110,14 +110,14 @@ export function ContactSection() {
  • -

    دفتر مرکزی

    +

    {content.address_label}

    {brand.address}

  • -

    تلفن

    +

    {content.phone_label}

    ) : (
    setForm({ ...form, name: e.target.value })} - placeholder="نام و نام خانوادگی" + placeholder={content.name_placeholder} className="mt-3 w-full border border-ivory/20 bg-ink/45 px-5 py-4 text-sm font-light text-ivory placeholder:text-ivory/40 transition-colors duration-200 ease-lux focus:border-copper focus:outline-none" />
    setForm({ ...form, phone: e.target.value })} - placeholder="09xx xxx xxxx" + placeholder={content.phone_placeholder} className="mt-3 w-full border border-ivory/20 bg-ink/45 px-5 py-4 text-right text-sm font-light text-ivory placeholder:text-ivory/40 transition-colors duration-200 ease-lux focus:border-copper focus:outline-none" />