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:
co-authored by
Cursor
parent
c962fe524b
commit
d9bdfbbb12
@@ -1200,6 +1200,7 @@ const en = {
|
|||||||
'blog.form.uploadTitleImage': 'Upload title image',
|
'blog.form.uploadTitleImage': 'Upload title image',
|
||||||
'blog.form.changeTitleImage': 'Change title image',
|
'blog.form.changeTitleImage': 'Change title image',
|
||||||
'blog.form.cropHint': 'Click to select, then crop',
|
'blog.form.cropHint': 'Click to select, then crop',
|
||||||
|
'blog.form.aspectAria': 'Title image aspect ratio',
|
||||||
'blog.form.type': 'Type',
|
'blog.form.type': 'Type',
|
||||||
'blog.form.category': 'Category',
|
'blog.form.category': 'Category',
|
||||||
'blog.form.categoryPlaceholder': 'Search and select category...',
|
'blog.form.categoryPlaceholder': 'Search and select category...',
|
||||||
@@ -3512,6 +3513,7 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'blog.form.uploadTitleImage': 'آپلود تصویر عنوان',
|
'blog.form.uploadTitleImage': 'آپلود تصویر عنوان',
|
||||||
'blog.form.changeTitleImage': 'تغییر تصویر عنوان',
|
'blog.form.changeTitleImage': 'تغییر تصویر عنوان',
|
||||||
'blog.form.cropHint': 'برای انتخاب کلیک کنید، سپس برش دهید',
|
'blog.form.cropHint': 'برای انتخاب کلیک کنید، سپس برش دهید',
|
||||||
|
'blog.form.aspectAria': 'نسبت تصویر عنوان',
|
||||||
'blog.form.type': 'نوع',
|
'blog.form.type': 'نوع',
|
||||||
'blog.form.category': 'دستهبندی',
|
'blog.form.category': 'دستهبندی',
|
||||||
'blog.form.categoryPlaceholder': 'جستجو و انتخاب دستهبندی...',
|
'blog.form.categoryPlaceholder': 'جستجو و انتخاب دستهبندی...',
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ import { flattenCategories } from '../utils/categories'
|
|||||||
import pageStyles from '../components/PageContent.module.css'
|
import pageStyles from '../components/PageContent.module.css'
|
||||||
import styles from './AddNewProductPage.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() {
|
export function AddNewBlogPage() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -43,6 +50,7 @@ export function AddNewBlogPage() {
|
|||||||
const [abstract, setAbstract] = useState('')
|
const [abstract, setAbstract] = useState('')
|
||||||
const [mainTextHtml, setMainTextHtml] = useState('')
|
const [mainTextHtml, setMainTextHtml] = useState('')
|
||||||
const [titleImage, setTitleImage] = useState<string | null>(null)
|
const [titleImage, setTitleImage] = useState<string | null>(null)
|
||||||
|
const [titleImageAspect, setTitleImageAspect] = useState<TitleImageAspectId>('3:2')
|
||||||
const [tags, setTags] = useState<string[]>([])
|
const [tags, setTags] = useState<string[]>([])
|
||||||
const [featuredMediaId, setFeaturedMediaId] = useState<string | null>(null)
|
const [featuredMediaId, setFeaturedMediaId] = useState<string | null>(null)
|
||||||
const [loaded, setLoaded] = useState(!isEdit)
|
const [loaded, setLoaded] = useState(!isEdit)
|
||||||
@@ -50,6 +58,8 @@ export function AddNewBlogPage() {
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const categoryOptions = flattenCategories(categories)
|
const categoryOptions = flattenCategories(categories)
|
||||||
|
const selectedAspect =
|
||||||
|
TITLE_IMAGE_ASPECTS.find((option) => option.id === titleImageAspect)?.aspect ?? 3 / 2
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
@@ -190,16 +200,46 @@ export function AddNewBlogPage() {
|
|||||||
|
|
||||||
<form className={styles.form} onSubmit={handleSubmit}>
|
<form className={styles.form} onSubmit={handleSubmit}>
|
||||||
<div className={styles.formGrid}>
|
<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>
|
<label>{t('blog.form.titleImage')}</label>
|
||||||
<ImageCropper
|
<ImageCropper
|
||||||
value={titleImage}
|
value={titleImage}
|
||||||
onChange={setTitleImage}
|
onChange={setTitleImage}
|
||||||
aspect={3 / 2}
|
aspect={selectedAspect}
|
||||||
uploadLabel={t('blog.form.uploadTitleImage')}
|
uploadLabel={t('blog.form.uploadTitleImage')}
|
||||||
hint={t('blog.form.cropHint')}
|
hint={t('blog.form.cropHint')}
|
||||||
changeLabel={t('blog.form.changeTitleImage')}
|
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>
|
||||||
|
|
||||||
<div className={`${styles.field} ${styles.col3}`}>
|
<div className={`${styles.field} ${styles.col3}`}>
|
||||||
|
|||||||
@@ -283,6 +283,49 @@
|
|||||||
color: var(--primary);
|
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 {
|
.videoEmbedRow {
|
||||||
grid-column: span 12;
|
grid-column: span 12;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ export function BlogListPage() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className={pageStyles.grid}>
|
<div className={pageStyles.gridCols4}>
|
||||||
{blogs.map((blog) => (
|
{blogs.map((blog) => (
|
||||||
<BlogCard
|
<BlogCard
|
||||||
key={blog.id}
|
key={blog.id}
|
||||||
|
|||||||
Reference in New Issue
Block a user