api ulandi

This commit is contained in:
Samandar Turgunboyev
2025-10-29 18:41:59 +05:00
parent a9e99f9755
commit 2d0285dafc
64 changed files with 6319 additions and 2352 deletions

View File

@@ -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("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 [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 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() > 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 (3060 belgi)"
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">
<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 (120160 belgi)"
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">
@@ -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"
>
Ochirish
{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 Malumotlari
</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 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>
);