137 lines
4.7 KiB
TypeScript
137 lines
4.7 KiB
TypeScript
import React from 'react';
|
||
import {
|
||
CheckCircle2Icon,
|
||
CircleAlertIcon,
|
||
GlobeIcon,
|
||
KeyIcon,
|
||
SearchCheckIcon,
|
||
TextIcon } from
|
||
'lucide-react';
|
||
import { SurfaceCard } from './ui/SurfaceCard';
|
||
import { TextField } from './ui/TextField';
|
||
import { TextArea } from './ui/TextArea';
|
||
|
||
export interface SeoState {
|
||
focusKeyword: string;
|
||
seoTitle: string;
|
||
metaDescription: string;
|
||
slug: string;
|
||
}
|
||
|
||
interface SeoPanelProps {
|
||
value: SeoState;
|
||
onChange: (value: SeoState) => void;
|
||
}
|
||
|
||
interface Check {
|
||
label: string;
|
||
passed: boolean;
|
||
}
|
||
|
||
export function SeoPanel({ value, onChange }: SeoPanelProps) {
|
||
const set = (key: keyof SeoState, next: string) => onChange({ ...value, [key]: next });
|
||
|
||
const checks: Check[] = [
|
||
{
|
||
label: 'کلمه کلیدی در عنوان سئو آمده است',
|
||
passed: Boolean(value.focusKeyword) && value.seoTitle.includes(value.focusKeyword)
|
||
},
|
||
{
|
||
label: 'طول عنوان سئو بین ۳۰ تا ۶۰ نویسه است',
|
||
passed: value.seoTitle.length >= 30 && value.seoTitle.length <= 60
|
||
},
|
||
{
|
||
label: 'توضیح متا بین ۷۰ تا ۱۶۰ نویسه است',
|
||
passed: value.metaDescription.length >= 70 && value.metaDescription.length <= 160
|
||
},
|
||
{
|
||
label: 'کلمه کلیدی در توضیح متا آمده است',
|
||
passed: Boolean(value.focusKeyword) && value.metaDescription.includes(value.focusKeyword)
|
||
},
|
||
{ label: 'نامک (Slug) کوتاه و خواناست', passed: value.slug.length > 0 && value.slug.length <= 60 }];
|
||
|
||
|
||
const passedCount = checks.filter((check) => check.passed).length;
|
||
const score = Math.round(passedCount / checks.length * 100);
|
||
const scoreLabel = score >= 80 ? 'عالی' : score >= 50 ? 'قابل بهبود' : 'ضعیف';
|
||
|
||
return (
|
||
<SurfaceCard
|
||
title="تنظیمات سئو"
|
||
description="پیش از انتشار، امتیاز سئوی مقاله را بررسی کنید."
|
||
icon={SearchCheckIcon}
|
||
actions={
|
||
<span
|
||
className={`rounded-full px-4 py-1.5 text-xs ${
|
||
score >= 80 ? 'bg-copper text-[#241a11]' : 'bg-copper-soft text-copper'}`
|
||
}>
|
||
|
||
امتیاز سئو: {score} — {scoreLabel}
|
||
</span>
|
||
}>
|
||
|
||
<div className="grid gap-8 xl:grid-cols-[1.1fr_1fr]">
|
||
<div className="space-y-6">
|
||
<TextField
|
||
label="کلمه کلیدی اصلی"
|
||
icon={KeyIcon}
|
||
value={value.focusKeyword}
|
||
onChange={(event) => set('focusKeyword', event.target.value)}
|
||
placeholder="سرمایهگذاری ملکی" />
|
||
|
||
<TextField
|
||
label="عنوان سئو"
|
||
icon={TextIcon}
|
||
value={value.seoTitle}
|
||
onChange={(event) => set('seoTitle', event.target.value)}
|
||
hint={`${value.seoTitle.length} از ۶۰ نویسه`} />
|
||
|
||
<TextField
|
||
label="نامک (Slug)"
|
||
icon={GlobeIcon}
|
||
value={value.slug}
|
||
onChange={(event) => set('slug', event.target.value)}
|
||
placeholder="real-estate-investment-guide" />
|
||
|
||
<TextArea
|
||
label="توضیح متا"
|
||
rows={3}
|
||
value={value.metaDescription}
|
||
onChange={(event) => set('metaDescription', event.target.value)}
|
||
hint={`${value.metaDescription.length} از ۱۶۰ نویسه`} />
|
||
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
<div className="rounded-2xl border border-white/[0.07] bg-ink p-6">
|
||
<p className="text-xs text-ivory/40">پیشنمایش نتیجه گوگل</p>
|
||
<div className="mt-4" dir="ltr">
|
||
<p className="text-xs text-[#9aa0a6]">
|
||
arameshplaza.com › blog › {value.slug || 'article'}
|
||
</p>
|
||
<p className="mt-1.5 text-lg leading-7 text-[#8ab4f8]" dir="rtl">
|
||
{value.seoTitle || 'عنوان سئو را وارد کنید'}
|
||
</p>
|
||
<p className="mt-1.5 text-sm leading-6 text-[#bdc1c6]" dir="rtl">
|
||
{value.metaDescription || 'توضیح متا اینجا نمایش داده میشود.'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<ul className="flex flex-col gap-3">
|
||
{checks.map((check) =>
|
||
<li key={check.label} className="flex items-start gap-3 text-sm">
|
||
{check.passed ?
|
||
<CheckCircle2Icon className="mt-0.5 h-4 w-4 shrink-0 text-copper" /> :
|
||
|
||
<CircleAlertIcon className="mt-0.5 h-4 w-4 shrink-0 text-ivory/30" />
|
||
}
|
||
<span className={check.passed ? 'text-ivory/80' : 'text-ivory/40'}>{check.label}</span>
|
||
</li>
|
||
)}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</SurfaceCard>);
|
||
|
||
} |