first commit
This commit is contained in:
269
src/pages/seo/ui/Seo.tsx
Normal file
269
src/pages/seo/ui/Seo.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
FileText,
|
||||
Image as ImageIcon,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import { useState, type ChangeEvent } from "react";
|
||||
|
||||
type SeoData = {
|
||||
title: string;
|
||||
description: string;
|
||||
keywords: string;
|
||||
ogTitle: string;
|
||||
ogDescription: string;
|
||||
ogImage: string;
|
||||
};
|
||||
|
||||
export default function Seo() {
|
||||
const [formData, setFormData] = useState<SeoData>({
|
||||
title: "",
|
||||
description: "",
|
||||
keywords: "",
|
||||
ogTitle: "",
|
||||
ogDescription: "",
|
||||
ogImage: "",
|
||||
});
|
||||
|
||||
const [savedSeo, setSavedSeo] = useState<SeoData | null>(null);
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||
|
||||
const handleChange = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleImageUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const result = event.target?.result as string;
|
||||
setImagePreview(result);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
ogImage: result,
|
||||
}));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setSavedSeo(formData);
|
||||
setFormData({
|
||||
description: "",
|
||||
keywords: "",
|
||||
ogDescription: "",
|
||||
ogImage: "",
|
||||
ogTitle: "",
|
||||
title: "",
|
||||
});
|
||||
};
|
||||
|
||||
const getTitleLength = () => formData.title.length;
|
||||
const getDescriptionLength = () => formData.description.length;
|
||||
|
||||
const isValidTitle = getTitleLength() > 30 && getTitleLength() <= 60;
|
||||
const isValidDescription =
|
||||
getDescriptionLength() > 120 && getDescriptionLength() <= 160;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 p-8 w-full">
|
||||
<div className="max-w-[90%] mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<TrendingUp className="w-8 h-8 text-blue-400" />
|
||||
<h1 className="text-4xl font-bold text-white">SEO Manager</h1>
|
||||
</div>
|
||||
<p className="text-slate-400">
|
||||
Saytingizni qidiruv tizimida yaxshi pozitsiyaga keltiring
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="grid grid-cols-1 gap-8">
|
||||
<div className="bg-slate-800 rounded-lg p-6 space-y-6">
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-2">
|
||||
<FileText className="inline w-4 h-4 mr-1" /> Page Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
placeholder="Sahifa sarlavhasi (30–60 belgi)"
|
||||
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm text-slate-400">
|
||||
{getTitleLength()} / 60
|
||||
</span>
|
||||
{isValidTitle && (
|
||||
<CheckCircle className="w-5 h-5 text-green-400" />
|
||||
)}
|
||||
{getTitleLength() > 0 && !isValidTitle && (
|
||||
<AlertCircle className="w-5 h-5 text-yellow-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-2">
|
||||
Meta Description
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
placeholder="Sahifa tavsifi (120–160 belgi)"
|
||||
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||
/>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm text-slate-400">
|
||||
{getDescriptionLength()} / 160
|
||||
</span>
|
||||
{isValidDescription && (
|
||||
<CheckCircle className="w-5 h-5 text-green-400" />
|
||||
)}
|
||||
{getDescriptionLength() > 0 && !isValidDescription && (
|
||||
<AlertCircle className="w-5 h-5 text-yellow-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Keywords */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-2">
|
||||
Keywords
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="keywords"
|
||||
value={formData.keywords}
|
||||
onChange={handleChange}
|
||||
placeholder="Kalit so'zlar (vergul bilan ajratilgan)"
|
||||
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Masalan: Python, Web Development, Coding
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* OG Tags */}
|
||||
<div className="border-t border-slate-700 pt-6">
|
||||
<h3 className="text-sm font-semibold text-white mb-4">
|
||||
Open Graph (Ijtimoiy Tarmoqlar)
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-slate-300 mb-2">
|
||||
OG Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="ogTitle"
|
||||
value={formData.ogTitle}
|
||||
onChange={handleChange}
|
||||
placeholder="Ijtimoiy tarmoqdagi sarlavha"
|
||||
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-slate-300 mb-2">
|
||||
OG Description
|
||||
</label>
|
||||
<textarea
|
||||
name="ogDescription"
|
||||
value={formData.ogDescription}
|
||||
onChange={handleChange}
|
||||
placeholder="Ijtimoiy tarmoqdagi tavsif"
|
||||
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-slate-300 mb-2">
|
||||
<ImageIcon className="inline w-4 h-4 mr-1" /> OG Image
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageUpload}
|
||||
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 file:bg-blue-600 file:text-white file:px-3 file:py-1 file:rounded file:border-0 file:cursor-pointer"
|
||||
/>
|
||||
{imagePreview && (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
className="w-full h-40 object-cover rounded-lg"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setImagePreview(null);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
ogImage: "",
|
||||
}));
|
||||
}}
|
||||
className="mt-2 text-xs bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded"
|
||||
>
|
||||
O‘chirish
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg transition-colors"
|
||||
>
|
||||
Saqlash
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Saved SEO Data (Preview) */}
|
||||
{savedSeo && (
|
||||
<div className="mt-8 bg-slate-700 rounded-lg p-6 text-slate-200">
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
Saqlangan SEO Ma’lumotlari
|
||||
</h3>
|
||||
<pre className="bg-slate-800 p-4 rounded text-xs overflow-auto">
|
||||
{JSON.stringify(
|
||||
{
|
||||
...savedSeo,
|
||||
ogImage: savedSeo.ogImage
|
||||
? savedSeo.ogImage.substring(0, 100) + "..."
|
||||
: "",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user