Show four blog cards per row and let editors crop thumbnails as 1:1 or 3:2.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-01 10:15:08 +03:30
co-authored by Cursor
parent c962fe524b
commit d9bdfbbb12
4 changed files with 88 additions and 3 deletions
+2
View File
@@ -1200,6 +1200,7 @@ const en = {
'blog.form.uploadTitleImage': 'Upload title image',
'blog.form.changeTitleImage': 'Change title image',
'blog.form.cropHint': 'Click to select, then crop',
'blog.form.aspectAria': 'Title image aspect ratio',
'blog.form.type': 'Type',
'blog.form.category': 'Category',
'blog.form.categoryPlaceholder': 'Search and select category...',
@@ -3512,6 +3513,7 @@ const fa: Record<MessageKey, string> = {
'blog.form.uploadTitleImage': 'آپلود تصویر عنوان',
'blog.form.changeTitleImage': 'تغییر تصویر عنوان',
'blog.form.cropHint': 'برای انتخاب کلیک کنید، سپس برش دهید',
'blog.form.aspectAria': 'نسبت تصویر عنوان',
'blog.form.type': 'نوع',
'blog.form.category': 'دسته‌بندی',
'blog.form.categoryPlaceholder': 'جستجو و انتخاب دسته‌بندی...',
+42 -2
View File
@@ -29,6 +29,13 @@ import { flattenCategories } from '../utils/categories'
import pageStyles from '../components/PageContent.module.css'
import styles from './AddNewProductPage.module.css'
type TitleImageAspectId = '1:1' | '3:2'
const TITLE_IMAGE_ASPECTS: { id: TitleImageAspectId; aspect: number }[] = [
{ id: '1:1', aspect: 1 },
{ id: '3:2', aspect: 3 / 2 },
]
export function AddNewBlogPage() {
const { id } = useParams()
const navigate = useNavigate()
@@ -43,6 +50,7 @@ export function AddNewBlogPage() {
const [abstract, setAbstract] = useState('')
const [mainTextHtml, setMainTextHtml] = useState('')
const [titleImage, setTitleImage] = useState<string | null>(null)
const [titleImageAspect, setTitleImageAspect] = useState<TitleImageAspectId>('3:2')
const [tags, setTags] = useState<string[]>([])
const [featuredMediaId, setFeaturedMediaId] = useState<string | null>(null)
const [loaded, setLoaded] = useState(!isEdit)
@@ -50,6 +58,8 @@ export function AddNewBlogPage() {
const [error, setError] = useState('')
const categoryOptions = flattenCategories(categories)
const selectedAspect =
TITLE_IMAGE_ASPECTS.find((option) => option.id === titleImageAspect)?.aspect ?? 3 / 2
useEffect(() => {
const controller = new AbortController()
@@ -190,16 +200,46 @@ export function AddNewBlogPage() {
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.formGrid}>
<div className={`${styles.field} ${styles.col3} ${styles.rowSpan3}`}>
<div className={`${styles.field} ${styles.col3} ${styles.rowSpan3} ${styles.thumbnailField}`}>
<label>{t('blog.form.titleImage')}</label>
<ImageCropper
value={titleImage}
onChange={setTitleImage}
aspect={3 / 2}
aspect={selectedAspect}
uploadLabel={t('blog.form.uploadTitleImage')}
hint={t('blog.form.cropHint')}
changeLabel={t('blog.form.changeTitleImage')}
/>
<div
className={styles.aspectSwitch}
role="radiogroup"
aria-label={t('blog.form.aspectAria')}
>
{TITLE_IMAGE_ASPECTS.map((option) => (
<button
key={option.id}
type="button"
role="radio"
aria-checked={titleImageAspect === option.id}
className={`${styles.aspectSwitchBtn} ${
titleImageAspect === option.id ? styles.aspectSwitchBtnSelected : ''
}`}
onClick={() => {
if (option.id === titleImageAspect) return
setTitleImageAspect(option.id)
// Applied crop is locked to the old ratio — drop it so the user recrops.
// An in-progress crop stays; ImageCropper updates via the aspect prop.
if (titleImage) {
setTitleImage(null)
setFeaturedMediaId(null)
}
}}
disabled={isSubmitting}
>
{option.id}
</button>
))}
</div>
</div>
<div className={`${styles.field} ${styles.col3}`}>
@@ -283,6 +283,49 @@
color: var(--primary);
}
.aspectSwitch {
display: inline-flex;
align-self: center;
margin-top: 4px;
padding: 3px;
gap: 2px;
background: rgba(148, 163, 184, 0.14);
border: 1px solid rgba(148, 163, 184, 0.28);
border-radius: 50px;
}
.aspectSwitchBtn {
min-width: 52px;
padding: 6px 12px;
font-size: 12px;
font-weight: 600;
line-height: 1.2;
font-family: var(--font-en), var(--font-ui);
color: var(--text-secondary);
background: transparent;
border-radius: 50px;
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
}
.aspectSwitchBtn:hover:not(:disabled) {
color: var(--primary);
}
.aspectSwitchBtn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.aspectSwitchBtnSelected {
background: var(--primary);
color: #fff;
box-shadow: 0 1px 3px rgba(var(--primary-rgb) / 0.35);
}
.aspectSwitchBtnSelected:hover:not(:disabled) {
color: #fff;
}
.videoEmbedRow {
grid-column: span 12;
display: grid;
+1 -1
View File
@@ -190,7 +190,7 @@ export function BlogListPage() {
/>
) : (
<>
<div className={pageStyles.grid}>
<div className={pageStyles.gridCols4}>
{blogs.map((blog) => (
<BlogCard
key={blog.id}