feat: update UI to glassmorphic design and copper theme

This commit is contained in:
Behrad
2026-09-08 02:06:32 +03:30
parent 35a558db20
commit ddd986e037
94 changed files with 6973 additions and 959 deletions
+346 -4
View File
@@ -1,7 +1,7 @@
from datetime import datetime
from typing import Literal
from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field
from pydantic import BaseModel, ConfigDict, EmailStr, Field, create_model, field_validator
# ---------- پایه ----------
@@ -58,7 +58,9 @@ class ArticleOut(ORMModel):
image: str
read: str
excerpt: str
body: list[str]
body: dict
status: str
seo_keyphrase: str
class TestimonialOut(ORMModel):
@@ -112,6 +114,144 @@ 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 SeoMetaOut(ORMModel):
id: int
page_key: str
title: str
description: str
class ContactMessageOut(ORMModel):
id: int
name: str
@@ -154,6 +294,11 @@ class TokenOut(BaseModel):
token_type: str = "bearer"
class ChangePasswordIn(BaseModel):
current_password: str
new_password: str = Field(min_length=8, max_length=128)
class BrandIn(BaseModel):
name: str = ""
logo: str = ""
@@ -187,6 +332,18 @@ class ProjectIn(BaseModel):
body: list[str] = []
EMPTY_DOC = {"type": "doc", "content": [{"type": "paragraph"}]}
def validate_doc_body(value: object) -> dict:
"""بدنه مقاله باید یک سند TipTap (ProseMirror) معتبر از نظر ساختار باشد."""
if not value:
return dict(EMPTY_DOC)
if not isinstance(value, dict) or value.get("type") != "doc" or not isinstance(value.get("content"), list):
raise ValueError("body must be a TipTap doc: {type: 'doc', content: [...]}")
return value
class ArticleIn(BaseModel):
slug: str = Field(min_length=1, max_length=128)
title: str = Field(min_length=1, max_length=256)
@@ -195,7 +352,10 @@ class ArticleIn(BaseModel):
image: str = ""
read: str = ""
excerpt: str = ""
body: list[str] = []
body: dict = Field(default_factory=lambda: dict(EMPTY_DOC))
status: Literal["draft", "published"] = "draft"
seo_keyphrase: str = Field(default="", max_length=128)
_validate_body = field_validator("body")(validate_doc_body)
class TestimonialIn(BaseModel):
@@ -242,5 +402,187 @@ 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 SeoMetaIn(BaseModel):
page_key: str = Field(min_length=1, max_length=32)
title: str = ""
description: str = ""
class SeoMetaPatch(BaseModel):
page_key: Optional[str] = None
title: Optional[str] = None
description: Optional[str] = None
class MessageReadIn(BaseModel):
is_read: bool = True
# ---------- اسکیماهای PATCH (ویرایش جزئی — همه فیلدها اختیاری) ----------
def _make_patch(name: str, model_cls: type[BaseModel]) -> type[BaseModel]:
"""از اسکیمای ورودی، اسکیمای PATCH می‌سازد؛ فیلدهای ارسال‌نشده دست‌نخورده می‌مانند."""
fields = {fname: (Optional[f.annotation], None) for fname, f in model_cls.model_fields.items()}
return create_model(name, **fields)
BrandPatch = _make_patch("BrandPatch", BrandIn)
ServicePatch = _make_patch("ServicePatch", ServiceIn)
ProjectPatch = _make_patch("ProjectPatch", ProjectIn)
class ArticlePatch(BaseModel):
slug: Optional[str] = None
title: Optional[str] = None
date: Optional[str] = None
category: Optional[str] = None
image: Optional[str] = None
read: Optional[str] = None
excerpt: Optional[str] = None
body: Optional[dict] = None
status: Optional[Literal["draft", "published"]] = None
seo_keyphrase: Optional[str] = None
_validate_body = field_validator("body")(validate_doc_body)
TestimonialPatch = _make_patch("TestimonialPatch", TestimonialIn)
FaqPatch = _make_patch("FaqPatch", FaqIn)
TeamMemberPatch = _make_patch("TeamMemberPatch", TeamMemberIn)
TimelineItemPatch = _make_patch("TimelineItemPatch", TimelineItemIn)
StatPatch = _make_patch("StatPatch", StatIn)
WhyUsItemPatch = _make_patch("WhyUsItemPatch", WhyUsItemIn)
BeforeAfterPatch = _make_patch("BeforeAfterPatch", BeforeAfterIn)
NavItemPatch = _make_patch("NavItemPatch", NavItemIn)
HeroPatch = _make_patch("HeroPatch", HeroIn)
StoryPatch = _make_patch("StoryPatch", StoryIn)
SectionHeadingPatch = _make_patch("SectionHeadingPatch", SectionHeadingIn)
PageHeroPatch = _make_patch("PageHeroPatch", PageHeroIn)
InvestmentModelPatch = _make_patch("InvestmentModelPatch", InvestmentModelIn)
ProcessStepPatch = _make_patch("ProcessStepPatch", ProcessStepIn)
OfficePatch = _make_patch("OfficePatch", OfficeIn)
GalleryImagePatch = _make_patch("GalleryImagePatch", GalleryImageIn)
StatusLabelPatch = _make_patch("StatusLabelPatch", StatusLabelIn)
ContactSectionPatch = _make_patch("ContactSectionPatch", ContactSectionIn)
NewsletterSectionPatch = _make_patch("NewsletterSectionPatch", NewsletterSectionIn)
FooterPatch = _make_patch("FooterPatch", FooterIn)
class ReorderIn(BaseModel):
"""ترتیب جدید آیتم‌های یک مجموعه بر اساس id."""
ids: list[int]