/** * Destination Form Page * Add/Edit Destination form with clean, minimal SaaS-style design */ import React, { useState, useEffect, useMemo } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, Save, Loader2, Edit2, X, Eye } from "lucide-react"; import { __ } from "../lib/i18n"; import { usePermissions } from "../hooks/usePermissions"; import { useToast } from "../components/ui/toast"; import { fetchSettings } from "../api/settings-api"; import { apiClient } from "../lib/api-client"; import { generateSlug } from "../lib/slug"; import { buildYatraSinglePreviewUrl } from "../lib/frontend-permalink-urls"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Select } from "../components/ui/select"; import { PageHeader } from "../components/common/PageHeader"; import { Card, CardContent, CardHeader, CardTitle, } from "../components/ui/card"; import { ConditionalRender } from "../components/ui/conditional-render"; import { IconPicker, IconPickerValue } from "../components/ui/icon-picker"; import { RichTextEditor } from "../components/ui/rich-text-editor"; import { ClassificationLandingPageField, fetchPublishedPagePermalink, parseLandingPageIdFromMetadata, } from "../components/classifications/ClassificationLandingPageField"; interface DestinationFormData { name: string; slug: string; description: string; icon: { type: "icon" | "image"; value: string; } | null; status: string; seo_title: string; seo_description: string; seo_keywords: string; landing_page_id: number | null; } const DestinationForm: React.FC = () => { const queryClient = useQueryClient(); const { can } = usePermissions(); const { showToast } = useToast(); const [formData, setFormData] = useState({ name: "", slug: "", description: "", icon: null, status: "publish", seo_title: "", seo_description: "", seo_keywords: "", landing_page_id: null, }); const [errors, setErrors] = useState>({}); const [isSubmitting, setIsSubmitting] = useState(false); const [isSlugEditable, setIsSlugEditable] = useState(false); // Get action and id from URL const action = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("action") || "create"; }, []); const destinationId = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("id") ? parseInt(params.get("id") || "0") : null; }, []); const isEditMode = action === "edit" && destinationId !== null; // Fetch destination data if editing const { data: destinationData, isLoading: isLoadingDestination } = useQuery({ queryKey: ["destination", destinationId], queryFn: async () => { if (!destinationId) return null; try { const response = await apiClient.get(`/destinations/${destinationId}`); return response; } catch (error: any) { showToast( error?.message || __("Failed to load destination", "yatra"), "error", ); throw error; } }, enabled: isEditMode && can("yatra_view_trips"), }); // Fetch settings for permalink handling const { data: settings } = useQuery({ queryKey: ["settings"], queryFn: async () => { try { const response = await fetchSettings(); return response?.data || response; } catch (error) { return {}; } }, }); // Load destination data into form when editing useEffect(() => { if (destinationData && isEditMode) { setFormData({ name: destinationData.name || "", slug: destinationData.slug || "", description: destinationData.description || "", icon: (destinationData.icon as IconPickerValue) || null, status: destinationData.status || "draft", seo_title: destinationData.metadata?.seo_title || "", seo_description: destinationData.metadata?.seo_description || "", seo_keywords: destinationData.metadata?.seo_keywords || "", landing_page_id: parseLandingPageIdFromMetadata( destinationData.metadata as { landing_page_id?: unknown }, ), }); } }, [destinationData, isEditMode]); const handleNameChange = (value: string) => { // Auto-generate slug from name only in ADD mode (not in EDIT mode) // In EDIT mode, slug only changes if user explicitly edits it if (!isEditMode && !isSlugEditable) { const newSlug = generateSlug(value); setFormData((prev) => ({ ...prev, name: value, slug: newSlug, })); } else { setFormData((prev) => ({ ...prev, name: value, })); } if (errors.name) { setErrors((prev) => ({ ...prev, name: "" })); } }; const handleSlugChange = (value: string) => { // Only allow manual slug editing if edit mode is enabled if (isSlugEditable) { setFormData((prev) => ({ ...prev, slug: value })); if (errors.slug) { setErrors((prev) => ({ ...prev, slug: "" })); } } }; const handleToggleSlugEdit = () => { if (isSlugEditable) { // If disabling edit, regenerate slug from name const newSlug = generateSlug(formData.name); setFormData((prev) => ({ ...prev, slug: newSlug })); } setIsSlugEditable(!isSlugEditable); }; const handleFieldChange = ( field: keyof DestinationFormData, value: string | IconPickerValue | null, ) => { setFormData((prev) => ({ ...prev, [field]: value })); if (errors[field]) { setErrors((prev) => ({ ...prev, [field]: "" })); } }; const validateForm = (): boolean => { const newErrors: Record = {}; if (!formData.name.trim()) { newErrors.name = __("Name is required", "yatra"); } if (!formData.slug.trim()) { newErrors.slug = __("Slug is required", "yatra"); } else if (!/^[\p{L}\p{N}-]+$/u.test(formData.slug)) { newErrors.slug = __( "Slug can only contain letters, numbers, and hyphens", "yatra", ); } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; // Create/Update mutation const saveMutation = useMutation({ mutationFn: async (data: DestinationFormData) => { const payload: any = { name: data.name.trim(), slug: data.slug.trim(), description: data.description.trim(), icon: data.icon, status: data.status, seo_title: data.seo_title.trim(), seo_description: data.seo_description.trim(), seo_keywords: data.seo_keywords.trim(), landing_page_id: data.landing_page_id ?? null, }; // If slug was manually edited, add flag to preserve it if (isEditMode && isSlugEditable) { payload.preserve_slug = true; } if (isEditMode && destinationId) { return await apiClient.put(`/destinations/${destinationId}`, payload); } else { return await apiClient.post("/destinations", payload); } }, onSuccess: (response) => { queryClient.invalidateQueries({ queryKey: ["destinations"] }); queryClient.invalidateQueries({ queryKey: ["destination", destinationId], }); showToast( isEditMode ? __("Destination updated successfully", "yatra") : __("Destination created successfully", "yatra"), "success", ); setIsSubmitting(false); if (!isEditMode) { const newId = response?.id; if (newId) { window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=destinations&action=edit&id=${newId}`; } else { // Fallback to list if ID missing window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=destinations`; } } }, onError: (error: any) => { const errorMessage = error?.message || __("An error occurred while saving the destination", "yatra"); showToast(errorMessage, "error"); setIsSubmitting(false); }, }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validateForm()) { showToast(__("Please fix the form errors", "yatra"), "warning"); return; } setIsSubmitting(true); setErrors({}); saveMutation.mutate(formData); }; const handleCancel = () => { window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=destinations`; }; if (isEditMode && isLoadingDestination) { return (
{/* Header Skeleton */}
{/* Form Skeleton */}
{/* Main Fields */}
{/* Name field */}
{/* Slug field */}
{/* Description field */}
{/* Sidebar */}
); } return (
{formData.slug && ( )}
} />
{/* Main Form Fields */}
{/* Basic Information */} {__("Basic Information", "yatra")} {/* Name */}
handleNameChange(e.target.value)} placeholder={__("Enter destination name", "yatra")} className={errors.name ? "border-red-500" : ""} required /> {errors.name && (

{errors.name}

)}
{/* Slug */}
handleSlugChange(e.target.value)} placeholder={__("destination-slug", "yatra")} className={`pr-10 ${errors.slug ? "border-red-500" : ""} ${!isSlugEditable ? "bg-gray-50 dark:bg-gray-800 cursor-not-allowed" : ""}`} disabled={!isSlugEditable} required />
{errors.slug && (

{errors.slug}

)}

{isSlugEditable ? __( "Manually editing slug. Click X to cancel and regenerate from name.", "yatra", ) : __( "Auto-generated from name. Click edit icon to customize.", "yatra", )}

{/* Description */} handleFieldChange("description", value) } placeholder={__( "Write a rich description (supports formatting, lists, links...)", "yatra", )} helperText={__( "Use formatting, bullet lists, and links to create a compelling description. HTML is supported.", "yatra", )} minHeight={360} maxHeight={720} />
setFormData((prev) => ({ ...prev, landing_page_id: id })) } />
{/* Sidebar */}
{/* Status */} {__("Status", "yatra")}
{/* SEO Settings */} {__("SEO Settings", "yatra")}
handleFieldChange("seo_title", e.target.value) } placeholder={__( "e.g., {name} Destinations | Your Travel Agency", "yatra", )} className="w-full" />

{__( "Custom title for search engines. Use {name} as placeholder.", "yatra", )}