api ulandi
This commit is contained in:
@@ -1,13 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createSeo,
|
||||
deleteSeo,
|
||||
getAllSeo,
|
||||
getDetailSeo,
|
||||
updateSeo,
|
||||
} from "@/pages/seo/lib/api";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
FileText,
|
||||
Image as ImageIcon,
|
||||
Loader2,
|
||||
Pencil,
|
||||
Trash2,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import { useState, type ChangeEvent } from "react";
|
||||
import { useEffect, useState, type ChangeEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type SeoData = {
|
||||
title: string;
|
||||
@@ -15,7 +28,7 @@ type SeoData = {
|
||||
keywords: string;
|
||||
ogTitle: string;
|
||||
ogDescription: string;
|
||||
ogImage: string;
|
||||
ogImage: File | null | string;
|
||||
};
|
||||
|
||||
export default function Seo() {
|
||||
@@ -25,10 +38,107 @@ export default function Seo() {
|
||||
keywords: "",
|
||||
ogTitle: "",
|
||||
ogDescription: "",
|
||||
ogImage: "",
|
||||
ogImage: null,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const [edit, setEdit] = useState<number | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: async (body: FormData) => createSeo(body),
|
||||
onSuccess: () => {
|
||||
toast.success(t("Ma’lumotlar muvaffaqiyatli saqlandi"), {
|
||||
position: "top-center",
|
||||
});
|
||||
setFormData({
|
||||
description: "",
|
||||
keywords: "",
|
||||
ogDescription: "",
|
||||
ogImage: null,
|
||||
ogTitle: "",
|
||||
title: "",
|
||||
});
|
||||
queryClient.refetchQueries({ queryKey: ["seo_all"] });
|
||||
queryClient.refetchQueries({ queryKey: ["seo_detail"] });
|
||||
setImagePreview(null);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
position: "top-center",
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: editSeo, isPending: editPending } = useMutation({
|
||||
mutationFn: async ({ body, id }: { body: FormData; id: number }) =>
|
||||
updateSeo({ body, id }),
|
||||
onSuccess: () => {
|
||||
toast.success(t("Ma’lumotlar muvaffaqiyatli saqlandi"), {
|
||||
position: "top-center",
|
||||
});
|
||||
setEdit(null);
|
||||
setFormData({
|
||||
description: "",
|
||||
keywords: "",
|
||||
ogDescription: "",
|
||||
ogImage: null,
|
||||
ogTitle: "",
|
||||
title: "",
|
||||
});
|
||||
queryClient.refetchQueries({ queryKey: ["seo_all"] });
|
||||
queryClient.refetchQueries({ queryKey: ["seo_detail"] });
|
||||
setImagePreview(null);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
position: "top-center",
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteSeoo } = useMutation({
|
||||
mutationFn: (id: number) => deleteSeo({ id }),
|
||||
onSuccess: () => {
|
||||
setEdit(null);
|
||||
setFormData({
|
||||
description: "",
|
||||
keywords: "",
|
||||
ogDescription: "",
|
||||
ogImage: null,
|
||||
ogTitle: "",
|
||||
title: "",
|
||||
});
|
||||
queryClient.refetchQueries({ queryKey: ["seo_all"] });
|
||||
queryClient.refetchQueries({ queryKey: ["seo_detail"] });
|
||||
setImagePreview(null);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
position: "top-center",
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { data: allSeo, isLoading } = useQuery({
|
||||
queryKey: ["seo_all"],
|
||||
queryFn: () => getAllSeo({ page: 1, page_size: 99 }),
|
||||
select(data) {
|
||||
return data.data.data.results;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: detailSeo } = useQuery({
|
||||
queryKey: ["seo_detail", edit],
|
||||
queryFn: () => getDetailSeo(edit!),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
enabled: !!edit,
|
||||
});
|
||||
|
||||
const [savedSeo, setSavedSeo] = useState<SeoData | null>(null);
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||
|
||||
const handleChange = (
|
||||
@@ -44,37 +154,59 @@ export default function Seo() {
|
||||
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);
|
||||
setFormData((prev) => ({ ...prev, ogImage: file }));
|
||||
setImagePreview(URL.createObjectURL(file));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setSavedSeo(formData);
|
||||
setFormData({
|
||||
description: "",
|
||||
keywords: "",
|
||||
ogDescription: "",
|
||||
ogImage: "",
|
||||
ogTitle: "",
|
||||
title: "",
|
||||
});
|
||||
const handleEdit = (id: number) => {
|
||||
setEdit(id);
|
||||
};
|
||||
|
||||
const getTitleLength = () => formData.title.length;
|
||||
const getDescriptionLength = () => formData.description.length;
|
||||
const handleDelete = (id: number) => {
|
||||
deleteSeoo(id);
|
||||
};
|
||||
|
||||
const isValidTitle = getTitleLength() > 30 && getTitleLength() <= 60;
|
||||
useEffect(() => {
|
||||
if (detailSeo) {
|
||||
setFormData({
|
||||
description: detailSeo.description,
|
||||
keywords: detailSeo.keywords,
|
||||
ogDescription: detailSeo.og_description,
|
||||
ogImage: detailSeo.og_image,
|
||||
ogTitle: detailSeo.og_title,
|
||||
title: detailSeo.title,
|
||||
});
|
||||
setImagePreview(detailSeo.og_image || null);
|
||||
}
|
||||
}, [detailSeo, edit]);
|
||||
|
||||
const handleSave = () => {
|
||||
const form = new FormData();
|
||||
form.append("title", formData.title);
|
||||
form.append("description", formData.description);
|
||||
form.append("keywords", formData.keywords);
|
||||
form.append("og_title", formData.ogTitle);
|
||||
form.append("og_description", formData.ogDescription);
|
||||
|
||||
// faqat File bo‘lsa qo‘shamiz
|
||||
if (formData.ogImage instanceof File) {
|
||||
form.append("og_image", formData.ogImage);
|
||||
}
|
||||
|
||||
if (edit) {
|
||||
editSeo({ body: form, id: edit });
|
||||
} else {
|
||||
mutate(form);
|
||||
}
|
||||
};
|
||||
|
||||
const getTitleLength = () => formData.title?.length ?? 0;
|
||||
const getDescriptionLength = () => formData.description?.length ?? 0;
|
||||
|
||||
const isValidTitle = getTitleLength() > 10 && getTitleLength() <= 60;
|
||||
const isValidDescription =
|
||||
getDescriptionLength() > 120 && getDescriptionLength() <= 160;
|
||||
getDescriptionLength() > 60 && getDescriptionLength() <= 160;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 p-8 w-full">
|
||||
@@ -83,33 +215,32 @@ export default function Seo() {
|
||||
<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>
|
||||
<h1 className="text-4xl font-bold text-white">
|
||||
{t("SEO Manager")}
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-slate-400">
|
||||
Saytingizni qidiruv tizimida yaxshi pozitsiyaga keltiring
|
||||
{t("Saytingizni qidiruv tizimida yaxshi pozitsiyaga keltiring")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
{/* Main Form */}
|
||||
<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
|
||||
<FileText className="inline w-4 h-4 mr-1" /> {t("Page Title")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
placeholder="Sahifa sarlavhasi (30–60 belgi)"
|
||||
placeholder={t("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" />
|
||||
)}
|
||||
@@ -122,13 +253,13 @@ export default function Seo() {
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-2">
|
||||
Meta Description
|
||||
{t("Meta Description")}
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
placeholder="Sahifa tavsifi (120–160 belgi)"
|
||||
placeholder={t("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">
|
||||
@@ -147,58 +278,59 @@ export default function Seo() {
|
||||
{/* Keywords */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-2">
|
||||
Keywords
|
||||
{t("Keywords")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="keywords"
|
||||
value={formData.keywords}
|
||||
onChange={handleChange}
|
||||
placeholder="Kalit so'zlar (vergul bilan ajratilgan)"
|
||||
placeholder={t("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
|
||||
{t("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)
|
||||
{t("Open Graph (Ijtimoiy Tarmoqlar)")}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-slate-300 mb-2">
|
||||
OG Title
|
||||
{t("OG Title")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="ogTitle"
|
||||
value={formData.ogTitle}
|
||||
onChange={handleChange}
|
||||
placeholder="Ijtimoiy tarmoqdagi sarlavha"
|
||||
placeholder={t("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
|
||||
{t("OG Description")}
|
||||
</label>
|
||||
<textarea
|
||||
name="ogDescription"
|
||||
value={formData.ogDescription}
|
||||
onChange={handleChange}
|
||||
placeholder="Ijtimoiy tarmoqdagi tavsif"
|
||||
placeholder={t("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
|
||||
<ImageIcon className="inline w-4 h-4 mr-1" />{" "}
|
||||
{t("OG Image")}
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
@@ -220,12 +352,12 @@ export default function Seo() {
|
||||
setImagePreview(null);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
ogImage: "",
|
||||
ogImage: null,
|
||||
}));
|
||||
}}
|
||||
className="mt-2 text-xs bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded"
|
||||
>
|
||||
O‘chirish
|
||||
{t("O'chirish")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -236,33 +368,73 @@ export default function Seo() {
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg transition-colors"
|
||||
disabled={isPending}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
Saqlash
|
||||
{isPending || editPending ? (
|
||||
<Loader2 className="animate-spin mx-auto" />
|
||||
) : edit ? (
|
||||
t("Tahrirlash")
|
||||
) : (
|
||||
t("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>
|
||||
)}
|
||||
{/* Saved SEO Data */}
|
||||
<div className="mt-8 bg-slate-700 rounded-lg p-6 text-slate-200">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("Saqlangan SEO Ma’lumotlari")}
|
||||
</h3>
|
||||
|
||||
{isLoading ? (
|
||||
<p>{t("Yuklanmoqda...")}</p>
|
||||
) : allSeo && allSeo.length > 0 ? (
|
||||
<div className="space-y-4 max-h-[400px] overflow-y-auto">
|
||||
{allSeo.map((seo) => (
|
||||
<div
|
||||
key={seo.id}
|
||||
className="bg-slate-800 p-4 rounded-lg border border-slate-600 relative"
|
||||
>
|
||||
<div className="absolute top-2 right-2 flex gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(seo.id)}
|
||||
className="p-1 text-blue-400 hover:text-blue-600"
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(seo.id)}
|
||||
className="p-1 text-red-400 hover:text-red-600"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h4 className="text-white font-semibold">{seo.title}</h4>
|
||||
<p className="text-sm text-slate-300">
|
||||
{seo.description?.substring(0, 150)}...
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">
|
||||
<strong>{t("Keywords")}:</strong> {seo.keywords}
|
||||
</p>
|
||||
{seo.og_image && (
|
||||
<img
|
||||
src={seo.og_image}
|
||||
alt="OG"
|
||||
className="w-full h-40 object-cover rounded-lg mt-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-400">
|
||||
{t("Hozircha SEO ma’lumotlari mavjud emas.")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user