Files
simple-admin/src/pages/seo/ui/Seo.tsx
Samandar Turgunboyev 2d0285dafc api ulandi
2025-10-29 18:41:59 +05:00

442 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 { useEffect, useState, type ChangeEvent } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
type SeoData = {
title: string;
description: string;
keywords: string;
ogTitle: string;
ogDescription: string;
ogImage: File | null | string;
};
export default function Seo() {
const [formData, setFormData] = useState<SeoData>({
title: "",
description: "",
keywords: "",
ogTitle: "",
ogDescription: "",
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("Malumotlar 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("Malumotlar 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 [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) {
setFormData((prev) => ({ ...prev, ogImage: file }));
setImagePreview(URL.createObjectURL(file));
}
};
const handleEdit = (id: number) => {
setEdit(id);
};
const handleDelete = (id: number) => {
deleteSeoo(id);
};
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 bolsa qoshamiz
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() > 60 && 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">
{t("SEO Manager")}
</h1>
</div>
<p className="text-slate-400">
{t("Saytingizni qidiruv tizimida yaxshi pozitsiyaga keltiring")}
</p>
</div>
{/* 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" /> {t("Page Title")}
</label>
<input
type="text"
name="title"
value={formData.title}
onChange={handleChange}
placeholder={t("Sahifa sarlavhasi (3060 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">
{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">
{t("Meta Description")}
</label>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
placeholder={t("Sahifa tavsifi (120160 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">
{t("Keywords")}
</label>
<input
type="text"
name="keywords"
value={formData.keywords}
onChange={handleChange}
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">
{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">
{t("Open Graph (Ijtimoiy Tarmoqlar)")}
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm text-slate-300 mb-2">
{t("OG Title")}
</label>
<input
type="text"
name="ogTitle"
value={formData.ogTitle}
onChange={handleChange}
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">
{t("OG Description")}
</label>
<textarea
name="ogDescription"
value={formData.ogDescription}
onChange={handleChange}
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" />{" "}
{t("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: null,
}));
}}
className="mt-2 text-xs bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded"
>
{t("O'chirish")}
</button>
</div>
)}
</div>
</div>
</div>
</div>
<button
onClick={handleSave}
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"
>
{isPending || editPending ? (
<Loader2 className="animate-spin mx-auto" />
) : edit ? (
t("Tahrirlash")
) : (
t("Saqlash")
)}
</button>
</div>
</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 Malumotlari")}
</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 malumotlari mavjud emas.")}
</p>
)}
</div>
</div>
</div>
);
}