Initial commit - uploading project files
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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>);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user