fix(en): هشت اصلاح سایت انگلیسی — دکمهٔ هیرو با ps منطقی، پیوند درباره ما زیر تیتر، justify متن درباره ما، متای مقاله (نویسنده/تاریخ/زمان مطالعه) دوزبانه، تیتر فهرست مطالب، H1 انگلیسی در ادیتور + چکلیست سئو زبانآگاه، بازگشت به وبلاگ با حفظ زبان، دستهبندی انگلیسی
This commit is contained in:
@@ -327,7 +327,7 @@ export function ArticleWritePage() {
|
|||||||
setDeleting(true)
|
setDeleting(true)
|
||||||
await runSave(async () => {
|
await runSave(async () => {
|
||||||
await adminApi.remove('articles', createdId)
|
await adminApi.remove('articles', createdId)
|
||||||
navigate('/blog')
|
navigate(`${editLang === 'en' ? '/en' : ''}/blog`)
|
||||||
})
|
})
|
||||||
setDeleting(false)
|
setDeleting(false)
|
||||||
}
|
}
|
||||||
@@ -353,7 +353,7 @@ export function ArticleWritePage() {
|
|||||||
<header className="sticky top-0 z-40 border-b border-ivory/10 bg-ink/95 backdrop-blur-md">
|
<header className="sticky top-0 z-40 border-b border-ivory/10 bg-ink/95 backdrop-blur-md">
|
||||||
<div className="mx-auto flex max-w-4xl items-center justify-between gap-4 px-6 py-3">
|
<div className="mx-auto flex max-w-4xl items-center justify-between gap-4 px-6 py-3">
|
||||||
<Link
|
<Link
|
||||||
to="/blog"
|
to={`${editLang === 'en' ? '/en' : ''}/blog`}
|
||||||
data-admin-link
|
data-admin-link
|
||||||
className="flex items-center gap-2 text-sm font-light text-ivory/70 transition-colors hover:text-copper"
|
className="flex items-center gap-2 text-sm font-light text-ivory/70 transition-colors hover:text-copper"
|
||||||
>
|
>
|
||||||
@@ -451,7 +451,7 @@ export function ArticleWritePage() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
{...field}
|
value={editLang === 'en' ? String(draft.title_en ?? '') : field.value}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
field.onChange(event.target.value)
|
field.onChange(event.target.value)
|
||||||
queueSave({ [suf('title')]: event.target.value })
|
queueSave({ [suf('title')]: event.target.value })
|
||||||
@@ -548,10 +548,10 @@ export function ArticleWritePage() {
|
|||||||
{/* چکیده و متادیتا */}
|
{/* چکیده و متادیتا */}
|
||||||
<SeoChecklist
|
<SeoChecklist
|
||||||
input={{
|
input={{
|
||||||
title: draft.title,
|
title: (editLang === 'en' ? draft.title_en : draft.title) ?? '',
|
||||||
slug: draft.slug,
|
slug: draft.slug,
|
||||||
excerpt: draft.meta_description || draft.excerpt,
|
excerpt: draft.meta_description || (editLang === 'en' ? draft.excerpt_en : draft.excerpt) || '',
|
||||||
body: draft.body,
|
body: (editLang === 'en' ? draft.body_en : draft.body) ?? draft.body,
|
||||||
keyphrase: draft.seo_keyphrase,
|
keyphrase: draft.seo_keyphrase,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -594,23 +594,34 @@ export function ArticleWritePage() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-light text-ivory/50">دستهبندی مقاله (Category)</label>
|
<label className="text-xs font-light text-ivory/50">
|
||||||
|
دستهبندی مقاله (Category){editLang === 'en' ? ' — نسخه انگلیسی' : ''}
|
||||||
|
</label>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{/* تکانتخابی: ورودی/خروجی Controller یک {value,label} یا null است */}
|
{editLang === 'en' ? (
|
||||||
<Controller
|
<input
|
||||||
name="category"
|
type="text"
|
||||||
control={control}
|
{...textField('category_en')}
|
||||||
render={({ field }) => (
|
placeholder={draft.category ? `English category (فارسی: ${draft.category})…` : 'English category…'}
|
||||||
<CategorySelect
|
className="w-full border border-ivory/15 bg-char px-3 py-2 text-sm font-light text-ivory focus:border-copper focus:outline-none"
|
||||||
value={field.value}
|
/>
|
||||||
options={ARTICLE_CATEGORIES}
|
) : (
|
||||||
onChange={(option) => {
|
/* تکانتخابی: ورودی/خروجی Controller یک {value,label} یا null است */
|
||||||
field.onChange(option)
|
<Controller
|
||||||
queueSave({ category: categoryValue(option) })
|
name="category"
|
||||||
}}
|
control={control}
|
||||||
/>
|
render={({ field }) => (
|
||||||
)}
|
<CategorySelect
|
||||||
/>
|
value={field.value}
|
||||||
|
options={ARTICLE_CATEGORIES}
|
||||||
|
onChange={(option) => {
|
||||||
|
field.onChange(option)
|
||||||
|
queueSave({ category: categoryValue(option) })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CalendarIcon, ClockIcon } from 'lucide-react'
|
|||||||
import type { Article } from '../data/site'
|
import type { Article } from '../data/site'
|
||||||
import { JALALI_TIME_FORMATTER, formatJalali } from '../utils/date'
|
import { JALALI_TIME_FORMATTER, formatJalali } from '../utils/date'
|
||||||
import { toPersianDigits } from '../utils/digits'
|
import { toPersianDigits } from '../utils/digits'
|
||||||
|
import { useLang } from '../i18n/lang'
|
||||||
|
|
||||||
type ArticleMetaProps = {
|
type ArticleMetaProps = {
|
||||||
article: Article
|
article: Article
|
||||||
@@ -13,13 +14,17 @@ type ArticleMetaProps = {
|
|||||||
* آواتار حرف اول نویسنده، نام نویسنده، تاریخ جلالی و زمان مطالعه.
|
* آواتار حرف اول نویسنده، نام نویسنده، تاریخ جلالی و زمان مطالعه.
|
||||||
*/
|
*/
|
||||||
export function ArticleMeta({ article, className = '' }: ArticleMetaProps) {
|
export function ArticleMeta({ article, className = '' }: ArticleMetaProps) {
|
||||||
|
const { t, lang } = useLang()
|
||||||
const rawDate = (article.date ?? '').trim()
|
const rawDate = (article.date ?? '').trim()
|
||||||
const author = (article.author ?? '').trim() || 'گروه ساختمانی آرامش'
|
const author = (article.author ?? '').trim() || t('brandFallback')
|
||||||
const initial = author.charAt(0)
|
const initial = author.charAt(0)
|
||||||
|
|
||||||
// تاریخ ذخیرهشده (میلادی) به شمسی تبدیل میشود؛ در نبود تاریخ، امروز با ساعت نمایش داده میشود
|
// فارسی: تاریخ شمسی با ارقام فارسی؛ انگلیسی: تاریخ میلادی لاتین
|
||||||
|
const enDateLabel = rawDate
|
||||||
|
? new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(rawDate))
|
||||||
|
: new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date())
|
||||||
const todayLabel = `${formatJalali(new Date())} — ساعت ${JALALI_TIME_FORMATTER.format(new Date())}`
|
const todayLabel = `${formatJalali(new Date())} — ساعت ${JALALI_TIME_FORMATTER.format(new Date())}`
|
||||||
const dateLabel = formatJalali(rawDate, todayLabel)
|
const dateLabel = lang === 'en' ? enDateLabel : formatJalali(rawDate, todayLabel)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-wrap items-center gap-x-6 gap-y-4 ${className}`}>
|
<div className={`flex flex-wrap items-center gap-x-6 gap-y-4 ${className}`}>
|
||||||
@@ -30,7 +35,7 @@ export function ArticleMeta({ article, className = '' }: ArticleMetaProps) {
|
|||||||
</span>
|
</span>
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="text-sm font-bold text-ivory">{author}</span>
|
<span className="text-sm font-bold text-ivory">{author}</span>
|
||||||
<span className="text-[11px] font-light text-ivory/45">نویسنده</span>
|
<span className="text-[11px] font-light text-ivory/45">{t('author')}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -46,7 +51,7 @@ export function ArticleMeta({ article, className = '' }: ArticleMetaProps) {
|
|||||||
{article.read && (
|
{article.read && (
|
||||||
<span className="flex items-center gap-2 text-xs font-light text-ivory/60">
|
<span className="flex items-center gap-2 text-xs font-light text-ivory/60">
|
||||||
<ClockIcon className="h-3.5 w-3.5 text-copper" aria-hidden="true" />
|
<ClockIcon className="h-3.5 w-3.5 text-copper" aria-hidden="true" />
|
||||||
{toPersianDigits(article.read)}
|
{lang === 'fa' ? toPersianDigits(article.read) : article.read}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { motion, useScroll, useTransform } from 'framer-motion'
|
import { motion, useScroll, useTransform } from 'framer-motion'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { ArrowLeftIcon, ChevronDownIcon } from 'lucide-react'
|
import { ArrowLeftIcon, ChevronDownIcon } from 'lucide-react'
|
||||||
import { ButtonLink } from '../Button'
|
import { ButtonLink } from '../Button'
|
||||||
import { useSite } from '../../api'
|
import { useSite } from '../../api'
|
||||||
@@ -77,6 +78,22 @@ export function Hero() {
|
|||||||
<InlineEdit multiline value={hero.subtitle} onSave={(v) => saveHero('subtitle', v)} />
|
<InlineEdit multiline value={hero.subtitle} onSave={(v) => saveHero('subtitle', v)} />
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
|
{/* پیوند درباره ما — زیر تیتر، در هر دو زبان */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 18 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.24, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={href('/about')}
|
||||||
|
className="inline-flex items-center gap-1.5 border-b border-ivory/25 pb-0.5 text-sm font-light text-ivory/70 transition-colors duration-200 hover:border-copper hover:text-copper"
|
||||||
|
>
|
||||||
|
{t('aboutUs')}
|
||||||
|
<ArrowLeftIcon className="h-3.5 w-3.5 ltr:rotate-180" aria-hidden="true" />
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* داک دکمهها: استایل کپسولی، شیشهای و دایرهای */}
|
{/* داک دکمهها: استایل کپسولی، شیشهای و دایرهای */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 22 }}
|
initial={{ opacity: 0, y: 22 }}
|
||||||
@@ -88,7 +105,7 @@ export function Hero() {
|
|||||||
<ButtonLink
|
<ButtonLink
|
||||||
to={href(hero.cta_primary_href)}
|
to={href(hero.cta_primary_href)}
|
||||||
variant="copper"
|
variant="copper"
|
||||||
className="!p-1.5 !pr-7 !rounded-full shadow-lg shadow-black/40 hover:shadow-copper/25 transition-all duration-300"
|
className="!p-1.5 !ps-7 !rounded-full shadow-lg shadow-black/40 hover:shadow-copper/25 transition-all duration-300"
|
||||||
>
|
>
|
||||||
<span className="text-sm font-medium tracking-wide text-ivory">
|
<span className="text-sm font-medium tracking-wide text-ivory">
|
||||||
<InlineEdit value={hero.cta_primary_label} onSave={(v) => saveHero('cta_primary_label', v)} />
|
<InlineEdit value={hero.cta_primary_label} onSave={(v) => saveHero('cta_primary_label', v)} />
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function Story({ showLink = true, showLabel = true, showQuote = false, al
|
|||||||
|
|
||||||
<Reveal delay={0.08}>
|
<Reveal delay={0.08}>
|
||||||
{story.paragraphs.map((paragraph, i) => (
|
{story.paragraphs.map((paragraph, i) => (
|
||||||
<p key={i} className={`text-base font-light leading-9 text-ivory/65 ${i === 0 ? 'mt-8' : 'mt-5'}`}>
|
<p key={i} className={`text-base font-light leading-9 text-ivory/65 text-justify ${i === 0 ? 'mt-8' : 'mt-5'}`}>
|
||||||
<InlineEdit multiline value={paragraph} onSave={(v) => saveParagraph(i, v)} />
|
<InlineEdit multiline value={paragraph} onSave={(v) => saveParagraph(i, v)} />
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ export const fa = {
|
|||||||
relatedArticlesAria: 'مقالات مرتبط',
|
relatedArticlesAria: 'مقالات مرتبط',
|
||||||
relatedArticles: 'مقالات مرتبط',
|
relatedArticles: 'مقالات مرتبط',
|
||||||
readArticlePrefix: 'خواندن مقاله',
|
readArticlePrefix: 'خواندن مقاله',
|
||||||
|
author: 'نویسنده',
|
||||||
|
tableOfContents: 'فهرست مطالب',
|
||||||
|
aboutUs: 'درباره ما',
|
||||||
articleImagePrefix: 'تصویر مقاله',
|
articleImagePrefix: 'تصویر مقاله',
|
||||||
featuredArticle: 'مقاله ویژه',
|
featuredArticle: 'مقاله ویژه',
|
||||||
readMore: 'ادامه مطلب',
|
readMore: 'ادامه مطلب',
|
||||||
@@ -204,6 +207,9 @@ export const en: Dict = {
|
|||||||
relatedArticlesAria: 'Related articles',
|
relatedArticlesAria: 'Related articles',
|
||||||
relatedArticles: 'Related articles',
|
relatedArticles: 'Related articles',
|
||||||
readArticlePrefix: 'Read article',
|
readArticlePrefix: 'Read article',
|
||||||
|
author: 'Author',
|
||||||
|
tableOfContents: 'Table of Contents',
|
||||||
|
aboutUs: 'About Us',
|
||||||
articleImagePrefix: 'Image of article',
|
articleImagePrefix: 'Image of article',
|
||||||
featuredArticle: 'Featured article',
|
featuredArticle: 'Featured article',
|
||||||
readMore: 'Read more',
|
readMore: 'Read more',
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export function ArticleDetail() {
|
|||||||
dir={dir}
|
dir={dir}
|
||||||
className={`order-first lg:order-none lg:row-start-1 ${tocColumns}`}
|
className={`order-first lg:order-none lg:row-start-1 ${tocColumns}`}
|
||||||
>
|
>
|
||||||
<TableOfContents items={toc} className="lg:sticky lg:top-28" />
|
<TableOfContents items={toc} title={t('tableOfContents')} className="lg:sticky lg:top-28" />
|
||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function DraftBadge({ className = '' }: { className?: string }) {
|
|||||||
|
|
||||||
export function Blog() {
|
export function Blog() {
|
||||||
const { articles } = useSite()
|
const { articles } = useSite()
|
||||||
const { t, href } = useLang()
|
const { t, href, lang } = useLang()
|
||||||
const { isEditMode } = useAdmin()
|
const { isEditMode } = useAdmin()
|
||||||
|
|
||||||
// دکمه «نوشتن مقاله جدید» فقط در حالت ویرایش ادمین دیده میشود
|
// دکمه «نوشتن مقاله جدید» فقط در حالت ویرایش ادمین دیده میشود
|
||||||
@@ -96,10 +96,12 @@ export function Blog() {
|
|||||||
<p className="mt-6 text-sm font-light leading-10 text-ivory/65">{featured.excerpt}</p>
|
<p className="mt-6 text-sm font-light leading-10 text-ivory/65">{featured.excerpt}</p>
|
||||||
<div className="mt-10 flex items-center justify-between border-t border-ivory/10 pt-8 text-xs font-light text-ivory/50">
|
<div className="mt-10 flex items-center justify-between border-t border-ivory/10 pt-8 text-xs font-light text-ivory/50">
|
||||||
<span className="flex items-center gap-4">
|
<span className="flex items-center gap-4">
|
||||||
<span>{formatJalali(featured.date)}</span>
|
<span>{lang === 'en'
|
||||||
|
? new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(featured.date))
|
||||||
|
: formatJalali(featured.date)}</span>
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<ClockIcon className="h-3.5 w-3.5" />
|
<ClockIcon className="h-3.5 w-3.5" />
|
||||||
{toPersianDigits(featured.read)}
|
{lang === 'en' ? featured.read : toPersianDigits(featured.read)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-2 text-copper">
|
<span className="flex items-center gap-2 text-copper">
|
||||||
|
|||||||
Reference in New Issue
Block a user