پروژه کامل: فرانت متصل به API + داکرایز
- اتصال کامل فرانت به API: همه محتوای سایت (پروژهها، مقالات، خدمات، برند و...) از بکاند خوانده میشود (SiteProvider + useSite) - فرم تماس و خبرنامه متصل به بکاند و تستشده در مرورگر - ساختار مونوریپو: frontend/ + backend/ - Dockerfile برای هر دو بخش + docker-compose (PostgreSQL + FastAPI + nginx) - nginx با پروکسی /api — بدون نیاز به CORS در حالت داکر - README کامل فارسی
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
"""انتهای پنل مدیریت — همه مسیرها نیازمند توکن JWT معتبر هستند."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import models, schemas
|
||||
from ..auth import create_access_token, get_current_admin, verify_password
|
||||
from ..database import get_db
|
||||
from ..schemas import (
|
||||
ArticleIn,
|
||||
ArticleOut,
|
||||
BeforeAfterIn,
|
||||
BeforeAfterOut,
|
||||
BrandIn,
|
||||
BrandOut,
|
||||
ContactMessageOut,
|
||||
FaqIn,
|
||||
FaqOut,
|
||||
LoginIn,
|
||||
MessageReadIn,
|
||||
ProjectIn,
|
||||
ProjectOut,
|
||||
ServiceIn,
|
||||
ServiceOut,
|
||||
StatIn,
|
||||
StatOut,
|
||||
SubscriberOut,
|
||||
TeamMemberIn,
|
||||
TeamMemberOut,
|
||||
TestimonialIn,
|
||||
TestimonialOut,
|
||||
TimelineItemIn,
|
||||
TimelineItemOut,
|
||||
TokenOut,
|
||||
WhyUsItemIn,
|
||||
WhyUsItemOut,
|
||||
)
|
||||
|
||||
# روتر عمومی برای ورود — بدون نیاز به توکن
|
||||
auth_router = APIRouter()
|
||||
|
||||
# روتر محافظتشده — همه مسیرهای آن فقط با توکن معتبر قابل دسترسی هستند
|
||||
router = APIRouter(dependencies=[Depends(get_current_admin)])
|
||||
|
||||
|
||||
@auth_router.post("/auth/login", response_model=TokenOut)
|
||||
def login(payload: LoginIn, db: Session = Depends(get_db)):
|
||||
user = db.query(models.AdminUser).filter(models.AdminUser.username == payload.username).first()
|
||||
if user is None or not verify_password(payload.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="نام کاربری یا رمز عبور اشتباه است",
|
||||
)
|
||||
return TokenOut(access_token=create_access_token(user.username))
|
||||
|
||||
|
||||
def register_crud(path: str, model, out_schema, create_schema) -> None:
|
||||
"""ثبت پنج مسیر CRUD استاندارد (لیست، ساخت، دریافت، ویرایش، حذف) روی روتر مدیریت."""
|
||||
|
||||
@router.get(f"/{path}", response_model=list[out_schema])
|
||||
def list_items(db: Session = Depends(get_db)):
|
||||
return db.query(model).order_by(model.id).all()
|
||||
|
||||
@router.post(f"/{path}", response_model=out_schema, status_code=201)
|
||||
def create_item(payload: create_schema, db: Session = Depends(get_db)):
|
||||
item = model(**payload.model_dump())
|
||||
db.add(item)
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=409, detail="رکوردی با این مقدار تکراری وجود دارد (مثلاً slug یا ایمیل)")
|
||||
db.refresh(item)
|
||||
return item
|
||||
|
||||
@router.get(f"/{path}/{{item_id}}", response_model=out_schema)
|
||||
def get_item(item_id: int, db: Session = Depends(get_db)):
|
||||
item = db.get(model, item_id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="رکورد یافت نشد")
|
||||
return item
|
||||
|
||||
@router.put(f"/{path}/{{item_id}}", response_model=out_schema)
|
||||
def update_item(item_id: int, payload: create_schema, db: Session = Depends(get_db)):
|
||||
item = db.get(model, item_id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="رکورد یافت نشد")
|
||||
for key, value in payload.model_dump().items():
|
||||
setattr(item, key, value)
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=409, detail="رکوردی با این مقدار تکراری وجود دارد (مثلاً slug یا ایمیل)")
|
||||
db.refresh(item)
|
||||
return item
|
||||
|
||||
@router.delete(f"/{path}/{{item_id}}", status_code=204)
|
||||
def delete_item(item_id: int, db: Session = Depends(get_db)):
|
||||
item = db.get(model, item_id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="رکورد یافت نشد")
|
||||
db.delete(item)
|
||||
db.commit()
|
||||
|
||||
|
||||
# CRUD محتوا
|
||||
register_crud("brand", models.Brand, BrandOut, BrandIn)
|
||||
register_crud("services", models.Service, ServiceOut, ServiceIn)
|
||||
register_crud("projects", models.Project, ProjectOut, ProjectIn)
|
||||
register_crud("articles", models.Article, ArticleOut, ArticleIn)
|
||||
register_crud("testimonials", models.Testimonial, TestimonialOut, TestimonialIn)
|
||||
register_crud("faqs", models.Faq, FaqOut, FaqIn)
|
||||
register_crud("team", models.TeamMember, TeamMemberOut, TeamMemberIn)
|
||||
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)
|
||||
|
||||
|
||||
# پیامهای تماس
|
||||
@router.get("/messages", response_model=list[ContactMessageOut])
|
||||
def list_messages(unread_only: bool = False, db: Session = Depends(get_db)):
|
||||
query = db.query(models.ContactMessage)
|
||||
if unread_only:
|
||||
query = query.filter(models.ContactMessage.is_read.is_(False))
|
||||
return query.order_by(models.ContactMessage.created_at.desc()).all()
|
||||
|
||||
|
||||
@router.patch("/messages/{message_id}", response_model=ContactMessageOut)
|
||||
def update_message(message_id: int, payload: MessageReadIn, db: Session = Depends(get_db)):
|
||||
message = db.get(models.ContactMessage, message_id)
|
||||
if message is None:
|
||||
raise HTTPException(status_code=404, detail="پیام یافت نشد")
|
||||
message.is_read = payload.is_read
|
||||
db.commit()
|
||||
db.refresh(message)
|
||||
return message
|
||||
|
||||
|
||||
@router.delete("/messages/{message_id}", status_code=204)
|
||||
def delete_message(message_id: int, db: Session = Depends(get_db)):
|
||||
message = db.get(models.ContactMessage, message_id)
|
||||
if message is None:
|
||||
raise HTTPException(status_code=404, detail="پیام یافت نشد")
|
||||
db.delete(message)
|
||||
db.commit()
|
||||
|
||||
|
||||
# مشترکین خبرنامه
|
||||
@router.get("/subscribers", response_model=list[SubscriberOut])
|
||||
def list_subscribers(db: Session = Depends(get_db)):
|
||||
return db.query(models.Subscriber).order_by(models.Subscriber.created_at.desc()).all()
|
||||
|
||||
|
||||
@router.delete("/subscribers/{subscriber_id}", status_code=204)
|
||||
def delete_subscriber(subscriber_id: int, db: Session = Depends(get_db)):
|
||||
subscriber = db.get(models.Subscriber, subscriber_id)
|
||||
if subscriber is None:
|
||||
raise HTTPException(status_code=404, detail="مشترک یافت نشد")
|
||||
db.delete(subscriber)
|
||||
db.commit()
|
||||
@@ -0,0 +1,42 @@
|
||||
"""انتهای فرمها — ثبت پیام تماس و عضویت در خبرنامه."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from .. import models
|
||||
from ..schemas import ContactIn, ContactMessageOut, NewsletterIn, SubscriberOut
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/contact", response_model=ContactMessageOut, status_code=201)
|
||||
def submit_contact(payload: ContactIn, db: Session = Depends(get_db)):
|
||||
message = models.ContactMessage(
|
||||
name=payload.name.strip(),
|
||||
phone=payload.phone.strip(),
|
||||
note=payload.note.strip(),
|
||||
)
|
||||
db.add(message)
|
||||
db.commit()
|
||||
db.refresh(message)
|
||||
return message
|
||||
|
||||
|
||||
@router.post("/newsletter", response_model=SubscriberOut, status_code=201)
|
||||
def subscribe(payload: NewsletterIn, db: Session = Depends(get_db)):
|
||||
email = str(payload.email).lower()
|
||||
subscriber = db.query(models.Subscriber).filter(models.Subscriber.email == email).first()
|
||||
|
||||
if subscriber is not None:
|
||||
if not subscriber.is_active:
|
||||
subscriber.is_active = True
|
||||
db.commit()
|
||||
db.refresh(subscriber)
|
||||
return subscriber
|
||||
|
||||
subscriber = models.Subscriber(email=email)
|
||||
db.add(subscriber)
|
||||
db.commit()
|
||||
db.refresh(subscriber)
|
||||
return subscriber
|
||||
@@ -0,0 +1,122 @@
|
||||
"""انتهای عمومی — خواندن محتوای سایت بدون نیاز به احراز هویت."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from .. import models
|
||||
from ..schemas import (
|
||||
ArticleOut,
|
||||
BeforeAfterOut,
|
||||
BrandOut,
|
||||
FaqOut,
|
||||
ProjectOut,
|
||||
ServiceOut,
|
||||
StatOut,
|
||||
TeamMemberOut,
|
||||
TestimonialOut,
|
||||
TimelineItemOut,
|
||||
WhyUsItemOut,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "app": "ArameshPlaza API"}
|
||||
|
||||
|
||||
@router.get("/brand", response_model=BrandOut)
|
||||
def get_brand(db: Session = Depends(get_db)):
|
||||
brand = db.query(models.Brand).first()
|
||||
if brand is None:
|
||||
raise HTTPException(status_code=404, detail="اطلاعات برند یافت نشد")
|
||||
return brand
|
||||
|
||||
|
||||
@router.get("/services", response_model=list[ServiceOut])
|
||||
def get_services(db: Session = Depends(get_db)):
|
||||
return db.query(models.Service).all()
|
||||
|
||||
|
||||
@router.get("/projects", response_model=list[ProjectOut])
|
||||
def get_projects(db: Session = Depends(get_db)):
|
||||
return db.query(models.Project).order_by(models.Project.id).all()
|
||||
|
||||
|
||||
@router.get("/projects/{slug}", response_model=ProjectOut)
|
||||
def get_project(slug: str, db: Session = Depends(get_db)):
|
||||
project = db.query(models.Project).filter(models.Project.slug == slug).first()
|
||||
if project is None:
|
||||
raise HTTPException(status_code=404, detail="پروژه یافت نشد")
|
||||
return project
|
||||
|
||||
|
||||
@router.get("/articles", response_model=list[ArticleOut])
|
||||
def get_articles(db: Session = Depends(get_db)):
|
||||
return db.query(models.Article).order_by(models.Article.id).all()
|
||||
|
||||
|
||||
@router.get("/articles/{slug}", response_model=ArticleOut)
|
||||
def get_article(slug: str, db: Session = Depends(get_db)):
|
||||
article = db.query(models.Article).filter(models.Article.slug == slug).first()
|
||||
if article is None:
|
||||
raise HTTPException(status_code=404, detail="مقاله یافت نشد")
|
||||
return article
|
||||
|
||||
|
||||
@router.get("/testimonials", response_model=list[TestimonialOut])
|
||||
def get_testimonials(db: Session = Depends(get_db)):
|
||||
return db.query(models.Testimonial).all()
|
||||
|
||||
|
||||
@router.get("/faqs", response_model=list[FaqOut])
|
||||
def get_faqs(db: Session = Depends(get_db)):
|
||||
return db.query(models.Faq).all()
|
||||
|
||||
|
||||
@router.get("/team", response_model=list[TeamMemberOut])
|
||||
def get_team(db: Session = Depends(get_db)):
|
||||
return db.query(models.TeamMember).all()
|
||||
|
||||
|
||||
@router.get("/timeline", response_model=list[TimelineItemOut])
|
||||
def get_timeline(db: Session = Depends(get_db)):
|
||||
return db.query(models.TimelineItem).order_by(models.TimelineItem.id).all()
|
||||
|
||||
|
||||
@router.get("/stats", response_model=list[StatOut])
|
||||
def get_stats(db: Session = Depends(get_db)):
|
||||
return db.query(models.Stat).all()
|
||||
|
||||
|
||||
@router.get("/why-us", response_model=list[WhyUsItemOut])
|
||||
def get_why_us(db: Session = Depends(get_db)):
|
||||
return db.query(models.WhyUsItem).all()
|
||||
|
||||
|
||||
@router.get("/before-after", response_model=BeforeAfterOut)
|
||||
def get_before_after(db: Session = Depends(get_db)):
|
||||
item = db.query(models.BeforeAfter).first()
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="بخش قبل/بعد یافت نشد")
|
||||
return item
|
||||
|
||||
|
||||
@router.get("/site")
|
||||
def get_site(db: Session = Depends(get_db)):
|
||||
"""همه محتوای سایت در یک درخواست — مناسب برای بارگذاری اولیه فرانتاند."""
|
||||
return {
|
||||
"brand": db.query(models.Brand).first(),
|
||||
"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(),
|
||||
"testimonials": db.query(models.Testimonial).all(),
|
||||
"faqs": db.query(models.Faq).all(),
|
||||
"team": db.query(models.TeamMember).all(),
|
||||
"timeline": db.query(models.TimelineItem).order_by(models.TimelineItem.id).all(),
|
||||
"stats": db.query(models.Stat).all(),
|
||||
"why_us": db.query(models.WhyUsItem).all(),
|
||||
"before_after": db.query(models.BeforeAfter).first(),
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import bcrypt
|
||||
import jwt
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .config import settings
|
||||
from .database import get_db
|
||||
from .models import AdminUser
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
|
||||
|
||||
def verify_password(password: str, hashed: str) -> bool:
|
||||
return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))
|
||||
|
||||
|
||||
def create_access_token(username: str) -> str:
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes)
|
||||
payload = {"sub": username, "exp": expire}
|
||||
return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)
|
||||
|
||||
|
||||
def get_current_admin(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
db: Session = Depends(get_db),
|
||||
) -> AdminUser:
|
||||
"""وابستگی محافظتشده — فقط با توکن JWT معتبر دسترسی میدهد."""
|
||||
credentials_error = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="احراز هویت نامعتبر است",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
|
||||
except jwt.PyJWTError:
|
||||
raise credentials_error
|
||||
|
||||
username = payload.get("sub")
|
||||
if not username:
|
||||
raise credentials_error
|
||||
|
||||
user = db.query(AdminUser).filter(AdminUser.username == username).first()
|
||||
if user is None:
|
||||
raise credentials_error
|
||||
return user
|
||||
@@ -0,0 +1,22 @@
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""تنظیمات برنامه — از متغیرهای محیطی یا فایل .env خوانده میشود."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
database_url: str = "postgresql+psycopg://postgres:postgres@localhost:5432/arameshplaza_api"
|
||||
secret_key: str = "dev-secret-change-me-in-production"
|
||||
algorithm: str = "HS256"
|
||||
access_token_expire_minutes: int = 1440
|
||||
cors_origins: list[str] = ["*"]
|
||||
admin_username: str = "admin"
|
||||
admin_password: str = "aramesh123"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,20 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
||||
|
||||
from .config import settings
|
||||
|
||||
engine = create_engine(settings.database_url, pool_pre_ping=True)
|
||||
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
def get_db():
|
||||
"""وابستگی FastAPI برای تزریق نشست دیتابیس."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
@@ -0,0 +1,42 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from . import models # noqa: F401 — ثبت مدلها پیش از ساخت جداول
|
||||
from .api import admin, forms, public
|
||||
from .config import settings
|
||||
from .database import Base, engine
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
app = FastAPI(
|
||||
title="ArameshPlaza API",
|
||||
description=(
|
||||
"بکاند سایت آرامش پلازا — API محتوای سایت، فرم تماس و خبرنامه، "
|
||||
"و پنل مدیریت با احراز هویت JWT."
|
||||
),
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_credentials=False,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(public.router, prefix="/api", tags=["عمومی"])
|
||||
app.include_router(forms.router, prefix="/api", tags=["فرمها"])
|
||||
app.include_router(admin.auth_router, prefix="/api", tags=["احراز هویت"])
|
||||
app.include_router(admin.router, prefix="/api/admin", tags=["پنل مدیریت"])
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def root():
|
||||
return {
|
||||
"app": "ArameshPlaza API",
|
||||
"docs": "/docs",
|
||||
"health": "/api/health",
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import JSON, Boolean, DateTime, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .database import Base
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class AdminUser(Base):
|
||||
__tablename__ = "admin_users"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
password_hash: Mapped[str] = mapped_column(String(255))
|
||||
|
||||
|
||||
class Brand(Base):
|
||||
__tablename__ = "brand"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128), default="")
|
||||
logo: Mapped[str] = mapped_column(Text, default="")
|
||||
hero: Mapped[str] = mapped_column(Text, default="")
|
||||
contact_bg: Mapped[str] = mapped_column(Text, default="")
|
||||
phone: Mapped[str] = mapped_column(String(64), default="")
|
||||
phone_href: Mapped[str] = mapped_column(String(128), default="")
|
||||
email: Mapped[str] = mapped_column(String(128), default="")
|
||||
address: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
|
||||
class Service(Base):
|
||||
__tablename__ = "services"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str] = mapped_column(String(256))
|
||||
desc: Mapped[str] = mapped_column(Text, default="")
|
||||
icon: Mapped[str] = mapped_column(String(32), default="draft")
|
||||
|
||||
|
||||
class Project(Base):
|
||||
__tablename__ = "projects"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
slug: Mapped[str] = mapped_column(String(128), unique=True, index=True)
|
||||
title: Mapped[str] = mapped_column(String(256))
|
||||
location: Mapped[str] = mapped_column(String(128), default="")
|
||||
year: Mapped[str] = mapped_column(String(64), default="")
|
||||
status: Mapped[str] = mapped_column(String(16), default="done") # done | progress
|
||||
image: Mapped[str] = mapped_column(Text, default="")
|
||||
gallery: Mapped[list] = mapped_column(JSON, default=list)
|
||||
area: Mapped[str] = mapped_column(String(64), default="")
|
||||
floors: Mapped[str] = mapped_column(String(64), default="")
|
||||
units: Mapped[str] = mapped_column(String(64), default="")
|
||||
client: Mapped[str] = mapped_column(String(128), default="")
|
||||
summary: Mapped[str] = mapped_column(Text, default="")
|
||||
body: Mapped[list] = mapped_column(JSON, default=list)
|
||||
|
||||
|
||||
class Article(Base):
|
||||
__tablename__ = "articles"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
slug: Mapped[str] = mapped_column(String(128), unique=True, index=True)
|
||||
title: Mapped[str] = mapped_column(String(256))
|
||||
date: Mapped[str] = mapped_column(String(64), default="")
|
||||
category: Mapped[str] = mapped_column(String(64), default="")
|
||||
image: Mapped[str] = mapped_column(Text, default="")
|
||||
read: Mapped[str] = mapped_column(String(32), default="")
|
||||
excerpt: Mapped[str] = mapped_column(Text, default="")
|
||||
body: Mapped[list] = mapped_column(JSON, default=list)
|
||||
|
||||
|
||||
class Testimonial(Base):
|
||||
__tablename__ = "testimonials"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128))
|
||||
role: Mapped[str] = mapped_column(String(128), default="")
|
||||
quote: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
|
||||
class Faq(Base):
|
||||
__tablename__ = "faqs"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
question: Mapped[str] = mapped_column(Text)
|
||||
answer: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
|
||||
class TeamMember(Base):
|
||||
__tablename__ = "team"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128))
|
||||
role: Mapped[str] = mapped_column(String(128), default="")
|
||||
bio: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
|
||||
class TimelineItem(Base):
|
||||
__tablename__ = "timeline"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
year: Mapped[str] = mapped_column(String(32))
|
||||
title: Mapped[str] = mapped_column(String(256))
|
||||
desc: Mapped[str] = mapped_column(Text, default="")
|
||||
image: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
|
||||
class Stat(Base):
|
||||
__tablename__ = "stats"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
value: Mapped[int] = mapped_column(Integer)
|
||||
suffix: Mapped[str] = mapped_column(String(16), default="")
|
||||
label: Mapped[str] = mapped_column(String(128))
|
||||
|
||||
|
||||
class WhyUsItem(Base):
|
||||
__tablename__ = "why_us"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str] = mapped_column(String(256))
|
||||
desc: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
|
||||
class BeforeAfter(Base):
|
||||
__tablename__ = "before_after"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str] = mapped_column(String(256), default="")
|
||||
lead: Mapped[str] = mapped_column(Text, default="")
|
||||
before: Mapped[str] = mapped_column(Text, default="")
|
||||
after: Mapped[str] = mapped_column(Text, default="")
|
||||
before_label: Mapped[str] = mapped_column(String(128), default="")
|
||||
after_label: Mapped[str] = mapped_column(String(128), default="")
|
||||
|
||||
|
||||
class ContactMessage(Base):
|
||||
__tablename__ = "contact_messages"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128))
|
||||
phone: Mapped[str] = mapped_column(String(32))
|
||||
note: Mapped[str] = mapped_column(Text, default="")
|
||||
is_read: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
|
||||
|
||||
class Subscriber(Base):
|
||||
__tablename__ = "subscribers"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
email: Mapped[str] = mapped_column(String(256), unique=True, index=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
@@ -0,0 +1,246 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||||
|
||||
# ---------- پایه ----------
|
||||
|
||||
|
||||
class ORMModel(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# ---------- خروجیها ----------
|
||||
|
||||
|
||||
class BrandOut(ORMModel):
|
||||
id: int
|
||||
name: str
|
||||
logo: str
|
||||
hero: str
|
||||
contact_bg: str
|
||||
phone: str
|
||||
phone_href: str
|
||||
email: str
|
||||
address: str
|
||||
|
||||
|
||||
class ServiceOut(ORMModel):
|
||||
id: int
|
||||
title: str
|
||||
desc: str
|
||||
icon: str
|
||||
|
||||
|
||||
class ProjectOut(ORMModel):
|
||||
id: int
|
||||
slug: str
|
||||
title: str
|
||||
location: str
|
||||
year: str
|
||||
status: str
|
||||
image: str
|
||||
gallery: list[str]
|
||||
area: str
|
||||
floors: str
|
||||
units: str
|
||||
client: str
|
||||
summary: str
|
||||
body: list[str]
|
||||
|
||||
|
||||
class ArticleOut(ORMModel):
|
||||
id: int
|
||||
slug: str
|
||||
title: str
|
||||
date: str
|
||||
category: str
|
||||
image: str
|
||||
read: str
|
||||
excerpt: str
|
||||
body: list[str]
|
||||
|
||||
|
||||
class TestimonialOut(ORMModel):
|
||||
id: int
|
||||
name: str
|
||||
role: str
|
||||
quote: str
|
||||
|
||||
|
||||
class FaqOut(ORMModel):
|
||||
id: int
|
||||
question: str
|
||||
answer: str
|
||||
|
||||
|
||||
class TeamMemberOut(ORMModel):
|
||||
id: int
|
||||
name: str
|
||||
role: str
|
||||
bio: str
|
||||
|
||||
|
||||
class TimelineItemOut(ORMModel):
|
||||
id: int
|
||||
year: str
|
||||
title: str
|
||||
desc: str
|
||||
image: str
|
||||
|
||||
|
||||
class StatOut(ORMModel):
|
||||
id: int
|
||||
value: int
|
||||
suffix: str
|
||||
label: str
|
||||
|
||||
|
||||
class WhyUsItemOut(ORMModel):
|
||||
id: int
|
||||
title: str
|
||||
desc: str
|
||||
|
||||
|
||||
class BeforeAfterOut(ORMModel):
|
||||
id: int
|
||||
title: str
|
||||
lead: str
|
||||
before: str
|
||||
after: str
|
||||
before_label: str
|
||||
after_label: str
|
||||
|
||||
|
||||
class ContactMessageOut(ORMModel):
|
||||
id: int
|
||||
name: str
|
||||
phone: str
|
||||
note: str
|
||||
is_read: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class SubscriberOut(ORMModel):
|
||||
id: int
|
||||
email: str
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
# ---------- ورودی فرمها ----------
|
||||
|
||||
|
||||
class ContactIn(BaseModel):
|
||||
name: str = Field(min_length=3, max_length=128)
|
||||
phone: str = Field(pattern=r"^[\d۰-۹+\-\s]{8,15}$")
|
||||
note: str = Field(default="", max_length=2000)
|
||||
|
||||
|
||||
class NewsletterIn(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
|
||||
# ---------- ورودیهای پنل مدیریت ----------
|
||||
|
||||
|
||||
class LoginIn(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class TokenOut(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
|
||||
class BrandIn(BaseModel):
|
||||
name: str = ""
|
||||
logo: str = ""
|
||||
hero: str = ""
|
||||
contact_bg: str = ""
|
||||
phone: str = ""
|
||||
phone_href: str = ""
|
||||
email: str = ""
|
||||
address: str = ""
|
||||
|
||||
|
||||
class ServiceIn(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=256)
|
||||
desc: str = ""
|
||||
icon: str = "draft"
|
||||
|
||||
|
||||
class ProjectIn(BaseModel):
|
||||
slug: str = Field(min_length=1, max_length=128)
|
||||
title: str = Field(min_length=1, max_length=256)
|
||||
location: str = ""
|
||||
year: str = ""
|
||||
status: Literal["done", "progress"] = "done"
|
||||
image: str = ""
|
||||
gallery: list[str] = []
|
||||
area: str = ""
|
||||
floors: str = ""
|
||||
units: str = ""
|
||||
client: str = ""
|
||||
summary: str = ""
|
||||
body: list[str] = []
|
||||
|
||||
|
||||
class ArticleIn(BaseModel):
|
||||
slug: str = Field(min_length=1, max_length=128)
|
||||
title: str = Field(min_length=1, max_length=256)
|
||||
date: str = ""
|
||||
category: str = ""
|
||||
image: str = ""
|
||||
read: str = ""
|
||||
excerpt: str = ""
|
||||
body: list[str] = []
|
||||
|
||||
|
||||
class TestimonialIn(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=128)
|
||||
role: str = ""
|
||||
quote: str = ""
|
||||
|
||||
|
||||
class FaqIn(BaseModel):
|
||||
question: str = Field(min_length=1)
|
||||
answer: str = ""
|
||||
|
||||
|
||||
class TeamMemberIn(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=128)
|
||||
role: str = ""
|
||||
bio: str = ""
|
||||
|
||||
|
||||
class TimelineItemIn(BaseModel):
|
||||
year: str = Field(min_length=1, max_length=32)
|
||||
title: str = Field(min_length=1, max_length=256)
|
||||
desc: str = ""
|
||||
image: str = ""
|
||||
|
||||
|
||||
class StatIn(BaseModel):
|
||||
value: int
|
||||
suffix: str = ""
|
||||
label: str = Field(min_length=1, max_length=128)
|
||||
|
||||
|
||||
class WhyUsItemIn(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=256)
|
||||
desc: str = ""
|
||||
|
||||
|
||||
class BeforeAfterIn(BaseModel):
|
||||
title: str = ""
|
||||
lead: str = ""
|
||||
before: str = ""
|
||||
after: str = ""
|
||||
before_label: str = ""
|
||||
after_label: str = ""
|
||||
|
||||
|
||||
class MessageReadIn(BaseModel):
|
||||
is_read: bool = True
|
||||
@@ -0,0 +1,84 @@
|
||||
"""اسکریپت seed دیتابیس.
|
||||
|
||||
اجرا:
|
||||
python -m app.seed # فقط اگر دیتابیس خالی باشد
|
||||
python -m app.seed --force # حذف کامل دادهها و ساخت مجدد
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
from . import models, seed_data
|
||||
from .auth import hash_password
|
||||
from .config import settings
|
||||
from .database import Base, SessionLocal, engine
|
||||
|
||||
|
||||
def seed(force: bool = False) -> None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
if force:
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
print("جداول قبلی حذف شدند.")
|
||||
|
||||
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))
|
||||
|
||||
if db.query(models.AdminUser).count() == 0:
|
||||
db.add(
|
||||
models.AdminUser(
|
||||
username=settings.admin_username,
|
||||
password_hash=hash_password(settings.admin_password),
|
||||
)
|
||||
)
|
||||
print(f"کاربر ادمین ساخته شد: {settings.admin_username}")
|
||||
|
||||
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} رکورد")
|
||||
print("✔ seed با موفقیت انجام شد.")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Seed کردن دیتابیس آرامش پلازا")
|
||||
parser.add_argument("--force", action="store_true", help="حذف همه جداول و ساخت مجدد")
|
||||
args = parser.parse_args()
|
||||
seed(force=args.force)
|
||||
@@ -0,0 +1,291 @@
|
||||
"""دادههای اولیه سایت آرامش پلازا — برگرفته از فایل src/data/site.ts فرانتاند."""
|
||||
|
||||
BRAND = {
|
||||
"name": "آرامش پلازا",
|
||||
"logo": "https://i.postimg.cc/XYNrsHsM/Chat-GPT-Image-Aug-6-2026-04-34-46-PM.png",
|
||||
"hero": "https://i.postimg.cc/J0bKgQVg/Gemini-Generated-Image-mdh969mdh969mdh9.png",
|
||||
"contact_bg": "https://i.postimg.cc/BbccNQHr/Gemini-Generated-Image-dafc2hdafc2hdafc.png",
|
||||
"phone": "۰۲۱ ۸۸ ۷۴ ۲۲ ۱۰",
|
||||
"phone_href": "tel:+982188742210",
|
||||
"email": "info@arameshplaza.com",
|
||||
"address": "تهران، خیابان ولیعصر، برج آرامش، طبقه هجدهم",
|
||||
}
|
||||
|
||||
IMAGES = [
|
||||
"https://i.postimg.cc/wTQ0QFkG/Gemini-Generated-Image-1tjszd1tjszd1tjs.png",
|
||||
"https://i.postimg.cc/1zfBPS1G/Gemini-Generated-Image-at69yhat69yhat69.png",
|
||||
"https://i.postimg.cc/BbccNQHr/Gemini-Generated-Image-dafc2hdafc2hdafc.png",
|
||||
"https://i.postimg.cc/4xFvW0HX/Gemini-Generated-Image-n7mhonn7mhonn7mh.png",
|
||||
"https://i.postimg.cc/N036cr4s/Gemini-Generated-Image-ytbhvvytbhvvytbh.png",
|
||||
]
|
||||
|
||||
SERVICES = [
|
||||
{"title": "طراحی معماری", "desc": "خلق فرمهای معاصر با نگاه ایرانی؛ از کانسپت اولیه تا نقشههای اجرایی و جزئیات نما.", "icon": "draft"},
|
||||
{"title": "ساخت برجهای مسکونی", "desc": "اجرای برجهای بلندمرتبه با استانداردهای سازهای بینالمللی و کیفیت تحویل بینقص.", "icon": "tower"},
|
||||
{"title": "مجتمعهای تجاری", "desc": "طراحی و ساخت مراکز تجاری با مطالعه دقیق جریان مخاطب و بازدهی بلندمدت سرمایه.", "icon": "mall"},
|
||||
{"title": "ویلاسازی", "desc": "ویلاهای اختصاصی همنشین با طبیعت؛ محرمانگی، نور طبیعی و مصالح ماندگار.", "icon": "villa"},
|
||||
{"title": "بازسازی", "desc": "بازآفرینی ساختمانهای موجود با ارتقای سازه، نما و تجربه زندگی در فضا.", "icon": "renew"},
|
||||
{"title": "مدیریت پروژه", "desc": "کنترل همزمان زمان، هزینه و کیفیت با گزارشدهی شفاف در تمام مراحل ساخت.", "icon": "manage"},
|
||||
{"title": "سرمایهگذاری ساختمانی", "desc": "ساختارهای مشارکت شفاف، ارزیابی ریسک و طراحی سبد ملکی متناسب با سرمایه شما.", "icon": "invest"},
|
||||
{"title": "محوطهسازی و لنداسکیپ", "desc": "طراحی حیاط، بام سبز و مسیرهای دسترسی؛ ادامه معماری در فضای باز پروژه.", "icon": "landscape"},
|
||||
]
|
||||
|
||||
STATS = [
|
||||
{"value": 21, "suffix": "+", "label": "سال تجربه ساخت"},
|
||||
{"value": 47, "suffix": "", "label": "پروژه تحویلشده"},
|
||||
{"value": 860, "suffix": "هزار", "label": "مترمربع زیربنا"},
|
||||
{"value": 1200, "suffix": "+", "label": "خانواده ساکن"},
|
||||
]
|
||||
|
||||
PROJECTS = [
|
||||
{
|
||||
"slug": "aramesh-elahieh",
|
||||
"title": "برج آرامش الهیه",
|
||||
"location": "تهران، الهیه",
|
||||
"year": "۱۴۰۲",
|
||||
"status": "done",
|
||||
"image": IMAGES[0],
|
||||
"gallery": [IMAGES[0], IMAGES[3], IMAGES[1]],
|
||||
"area": "۲۴٬۰۰۰ متر مربع",
|
||||
"floors": "۱۸ طبقه",
|
||||
"units": "۵۶ واحد",
|
||||
"client": "سرمایهگذاری خصوصی",
|
||||
"summary": "یک برج مسکونی لوکس با نمای سنگ تراورتن و پوسته دوپوستهای که مصرف انرژی را تا ۳۵ درصد کاهش داده است.",
|
||||
"body": [
|
||||
"برج آرامش الهیه روی زمینی با شیب طبیعی شمالی طراحی شد؛ همین شیب فرصت داد تا لابی، استخر و فضاهای مشترک در سه تراز مختلف بنشینند و همه واحدها دید بیواسطه به شهر داشته باشند.",
|
||||
"پوسته بیرونی ترکیبی از سنگ تراورتن روشن و پنجرههای قدی با شیشه سهجداره است. سایهبانهای عمودی مسیرنگ، نور مستقیم تابستان را میشکنند و در زمستان اجازه ورود کامل نور را میدهند.",
|
||||
"اجرای پروژه در سیودو ماه به پایان رسید و تمام واحدها پیش از تحویل کلید به فروش رسیدند.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"slug": "parseh-mall",
|
||||
"title": "مجتمع تجاری پارسه",
|
||||
"location": "اصفهان، مرکز شهر",
|
||||
"year": "۱۴۰۱",
|
||||
"status": "done",
|
||||
"image": IMAGES[1],
|
||||
"gallery": [IMAGES[1], IMAGES[2], IMAGES[4]],
|
||||
"area": "۳۸٬۵۰۰ متر مربع",
|
||||
"floors": "۷ طبقه",
|
||||
"units": "۱۴۰ واحد تجاری",
|
||||
"client": "گروه سرمایهگذاری پارسه",
|
||||
"summary": "مرکز تجاری با آتریوم مرکزی و نورگیر سقفی که جریان مخاطب را بهصورت طبیعی در همه ترازها توزیع میکند.",
|
||||
"body": [
|
||||
"مطالعه رفتار مخاطب پیش از طراحی نشان داد کوتاهترین مسیر عمودی، کلید موفقیت تجاری این مجموعه است؛ بنابراین آتریوم مرکزی با پلهبرقیهای قطری در قلب پلان قرار گرفت.",
|
||||
"نورگیر سقفی با شیشه لو-ای، روشنایی روز را تا تراز منفی دو میرساند و مصرف روشنایی مصنوعی را بهطور محسوس کاهش میدهد.",
|
||||
"مجموعه دو ماه پیش از موعد قراردادی به بهرهبرداری رسید.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"slug": "daryakenar-villas",
|
||||
"title": "ویلاهای دریاکنار",
|
||||
"location": "مازندران، نوشهر",
|
||||
"year": "۱۴۰۰",
|
||||
"status": "done",
|
||||
"image": IMAGES[2],
|
||||
"gallery": [IMAGES[2], IMAGES[4], IMAGES[0]],
|
||||
"area": "۹٬۲۰۰ متر مربع",
|
||||
"floors": "۲ طبقه",
|
||||
"units": "۱۲ ویلا",
|
||||
"client": "مجموعه خصوصی دریاکنار",
|
||||
"summary": "دوازده ویلای اختصاصی با چیدمان شانهای که برای هر واحد، دید مستقیم به دریا و محرمانگی کامل فراهم میکند.",
|
||||
"body": [
|
||||
"چیدمان شانهای ویلاها نتیجه دهها بررسی زاویه دید بود؛ هیچ ویلایی در دید ویلای دیگر نیست و همه به دریا باز میشوند.",
|
||||
"مصالح، پاسخی به اقلیم مرطوب شمال است: چوب ترموود، بتن اکسپوز و پوششهای ضد نمک برای فلزات نما.",
|
||||
"محوطه با گیاهان بومی طراحی شد تا نیاز آبیاری در طول سال حداقل باشد.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"slug": "niavaran-twins",
|
||||
"title": "برج دوقلوی نیاوران",
|
||||
"location": "تهران، نیاوران",
|
||||
"year": "۱۳۹۹",
|
||||
"status": "done",
|
||||
"image": IMAGES[3],
|
||||
"gallery": [IMAGES[3], IMAGES[0], IMAGES[2]],
|
||||
"area": "۳۱٬۷۰۰ متر مربع",
|
||||
"floors": "۲۰ طبقه",
|
||||
"units": "۸۰ واحد",
|
||||
"client": "مشارکت با مالکان زمین",
|
||||
"summary": "دو برج همسان با پادست مشترک که فضاهای رفاهی و پارکینگها را در یک تراز واحد ادغام میکند.",
|
||||
"body": [
|
||||
"دو برج روی یک پادست مشترک نشستهاند؛ این تصمیم اجازه داد فضاهای رفاهی، سالن اجتماعات و پارکینگها بین دو ساختمان مشترک شوند و سطح اشغال باغ کاهش یابد.",
|
||||
"سازه با سیستم دیوار برشی و دال پسکشیده اجرا شد تا دهانههای داخلی آزاد و پلانها قابل شخصیسازی باشند.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"slug": "kish-garden-tower",
|
||||
"title": "باغبرج کیش",
|
||||
"location": "کیش، مروارید",
|
||||
"year": "۱۴۰۵ (پیشبینی)",
|
||||
"status": "progress",
|
||||
"image": IMAGES[4],
|
||||
"gallery": [IMAGES[4], IMAGES[1], IMAGES[3]],
|
||||
"area": "۱۸٬۴۰۰ متر مربع",
|
||||
"floors": "۱۴ طبقه",
|
||||
"units": "۶۲ واحد اقامتی",
|
||||
"client": "صندوق پروژهمحور آرامش",
|
||||
"summary": "باغبرجی اقامتی با تراسهای سبز پیوسته که در حال ساخت است؛ پیشرفت فیزیکی پروژه ۴۸ درصد.",
|
||||
"body": [
|
||||
"تراسهای سبز پیوسته، پوستهای زنده برای ساختمان میسازند که در اقلیم گرم جزیره، دمای سطح نما را بهطور محسوس پایین میآورد.",
|
||||
"در حال حاضر سازه تا تراز طبقه نهم اجرا شده و نصب نما از ضلع جنوبی آغاز شده است.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"slug": "aramesh-business-center",
|
||||
"title": "مرکز تجاری آرامش تهران",
|
||||
"location": "تهران، سعادتآباد",
|
||||
"year": "۱۴۰۶ (پیشبینی)",
|
||||
"status": "progress",
|
||||
"image": IMAGES[1],
|
||||
"gallery": [IMAGES[1], IMAGES[3], IMAGES[4]],
|
||||
"area": "۲۷٬۳۰۰ متر مربع",
|
||||
"floors": "۱۲ طبقه",
|
||||
"units": "۹۰ دفتر اداری",
|
||||
"client": "مشارکت با هلدینگ آرامش",
|
||||
"summary": "مجتمع اداری-تجاری با هسته مرکزی شفاف و لابی دوبلکس؛ در مرحله اجرای سفتکاری.",
|
||||
"body": [
|
||||
"برنامهریزی فضایی این مجموعه بر اساس دفاتر انعطافپذیر انجام شد؛ هر تراز میتواند به دو، سه یا شش دفتر مستقل تقسیم شود.",
|
||||
"اجرای تأسیسات مکانیکی بهصورت همزمان با سفتکاری پیش میرود تا زمان کل پروژه کوتاهتر شود.",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
BEFORE_AFTER = {
|
||||
"title": "از کانسپت تا خلق واقعیت",
|
||||
"lead": "دستگیره را بکشید و ببینید نمای ساختمانی چگونه از حالت اولیه به فرم مدرن تبدیل میشود.",
|
||||
"before": "https://cdn.housedesign.ai/se/housedesign/assets/housedesign-home/compare/1_before.webp",
|
||||
"after": "https://cdn.housedesign.ai/se/housedesign/assets/housedesign-home/compare/1_after.webp",
|
||||
"before_label": "کانسپت اولیه",
|
||||
"after_label": "پروژه تکمیلشده",
|
||||
}
|
||||
|
||||
TIMELINE = [
|
||||
{"year": "۱۳۸۳", "title": "آغاز مسیر", "desc": "تأسیس دفتر معماری آرامش با تیمی چهارنفره و نخستین پروژه بازسازی در تهران.", "image": IMAGES[2]},
|
||||
{"year": "۱۳۸۵", "title": "نخستین ساخت مستقل", "desc": "تحویل اولین ساختمان مسکونی هفتطبقه؛ آغاز همکاری با پیمانکاران سازهای بزرگ.", "image": IMAGES[0]},
|
||||
{"year": "۱۳۸۷", "title": "ورود به بازار تجاری", "desc": "طراحی و اجرای اولین مجتمع تجاری با رویکرد بازدهی سرمایه و جریان مخاطب.", "image": IMAGES[1]},
|
||||
{"year": "۱۳۸۹", "title": "استاندارد بینالمللی", "desc": "استقرار نظام کنترل کیفیت و دریافت گواهینامههای مدیریت پروژه.", "image": IMAGES[4]},
|
||||
{"year": "۱۳۹۵", "title": "بلندمرتبهسازی", "desc": "آغاز ساخت نخستین برج بیستطبقه شرکت در شمال تهران.", "image": IMAGES[3]},
|
||||
{"year": "۱۴۰۳", "title": "آرامش پلازا امروز", "desc": "پرتفوی ۴۷ پروژه، دفاتر در سه شهر و صندوق سرمایهگذاری ساختمانی اختصاصی.", "image": IMAGES[0]},
|
||||
]
|
||||
|
||||
WHY = [
|
||||
{"title": "کیفیت بدون مصالحه", "desc": "انتخاب مصالح و مجریان بر اساس ماندگاری چنددههای، نه سریعترین تحویل."},
|
||||
{"title": "شفافیت مالی کامل", "desc": "گزارش ماهانه هزینهها و پیشرفت فیزیکی برای همه شرکای پروژه."},
|
||||
{"title": "تحویل بهموقع", "desc": "۹۴٪ پروژههای ما در بازه زمانی قراردادی یا زودتر تحویل داده شدهاند."},
|
||||
{"title": "طراحی اختصاصی", "desc": "هیچ نقشهای دو بار اجرا نمیشود؛ هر پروژه هویت معماری خودش را دارد."},
|
||||
{"title": "خدمات پس از تحویل", "desc": "پنج سال پشتیبانی فنی و نگهداری تأسیسات پس از تحویل کلید."},
|
||||
{"title": "پایداری و انرژی", "desc": "طراحی پوسته و تأسیسات با هدف کاهش مصرف انرژی تا ۳۵ درصد."},
|
||||
]
|
||||
|
||||
TESTIMONIALS = [
|
||||
{"name": "مهندس سارا کیانی", "role": "سرمایهگذار پروژه پارسه", "quote": "گزارشهای ماهانه آرامش پلازا آنقدر دقیق بود که هیچوقت نیاز نشد نگران سرمایهام باشم. پروژه دو ماه زودتر از موعد به بهرهبرداری رسید."},
|
||||
{"name": "دکتر امیرحسین راد", "role": "ساکن برج الهیه", "quote": "کیفیت اجرا در جزئیاتی دیده میشود که معمولاً کسی به آنها فکر نمیکند؛ از عایق صوتی پنجرهها تا نور غیرمستقیم راهروها."},
|
||||
{"name": "مریم دلاور", "role": "مدیر مجموعه ویلاهای دریاکنار", "quote": "همکاری با تیم طراحی، تجربهای واقعاً حرفهای بود. ایدههای ما را شنیدند و به زبان معماری ترجمه کردند."},
|
||||
]
|
||||
|
||||
TEAM = [
|
||||
{"name": "کامران آرامش", "role": "بنیانگذار و مدیرعامل", "bio": "بیش از سه دهه تجربه در ساخت بلندمرتبه."},
|
||||
{"name": "نگار شریفی", "role": "مدیر ارشد طراحی", "bio": "معمار ارشد، فارغالتحصیل دانشگاه تهران و میلان."},
|
||||
{"name": "بهزاد مرادی", "role": "مدیر اجرایی پروژهها", "bio": "مسئول کنترل کیفیت و زمانبندی همه کارگاهها."},
|
||||
{"name": "الهام نیکآیین", "role": "مدیر سرمایهگذاری", "bio": "طراح ساختارهای مشارکت و تحلیل بازدهی ملکی."},
|
||||
]
|
||||
|
||||
ARTICLES = [
|
||||
{
|
||||
"slug": "natural-light",
|
||||
"title": "معماری آرام: چرا نور طبیعی گرانترین مصالح ساختمان است؟",
|
||||
"date": "۲۴ مرداد ۱۴۰۳",
|
||||
"category": "معماری",
|
||||
"image": IMAGES[1],
|
||||
"read": "۶ دقیقه",
|
||||
"excerpt": "نور طبیعی نهتنها کیفیت زندگی در فضا را تعیین میکند، بلکه مستقیماً بر مصرف انرژی و ارزش بلندمدت ملک اثر میگذارد.",
|
||||
"body": [
|
||||
"در بازار ساختوساز، ارزش یک واحد اغلب با متراژ سنجیده میشود؛ اما تجربه ما در بیست سال گذشته نشان داده کیفیت نور، تعیینکنندهتر از چند متر اضافه است.",
|
||||
"جهتگیری ساختمان، عمق دهانه و ارتفاع کف تا سقف، سه متغیر اصلی نور طبیعی هستند. تصمیم درباره هر سه، پیش از ترسیم اولین پلان گرفته میشود و بعد از آن تقریباً برگشتپذیر نیست.",
|
||||
"در برج آرامش الهیه، چرخش ۱۲ درجهای توده ساختمان نسبت به شمال، دو ساعت نور مفید بیشتر در روزهای زمستان به واحدها اضافه کرد؛ تصمیمی بیهزینه با اثری چنددههای.",
|
||||
"برای انتخاب یک واحد، پیشنهاد ما ساده است: بازدید را در ساعتهای میانی صبح و اواخر بعدازظهر انجام دهید و کیفیت نور را در همان ساعتهایی بسنجید که واقعاً در خانه هستید.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"slug": "real-estate-portfolio",
|
||||
"title": "راهنمای انتخاب سبد سرمایهگذاری ملکی در بازار امروز",
|
||||
"date": "۱۰ مرداد ۱۴۰۳",
|
||||
"category": "سرمایهگذاری",
|
||||
"image": IMAGES[3],
|
||||
"read": "۹ دقیقه",
|
||||
"excerpt": "سه معیار کلیدی برای تشکیل سبدی متعادل از دارایی ملکی، بر اساس افق زمانی و سطح ریسک شما.",
|
||||
"body": [
|
||||
"سبد ملکی متعادل، ترکیبی از داراییهای با نقدشوندگی متفاوت است؛ نه همه سرمایه در یک پروژه بلندمدت، نه همه در واحدهای آماده.",
|
||||
"افق زمانی، نخستین معیار است. اگر برنامه شما کمتر از دو سال است، پیشخرید در پروژهای که تازه شروع شده انتخاب پرریسکی خواهد بود.",
|
||||
"معیار دوم شفافیت گزارشدهی است. بدون گزارش ماهانه پیشرفت فیزیکی و مالی، ارزیابی وضعیت سرمایه شما عملاً غیرممکن است.",
|
||||
"معیار سوم، تنوع کاربری است. ترکیب مسکونی و تجاری، نوسان بازدهی سبد را در دورههای رکود محسوس کاهش میدهد.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"slug": "facade-quality",
|
||||
"title": "پنج نکته فنی که کیفیت نما را پس از ده سال مشخص میکند",
|
||||
"date": "۲۸ تیر ۱۴۰۳",
|
||||
"category": "اجرا",
|
||||
"image": IMAGES[4],
|
||||
"read": "۵ دقیقه",
|
||||
"excerpt": "نمای ساختمان در سال اول زیبا به نظر میرسد؛ تفاوت کیفیت اجرا در سال دهم آشکار میشود.",
|
||||
"body": [
|
||||
"اتصالات مکانیکی سنگ نما، مهمتر از خود سنگ است. اتصال چسبی در اقلیم سردوگرم ایران عمر کوتاهی دارد.",
|
||||
"درزهای انقباضی باید در نقشه دیده شوند، نه در کارگاه تصمیمگیری شوند؛ نبودشان اولین دلیل ترکهای افقی نما است.",
|
||||
"آببندی دور پنجره، پرتکرارترین نقطه شکست است. اجرای نوار آببند پیش از نصب قرنیز، این ریسک را حذف میکند.",
|
||||
"زیرسازی گالوانیزه گرم، در برابر زیرسازی رنگشده، تفاوت قیمت اندکی دارد و تفاوت عمر بسیار زیادی.",
|
||||
"در پایان، برنامه بازدید دورهای سالانه نما را جدی بگیرید؛ ترمیم یک درز، هزینهای در حد هیچ در برابر بازسازی کل نما است.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"slug": "commercial-yield",
|
||||
"title": "چگونه بازدهی یک پروژه تجاری را پیش از ساخت تخمین بزنیم؟",
|
||||
"date": "۱۵ تیر ۱۴۰۳",
|
||||
"category": "سرمایهگذاری",
|
||||
"image": IMAGES[2],
|
||||
"read": "۷ دقیقه",
|
||||
"excerpt": "مدل سادهای برای برآورد بازدهی، بر پایه جریان مخاطب، ترکیب واحدها و نرخ اجاره منطقه.",
|
||||
"body": [
|
||||
"برآورد بازدهی تجاری با سه عدد آغاز میشود: جریان روزانه مخاطب، نرخ اجاره هر مترمربع در منطقه و نسبت سطح مفید به کل.",
|
||||
"خطای رایج، نادیدهگرفتن ترازهای بالاتر است. بدون طراحی درست مسیر عمودی، ترازهای سه به بالا معمولاً نیمی از نرخ تراز همکف را هم بهدست نمیآورند.",
|
||||
"در نهایت، سناریوی محافظهکارانه را هم بنویسید؛ اگر پروژه در سناریوی بدبینانه هم توجیه داشت، سرمایهگذاری منطقی است.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"slug": "facade-materials",
|
||||
"title": "مصالح نما: مقایسه سنگ، کامپوزیت و ترموود در اقلیم ایران",
|
||||
"date": "۲ تیر ۱۴۰۳",
|
||||
"category": "اجرا",
|
||||
"image": IMAGES[0],
|
||||
"read": "۸ دقیقه",
|
||||
"excerpt": "هر مصالح نما در اقلیم متفاوتی درست کار میکند؛ انتخاب اشتباه، هزینه نگهداری را چند برابر میکند.",
|
||||
"body": [
|
||||
"سنگ در اقلیم خشک، بهترین ماندگاری را دارد؛ اما وزن آن نیازمند زیرسازی دقیق و محاسبه سازهای است.",
|
||||
"کامپوزیت انتخاب سریع و سبکی است، مشروط بر انتخاب گرید ضد آتش و درنظرگرفتن انقباض حرارتی در طول ورق.",
|
||||
"ترموود در اقلیم مرطوب شمال، بهترین پاسخ زیباییشناسانه است، ولی برنامه نگهداری دوسالانه دارد.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"slug": "renovate-or-rebuild",
|
||||
"title": "بازسازی یا نوسازی؟ معیارهای تصمیمگیری برای مالکان",
|
||||
"date": "۲۰ خرداد ۱۴۰۳",
|
||||
"category": "معماری",
|
||||
"image": IMAGES[1],
|
||||
"read": "۶ دقیقه",
|
||||
"excerpt": "اگر عمر سازه و ضوابط شهری اجازه دهند، بازسازی میتواند بازدهی بالاتری از تخریب کامل داشته باشد.",
|
||||
"body": [
|
||||
"نخست وضعیت سازه ارزیابی میشود؛ اگر مقاومسازی هزینهای بیش از ۴۰ درصد ساخت نو دارد، نوسازی منطقیتر است.",
|
||||
"دومین معیار، ضوابط شهری روز است. گاهی تخریب، سطح زیربنای مجاز کمتری به شما میدهد و این تصمیم را کاملاً تغییر میدهد.",
|
||||
"در بازسازی، بیشترین ارزش افزوده از سه جا میآید: نما، ورودی و بازآرایی پلان داخلی.",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
FAQS = [
|
||||
{"question": "حداقل سرمایه برای مشارکت در پروژههای آرامش پلازا چقدر است؟", "answer": "ساختار مشارکت هر پروژه متفاوت است، اما معمولاً واحدهای سرمایهگذاری از سطح مشخصی آغاز میشوند و جزئیات دقیق در جلسه ارزیابی اولیه ارائه میشود."},
|
||||
{"question": "مدت زمان معمول ساخت یک برج مسکونی چقدر است؟", "answer": "بسته به تعداد طبقات و شرایط زمین، میانگین اجرای یک برج مسکونی بین ۲۸ تا ۴۲ ماه از زمان صدور پروانه تا تحویل کلید است."},
|
||||
{"question": "آیا امکان تغییر نقشه داخلی واحد وجود دارد؟", "answer": "بله. تا پیش از پایان مرحله سفتکاری، امکان شخصیسازی پلان داخلی با هماهنگی تیم طراحی و تأیید سازه فراهم است."},
|
||||
{"question": "خدمات پس از تحویل شامل چه مواردی میشود؟", "answer": "پنج سال پشتیبانی فنی تأسیسات مکانیکی و برقی، بازدید دورهای سالانه و تضمین اجرای نما و عایقکاری."},
|
||||
{"question": "پروژهها در چه شهرهایی اجرا میشوند؟", "answer": "در حال حاضر تهران، اصفهان و سواحل شمالی کشور مناطق اصلی فعالیت ما هستند و پروژهای نیز در جزیره کیش در دست ساخت است."},
|
||||
]
|
||||
Reference in New Issue
Block a user