fix(en): هشت اصلاح سایت انگلیسی — دکمهٔ هیرو با ps منطقی، پیوند درباره ما زیر تیتر، justify متن درباره ما، متای مقاله (نویسنده/تاریخ/زمان مطالعه) دوزبانه، تیتر فهرست مطالب، H1 انگلیسی در ادیتور + چک‌لیست سئو زبان‌آگاه، بازگشت به وبلاگ با حفظ زبان، دسته‌بندی انگلیسی

This commit is contained in:
aramesh-site deploy
2026-10-01 16:54:44 +00:00
parent d56dff2a2f
commit 632edaf081
7 changed files with 74 additions and 33 deletions
+33 -22
View File
@@ -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>
+10 -5
View File
@@ -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>
+18 -1
View File
@@ -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)} />
+1 -1
View File
@@ -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>
))} ))}
+6
View File
@@ -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',
+1 -1
View File
@@ -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>
+5 -3
View File
@@ -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">