- مقالهها ستون status (draft/published) گرفتند — GET /api/site و همه انتهای عمومی فقط منتشرشده را برمیگردانند؛ autosave دیگر پیشنویس نیمهکاره را زنده نمیکند - کلمه کلیدی هدف چکلیست سئو فیلد seo_keyphrase شد (قبلاً localStorage بود) - robots.txt و sitemap.xml از بکاند + سرو /blog و /projects با تزریق title/description/OG/JSON-LD از دیتابیس برای خزندههای بدون JS (app/seo_pages.py) - فرانت: applySeo در seo.ts (OG/توییتر/canonical/JSON-LD)، ادغام پیشنویس برای ادمین، برچسب پیشنویس در وبلاگ، حالت خالی وبلاگ، دکمه انتشار در صفحه نوشتن - تغییر رمز ادمین POST /api/auth/change-password + دیالوگ «رمز» در نوار مدیریت - محدودسازی ورود: ۵ تلاش ناموفق در ۱۵ دقیقه از هر IP → 429 - ساخت دکمه + برای ردیف سئوی صفحههای بدون ردیف (SingletonPill) - تستهای pytest (۱۵ مورد) + پینکردن requirements + حذف package.json سرگردان فرانت
68 lines
2.8 KiB
TypeScript
68 lines
2.8 KiB
TypeScript
import React from 'react';
|
|
import { ArrowLeftIcon } from 'lucide-react';
|
|
import { Reveal } from '../Reveal';
|
|
import { ButtonLink } from '../Button';
|
|
import { ArticleCard } from '../ArticleCard';
|
|
import { useSectionHeading, useSite } from '../../api';
|
|
import { useFieldSaver } from '../../admin/AdminContext';
|
|
import { InlineEdit } from '../admin/InlineEdit';
|
|
import { AddItemButton, EditableItem } from '../../admin/Editable';
|
|
|
|
export function Articles() {
|
|
const { articles } = useSite();
|
|
const heading = useSectionHeading('articles');
|
|
const save = useFieldSaver();
|
|
|
|
// پیشنویسها فقط در صفحه وبلاگ (برای ادمین) دیده میشوند، نه در خانه
|
|
const published = articles.filter((a) => a.status !== 'draft');
|
|
|
|
const saveHeading = (field: 'label' | 'title' | 'lead' | 'cta_label', value: string) =>
|
|
save('section-headings', heading.id, { [field]: value }, (d) => {
|
|
const h = d.section_headings.find((s) => s.id === heading.id);
|
|
if (h) (h as Record<string, unknown>)[field] = value;
|
|
});
|
|
|
|
return (
|
|
<section className="bg-char py-24 lg:py-32" aria-labelledby="articles-title">
|
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
|
<Reveal className="max-w-2xl text-right">
|
|
<p className="text-sm font-light text-copper">
|
|
<InlineEdit value={heading.label} onSave={(v) => saveHeading('label', v)} />
|
|
</p>
|
|
<h2 id="articles-title" className="mt-5 text-section font-bold text-ivory">
|
|
<InlineEdit value={heading.title} onSave={(v) => saveHeading('title', v)} />
|
|
</h2>
|
|
<p className="mt-5 text-base font-light leading-9 text-ivory/60">
|
|
<InlineEdit multiline value={heading.lead} onSave={(v) => saveHeading('lead', v)} />
|
|
</p>
|
|
<div className="mt-6 h-px w-24 bg-copper/70" />
|
|
</Reveal>
|
|
|
|
<div className="mt-14 grid gap-6 lg:grid-cols-3">
|
|
{published.slice(0, 3).map((article, i) =>
|
|
<Reveal key={article.id} delay={0.06 * i} className="h-full">
|
|
<EditableItem resource="articles" item={article} className="h-full" editHref={`/admin/write/${article.id}`}>
|
|
<ArticleCard article={article} />
|
|
</EditableItem>
|
|
</Reveal>
|
|
)}
|
|
</div>
|
|
|
|
{heading.cta_label && heading.cta_href && (
|
|
<Reveal delay={0.1}>
|
|
<div className="mt-12 flex justify-center">
|
|
<ButtonLink to={heading.cta_href} variant="outline" className="font-light">
|
|
<InlineEdit value={heading.cta_label} onSave={(v) => saveHeading('cta_label', v)} />
|
|
<ArrowLeftIcon className="h-4 w-4" />
|
|
</ButtonLink>
|
|
</div>
|
|
</Reveal>
|
|
)}
|
|
<div className="mt-6">
|
|
<AddItemButton resource="articles" />
|
|
</div>
|
|
</div>
|
|
</section>);
|
|
|
|
}
|