first commit
This commit is contained in:
316
src/pages/tour-settings/ui/TourSettings.tsx
Normal file
316
src/pages/tour-settings/ui/TourSettings.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
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 { useEffect, useState } from "react";
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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`,
|
||||
);
|
||||
const data = await response.json();
|
||||
return data.display_name || "";
|
||||
}
|
||||
|
||||
export default function ContactSettings() {
|
||||
const [contact, setContact] = useState<ContactInfo | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [form, setForm] = useState<ContactInfo>({});
|
||||
const [coords, setCoords] = useState({
|
||||
latitude: 41.311081,
|
||||
longitude: 69.240562,
|
||||
}); // Toshkent default
|
||||
|
||||
// Load saved contact
|
||||
useEffect(() => {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) setContact(JSON.parse(raw));
|
||||
}, []);
|
||||
|
||||
// Populate form when editing
|
||||
useEffect(() => {
|
||||
if (open && editing && contact) setForm(contact);
|
||||
if (!open && !editing) setForm({});
|
||||
}, [open, editing, contact]);
|
||||
|
||||
const handleChange = <K extends keyof ContactInfo>(
|
||||
key: K,
|
||||
value: ContactInfo[K],
|
||||
) => {
|
||||
setForm((s) => ({ ...s, [key]: value }));
|
||||
};
|
||||
|
||||
const handleMapClick = async (e: MapClickEvent) => {
|
||||
const lat = e.get("coords")[0];
|
||||
const lon = e.get("coords")[1];
|
||||
setCoords({ latitude: lat, longitude: lon });
|
||||
|
||||
const addressName = await getAddressFromCoords(lat, lon);
|
||||
setForm((s) => ({ ...s, address: addressName }));
|
||||
};
|
||||
|
||||
const saveContact = () => {
|
||||
if (!form.email && !form.phonePrimary) {
|
||||
alert("Iltimos email yoki telefon kiriting");
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(form));
|
||||
setContact(form);
|
||||
setOpen(false);
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
const startAdd = () => {
|
||||
setForm({});
|
||||
setEditing(false);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const startEdit = () => {
|
||||
setEditing(true);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const removeContact = () => {
|
||||
if (!confirm("Contact ma'lumotlarini o'chirishni xohlaysizmi?")) return;
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
setContact(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto p-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-semibold text-gray-100">
|
||||
Contact settings
|
||||
</h2>
|
||||
{!contact && (
|
||||
<Button onClick={startAdd} className="flex items-center gap-2">
|
||||
<Plus size={16} /> 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>
|
||||
) : (
|
||||
<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}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Dialog */}
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(v) => {
|
||||
setOpen(v);
|
||||
if (!v) setEditing(false);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="h-[90%] overflow-y-scroll">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editing ? "Kontaktni tahrirlash" : "Kontakt qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Map */}
|
||||
<YMaps query={{ lang: "en_RU" }}>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [coords.latitude, coords.longitude],
|
||||
zoom: 13,
|
||||
}}
|
||||
width="100%"
|
||||
height="400px"
|
||||
onClick={handleMapClick}
|
||||
>
|
||||
<Placemark geometry={[coords.latitude, coords.longitude]} />
|
||||
</Map>
|
||||
</YMaps>
|
||||
|
||||
<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>
|
||||
<Input
|
||||
value={form.address || ""}
|
||||
onChange={(e) => handleChange("address", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Telegram</Label>
|
||||
<Input
|
||||
value={form.telegram || ""}
|
||||
onChange={(e) => handleChange("telegram", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Instagram</Label>
|
||||
<Input
|
||||
value={form.instagram || ""}
|
||||
onChange={(e) => handleChange("instagram", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Linkedin</Label>
|
||||
<Input
|
||||
value={form.linkedin || ""}
|
||||
onChange={(e) => handleChange("linkedin", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Facebook</Label>
|
||||
<Input
|
||||
value={form.facebook || ""}
|
||||
onChange={(e) => handleChange("facebook", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Twitter</Label>
|
||||
<Input
|
||||
value={form.twiter || ""}
|
||||
onChange={(e) => handleChange("twiter", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
value={form.email || ""}
|
||||
onChange={(e) => handleChange("email", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Asosiy telefon</Label>
|
||||
<Input
|
||||
value={form.phonePrimary || ""}
|
||||
onChange={(e) => handleChange("phonePrimary", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Label>Qo'shimcha telefon</Label>
|
||||
<Input
|
||||
value={form.phoneSecondary || ""}
|
||||
onChange={(e) => handleChange("phoneSecondary", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-6 flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setEditing(false);
|
||||
}}
|
||||
>
|
||||
Bekor qilish
|
||||
</Button>
|
||||
<Button onClick={saveContact}>Saqlash</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user