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

@@ -0,0 +1,70 @@
import type {
GetDetailSiteSetting,
GetSiteSetting,
} from "@/pages/tour-settings/lib/types";
import httpClient from "@/shared/config/api/httpClient";
import { SITE_SETTING } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
const createSiteSetting = async (body: {
address: string;
latitude: string;
longitude: string;
telegram: string;
instagram: string;
facebook: string;
twitter: string;
email: string;
main_phone: string;
other_phone: string;
}) => {
const res = await httpClient.post(SITE_SETTING, body);
return res;
};
const updateSiteSetting = async ({
body,
id,
}: {
id: number;
body: {
address: string;
latitude: string;
longitude: string;
telegram: string;
instagram: string;
facebook: string;
twitter: string;
email: string;
main_phone: string;
other_phone: string;
};
}) => {
const res = await httpClient.patch(`${SITE_SETTING}${id}/`, body);
return res;
};
const getSiteSetting = async (): Promise<AxiosResponse<GetSiteSetting>> => {
const res = await httpClient.get(SITE_SETTING);
return res;
};
const getDetailSiteSetting = async (
id: number,
): Promise<AxiosResponse<GetDetailSiteSetting>> => {
const res = await httpClient.get(`${SITE_SETTING}${id}/`);
return res;
};
const deleteSiteSetting = async (id: number) => {
const res = await httpClient.delete(`${SITE_SETTING}${id}/`);
return res;
};
export {
createSiteSetting,
deleteSiteSetting,
getDetailSiteSetting,
getSiteSetting,
updateSiteSetting,
};

View File

@@ -0,0 +1,43 @@
export interface GetSiteSetting {
status: boolean;
data: {
links: {
previous: string;
next: string;
};
total_items: number;
total_pages: number;
page_size: number;
current_page: number;
results: {
id: number;
address: string;
latitude: string;
longitude: string;
telegram: string;
instagram: string;
facebook: string;
twitter: string;
email: string;
main_phone: string;
other_phone: string;
}[];
};
}
export interface GetDetailSiteSetting {
status: boolean;
data: {
id: number;
address: string;
latitude: string;
longitude: string;
telegram: string;
instagram: string;
facebook: string;
twitter: string;
email: string;
main_phone: string;
other_phone: string;
};
}

View File

@@ -1,5 +1,13 @@
"use client";
import {
createSiteSetting,
deleteSiteSetting,
getDetailSiteSetting,
getSiteSetting,
updateSiteSetting,
} from "@/pages/tour-settings/lib/api";
import formatPhone from "@/shared/lib/formatPhone";
import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
import {
@@ -12,27 +20,28 @@ import {
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import { Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
import { Edit, Plus, Trash } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Edit, Loader2, Plus, Trash } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
type MapClickEvent = {
get: (key: "coords") => [number, number];
};
type ContactInfo = {
telegram?: string;
instagram?: string;
facebook?: string;
twiter?: string;
linkedin?: string;
address?: string;
email?: string;
phonePrimary?: string;
phoneSecondary?: string;
telegram: string;
instagram: string;
facebook: string;
twiter: string;
linkedin: string;
address: string;
email: string;
phonePrimary: string;
phoneSecondary: string;
};
const STORAGE_KEY = "site_contact_info";
async function getAddressFromCoords(lat: number, lon: number) {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json`,
@@ -42,26 +51,174 @@ async function getAddressFromCoords(lat: number, lon: number) {
}
export default function ContactSettings() {
const [contact, setContact] = useState<ContactInfo | null>(null);
const { t } = useTranslation();
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState(false);
const [form, setForm] = useState<ContactInfo>({});
const [editing, setEditing] = useState<number | null>(null);
const [form, setForm] = useState<ContactInfo>({
telegram: "",
instagram: "",
facebook: "",
twiter: "",
linkedin: "",
address: "",
email: "",
phonePrimary: "+998",
phoneSecondary: "+998",
});
const [coords, setCoords] = useState({
latitude: 41.311081,
longitude: 69.240562,
}); // Toshkent default
});
const { mutate: create, isPending } = useMutation({
mutationFn: (body: {
address: string;
latitude: string;
longitude: string;
telegram: string;
instagram: string;
facebook: string;
twitter: string;
email: string;
main_phone: string;
other_phone: string;
}) => createSiteSetting(body),
onSuccess: () => {
setForm({
telegram: "",
instagram: "",
facebook: "",
twiter: "",
linkedin: "",
address: "",
email: "",
phonePrimary: "",
phoneSecondary: "",
});
setOpen(false);
queryClient.refetchQueries({ queryKey: ["contact"] });
queryClient.refetchQueries({ queryKey: ["contact_detail"] });
setEditing(null);
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
// Load saved contact
useEffect(() => {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) setContact(JSON.parse(raw));
}, []);
const { mutate: update } = useMutation({
mutationFn: ({
body,
id,
}: {
id: number;
body: {
address: string;
latitude: string;
longitude: string;
telegram: string;
instagram: string;
facebook: string;
twitter: string;
email: string;
main_phone: string;
other_phone: string;
};
}) => updateSiteSetting({ body, id }),
onSuccess: () => {
setForm({
telegram: "",
instagram: "",
facebook: "",
twiter: "",
linkedin: "",
address: "",
email: "",
phonePrimary: "",
phoneSecondary: "",
});
setOpen(false);
queryClient.refetchQueries({ queryKey: ["contact"] });
queryClient.refetchQueries({ queryKey: ["contact_detail"] });
setEditing(null);
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const { mutate: remover } = useMutation({
mutationFn: (id: number) => deleteSiteSetting(id),
onSuccess: () => {
setForm({
telegram: "",
instagram: "",
facebook: "",
twiter: "",
linkedin: "",
address: "",
email: "",
phonePrimary: "",
phoneSecondary: "",
});
setOpen(false);
queryClient.refetchQueries({ queryKey: ["contact"] });
queryClient.refetchQueries({ queryKey: ["contact_detail"] });
setEditing(null);
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const { data, isLoading } = useQuery({
queryKey: ["contact"],
queryFn: () => {
return getSiteSetting();
},
select: (data) => {
return data.data.data;
},
});
const { data: detail } = useQuery({
queryKey: ["contact_detail", editing],
queryFn: () => {
return getDetailSiteSetting(editing!);
},
select: (data) => {
return data.data.data;
},
enabled: !!editing,
});
// Populate form when editing
useEffect(() => {
if (open && editing && contact) setForm(contact);
if (!open && !editing) setForm({});
}, [open, editing, contact]);
if (editing && detail) {
setForm({
address: detail.address,
email: detail.email,
facebook: detail.facebook,
instagram: detail.instagram,
linkedin: detail.twitter,
phonePrimary: detail.main_phone,
phoneSecondary: detail.other_phone,
telegram: detail.telegram,
twiter: detail.twitter,
});
setCoords({
latitude: Number(detail.latitude),
longitude: Number(detail.longitude),
});
}
}, [editing, detail]);
const handleChange = <K extends keyof ContactInfo>(
key: K,
@@ -80,139 +237,196 @@ export default function ContactSettings() {
};
const saveContact = () => {
if (!form.email && !form.phonePrimary) {
alert("Iltimos email yoki telefon kiriting");
return;
const shortLat = Number(coords.latitude.toFixed(6)); // 6ta raqamgacha
const shortLon = Number(coords.longitude.toFixed(6));
if (editing) {
update({
body: {
address: form.address,
email: form.email,
facebook: form.facebook,
instagram: form.instagram,
latitude: String(shortLat),
longitude: String(shortLon),
main_phone: form.phonePrimary,
other_phone: form.phoneSecondary,
telegram: form.telegram,
twitter: form.twiter,
},
id: editing,
});
} else {
create({
address: form.address,
email: form.email,
facebook: form.facebook,
instagram: form.instagram,
latitude: String(shortLat),
longitude: String(shortLon),
main_phone: form.phonePrimary,
other_phone: form.phoneSecondary,
telegram: form.telegram,
twitter: form.twiter,
});
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(form));
setContact(form);
setOpen(false);
setEditing(false);
};
const startAdd = () => {
setForm({});
setEditing(false);
setForm({
telegram: "",
instagram: "",
facebook: "",
twiter: "",
linkedin: "",
address: "",
email: "",
phonePrimary: "",
phoneSecondary: "",
});
setEditing(null);
setOpen(true);
};
const startEdit = () => {
setEditing(true);
const startEdit = (id: number) => {
setEditing(id);
setOpen(true);
};
const removeContact = () => {
if (!confirm("Contact ma'lumotlarini o'chirishni xohlaysizmi?")) return;
localStorage.removeItem(STORAGE_KEY);
setContact(null);
const removeContact = (id: number) => {
remover(id);
};
return (
<div className="w-full mx-auto p-4">
<div className="w-full h-screen mx-auto p-4">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-semibold text-gray-100">
Contact settings
{t("Contact settings")}
</h2>
{!contact && (
{data && data?.total_items === 0 && (
<Button onClick={startAdd} className="flex items-center gap-2">
<Plus size={16} /> Qo'shish
<Plus size={16} /> {t("Qo'shish")}
</Button>
)}
</div>
{!contact ? (
<Card className="bg-gray-900">
<CardHeader>
<CardTitle>Hozircha kontakt ma'lumotlari qo'shilmagan</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Sayt uchun telegram, instagram, manzil, email va telefonni bu
yerda saqlang. Siz faqat bir marta qo'sha olasiz keyin
tahrirlash mumkin.
</p>
<div className="mt-4">
<Button onClick={startAdd} className="flex items-center gap-2">
<Plus size={14} /> Qo'shish
</Button>
</div>
</CardContent>
</Card>
{isLoading ? (
<div className="flex justify-center items-center h-60">
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
<span className="ml-2 text-gray-400">{t("Yuklanmoqda...")}</span>
</div>
) : (
<Card className="bg-gray-900">
<CardHeader className="flex items-center justify-between">
<CardTitle>Kontakt ma'lumotlari</CardTitle>
<div className="flex gap-2">
<Button
variant="outline"
onClick={startEdit}
className="flex items-center gap-2"
>
<Edit size={14} /> Tahrirlash
</Button>
<Button
variant="destructive"
onClick={removeContact}
className="flex items-center gap-2"
>
<Trash size={14} /> O'chirish
</Button>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<div className="text-sm text-muted-foreground">Telegram</div>
<div className="text-sm">{contact.telegram || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Instagram</div>
<div className="text-sm">{contact.instagram || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Facebook</div>
<div className="text-sm">{contact.facebook || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">LinkedIn</div>
<div className="text-sm">{contact.linkedin || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Twitter</div>
<div className="text-sm">{contact.twiter || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Manzil</div>
<div className="text-sm">{contact.address || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Email</div>
<div className="text-sm">{contact.email || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Telefonlar</div>
<div className="text-sm">
{contact.phonePrimary || "—"}
{contact.phoneSecondary ? ` • ${contact.phoneSecondary}` : ""}
<>
{data && data?.total_items === 0 ? (
<Card className="bg-gray-900">
<CardHeader>
<CardTitle>
{t("Hozircha kontakt ma'lumotlari qo'shilmagan")}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{t(
"Sayt uchun telegram, instagram, manzil, email va telefonni bu yerda saqlang. Siz faqat bir marta qo'sha olasiz — keyin tahrirlash mumkin.",
)}
</p>
<div className="mt-4">
<Button
onClick={startAdd}
className="flex items-center gap-2"
>
<Plus size={14} /> {t("Qo'shish")}
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
) : (
<>
{data &&
data.results.map((e) => (
<Card className="bg-gray-900">
<CardHeader className="flex items-center justify-between">
<CardTitle>{t("Kontakt ma'lumotlari")}</CardTitle>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => startEdit(e.id)}
className="flex items-center gap-2"
>
<Edit size={14} /> {t("Tahrirlash")}
</Button>
<Button
variant="destructive"
onClick={() => removeContact(e.id)}
className="flex items-center gap-2"
>
<Trash size={14} /> {t("O'chirish")}
</Button>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<div className="text-sm text-muted-foreground">
Telegram
</div>
<div className="text-sm">{e.telegram || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">
Instagram
</div>
<div className="text-sm">{e.instagram || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">
Facebook
</div>
<div className="text-sm">{e.facebook || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">
Twitter
</div>
<div className="text-sm">{e.twitter || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">
{t("Manzil")}
</div>
<div className="text-sm">{e.address || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">
Email
</div>
<div className="text-sm">{e.email || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">
{t("Telefon raqam")}
</div>
<p>{e.main_phone}</p>
<p>{e.other_phone}</p>
</div>
</div>
</CardContent>
</Card>
))}
</>
)}
</>
)}
{/* Dialog */}
<Dialog
open={open}
onOpenChange={(v) => {
setOpen(v);
if (!v) setEditing(false);
if (!v) setEditing(null);
}}
>
<DialogContent className="h-[90%] overflow-y-scroll">
<DialogHeader>
<DialogTitle>
{editing ? "Kontaktni tahrirlash" : "Kontakt qo'shish"}
{editing ? t("Kontaktni tahrirlash") : t("Kontakt qo'shish")}
</DialogTitle>
</DialogHeader>
@@ -233,7 +447,7 @@ export default function ContactSettings() {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 mt-4">
<div className="flex flex-col gap-2 sm:col-span-2">
<Label>Manzil</Label>
<Label>{t("Manzil")}</Label>
<Input
value={form.address || ""}
onChange={(e) => handleChange("address", e.target.value)}
@@ -282,16 +496,16 @@ export default function ContactSettings() {
/>
</div>
<div className="flex flex-col gap-2">
<Label>Asosiy telefon</Label>
<Label>{t("Asosiy telefon")}</Label>
<Input
value={form.phonePrimary || ""}
value={formatPhone(form.phonePrimary)}
onChange={(e) => handleChange("phonePrimary", e.target.value)}
/>
</div>
<div className="flex flex-col gap-2 w-full">
<Label>Qo'shimcha telefon</Label>
<Label>{t("Qo'shimcha telefon")}</Label>
<Input
value={form.phoneSecondary || ""}
value={formatPhone(form.phoneSecondary)}
onChange={(e) => handleChange("phoneSecondary", e.target.value)}
/>
</div>
@@ -302,12 +516,14 @@ export default function ContactSettings() {
variant="ghost"
onClick={() => {
setOpen(false);
setEditing(false);
setEditing(null);
}}
>
Bekor qilish
{t("Bekor qilish")}
</Button>
<Button onClick={saveContact}>
{isPending ? <Loader2 className="animate-spin" /> : t("Saqlash")}
</Button>
<Button onClick={saveContact}>Saqlash</Button>
</DialogFooter>
</DialogContent>
</Dialog>