PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / TripForm.tsx

TripForm.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.2.9, at resources/js/pages/TripForm.tsx

9,233 lines 389.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Trip Form Page - Wizard Style
3 * Multi-step form for creating/editing trips with sidebar navigation
4 */
5
6 import React, { useState, useEffect, useMemo, useRef } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8
9 // Utility function to strip HTML and sanitize text for SEO preview
10 const sanitizeTextForSEO = (text: string, maxLength: number = 160): string => {
11 if (!text) return "";
12
13 // Remove HTML tags
14 const plainText = text.replace(/<[^>]*>/g, "");
15
16 // Decode HTML entities
17 const decodedText = plainText
18 .replace(/&amp;/g, "&")
19 .replace(/&lt;/g, "<")
20 .replace(/&gt;/g, ">")
21 .replace(/&quot;/g, '"')
22 .replace(/&#39;/g, "'")
23 .replace(/&nbsp;/g, " ");
24
25 // Remove extra whitespace and trim
26 const cleanText = decodedText.replace(/\s+/g, " ").trim();
27
28 // Truncate if needed
29 if (cleanText.length > maxLength) {
30 return cleanText.substring(0, maxLength) + "...";
31 }
32
33 return cleanText;
34 };
35 import {
36 Save,
37 Loader2,
38 Sparkles,
39 Calendar,
40 Clock,
41 Image,
42 HelpCircle,
43 Search,
44 Settings,
45 CheckCircle2,
46 CheckSquare,
47 FileText,
48 DollarSign,
49 AlertCircle,
50 Plus,
51 Upload,
52 MapPin,
53 Tag,
54 GripVertical,
55 ChevronUp,
56 ChevronDown,
57 ChevronLeft,
58 ChevronRight,
59 Copy,
60 Check,
61 BookOpen,
62 Download,
63 Eye,
64 X,
65 Trash2,
66 Users,
67 Box,
68 BarChart3,
69 Database,
70 History,
71 Home,
72 Car,
73 Lightbulb,
74 } from "lucide-react";
75 import { RichTextEditor } from "../components/ui/rich-text-editor";
76 import { IconPicker, IconPickerValue } from "../components/ui/icon-picker";
77 import { __ } from "../lib/i18n";
78 import { usePermissions } from "../hooks/usePermissions";
79 import { fetchSettings } from "../api/settings-api";
80 import { apiClient } from "../lib/api-client";
81 import { wpService } from "../lib/api-client";
82 import { Button } from "../components/ui/button";
83 import { Input } from "@/components/ui/input";
84 import { DatePicker } from "@/components/ui/date-picker";
85 import { prepareWordPressMediaFrameOpen } from "../lib/wp-media-open";
86 import { Modal } from "../components/ui/modal";
87 import { Select } from "../components/ui/select";
88 import { Alert } from "../components/ui/alert";
89 import {
90 Card,
91 CardContent,
92 CardHeader,
93 CardTitle,
94 CardDescription,
95 } from "../components/ui/card";
96 import { Badge } from "../components/ui/badge";
97 import TripAttributesSection from "../components/trip-form/TripAttributesSection";
98 import { HelpText } from "../components/ui/help-text";
99 import { getCurrencySymbol } from "../data/currencies";
100 import { ItinerarySection } from "../components/trip-form/sections/ItinerarySection";
101 import { IncludedSection } from "../components/trip-form/sections/IncludedSection";
102 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
103 import { useToast } from "../components/ui/toast";
104 import { MultiSelect } from "../components/ui/multi-select";
105 import { TestimonialsSelector } from "../components/trip-form/TestimonialsSelector";
106 import { LocationPicker } from "../components/trip-form/LocationPicker";
107 import { ProFeature, ProBadge } from "../components/ProFeature";
108 import { getErrorContext } from "../lib/errors";
109 import {
110 buildYatraSinglePublicUrls,
111 isWordPressPlainPermalink,
112 } from "../lib/frontend-permalink-urls";
113
114 type SectionId =
115 | "basic" // 1. Basic Information (title, description, highlights, featured image, trip type, duration)
116 | "location" // 2. Location & Geography
117 | "duration" // 3. Schedule & Availability (renamed)
118 | "pricing" // 4. Pricing & Payment
119 | "booking" // 5. Booking Requirements
120 | "attributes" // 6. Trip Attributes
121 | "itinerary" // 7. Itinerary Builder (includes Included/Excluded)
122 | "included" // 8. What's Included/Excluded (deprecated - merged into itinerary)
123 | "media" // 9. Media & Content (gallery, video, story, testimonials)
124 | "downloads" // 9b. Downloads (Pro module)
125 | "categorization" // 10. Categorization & Tags (category, activities, difficulty, tags)
126 | "faqs" // 11. FAQs
127 | "seo" // 12. SEO Settings
128 | "advanced"; // 13. Advanced Settings (status, scheduling, frontend tabs)
129
130 interface Section {
131 id: SectionId;
132 label: string;
133 icon: React.ComponentType<{ className?: string }>;
134 required: boolean;
135 completed: boolean;
136 hasErrors?: boolean;
137 }
138
139 interface FAQ {
140 question: string;
141 answer: string;
142 }
143
144 interface TravelerCategory {
145 id: number;
146 label: string;
147 description: string;
148 age_min?: number;
149 age_max?: number;
150 status: "active" | "inactive" | "publish" | "draft";
151 pricing_mode?: "per_person" | "per_group";
152 min_pax?: number | null;
153 max_pax?: number | null;
154 }
155
156 interface PriceType {
157 category_id: number;
158 original_price: string;
159 discounted_price: string;
160 is_default?: boolean;
161 }
162
163 interface TripCategoryOption {
164 id: number;
165 name: string;
166 slug?: string;
167 description?: string;
168 parent_id: number | null;
169 status?: string;
170 subcategories?: TripCategoryOption[];
171 }
172
173 interface DifficultyLevelOption {
174 id: number;
175 name: string;
176 slug?: string;
177 description?: string;
178 level_order?: number;
179 status?: string;
180 }
181
182 interface TripAmenityItem {
183 title: string;
184 description: string;
185 }
186
187 interface DownloadableItem {
188 id?: number | null;
189 title: string;
190 description: string;
191 attachment_id: number | null;
192 attachment_url?: string;
193 attachment_title?: string;
194 visibility: "public" | "logged_in" | "booked_only";
195 enabled: boolean;
196 sort_order?: number;
197 }
198
199 const buildCategoryOptionNodes = (
200 categories: TripCategoryOption[],
201 depth = 0,
202 ): React.ReactNode[] => {
203 return categories.flatMap((category) => {
204 const optionValue =
205 category.slug || category.name || category.id?.toString() || "";
206 const labelPrefix =
207 depth > 0 ? `${"-- ".repeat(depth)}${category.name}` : category.name;
208 const nodes: React.ReactNode[] = [
209 <option key={`category-option-${category.id}`} value={optionValue}>
210 {labelPrefix}
211 </option>,
212 ];
213
214 if (
215 Array.isArray(category.subcategories) &&
216 category.subcategories.length > 0
217 ) {
218 nodes.push(
219 ...buildCategoryOptionNodes(category.subcategories, depth + 1),
220 );
221 }
222
223 return nodes;
224 });
225 };
226
227 const extractArrayPayload = (payload: any): any[] => {
228 if (!payload) return [];
229 if (Array.isArray(payload)) return payload;
230 if (Array.isArray(payload.data)) return payload.data;
231 if (Array.isArray(payload?.data?.data)) return payload.data.data;
232 return [];
233 };
234
235 const normalizeAmenityItems = (items: unknown): TripAmenityItem[] => {
236 if (!items) return [];
237 if (Array.isArray(items)) {
238 return items
239 .map((item) => {
240 if (typeof item === "string") {
241 return { title: item, description: "" };
242 }
243 if (item && typeof item === "object") {
244 const obj = item as Partial<TripAmenityItem>;
245 return {
246 title: (obj.title ?? "").toString(),
247 description: (obj.description ?? "").toString(),
248 };
249 }
250 return { title: String(item), description: "" };
251 })
252 .filter((item) => item.title.trim().length > 0);
253 }
254 if (typeof items === "string") {
255 try {
256 const parsed = JSON.parse(items);
257 if (Array.isArray(parsed)) {
258 return normalizeAmenityItems(parsed);
259 }
260 } catch {
261 return [{ title: items, description: "" }];
262 }
263 }
264 return [];
265 };
266
267 interface TimeSlot {
268 id: string;
269 time: string; // HH:MM format
270 label: string;
271 }
272
273 interface TripFormData {
274 // Overview
275 title: string;
276 slug: string;
277 description: string;
278 highlights: string[];
279 trip_details: string;
280 short_description: string;
281 what_makes_special: string; // What Makes This Trip Special
282 trip_story: string; // Trip Story/Narrative
283 video_url: string; // Video Embed URL
284 virtual_tour_url: string; // 360° Virtual Tour URL
285 testimonial_review_ids: number[]; // Array of review IDs to display as testimonials
286
287 // Location & Geography
288 destinations: number[]; // Array of destination IDs
289 starting_location: string;
290 ending_location: string;
291 countries: string[];
292 regions: string[];
293 starting_latitude: string;
294 starting_longitude: string;
295 ending_latitude: string;
296 ending_longitude: string;
297 landmarks: string[]; // Geographic Tags - Landmarks
298
299 // Duration & Schedule
300 trip_type: "single_day" | "multi_day" | "flexible";
301 duration_days: string;
302 duration_nights: string;
303 available_from: string;
304 available_to: string;
305 booking_window_days: string;
306 seasonal_availability: string;
307 best_season: string; // Best season indicator
308 peak_season: string; // Peak season indicator
309 off_season: string; // Off-season indicator
310
311 // Activity & Category
312 activity_types: number[]; // Array of activity IDs (changed from string[])
313 difficulty_level: string;
314 trip_category: number[]; // Array of category IDs
315 tags: string[];
316 featured_priority: "none" | "featured" | "new" | "limited"; // Featured Priority
317
318 // Accommodation
319 accommodation_type: string;
320 meal_plan: string;
321 accommodation_details: string;
322
323 // Transportation
324 transportation_included: boolean;
325 pickup_location: string;
326 dropoff_location: string;
327 transportation_details: string;
328
329 // Pricing
330 pricing_type: "regular" | "traveler_based";
331 original_price: string;
332 discounted_price: string;
333 price_types: PriceType[];
334 deposit_amount: string;
335 deposit_percentage: string;
336 payment_terms: string;
337 max_travelers: string;
338 min_travelers: string;
339 booking_deadline_hours: string;
340 cancellation_policy: string;
341 age_min: string;
342 age_max: string;
343 physical_requirements: string;
344 visa_requirements: string;
345 vaccination_requirements: string;
346 disable_booking: boolean; // Pro: enquiry-only mode (stored in custom_fields)
347
348 // Fallback Settings (for trips without availability dates/rules)
349 has_default_time_slots: boolean; // For day tours: enable multiple time slots
350 default_time_slots: TimeSlot[]; // Array of time slot objects for day tours
351 departure_time: string; // Default departure time
352
353 // Included/Excluded
354 included_items: TripAmenityItem[];
355 excluded_items: TripAmenityItem[];
356
357 // Attributes & Properties
358 attributes: Record<number, any>; // attribute_id -> value mapping
359
360 // Itinerary
361 itinerary_days: ItineraryDay[];
362
363 // Gallery
364 gallery_images: Array<{
365 id: number;
366 url: string;
367 thumbnail_url?: string;
368 alt_text?: string;
369 caption?: string;
370 }>;
371 featured_image: number | null;
372
373 // Downloads
374 downloadable_items: DownloadableItem[];
375
376 // FAQs
377 faqs: FAQ[];
378
379 // Frontend Tabs
380 frontend_tabs: FrontendTab[];
381
382 // Availability
383 availability_dates: AvailabilityDate[];
384
385 // Status & Lifecycle
386 status:
387 | "draft"
388 | "review"
389 | "approved"
390 | "publish"
391 | "archived"
392 | "suspended";
393 scheduled_publish_date: string; // Scheduled Publishing
394 scheduled_unpublish_date: string; // Scheduled Unpublishing
395 version: number; // Version Control
396 seasonal_auto_enable: boolean; // Auto-enable/disable based on dates
397 seasonal_enable_date: string; // Date to auto-enable
398 seasonal_disable_date: string; // Date to auto-disable
399
400 // SEO
401 meta_title: string;
402 meta_description: string;
403 meta_keywords: string;
404 }
405
406 interface FrontendTab {
407 id: string;
408 label: string;
409 enabled: boolean;
410 order: number;
411 content_type:
412 | "overview"
413 | "itinerary"
414 | "included_excluded"
415 | "location"
416 | "important_info"
417 | "downloads"
418 | "faq"
419 | "trip_story"
420 | "what_makes_special"
421 | "testimonials"
422 | "reviews"
423 | "downloads"
424 | "custom"
425 | "general"
426 | "gallery"
427 | "faqs";
428 custom_content?: string;
429 icon?: IconPickerValue | null;
430 }
431
432 interface AvailabilityDate {
433 id: string;
434 /** Present when availability row is tied to a specific trip */
435 trip_id?: number;
436 departure_date: string;
437 arrival_date: string;
438 seats_remaining: string;
439 original_price: string;
440 discounted_price: string;
441 discount_percentage: string;
442 status: "available" | "sold_out" | "limited" | "closed";
443 from_location?: string;
444 to_location?: string;
445 from_latitude?: string;
446 from_longitude?: string;
447 to_latitude?: string;
448 to_longitude?: string;
449 }
450
451 interface ItineraryEntry {
452 id: string;
453 day: number;
454 day_title?: string;
455 // Use IDs to match ItineraryForm structure
456 item_type_id: string; // ID of the item type (Activity, Meal, Accommodation, Transportation, Rest)
457 item_id: string; // ID of the specific item (Hiking, Breakfast, Hotel, etc.)
458 // Keep legacy fields for backward compatibility
459 item_type?: "Meal" | "Activity" | "Accommodation" | "Transportation";
460 item_name?: string;
461 item_icon?: string;
462 // Entry details
463 title: string;
464 description: string;
465 location?: string;
466 location_latitude?: string;
467 location_longitude?: string;
468 duration?: string;
469 start_time: string;
470 end_time: string;
471 time_type: "exact" | "approximate" | "all_day" | "flexible";
472 cost?: string;
473 cost_per_person: boolean;
474 notes?: string;
475 included_items: string[];
476 excluded_items: string[];
477 images: string[];
478 status?: "active" | "inactive";
479 }
480
481 interface ItineraryDay {
482 day: number;
483 day_title?: string;
484 entries: ItineraryEntry[];
485 }
486
487 // getCurrencySymbol is now imported from '../data/currencies'
488
489 const TripForm: React.FC = () => {
490 const queryClient = useQueryClient();
491 const { can, isPro } = usePermissions();
492 const { showToast } = useToast();
493 // Downloads is now a FREE feature - always show the UI
494 const showDownloadsUI = true;
495
496 const [featuredImagePreview, setFeaturedImagePreview] = useState<string>("");
497 const [isResolvingFeaturedImage, setIsResolvingFeaturedImage] =
498 useState<boolean>(false);
499 const featuredImageCache = useRef<Record<number, string>>({});
500 const mediaBaseUrl = useMemo(() => {
501 const apiUrl = (window as any)?.yatraAdmin?.apiUrl;
502 return apiUrl ? apiUrl.replace(/\/yatra\/v1\/?$/, "") : "";
503 }, []);
504
505 // Get section from URL on initial load
506 const getInitialSection = (): SectionId => {
507 const urlParams = new URLSearchParams(window.location.search);
508 const sectionFromUrl = urlParams.get("section") as SectionId | null;
509 const validSections: SectionId[] = [
510 "basic",
511 "location",
512 "duration",
513 "pricing",
514 "booking",
515 "attributes",
516 "itinerary",
517 "included",
518 "media",
519 "downloads",
520 "categorization",
521 "faqs",
522 "seo",
523 "advanced",
524 ];
525 if (sectionFromUrl && validSections.includes(sectionFromUrl)) {
526 return sectionFromUrl;
527 }
528 return "basic";
529 };
530
531 const [currentSection, setCurrentSection] =
532 useState<SectionId>(getInitialSection);
533 const [visitedSections, setVisitedSections] = useState<Set<SectionId>>(
534 () => new Set([getInitialSection()]),
535 );
536 const [showCategorySelector, setShowCategorySelector] = useState(false);
537 const [showLandmarkDialog, setShowLandmarkDialog] = useState(false);
538 const [landmarkInput, setLandmarkInput] = useState("");
539
540 // Sub-tab state for Trip Details section (itinerary)
541 const [tripDetailsTab, setTripDetailsTab] = useState<
542 "itinerary" | "included"
543 >("itinerary");
544
545 // Track visited sections for lazy loading and update URL
546 useEffect(() => {
547 setVisitedSections((prev) => new Set([...prev, currentSection]));
548
549 // Update URL with current section (without page reload)
550 const url = new URL(window.location.href);
551 url.searchParams.set("section", currentSection);
552 window.history.replaceState({}, "", url.toString());
553 }, [currentSection]);
554
555 // Modal states for adding items
556 const [showHighlightModal, setShowHighlightModal] = useState(false);
557 const [modalInput, setModalInput] = useState({
558 text: "",
559 question: "",
560 answer: "",
561 });
562
563 // Revision states
564 const [showRevisionsDialog, setShowRevisionsDialog] = useState(false);
565 const [selectedRevisionId, setSelectedRevisionId] = useState<number | null>(
566 null,
567 );
568 const [showRevisionConfirm, setShowRevisionConfirm] = useState(false);
569
570 // UI Enhancement states
571 const [simpleMode, setSimpleMode] = useState(false); // Quick Start mode
572 const [showSlugPreview, setShowSlugPreview] = useState(true);
573 const [dummyDataIndex, setDummyDataIndex] = useState(0); // Track which dummy data set to use
574
575 // Static/dummy revisions data for UI only
576 const dummyRevisions = [
577 {
578 id: 1,
579 version: 3,
580 created_by_name: "Admin User",
581 created_at: new Date().toISOString(),
582 },
583 {
584 id: 2,
585 version: 2,
586 created_by_name: "Admin User",
587 created_at: new Date(Date.now() - 86400000).toISOString(),
588 },
589 {
590 id: 3,
591 version: 1,
592 created_by_name: "Admin User",
593 created_at: new Date(Date.now() - 172800000).toISOString(),
594 },
595 ];
596
597 // Comprehensive dummy trip data sets (3 different trips)
598 const dummyTripsData: TripFormData[] = [
599 {
600 // Trip 1: Beach Adventure
601 title: "7-Day Bali Beach Adventure",
602 slug: "7-day-bali-beach-adventure",
603 description:
604 "Escape to paradise with our 7-day Bali beach adventure. Experience pristine beaches, explore ancient temples, enjoy world-class spa treatments, and immerse yourself in the rich Balinese culture. This carefully curated journey combines relaxation with adventure, offering the perfect balance for travelers seeking both tranquility and excitement.",
605 short_description:
606 "Escape to paradise with our 7-day Bali beach adventure featuring pristine beaches, ancient temples, and cultural immersion.",
607 highlights: [
608 "Pristine white sand beaches",
609 "Ancient temple visits",
610 "Traditional spa treatments",
611 "Cultural dance performances",
612 "Sunset dinners by the ocean",
613 ],
614 trip_details:
615 "This comprehensive 7-day journey takes you through the best of Bali. Start your adventure in Seminyak with its trendy beach clubs and world-class restaurants. Visit the iconic Tanah Lot Temple perched on a rock formation in the sea. Explore the cultural heart of Ubud, known for its rice terraces, monkey forest, and art galleries. Enjoy traditional Balinese spa treatments and witness captivating cultural performances. End your trip with a relaxing stay at a beachfront resort where you can unwind and reflect on your incredible journey.",
616 what_makes_special:
617 "This trip offers exclusive access to private beach areas, personalized cultural experiences, and a perfect blend of relaxation and adventure. Our local guides share insider knowledge and hidden gems that most tourists never discover.",
618 trip_story:
619 "Imagine waking up to the gentle sound of waves lapping against the shore. As the sun rises over the horizon, you step onto your private balcony to witness a breathtaking sunrise. Your day begins with a traditional Balinese breakfast before heading out to explore ancient temples that have stood for centuries. In the afternoon, you find yourself surrounded by emerald-green rice terraces, learning about traditional farming methods from local farmers. As evening approaches, you're treated to a mesmerizing cultural dance performance followed by a candlelit dinner on the beach. This is more than a vacation—it's a journey into the heart and soul of Bali.",
620 video_url: "https://www.youtube.com/watch?v=example1",
621 virtual_tour_url: "",
622 testimonial_review_ids: [], // Will be populated from actual reviews
623 destinations: [], // Will be populated based on available destinations
624 starting_location: "Ngurah Rai International Airport (DPS)",
625 ending_location: "Seminyak Beach Resort",
626 countries: ["Indonesia"],
627 regions: ["Bali"],
628 starting_latitude: "-8.3405",
629 starting_longitude: "115.0920",
630 ending_latitude: "-8.5069",
631 ending_longitude: "115.2625",
632 landmarks: [
633 "Tanah Lot Temple",
634 "Ubud Monkey Forest",
635 "Tegallalang Rice Terrace",
636 "Seminyak Beach",
637 ],
638 trip_type: "multi_day",
639 duration_days: "7",
640 duration_nights: "6",
641 available_from: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
642 .toISOString()
643 .split("T")[0], // 30 days from now
644 available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
645 .toISOString()
646 .split("T")[0], // 1 year from now
647 booking_window_days: "30",
648 seasonal_availability: "Year-round",
649 best_season: "April to October",
650 peak_season: "July to August",
651 off_season: "November to March",
652 activity_types: [], // Will be populated based on available activities
653 difficulty_level: "",
654 trip_category: [],
655 tags: ["family-friendly", "beach", "relaxation", "cultural", "spa"],
656 featured_priority: "featured",
657 accommodation_type: "Resort",
658 meal_plan: "breakfast",
659 accommodation_details:
660 "4-star beachfront resort with private balconies, infinity pool, and spa facilities",
661 transportation_included: true,
662 pickup_location: "Ngurah Rai International Airport",
663 dropoff_location: "Seminyak Beach Resort",
664 transportation_details:
665 "Private air-conditioned vehicle with professional driver",
666 pricing_type: "traveler_based",
667 original_price: "",
668 discounted_price: "",
669 price_types: [
670 { category_id: 1, original_price: "1250", discounted_price: "" },
671 { category_id: 2, original_price: "625", discounted_price: "" },
672 { category_id: 3, original_price: "0", discounted_price: "" },
673 ],
674 deposit_amount: "300",
675 deposit_percentage: "",
676 payment_terms:
677 "50% deposit required at booking, remaining 50% due 30 days before departure",
678 max_travelers: "12",
679 min_travelers: "2",
680 booking_deadline_hours: "24",
681 cancellation_policy:
682 "Free cancellation up to 30 days before departure. 50% refund for cancellations 15-30 days before. No refund for cancellations less than 15 days before.",
683 age_min: "8",
684 age_max: "",
685 physical_requirements:
686 "Moderate fitness level required. Some walking involved but no strenuous activities.",
687 visa_requirements:
688 "Visa on arrival available for most nationalities. Valid passport required with at least 6 months validity.",
689 vaccination_requirements:
690 "No mandatory vaccinations. Recommended: Hepatitis A, Typhoid, and routine vaccinations.",
691 disable_booking: false,
692 has_default_time_slots: false,
693 default_time_slots: [],
694 departure_time: "",
695 included_items: [
696 {
697 title: "Accommodation",
698 description: "6 nights at 4-star beachfront resort",
699 },
700 { title: "Breakfast", description: "Daily breakfast included" },
701 {
702 title: "Airport transfers",
703 description: "Private transfers to and from airport",
704 },
705 {
706 title: "Temple visits",
707 description: "Entrance fees to all temples included",
708 },
709 {
710 title: "Cultural performances",
711 description: "Traditional dance show tickets",
712 },
713 {
714 title: "Professional guide",
715 description: "English-speaking local guide",
716 },
717 ],
718 excluded_items: [
719 {
720 title: "International flights",
721 description: "Flights to and from Bali not included",
722 },
723 {
724 title: "Lunch and dinner",
725 description: "Meals other than breakfast",
726 },
727 {
728 title: "Travel insurance",
729 description: "Travel insurance recommended but not included",
730 },
731 {
732 title: "Personal expenses",
733 description: "Souvenirs, tips, and personal items",
734 },
735 ],
736 itinerary_days: [],
737 gallery_images: [],
738 featured_image: null,
739 downloadable_items: [],
740 faqs: [
741 {
742 question: "What is the best time to visit Bali?",
743 answer:
744 "The best time to visit Bali is during the dry season from April to October, when you can expect sunny days and minimal rainfall.",
745 },
746 {
747 question: "Do I need a visa?",
748 answer:
749 "Most nationalities can get a visa on arrival at the airport. A valid passport with at least 6 months validity is required.",
750 },
751 {
752 question: "What should I pack?",
753 answer:
754 "Pack light, breathable clothing, swimwear, sunscreen, insect repellent, and comfortable walking shoes. Modest clothing is required for temple visits.",
755 },
756 ],
757 frontend_tabs: [
758 {
759 id: "overview",
760 label: "Overview",
761 enabled: true,
762 order: 1,
763 content_type: "general",
764 icon: { type: "icon", value: "book-open" },
765 },
766 {
767 id: "itinerary",
768 label: "Itinerary",
769 enabled: true,
770 order: 2,
771 content_type: "itinerary",
772 icon: { type: "icon", value: "calendar" },
773 },
774 {
775 id: "included",
776 label: "Included",
777 enabled: true,
778 order: 3,
779 content_type: "included_excluded",
780 icon: { type: "icon", value: "check" },
781 },
782 {
783 id: "location",
784 label: "Location",
785 enabled: true,
786 order: 4,
787 content_type: "gallery",
788 icon: { type: "icon", value: "map-pin" },
789 },
790 {
791 id: "important_info",
792 label: "Important Info",
793 enabled: true,
794 order: 5,
795 content_type: "general",
796 icon: { type: "icon", value: "file-text" },
797 },
798 {
799 id: "downloads",
800 label: "Downloads",
801 enabled: true,
802 order: 6,
803 content_type: "downloads",
804 icon: { type: "icon", value: "download" },
805 },
806 {
807 id: "faq",
808 label: "FAQ",
809 enabled: true,
810 order: 7,
811 content_type: "faqs",
812 icon: { type: "icon", value: "help-circle" },
813 },
814 {
815 id: "trip_story",
816 label: "Story",
817 enabled: true,
818 order: 8,
819 content_type: "custom",
820 custom_content: "",
821 icon: { type: "icon", value: "book" },
822 },
823 {
824 id: "what_makes_special",
825 label: "Special",
826 enabled: true,
827 order: 9,
828 content_type: "custom",
829 custom_content: "",
830 icon: { type: "icon", value: "star" },
831 },
832 {
833 id: "testimonials",
834 label: "Testimonials",
835 enabled: true,
836 order: 10,
837 content_type: "reviews",
838 icon: { type: "icon", value: "message-circle" },
839 },
840 ],
841 availability_dates: [],
842 status: "draft",
843 scheduled_publish_date: "",
844 scheduled_unpublish_date: "",
845 version: 1,
846 seasonal_auto_enable: false,
847 seasonal_enable_date: "",
848 seasonal_disable_date: "",
849 meta_title: "7-Day Bali Beach Adventure | Luxury Beach Resort Experience",
850 meta_description:
851 "Experience the best of Bali with our 7-day beach adventure. Pristine beaches, ancient temples, cultural immersion, and world-class spa treatments await.",
852 meta_keywords:
853 "Bali, beach vacation, cultural tour, spa retreat, Indonesia travel",
854 attributes: {},
855 },
856 {
857 // Trip 2: Mountain Trekking Adventure
858 title: "Everest Base Camp Trek - 14 Days",
859 slug: "everest-base-camp-trek-14-days",
860 description:
861 "Embark on the adventure of a lifetime with our 14-day Everest Base Camp trek. This challenging yet rewarding journey takes you through the heart of the Himalayas, passing through traditional Sherpa villages, ancient monasteries, and breathtaking mountain landscapes. Experience the rich culture of the Khumbu region while pushing your limits to reach the base of the world's highest mountain.",
862 short_description:
863 "Embark on the adventure of a lifetime with our 14-day Everest Base Camp trek through the heart of the Himalayas.",
864 highlights: [
865 "Trek to Everest Base Camp (5,364m)",
866 "Visit ancient Buddhist monasteries",
867 "Experience Sherpa culture",
868 "Breathtaking mountain views",
869 "Professional mountain guides",
870 ],
871 trip_details:
872 "This 14-day trekking adventure begins in Kathmandu, where you'll prepare for your journey and meet your experienced guides. Fly to Lukla, the gateway to the Khumbu region, and begin your trek through the stunning Himalayan landscape. Pass through traditional Sherpa villages like Namche Bazaar, Tengboche, and Dingboche, each offering unique cultural experiences and acclimatization opportunities. Visit ancient monasteries, learn about Sherpa traditions, and witness the daily life of mountain communities. The journey culminates at Everest Base Camp, where you'll stand in the shadow of the world's highest peak. Along the way, you'll be treated to spectacular views of peaks like Ama Dablam, Lhotse, and of course, Mount Everest itself.",
873 what_makes_special:
874 "Our trek includes experienced mountain guides, proper acclimatization schedules, high-altitude porters, and comprehensive safety equipment. We prioritize responsible tourism, supporting local communities and ensuring minimal environmental impact.",
875 trip_story:
876 "The crisp mountain air fills your lungs as you take your first steps on the trail. With each passing day, the mountains grow larger, the air thinner, and the sense of accomplishment greater. You wake before dawn to witness the sun painting the peaks in shades of gold and pink. You share meals with Sherpa families, learning about their way of life and the challenges they face in this harsh yet beautiful environment. As you approach Base Camp, the anticipation builds. When you finally arrive, standing at 5,364 meters with Everest towering above, you realize this is more than a trek—it's a transformation. The journey changes you, teaching resilience, appreciation for nature, and respect for the mountains and the people who call them home.",
877 video_url: "https://www.youtube.com/watch?v=example2",
878 virtual_tour_url: "",
879 testimonial_review_ids: [], // Will be populated from actual reviews
880 destinations: [], // Will be populated
881 starting_location: "Kathmandu International Airport",
882 ending_location: "Lukla Airport",
883 countries: ["Nepal"],
884 regions: ["Khumbu Region"],
885 starting_latitude: "27.9881",
886 starting_longitude: "86.9250",
887 ending_latitude: "27.6837",
888 ending_longitude: "86.7330",
889 landmarks: [
890 "Mount Everest",
891 "Namche Bazaar",
892 "Tengboche Monastery",
893 "Kala Patthar",
894 ],
895 trip_type: "multi_day",
896 duration_days: "14",
897 duration_nights: "13",
898 available_from: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000)
899 .toISOString()
900 .split("T")[0],
901 available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
902 .toISOString()
903 .split("T")[0],
904 booking_window_days: "60",
905 seasonal_availability: "March to May, September to November",
906 best_season: "October to November",
907 peak_season: "October to November",
908 off_season: "December to February, June to August",
909 activity_types: [],
910 difficulty_level: "",
911 trip_category: [],
912 tags: ["trekking", "mountains", "adventure", "challenging", "everest"],
913 featured_priority: "featured",
914 accommodation_type: "Teahouse",
915 meal_plan: "full_board",
916 accommodation_details:
917 "Traditional teahouses along the route with basic amenities. Rooms are shared, and facilities become more basic as altitude increases.",
918 transportation_included: true,
919 pickup_location: "Kathmandu International Airport",
920 dropoff_location: "Lukla Airport",
921 transportation_details:
922 "Domestic flights Kathmandu-Lukla-Kathmandu included. Airport transfers included.",
923 pricing_type: "regular",
924 original_price: "1899",
925 discounted_price: "1699",
926 deposit_amount: "500",
927 deposit_percentage: "",
928 payment_terms:
929 "50% deposit required at booking, remaining 50% due 60 days before departure",
930 max_travelers: "12",
931 min_travelers: "2",
932 booking_deadline_hours: "24",
933 cancellation_policy:
934 "Free cancellation up to 60 days before departure. 50% refund for cancellations 30-60 days before. No refund for cancellations less than 30 days before.",
935 age_min: "18",
936 age_max: "65",
937 physical_requirements:
938 "Excellent physical fitness required. Previous trekking experience recommended. Must be able to walk 6-8 hours daily at high altitude.",
939 visa_requirements:
940 "Tourist visa required for Nepal. Can be obtained on arrival at airport or in advance. Valid passport required.",
941 vaccination_requirements:
942 "Recommended: Hepatitis A, Typhoid, Japanese Encephalitis, and routine vaccinations. Consult with travel health clinic.",
943 disable_booking: false,
944 has_default_time_slots: false,
945 default_time_slots: [],
946 departure_time: "",
947 included_items: [
948 {
949 title: "Accommodation",
950 description: "13 nights in teahouses along the route",
951 },
952 {
953 title: "All meals",
954 description: "Breakfast, lunch, and dinner included",
955 },
956 {
957 title: "Professional guides",
958 description: "Experienced mountain guides and porters",
959 },
960 {
961 title: "Permits",
962 description: "TIMS and Sagarmatha National Park permits",
963 },
964 {
965 title: "Domestic flights",
966 description: "Kathmandu-Lukla-Kathmandu flights",
967 },
968 {
969 title: "Equipment",
970 description: "Sleeping bag and down jacket rental",
971 },
972 ],
973 excluded_items: [
974 {
975 title: "International flights",
976 description: "Flights to and from Kathmandu",
977 },
978 {
979 title: "Travel insurance",
980 description: "Comprehensive travel and medical insurance required",
981 },
982 {
983 title: "Personal equipment",
984 description: "Trekking boots, clothing, and personal items",
985 },
986 {
987 title: "Tips",
988 description: "Tips for guides and porters (recommended)",
989 },
990 {
991 title: "Personal expenses",
992 description: "Drinks, snacks, and souvenirs",
993 },
994 ],
995 itinerary_days: [],
996 gallery_images: [],
997 featured_image: null,
998 downloadable_items: [],
999 faqs: [
1000 {
1001 question: "How difficult is the trek?",
1002 answer:
1003 "This is a challenging trek requiring excellent physical fitness. You'll be walking 6-8 hours daily at high altitude. Previous trekking experience is recommended.",
1004 },
1005 {
1006 question: "What is the altitude at Base Camp?",
1007 answer:
1008 "Everest Base Camp is located at 5,364 meters (17,598 feet) above sea level.",
1009 },
1010 {
1011 question: "What happens if I get altitude sickness?",
1012 answer:
1013 "Our guides are trained to recognize and treat altitude sickness. We have proper acclimatization schedules and emergency descent plans in place.",
1014 },
1015 ],
1016 frontend_tabs: [
1017 {
1018 id: "overview",
1019 label: "Overview",
1020 enabled: true,
1021 order: 1,
1022 content_type: "general",
1023 icon: { type: "icon", value: "book-open" },
1024 },
1025 {
1026 id: "itinerary",
1027 label: "Itinerary",
1028 enabled: true,
1029 order: 2,
1030 content_type: "itinerary",
1031 icon: { type: "icon", value: "calendar" },
1032 },
1033 {
1034 id: "included",
1035 label: "Included",
1036 enabled: true,
1037 order: 3,
1038 content_type: "included_excluded",
1039 icon: { type: "icon", value: "check" },
1040 },
1041 {
1042 id: "location",
1043 label: "Location",
1044 enabled: true,
1045 order: 4,
1046 content_type: "gallery",
1047 icon: { type: "icon", value: "map-pin" },
1048 },
1049 {
1050 id: "important_info",
1051 label: "Important Info",
1052 enabled: true,
1053 order: 5,
1054 content_type: "general",
1055 icon: { type: "icon", value: "info" },
1056 },
1057 {
1058 id: "downloads",
1059 label: "Downloads",
1060 enabled: true,
1061 order: 6,
1062 content_type: "downloads",
1063 icon: { type: "icon", value: "download" },
1064 },
1065 {
1066 id: "faq",
1067 label: "FAQ",
1068 enabled: true,
1069 order: 7,
1070 content_type: "faqs",
1071 icon: { type: "icon", value: "help-circle" },
1072 },
1073 {
1074 id: "trip_story",
1075 label: "Story",
1076 enabled: true,
1077 order: 8,
1078 content_type: "custom",
1079 custom_content: "",
1080 icon: { type: "icon", value: "book" },
1081 },
1082 {
1083 id: "what_makes_special",
1084 label: "Special",
1085 enabled: true,
1086 order: 9,
1087 content_type: "custom",
1088 custom_content: "",
1089 icon: { type: "icon", value: "star" },
1090 },
1091 {
1092 id: "testimonials",
1093 label: "Testimonials",
1094 enabled: true,
1095 order: 10,
1096 content_type: "reviews",
1097 icon: { type: "icon", value: "message-circle" },
1098 },
1099 ],
1100 availability_dates: [],
1101 status: "draft",
1102 scheduled_publish_date: "",
1103 scheduled_unpublish_date: "",
1104 version: 1,
1105 seasonal_auto_enable: false,
1106 seasonal_enable_date: "",
1107 seasonal_disable_date: "",
1108 meta_title:
1109 "Everest Base Camp Trek - 14 Days | Ultimate Himalayan Adventure",
1110 meta_description:
1111 "Embark on the adventure of a lifetime with our 14-day Everest Base Camp trek. Experience Sherpa culture, ancient monasteries, and breathtaking mountain views.",
1112 meta_keywords:
1113 "Everest Base Camp, trekking, Nepal, Himalayas, adventure travel, mountain trek",
1114 attributes: {},
1115 price_types: [],
1116 },
1117 {
1118 // Trip 3: European City Tour
1119 title: "European Grand Tour - 10 Days",
1120 slug: "european-grand-tour-10-days",
1121 description:
1122 "Discover the best of Europe with our 10-day grand tour covering Paris, Rome, and Barcelona. Experience world-famous landmarks, indulge in exquisite cuisine, explore rich history and art, and immerse yourself in diverse European cultures. This carefully crafted journey takes you through three of Europe's most iconic cities, each offering unique experiences and unforgettable memories.",
1123 short_description:
1124 "Discover the best of Europe with our 10-day grand tour covering Paris, Rome, and Barcelona.",
1125 highlights: [
1126 "Eiffel Tower and Louvre Museum",
1127 "Colosseum and Vatican City",
1128 "Sagrada Familia and Park Güell",
1129 "World-class cuisine",
1130 "Professional local guides",
1131 ],
1132 trip_details:
1133 "Begin your European adventure in the City of Light—Paris. Explore iconic landmarks like the Eiffel Tower, Louvre Museum, and Notre-Dame Cathedral. Stroll along the Champs-Élysées, enjoy a Seine River cruise, and indulge in French pastries at charming cafés. Next, travel to Rome, the Eternal City, where ancient history comes alive. Visit the Colosseum, Roman Forum, and Vatican City with its stunning Sistine Chapel. Enjoy authentic Italian cuisine and gelato while exploring cobblestone streets. Conclude your journey in Barcelona, Spain's vibrant cultural capital. Admire Gaudí's architectural masterpieces including the Sagrada Familia and Park Güell. Experience the lively atmosphere of Las Ramblas, enjoy tapas and sangria, and relax on beautiful Mediterranean beaches.",
1134 what_makes_special:
1135 "This tour includes skip-the-line tickets to major attractions, private guided tours in each city, and carefully selected accommodations in prime locations. Our small group size ensures personalized attention and authentic local experiences.",
1136 trip_story:
1137 "Your European adventure begins as you step off the plane in Paris, greeted by the elegant architecture and romantic atmosphere that has inspired artists for centuries. Each day brings new discoveries—from the artistic treasures of the Louvre to the bohemian charm of Montmartre. In Rome, you walk in the footsteps of emperors and gladiators, feeling the weight of history in every ancient stone. The Vatican's art and architecture leave you in awe, while a simple plate of pasta in a local trattoria reminds you that the best experiences are often the simplest. Barcelona welcomes you with its unique blend of Gothic and Modernist architecture, vibrant street life, and Mediterranean warmth. As you watch the sunset from Park Güell, you realize that this journey has not just shown you three cities—it has shown you three different ways of living, three different approaches to art and culture, and three different reasons to fall in love with Europe.",
1138 video_url: "https://www.youtube.com/watch?v=example3",
1139 virtual_tour_url: "",
1140 testimonial_review_ids: [], // Will be populated from actual reviews
1141 destinations: [],
1142 starting_location: "Charles de Gaulle Airport (CDG)",
1143 ending_location: "El Prat Airport (BCN)",
1144 countries: ["France", "Italy", "Spain"],
1145 regions: ["Île-de-France", "Lazio", "Catalonia"],
1146 starting_latitude: "48.8566",
1147 starting_longitude: "2.3522",
1148 ending_latitude: "41.3792",
1149 ending_longitude: "2.1281",
1150 landmarks: [
1151 "Eiffel Tower",
1152 "Colosseum",
1153 "Sagrada Familia",
1154 "Louvre Museum",
1155 "Vatican City",
1156 ],
1157 trip_type: "multi_day",
1158 duration_days: "10",
1159 duration_nights: "9",
1160 available_from: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000)
1161 .toISOString()
1162 .split("T")[0],
1163 available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
1164 .toISOString()
1165 .split("T")[0],
1166 booking_window_days: "45",
1167 seasonal_availability: "Year-round",
1168 best_season: "April to June, September to October",
1169 peak_season: "June to August",
1170 off_season: "November to March",
1171 activity_types: [],
1172 difficulty_level: "",
1173 trip_category: [],
1174 tags: ["cultural", "city-tour", "history", "art", "food"],
1175 featured_priority: "featured",
1176 accommodation_type: "Hotel",
1177 meal_plan: "breakfast",
1178 accommodation_details:
1179 "4-star hotels in city centers with easy access to major attractions",
1180 transportation_included: true,
1181 pickup_location: "Charles de Gaulle Airport",
1182 dropoff_location: "El Prat Airport",
1183 transportation_details:
1184 "High-speed train between cities, private transfers, and metro passes included",
1185 pricing_type: "regular",
1186 original_price: "2499",
1187 discounted_price: "",
1188 price_types: [],
1189 deposit_amount: "600",
1190 deposit_percentage: "",
1191 payment_terms:
1192 "40% deposit required at booking, remaining 60% due 45 days before departure",
1193 max_travelers: "20",
1194 min_travelers: "4",
1195 booking_deadline_hours: "24",
1196 cancellation_policy:
1197 "Free cancellation up to 45 days before departure. 75% refund for cancellations 30-45 days before. 50% refund for cancellations 15-30 days before. No refund for cancellations less than 15 days before.",
1198 age_min: "",
1199 age_max: "",
1200 physical_requirements:
1201 "Moderate walking required. Some sites involve stairs and uneven surfaces. Suitable for most fitness levels.",
1202 visa_requirements:
1203 "Schengen visa required for most non-EU nationals. Apply well in advance as processing can take several weeks.",
1204 vaccination_requirements:
1205 "No mandatory vaccinations. Routine vaccinations recommended.",
1206 disable_booking: false,
1207 has_default_time_slots: false,
1208 default_time_slots: [],
1209 departure_time: "",
1210 included_items: [
1211 {
1212 title: "Accommodation",
1213 description: "9 nights in 4-star city center hotels",
1214 },
1215 { title: "Breakfast", description: "Daily breakfast included" },
1216 {
1217 title: "Transportation",
1218 description:
1219 "High-speed trains, airport transfers, and city metro passes",
1220 },
1221 {
1222 title: "Guided tours",
1223 description: "Professional local guides in each city",
1224 },
1225 {
1226 title: "Skip-the-line tickets",
1227 description: "Priority access to major attractions",
1228 },
1229 {
1230 title: "Welcome dinner",
1231 description: "Traditional welcome dinner in Paris",
1232 },
1233 ],
1234 excluded_items: [
1235 {
1236 title: "International flights",
1237 description: "Flights to Paris and from Barcelona",
1238 },
1239 {
1240 title: "Lunch and dinner",
1241 description: "Meals other than breakfast and welcome dinner",
1242 },
1243 {
1244 title: "Travel insurance",
1245 description: "Travel insurance recommended",
1246 },
1247 {
1248 title: "Personal expenses",
1249 description: "Souvenirs, tips, and personal items",
1250 },
1251 ],
1252 itinerary_days: [],
1253 gallery_images: [],
1254 featured_image: null,
1255 downloadable_items: [],
1256 faqs: [
1257 {
1258 question: "Do I need a visa?",
1259 answer:
1260 "Most non-EU nationals need a Schengen visa. Apply at the embassy of your first entry country (France) well in advance.",
1261 },
1262 {
1263 question: "What languages are spoken?",
1264 answer:
1265 "English-speaking guides provided. Local languages are French, Italian, and Spanish, but English is widely spoken in tourist areas.",
1266 },
1267 {
1268 question: "Is this suitable for families?",
1269 answer:
1270 "Yes, this tour is family-friendly. However, some museums and sites may have age restrictions for children.",
1271 },
1272 ],
1273 frontend_tabs: [
1274 {
1275 id: "overview",
1276 label: "Overview",
1277 enabled: true,
1278 order: 1,
1279 content_type: "general",
1280 icon: { type: "icon", value: "book-open" },
1281 },
1282 {
1283 id: "itinerary",
1284 label: "Itinerary",
1285 enabled: true,
1286 order: 2,
1287 content_type: "itinerary",
1288 icon: { type: "icon", value: "calendar" },
1289 },
1290 {
1291 id: "included",
1292 label: "Included",
1293 enabled: true,
1294 order: 3,
1295 content_type: "included_excluded",
1296 icon: { type: "icon", value: "check" },
1297 },
1298 {
1299 id: "location",
1300 label: "Location",
1301 enabled: true,
1302 order: 4,
1303 content_type: "gallery",
1304 icon: { type: "icon", value: "map-pin" },
1305 },
1306 {
1307 id: "important_info",
1308 label: "Important Info",
1309 enabled: true,
1310 order: 5,
1311 content_type: "general",
1312 icon: { type: "icon", value: "info" },
1313 },
1314 {
1315 id: "downloads",
1316 label: "Downloads",
1317 enabled: true,
1318 order: 6,
1319 content_type: "downloads",
1320 icon: { type: "icon", value: "download" },
1321 },
1322 {
1323 id: "faq",
1324 label: "FAQ",
1325 enabled: true,
1326 order: 7,
1327 content_type: "faqs",
1328 icon: { type: "icon", value: "help-circle" },
1329 },
1330 {
1331 id: "trip_story",
1332 label: "Story",
1333 enabled: true,
1334 order: 8,
1335 content_type: "custom",
1336 custom_content: "",
1337 icon: { type: "icon", value: "book" },
1338 },
1339 {
1340 id: "what_makes_special",
1341 label: "Special",
1342 enabled: true,
1343 order: 9,
1344 content_type: "custom",
1345 custom_content: "",
1346 icon: { type: "icon", value: "star" },
1347 },
1348 {
1349 id: "testimonials",
1350 label: "Testimonials",
1351 enabled: true,
1352 order: 10,
1353 content_type: "reviews",
1354 icon: { type: "icon", value: "message-circle" },
1355 },
1356 ],
1357 availability_dates: [],
1358 status: "draft",
1359 scheduled_publish_date: "",
1360 scheduled_unpublish_date: "",
1361 version: 1,
1362 seasonal_auto_enable: false,
1363 seasonal_enable_date: "",
1364 seasonal_disable_date: "",
1365 meta_title: "European Grand Tour - 10 Days | Paris, Rome & Barcelona",
1366 meta_description:
1367 "Discover the best of Europe with our 10-day grand tour covering Paris, Rome, and Barcelona. Experience iconic landmarks, world-class cuisine, and rich history.",
1368 meta_keywords:
1369 "Europe tour, Paris, Rome, Barcelona, European travel, cultural tour",
1370 attributes: {},
1371 },
1372 ];
1373
1374 const [formData, setFormData] = useState<TripFormData>({
1375 title: "",
1376 slug: "",
1377 description: "",
1378 highlights: [],
1379 trip_details: "",
1380 short_description: "",
1381 what_makes_special: "",
1382 trip_story: "",
1383 video_url: "",
1384 virtual_tour_url: "",
1385 testimonial_review_ids: [],
1386 destinations: [], // Array of destination IDs
1387 starting_location: "",
1388 ending_location: "",
1389 countries: [],
1390 regions: [],
1391 starting_latitude: "",
1392 starting_longitude: "",
1393 ending_latitude: "",
1394 ending_longitude: "",
1395 landmarks: [],
1396 trip_type: "multi_day",
1397 duration_days: "",
1398 duration_nights: "",
1399 available_from: "",
1400 available_to: "",
1401 booking_window_days: "",
1402 seasonal_availability: "",
1403 best_season: "",
1404 peak_season: "",
1405 off_season: "",
1406 activity_types: [], // Array of activity IDs
1407 difficulty_level: "",
1408 trip_category: [],
1409 tags: [],
1410 featured_priority: "none",
1411 accommodation_type: "",
1412 meal_plan: "",
1413 accommodation_details: "",
1414 transportation_included: false,
1415 pickup_location: "",
1416 dropoff_location: "",
1417 transportation_details: "",
1418 pricing_type: "regular",
1419 original_price: "",
1420 discounted_price: "",
1421 price_types: [],
1422 deposit_amount: "",
1423 deposit_percentage: "",
1424 payment_terms: "",
1425 max_travelers: "10",
1426 min_travelers: "2",
1427 booking_deadline_hours: "24",
1428 cancellation_policy: "Flexible",
1429 age_min: "18",
1430 age_max: "65",
1431 physical_requirements: "Moderate",
1432 visa_requirements: "Schengen visa",
1433 vaccination_requirements: "COVID-19 vaccination",
1434 disable_booking: false,
1435 has_default_time_slots: false,
1436 default_time_slots: [],
1437 departure_time: "09:00",
1438 included_items: [],
1439 excluded_items: [],
1440 attributes: {}, // attribute_id -> value mapping
1441 itinerary_days: [],
1442 gallery_images: [],
1443 featured_image: null,
1444 downloadable_items: [],
1445 faqs: [],
1446 frontend_tabs: [
1447 {
1448 id: "overview",
1449 label: "Overview",
1450 enabled: true,
1451 order: 1,
1452 content_type: "general",
1453 icon: { type: "icon", value: "book-open" },
1454 },
1455 {
1456 id: "itinerary",
1457 label: "Itinerary",
1458 enabled: true,
1459 order: 2,
1460 content_type: "itinerary",
1461 icon: { type: "icon", value: "calendar" },
1462 },
1463 {
1464 id: "included",
1465 label: "Included",
1466 enabled: true,
1467 order: 3,
1468 content_type: "included_excluded",
1469 icon: { type: "icon", value: "check" },
1470 },
1471 {
1472 id: "location",
1473 label: "Location",
1474 enabled: true,
1475 order: 4,
1476 content_type: "gallery",
1477 icon: { type: "icon", value: "map-pin" },
1478 },
1479 {
1480 id: "important_info",
1481 label: "Important Info",
1482 enabled: true,
1483 order: 5,
1484 content_type: "general",
1485 icon: { type: "icon", value: "info" },
1486 },
1487 {
1488 id: "downloads",
1489 label: "Downloads",
1490 enabled: true,
1491 order: 6,
1492 content_type: "downloads",
1493 icon: { type: "icon", value: "download" },
1494 },
1495 {
1496 id: "faq",
1497 label: "FAQ",
1498 enabled: true,
1499 order: 7,
1500 content_type: "faqs",
1501 icon: { type: "icon", value: "help-circle" },
1502 },
1503 {
1504 id: "trip_story",
1505 label: "Story",
1506 enabled: true,
1507 order: 8,
1508 content_type: "custom",
1509 custom_content: "",
1510 icon: { type: "icon", value: "book" },
1511 },
1512 {
1513 id: "what_makes_special",
1514 label: "Special",
1515 enabled: true,
1516 order: 9,
1517 content_type: "custom",
1518 custom_content: "",
1519 icon: { type: "icon", value: "star" },
1520 },
1521 {
1522 id: "testimonials",
1523 label: "Testimonials",
1524 enabled: true,
1525 order: 10,
1526 content_type: "reviews",
1527 icon: { type: "icon", value: "message-circle" },
1528 },
1529 ],
1530 availability_dates: [],
1531 status: "draft",
1532 scheduled_publish_date: "",
1533 scheduled_unpublish_date: "",
1534 version: 1,
1535 seasonal_auto_enable: false,
1536 seasonal_enable_date: "",
1537 seasonal_disable_date: "",
1538 meta_title: "",
1539 meta_description: "",
1540 meta_keywords: "",
1541 });
1542
1543 const [errors, setErrors] = useState<Record<string, string>>({});
1544 const [isSubmitting, setIsSubmitting] = useState(false);
1545 const [copiedErrorDetails, setCopiedErrorDetails] = useState(false);
1546
1547 const copyErrorDetailsToClipboard = (text: string) => {
1548 if (!text) return;
1549 const fallbackCopy = () => {
1550 const textarea = document.createElement("textarea");
1551 textarea.value = text;
1552 textarea.style.position = "fixed";
1553 textarea.style.opacity = "0";
1554 document.body.appendChild(textarea);
1555 textarea.select();
1556 try {
1557 document.execCommand("copy");
1558 } catch (e) {
1559 // ignore
1560 }
1561 document.body.removeChild(textarea);
1562 setCopiedErrorDetails(true);
1563 setTimeout(() => setCopiedErrorDetails(false), 1500);
1564 };
1565
1566 try {
1567 if (navigator?.clipboard?.writeText) {
1568 navigator.clipboard
1569 .writeText(text)
1570 .then(() => {
1571 setCopiedErrorDetails(true);
1572 setTimeout(() => setCopiedErrorDetails(false), 1500);
1573 })
1574 .catch(() => fallbackCopy());
1575 } else {
1576 fallbackCopy();
1577 }
1578 } catch {
1579 fallbackCopy();
1580 }
1581 };
1582
1583 // Get action and id from URL
1584 const action = useMemo(() => {
1585 const params = new URLSearchParams(window.location.search);
1586 return params.get("action") || "create";
1587 }, []);
1588
1589 const tripId = useMemo(() => {
1590 const params = new URLSearchParams(window.location.search);
1591 return params.get("id") ? parseInt(params.get("id") || "0") : null;
1592 }, []);
1593
1594 const isEditMode = action === "edit" && tripId !== null;
1595
1596 // Fetch traveler categories - LAZY LOAD: only when pricing section is visited
1597 const { data: travelerCategoriesData, isLoading: isLoadingCategories } =
1598 useQuery({
1599 queryKey: ["traveler-categories"],
1600 queryFn: async () => {
1601 try {
1602 const response = await apiClient.get("/traveler-categories", {
1603 params: {
1604 per_page: 100,
1605 status: "publish", // Only get published categories
1606 },
1607 });
1608 const categories =
1609 response?.data?.data || response?.data || response || [];
1610 return Array.isArray(categories) ? categories : [];
1611 } catch (error: any) {
1612 showToast(
1613 error?.message || __("Failed to load traveler categories", "yatra"),
1614 "error",
1615 );
1616 return [];
1617 }
1618 },
1619 enabled:
1620 can("yatra_view_trips") &&
1621 (currentSection === "pricing" || visitedSections.has("pricing")),
1622 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1623 });
1624
1625 // Get only active/published categories
1626 const activeCategories = useMemo(() => {
1627 const categories = travelerCategoriesData || [];
1628 return categories.filter(
1629 (cat: TravelerCategory) =>
1630 cat.status === "active" || cat.status === "publish",
1631 );
1632 }, [travelerCategoriesData]);
1633
1634 // Fetch settings to get global currency
1635 const { data: settingsData } = useQuery({
1636 queryKey: ["settings"],
1637 queryFn: async () => {
1638 try {
1639 const response = await fetchSettings();
1640 return response;
1641 } catch (error: any) {
1642 // Return default currency if settings fetch fails
1643 return { currency: "USD" };
1644 }
1645 },
1646 enabled: can("yatra_view_trips"),
1647 });
1648
1649 // Get global currency from settings, default to USD
1650 const cur = settingsData?.currency;
1651 const defCur = settingsData?.default_currency;
1652 const globalCurrency =
1653 typeof cur === "string" && cur.length > 0
1654 ? cur
1655 : typeof defCur === "string" && defCur.length > 0
1656 ? defCur
1657 : "USD";
1658
1659 // Fetch activities from API - LAZY LOAD: only when categorization section is visited
1660 const { data: activitiesData } = useQuery({
1661 queryKey: ["activities-published"],
1662 queryFn: async () => {
1663 try {
1664 const response = await apiClient.get("/activities", {
1665 params: {
1666 per_page: 100,
1667 status: "publish", // Only get published activities
1668 },
1669 });
1670 return response.data || [];
1671 } catch (error: any) {
1672 showToast(
1673 error?.message || __("Failed to load activities", "yatra"),
1674 "error",
1675 );
1676 return [];
1677 }
1678 },
1679 enabled:
1680 can("yatra_view_trips") &&
1681 (currentSection === "categorization" ||
1682 visitedSections.has("categorization")),
1683 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1684 });
1685
1686 // LAZY LOAD: only when categorization section is visited
1687 const { data: tripCategoriesResponse } = useQuery({
1688 queryKey: ["trip-categories", "published"],
1689 queryFn: async () => {
1690 try {
1691 const response = await apiClient.get("/trip-categories", {
1692 params: {
1693 per_page: 100,
1694 status: "publish",
1695 hierarchical: true,
1696 orderby: "name",
1697 order: "ASC",
1698 },
1699 });
1700 return response;
1701 } catch (error: any) {
1702 showToast(
1703 error?.message || __("Failed to load trip categories", "yatra"),
1704 "error",
1705 );
1706 return [];
1707 }
1708 },
1709 enabled: can("yatra_view_trips"),
1710 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1711 });
1712
1713 // LAZY LOAD: only when categorization section is visited
1714 const {
1715 data: difficultyLevelsResponse,
1716 isLoading: isLoadingDifficultyLevels,
1717 } = useQuery({
1718 queryKey: ["difficulty-levels", "published"],
1719 queryFn: async () => {
1720 try {
1721 const response = await apiClient.get("/difficulty-levels", {
1722 params: {
1723 per_page: 100,
1724 status: "publish",
1725 orderby: "sorting",
1726 order: "ASC",
1727 },
1728 });
1729 return response.data || [];
1730 } catch (error: any) {
1731 showToast(
1732 error?.message || __("Failed to load difficulty levels", "yatra"),
1733 "error",
1734 );
1735 return [];
1736 }
1737 },
1738 enabled:
1739 can("yatra_view_trips") &&
1740 (currentSection === "categorization" ||
1741 visitedSections.has("categorization")),
1742 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1743 });
1744
1745 const tripCategories: TripCategoryOption[] = useMemo(() => {
1746 const payload = extractArrayPayload(tripCategoriesResponse);
1747 return (payload as TripCategoryOption[]).filter((category) => {
1748 if (!category) return false;
1749 if (typeof category !== "object") return false;
1750 if (
1751 "status" in category &&
1752 category.status &&
1753 category.status !== "publish"
1754 ) {
1755 return false;
1756 }
1757 return true;
1758 });
1759 }, [tripCategoriesResponse]);
1760
1761 const difficultyLevels: DifficultyLevelOption[] = useMemo(() => {
1762 const payload = extractArrayPayload(difficultyLevelsResponse);
1763 return (payload as DifficultyLevelOption[]).filter((level) => {
1764 if (!level) return false;
1765 if (typeof level !== "object") return false;
1766 if ("status" in level && level.status && level.status !== "publish") {
1767 return false;
1768 }
1769 return true;
1770 });
1771 }, [difficultyLevelsResponse]);
1772
1773 // Fetch destinations from API - LAZY LOAD: only when location section is visited
1774 const { data: destinationsData } = useQuery({
1775 queryKey: ["destinations-published"],
1776 queryFn: async () => {
1777 try {
1778 const response = await apiClient.get("/destinations", {
1779 params: {
1780 per_page: 100,
1781 status: "publish", // Only get published destinations
1782 },
1783 });
1784 return response.data || [];
1785 } catch (error: any) {
1786 showToast(
1787 error?.message || __("Failed to load destinations", "yatra"),
1788 "error",
1789 );
1790 return [];
1791 }
1792 },
1793 enabled:
1794 can("yatra_view_trips") &&
1795 (isEditMode ||
1796 currentSection === "location" ||
1797 visitedSections.has("location")),
1798 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1799 });
1800
1801 // Fetch trip data if editing
1802 const {
1803 data: tripData,
1804 isLoading: isLoadingTrip,
1805 error: tripError,
1806 } = useQuery({
1807 queryKey: ["trip", tripId],
1808 queryFn: async () => {
1809 if (!tripId) return null;
1810 try {
1811 const response = await apiClient.get(`/trips/${tripId}`);
1812 // WordPress REST API returns data directly, but check if it's wrapped
1813 // Some endpoints return { data: {...} }, others return {...} directly
1814 const tripData = response?.data || response;
1815 return tripData;
1816 } catch (error: any) {
1817 console.error("Error loading trip:", error);
1818 showToast(
1819 error?.message || __("Failed to load trip data", "yatra"),
1820 "error",
1821 );
1822 throw error;
1823 }
1824 },
1825 enabled: !!tripId && isEditMode,
1826 staleTime: 0, // Always fetch fresh data to ensure downloadable items persist after save
1827 });
1828
1829 // Build error context for edit error state
1830 const tripErrorContext = useMemo(
1831 () => getErrorContext(tripError),
1832 [tripError],
1833 );
1834
1835 // Fetch trip attributes if editing (TripAttributesSection waits until resolved to avoid empty-first-render bug)
1836 const tripAttributesQueryEnabled = Boolean(tripId && isEditMode);
1837 const { data: tripAttributesData, isPending: isTripAttributesPending } =
1838 useQuery({
1839 queryKey: ["trip-attributes", tripId],
1840 queryFn: async () => {
1841 if (!tripId) return {};
1842 try {
1843 const response = await apiClient.get(`/trips/${tripId}/attributes`);
1844
1845 // Try different ways to extract the data
1846 let payload = response?.data;
1847 if (!payload || !Array.isArray(payload)) {
1848 payload = response;
1849 }
1850 if (!payload || !Array.isArray(payload)) {
1851 payload = response?.data?.data;
1852 }
1853
1854 const attributes = Array.isArray(payload)
1855 ? payload
1856 : Array.isArray(payload?.data)
1857 ? payload.data
1858 : [];
1859
1860 // Convert to attributeId => value mapping for form
1861 const attributesMap: Record<number, any> = {};
1862 attributes.forEach((attr: any) => {
1863 const attributeId = Number(attr.attribute_id || attr.id);
1864 if (attributeId) {
1865 let value = "";
1866
1867 // Read from relationship_metadata (contains complete attribute data)
1868 if (attr.relationship_metadata) {
1869 try {
1870 const metadata =
1871 typeof attr.relationship_metadata === "string"
1872 ? JSON.parse(attr.relationship_metadata)
1873 : attr.relationship_metadata;
1874
1875 value = metadata.value || "";
1876 } catch (e) {
1877 console.warn(
1878 "Failed to parse relationship_metadata:",
1879 attr.relationship_metadata,
1880 e,
1881 );
1882 value = "";
1883 }
1884 } else {
1885 // Fallback to old format if metadata is not available
1886 value = attr.value || "";
1887 }
1888
1889 attributesMap[attributeId] = value;
1890 }
1891 });
1892
1893 return attributesMap;
1894 } catch (error) {
1895 console.error("Failed to fetch trip attributes:", error);
1896 return {};
1897 }
1898 },
1899 enabled: tripAttributesQueryEnabled,
1900 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1901 });
1902 const tripAttributesReady =
1903 !tripAttributesQueryEnabled || !isTripAttributesPending;
1904
1905 // Helper function to normalize array of items to IDs
1906 // Helper to normalize highlights (can be array of strings or objects)
1907 const normalizeHighlights = (highlights: any): string[] => {
1908 if (!highlights) return [];
1909 if (Array.isArray(highlights)) {
1910 return highlights
1911 .map((h: any) => {
1912 if (typeof h === "string") return h;
1913 if (h && typeof h === "object") {
1914 return h.highlight_text || h.text || h.title || String(h);
1915 }
1916 return String(h);
1917 })
1918 .filter((h: string) => h.trim().length > 0);
1919 }
1920 if (typeof highlights === "string") {
1921 try {
1922 const parsed = JSON.parse(highlights);
1923 return normalizeHighlights(parsed);
1924 } catch {
1925 return [highlights];
1926 }
1927 }
1928 return [];
1929 };
1930
1931 const normalizeDownloadableItems = (items: any): DownloadableItem[] => {
1932 if (!items || !Array.isArray(items)) return [];
1933
1934 return items
1935 .filter((row: any) => row && typeof row === "object")
1936 .map((row: any, idx: number) => {
1937 const rawVisibility = (row.visibility ?? "booked_only") as any;
1938 const mappedVisibility =
1939 rawVisibility === "paid_only" ? "booked_only" : rawVisibility;
1940 const safeVisibility: DownloadableItem["visibility"] = [
1941 "public",
1942 "logged_in",
1943 "booked_only",
1944 ].includes(mappedVisibility)
1945 ? mappedVisibility
1946 : "booked_only";
1947
1948 const title = (
1949 row.title ??
1950 row.download_title ??
1951 row.downlaod_title ??
1952 ""
1953 ).toString();
1954 const description = (
1955 row.description ??
1956 row.download_description ??
1957 row.downlaod_description ??
1958 ""
1959 ).toString();
1960 const attachmentIdRaw =
1961 row.attachment_id ?? row.download_file ?? row.downlaod_file;
1962 const attachmentUrl = (
1963 row.attachment_url ??
1964 row.content_url ??
1965 ""
1966 ).toString();
1967 const attachmentTitle = (row.attachment_title ?? "").toString();
1968 const enabledRaw =
1969 row.enabled ??
1970 row.is_downloadable ??
1971 row.download_enabled ??
1972 row.downlaod_enabled;
1973
1974 return {
1975 id: row.id != null ? Number(row.id) : null,
1976 title,
1977 description,
1978 attachment_id:
1979 attachmentIdRaw != null ? Number(attachmentIdRaw) : null,
1980 attachment_url: attachmentUrl,
1981 attachment_title: attachmentTitle,
1982 visibility: safeVisibility,
1983 enabled: enabledRaw != null ? Boolean(enabledRaw) : true,
1984 sort_order: row.sort_order != null ? Number(row.sort_order) : idx + 1,
1985 };
1986 });
1987 };
1988
1989 // Helper to extract IDs from mixed arrays
1990 const extractIds = (items: any): number[] => {
1991 if (!items || !Array.isArray(items)) return [];
1992 return items
1993 .map((item: any) => {
1994 if (typeof item === "number") return item;
1995 if (typeof item === "string") return parseInt(item) || 0;
1996 if (item && typeof item === "object") {
1997 return (
1998 item.id ||
1999 item.destination_id ||
2000 item.activity_id ||
2001 item.category_id ||
2002 0
2003 );
2004 }
2005 return 0;
2006 })
2007 .filter((id: number) => !isNaN(id) && id > 0);
2008 };
2009
2010 // Helper to normalize itinerary days
2011 const normalizeItineraryDays = (days: any): any[] => {
2012 if (!days) return [];
2013 if (Array.isArray(days)) return days;
2014 if (typeof days === "string") {
2015 try {
2016 const parsed = JSON.parse(days);
2017 return Array.isArray(parsed) ? parsed : [];
2018 } catch {
2019 return [];
2020 }
2021 }
2022 return [];
2023 };
2024
2025 // Helper to normalize availability dates
2026 const normalizeAvailabilityDates = (dates: any): any[] => {
2027 if (!dates) return [];
2028 if (Array.isArray(dates)) return dates;
2029 if (typeof dates === "string") {
2030 try {
2031 const parsed = JSON.parse(dates);
2032 return Array.isArray(parsed) ? parsed : [];
2033 } catch {
2034 return [];
2035 }
2036 }
2037 return [];
2038 };
2039
2040 // Utility function to normalize gallery images
2041 const normalizeGalleryImages = (
2042 images: any,
2043 ): Array<{
2044 id: number;
2045 url: string;
2046 thumbnail_url?: string;
2047 alt_text?: string;
2048 caption?: string;
2049 }> => {
2050 if (!images) return [];
2051 if (Array.isArray(images)) {
2052 return images
2053 .map((img: any) => {
2054 if (typeof img === "string") {
2055 return { id: 0, url: img };
2056 }
2057 if (img && typeof img === "object") {
2058 return {
2059 id: img.id || img.image_id || 0,
2060 url: img.url || img.image_url || img.src || "",
2061 thumbnail_url: img.thumbnail_url || img.thumb_url || "",
2062 alt_text: img.alt_text || img.alt || "",
2063 caption: img.caption || img.title || "",
2064 };
2065 }
2066 return { id: 0, url: "" };
2067 })
2068 .filter((item: any) => item.url);
2069 }
2070 if (typeof images === "string") {
2071 try {
2072 const parsed = JSON.parse(images);
2073 return normalizeGalleryImages(parsed);
2074 } catch {
2075 return [{ id: 0, url: images }];
2076 }
2077 }
2078 return [];
2079 };
2080
2081 // Helper to normalize FAQs
2082 const normalizeFaqs = (faqs: any): FAQ[] => {
2083 if (!faqs || !Array.isArray(faqs)) return [];
2084 return faqs
2085 .map((faq: any) => {
2086 if (faq && typeof faq === "object") {
2087 return {
2088 question: faq.question || "",
2089 answer: faq.answer || "",
2090 };
2091 }
2092 return { question: "", answer: "" };
2093 })
2094 .filter((faq: any) => faq.question && faq.answer);
2095 };
2096
2097 useEffect(() => {
2098 if (!tripData || !isEditMode) {
2099 return;
2100 }
2101
2102 // Preserve current itinerary_days and gallery_images if they exist and have content
2103 // This prevents wiping out data when user updates trip in itinerary builder
2104 const currentItineraryDays = formData.itinerary_days || [];
2105 const currentGalleryImages = formData.gallery_images || [];
2106
2107 // Only use database data if current data is empty or if this is initial load
2108 const shouldPreserveItinerary = currentItineraryDays.length > 0;
2109 const shouldPreserveGallery = currentGalleryImages.length > 0;
2110
2111 setFormData({
2112 title: tripData.title || "",
2113 slug: tripData.slug || "",
2114 description: tripData.description || "",
2115 highlights: normalizeHighlights(tripData.highlights),
2116 trip_details: tripData.trip_details || "",
2117 short_description: tripData.short_description || "",
2118 what_makes_special: tripData.what_makes_special || "",
2119 trip_story: tripData.trip_story || "",
2120 video_url: tripData.video_url || "",
2121 virtual_tour_url: tripData.virtual_tour_url || "",
2122 testimonial_review_ids: Array.isArray(tripData.testimonial_review_ids)
2123 ? tripData.testimonial_review_ids
2124 : [],
2125 destinations: extractIds(tripData.destinations || []),
2126 starting_location: tripData.starting_location || "",
2127 ending_location: tripData.ending_location || "",
2128 countries: Array.isArray(tripData.countries) ? tripData.countries : [],
2129 regions: Array.isArray(tripData.regions) ? tripData.regions : [],
2130 starting_latitude: tripData.starting_latitude?.toString() || "",
2131 starting_longitude: tripData.starting_longitude?.toString() || "",
2132 ending_latitude: tripData.ending_latitude?.toString() || "",
2133 ending_longitude: tripData.ending_longitude?.toString() || "",
2134 landmarks: Array.isArray(tripData.landmarks) ? tripData.landmarks : [],
2135 trip_type: (tripData.trip_type ||
2136 (tripData.duration_days &&
2137 parseInt(tripData.duration_days?.toString() || "0") === 1
2138 ? "single_day"
2139 : "multi_day")) as "single_day" | "multi_day" | "flexible",
2140 duration_days: tripData.duration_days?.toString() || "",
2141 duration_nights: tripData.duration_nights?.toString() || "",
2142 available_from: tripData.available_from || "",
2143 available_to: tripData.available_to || "",
2144 booking_window_days: tripData.booking_window_days?.toString() || "",
2145 seasonal_availability: tripData.seasonal_availability || "",
2146 best_season: tripData.best_season || "",
2147 peak_season: tripData.peak_season || "",
2148 off_season: tripData.off_season || "",
2149 activity_types: extractIds(tripData.activity_types || []),
2150 difficulty_level: tripData.difficulty_level?.toString() || "",
2151 trip_category: extractIds(tripData.trip_category || []),
2152 tags: Array.isArray(tripData.tags) ? tripData.tags : [],
2153 featured_priority: tripData.featured_priority || "none",
2154 accommodation_type: tripData.accommodation_type || "",
2155 meal_plan: tripData.meal_plan || "",
2156 accommodation_details: tripData.accommodation_details || "",
2157 transportation_included: tripData.transportation_included || false,
2158 pickup_location: tripData.pickup_location || "",
2159 dropoff_location: tripData.dropoff_location || "",
2160 transportation_details: tripData.transportation_details || "",
2161 pricing_type: (tripData.pricing_type ||
2162 (tripData.price_types &&
2163 Array.isArray(tripData.price_types) &&
2164 tripData.price_types.length > 0
2165 ? "traveler_based"
2166 : "regular")) as "regular" | "traveler_based",
2167 original_price: tripData.original_price?.toString() || "",
2168 discounted_price: tripData.discounted_price?.toString() || "",
2169 price_types: Array.isArray(tripData.price_types)
2170 ? tripData.price_types.map((pt: any) => ({
2171 category_id: Number(pt.category_id) || 0,
2172 original_price: pt.original_price?.toString() || "",
2173 discounted_price: pt.discounted_price?.toString() || "",
2174 is_default: Boolean(pt.is_default),
2175 }))
2176 : [],
2177 deposit_amount: tripData.deposit_amount?.toString() || "",
2178 deposit_percentage: tripData.deposit_percentage?.toString() || "",
2179 payment_terms: tripData.payment_terms || "",
2180 max_travelers: tripData.max_travelers?.toString() || "",
2181 min_travelers: tripData.min_travelers?.toString() || "",
2182 booking_deadline_hours: tripData.booking_deadline_hours || "",
2183 cancellation_policy: tripData.cancellation_policy || "",
2184 age_min: tripData.age_min?.toString() || "",
2185 age_max: tripData.age_max?.toString() || "",
2186 physical_requirements: tripData.physical_requirements || "",
2187 visa_requirements: tripData.visa_requirements || "",
2188 vaccination_requirements: tripData.vaccination_requirements || "",
2189 disable_booking: Boolean(tripData.custom_fields?.disable_booking),
2190 has_default_time_slots: tripData.has_default_time_slots || false,
2191 default_time_slots: Array.isArray(tripData.default_time_slots)
2192 ? tripData.default_time_slots
2193 : tripData.default_time_slots
2194 ? JSON.parse(tripData.default_time_slots)
2195 : [],
2196 departure_time: tripData.departure_time || "09:00",
2197 included_items: normalizeAmenityItems(tripData.included_items),
2198 excluded_items: normalizeAmenityItems(tripData.excluded_items),
2199 // Preserve current itinerary data if it exists, otherwise use database data
2200 itinerary_days: shouldPreserveItinerary
2201 ? currentItineraryDays
2202 : normalizeItineraryDays(tripData.itinerary_days),
2203 // Preserve current gallery data if it exists, otherwise use database data
2204 gallery_images: shouldPreserveGallery
2205 ? currentGalleryImages
2206 : normalizeGalleryImages(tripData.gallery_images),
2207 featured_image: tripData.featured_image
2208 ? Number(tripData.featured_image)
2209 : null,
2210 downloadable_items: normalizeDownloadableItems(
2211 tripData.downloadable_items,
2212 ),
2213 faqs: normalizeFaqs(tripData.faqs),
2214 frontend_tabs: Array.isArray(tripData.frontend_tabs)
2215 ? tripData.frontend_tabs
2216 : [
2217 // Core sections (always present) - in logical order
2218 {
2219 id: "overview",
2220 label: "Overview",
2221 enabled: true,
2222 order: 1,
2223 content_type: "overview",
2224 icon: { type: "icon", value: "book" },
2225 },
2226 {
2227 id: "itinerary",
2228 label: "Itinerary",
2229 enabled: true,
2230 order: 2,
2231 content_type: "itinerary",
2232 icon: { type: "icon", value: "calendar" },
2233 },
2234 {
2235 id: "included",
2236 label: "Included",
2237 enabled: true,
2238 order: 3,
2239 content_type: "included_excluded",
2240 icon: { type: "icon", value: "check" },
2241 },
2242 {
2243 id: "location",
2244 label: "Location",
2245 enabled: true,
2246 order: 4,
2247 content_type: "location",
2248 icon: { type: "icon", value: "map-pin" },
2249 },
2250 {
2251 id: "important_info",
2252 label: "Important Info",
2253 enabled: true,
2254 order: 5,
2255 content_type: "important_info",
2256 icon: { type: "icon", value: "info" },
2257 },
2258
2259 // Conditional sections (enabled by default, shown conditionally on frontend)
2260 {
2261 id: "downloads",
2262 label: "Downloads",
2263 enabled: true,
2264 order: 6,
2265 content_type: "downloads",
2266 icon: { type: "icon", value: "download" },
2267 },
2268 {
2269 id: "faq",
2270 label: "FAQ",
2271 enabled: true,
2272 order: 7,
2273 content_type: "faq",
2274 icon: { type: "icon", value: "help-circle" },
2275 },
2276 {
2277 id: "trip_story",
2278 label: "Story",
2279 enabled: true,
2280 order: 8,
2281 content_type: "trip_story",
2282 custom_content: "",
2283 icon: { type: "icon", value: "book" },
2284 },
2285 {
2286 id: "what_makes_special",
2287 label: "Special",
2288 enabled: true,
2289 order: 9,
2290 content_type: "what_makes_special",
2291 custom_content: "",
2292 icon: { type: "icon", value: "star" },
2293 },
2294 {
2295 id: "testimonials",
2296 label: "Testimonials",
2297 enabled: true,
2298 order: 10,
2299 content_type: "testimonials",
2300 icon: { type: "icon", value: "message-circle" },
2301 },
2302 ],
2303 availability_dates: normalizeAvailabilityDates(
2304 tripData.availability_dates,
2305 ),
2306 status: (tripData.status || "draft") as
2307 | "draft"
2308 | "review"
2309 | "approved"
2310 | "publish"
2311 | "archived"
2312 | "suspended",
2313 scheduled_publish_date: tripData.scheduled_publish_date || "",
2314 scheduled_unpublish_date: tripData.scheduled_unpublish_date || "",
2315 version: tripData.version || 1,
2316 seasonal_auto_enable: tripData.seasonal_auto_enable || false,
2317 seasonal_enable_date: tripData.seasonal_enable_date || "",
2318 seasonal_disable_date: tripData.seasonal_disable_date || "",
2319 meta_title: tripData.meta_title || "",
2320 meta_description: tripData.meta_description || "",
2321 meta_keywords: tripData.meta_keywords || "",
2322 attributes: tripAttributesData || {},
2323 });
2324
2325 if (tripData.featured_image_url && tripData.featured_image) {
2326 const numericId = Number(tripData.featured_image) || 0;
2327 setFeaturedImagePreview(tripData.featured_image_url);
2328 if (numericId > 0) {
2329 featuredImageCache.current[numericId] = tripData.featured_image_url;
2330 }
2331 } else if (!tripData.featured_image) {
2332 setFeaturedImagePreview("");
2333 }
2334 }, [tripData, tripAttributesData, destinationsData, isEditMode, tripId]);
2335
2336 useEffect(() => {
2337 let isMounted = true;
2338
2339 const resolveFeaturedImage = async () => {
2340 const attachmentId = formData.featured_image;
2341
2342 if (!attachmentId) {
2343 setFeaturedImagePreview("");
2344 return;
2345 }
2346
2347 const cachedUrl = featuredImageCache.current[attachmentId];
2348 if (cachedUrl) {
2349 setFeaturedImagePreview(cachedUrl);
2350 return;
2351 }
2352
2353 if (!mediaBaseUrl) {
2354 setFeaturedImagePreview("");
2355 return;
2356 }
2357
2358 setIsResolvingFeaturedImage(true);
2359 try {
2360 const data = await wpService.getMedia(attachmentId);
2361 const url = data?.source_url || "";
2362 if (url && isMounted) {
2363 featuredImageCache.current[attachmentId] = url;
2364 setFeaturedImagePreview(url);
2365 } else if (isMounted) {
2366 setFeaturedImagePreview("");
2367 }
2368 } catch (error) {
2369 console.error("Failed to resolve featured image URL:", error);
2370 if (isMounted) {
2371 setFeaturedImagePreview("");
2372 }
2373 } finally {
2374 if (isMounted) {
2375 setIsResolvingFeaturedImage(false);
2376 }
2377 }
2378 };
2379
2380 resolveFeaturedImage();
2381
2382 return () => {
2383 isMounted = false;
2384 };
2385 }, [formData.featured_image, mediaBaseUrl]);
2386
2387 // Map errors to sections - also check for price_type errors
2388 const getSectionErrors = (sectionId: SectionId): string[] => {
2389 const errorMap: Record<SectionId, string[]> = {
2390 basic: [
2391 "title",
2392 "slug",
2393 "description",
2394 "featured_image",
2395 "trip_type",
2396 "duration_days",
2397 "duration_nights",
2398 ],
2399 location: ["destinations", "starting_location", "ending_location"],
2400 duration: ["available_from", "available_to", "booking_window_days"],
2401 pricing: ["original_price", "discounted_price", "price_types"],
2402 booking: ["min_travelers", "max_travelers", "age_min", "age_max"],
2403 attributes: ["attributes"],
2404 itinerary: ["itinerary_days"],
2405 included: ["included_items", "excluded_items"],
2406 media: ["gallery_images", "video_url", "virtual_tour_url"], // Removed featured_image - it's in basic section
2407 downloads: ["downloadable_items"],
2408 categorization: ["trip_category", "activity_types"],
2409 faqs: ["faqs"],
2410 seo: ["meta_title", "meta_description"],
2411 advanced: [],
2412 };
2413
2414 const sectionFields = errorMap[sectionId] || [];
2415 const fieldErrors = sectionFields.filter((field) => errors[field]);
2416
2417 // Also check for price_type errors (they have dynamic keys like price_type_0_original)
2418 if (sectionId === "pricing") {
2419 const priceTypeErrors = Object.keys(errors).filter((key) =>
2420 key.startsWith("price_type_"),
2421 );
2422 return [...fieldErrors, ...priceTypeErrors];
2423 }
2424
2425 return fieldErrors;
2426 };
2427
2428 // Define sections - Reorganized for beginner-friendly UX
2429 // PHASE 1: ESSENTIALS (Must complete for publishable trip)
2430 const essentialsSections: Section[] = [
2431 // 1. Trip Basics - What you're offering
2432 {
2433 id: "basic",
2434 label: __("Trip Basics", "yatra"),
2435 icon: FileText,
2436 required: true,
2437 completed: !!(formData.title?.trim() && formData.slug?.trim()),
2438 hasErrors: getSectionErrors("basic").length > 0,
2439 },
2440
2441 // 2. Location & Route - Where it happens
2442 {
2443 id: "location",
2444 label: __("Location & Route", "yatra"),
2445 icon: MapPin,
2446 required: false,
2447 completed: !!(formData.destinations.length > 0),
2448 hasErrors: getSectionErrors("location").length > 0,
2449 },
2450
2451 // 3. Pricing - How much it costs
2452 {
2453 id: "pricing",
2454 label: __("Pricing", "yatra"),
2455 icon: DollarSign,
2456 required: true,
2457 completed:
2458 formData.pricing_type === "regular"
2459 ? !!(
2460 formData.original_price && parseFloat(formData.original_price) > 0
2461 )
2462 : formData.price_types.some(
2463 (pt) => pt.original_price && parseFloat(pt.original_price) > 0,
2464 ),
2465 hasErrors: getSectionErrors("pricing").length > 0,
2466 },
2467
2468 // 4. Availability & Booking - When available + booking rules (merged duration + booking)
2469 {
2470 id: "duration",
2471 label: __("Availability & Booking", "yatra"),
2472 icon: Calendar,
2473 required: false,
2474 completed: !!(
2475 formData.available_from ||
2476 formData.available_to ||
2477 (formData.min_travelers && formData.max_travelers)
2478 ),
2479 hasErrors:
2480 getSectionErrors("duration").length > 0 ||
2481 getSectionErrors("booking").length > 0,
2482 },
2483 ];
2484
2485 // PHASE 2: DETAILS (Enhance trip quality)
2486 const detailsSections: Section[] = [
2487 // 5. Trip Details - Description + Itinerary + Included/Excluded (merged 3 sections)
2488 {
2489 id: "itinerary",
2490 label: __("Trip Details", "yatra"),
2491 icon: BookOpen,
2492 required: false,
2493 completed:
2494 formData.itinerary_days.length > 0 ||
2495 formData.included_items.length > 0 ||
2496 formData.excluded_items.length > 0,
2497 hasErrors:
2498 getSectionErrors("itinerary").length > 0 ||
2499 getSectionErrors("included").length > 0,
2500 },
2501 ];
2502
2503 // PHASE 3: OPTIMIZATION (Improve discoverability)
2504 const optimizationSections: Section[] = [
2505 // 6. Media & Gallery - Photos, videos, testimonials
2506 {
2507 id: "media",
2508 label: __("Media & Gallery", "yatra"),
2509 icon: Image,
2510 required: false,
2511 completed: formData.gallery_images.length > 0 || !!formData.video_url,
2512 hasErrors: getSectionErrors("media").length > 0,
2513 },
2514 ...(showDownloadsUI
2515 ? ([
2516 {
2517 id: "downloads",
2518 label: __("Downloads", "yatra"),
2519 icon: Download,
2520 required: false,
2521 completed: (formData.downloadable_items || []).length > 0,
2522 hasErrors: getSectionErrors("downloads").length > 0,
2523 },
2524 ] as Section[])
2525 : []),
2526
2527 // 7. Categories & Attributes - Classification + Custom Attributes (merged 2 sections)
2528 {
2529 id: "categorization",
2530 label: __("Categories & Attributes", "yatra"),
2531 icon: Tag,
2532 required: false,
2533 completed: !!(
2534 formData.trip_category ||
2535 formData.activity_types.length > 0 ||
2536 formData.tags.length > 0 ||
2537 (formData.attributes && Object.keys(formData.attributes).length > 0)
2538 ),
2539 hasErrors:
2540 getSectionErrors("categorization").length > 0 ||
2541 getSectionErrors("attributes").length > 0,
2542 },
2543
2544 // 8. SEO & Marketing - SEO + FAQs (merged, fixed duplicate)
2545 {
2546 id: "seo",
2547 label: __("SEO & Marketing", "yatra"),
2548 icon: Search,
2549 required: false,
2550 completed: !!(
2551 formData.meta_title ||
2552 formData.meta_description ||
2553 formData.faqs.length > 0
2554 ),
2555 hasErrors:
2556 getSectionErrors("seo").length > 0 ||
2557 getSectionErrors("faqs").length > 0,
2558 },
2559 ];
2560
2561 // PHASE 4: ADVANCED (Power users only)
2562 const advancedSections: Section[] = [
2563 // 9. Advanced Settings - Publishing, scheduling, technical
2564 {
2565 id: "advanced",
2566 label: __("Advanced Settings", "yatra"),
2567 icon: Settings,
2568 required: false,
2569 completed: false,
2570 hasErrors: false,
2571 },
2572 ];
2573
2574 const allSections = [
2575 ...essentialsSections,
2576 ...detailsSections,
2577 ...optimizationSections,
2578 ...advancedSections,
2579 ];
2580 const currentStepIndex = allSections.findIndex(
2581 (s) => s.id === currentSection,
2582 );
2583
2584 // Navigation helpers
2585 const goToNextSection = () => {
2586 if (currentStepIndex < allSections.length - 1) {
2587 setCurrentSection(allSections[currentStepIndex + 1].id);
2588 }
2589 };
2590
2591 const goToPreviousSection = () => {
2592 if (currentStepIndex > 0) {
2593 setCurrentSection(allSections[currentStepIndex - 1].id);
2594 }
2595 };
2596
2597 // Smart defaults - Auto-calculate nights from days (only if nights is empty)
2598 useEffect(() => {
2599 if (
2600 formData.duration_days &&
2601 formData.trip_type === "multi_day" &&
2602 (!formData.duration_nights || formData.duration_nights === "")
2603 ) {
2604 const days = parseInt(formData.duration_days);
2605 if (days > 0 && !isNaN(days)) {
2606 setFormData((prev) => ({
2607 ...prev,
2608 duration_nights: String(Math.max(0, days - 1)),
2609 }));
2610 }
2611 }
2612 }, [formData.duration_days, formData.trip_type]);
2613
2614 // Auto-calculate days from nights (only if days is empty)
2615 useEffect(() => {
2616 if (
2617 formData.duration_nights &&
2618 formData.trip_type === "multi_day" &&
2619 (!formData.duration_days || formData.duration_days === "")
2620 ) {
2621 const nights = parseInt(formData.duration_nights);
2622 if (nights > 0 && !isNaN(nights)) {
2623 setFormData((prev) => ({ ...prev, duration_days: String(nights + 1) }));
2624 }
2625 }
2626 }, [formData.duration_nights, formData.trip_type]);
2627
2628 // Auto-generate slug from title
2629 const generateSlug = (title: string): string => {
2630 return title
2631 .toLowerCase()
2632 .trim()
2633 .replace(/[^\w\s-]/g, "")
2634 .replace(/[\s_-]+/g, "-")
2635 .replace(/^-+|-+$/g, "");
2636 };
2637
2638 const handleTitleChange = (value: string) => {
2639 setFormData((prev) => ({
2640 ...prev,
2641 title: value,
2642 slug: prev.slug || generateSlug(value),
2643 }));
2644 if (errors.title) {
2645 setErrors((prev) => ({ ...prev, title: "" }));
2646 }
2647 };
2648
2649 const handleFieldChange = (field: keyof TripFormData, value: any) => {
2650 setFormData((prev) => ({ ...prev, [field]: value }));
2651 if (errors[field]) {
2652 setErrors((prev) => ({ ...prev, [field]: "" }));
2653 }
2654
2655 if (field === "featured_image") {
2656 // Handle explicit null (when removing image)
2657 if (value === null || value === "" || value === undefined) {
2658 setFeaturedImagePreview("");
2659 } else {
2660 const numericValue = typeof value === "number" ? value : Number(value);
2661 if (!numericValue) {
2662 setFeaturedImagePreview("");
2663 } else if (featuredImageCache.current[numericValue]) {
2664 setFeaturedImagePreview(featuredImageCache.current[numericValue]);
2665 }
2666 }
2667 }
2668 };
2669
2670 const handleHighlightAdd = () => {
2671 setShowHighlightModal(true);
2672 setModalInput({ text: "", question: "", answer: "" });
2673 };
2674
2675 const handleHighlightSave = () => {
2676 if (modalInput.text && modalInput.text.trim()) {
2677 setFormData((prev) => ({
2678 ...prev,
2679 highlights: [...prev.highlights, modalInput.text.trim()],
2680 }));
2681 setShowHighlightModal(false);
2682 setModalInput({ text: "", question: "", answer: "" });
2683 }
2684 };
2685
2686 const handleHighlightRemove = (index: number) => {
2687 setFormData((prev) => ({
2688 ...prev,
2689 highlights: prev.highlights.filter((_, i) => i !== index),
2690 }));
2691 };
2692
2693 // Included/Excluded handlers - will be integrated into ItinerarySection component
2694 // Keeping for future integration when included/excluded are merged into itinerary section
2695 // const handleIncludedAdd = () => { ... };
2696 // const handleIncludedRemove = (index: number) => { ... };
2697 // const handleExcludedAdd = () => { ... };
2698 // const handleExcludedRemove = (index: number) => { ... };
2699
2700 const handleFAQAdd = () => {
2701 setFormData((prev) => ({
2702 ...prev,
2703 faqs: [...prev.faqs, { question: "", answer: "" }],
2704 }));
2705 };
2706
2707 const handleFAQRemove = (index: number) => {
2708 setFormData((prev) => ({
2709 ...prev,
2710 faqs: prev.faqs.filter((_, i) => i !== index),
2711 }));
2712 };
2713
2714 const handleFAQChange = (
2715 index: number,
2716 field: "question" | "answer",
2717 value: string,
2718 ) => {
2719 setFormData((prev) => ({
2720 ...prev,
2721 faqs: prev.faqs.map((faq, i) =>
2722 i === index ? { ...faq, [field]: value } : faq,
2723 ),
2724 }));
2725 };
2726
2727 const handleGalleryAdd = () => {
2728 // Use WordPress media library with multiple selection
2729 if (window.wp && window.wp.media) {
2730 const mediaUploader = window.wp.media({
2731 title: __("Select Gallery Images", "yatra"),
2732 button: { text: __("Add to Gallery", "yatra") },
2733 multiple: true, // Allow multiple image selection
2734 library: { type: "image" },
2735 });
2736
2737 mediaUploader.on("select", () => {
2738 const selection = mediaUploader.state().get("selection");
2739 const newImages: Array<{
2740 id: number;
2741 url: string;
2742 thumbnail_url?: string;
2743 alt_text?: string;
2744 caption?: string;
2745 }> = [];
2746
2747 selection.each((attachment: any) => {
2748 const image = attachment.toJSON();
2749 if (image.url) {
2750 newImages.push({
2751 id: image.id || 0,
2752 url: image.url,
2753 thumbnail_url:
2754 image.sizes?.thumbnail?.url ||
2755 image.sizes?.medium?.url ||
2756 image.url,
2757 alt_text: image.alt || "",
2758 caption: image.caption || "",
2759 });
2760 }
2761 });
2762
2763 if (newImages.length > 0) {
2764 setFormData((prev) => ({
2765 ...prev,
2766 gallery_images: [...prev.gallery_images, ...newImages],
2767 }));
2768 }
2769 });
2770
2771 prepareWordPressMediaFrameOpen();
2772 mediaUploader.open();
2773 } else {
2774 // Fallback for when wp.media is not available
2775 showToast(
2776 __(
2777 "Media library not available. Please ensure you are logged in as admin.",
2778 "yatra",
2779 ),
2780 "error",
2781 );
2782 }
2783 };
2784
2785 const handleGalleryRemove = (index: number) => {
2786 setFormData((prev) => ({
2787 ...prev,
2788 gallery_images: prev.gallery_images.filter((_, i) => i !== index),
2789 }));
2790 };
2791
2792 const handleGalleryReorder = (fromIndex: number, toIndex: number) => {
2793 setFormData((prev) => {
2794 const newImages = [...prev.gallery_images];
2795 const [movedImage] = newImages.splice(fromIndex, 1);
2796 newImages.splice(toIndex, 0, movedImage);
2797 return { ...prev, gallery_images: newImages };
2798 });
2799 };
2800
2801 const handleDownloadableItemAdd = () => {
2802 setFormData((prev) => {
2803 const nextOrder = (prev.downloadable_items?.length || 0) + 1;
2804 return {
2805 ...prev,
2806 downloadable_items: [
2807 ...(prev.downloadable_items || []),
2808 {
2809 id: null,
2810 title: "",
2811 description: "",
2812 attachment_id: null,
2813 attachment_url: "",
2814 attachment_title: "",
2815 visibility: "booked_only",
2816 enabled: true,
2817 sort_order: nextOrder,
2818 },
2819 ],
2820 };
2821 });
2822 };
2823
2824 const handleDownloadableItemRemove = (index: number) => {
2825 setFormData((prev) => ({
2826 ...prev,
2827 downloadable_items: (prev.downloadable_items || []).filter(
2828 (_, i) => i !== index,
2829 ),
2830 }));
2831 };
2832
2833 const handleDownloadableItemMove = (fromIndex: number, toIndex: number) => {
2834 setFormData((prev) => {
2835 const items = [...(prev.downloadable_items || [])];
2836 const [moved] = items.splice(fromIndex, 1);
2837 items.splice(toIndex, 0, moved);
2838
2839 const normalized = items.map((item, idx) => ({
2840 ...item,
2841 sort_order: idx + 1,
2842 }));
2843
2844 return { ...prev, downloadable_items: normalized };
2845 });
2846 };
2847
2848 const handleDownloadableItemChange = (
2849 index: number,
2850 field: keyof DownloadableItem,
2851 value: any,
2852 ) => {
2853 setFormData((prev) => ({
2854 ...prev,
2855 downloadable_items: (prev.downloadable_items || []).map((item, i) =>
2856 i === index ? { ...item, [field]: value } : item,
2857 ),
2858 }));
2859 };
2860
2861 const handleDownloadableItemSelectFile = (index: number) => {
2862 if (window.wp && window.wp.media) {
2863 const mediaUploader = window.wp.media({
2864 title: __("Select File", "yatra"),
2865 button: { text: __("Use this file", "yatra") },
2866 multiple: false,
2867 });
2868
2869 mediaUploader.on("select", () => {
2870 const selection = mediaUploader.state().get("selection");
2871 const attachment = selection.first();
2872 if (!attachment) return;
2873 const file = attachment.toJSON();
2874
2875 handleDownloadableItemChange(index, "attachment_id", file.id || null);
2876 handleDownloadableItemChange(index, "attachment_url", file.url || "");
2877 handleDownloadableItemChange(
2878 index,
2879 "attachment_title",
2880 file.title || file.filename || "",
2881 );
2882 });
2883
2884 prepareWordPressMediaFrameOpen();
2885 mediaUploader.open();
2886 } else {
2887 // Fallback for when wp.media is not available
2888 showToast(
2889 __(
2890 "Media library not available. Please ensure you are logged in as admin.",
2891 "yatra",
2892 ),
2893 "error",
2894 );
2895 }
2896 };
2897
2898 const handlePriceTypeAdd = (categoryId: number) => {
2899 // Check if category already exists (compare as numbers to handle string/number mismatch)
2900 if (
2901 formData.price_types.some(
2902 (pt) => Number(pt.category_id) === Number(categoryId),
2903 )
2904 ) {
2905 showToast(
2906 __("This category already has pricing set", "yatra"),
2907 "warning",
2908 );
2909 return;
2910 }
2911 setFormData((prev) => ({
2912 ...prev,
2913 price_types: [
2914 ...prev.price_types,
2915 {
2916 category_id: categoryId,
2917 original_price: "",
2918 discounted_price: "",
2919 is_default: false,
2920 },
2921 ],
2922 }));
2923 };
2924
2925 const handlePriceTypeRemove = (categoryId: number) => {
2926 setFormData((prev) => ({
2927 ...prev,
2928 price_types: prev.price_types.filter(
2929 (pt) => Number(pt.category_id) !== Number(categoryId),
2930 ),
2931 }));
2932 };
2933
2934 const handlePriceTypeChange = (
2935 categoryId: number,
2936 field: "original_price" | "discounted_price",
2937 value: string,
2938 ) => {
2939 setFormData((prev) => ({
2940 ...prev,
2941 price_types: prev.price_types.map((pt) =>
2942 Number(pt.category_id) === Number(categoryId)
2943 ? { ...pt, [field]: value }
2944 : pt,
2945 ),
2946 }));
2947 };
2948
2949 const handlePriceTypeDefaultChange = (
2950 categoryId: number,
2951 isDefault: boolean,
2952 ) => {
2953 setFormData((prev) => ({
2954 ...prev,
2955 price_types: prev.price_types.map((pt) => {
2956 if (Number(pt.category_id) === Number(categoryId)) {
2957 return { ...pt, is_default: isDefault };
2958 }
2959 // Only allow one default at a time.
2960 return isDefault ? { ...pt, is_default: false } : pt;
2961 }),
2962 }));
2963 };
2964
2965 // Frontend Tabs Handlers
2966 const handleTabToggle = (tabId: string) => {
2967 setFormData((prev) => ({
2968 ...prev,
2969 frontend_tabs: prev.frontend_tabs.map((tab) =>
2970 tab.id === tabId ? { ...tab, enabled: !tab.enabled } : tab,
2971 ),
2972 }));
2973 };
2974
2975 // Handle dummy data fill
2976 const handleFillDummyData = () => {
2977 // Cycle through dummy data sets
2978 const nextIndex = (dummyDataIndex + 1) % dummyTripsData.length;
2979 setDummyDataIndex(nextIndex);
2980
2981 const dummyData = dummyTripsData[nextIndex];
2982
2983 // Populate form with dummy data
2984 setFormData({
2985 ...dummyData,
2986 // Ensure arrays are properly set
2987 destinations: dummyData.destinations || [],
2988 activity_types: dummyData.activity_types || [],
2989 price_types: dummyData.price_types || [],
2990 included_items: dummyData.included_items || [],
2991 excluded_items: dummyData.excluded_items || [],
2992 highlights: dummyData.highlights || [],
2993 faqs: dummyData.faqs || [],
2994 gallery_images: dummyData.gallery_images || [],
2995 itinerary_days: dummyData.itinerary_days || [],
2996 tags: dummyData.tags || [],
2997 testimonial_review_ids: dummyData.testimonial_review_ids || [],
2998 countries: dummyData.countries || [],
2999 regions: dummyData.regions || [],
3000 landmarks: dummyData.landmarks || [],
3001 availability_dates: dummyData.availability_dates || [],
3002 frontend_tabs: dummyData.frontend_tabs || [],
3003 has_default_time_slots: dummyData.has_default_time_slots || false,
3004 default_time_slots: dummyData.default_time_slots || [],
3005 departure_time: dummyData.departure_time || "09:00",
3006 downloadable_items: dummyData.downloadable_items || [],
3007 });
3008
3009 // Show toast notification
3010 showToast(
3011 __("Dummy data filled", "yatra") +
3012 ` (${nextIndex + 1}/${dummyTripsData.length})`,
3013 "success",
3014 );
3015 };
3016
3017 const handleTabLabelChange = (tabId: string, label: string) => {
3018 setFormData((prev) => ({
3019 ...prev,
3020 frontend_tabs: prev.frontend_tabs.map((tab) =>
3021 tab.id === tabId ? { ...tab, label } : tab,
3022 ),
3023 }));
3024 };
3025
3026 const handleTabMove = (tabId: string, direction: "up" | "down") => {
3027 setFormData((prev) => {
3028 const tabs = [...prev.frontend_tabs];
3029 const index = tabs.findIndex((t) => t.id === tabId);
3030 if (index === -1) return prev;
3031
3032 const newIndex = direction === "up" ? index - 1 : index + 1;
3033 if (newIndex < 0 || newIndex >= tabs.length) return prev;
3034
3035 [tabs[index], tabs[newIndex]] = [tabs[newIndex], tabs[index]];
3036 // Update order
3037 tabs.forEach((tab, i) => {
3038 tab.order = i + 1;
3039 });
3040
3041 return { ...prev, frontend_tabs: tabs };
3042 });
3043 };
3044
3045 const handleTabRemove = (tabId: string) => {
3046 setFormData((prev) => ({
3047 ...prev,
3048 frontend_tabs: prev.frontend_tabs
3049 .filter((tab) => tab.id !== tabId)
3050 .map((tab, i) => ({ ...tab, order: i + 1 })),
3051 }));
3052 };
3053
3054 const handleTabAdd = () => {
3055 const newTabId = `custom_${Date.now()}`;
3056 const maxOrder = Math.max(...formData.frontend_tabs.map((t) => t.order), 0);
3057 setFormData((prev) => ({
3058 ...prev,
3059 frontend_tabs: [
3060 ...prev.frontend_tabs,
3061 {
3062 id: newTabId,
3063 label: __("New Tab", "yatra"),
3064 enabled: true,
3065 order: maxOrder + 1,
3066 content_type: "custom",
3067 custom_content: "",
3068 },
3069 ],
3070 }));
3071 };
3072
3073 const handleTabContentTypeChange = (
3074 tabId: string,
3075 contentType: FrontendTab["content_type"],
3076 ) => {
3077 setFormData((prev) => ({
3078 ...prev,
3079 frontend_tabs: prev.frontend_tabs.map((tab) =>
3080 tab.id === tabId ? { ...tab, content_type: contentType } : tab,
3081 ),
3082 }));
3083 };
3084
3085 const handleTabCustomContentChange = (tabId: string, content: string) => {
3086 setFormData((prev) => ({
3087 ...prev,
3088 frontend_tabs: prev.frontend_tabs.map((tab) =>
3089 tab.id === tabId ? { ...tab, custom_content: content } : tab,
3090 ),
3091 }));
3092 };
3093
3094 const handleTabIconChange = (tabId: string, icon: IconPickerValue | null) => {
3095 setFormData((prev) => ({
3096 ...prev,
3097 frontend_tabs: prev.frontend_tabs.map((tab) =>
3098 tab.id === tabId ? { ...tab, icon } : tab,
3099 ),
3100 }));
3101 };
3102
3103 // Drag and drop handlers
3104 const [draggedTab, setDraggedTab] = useState<string | null>(null);
3105 const [dragOverTab, setDragOverTab] = useState<string | null>(null);
3106
3107 const handleDragStart = (e: React.DragEvent, tabId: string) => {
3108 setDraggedTab(tabId);
3109 e.dataTransfer.effectAllowed = "move";
3110 };
3111
3112 const handleDragOver = (e: React.DragEvent) => {
3113 e.preventDefault();
3114 e.dataTransfer.dropEffect = "move";
3115 };
3116
3117 const handleDragEnter = (tabId: string) => {
3118 setDragOverTab(tabId);
3119 };
3120
3121 const handleDragLeave = () => {
3122 setDragOverTab(null);
3123 };
3124
3125 const handleDrop = (e: React.DragEvent, targetTabId: string) => {
3126 e.preventDefault();
3127 setDragOverTab(null);
3128
3129 if (!draggedTab || draggedTab === targetTabId) {
3130 setDraggedTab(null);
3131 return;
3132 }
3133
3134 setFormData((prev) => {
3135 const tabs = [...prev.frontend_tabs];
3136 const draggedIndex = tabs.findIndex((t) => t.id === draggedTab);
3137 const targetIndex = tabs.findIndex((t) => t.id === targetTabId);
3138
3139 if (draggedIndex === -1 || targetIndex === -1) {
3140 return prev;
3141 }
3142
3143 // Remove dragged tab and insert at new position
3144 const [draggedTabObj] = tabs.splice(draggedIndex, 1);
3145 tabs.splice(targetIndex, 0, draggedTabObj);
3146
3147 // Update order values
3148 tabs.forEach((tab, i) => {
3149 tab.order = i + 1;
3150 });
3151
3152 return { ...prev, frontend_tabs: tabs };
3153 });
3154
3155 setDraggedTab(null);
3156 };
3157
3158 const handleDragEnd = () => {
3159 setDraggedTab(null);
3160 setDragOverTab(null);
3161 };
3162
3163 const buildEssentialFieldErrors = (): Record<string, string> => {
3164 const newErrors: Record<string, string> = {};
3165
3166 if (!formData.title.trim()) {
3167 newErrors.title = __("Title is required", "yatra");
3168 }
3169
3170 if (!formData.slug.trim()) {
3171 newErrors.slug = __("Slug is required", "yatra");
3172 } else if (!/^[a-z0-9-]+$/.test(formData.slug)) {
3173 newErrors.slug = __(
3174 "Slug can only contain lowercase letters, numbers, and hyphens",
3175 "yatra",
3176 );
3177 }
3178
3179 return newErrors;
3180 };
3181
3182 const validateForm = (): boolean => {
3183 const newErrors = buildEssentialFieldErrors();
3184 setErrors(newErrors);
3185 return Object.keys(newErrors).length === 0;
3186 };
3187
3188 // Save mutation
3189 const saveMutation = useMutation({
3190 mutationFn: async (
3191 data: TripFormData & { status?: "draft" | "publish" },
3192 ) => {
3193 const payload = {
3194 title: data.title.trim(),
3195 slug: data.slug.trim(),
3196 description: data.description.trim(),
3197 short_description: data.short_description.trim(),
3198 highlights: data.highlights,
3199 trip_details: data.trip_details.trim(),
3200 what_makes_special: data.what_makes_special.trim(),
3201 trip_story: data.trip_story.trim(),
3202 video_url: data.video_url.trim(),
3203 virtual_tour_url: data.virtual_tour_url.trim(),
3204 testimonial_review_ids: Array.isArray(data.testimonial_review_ids)
3205 ? data.testimonial_review_ids.filter(
3206 (id): id is number => id !== null && id !== undefined && id > 0,
3207 )
3208 : [],
3209 destinations: data.destinations || [], // Array of destination IDs
3210 starting_location: data.starting_location.trim(),
3211 ending_location: data.ending_location.trim(),
3212 countries: data.countries || [],
3213 regions: data.regions || [],
3214 starting_latitude: data.starting_latitude
3215 ? parseFloat(data.starting_latitude)
3216 : null,
3217 starting_longitude: data.starting_longitude
3218 ? parseFloat(data.starting_longitude)
3219 : null,
3220 ending_latitude: data.ending_latitude
3221 ? parseFloat(data.ending_latitude)
3222 : null,
3223 ending_longitude: data.ending_longitude
3224 ? parseFloat(data.ending_longitude)
3225 : null,
3226 landmarks: data.landmarks || [],
3227 trip_type: data.trip_type,
3228 duration_days: data.duration_days ? parseInt(data.duration_days) : null,
3229 duration_nights: data.duration_nights
3230 ? parseInt(data.duration_nights)
3231 : null,
3232 available_from: data.available_from || null,
3233 available_to: data.available_to || null,
3234 booking_window_days: data.booking_window_days
3235 ? parseInt(data.booking_window_days)
3236 : null,
3237 seasonal_availability: data.seasonal_availability || "",
3238 best_season: data.best_season.trim(),
3239 peak_season: data.peak_season.trim(),
3240 off_season: data.off_season.trim(),
3241 activity_types: data.activity_types || [], // Array of activity IDs
3242 difficulty_level: parseInt(data.difficulty_level) || null,
3243 trip_category: (() => {
3244 const rawCategories = data.trip_category || [];
3245
3246 // If no trip categories available yet, don't filter - send raw data
3247 if (tripCategories.length === 0) {
3248 return rawCategories;
3249 }
3250
3251 // Filter valid categories - save exactly what user selects
3252 const validCategories = rawCategories.filter((_catId) => {
3253 // Always return true - save all selected categories regardless of availability
3254 return true;
3255 });
3256
3257 return validCategories;
3258 })(),
3259 tags: data.tags || [],
3260 accommodation_type: data.accommodation_type || "",
3261 meal_plan: data.meal_plan || "",
3262 accommodation_details: data.accommodation_details.trim(),
3263 transportation_included: data.transportation_included || false,
3264 pickup_location: data.pickup_location.trim(),
3265 dropoff_location: data.dropoff_location.trim(),
3266 transportation_details: data.transportation_details.trim(),
3267 pricing_type: data.pricing_type,
3268 original_price:
3269 data.pricing_type === "regular"
3270 ? data.original_price
3271 ? parseFloat(data.original_price)
3272 : null
3273 : null,
3274 discounted_price:
3275 data.pricing_type === "regular"
3276 ? data.discounted_price
3277 ? parseFloat(data.discounted_price)
3278 : null
3279 : null,
3280 price_types:
3281 data.pricing_type === "traveler_based"
3282 ? data.price_types.map((pt) => ({
3283 category_id: pt.category_id,
3284 is_default: Boolean((pt as any).is_default),
3285 original_price: pt.original_price
3286 ? parseFloat(pt.original_price)
3287 : 0,
3288 discounted_price: pt.discounted_price
3289 ? parseFloat(pt.discounted_price)
3290 : null,
3291 }))
3292 : [],
3293 deposit_amount: data.deposit_amount
3294 ? parseFloat(data.deposit_amount)
3295 : null,
3296 deposit_percentage: data.deposit_percentage
3297 ? parseFloat(data.deposit_percentage)
3298 : null,
3299 payment_terms: data.payment_terms.trim(),
3300 max_travelers: data.max_travelers ? parseInt(data.max_travelers) : null,
3301 min_travelers: data.min_travelers ? parseInt(data.min_travelers) : null,
3302 booking_deadline_hours: data.booking_deadline_hours || null,
3303 cancellation_policy: data.cancellation_policy || "",
3304 custom_fields: { disable_booking: data.disable_booking },
3305 age_min: data.age_min ? parseInt(data.age_min) : null,
3306 age_max: data.age_max ? parseInt(data.age_max) : null,
3307 physical_requirements: data.physical_requirements.trim(),
3308 visa_requirements: data.visa_requirements.trim(),
3309 vaccination_requirements: data.vaccination_requirements.trim(),
3310 has_default_time_slots: data.has_default_time_slots || false,
3311 default_time_slots: JSON.stringify(data.default_time_slots || []),
3312 departure_time: data.departure_time || "09:00",
3313 included_items: (data.included_items || [])
3314 .map((item) => ({
3315 title: item.title?.trim() || "",
3316 description: item.description?.trim() || "",
3317 }))
3318 .filter((item) => item.title),
3319 excluded_items: (data.excluded_items || [])
3320 .map((item) => ({
3321 title: item.title?.trim() || "",
3322 description: item.description?.trim() || "",
3323 }))
3324 .filter((item) => item.title),
3325 itinerary_days: data.itinerary_days || [],
3326 gallery_images: data.gallery_images || [],
3327 featured_image: data.featured_image ?? null,
3328 faqs: data.faqs || [],
3329 frontend_tabs: data.frontend_tabs.map((tab) => ({
3330 id: tab.id,
3331 label: tab.label,
3332 enabled: tab.enabled,
3333 order: tab.order,
3334 content_type: tab.content_type,
3335 custom_content: tab.custom_content || null,
3336 icon: tab.icon || null,
3337 })),
3338 availability_dates: data.availability_dates.map((avail) => ({
3339 id: avail.id,
3340 departure_date: avail.departure_date || null,
3341 arrival_date: avail.arrival_date || null,
3342 seats_remaining: avail.seats_remaining || null,
3343 original_price: avail.original_price
3344 ? parseFloat(avail.original_price)
3345 : null,
3346 discounted_price: avail.discounted_price
3347 ? parseFloat(avail.discounted_price)
3348 : null,
3349 discount_percentage: avail.discount_percentage
3350 ? parseFloat(avail.discount_percentage)
3351 : null,
3352 status: avail.status || "available",
3353 from_location: avail.from_location || null,
3354 to_location: avail.to_location || null,
3355 from_latitude: avail.from_latitude || null,
3356 from_longitude: avail.from_longitude || null,
3357 to_latitude: avail.to_latitude || null,
3358 to_longitude: avail.to_longitude || null,
3359 })),
3360 status: data.status === "publish" ? "publish" : "draft",
3361 scheduled_publish_date: data.scheduled_publish_date || null,
3362 scheduled_unpublish_date: data.scheduled_unpublish_date || null,
3363 version: data.version || 1,
3364 seasonal_auto_enable: data.seasonal_auto_enable || false,
3365 seasonal_enable_date: data.seasonal_enable_date || null,
3366 seasonal_disable_date: data.seasonal_disable_date || null,
3367 meta_title: data.meta_title || "",
3368 meta_description: data.meta_description || "",
3369 meta_keywords: data.meta_keywords || "",
3370 attributes: data.attributes || {},
3371 featured_priority: data.featured_priority,
3372 };
3373
3374 if (showDownloadsUI) {
3375 (payload as any).downloadable_items = (data.downloadable_items || [])
3376 .map((item, idx) => ({
3377 id: item.id ?? null,
3378 title: (item.title || "").trim(),
3379 description: item.description || "",
3380 attachment_id: item.attachment_id ?? null,
3381 visibility: item.visibility || "booked_only",
3382 enabled: item.enabled !== false,
3383 sort_order: item.sort_order != null ? item.sort_order : idx + 1,
3384 attachment_url: item.attachment_url || "",
3385 attachment_title: item.attachment_title || "",
3386
3387 // Prefixed keys (requested)
3388 download_title: (item.title || "").trim(),
3389 download_description: item.description || "",
3390 download_visibility: item.visibility || "booked_only",
3391 download_enabled: item.enabled !== false,
3392 download_file: item.attachment_id ?? null,
3393 }))
3394 .filter((item) => item.title);
3395 }
3396
3397 if (isEditMode && tripId) {
3398 const response = await apiClient.put(`/trips/${tripId}`, payload);
3399 return response.data || response;
3400 } else {
3401 const response = await apiClient.post("/trips", payload);
3402 return response.data || response;
3403 }
3404 },
3405 onSuccess: (data, variables) => {
3406 queryClient.invalidateQueries({ queryKey: ["trips"] });
3407 queryClient.invalidateQueries({ queryKey: ["trip", tripId] });
3408 // Force refetch the trip data immediately
3409 queryClient.refetchQueries({ queryKey: ["trip", tripId] });
3410 setIsSubmitting(false);
3411
3412 // Show success message - different for edit vs create mode
3413 let successMessage: string;
3414 if (isEditMode) {
3415 // Edit mode: updating existing trip
3416 successMessage =
3417 variables.status === "publish"
3418 ? __("Trip updated and published successfully", "yatra")
3419 : __("Trip updated successfully", "yatra");
3420 } else {
3421 // Create mode: creating new trip
3422 successMessage =
3423 variables.status === "publish"
3424 ? __("Trip created and published successfully", "yatra")
3425 : __("Trip saved as draft successfully", "yatra");
3426 }
3427 showToast(successMessage, "success");
3428
3429 // Only redirect when creating a NEW trip - stay on page when editing
3430 if (!isEditMode && data?.id) {
3431 // If creating new trip, redirect to edit mode so user can continue editing
3432 setTimeout(() => {
3433 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&action=edit&id=${data.id}`;
3434 }, 1000);
3435 }
3436 // When editing (isEditMode), always stay on the same page - no redirect
3437 },
3438 onError: (error: any) => {
3439 const errorMessage =
3440 error?.response?.data?.message ||
3441 error?.message ||
3442 __("An error occurred while saving", "yatra");
3443 showToast(errorMessage, "error");
3444 setErrors({ submit: errorMessage });
3445 setIsSubmitting(false);
3446 },
3447 });
3448
3449 // Light validation for draft saves (only essential fields)
3450 const validateDraft = (): boolean => {
3451 const newErrors = buildEssentialFieldErrors();
3452 setErrors(newErrors);
3453 return Object.keys(newErrors).length === 0;
3454 };
3455
3456 const handleSaveDraft = async () => {
3457 if (!validateDraft()) {
3458 showToast(
3459 __("Please add a trip title and URL before saving.", "yatra"),
3460 "error",
3461 );
3462 return;
3463 }
3464 setIsSubmitting(true);
3465 saveMutation.mutate({ ...formData, status: "draft" });
3466 };
3467
3468 const handlePublish = async () => {
3469 if (!validateForm()) {
3470 const firstError = Object.keys(errors)[0];
3471 if (firstError) {
3472 showToast(
3473 __("Trip title and slug are required before publishing.", "yatra"),
3474 "error",
3475 );
3476 const errorElement = document.querySelector(
3477 `[name="${firstError}"], #${firstError}`,
3478 );
3479 if (errorElement) {
3480 errorElement.scrollIntoView({ behavior: "smooth", block: "center" });
3481 }
3482 }
3483 return;
3484 }
3485 setIsSubmitting(true);
3486 saveMutation.mutate({ ...formData, status: "publish" });
3487 };
3488
3489 // Enter should trigger publish/update (skip multiline and interactive controls)
3490 useEffect(() => {
3491 const onKeyDown = (e: KeyboardEvent) => {
3492 if (e.key !== "Enter" || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey)
3493 return;
3494
3495 const target = e.target as HTMLElement | null;
3496 if (!target) return;
3497
3498 const tag = target.tagName.toLowerCase();
3499 const isTextarea = tag === "textarea";
3500 const isButton = tag === "button";
3501 const isLink = tag === "a";
3502 const isContentEditable =
3503 target.getAttribute("contenteditable") === "true";
3504
3505 // Ignore Enter in multiline or interactive elements
3506 if (isTextarea || isButton || isLink || isContentEditable) return;
3507
3508 const inputType = (target as HTMLInputElement).type;
3509 if (["submit", "button", "file"].includes(inputType)) return;
3510
3511 e.preventDefault();
3512 if (!isSubmitting) {
3513 handlePublish();
3514 }
3515 };
3516
3517 window.addEventListener("keydown", onKeyDown);
3518 return () => window.removeEventListener("keydown", onKeyDown);
3519 }, [isSubmitting, handlePublish]);
3520
3521 // Using static revisions data for UI only
3522 const revisions = dummyRevisions;
3523 const isLoadingRevisions = false;
3524
3525 const handlePreview = async () => {
3526 const slug = (formData.slug || "").trim();
3527 if (!slug) {
3528 showToast(
3529 __(
3530 "Trip slug is missing. Please add a slug before previewing.",
3531 "yatra",
3532 ),
3533 "error",
3534 );
3535 return;
3536 }
3537
3538 const siteUrl = (window as any)?.yatraAdmin?.siteUrl || "";
3539 const { plainUrl, prettyUrl } = buildYatraSinglePublicUrls({
3540 entity: "trip",
3541 slug,
3542 siteUrl,
3543 bases: settingsData as Record<string, unknown> | null,
3544 });
3545
3546 if (isWordPressPlainPermalink()) {
3547 window.open(plainUrl, "_blank", "noopener,noreferrer");
3548 return;
3549 }
3550
3551 let apiPermalink = (tripData as any)?.permalink || (tripData as any)?.url;
3552
3553 if (!apiPermalink && tripId) {
3554 try {
3555 const detail = await apiClient.get(`/trips/${tripId}`);
3556 apiPermalink =
3557 (detail as any)?.permalink || (detail as any)?.url || apiPermalink;
3558 } catch (error) {
3559 // fall through to prettyUrl
3560 }
3561 }
3562
3563 if (apiPermalink) {
3564 window.open(apiPermalink, "_blank", "noopener,noreferrer");
3565 return;
3566 }
3567
3568 window.open(prettyUrl, "_blank", "noopener,noreferrer");
3569 };
3570
3571 const handleRevisionClick = (revisionId: number) => {
3572 setSelectedRevisionId(revisionId);
3573 setShowRevisionConfirm(true);
3574 };
3575
3576 const handleRevisionConfirm = () => {
3577 // UI only - no actual functionality
3578 if (selectedRevisionId) {
3579 showToast(__("Revision feature is coming soon", "yatra"), "info");
3580 }
3581 setShowRevisionConfirm(false);
3582 setShowRevisionsDialog(false);
3583 setSelectedRevisionId(null);
3584 };
3585
3586 // Skeleton loader for edit mode
3587 if (isEditMode && isLoadingTrip) {
3588 return (
3589 <div className="flex-1 min-h-0 flex flex-col bg-white dark:bg-gray-900 overflow-hidden">
3590 {/* Header Skeleton */}
3591 <div className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-6 py-4 flex items-center justify-between">
3592 <div className="flex items-center gap-4 min-w-0 flex-1">
3593 <div className="min-w-0">
3594 <div className="h-6 w-64 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2" />
3595 <div className="flex items-center gap-3">
3596 <div className="h-5 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3597 <div className="h-6 w-28 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse" />
3598 </div>
3599 </div>
3600 </div>
3601 <div className="flex items-center gap-2">
3602 <div className="h-9 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3603 <div className="h-9 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3604 <div className="h-9 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3605 <div className="h-9 w-36 bg-blue-200 dark:bg-blue-800 rounded animate-pulse" />
3606 </div>
3607 </div>
3608
3609 {/* Navigation Bar Skeleton */}
3610 <div className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 px-4 py-2.5">
3611 <div className="flex items-center justify-between gap-4">
3612 <div className="h-9 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3613 <div className="flex-1 flex flex-col items-center gap-1 max-w-md mx-4">
3614 <div className="h-3 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3615 <div className="w-full h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse" />
3616 <div className="h-2.5 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3617 </div>
3618 <div className="h-9 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3619 </div>
3620 </div>
3621
3622 {/* Main Content Skeleton */}
3623 <div className="flex-1 flex overflow-hidden">
3624 {/* Sidebar Skeleton */}
3625 <div className="w-80 bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 overflow-y-auto">
3626 <div className="p-5 pb-8 space-y-6">
3627 {/* Essentials Section Skeleton */}
3628 <div>
3629 <div className="h-3.5 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-3 px-1" />
3630 <div className="space-y-0.5">
3631 {[...Array(8)].map((_, i) => (
3632 <div
3633 key={`essentials-${i}`}
3634 className="flex items-center gap-2 px-3 py-2.5 rounded-md"
3635 >
3636 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" />
3637 <div className="h-4 flex-1 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3638 </div>
3639 ))}
3640 </div>
3641 </div>
3642
3643 {/* Marketing Section Skeleton */}
3644 <div>
3645 <div className="h-3.5 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-3 px-1" />
3646 <div className="space-y-0.5">
3647 {[...Array(9)].map((_, i) => (
3648 <div
3649 key={`marketing-${i}`}
3650 className="flex items-center gap-2 px-3 py-2.5 rounded-md"
3651 >
3652 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" />
3653 <div className="h-4 flex-1 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3654 </div>
3655 ))}
3656 </div>
3657 </div>
3658
3659 {/* Lifecycle Section Skeleton */}
3660 <div>
3661 <div className="flex items-center gap-2 px-3 py-2.5 rounded-md">
3662 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" />
3663 <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3664 </div>
3665 </div>
3666 </div>
3667 </div>
3668
3669 {/* Main Form Area Skeleton */}
3670 <div className="flex-1 overflow-y-auto p-6">
3671 <div className="max-w-4xl mx-auto space-y-6">
3672 {/* Section Header Skeleton */}
3673 <div className="flex items-center gap-2 mb-4">
3674 <div className="w-5 h-5 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3675 <div className="h-6 w-56 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3676 </div>
3677 <div className="h-4 w-96 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-6" />
3678
3679 {/* Form Fields Skeleton */}
3680 <Card>
3681 <CardContent className="p-6 space-y-6">
3682 {/* Title Field */}
3683 <div className="space-y-2">
3684 <div className="h-3.5 w-12 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3685 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3686 </div>
3687
3688 {/* Slug Field */}
3689 <div className="space-y-2">
3690 <div className="h-3.5 w-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3691 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3692 </div>
3693
3694 {/* Description Field */}
3695 <div className="space-y-2">
3696 <div className="h-3.5 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3697 <div className="h-32 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3698 </div>
3699
3700 {/* Featured Image Skeleton */}
3701 <div className="space-y-2">
3702 <div className="h-3.5 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3703 <div className="h-48 w-full bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse" />
3704 </div>
3705
3706 {/* Grid Fields Skeleton */}
3707 <div className="grid grid-cols-2 gap-4">
3708 {[...Array(4)].map((_, i) => (
3709 <div key={`grid-${i}`} className="space-y-3">
3710 <div className="h-3.5 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3711 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3712 </div>
3713 ))}
3714 </div>
3715 </CardContent>
3716 </Card>
3717 </div>
3718 </div>
3719 </div>
3720 </div>
3721 );
3722 }
3723
3724 if (isEditMode && tripError) {
3725 const requestInfo =
3726 tripErrorContext.requestInfo || (tripError as any)?.config || {};
3727 const method = (requestInfo.method || "GET").toUpperCase?.() || "GET";
3728 const url = requestInfo.url || (tripError as any)?.config?.url || "";
3729 const payload =
3730 requestInfo.payload || (tripError as any)?.config?.data || "";
3731 const composedErrorText = `Method: ${method}\nURL: ${url}\nPayload: ${payload || "N/A"}\n\n${tripErrorContext.details || ""}`;
3732
3733 return (
3734 <div className="min-h-screen flex items-center justify-center bg-white">
3735 <div className="w-full max-w-5xl px-6 py-12">
3736 <div className="relative flex flex-col items-center text-center gap-4 rounded-2xl border border-dashed border-[#fddfe1] bg-gradient-to-br from-[#fff7f8] via-white to-[#fff9f5] p-10 shadow-sm">
3737 <div className="w-16 h-16 rounded-2xl bg-white border border-[#ffdede] flex items-center justify-center">
3738 <svg
3739 className="w-10 h-10 text-red-500"
3740 fill="none"
3741 stroke="currentColor"
3742 viewBox="0 0 24 24"
3743 strokeWidth="1.8"
3744 >
3745 <path
3746 strokeLinecap="round"
3747 strokeLinejoin="round"
3748 d="M12 9v4m0 4h.01M10.29 3.86L2.82 17a2 2 0 001.71 3h14.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
3749 />
3750 </svg>
3751 </div>
3752 <div className="space-y-1">
3753 <h2 className="text-2xl font-semibold text-gray-900">
3754 {__("Error Loading Trips", "yatra")}
3755 </h2>
3756 <p className="text-sm text-gray-600">
3757 {__(
3758 "We couldn’t connect to the trips service. Please refresh or try again shortly.",
3759 "yatra",
3760 )}
3761 </p>
3762 </div>
3763 <div className="flex flex-wrap gap-2 justify-center">
3764 <Button
3765 onClick={() =>
3766 queryClient.invalidateQueries({ queryKey: ["trip", tripId] })
3767 }
3768 >
3769 {__("Try again", "yatra")}
3770 </Button>
3771 <Button
3772 variant="outline"
3773 onClick={() => {
3774 if (typeof window !== "undefined") {
3775 window.open(
3776 "https://wordpress.org/support/plugin/yatra",
3777 "_blank",
3778 );
3779 }
3780 }}
3781 >
3782 {__("Visit support center", "yatra")}
3783 </Button>
3784 </div>
3785
3786 <div className="w-full mt-10">
3787 <div className="relative w-full text-left rounded-2xl border border-gray-200 bg-white shadow-sm space-y-0">
3788 <div className="flex items-center justify-between px-5 py-3 border-b border-gray-200">
3789 <p className="text-sm font-medium text-gray-800">
3790 {__("Technical details", "yatra")}
3791 </p>
3792 <Button
3793 type="button"
3794 size="sm"
3795 variant="ghost"
3796 className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
3797 onClick={() =>
3798 copyErrorDetailsToClipboard(composedErrorText)
3799 }
3800 >
3801 {copiedErrorDetails ? (
3802 <>
3803 <Check className="w-4 h-4" />
3804 {__("Copied", "yatra")}
3805 </>
3806 ) : (
3807 <>
3808 <Copy className="w-4 h-4" />
3809 {__("Copy details", "yatra")}
3810 </>
3811 )}
3812 </Button>
3813 </div>
3814 <div className="px-5 py-3 border-b border-gray-200 space-y-2 text-sm text-left text-gray-800">
3815 <div>
3816 <span className="font-medium">
3817 {__("Method:", "yatra")}
3818 </span>{" "}
3819 <span className="font-mono">{method}</span>
3820 </div>
3821 <div className="break-all">
3822 <span className="font-medium">{__("URL:", "yatra")}</span>{" "}
3823 <span className="font-mono">
3824 {url || __("N/A", "yatra")}
3825 </span>
3826 </div>
3827 {payload && (
3828 <div>
3829 <span className="font-medium block mb-1">
3830 {__("Payload:", "yatra")}
3831 </span>
3832 <pre className="max-h-40 overflow-auto px-3 py-2 rounded bg-gray-50 text-xs font-mono text-gray-900 whitespace-pre-wrap border border-gray-200">
3833 {payload}
3834 </pre>
3835 </div>
3836 )}
3837 </div>
3838 <pre className="max-h-56 overflow-auto px-5 py-3 text-xs leading-relaxed font-mono text-gray-900 whitespace-pre-wrap bg-white">
3839 {tripErrorContext.details ||
3840 JSON.stringify(
3841 {
3842 message:
3843 tripError instanceof Error
3844 ? tripError.message
3845 : __("Failed to load trip data", "yatra"),
3846 method,
3847 url,
3848 payload,
3849 },
3850 null,
3851 2,
3852 )}
3853 </pre>
3854 </div>
3855 </div>
3856 </div>
3857 </div>
3858 </div>
3859 );
3860 }
3861
3862 const renderSectionContent = () => {
3863 switch (currentSection) {
3864 case "basic":
3865 return (
3866 <div className="space-y-6">
3867 <div className="flex items-center gap-2 mb-3">
3868 <FileText className="w-5 h-5 text-blue-600 dark:text-blue-400" />
3869 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
3870 {__("Basic Information", "yatra")}
3871 </h2>
3872 <Badge
3873 variant="outline"
3874 className="ml-2 text-xs bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800"
3875 >
3876 {__("Start Here", "yatra")}
3877 </Badge>
3878 </div>
3879 <div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-500 dark:border-blue-400 p-3 rounded-r-md mb-6">
3880 <p className="text-sm text-blue-900 dark:text-blue-100 font-medium mb-1">
3881 {__("💡 Getting Started", "yatra")}
3882 </p>
3883 <p className="text-xs text-blue-800 dark:text-blue-200">
3884 {__(
3885 "Only the Trip Title and Trip URL are required to create a draft. Everything else is optional for now, but filling it in is highly recommended for better discovery and conversions.",
3886 "yatra",
3887 )}
3888 </p>
3889 </div>
3890
3891 {/* Essential Fields - Single View (No Tabs) */}
3892 <div className="space-y-6">
3893 {/* Tour Title */}
3894 <div className="mb-4">
3895 <div className="flex items-center justify-between mb-1.5">
3896 <label
3897 htmlFor="title"
3898 className="block text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5"
3899 >
3900 {__("Trip Title", "yatra")}
3901 <Badge
3902 variant="outline"
3903 className="ml-1 text-[10px] px-1.5 py-0 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800"
3904 >
3905 {__("Required", "yatra")}
3906 </Badge>
3907 <button
3908 type="button"
3909 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto"
3910 title={__(
3911 "A catchy title that describes your trip. Recommended: 50-60 characters for best SEO results.",
3912 "yatra",
3913 )}
3914 >
3915 <HelpCircle className="w-3.5 h-3.5" />
3916 </button>
3917 </label>
3918 <span
3919 className={`text-xs font-medium ${
3920 formData.title.length > 60
3921 ? "text-red-600 dark:text-red-400"
3922 : formData.title.length >= 50
3923 ? "text-green-600 dark:text-green-400"
3924 : "text-gray-500 dark:text-gray-400"
3925 }`}
3926 >
3927 {formData.title.length}/60
3928 </span>
3929 </div>
3930 <HelpText
3931 text={__(
3932 '💡 Tip: Include your destination and trip length. Example: "Bali Beach Retreat - 7 Days" or "Paris City Tour - Half Day"',
3933 "yatra",
3934 )}
3935 className="mb-2"
3936 />
3937 <Input
3938 id="title"
3939 type="text"
3940 value={formData.title}
3941 onChange={(e) => handleTitleChange(e.target.value)}
3942 placeholder={__("e.g., Bali Beach Retreat - 7 Days", "yatra")}
3943 maxLength={100}
3944 className={`${errors.title ? "border-red-500" : formData.title && formData.title.length <= 60 ? "border-green-500" : ""} transition-colors`}
3945 required
3946 />
3947 {errors.title ? (
3948 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
3949 <AlertCircle className="w-4 h-4" />
3950 {errors.title}
3951 </p>
3952 ) : (
3953 formData.title &&
3954 formData.title.length > 60 && (
3955 <p className="mt-1.5 text-xs text-yellow-600 dark:text-yellow-400 flex items-center gap-1">
3956 <AlertCircle className="w-3.5 h-3.5" />
3957 {__(
3958 "Title is longer than recommended for SEO (60 characters)",
3959 "yatra",
3960 )}
3961 </p>
3962 )
3963 )}
3964 </div>
3965
3966 {/* URL Slug */}
3967 <div className="mb-4">
3968 <div className="flex items-center justify-between mb-1.5">
3969 <label
3970 htmlFor="slug"
3971 className="block text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5"
3972 >
3973 {__("Trip URL", "yatra")}
3974 <Badge
3975 variant="outline"
3976 className="ml-1 text-[10px] px-1.5 py-0 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800"
3977 >
3978 {__("Required", "yatra")}
3979 </Badge>
3980 <button
3981 type="button"
3982 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto"
3983 title={__(
3984 "The web address for your trip page. Auto-generated from your title, but you can customize it.",
3985 "yatra",
3986 )}
3987 >
3988 <HelpCircle className="w-3.5 h-3.5" />
3989 </button>
3990 </label>
3991 {showSlugPreview && formData.slug && (
3992 <button
3993 type="button"
3994 onClick={() => setShowSlugPreview(false)}
3995 className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
3996 >
3997 {__("Hide", "yatra")}
3998 </button>
3999 )}
4000 </div>
4001 <HelpText
4002 text={__(
4003 "💡 This is automatically created from your title. You usually don't need to change it unless you want a custom web address.",
4004 "💡 This is automatically created from your title. You usually don't need to change it unless you want a custom web address.",
4005 )}
4006 className="mb-2"
4007 />
4008 <Input
4009 id="slug"
4010 type="text"
4011 value={formData.slug}
4012 onChange={(e) => handleFieldChange("slug", e.target.value)}
4013 placeholder={__("bali-beach-retreat-7-days", "yatra")}
4014 className={`font-mono text-sm ${errors.slug ? "border-red-500" : ""}`}
4015 required
4016 />
4017 {errors.slug && (
4018 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4019 <AlertCircle className="w-4 h-4" />
4020 {errors.slug}
4021 </p>
4022 )}
4023 {showSlugPreview && formData.slug && !errors.slug && (
4024 <div className="mt-2 p-2.5 bg-blue-50 dark:bg-blue-900/20 rounded text-xs text-gray-600 dark:text-gray-400 border border-blue-200 dark:border-blue-800 flex items-center justify-between">
4025 <div>
4026 <span className="text-gray-500 dark:text-gray-400">
4027 {__("Preview URL:", "yatra")}{" "}
4028 </span>
4029 <span className="font-mono text-blue-600 dark:text-blue-400">
4030 {(window as any).yatraAdmin?.siteUrl || "yoursite.com"}/
4031 {settingsData?.trip_base || "trip"}/{formData.slug}
4032 </span>
4033 </div>
4034 <button
4035 type="button"
4036 onClick={() => {
4037 const tripBase = settingsData?.trip_base || "trip";
4038 const url = `${(window as any).yatraAdmin?.siteUrl || "yoursite.com"}/${tripBase}/${formData.slug}`;
4039 navigator.clipboard.writeText(url);
4040 showToast(
4041 __("URL copied to clipboard", "yatra"),
4042 "success",
4043 );
4044 }}
4045 className="ml-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-1"
4046 title={__("Copy URL", "yatra")}
4047 >
4048 <Copy className="w-3.5 h-3.5" />
4049 </button>
4050 </div>
4051 )}
4052 {!showSlugPreview && formData.slug && (
4053 <button
4054 type="button"
4055 onClick={() => setShowSlugPreview(true)}
4056 className="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline"
4057 >
4058 {__("Show URL Preview", "yatra")}
4059 </button>
4060 )}
4061 </div>
4062
4063 {/* Short Description */}
4064 <div className="mb-4">
4065 <div className="flex items-center justify-between mb-1.5">
4066 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5">
4067 {__("Short Description", "yatra")}
4068 <Badge
4069 variant="outline"
4070 className="ml-1 text-[10px] px-1.5 py-0 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-800"
4071 >
4072 {__("Recommended", "yatra")}
4073 </Badge>
4074 <button
4075 type="button"
4076 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto"
4077 title={__(
4078 "A brief summary that appears in listings. Recommended: 100-150 characters for best results.",
4079 "yatra",
4080 )}
4081 >
4082 <HelpCircle className="w-3.5 h-3.5" />
4083 </button>
4084 </label>
4085 <span
4086 className={`text-xs font-medium ${
4087 formData.short_description.length > 200
4088 ? "text-red-600 dark:text-red-400"
4089 : formData.short_description.length >= 100 &&
4090 formData.short_description.length <= 150
4091 ? "text-green-600 dark:text-green-400"
4092 : formData.short_description.length > 0
4093 ? "text-yellow-600 dark:text-yellow-400"
4094 : "text-gray-500 dark:text-gray-400"
4095 }`}
4096 >
4097 {formData.short_description.length}/200
4098 </span>
4099 </div>
4100 <HelpText
4101 text={__(
4102 '💡 Write 2-3 sentences that grab attention! This appears in trip listings. Example: "Escape to paradise with our 7-day Bali beach retreat. Experience stunning sunsets, world-class diving, and authentic local culture."',
4103 "yatra",
4104 )}
4105 className="mb-2"
4106 />
4107 <RichTextEditor
4108 value={formData.short_description}
4109 onChange={(value) =>
4110 handleFieldChange("short_description", value)
4111 }
4112 placeholder={__(
4113 "Escape to paradise with our 7-day Bali beach retreat...",
4114 "yatra",
4115 )}
4116 minHeight={120}
4117 />
4118 {formData.short_description.length > 0 &&
4119 formData.short_description.length < 100 && (
4120 <p className="mt-1.5 text-xs text-yellow-600 dark:text-yellow-400 flex items-center gap-1">
4121 <AlertCircle className="w-3.5 h-3.5" />
4122 {__(
4123 "Consider adding more details (recommended: 100-150 characters)",
4124 "yatra",
4125 )}
4126 </p>
4127 )}
4128 </div>
4129
4130 {/* Tour Description */}
4131 <div className="mb-4">
4132 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5 flex items-center gap-1.5">
4133 {__("Trip Description", "yatra")}
4134 <Badge
4135 variant="outline"
4136 className="ml-1 text-[10px] px-1.5 py-0 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-800"
4137 >
4138 {__("Recommended", "yatra")}
4139 </Badge>
4140 </label>
4141 <HelpText
4142 text={__(
4143 "💡 Tell travelers what makes your trip special! Describe the experience, highlights, and what they'll see. Write 2-4 paragraphs. Be enthusiastic and detailed!",
4144 "💡 Tell travelers what makes your trip special! Describe the experience, highlights, and what they'll see. Write 2-4 paragraphs. Be enthusiastic and detailed!",
4145 )}
4146 className="mb-2"
4147 />
4148 <RichTextEditor
4149 value={formData.description}
4150 onChange={(value) => handleFieldChange("description", value)}
4151 placeholder={__(
4152 "Escape to paradise with our 7-day Bali beach retreat... Or describe your single day trip experience...",
4153 "yatra",
4154 )}
4155 minHeight={260}
4156 />
4157 {errors.description && (
4158 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4159 <AlertCircle className="w-4 h-4" />
4160 {errors.description}
4161 </p>
4162 )}
4163 </div>
4164
4165 {/* Featured Image */}
4166 <div className="mb-6">
4167 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5 flex items-center gap-1.5">
4168 {__("Featured Image", "yatra")}
4169 <Badge
4170 variant="outline"
4171 className="ml-1 text-[10px] px-1.5 py-0 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-800"
4172 >
4173 {__("Recommended", "yatra")}
4174 </Badge>
4175 <button
4176 type="button"
4177 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto"
4178 title={__(
4179 "The main image that represents your trip. This appears in listings and as the hero image on the trip page.",
4180 "yatra",
4181 )}
4182 >
4183 <HelpCircle className="w-3.5 h-3.5" />
4184 </button>
4185 </label>
4186 <HelpText
4187 text={__(
4188 "💡 Choose your best photo! This is the first thing travelers see. Use a high-quality image (1200x800px recommended) that shows what makes your trip special.",
4189 "yatra",
4190 )}
4191 className="mb-2"
4192 />
4193 {formData.featured_image ? (
4194 <div className="relative group">
4195 <div
4196 className={`aspect-video bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden border-2 ${
4197 errors.featured_image
4198 ? "border-red-500 dark:border-red-600"
4199 : "border-gray-200 dark:border-gray-700"
4200 }`}
4201 >
4202 {featuredImagePreview ? (
4203 <img
4204 src={featuredImagePreview}
4205 alt={__("Featured Image", "yatra")}
4206 className="w-full h-full object-cover"
4207 />
4208 ) : (
4209 <div className="flex items-center justify-center w-full h-full text-sm text-gray-500 dark:text-gray-400">
4210 {isResolvingFeaturedImage
4211 ? __("Loading image...", "yatra")
4212 : __("Preview unavailable", "yatra")}
4213 </div>
4214 )}
4215 </div>
4216 <button
4217 type="button"
4218 onClick={() => handleFieldChange("featured_image", null)}
4219 className="absolute top-2 right-2 bg-red-600 text-white rounded-full p-1.5 opacity-0 group-hover:opacity-100 transition-opacity"
4220 title={__("Remove image", "yatra")}
4221 >
4222 <X className="w-4 h-4" />
4223 </button>
4224 </div>
4225 ) : (
4226 <button
4227 type="button"
4228 onClick={() => {
4229 // Use WordPress media library
4230 if (window.wp && window.wp.media) {
4231 const mediaUploader = window.wp.media({
4232 title: __("Select Featured Image", "yatra"),
4233 button: { text: __("Use this image", "yatra") },
4234 multiple: false,
4235 });
4236 mediaUploader.on("select", () => {
4237 const attachment = mediaUploader
4238 .state()
4239 .get("selection")
4240 .first()
4241 .toJSON();
4242 handleFieldChange("featured_image", attachment.id);
4243 });
4244 prepareWordPressMediaFrameOpen();
4245 mediaUploader.open();
4246 }
4247 }}
4248 className={`w-full aspect-video border-2 border-dashed rounded-lg flex flex-col items-center justify-center transition-colors ${
4249 errors.featured_image
4250 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
4251 : "border-gray-300 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-400"
4252 }`}
4253 >
4254 <Upload
4255 className={`w-10 h-10 mb-2 ${errors.featured_image ? "text-red-400" : "text-gray-400"}`}
4256 />
4257 <span
4258 className={`text-sm font-medium ${errors.featured_image ? "text-red-600 dark:text-red-400" : "text-gray-600 dark:text-gray-400"}`}
4259 >
4260 {__("Upload Featured Image", "yatra")}
4261 </span>
4262 <span
4263 className={`text-xs mt-1 ${errors.featured_image ? "text-red-500 dark:text-red-500" : "text-gray-500 dark:text-gray-500"}`}
4264 >
4265 {__("Recommended: 1200x800px", "yatra")}
4266 </span>
4267 </button>
4268 )}
4269 {errors.featured_image && (
4270 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4271 <AlertCircle className="w-4 h-4" />
4272 {errors.featured_image}
4273 </p>
4274 )}
4275 </div>
4276
4277 {/* Trip Highlights */}
4278 <Card className="mt-6">
4279 <CardHeader>
4280 <CardTitle>{__("Trip Highlights", "yatra")}</CardTitle>
4281 <CardDescription>
4282 {__(
4283 "Add key highlights that make your trip special. These will be displayed prominently on your trip page.",
4284 "yatra",
4285 )}
4286 </CardDescription>
4287 </CardHeader>
4288 <CardContent className="space-y-3">
4289 {formData.highlights.length > 0 ? (
4290 <div className="space-y-2">
4291 {formData.highlights.map((highlight, index) => (
4292 <div
4293 key={index}
4294 className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"
4295 >
4296 <CheckCircle2 className="w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0" />
4297 <span className="flex-1 text-sm text-gray-900 dark:text-white">
4298 {highlight}
4299 </span>
4300 <Button
4301 type="button"
4302 variant="ghost"
4303 size="sm"
4304 onClick={() => handleHighlightRemove(index)}
4305 className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"
4306 >
4307 <X className="w-4 h-4" />
4308 </Button>
4309 </div>
4310 ))}
4311 </div>
4312 ) : (
4313 <div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
4314 <Sparkles className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
4315 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
4316 {__("No highlights added yet", "yatra")}
4317 </p>
4318 <p className="text-xs text-gray-500 dark:text-gray-500 mb-4">
4319 {__(
4320 'Add key selling points like "Private guide", "All meals included", or "Skip-the-line tickets"',
4321 "yatra",
4322 )}
4323 </p>
4324 </div>
4325 )}
4326 <Button
4327 type="button"
4328 variant="outline"
4329 onClick={handleHighlightAdd}
4330 className="w-full"
4331 >
4332 <Plus className="w-4 h-4 mr-2" />
4333 {__("Add Highlight", "yatra")}
4334 </Button>
4335 </CardContent>
4336 </Card>
4337
4338 {/* Trip Type & Duration - Moved from Duration section */}
4339 <div className="mt-6 space-y-4">
4340 <div className="border-t border-gray-200 dark:border-gray-700 pt-6">
4341 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
4342 {__("Trip Type & Duration", "yatra")}
4343 </h3>
4344
4345 {/* Trip Type */}
4346 <div className="mb-4">
4347 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-3">
4348 {__("Trip Type", "yatra")}
4349 </label>
4350 <div
4351 className={`grid grid-cols-1 md:grid-cols-2 gap-4 ${
4352 errors.trip_type ? "mb-2" : ""
4353 }`}
4354 >
4355 <label
4356 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
4357 formData.trip_type === "single_day"
4358 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
4359 : errors.trip_type
4360 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
4361 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
4362 }`}
4363 >
4364 <input
4365 type="radio"
4366 name="trip_type"
4367 value="single_day"
4368 checked={formData.trip_type === "single_day"}
4369 onChange={(e) => {
4370 handleFieldChange("trip_type", e.target.value);
4371 if (e.target.value === "single_day") {
4372 setFormData((prev) => ({
4373 ...prev,
4374 duration_days: "1",
4375 duration_nights: "0",
4376 }));
4377 }
4378 }}
4379 className="sr-only"
4380 />
4381 <div className="flex flex-1">
4382 <div className="flex flex-col">
4383 <span
4384 className={`block text-sm font-semibold ${
4385 formData.trip_type === "single_day"
4386 ? "text-blue-900 dark:text-blue-300"
4387 : "text-gray-900 dark:text-white"
4388 }`}
4389 >
4390 {__("Single Day Trip", "yatra")}
4391 </span>
4392 <span
4393 className={`mt-1 flex items-center text-sm ${
4394 formData.trip_type === "single_day"
4395 ? "text-blue-700 dark:text-blue-400"
4396 : "text-gray-500 dark:text-gray-400"
4397 }`}
4398 >
4399 {__(
4400 "Trip completed within one day (no overnight stay)",
4401 "yatra",
4402 )}
4403 </span>
4404 </div>
4405 </div>
4406 {formData.trip_type === "single_day" && (
4407 <div className="absolute top-4 right-4">
4408 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
4409 </div>
4410 )}
4411 </label>
4412
4413 <label
4414 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
4415 formData.trip_type === "multi_day"
4416 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
4417 : errors.trip_type
4418 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
4419 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
4420 }`}
4421 >
4422 <input
4423 type="radio"
4424 name="trip_type"
4425 value="multi_day"
4426 checked={formData.trip_type === "multi_day"}
4427 onChange={(e) =>
4428 handleFieldChange("trip_type", e.target.value)
4429 }
4430 className="sr-only"
4431 />
4432 <div className="flex flex-1">
4433 <div className="flex flex-col">
4434 <span
4435 className={`block text-sm font-semibold ${
4436 formData.trip_type === "multi_day"
4437 ? "text-blue-900 dark:text-blue-300"
4438 : "text-gray-900 dark:text-white"
4439 }`}
4440 >
4441 {__("Multi-Day Trip", "yatra")}
4442 </span>
4443 <span
4444 className={`mt-1 flex items-center text-sm ${
4445 formData.trip_type === "multi_day"
4446 ? "text-blue-700 dark:text-blue-400"
4447 : "text-gray-500 dark:text-gray-400"
4448 }`}
4449 >
4450 {__(
4451 "Trip spans multiple days with overnight stays",
4452 "yatra",
4453 )}
4454 </span>
4455 </div>
4456 </div>
4457 {formData.trip_type === "multi_day" && (
4458 <div className="absolute top-4 right-4">
4459 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
4460 </div>
4461 )}
4462 </label>
4463 </div>
4464 {errors.trip_type && (
4465 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4466 <AlertCircle className="w-4 h-4" />
4467 {errors.trip_type}
4468 </p>
4469 )}
4470 </div>
4471
4472 {/* Duration Days & Nights */}
4473 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
4474 <div>
4475 <label
4476 htmlFor="duration_days"
4477 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
4478 >
4479 {__("Duration (Days)", "yatra")}
4480 </label>
4481 <Input
4482 id="duration_days"
4483 type="number"
4484 min="1"
4485 value={formData.duration_days}
4486 onChange={(e) => {
4487 const days = e.target.value;
4488 handleFieldChange("duration_days", days);
4489 // Auto-set nights based on trip type
4490 if (formData.trip_type === "single_day") {
4491 setFormData((prev) => ({
4492 ...prev,
4493 duration_nights: "0",
4494 }));
4495 } else if (days && parseInt(days) > 1) {
4496 // For multi-day, typically nights = days - 1
4497 const nights = Math.max(
4498 0,
4499 parseInt(days) - 1,
4500 ).toString();
4501 setFormData((prev) => ({
4502 ...prev,
4503 duration_nights: nights,
4504 }));
4505 }
4506 }}
4507 placeholder={
4508 formData.trip_type === "single_day"
4509 ? "1"
4510 : __("e.g., 7", "yatra")
4511 }
4512 className={errors.duration_days ? "border-red-500" : ""}
4513 disabled={formData.trip_type === "single_day"}
4514 />
4515 <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
4516 {formData.trip_type === "single_day"
4517 ? __("Single day trips are always 1 day", "yatra")
4518 : __(
4519 "Enter the number of days for your trip",
4520 "yatra",
4521 )}
4522 </p>
4523 {errors.duration_days && (
4524 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4525 <AlertCircle className="w-4 h-4" />
4526 {errors.duration_days}
4527 </p>
4528 )}
4529 </div>
4530 <div>
4531 <label
4532 htmlFor="duration_nights"
4533 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
4534 >
4535 {__("Duration (Nights)", "yatra")}
4536 </label>
4537 <Input
4538 id="duration_nights"
4539 type="number"
4540 min="0"
4541 value={formData.duration_nights}
4542 onChange={(e) =>
4543 handleFieldChange("duration_nights", e.target.value)
4544 }
4545 placeholder={
4546 formData.trip_type === "single_day"
4547 ? "0"
4548 : __("e.g., 6", "yatra")
4549 }
4550 className={
4551 errors.duration_nights ? "border-red-500" : ""
4552 }
4553 disabled={formData.trip_type === "single_day"}
4554 />
4555 <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
4556 {formData.trip_type === "single_day"
4557 ? __(
4558 "Single day trips have 0 nights (no overnight stay)",
4559 "yatra",
4560 )
4561 : __(
4562 "Enter the number of nights (typically days - 1)",
4563 "yatra",
4564 )}
4565 </p>
4566 {errors.duration_nights && (
4567 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4568 <AlertCircle className="w-4 h-4" />
4569 {errors.duration_nights}
4570 </p>
4571 )}
4572 </div>
4573 </div>
4574 </div>
4575 </div>
4576 </div>
4577 </div>
4578 );
4579
4580 case "location":
4581 return (
4582 <div className="space-y-4">
4583 <div className="flex flex-wrap items-center gap-2 mb-2">
4584 <MapPin className="w-5 h-5 text-gray-500" />
4585 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
4586 {__("Location & Geography", "yatra")}
4587 </h2>
4588 <Badge
4589 variant="outline"
4590 className="text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-300 border-amber-200 dark:border-amber-800"
4591 >
4592 {__("Recommended", "yatra")}
4593 </Badge>
4594 </div>
4595 <div className="bg-amber-50 dark:bg-amber-900/20 border-l-4 border-amber-500 dark:border-amber-400 p-3 rounded-r-md">
4596 <p className="text-sm font-medium text-amber-900 dark:text-amber-100 mb-1">
4597 {__(
4598 "Specify where your trip takes place, including destinations, coordinates, and key landmarks.",
4599 "yatra",
4600 )}
4601 </p>
4602 <p className="text-xs text-amber-800 dark:text-amber-200">
4603 {__(
4604 "Optional but highly recommended to help travelers understand the experience.",
4605 "yatra",
4606 )}
4607 </p>
4608 </div>
4609
4610 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
4611 {__(
4612 "Optimize how your trip appears in search engines and social shares",
4613 "yatra",
4614 )}
4615 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
4616 {__(
4617 "Optional, but completing these fields improves SEO and click-through rates.",
4618 "yatra",
4619 )}
4620 </span>
4621 </p>
4622 <div className="space-y-4">
4623 {/* Destinations - Multiple Selection */}
4624 <div>
4625 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
4626 {__("Destinations", "yatra")}
4627 </label>
4628 {destinationsData && destinationsData.length > 0 ? (
4629 <MultiSelect
4630 value={formData.destinations.map((id) => id.toString())}
4631 onChange={(values) =>
4632 handleFieldChange(
4633 "destinations",
4634 values.map((v) => Number(v)),
4635 )
4636 }
4637 options={destinationsData.map((destination: any) => ({
4638 value: destination.id.toString(),
4639 label:
4640 destination.name || `Destination #${destination.id}`,
4641 }))}
4642 placeholder={__("Select destinations...", "yatra")}
4643 searchPlaceholder={__("Search destinations...", "yatra")}
4644 error={!!errors.destinations}
4645 />
4646 ) : (
4647 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
4648 <p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
4649 {__(
4650 "No destinations available. Please create destinations first.",
4651 "yatra",
4652 )}
4653 </p>
4654 <div className="space-y-2">
4655 <p className="text-xs text-gray-400 dark:text-gray-500">
4656 {__("To create destinations:", "yatra")}
4657 </p>
4658 <ol className="text-xs text-gray-400 dark:text-gray-500 text-left list-decimal list-inside space-y-1 max-w-md mx-auto">
4659 <li>
4660 {__(
4661 "Go to Yatra → Destinations in your WordPress admin",
4662 "yatra",
4663 )}
4664 </li>
4665 <li>{__('Click "Add New Destination"', "yatra")}</li>
4666 <li>
4667 {__("Enter destination name and details", "yatra")}
4668 </li>
4669 <li>{__('Set status to "Published"', "yatra")}</li>
4670 </ol>
4671 <a
4672 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=destinations&action=create"
4673 target="_blank"
4674 rel="noopener noreferrer"
4675 className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
4676 >
4677 <svg
4678 className="w-3 h-3 mr-1"
4679 fill="none"
4680 stroke="currentColor"
4681 viewBox="0 0 24 24"
4682 >
4683 <path
4684 strokeLinecap="round"
4685 strokeLinejoin="round"
4686 strokeWidth={2}
4687 d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
4688 />
4689 </svg>
4690 {__("Create Destinations", "yatra")}
4691 </a>
4692 </div>
4693 </div>
4694 )}
4695 <HelpText
4696 text={__(
4697 "Select all destinations included in this trip",
4698 "yatra",
4699 )}
4700 className="mt-2"
4701 />
4702 {errors.destinations && (
4703 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4704 <AlertCircle className="w-4 h-4" />
4705 {errors.destinations}
4706 </p>
4707 )}
4708 </div>
4709
4710 {/* Locations Section - Senior UI/UX Design */}
4711 <div className="space-y-8">
4712 {/* Section Header */}
4713 <div className="text-center space-y-2">
4714 <h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center justify-center gap-2">
4715 <div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
4716 <MapPin className="w-3 h-3 text-white" />
4717 </div>
4718 {__("Trip Locations", "yatra")}
4719 </h3>
4720 <p className="text-sm text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
4721 {__(
4722 "Set precise starting and ending points with location names and GPS coordinates. Use manual entry or visual map selection for maximum accuracy.",
4723 "yatra",
4724 )}
4725 </p>
4726 </div>
4727
4728 {/* Locations Grid */}
4729 <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
4730 {/* STARTING LOCATION */}
4731 <div className="space-y-6">
4732 {/* Location Header */}
4733 <div className="flex items-center gap-3 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-xl border border-blue-200 dark:border-blue-800">
4734 <div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
4735 <MapPin className="w-5 h-5 text-white" />
4736 </div>
4737 <div className="flex-1">
4738 <h4 className="text-base font-semibold text-blue-900 dark:text-blue-100">
4739 {__("Starting Point", "yatra")}
4740 </h4>
4741 <p className="text-xs text-blue-700 dark:text-blue-300">
4742 {__("Where the journey begins", "yatra")}
4743 </p>
4744 </div>
4745 <div className="flex items-center gap-2">
4746 {formData.starting_location && (
4747 <div
4748 className="w-2 h-2 bg-green-500 rounded-full animate-pulse"
4749 title="Location set"
4750 ></div>
4751 )}
4752 {formData.starting_latitude &&
4753 formData.starting_longitude && (
4754 <div
4755 className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"
4756 title="Coordinates set"
4757 ></div>
4758 )}
4759 </div>
4760 </div>
4761
4762 {/* Integrated Location Input with Map */}
4763 <div className="space-y-4">
4764 <div className="flex items-center justify-between">
4765 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
4766 <div className="w-4 h-4 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center">
4767 <span className="text-xs font-bold text-blue-600 dark:text-blue-400">
4768 1
4769 </span>
4770 </div>
4771 {__("Starting Location", "yatra")}
4772 </label>
4773 <div className="flex items-center gap-2">
4774 <span className="text-xs text-gray-500 dark:text-gray-400">
4775 {formData.starting_location
4776 ? "✓"
4777 : __("Required", "yatra")}
4778 </span>
4779 {formData.starting_latitude &&
4780 formData.starting_longitude && (
4781 <div className="flex items-center gap-1 text-xs text-green-600">
4782 <div className="w-2 h-2 bg-green-500 rounded-full"></div>
4783 {__("Coords Set", "yatra")}
4784 </div>
4785 )}
4786 </div>
4787 </div>
4788
4789 {/* LocationPicker - Primary Interface */}
4790 <div className="space-y-3">
4791 <LocationPicker
4792 value={{
4793 name: formData.starting_location,
4794 latitude: formData.starting_latitude,
4795 longitude: formData.starting_longitude,
4796 }}
4797 onChange={(locationData) => {
4798 handleFieldChange(
4799 "starting_location",
4800 locationData.name,
4801 );
4802 handleFieldChange(
4803 "starting_latitude",
4804 locationData.latitude,
4805 );
4806 handleFieldChange(
4807 "starting_longitude",
4808 locationData.longitude,
4809 );
4810 }}
4811 label=""
4812 placeholder={__(
4813 "Search for starting location...",
4814 "yatra",
4815 )}
4816 helpText=""
4817 required={false}
4818 defaultMapCenter={
4819 formData.starting_latitude &&
4820 formData.starting_longitude
4821 ? [
4822 parseFloat(formData.starting_latitude),
4823 parseFloat(formData.starting_longitude),
4824 ]
4825 : [-8.3405, 115.092]
4826 }
4827 defaultZoom={13}
4828 mapHeight="300px"
4829 showMapButton={false}
4830 searchLimit={8}
4831 __={__}
4832 className=""
4833 mapClassName="rounded-lg"
4834 />
4835 </div>
4836 </div>
4837
4838 {/* GPS Coordinates - Manual Override */}
4839 <div className="space-y-4">
4840 <div className="flex items-center justify-between">
4841 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
4842 <div className="w-4 h-4 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center">
4843 <span className="text-xs font-bold text-blue-600 dark:text-blue-400">
4844 2
4845 </span>
4846 </div>
4847 {__("GPS Coordinates", "yatra")}
4848 <span className="text-xs text-gray-500 dark:text-gray-400">
4849 ({__("Manual override", "yatra")})
4850 </span>
4851 </label>
4852 <div className="flex items-center gap-2">
4853 <button
4854 type="button"
4855 onClick={() => {
4856 if (navigator.geolocation) {
4857 navigator.geolocation.getCurrentPosition(
4858 (position) => {
4859 handleFieldChange(
4860 "starting_latitude",
4861 position.coords.latitude.toString(),
4862 );
4863 handleFieldChange(
4864 "starting_longitude",
4865 position.coords.longitude.toString(),
4866 );
4867 },
4868 (error) => {
4869 let message = __(
4870 "Unable to get your location",
4871 "yatra",
4872 );
4873 let showHttpsNotice = false;
4874
4875 switch (error.code) {
4876 case 1: // PERMISSION_DENIED
4877 if (
4878 error.message.includes(
4879 "secure origins",
4880 )
4881 ) {
4882 message = __(
4883 "Location access requires HTTPS. This feature will work on your live HTTPS site.",
4884 "yatra",
4885 );
4886 showHttpsNotice = true;
4887 } else {
4888 message = __(
4889 "Location access denied. Please allow location access in your browser.",
4890 "yatra",
4891 );
4892 }
4893 break;
4894 case 2: // POSITION_UNAVAILABLE
4895 message = __(
4896 "Location information is unavailable. Please try again.",
4897 "yatra",
4898 );
4899 break;
4900 case 3: // TIMEOUT
4901 message = __(
4902 "Location request timed out. Please try again.",
4903 "yatra",
4904 );
4905 break;
4906 }
4907
4908 if (showHttpsNotice) {
4909 alert(
4910 message +
4911 "\n\n" +
4912 __(
4913 "For local development, you can:\n1. Use a browser extension that allows geolocation on HTTP\n2. Set up a local HTTPS certificate\n3. Test on your live HTTPS site",
4914 "yatra",
4915 ),
4916 );
4917 } else {
4918 alert(message);
4919 }
4920 },
4921 );
4922 } else {
4923 alert(
4924 __(
4925 "Geolocation is not supported by your browser",
4926 "yatra",
4927 ),
4928 );
4929 }
4930 }}
4931 className="text-xs bg-green-100 text-green-700 px-3 py-1.5 rounded-lg hover:bg-green-200 transition-colors flex items-center gap-1"
4932 >
4933 <div className="w-3 h-3">
4934 <svg fill="currentColor" viewBox="0 0 20 20">
4935 <path
4936 fillRule="evenodd"
4937 d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z"
4938 clipRule="evenodd"
4939 />
4940 </svg>
4941 </div>
4942 {__("Use Current Location", "yatra")}
4943 </button>
4944 </div>
4945 </div>
4946
4947 <div className="grid grid-cols-2 gap-3">
4948 <div className="space-y-2">
4949 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
4950 <div className="w-2 h-2 bg-blue-400 rounded-full"></div>
4951 {__("Latitude", "yatra")}
4952 </label>
4953 <Input
4954 type="text"
4955 value={formData.starting_latitude}
4956 onChange={(e) =>
4957 handleFieldChange(
4958 "starting_latitude",
4959 e.target.value,
4960 )
4961 }
4962 className="w-full text-sm"
4963 />
4964 </div>
4965 <div className="space-y-2">
4966 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
4967 <div className="w-2 h-2 bg-blue-400 rounded-full"></div>
4968 {__("Longitude", "yatra")}
4969 </label>
4970 <Input
4971 type="text"
4972 value={formData.starting_longitude}
4973 onChange={(e) =>
4974 handleFieldChange(
4975 "starting_longitude",
4976 e.target.value,
4977 )
4978 }
4979 placeholder={__("e.g., 115.0920", "yatra")}
4980 className="w-full text-sm"
4981 />
4982 </div>
4983 </div>
4984 <p className="text-xs text-gray-500 dark:text-gray-400">
4985 {__(
4986 "Manual coordinate entry. These will be auto-filled when you select a location from the map above.",
4987 "yatra",
4988 )}
4989 </p>
4990 </div>
4991 </div>
4992
4993 {/* ENDING LOCATION */}
4994 <div className="space-y-6">
4995 {/* Location Header */}
4996 <div className="flex items-center gap-3 p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-xl border border-green-200 dark:border-green-800">
4997 <div className="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center shadow-lg">
4998 <CheckCircle2 className="w-5 h-5 text-white" />
4999 </div>
5000 <div className="flex-1">
5001 <h4 className="text-base font-semibold text-green-900 dark:text-green-100">
5002 {__("Ending Point", "yatra")}
5003 </h4>
5004 <p className="text-xs text-green-700 dark:text-green-300">
5005 {__("Where the journey concludes", "yatra")}
5006 </p>
5007 </div>
5008 <div className="flex items-center gap-2">
5009 {formData.ending_location && (
5010 <div
5011 className="w-2 h-2 bg-green-500 rounded-full animate-pulse"
5012 title="Location set"
5013 ></div>
5014 )}
5015 </div>
5016 </div>
5017
5018 {/* Integrated Location Input with Map */}
5019 <div className="space-y-4">
5020 <div className="flex items-center justify-between">
5021 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
5022 <div className="w-4 h-4 bg-green-100 dark:bg-green-900 rounded flex items-center justify-center">
5023 <span className="text-xs font-bold text-green-600 dark:text-green-400">
5024 1
5025 </span>
5026 </div>
5027 {__("Ending Location", "yatra")}
5028 </label>
5029 <div className="flex items-center gap-2">
5030 <span className="text-xs text-gray-500 dark:text-gray-400">
5031 {formData.ending_location
5032 ? "✓"
5033 : __("Required", "yatra")}
5034 </span>
5035 {formData.ending_latitude &&
5036 formData.ending_longitude && (
5037 <div className="flex items-center gap-1 text-xs text-green-600">
5038 <div className="w-2 h-2 bg-green-500 rounded-full"></div>
5039 {__("Coords Set", "yatra")}
5040 </div>
5041 )}
5042 </div>
5043 </div>
5044
5045 {/* LocationPicker - Primary Interface */}
5046 <div className="space-y-3">
5047 <LocationPicker
5048 value={{
5049 name: formData.ending_location,
5050 latitude: formData.ending_latitude,
5051 longitude: formData.ending_longitude,
5052 }}
5053 onChange={(locationData) => {
5054 handleFieldChange(
5055 "ending_location",
5056 locationData.name,
5057 );
5058 handleFieldChange(
5059 "ending_latitude",
5060 locationData.latitude,
5061 );
5062 handleFieldChange(
5063 "ending_longitude",
5064 locationData.longitude,
5065 );
5066 }}
5067 label=""
5068 placeholder={__(
5069 "Search for ending location...",
5070 "yatra",
5071 )}
5072 helpText=""
5073 required={false}
5074 defaultMapCenter={
5075 formData.ending_latitude &&
5076 formData.ending_longitude
5077 ? [
5078 parseFloat(formData.ending_latitude),
5079 parseFloat(formData.ending_longitude),
5080 ]
5081 : [-8.5069, 115.2625]
5082 }
5083 defaultZoom={13}
5084 mapHeight="300px"
5085 showMapButton={false}
5086 searchLimit={8}
5087 __={__}
5088 className=""
5089 mapClassName="rounded-lg"
5090 />
5091 </div>
5092 </div>
5093
5094 {/* GPS Coordinates - Manual Override */}
5095 <div className="space-y-4">
5096 <div className="flex items-center justify-between">
5097 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
5098 <div className="w-4 h-4 bg-green-100 dark:bg-green-900 rounded flex items-center justify-center">
5099 <span className="text-xs font-bold text-green-600 dark:text-green-400">
5100 2
5101 </span>
5102 </div>
5103 {__("GPS Coordinates", "yatra")}
5104 <span className="text-xs text-gray-500 dark:text-gray-400">
5105 ({__("Optional", "yatra")})
5106 </span>
5107 </label>
5108 </div>
5109
5110 <div className="grid grid-cols-2 gap-3">
5111 <div className="space-y-2">
5112 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
5113 <div className="w-2 h-2 bg-green-400 rounded-full"></div>
5114 {__("Latitude", "yatra")}
5115 </label>
5116 <Input
5117 type="text"
5118 value={formData.ending_latitude || ""}
5119 onChange={(e) =>
5120 handleFieldChange(
5121 "ending_latitude",
5122 e.target.value,
5123 )
5124 }
5125 placeholder={__("e.g., -8.5069", "yatra")}
5126 className="w-full text-sm"
5127 />
5128 </div>
5129 <div className="space-y-2">
5130 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
5131 <div className="w-2 h-2 bg-green-400 rounded-full"></div>
5132 {__("Longitude", "yatra")}
5133 </label>
5134 <Input
5135 type="text"
5136 value={formData.ending_longitude || ""}
5137 onChange={(e) =>
5138 handleFieldChange(
5139 "ending_longitude",
5140 e.target.value,
5141 )
5142 }
5143 placeholder={__("e.g., 115.2625", "yatra")}
5144 className="w-full text-sm"
5145 />
5146 </div>
5147 </div>
5148 <p className="text-xs text-gray-500 dark:text-gray-400">
5149 {__(
5150 "Optional: Manual coordinate entry. Auto-filled when you select a location from the map above.",
5151 "yatra",
5152 )}
5153 </p>
5154 </div>
5155 </div>
5156 </div>
5157 </div>
5158
5159 {/* Landmarks */}
5160 <div>
5161 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
5162 {__("Key Landmarks / Points of Interest", "yatra")}
5163 </label>
5164 <HelpText
5165 text={__(
5166 "Add notable landmarks or points of interest visited during this trip",
5167 "yatra",
5168 )}
5169 className="mb-2"
5170 />
5171 {formData.landmarks.length > 0 ? (
5172 <div className="space-y-2 mb-2">
5173 {formData.landmarks.map((landmark, index) => (
5174 <div
5175 key={index}
5176 className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700"
5177 >
5178 <MapPin className="w-4 h-4 text-gray-400 flex-shrink-0" />
5179 <span className="flex-1 text-sm text-gray-900 dark:text-white">
5180 {landmark}
5181 </span>
5182 <Button
5183 type="button"
5184 variant="ghost"
5185 size="sm"
5186 onClick={() => {
5187 const newLandmarks = [...formData.landmarks];
5188 newLandmarks.splice(index, 1);
5189 handleFieldChange("landmarks", newLandmarks);
5190 }}
5191 className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"
5192 >
5193 <X className="w-4 h-4" />
5194 </Button>
5195 </div>
5196 ))}
5197 </div>
5198 ) : (
5199 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center mb-2">
5200 <MapPin className="w-6 h-6 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
5201 <p className="text-xs text-gray-500 dark:text-gray-400">
5202 {__("No landmarks added yet", "yatra")}
5203 </p>
5204 </div>
5205 )}
5206 <Button
5207 type="button"
5208 variant="outline"
5209 onClick={() => {
5210 setShowLandmarkDialog(true);
5211 setLandmarkInput("");
5212 }}
5213 className="w-full"
5214 >
5215 <Plus className="w-4 h-4 mr-2" />
5216 {__("Add Landmark", "yatra")}
5217 </Button>
5218 </div>
5219 </div>
5220 </div>
5221 );
5222
5223 case "duration":
5224 return (
5225 <div className="space-y-6">
5226 <div className="flex flex-wrap items-center gap-2 mb-2">
5227 <Calendar className="w-5 h-5 text-gray-500" />
5228 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
5229 {__("Availability & Booking", "yatra")}
5230 </h2>
5231 <Badge
5232 variant="outline"
5233 className="text-xs bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-300 border-blue-200 dark:border-blue-800"
5234 >
5235 {__("Recommended", "yatra")}
5236 </Badge>
5237 </div>
5238 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
5239 {__("When is it available & who can book it?", "yatra")}
5240 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
5241 {__(
5242 "Set your trip's availability period, capacity limits, and booking requirements. This helps automate bookings and prevent overbooking.",
5243 "yatra",
5244 )}
5245 </span>
5246 </p>
5247
5248 <div className="space-y-6">
5249 {/* SECTION 1: Availability Period */}
5250 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5251 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5252 <Calendar className="w-4 h-4 text-blue-500" />
5253 {__("Availability Period", "yatra")}
5254 </h3>
5255 <div className="space-y-4">
5256 {/* Availability Dates */}
5257 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
5258 <div>
5259 <label
5260 htmlFor="available_from"
5261 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5262 >
5263 {__("Available From", "yatra")}
5264 </label>
5265 <DatePicker
5266 value={formData.available_from}
5267 onChange={(val) =>
5268 handleFieldChange("available_from", val)
5269 }
5270 placeholder={__("Select date", "yatra")}
5271 />
5272 <HelpText
5273 text={__(
5274 "Earliest date this trip becomes available for booking",
5275 "yatra",
5276 )}
5277 className="mt-2"
5278 />
5279 </div>
5280 <div>
5281 <label
5282 htmlFor="available_to"
5283 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5284 >
5285 {__("Available To", "yatra")}
5286 </label>
5287 <DatePicker
5288 value={formData.available_to}
5289 onChange={(val) =>
5290 handleFieldChange("available_to", val)
5291 }
5292 placeholder={__("Select date", "yatra")}
5293 />
5294 <HelpText
5295 text={__(
5296 "Latest date this trip is available for booking",
5297 "yatra",
5298 )}
5299 className="mt-2"
5300 />
5301 </div>
5302 </div>
5303
5304 {/* Booking Window */}
5305 <div>
5306 <label
5307 htmlFor="booking_window_days"
5308 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5309 >
5310 {__("Booking Window (Days in Advance)", "yatra")}
5311 </label>
5312 <Input
5313 id="booking_window_days"
5314 type="number"
5315 min="0"
5316 value={formData.booking_window_days}
5317 onChange={(e) =>
5318 handleFieldChange("booking_window_days", e.target.value)
5319 }
5320 placeholder={__("e.g., 30", "yatra")}
5321 />
5322 <HelpText
5323 text={__(
5324 "Minimum days in advance customers can book this trip",
5325 "yatra",
5326 )}
5327 className="mt-2"
5328 />
5329 </div>
5330
5331 {/* Seasonal Availability */}
5332 <div>
5333 <label
5334 htmlFor="seasonal_availability"
5335 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5336 >
5337 {__("Seasonal Availability Notes", "yatra")}
5338 </label>
5339 <Input
5340 id="seasonal_availability"
5341 type="text"
5342 value={formData.seasonal_availability}
5343 onChange={(e) =>
5344 handleFieldChange(
5345 "seasonal_availability",
5346 e.target.value,
5347 )
5348 }
5349 placeholder={__(
5350 "e.g., Available year-round except monsoon season",
5351 "yatra",
5352 )}
5353 />
5354 <HelpText
5355 text={__(
5356 "General notes about when this trip is typically available",
5357 "yatra",
5358 )}
5359 className="mt-2"
5360 />
5361 </div>
5362 </div>
5363 </div>
5364
5365 {/* SECTION 2: Capacity & Travelers */}
5366 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5367 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5368 <Users className="w-4 h-4 text-blue-500" />
5369 {__("Capacity & Travelers", "yatra")}
5370 </h3>
5371 <div className="space-y-4">
5372 {/* Group Size */}
5373 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
5374 <div>
5375 <label
5376 htmlFor="min_travelers"
5377 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5378 >
5379 {__("Minimum Travelers", "yatra")}
5380 </label>
5381 <Input
5382 id="min_travelers"
5383 type="number"
5384 min="1"
5385 value={formData.min_travelers}
5386 onChange={(e) =>
5387 handleFieldChange("min_travelers", e.target.value)
5388 }
5389 className={errors.min_travelers ? "border-red-500" : ""}
5390 />
5391 {errors.min_travelers && (
5392 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
5393 <AlertCircle className="w-4 h-4" />
5394 {errors.min_travelers}
5395 </p>
5396 )}
5397 </div>
5398 <div>
5399 <label
5400 htmlFor="max_travelers"
5401 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5402 >
5403 {__("Maximum Travelers", "yatra")}
5404 </label>
5405 <Input
5406 id="max_travelers"
5407 type="number"
5408 min="1"
5409 value={formData.max_travelers}
5410 onChange={(e) =>
5411 handleFieldChange("max_travelers", e.target.value)
5412 }
5413 className={errors.max_travelers ? "border-red-500" : ""}
5414 />
5415 {errors.max_travelers && (
5416 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
5417 <AlertCircle className="w-4 h-4" />
5418 {errors.max_travelers}
5419 </p>
5420 )}
5421 </div>
5422 </div>
5423
5424 {/* Fallback Settings */}
5425 <div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
5426 <div className="flex items-start gap-2 mb-3">
5427 <h4 className="text-sm font-medium text-gray-900 dark:text-white">
5428 {__("Fallback Settings", "yatra")}
5429 </h4>
5430 <span
5431 title={__(
5432 "These settings are used when the trip has no availability dates or recurring rules set. They provide default values for flexible booking.",
5433 "yatra",
5434 )}
5435 >
5436 <HelpCircle className="w-4 h-4 text-gray-400 cursor-help" />
5437 </span>
5438 </div>
5439
5440 {/* Info Banner */}
5441 <div className="mb-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
5442 <div className="flex items-start gap-2">
5443 <AlertCircle className="w-4 h-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
5444 <div className="text-xs text-amber-800 dark:text-amber-200">
5445 <p className="font-medium mb-1">
5446 {__("When are these settings used?", "yatra")}
5447 </p>
5448 <p className="text-amber-700 dark:text-amber-300">
5449 {__(
5450 "These settings apply ONLY when your trip has ZERO availability dates AND ZERO recurring rules. They provide defaults for flexible booking scenarios.",
5451 "yatra",
5452 )}
5453 </p>
5454 </div>
5455 </div>
5456 </div>
5457
5458 {/* Settings Content - Grouped by Trip Type */}
5459 <div className="space-y-4">
5460 {/* Day Tour Settings */}
5461 {formData.trip_type === "single_day" && (
5462 <div className="p-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
5463 <h5 className="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
5464 <Clock className="w-4 h-4" />
5465 {__("Day Tour Time Settings", "yatra")}
5466 </h5>
5467
5468 {/* Enable Multiple Time Slots Toggle */}
5469 <div className="mb-4 p-3 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700">
5470 <div className="flex items-start gap-3">
5471 <input
5472 type="checkbox"
5473 id="has_default_time_slots"
5474 checked={formData.has_default_time_slots}
5475 onChange={(e) =>
5476 handleFieldChange(
5477 "has_default_time_slots",
5478 e.target.checked,
5479 )
5480 }
5481 className="mt-0.5 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
5482 />
5483 <div className="flex-1">
5484 <label
5485 htmlFor="has_default_time_slots"
5486 className="text-sm font-medium text-gray-900 dark:text-white cursor-pointer"
5487 >
5488 {__("Enable Multiple Time Slots", "yatra")}
5489 </label>
5490 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
5491 {__(
5492 "Allow customers to select from multiple departure times (e.g., Morning, Afternoon, Evening tours)",
5493 "yatra",
5494 )}
5495 </p>
5496 </div>
5497 </div>
5498 </div>
5499
5500 {/* Time Slots Configuration (if enabled) */}
5501 {formData.has_default_time_slots && (
5502 <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
5503 <div className="flex items-center justify-between mb-3">
5504 <div>
5505 <label className="block text-sm font-medium text-gray-900 dark:text-white">
5506 {__("Time Slots Configuration", "yatra")}
5507 </label>
5508 <p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
5509 {__(
5510 "Add multiple departure times for customers to choose from.",
5511 "yatra",
5512 )}
5513 </p>
5514 </div>
5515 <Button
5516 type="button"
5517 variant="outline"
5518 size="sm"
5519 onClick={() => {
5520 const newSlot: TimeSlot = {
5521 id: `slot-${Date.now()}`,
5522 time: "09:00",
5523 label: "",
5524 };
5525 handleFieldChange("default_time_slots", [
5526 ...formData.default_time_slots,
5527 newSlot,
5528 ]);
5529 }}
5530 className="flex items-center gap-1"
5531 >
5532 <Plus className="w-4 h-4" />
5533 {__("Add Time Slot", "yatra")}
5534 </Button>
5535 </div>
5536
5537 {formData.default_time_slots.length === 0 ? (
5538 <div className="text-center py-8 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
5539 <Clock className="w-8 h-8 mx-auto text-gray-400 mb-2" />
5540 <p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
5541 {__("No time slots added yet", "yatra")}
5542 </p>
5543 <Button
5544 type="button"
5545 variant="outline"
5546 size="sm"
5547 onClick={() => {
5548 const newSlot: TimeSlot = {
5549 id: `slot-${Date.now()}`,
5550 time: "09:00",
5551 label: "Morning Tour",
5552 };
5553 handleFieldChange("default_time_slots", [
5554 newSlot,
5555 ]);
5556 }}
5557 >
5558 <Plus className="w-4 h-4 mr-1" />
5559 {__("Add First Time Slot", "yatra")}
5560 </Button>
5561 </div>
5562 ) : (
5563 <div className="space-y-3">
5564 {formData.default_time_slots.map(
5565 (slot, index) => (
5566 <div
5567 key={slot.id}
5568 className="flex items-start gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"
5569 >
5570 <div className="flex items-center justify-center w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex-shrink-0 mt-1">
5571 <span className="text-sm font-semibold text-blue-600 dark:text-blue-400">
5572 {index + 1}
5573 </span>
5574 </div>
5575 <div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
5576 <div>
5577 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
5578 {__("Time", "yatra")}
5579 </label>
5580 <Input
5581 type="time"
5582 value={slot.time}
5583 onChange={(e) => {
5584 const updated = [
5585 ...formData.default_time_slots,
5586 ];
5587 updated[index] = {
5588 ...slot,
5589 time: e.target.value,
5590 };
5591 handleFieldChange(
5592 "default_time_slots",
5593 updated,
5594 );
5595 }}
5596 className="w-full"
5597 />
5598 </div>
5599 <div>
5600 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
5601 {__("Label", "yatra")}
5602 </label>
5603 <Input
5604 type="text"
5605 value={slot.label}
5606 onChange={(e) => {
5607 const updated = [
5608 ...formData.default_time_slots,
5609 ];
5610 updated[index] = {
5611 ...slot,
5612 label: e.target.value,
5613 };
5614 handleFieldChange(
5615 "default_time_slots",
5616 updated,
5617 );
5618 }}
5619 placeholder={__(
5620 "e.g., Morning Tour, Afternoon Tour",
5621 "yatra",
5622 )}
5623 className="w-full"
5624 />
5625 </div>
5626 </div>
5627 <Button
5628 type="button"
5629 variant="ghost"
5630 size="sm"
5631 onClick={() => {
5632 const updated =
5633 formData.default_time_slots.filter(
5634 (_, i) => i !== index,
5635 );
5636 handleFieldChange(
5637 "default_time_slots",
5638 updated,
5639 );
5640 }}
5641 className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 flex-shrink-0 mt-1"
5642 title={__(
5643 "Remove time slot",
5644 "yatra",
5645 )}
5646 >
5647 <X className="w-4 h-4" />
5648 </Button>
5649 </div>
5650 ),
5651 )}
5652 </div>
5653 )}
5654 </div>
5655 )}
5656
5657 {/* Single Departure Time (if multiple slots disabled) */}
5658 {!formData.has_default_time_slots && (
5659 <div>
5660 <label
5661 htmlFor="departure_time_single"
5662 className="block text-sm font-medium text-gray-900 dark:text-white mb-2"
5663 >
5664 {__("Default Departure Time", "yatra")}
5665 </label>
5666 <Input
5667 id="departure_time_single"
5668 type="time"
5669 value={formData.departure_time}
5670 onChange={(e) =>
5671 handleFieldChange(
5672 "departure_time",
5673 e.target.value,
5674 )
5675 }
5676 className="max-w-xs"
5677 />
5678 <p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
5679 {__(
5680 "Single departure time for all bookings when multiple time slots are not enabled.",
5681 "yatra",
5682 )}
5683 </p>
5684 </div>
5685 )}
5686 </div>
5687 )}
5688
5689 {/* Multi-Day Trip Settings */}
5690 {formData.trip_type === "multi_day" && (
5691 <div className="p-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
5692 <h5 className="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
5693 <Calendar className="w-4 h-4" />
5694 {__("Multi-Day Trip Departure Settings", "yatra")}
5695 </h5>
5696
5697 <div>
5698 <label
5699 htmlFor="departure_time_multiday"
5700 className="block text-sm font-medium text-gray-900 dark:text-white mb-2"
5701 >
5702 {__("Default Departure Time", "yatra")}
5703 </label>
5704 <Input
5705 id="departure_time_multiday"
5706 type="time"
5707 value={formData.departure_time}
5708 onChange={(e) =>
5709 handleFieldChange(
5710 "departure_time",
5711 e.target.value,
5712 )
5713 }
5714 className="max-w-xs"
5715 />
5716 <p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
5717 {__(
5718 "Default departure time for trips without specific availability dates.",
5719 "yatra",
5720 )}
5721 </p>
5722 </div>
5723 </div>
5724 )}
5725
5726 {/* No Trip Type Selected */}
5727 {!formData.trip_type && (
5728 <div className="p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700 text-center">
5729 <p className="text-sm text-gray-500 dark:text-gray-400">
5730 {__(
5731 "Please select a trip type (Day Tour or Multi-Day) to configure fallback settings.",
5732 "yatra",
5733 )}
5734 </p>
5735 </div>
5736 )}
5737 </div>
5738 </div>
5739 </div>
5740 </div>
5741
5742 {/* SECTION 3: Booking Policies */}
5743 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5744 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5745 <FileText className="w-4 h-4 text-blue-500" />
5746 {__("Booking Policies", "yatra")}
5747 </h3>
5748 <div className="space-y-4">
5749 {/* Age Restrictions */}
5750 <div>
5751 <h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
5752 {__("Age Restrictions", "yatra")}
5753 </h4>
5754 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
5755 <div>
5756 <label
5757 htmlFor="age_min"
5758 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5759 >
5760 {__("Minimum Age", "yatra")}
5761 </label>
5762 <Input
5763 id="age_min"
5764 type="number"
5765 min="0"
5766 value={formData.age_min}
5767 onChange={(e) =>
5768 handleFieldChange("age_min", e.target.value)
5769 }
5770 placeholder={__("e.g., 18", "yatra")}
5771 className={errors.age_min ? "border-red-500" : ""}
5772 />
5773 {errors.age_min && (
5774 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
5775 <AlertCircle className="w-4 h-4" />
5776 {errors.age_min}
5777 </p>
5778 )}
5779 </div>
5780 <div>
5781 <label
5782 htmlFor="age_max"
5783 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5784 >
5785 {__("Maximum Age", "yatra")}
5786 </label>
5787 <Input
5788 id="age_max"
5789 type="number"
5790 min="0"
5791 value={formData.age_max}
5792 onChange={(e) =>
5793 handleFieldChange("age_max", e.target.value)
5794 }
5795 placeholder={__("e.g., 65", "yatra")}
5796 className={errors.age_max ? "border-red-500" : ""}
5797 />
5798 {errors.age_max && (
5799 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
5800 <AlertCircle className="w-4 h-4" />
5801 {errors.age_max}
5802 </p>
5803 )}
5804 </div>
5805 </div>
5806 </div>
5807
5808 {/* Requirements */}
5809 <div>
5810 <h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
5811 {__("Trip Requirements", "yatra")}
5812 </h4>
5813 <div className="space-y-4">
5814 <div>
5815 <label
5816 htmlFor="physical_requirements"
5817 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5818 >
5819 {__("Physical Requirements", "yatra")}
5820 </label>
5821 <textarea
5822 id="physical_requirements"
5823 value={formData.physical_requirements}
5824 onChange={(e) =>
5825 handleFieldChange(
5826 "physical_requirements",
5827 e.target.value,
5828 )
5829 }
5830 placeholder={__(
5831 "e.g., Moderate fitness level required. Some walking involved but no strenuous activities.",
5832 "yatra",
5833 )}
5834 rows={3}
5835 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
5836 />
5837 </div>
5838 <div>
5839 <label
5840 htmlFor="visa_requirements"
5841 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5842 >
5843 {__("Visa Requirements", "yatra")}
5844 </label>
5845 <textarea
5846 id="visa_requirements"
5847 value={formData.visa_requirements}
5848 onChange={(e) =>
5849 handleFieldChange(
5850 "visa_requirements",
5851 e.target.value,
5852 )
5853 }
5854 placeholder={__(
5855 "e.g., Tourist visa required. Can be obtained on arrival or in advance.",
5856 "yatra",
5857 )}
5858 rows={3}
5859 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
5860 />
5861 </div>
5862 <div>
5863 <label
5864 htmlFor="vaccination_requirements"
5865 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5866 >
5867 {__("Vaccination Requirements", "yatra")}
5868 </label>
5869 <textarea
5870 id="vaccination_requirements"
5871 value={formData.vaccination_requirements}
5872 onChange={(e) =>
5873 handleFieldChange(
5874 "vaccination_requirements",
5875 e.target.value,
5876 )
5877 }
5878 placeholder={__(
5879 "e.g., No mandatory vaccinations. Recommended: Hepatitis A, Typhoid, and routine vaccinations.",
5880 "yatra",
5881 )}
5882 rows={3}
5883 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
5884 />
5885 </div>
5886 </div>
5887 </div>
5888
5889 {/* Cancellation Policy */}
5890 <div>
5891 <h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
5892 {__("Cancellation Policy", "yatra")}
5893 </h4>
5894 <textarea
5895 id="cancellation_policy"
5896 value={formData.cancellation_policy}
5897 onChange={(e) =>
5898 handleFieldChange("cancellation_policy", e.target.value)
5899 }
5900 placeholder={__(
5901 "e.g., Free cancellation up to 30 days before departure. 50% refund for cancellations 15-30 days before. No refund for cancellations less than 15 days before.",
5902 "yatra",
5903 )}
5904 rows={4}
5905 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
5906 />
5907 </div>
5908 </div>
5909 </div>
5910
5911 {/* SECTION 3b: Disable Booking (Pro) */}
5912 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5913 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5914 <Calendar className="w-4 h-4 text-blue-500" />
5915 {__("Booking Availability", "yatra")}
5916 <ProBadge isProActive={isPro} />
5917 </h3>
5918 <ProFeature
5919 description={__(
5920 "disable the booking calendar and make trips enquiry-only",
5921 "yatra",
5922 )}
5923 moduleName={__("Enquiry Only Mode", "yatra")}
5924 pricingUrl="https://wpyatra.com/pricing"
5925 isProActive={isPro}
5926 isModuleEnabled={isPro}
5927 >
5928 <div className="flex items-start gap-3">
5929 <div className="flex items-center h-5 mt-0.5">
5930 <input
5931 id="disable_booking"
5932 type="checkbox"
5933 checked={formData.disable_booking}
5934 onChange={(e) =>
5935 handleFieldChange("disable_booking", e.target.checked)
5936 }
5937 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 cursor-pointer"
5938 />
5939 </div>
5940 <div>
5941 <label
5942 htmlFor="disable_booking"
5943 className="block text-sm font-medium text-gray-900 dark:text-white cursor-pointer"
5944 >
5945 {__("Disable Booking Calendar", "yatra")}
5946 </label>
5947 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
5948 {__(
5949 "When enabled, the booking form and date picker will be hidden. Visitors can only send an enquiry for this trip.",
5950 "yatra",
5951 )}
5952 </p>
5953 </div>
5954 </div>
5955 </ProFeature>
5956 </div>
5957
5958 {/* SECTION 4: Accommodation */}
5959 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5960 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5961 <Home className="w-4 h-4 text-blue-500" />
5962 {__("Accommodation", "yatra")}
5963 </h3>
5964 <div className="space-y-4">
5965 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
5966 <div>
5967 <label
5968 htmlFor="accommodation_type"
5969 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5970 >
5971 {__("Accommodation Type", "yatra")}
5972 </label>
5973 <Input
5974 id="accommodation_type"
5975 type="text"
5976 value={formData.accommodation_type}
5977 onChange={(e) =>
5978 handleFieldChange(
5979 "accommodation_type",
5980 e.target.value,
5981 )
5982 }
5983 placeholder={__(
5984 "e.g., Hotel, Resort, Teahouse, Camping",
5985 "yatra",
5986 )}
5987 />
5988 </div>
5989 <div>
5990 <label
5991 htmlFor="meal_plan"
5992 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5993 >
5994 {__("Meal Plan", "yatra")}
5995 </label>
5996 <Select
5997 id="meal_plan"
5998 value={formData.meal_plan}
5999 onChange={(e) =>
6000 handleFieldChange("meal_plan", e.target.value)
6001 }
6002 >
6003 <option value="">
6004 {__("Select meal plan", "yatra")}
6005 </option>
6006 <option value="breakfast">
6007 {__("Breakfast Only", "yatra")}
6008 </option>
6009 <option value="half_board">
6010 {__("Half Board (Breakfast + Dinner)", "yatra")}
6011 </option>
6012 <option value="full_board">
6013 {__("Full Board (All Meals)", "yatra")}
6014 </option>
6015 <option value="all_inclusive">
6016 {__("All Inclusive", "yatra")}
6017 </option>
6018 <option value="none">
6019 {__("No Meals Included", "yatra")}
6020 </option>
6021 </Select>
6022 </div>
6023 </div>
6024 <div>
6025 <label
6026 htmlFor="accommodation_details"
6027 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6028 >
6029 {__("Accommodation Details", "yatra")}
6030 </label>
6031 <textarea
6032 id="accommodation_details"
6033 value={formData.accommodation_details}
6034 onChange={(e) =>
6035 handleFieldChange(
6036 "accommodation_details",
6037 e.target.value,
6038 )
6039 }
6040 placeholder={__(
6041 "e.g., 4-star beachfront resort with private balconies, infinity pool, and spa facilities",
6042 "yatra",
6043 )}
6044 rows={3}
6045 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
6046 />
6047 </div>
6048 </div>
6049 </div>
6050
6051 {/* SECTION 5: Transportation */}
6052 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
6053 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
6054 <Car className="w-4 h-4 text-blue-500" />
6055 {__("Transportation", "yatra")}
6056 </h3>
6057 <div className="space-y-4">
6058 <div className="flex items-center gap-2">
6059 <input
6060 type="checkbox"
6061 id="transportation_included"
6062 checked={formData.transportation_included}
6063 onChange={(e) =>
6064 handleFieldChange(
6065 "transportation_included",
6066 e.target.checked,
6067 )
6068 }
6069 className="w-4 h-4 text-blue-600 bg-white border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
6070 />
6071 <label
6072 htmlFor="transportation_included"
6073 className="text-sm font-medium text-gray-900 dark:text-white"
6074 >
6075 {__("Transportation Included", "yatra")}
6076 </label>
6077 </div>
6078 {formData.transportation_included && (
6079 <>
6080 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
6081 <div>
6082 <label
6083 htmlFor="pickup_location"
6084 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6085 >
6086 {__("Pickup Location", "yatra")}
6087 </label>
6088 <Input
6089 id="pickup_location"
6090 type="text"
6091 value={formData.pickup_location}
6092 onChange={(e) =>
6093 handleFieldChange(
6094 "pickup_location",
6095 e.target.value,
6096 )
6097 }
6098 placeholder={__(
6099 "e.g., Airport, Hotel, City Center",
6100 "yatra",
6101 )}
6102 />
6103 </div>
6104 <div>
6105 <label
6106 htmlFor="dropoff_location"
6107 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6108 >
6109 {__("Dropoff Location", "yatra")}
6110 </label>
6111 <Input
6112 id="dropoff_location"
6113 type="text"
6114 value={formData.dropoff_location}
6115 onChange={(e) =>
6116 handleFieldChange(
6117 "dropoff_location",
6118 e.target.value,
6119 )
6120 }
6121 placeholder={__(
6122 "e.g., Airport, Hotel, City Center",
6123 "yatra",
6124 )}
6125 />
6126 </div>
6127 </div>
6128 <div>
6129 <label
6130 htmlFor="transportation_details"
6131 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6132 >
6133 {__("Transportation Details", "yatra")}
6134 </label>
6135 <textarea
6136 id="transportation_details"
6137 value={formData.transportation_details}
6138 onChange={(e) =>
6139 handleFieldChange(
6140 "transportation_details",
6141 e.target.value,
6142 )
6143 }
6144 placeholder={__(
6145 "e.g., Private air-conditioned vehicle with professional driver",
6146 "yatra",
6147 )}
6148 rows={3}
6149 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
6150 />
6151 </div>
6152 </>
6153 )}
6154 </div>
6155 </div>
6156 </div>
6157 </div>
6158 );
6159
6160 case "categorization":
6161 return (
6162 <div className="space-y-6">
6163 <div className="flex flex-wrap items-center gap-2 mb-2">
6164 <Tag className="w-5 h-5 text-gray-500" />
6165 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
6166 {__("Categories & Attributes", "yatra")}
6167 </h2>
6168 <Badge
6169 variant="outline"
6170 className="text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-300 border-amber-200 dark:border-amber-800"
6171 >
6172 {__("Recommended", "yatra")}
6173 </Badge>
6174 </div>
6175 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
6176 {__("How should travelers find this trip?", "yatra")}
6177 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
6178 {__(
6179 "Organize your trip with categories, activities, and custom attributes to help travelers discover it through search and filters.",
6180 "yatra",
6181 )}
6182 </span>
6183 </p>
6184
6185 <div className="space-y-6">
6186 {/* SECTION 1: Categories & Classification */}
6187 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
6188 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
6189 <Tag className="w-4 h-4 text-blue-500" />
6190 {__("Categories & Classification", "yatra")}
6191 </h3>
6192 <div className="space-y-4">
6193 {/* Trip Category */}
6194 <div>
6195 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6196 {__("Trip Categories", "yatra")}
6197 </label>
6198 {tripCategories && tripCategories.length > 0 ? (
6199 <MultiSelect
6200 value={formData.trip_category}
6201 onChange={(values) =>
6202 handleFieldChange(
6203 "trip_category",
6204 values.map((v) => Number(v)),
6205 )
6206 }
6207 options={(() => {
6208 // Flatten categories with hierarchy indication
6209 const flattenCategories = (
6210 cats: any[],
6211 prefix = "",
6212 ): { value: number; label: string }[] => {
6213 const result: { value: number; label: string }[] =
6214 [];
6215 cats.forEach((cat) => {
6216 result.push({
6217 value: cat.id,
6218 label: prefix + cat.name,
6219 });
6220 if (
6221 cat.subcategories &&
6222 cat.subcategories.length > 0
6223 ) {
6224 result.push(
6225 ...flattenCategories(
6226 cat.subcategories,
6227 prefix + "— ",
6228 ),
6229 );
6230 }
6231 });
6232 return result;
6233 };
6234 return flattenCategories(tripCategories);
6235 })()}
6236 placeholder={__("Select categories...", "yatra")}
6237 searchPlaceholder={__("Search categories...", "yatra")}
6238 />
6239 ) : (
6240 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
6241 <p className="text-sm text-gray-500 dark:text-gray-400">
6242 {__("No categories available. ", "yatra")}
6243 <a
6244 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories"
6245 target="_blank"
6246 rel="noopener noreferrer"
6247 className="text-blue-600 hover:text-blue-700 underline"
6248 >
6249 {__("Create categories here", "yatra")}
6250 </a>
6251 </p>
6252 </div>
6253 )}
6254 <HelpText
6255 text={__(
6256 "Select one or more categories for this trip",
6257 "yatra",
6258 )}
6259 className="mt-2"
6260 />
6261 </div>
6262
6263 {/* Difficulty Level */}
6264 <div>
6265 <label
6266 htmlFor="difficulty_level"
6267 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6268 >
6269 {__("Difficulty Level", "yatra")}
6270 </label>
6271 <Select
6272 id="difficulty_level"
6273 value={formData.difficulty_level}
6274 onChange={(e) =>
6275 handleFieldChange("difficulty_level", e.target.value)
6276 }
6277 disabled={isLoadingDifficultyLevels}
6278 >
6279 <option value="">
6280 {__("Select difficulty", "yatra")}
6281 </option>
6282 {isLoadingDifficultyLevels && (
6283 <option value="" disabled>
6284 {__("Loading difficulty levels...", "yatra")}
6285 </option>
6286 )}
6287 {!isLoadingDifficultyLevels &&
6288 difficultyLevels.length === 0 && (
6289 <option value="" disabled>
6290 {__(
6291 "No difficulty levels available - Click below to create",
6292 "yatra",
6293 )}
6294 </option>
6295 )}
6296 {!isLoadingDifficultyLevels &&
6297 difficultyLevels.map((level) => (
6298 <option
6299 key={`difficulty-${level.id}`}
6300 value={level.id?.toString() || ""}
6301 >
6302 {level.name}
6303 </option>
6304 ))}
6305 </Select>
6306 {!isLoadingDifficultyLevels &&
6307 difficultyLevels.length === 0 && (
6308 <div className="mt-3 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-md">
6309 <p className="text-sm text-amber-700 dark:text-amber-300">
6310 {__("No difficulty levels available. ", "yatra")}
6311 <a
6312 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=difficulty-levels"
6313 target="_blank"
6314 rel="noopener noreferrer"
6315 className="text-amber-800 hover:text-amber-900 underline font-medium"
6316 >
6317 {__("Create difficulty levels here", "yatra")}
6318 </a>
6319 </p>
6320 </div>
6321 )}
6322 <HelpText
6323 text={__(
6324 "Physical difficulty level required for this trip",
6325 "yatra",
6326 )}
6327 className="mt-2"
6328 />
6329 </div>
6330
6331 {/* Activity Types */}
6332 <div>
6333 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6334 {__("Activity Types", "yatra")}
6335 </label>
6336 {activitiesData && activitiesData.length > 0 ? (
6337 <MultiSelect
6338 value={formData.activity_types}
6339 onChange={(values) =>
6340 handleFieldChange(
6341 "activity_types",
6342 values.map((v) => Number(v)),
6343 )
6344 }
6345 options={activitiesData.map((activity: any) => ({
6346 value: activity.id,
6347 label: activity.name,
6348 }))}
6349 placeholder={__("Select activities...", "yatra")}
6350 searchPlaceholder={__("Search activities...", "yatra")}
6351 />
6352 ) : (
6353 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
6354 <p className="text-sm text-gray-500 dark:text-gray-400">
6355 {__("No activities available. ", "yatra")}
6356 <a
6357 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=activities"
6358 target="_blank"
6359 rel="noopener noreferrer"
6360 className="text-blue-600 hover:text-blue-700 underline"
6361 >
6362 {__("Create activities here", "yatra")}
6363 </a>
6364 </p>
6365 </div>
6366 )}
6367 <HelpText
6368 text={__(
6369 "Select all activities included in this trip",
6370 "yatra",
6371 )}
6372 className="mt-2"
6373 />
6374 </div>
6375
6376 {/* Featured Priority */}
6377 <div>
6378 <label
6379 htmlFor="featured_priority"
6380 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6381 >
6382 {__("Featured Priority", "yatra")}
6383 </label>
6384 <Select
6385 id="featured_priority"
6386 value={formData.featured_priority}
6387 onChange={(e) =>
6388 handleFieldChange(
6389 "featured_priority",
6390 e.target.value as TripFormData["featured_priority"],
6391 )
6392 }
6393 >
6394 <option value="none">{__("None", "yatra")}</option>
6395 <option value="featured">
6396 {__("Featured", "yatra")}
6397 </option>
6398 <option value="new">{__("New", "yatra")}</option>
6399 <option value="limited">
6400 {__("Limited Time", "yatra")}
6401 </option>
6402 </Select>
6403 <HelpText
6404 text={__(
6405 "Special designation for frontend display and promotion",
6406 "yatra",
6407 )}
6408 className="mt-2"
6409 />
6410 </div>
6411 </div>
6412 </div>
6413
6414 {/* SECTION 2: Custom Attributes */}
6415 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
6416 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
6417 <Settings className="w-4 h-4 text-blue-500" />
6418 {__("Custom Attributes", "yatra")}
6419 </h3>
6420 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
6421 {__(
6422 "Add custom attributes for advanced filtering and search capabilities.",
6423 "yatra",
6424 )}
6425 </p>
6426 <TripAttributesSection
6427 formData={formData}
6428 handleFieldChange={handleFieldChange}
6429 tripId={tripId || undefined}
6430 isEditMode={isEditMode}
6431 tripAttributesData={tripAttributesData}
6432 tripAttributesReady={tripAttributesReady}
6433 />
6434 </div>
6435 </div>
6436 </div>
6437 );
6438
6439 case "media":
6440 return (
6441 <div className="space-y-6">
6442 <div className="flex flex-wrap items-center gap-2 mb-2">
6443 <Image className="w-5 h-5 text-gray-500" />
6444 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
6445 {__("Media & Content", "yatra")}
6446 </h2>
6447 <Badge
6448 variant="outline"
6449 className="text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-300 border-amber-200 dark:border-amber-800"
6450 >
6451 {__("Recommended", "yatra")}
6452 </Badge>
6453 </div>
6454 <p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
6455 {__(
6456 "Add visual content, videos, stories, and testimonials to showcase your trip",
6457 "yatra",
6458 )}
6459 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
6460 {__(
6461 "Optional, but adding rich media greatly increases engagement and conversions.",
6462 "yatra",
6463 )}
6464 </span>
6465 </p>
6466
6467 <div className="space-y-6">
6468 {/* Photo Gallery */}
6469 <Card>
6470 <CardHeader>
6471 <CardTitle>{__("Photo Gallery", "yatra")}</CardTitle>
6472 <CardDescription>
6473 {__(
6474 "Upload images to showcase your trip. These will be displayed on the trip page.",
6475 "yatra",
6476 )}
6477 </CardDescription>
6478 </CardHeader>
6479 <CardContent>
6480 <div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4">
6481 {formData.gallery_images.map((image, index) => (
6482 <div key={index} className="relative group">
6483 <div className="aspect-video bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center overflow-hidden">
6484 <img
6485 src={image.thumbnail_url || image.url}
6486 alt={image.alt_text || `Gallery ${index + 1}`}
6487 className="w-full h-full object-cover"
6488 />
6489 </div>
6490 {/* Reorder buttons */}
6491 <div className="absolute top-2 left-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
6492 {index > 0 && (
6493 <button
6494 type="button"
6495 onClick={() =>
6496 handleGalleryReorder(index, index - 1)
6497 }
6498 className="bg-black/60 text-white rounded p-1 hover:bg-black/80"
6499 title={__("Move left", "yatra")}
6500 >
6501 <ChevronLeft className="w-4 h-4" />
6502 </button>
6503 )}
6504 {index < formData.gallery_images.length - 1 && (
6505 <button
6506 type="button"
6507 onClick={() =>
6508 handleGalleryReorder(index, index + 1)
6509 }
6510 className="bg-black/60 text-white rounded p-1 hover:bg-black/80"
6511 title={__("Move right", "yatra")}
6512 >
6513 <ChevronRight className="w-4 h-4" />
6514 </button>
6515 )}
6516 </div>
6517 {/* Order badge */}
6518 <span className="absolute bottom-2 left-2 bg-black/60 text-white text-xs px-1.5 py-0.5 rounded">
6519 {index + 1}
6520 </span>
6521 {/* Remove button */}
6522 <button
6523 type="button"
6524 onClick={() => handleGalleryRemove(index)}
6525 className="absolute top-2 right-2 bg-red-600 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
6526 >
6527 <X className="w-4 h-4" />
6528 </button>
6529 </div>
6530 ))}
6531 <button
6532 type="button"
6533 onClick={handleGalleryAdd}
6534 className={`aspect-video border-2 border-dashed rounded-lg flex flex-col items-center justify-center transition-colors ${
6535 errors.gallery_images
6536 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
6537 : "border-gray-300 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-400"
6538 }`}
6539 >
6540 <Upload
6541 className={`w-8 h-8 mb-2 ${errors.gallery_images ? "text-red-400" : "text-gray-400"}`}
6542 />
6543 <span
6544 className={`text-sm ${errors.gallery_images ? "text-red-600 dark:text-red-400" : "text-gray-600 dark:text-gray-400"}`}
6545 >
6546 {__("Add Image", "yatra")}
6547 </span>
6548 </button>
6549 </div>
6550 {errors.gallery_images && (
6551 <p className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
6552 <AlertCircle className="w-4 h-4" />
6553 {errors.gallery_images}
6554 </p>
6555 )}
6556 </CardContent>
6557 </Card>
6558 {/* Video & Virtual Tour */}
6559 <Card>
6560 <CardHeader>
6561 <CardTitle>{__("Video & Virtual Tour", "yatra")}</CardTitle>
6562 <CardDescription>
6563 {__(
6564 "Add video content to showcase your trip visually",
6565 "yatra",
6566 )}
6567 </CardDescription>
6568 </CardHeader>
6569 <CardContent className="space-y-4">
6570 <div>
6571 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6572 {__("Video URL", "yatra")} (YouTube/Vimeo)
6573 </label>
6574 <HelpText
6575 text={__(
6576 "Paste the full URL from YouTube or Vimeo. The video will be embedded on your trip page.",
6577 "yatra",
6578 )}
6579 className="mb-2"
6580 />
6581 <Input
6582 type="url"
6583 value={formData.video_url}
6584 onChange={(e) =>
6585 handleFieldChange("video_url", e.target.value)
6586 }
6587 placeholder="https://www.youtube.com/watch?v=..."
6588 className={`font-mono text-xs ${errors.video_url ? "border-red-500" : ""}`}
6589 />
6590 {errors.video_url && (
6591 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
6592 <AlertCircle className="w-4 h-4" />
6593 {errors.video_url}
6594 </p>
6595 )}
6596 </div>
6597 <div>
6598 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6599 {__("360° Virtual Tour URL", "yatra")}
6600 </label>
6601 <HelpText
6602 text={__(
6603 "Add a link to an interactive 360° virtual tour if available",
6604 "yatra",
6605 )}
6606 className="mb-2"
6607 />
6608 <Input
6609 type="url"
6610 value={formData.virtual_tour_url}
6611 onChange={(e) =>
6612 handleFieldChange("virtual_tour_url", e.target.value)
6613 }
6614 placeholder="https://..."
6615 className={`font-mono text-xs ${errors.virtual_tour_url ? "border-red-500" : ""}`}
6616 />
6617 {errors.virtual_tour_url && (
6618 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
6619 <AlertCircle className="w-4 h-4" />
6620 {errors.virtual_tour_url}
6621 </p>
6622 )}
6623 </div>
6624 </CardContent>
6625 </Card>
6626
6627 {/* Trip Story & What Makes Special */}
6628 <Card>
6629 <CardHeader>
6630 <CardTitle>
6631 {__("Trip Story & Special Features", "yatra")}
6632 </CardTitle>
6633 <CardDescription>
6634 {__(
6635 "Tell an engaging story and highlight what makes this trip unique",
6636 "yatra",
6637 )}
6638 </CardDescription>
6639 </CardHeader>
6640 <CardContent className="space-y-4">
6641 <div>
6642 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6643 {__("What Makes This Trip Special", "yatra")}
6644 </label>
6645 <HelpText
6646 text={__(
6647 "Highlight the unique selling points and what sets this trip apart from others",
6648 "yatra",
6649 )}
6650 className="mb-2"
6651 />
6652 <textarea
6653 value={formData.what_makes_special}
6654 onChange={(e) =>
6655 handleFieldChange("what_makes_special", e.target.value)
6656 }
6657 placeholder={__(
6658 "Describe what makes this trip unique and special...",
6659 "yatra",
6660 )}
6661 rows={5}
6662 className="flex w-full rounded-md border-2 border-gray-300 bg-white px-4 py-2.5 text-base font-normal text-gray-900 ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:ring-offset-gray-900 dark:placeholder:text-gray-500 dark:focus-visible:ring-blue-400 resize-none transition-colors"
6663 />
6664 </div>
6665 <div>
6666 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6667 {__("Trip Story / Narrative", "yatra")}
6668 </label>
6669 <HelpText
6670 text={__(
6671 "Tell an engaging story about this trip. Use storytelling to connect with potential travelers emotionally",
6672 "yatra",
6673 )}
6674 className="mb-2"
6675 />
6676 <textarea
6677 value={formData.trip_story}
6678 onChange={(e) =>
6679 handleFieldChange("trip_story", e.target.value)
6680 }
6681 placeholder={__(
6682 "Write an engaging narrative about this trip...",
6683 "yatra",
6684 )}
6685 rows={8}
6686 className="flex w-full rounded-md border-2 border-gray-300 bg-white px-4 py-2.5 text-base font-normal text-gray-900 ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:ring-offset-gray-900 dark:placeholder:text-gray-500 dark:focus-visible:ring-blue-400 resize-none transition-colors"
6687 />
6688 </div>
6689 </CardContent>
6690 </Card>
6691
6692 {/* Testimonials - Select from Reviews */}
6693 <TestimonialsSelector
6694 tripId={tripId}
6695 selectedReviewIds={formData.testimonial_review_ids}
6696 onChange={(reviewIds) =>
6697 handleFieldChange("testimonial_review_ids", reviewIds)
6698 }
6699 />
6700 </div>
6701 </div>
6702 );
6703
6704 case "downloads":
6705 if (!showDownloadsUI) {
6706 return (
6707 <div className="space-y-6">
6708 <Alert variant="info">
6709 {__("Downloads module is not enabled.", "yatra")}
6710 </Alert>
6711 </div>
6712 );
6713 }
6714
6715 return (
6716 <div className="space-y-6">
6717 <div className="flex flex-wrap items-center gap-2 mb-2">
6718 <Download className="w-5 h-5 text-gray-500" />
6719 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
6720 {__("Downloads", "yatra")}
6721 </h2>
6722 </div>
6723 <p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
6724 {__(
6725 "Attach files to this trip and control who can access them.",
6726 "yatra",
6727 )}
6728 </p>
6729
6730 <Card>
6731 <CardHeader>
6732 <CardTitle>{__("Downloads", "yatra")}</CardTitle>
6733 <CardDescription>
6734 {__(
6735 "Attach files to this trip and control who can access them.",
6736 "yatra",
6737 )}
6738 </CardDescription>
6739 </CardHeader>
6740 <CardContent className="space-y-4">
6741 {(formData.downloadable_items || []).length > 0 ? (
6742 <div className="space-y-3">
6743 {(formData.downloadable_items || []).map((item, index) => (
6744 <div
6745 key={index}
6746 className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg space-y-3"
6747 >
6748 <div className="flex items-start justify-between gap-2">
6749 <div className="flex-1">
6750 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6751 {__("Title", "yatra")}
6752 </label>
6753 <Input
6754 value={item.title}
6755 onChange={(e) =>
6756 handleDownloadableItemChange(
6757 index,
6758 "title",
6759 e.target.value,
6760 )
6761 }
6762 placeholder={__(
6763 "E.g. Packing list, itinerary PDF, waiver form...",
6764 "yatra",
6765 )}
6766 />
6767 </div>
6768
6769 <div className="flex gap-1 pt-7">
6770 {index > 0 && (
6771 <Button
6772 type="button"
6773 variant="ghost"
6774 size="sm"
6775 onClick={() =>
6776 handleDownloadableItemMove(index, index - 1)
6777 }
6778 title={__("Move up", "yatra")}
6779 >
6780 <ChevronUp className="w-4 h-4" />
6781 </Button>
6782 )}
6783 {index <
6784 (formData.downloadable_items || []).length -
6785 1 && (
6786 <Button
6787 type="button"
6788 variant="ghost"
6789 size="sm"
6790 onClick={() =>
6791 handleDownloadableItemMove(index, index + 1)
6792 }
6793 title={__("Move down", "yatra")}
6794 >
6795 <ChevronDown className="w-4 h-4" />
6796 </Button>
6797 )}
6798 <Button
6799 type="button"
6800 variant="ghost"
6801 size="sm"
6802 onClick={() =>
6803 handleDownloadableItemRemove(index)
6804 }
6805 className="text-red-600 hover:text-red-700"
6806 title={__("Remove", "yatra")}
6807 >
6808 <X className="w-4 h-4" />
6809 </Button>
6810 </div>
6811 </div>
6812
6813 <div>
6814 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6815 {__("Description", "yatra")} (
6816 {__("Optional", "yatra")})
6817 </label>
6818 <textarea
6819 value={item.description}
6820 onChange={(e) =>
6821 handleDownloadableItemChange(
6822 index,
6823 "description",
6824 e.target.value,
6825 )
6826 }
6827 rows={3}
6828 className="flex w-full rounded-md border-2 border-gray-300 bg-white px-4 py-2.5 text-base font-normal text-gray-900 ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:ring-offset-gray-900 dark:placeholder:text-gray-500 dark:focus-visible:ring-blue-400 resize-none transition-colors"
6829 />
6830 </div>
6831
6832 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
6833 <div>
6834 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6835 {__("Visibility", "yatra")}
6836 </label>
6837 <Select
6838 value={item.visibility}
6839 onChange={(e) =>
6840 handleDownloadableItemChange(
6841 index,
6842 "visibility",
6843 e.target.value,
6844 )
6845 }
6846 >
6847 <option value="public">
6848 {__("Public", "yatra")}
6849 </option>
6850 <option value="logged_in">
6851 {__("Private (My Account only)", "yatra")}
6852 </option>
6853 <option value="booked_only">
6854 {__(
6855 "Private (Booking confirmation only)",
6856 "yatra",
6857 )}
6858 </option>
6859 </Select>
6860 </div>
6861 <div>
6862 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6863 {__("File", "yatra")}
6864 </label>
6865 <div className="flex items-center gap-2">
6866 {item.attachment_url ? (
6867 <a
6868 href={item.attachment_url}
6869 target="_blank"
6870 rel="noreferrer"
6871 className="flex-shrink-0"
6872 title={__("View file", "yatra")}
6873 >
6874 {/\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(
6875 item.attachment_url,
6876 ) ? (
6877 <img
6878 src={item.attachment_url}
6879 alt={
6880 item.attachment_title ||
6881 __("Selected file", "yatra")
6882 }
6883 className="w-9 h-9 rounded border border-gray-200 dark:border-gray-700 object-cover bg-white dark:bg-gray-900"
6884 />
6885 ) : (
6886 <div className="w-9 h-9 rounded border border-gray-200 dark:border-gray-700 flex items-center justify-center bg-gray-50 dark:bg-gray-900">
6887 <FileText className="w-4 h-4 text-gray-500 dark:text-gray-400" />
6888 </div>
6889 )}
6890 </a>
6891 ) : (
6892 <div className="w-9 h-9 rounded border border-gray-200 dark:border-gray-700 flex items-center justify-center bg-gray-50 dark:bg-gray-900 flex-shrink-0">
6893 <FileText className="w-4 h-4 text-gray-400 dark:text-gray-600" />
6894 </div>
6895 )}
6896
6897 <div className="min-w-0 flex-1">
6898 <div className="flex items-center gap-2 min-w-0">
6899 <Button
6900 type="button"
6901 variant="outline"
6902 onClick={() =>
6903 handleDownloadableItemSelectFile(index)
6904 }
6905 className="flex items-center gap-2 flex-shrink-0"
6906 >
6907 <Upload className="w-4 h-4" />
6908 {item.attachment_id
6909 ? __("Change File", "yatra")
6910 : __("Select File", "yatra")}
6911 </Button>
6912 <span className="text-xs text-gray-600 dark:text-gray-400 truncate min-w-0">
6913 {item.attachment_title ||
6914 (item.attachment_id
6915 ? `#${item.attachment_id}`
6916 : __("No file selected", "yatra"))}
6917 </span>
6918 {item.attachment_url && (
6919 <a
6920 href={item.attachment_url}
6921 target="_blank"
6922 rel="noreferrer"
6923 className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1 flex-shrink-0"
6924 >
6925 <Eye className="w-3 h-3" />
6926 {__("View", "yatra")}
6927 </a>
6928 )}
6929 </div>
6930 </div>
6931 </div>
6932 </div>
6933 </div>
6934
6935 <div className="flex items-start gap-3">
6936 <div className="flex items-center gap-2 pt-0.5">
6937 <input
6938 type="checkbox"
6939 checked={item.enabled}
6940 onChange={(e) =>
6941 handleDownloadableItemChange(
6942 index,
6943 "enabled",
6944 e.target.checked,
6945 )
6946 }
6947 className="h-4 w-4"
6948 />
6949 <span className="text-sm text-gray-700 dark:text-gray-300">
6950 {__("Enabled", "yatra")}
6951 </span>
6952 </div>
6953 <div className="flex-1">
6954 <p className="text-xs text-gray-500 dark:text-gray-400 leading-relaxed">
6955 {__(
6956 "Controls whether this file appears and is available for download on the trip page. When disabled, the file is completely hidden from users.",
6957 "yatra",
6958 )}
6959 </p>
6960 </div>
6961 </div>
6962 </div>
6963 ))}
6964 </div>
6965 ) : (
6966 <div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
6967 <FileText className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
6968 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
6969 {__("No downloads added yet", "yatra")}
6970 </p>
6971 </div>
6972 )}
6973
6974 <Button
6975 type="button"
6976 variant="outline"
6977 onClick={handleDownloadableItemAdd}
6978 className="w-full"
6979 >
6980 <Plus className="w-4 h-4 mr-2" />
6981 {__("Add Download", "yatra")}
6982 </Button>
6983 </CardContent>
6984 </Card>
6985 </div>
6986 );
6987
6988 case "pricing":
6989 return (
6990 <div className="space-y-4">
6991 <div className="flex flex-wrap items-center gap-2 mb-2">
6992 <DollarSign className="w-5 h-5 text-gray-500" />
6993 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
6994 {__("Pricing & Payment", "yatra")}
6995 </h2>
6996 <Badge
6997 variant="outline"
6998 className="text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-300 border-amber-200 dark:border-amber-800"
6999 >
7000 {__("Recommended", "yatra")}
7001 </Badge>
7002 </div>
7003 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
7004 {__("How much does it cost?", "yatra")}
7005 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
7006 {__(
7007 "Set pricing for different traveler types and payment options. Optional for drafts, but adding pricing now makes publishing easier.",
7008 "yatra",
7009 )}
7010 </span>
7011 </p>
7012
7013 <div className="space-y-4">
7014 {/* Pricing Type Selection */}
7015 <div>
7016 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-3">
7017 {__("Pricing Type", "yatra")}
7018 </label>
7019 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
7020 <label
7021 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
7022 formData.pricing_type === "regular"
7023 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
7024 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
7025 }`}
7026 >
7027 <input
7028 type="radio"
7029 name="pricing_type"
7030 value="regular"
7031 checked={formData.pricing_type === "regular"}
7032 onChange={(e) =>
7033 handleFieldChange("pricing_type", e.target.value)
7034 }
7035 className="sr-only"
7036 />
7037 <div className="flex flex-1">
7038 <div className="flex flex-col">
7039 <span
7040 className={`block text-sm font-semibold ${
7041 formData.pricing_type === "regular"
7042 ? "text-blue-900 dark:text-blue-300"
7043 : "text-gray-900 dark:text-white"
7044 }`}
7045 >
7046 {__("Regular Pricing", "yatra")}
7047 </span>
7048 <span
7049 className={`mt-1 flex items-center text-sm ${
7050 formData.pricing_type === "regular"
7051 ? "text-blue-700 dark:text-blue-400"
7052 : "text-gray-500 dark:text-gray-400"
7053 }`}
7054 >
7055 {__("Set a single price for all travelers", "yatra")}
7056 </span>
7057 </div>
7058 </div>
7059 {formData.pricing_type === "regular" && (
7060 <div className="absolute top-4 right-4">
7061 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
7062 </div>
7063 )}
7064 </label>
7065
7066 <label
7067 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
7068 formData.pricing_type === "traveler_based"
7069 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
7070 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
7071 }`}
7072 >
7073 <input
7074 type="radio"
7075 name="pricing_type"
7076 value="traveler_based"
7077 checked={formData.pricing_type === "traveler_based"}
7078 onChange={(e) =>
7079 handleFieldChange("pricing_type", e.target.value)
7080 }
7081 className="sr-only"
7082 />
7083 <div className="flex flex-1">
7084 <div className="flex flex-col">
7085 <span
7086 className={`block text-sm font-semibold ${
7087 formData.pricing_type === "traveler_based"
7088 ? "text-blue-900 dark:text-blue-300"
7089 : "text-gray-900 dark:text-white"
7090 }`}
7091 >
7092 {__("Traveler-Based Pricing", "yatra")}
7093 </span>
7094 <span
7095 className={`mt-1 flex items-center text-sm ${
7096 formData.pricing_type === "traveler_based"
7097 ? "text-blue-700 dark:text-blue-400"
7098 : "text-gray-500 dark:text-gray-400"
7099 }`}
7100 >
7101 {__(
7102 "Set different prices for each traveler category",
7103 "yatra",
7104 )}
7105 </span>
7106 </div>
7107 </div>
7108 {formData.pricing_type === "traveler_based" && (
7109 <div className="absolute top-4 right-4">
7110 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
7111 </div>
7112 )}
7113 </label>
7114 </div>
7115 </div>
7116
7117 {/* Regular Pricing */}
7118 {formData.pricing_type === "regular" && (
7119 <div className="space-y-4">
7120 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
7121 <div>
7122 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
7123 {__("Original Price", "yatra")}
7124 </label>
7125 <div className="relative">
7126 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7127 {getCurrencySymbol(globalCurrency)}
7128 </span>
7129 <Input
7130 type="number"
7131 step="0.01"
7132 min="0"
7133 value={formData.original_price}
7134 onChange={(e) =>
7135 handleFieldChange("original_price", e.target.value)
7136 }
7137 placeholder="0.00"
7138 className={`pl-7 ${errors.original_price ? "border-red-500" : ""}`}
7139 />
7140 </div>
7141 {errors.original_price && (
7142 <p className="mt-1 text-xs text-red-600">
7143 {errors.original_price}
7144 </p>
7145 )}
7146 </div>
7147
7148 <div>
7149 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
7150 {__("Discounted Price", "yatra")} (
7151 {__("Optional", "yatra")})
7152 </label>
7153 <div className="relative">
7154 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7155 {getCurrencySymbol(globalCurrency)}
7156 </span>
7157 <Input
7158 type="number"
7159 step="0.01"
7160 min="0"
7161 value={formData.discounted_price}
7162 onChange={(e) =>
7163 handleFieldChange(
7164 "discounted_price",
7165 e.target.value,
7166 )
7167 }
7168 placeholder="0.00"
7169 className={`pl-7 ${errors.discounted_price ? "border-red-500" : ""}`}
7170 />
7171 </div>
7172 {errors.discounted_price && (
7173 <p className="mt-1 text-xs text-red-600">
7174 {errors.discounted_price}
7175 </p>
7176 )}
7177 </div>
7178 </div>
7179 </div>
7180 )}
7181
7182 {/* Traveler Category Pricing */}
7183 {formData.pricing_type === "traveler_based" && (
7184 <div>
7185 <div className="flex items-center justify-between mb-3">
7186 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400">
7187 {__("Traveler Category Pricing", "yatra")}
7188 </label>
7189 </div>
7190 <p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
7191 {__(
7192 "Add pricing for traveler categories. Categories are managed in Traveler Categories page.",
7193 "yatra",
7194 )}
7195 </p>
7196 {isLoadingCategories ? (
7197 <div className="flex items-center justify-center py-8">
7198 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
7199 </div>
7200 ) : activeCategories.length === 0 ? (
7201 <div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg text-center">
7202 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
7203 {__("No active traveler categories found.", "yatra")}
7204 </p>
7205 <Button
7206 type="button"
7207 variant="outline"
7208 onClick={() =>
7209 (window.location.href =
7210 "?subpage=traveler-categories&action=create")
7211 }
7212 className="flex items-center gap-2 mx-auto"
7213 >
7214 <Plus className="w-4 h-4" />
7215 {__("Create Category", "yatra")}
7216 </Button>
7217 </div>
7218 ) : (
7219 <div className="space-y-4">
7220 {/* Add Pricing Button */}
7221 <div className="relative">
7222 <Button
7223 type="button"
7224 variant="outline"
7225 onClick={() =>
7226 setShowCategorySelector(!showCategorySelector)
7227 }
7228 className="flex items-center gap-2"
7229 disabled={
7230 activeCategories.filter(
7231 (cat) =>
7232 !formData.price_types.some(
7233 (pt) =>
7234 Number(pt.category_id) === Number(cat.id),
7235 ),
7236 ).length === 0
7237 }
7238 >
7239 <Plus className="w-4 h-4" />
7240 {__("Add Pricing", "yatra")}
7241 </Button>
7242
7243 {/* Category Selection Dropdown */}
7244 {showCategorySelector && (
7245 <>
7246 <div
7247 className="fixed inset-0 z-10"
7248 onClick={() => setShowCategorySelector(false)}
7249 />
7250 <div className="absolute top-full left-0 mt-2 w-full max-w-md bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-20 max-h-96 overflow-y-auto">
7251 <div className="p-2">
7252 <div className="text-xs font-medium text-gray-700 dark:text-gray-300 px-3 py-2 mb-1">
7253 {__(
7254 "Select a category to add pricing",
7255 "yatra",
7256 )}
7257 </div>
7258 {activeCategories.filter(
7259 (cat) =>
7260 !formData.price_types.some(
7261 (pt) =>
7262 Number(pt.category_id) ===
7263 Number(cat.id),
7264 ),
7265 ).length === 0 ? (
7266 <div className="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center">
7267 {__(
7268 "All categories have pricing added",
7269 "yatra",
7270 )}
7271 </div>
7272 ) : (
7273 <div className="space-y-1">
7274 {activeCategories
7275 .filter(
7276 (cat) =>
7277 !formData.price_types.some(
7278 (pt) =>
7279 Number(pt.category_id) ===
7280 Number(cat.id),
7281 ),
7282 )
7283 .map((category) => {
7284 const ageRange =
7285 category.age_min !== undefined ||
7286 category.age_max !== undefined
7287 ? category.age_min !== undefined &&
7288 category.age_max !== undefined
7289 ? `${category.age_min}-${category.age_max} ${__("years", "yatra")}`
7290 : category.age_min !== undefined
7291 ? `${category.age_min}+ ${__("years", "yatra")}`
7292 : category.age_max !== undefined
7293 ? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}`
7294 : ""
7295 : null;
7296
7297 const pricingMode =
7298 category.pricing_mode || "per_person";
7299 const hasMin =
7300 category.min_pax !== null &&
7301 category.min_pax !== undefined;
7302 const hasMax =
7303 category.max_pax !== null &&
7304 category.max_pax !== undefined;
7305
7306 let pricingLabel = "";
7307 if (pricingMode === "per_group") {
7308 if (hasMin && hasMax) {
7309 pricingLabel = `${__("Per group", "yatra")} (${category.min_pax}-${category.max_pax})`;
7310 } else if (hasMin) {
7311 pricingLabel = `${__("Per group", "yatra")} (${__("From", "yatra")} ${category.min_pax})`;
7312 } else if (hasMax) {
7313 pricingLabel = `${__("Per group", "yatra")} (${__("Up to", "yatra")} ${category.max_pax})`;
7314 } else {
7315 pricingLabel = __(
7316 "Per group",
7317 "yatra",
7318 );
7319 }
7320 } else {
7321 pricingLabel = __(
7322 "Per person",
7323 "yatra",
7324 );
7325 }
7326
7327 return (
7328 <button
7329 key={category.id}
7330 type="button"
7331 onClick={() => {
7332 handlePriceTypeAdd(category.id);
7333 setShowCategorySelector(false);
7334 }}
7335 className="w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
7336 >
7337 <div className="font-medium text-sm text-gray-900 dark:text-white">
7338 {category.label}
7339 {ageRange && (
7340 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
7341 ({ageRange})
7342 </span>
7343 )}
7344 </div>
7345 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
7346 {category.description}
7347 {pricingLabel && (
7348 <span className="ml-1 text-[11px] font-medium text-gray-600 dark:text-gray-300">
7349 • {pricingLabel}
7350 </span>
7351 )}
7352 </div>
7353 </button>
7354 );
7355 })}
7356 </div>
7357 )}
7358 </div>
7359 </div>
7360 </>
7361 )}
7362 </div>
7363
7364 {/* Added Pricing List */}
7365 {formData.price_types.length > 0 && (
7366 <div className="space-y-3">
7367 {formData.price_types.map((priceType, index) => {
7368 // Use Number() to ensure consistent comparison (handles string/number mismatch)
7369 const category = activeCategories.find(
7370 (cat) =>
7371 Number(cat.id) ===
7372 Number(priceType.category_id),
7373 );
7374 if (!category) {
7375 return null;
7376 }
7377
7378 return (
7379 <div
7380 key={priceType.category_id}
7381 className="p-4 border border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/20 rounded-lg"
7382 >
7383 <div className="flex items-start justify-between mb-3">
7384 <div className="flex-1">
7385 <div className="flex items-center gap-2 mb-1">
7386 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
7387 {category.label}
7388 {(category.age_min !== undefined ||
7389 category.age_max !== undefined) && (
7390 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
7391 (
7392 {category.age_min !== undefined &&
7393 category.age_max !== undefined
7394 ? `${category.age_min}-${category.age_max} ${__("years", "yatra")}`
7395 : category.age_min !== undefined
7396 ? `${category.age_min}+ ${__("years", "yatra")}`
7397 : category.age_max !== undefined
7398 ? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}`
7399 : ""}
7400 )
7401 </span>
7402 )}
7403
7404 {/* Pricing mode & group size */}
7405 <span className="ml-2 inline-flex items-center gap-1 text-[11px] font-medium text-gray-600 dark:text-gray-300">
7406 {(() => {
7407 const pricingMode =
7408 category.pricing_mode ||
7409 "per_person";
7410 const hasMin =
7411 category.min_pax !== null &&
7412 category.min_pax !== undefined;
7413 const hasMax =
7414 category.max_pax !== null &&
7415 category.max_pax !== undefined;
7416
7417 if (pricingMode === "per_group") {
7418 if (hasMin && hasMax) {
7419 return `${__("Per group", "yatra")} (${category.min_pax}-${category.max_pax})`;
7420 }
7421 if (hasMin) {
7422 return `${__("Per group", "yatra")} (${__("From", "yatra")} ${category.min_pax})`;
7423 }
7424 if (hasMax) {
7425 return `${__("Per group", "yatra")} (${__("Up to", "yatra")} ${category.max_pax})`;
7426 }
7427 return __("Per group", "yatra");
7428 }
7429
7430 return __("Per person", "yatra");
7431 })()}
7432 </span>
7433 </h4>
7434 </div>
7435 <p className="text-xs text-gray-600 dark:text-gray-400">
7436 {category.description}
7437 </p>
7438 </div>
7439 <button
7440 type="button"
7441 onClick={() =>
7442 handlePriceTypeRemove(category.id)
7443 }
7444 className="p-1 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
7445 title={__("Remove Pricing", "yatra")}
7446 >
7447 <X className="w-4 h-4" />
7448 </button>
7449 </div>
7450 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
7451 <div>
7452 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
7453 {__("Original Price", "yatra")}
7454 </label>
7455 <div className="relative">
7456 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7457 {getCurrencySymbol(globalCurrency)}
7458 </span>
7459 <Input
7460 type="number"
7461 step="0.01"
7462 min="0"
7463 value={priceType.original_price}
7464 onChange={(e) =>
7465 handlePriceTypeChange(
7466 category.id,
7467 "original_price",
7468 e.target.value,
7469 )
7470 }
7471 placeholder="0.00"
7472 className={`pl-7 ${errors[`price_type_${index}_original`] ? "border-red-500" : ""}`}
7473 />
7474 </div>
7475 {errors[`price_type_${index}_original`] && (
7476 <p className="mt-1 text-xs text-red-600 dark:text-red-400">
7477 {errors[`price_type_${index}_original`]}
7478 </p>
7479 )}
7480 </div>
7481 <div>
7482 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
7483 {__("Discounted Price", "yatra")} (
7484 {__("Optional", "yatra")})
7485 </label>
7486 <div className="relative">
7487 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7488 {getCurrencySymbol(globalCurrency)}
7489 </span>
7490 <Input
7491 type="number"
7492 step="0.01"
7493 min="0"
7494 value={priceType.discounted_price}
7495 onChange={(e) =>
7496 handlePriceTypeChange(
7497 category.id,
7498 "discounted_price",
7499 e.target.value,
7500 )
7501 }
7502 placeholder="0.00"
7503 className={`pl-7 ${errors[`price_type_${index}_discounted`] ? "border-red-500" : ""}`}
7504 />
7505 </div>
7506 {errors[
7507 `price_type_${index}_discounted`
7508 ] && (
7509 <p className="mt-1 text-xs text-red-600 dark:text-red-400">
7510 {
7511 errors[
7512 `price_type_${index}_discounted`
7513 ]
7514 }
7515 </p>
7516 )}
7517 </div>
7518 </div>
7519
7520 <div className="mt-3 flex items-center justify-between">
7521 <label className="inline-flex items-center gap-2 text-xs font-medium text-gray-700 dark:text-gray-300">
7522 <input
7523 type="checkbox"
7524 checked={Boolean(priceType.is_default)}
7525 onChange={(e) =>
7526 handlePriceTypeDefaultChange(
7527 category.id,
7528 e.target.checked,
7529 )
7530 }
7531 className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600"
7532 />
7533 {__(
7534 "Default price (used on page load)",
7535 "yatra",
7536 )}
7537 </label>
7538 <span className="text-[11px] text-gray-500 dark:text-gray-400">
7539 {__("Traveler-based pricing only", "yatra")}
7540 </span>
7541 </div>
7542 </div>
7543 );
7544 })}
7545 </div>
7546 )}
7547
7548 {/* Empty State */}
7549 {formData.price_types.length === 0 && (
7550 <div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
7551 <Tag className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
7552 <p className="text-sm text-gray-600 dark:text-gray-400">
7553 {__(
7554 "No pricing added yet. Select a category above to add pricing.",
7555 "yatra",
7556 )}
7557 </p>
7558 </div>
7559 )}
7560 </div>
7561 )}
7562 {errors.price_types && (
7563 <p className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
7564 <AlertCircle className="w-4 h-4" />
7565 {errors.price_types}
7566 </p>
7567 )}
7568 </div>
7569 )}
7570
7571 {/* Deposit */}
7572 <div className="border-t border-gray-200 dark:border-gray-700 pt-4">
7573 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
7574 {__("Deposit & Payment Terms", "yatra")}
7575 </h3>
7576 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
7577 <div>
7578 <label
7579 htmlFor="deposit_amount"
7580 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7581 >
7582 {__("Deposit Amount", "yatra")}
7583 </label>
7584 <div className="relative">
7585 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">
7586 {getCurrencySymbol(globalCurrency)}
7587 </span>
7588 <Input
7589 id="deposit_amount"
7590 type="number"
7591 step="0.01"
7592 min="0"
7593 value={formData.deposit_amount}
7594 onChange={(e) =>
7595 handleFieldChange("deposit_amount", e.target.value)
7596 }
7597 placeholder="0.00"
7598 className="pl-7"
7599 />
7600 </div>
7601 </div>
7602 <div>
7603 <label
7604 htmlFor="deposit_percentage"
7605 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7606 >
7607 {__("Deposit Percentage", "yatra")}
7608 </label>
7609 <div className="relative">
7610 <Input
7611 id="deposit_percentage"
7612 type="number"
7613 step="0.01"
7614 min="0"
7615 max="100"
7616 value={formData.deposit_percentage}
7617 onChange={(e) =>
7618 handleFieldChange(
7619 "deposit_percentage",
7620 e.target.value,
7621 )
7622 }
7623 placeholder="0"
7624 className="pr-7"
7625 />
7626 <span className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500">
7627 %
7628 </span>
7629 </div>
7630 </div>
7631 </div>
7632 <div className="mt-4">
7633 <label
7634 htmlFor="payment_terms"
7635 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7636 >
7637 {__("Payment Terms", "yatra")}
7638 </label>
7639 <textarea
7640 id="payment_terms"
7641 value={formData.payment_terms}
7642 onChange={(e) =>
7643 handleFieldChange("payment_terms", e.target.value)
7644 }
7645 placeholder={__(
7646 "e.g., 50% deposit required at booking, balance due 30 days before departure",
7647 "yatra",
7648 )}
7649 rows={2}
7650 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
7651 />
7652 </div>
7653 </div>
7654
7655 {/* Group Pricing */}
7656 {/* Removed */}
7657 </div>
7658 </div>
7659 );
7660
7661 case "attributes":
7662 // Redirect to categorization section (attributes now merged there)
7663 setCurrentSection("categorization");
7664 return null;
7665
7666 case "itinerary":
7667 return (
7668 <div className="space-y-6">
7669 {/* Header */}
7670 <div className="flex flex-wrap items-center gap-2 mb-2">
7671 <BookOpen className="w-5 h-5 text-gray-500" />
7672 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
7673 {__("Trip Details", "yatra")}
7674 </h2>
7675 <Badge
7676 variant="outline"
7677 className="text-xs bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-300 border-blue-200 dark:border-blue-800"
7678 >
7679 {__("Recommended", "yatra")}
7680 </Badge>
7681 </div>
7682 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
7683 {__("What will travelers experience?", "yatra")}
7684 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
7685 {__(
7686 "Build your day-by-day itinerary and list what's included/excluded to set clear expectations.",
7687 "yatra",
7688 )}
7689 </span>
7690 </p>
7691
7692 {/* Sub-tabs */}
7693 <div className="border-b border-gray-200 dark:border-gray-700">
7694 <nav className="flex gap-6" aria-label="Trip Details Tabs">
7695 <button
7696 onClick={() => setTripDetailsTab("itinerary")}
7697 className={`pb-3 px-1 border-b-2 font-medium text-sm transition-colors ${
7698 tripDetailsTab === "itinerary"
7699 ? "border-blue-500 text-blue-600 dark:text-blue-400"
7700 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
7701 }`}
7702 >
7703 <div className="flex items-center gap-2">
7704 <Calendar className="w-4 h-4" />
7705 {__("Itinerary", "yatra")}
7706 </div>
7707 </button>
7708 <button
7709 onClick={() => setTripDetailsTab("included")}
7710 className={`pb-3 px-1 border-b-2 font-medium text-sm transition-colors ${
7711 tripDetailsTab === "included"
7712 ? "border-blue-500 text-blue-600 dark:text-blue-400"
7713 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
7714 }`}
7715 >
7716 <div className="flex items-center gap-2">
7717 <CheckSquare className="w-4 h-4" />
7718 {__("Included/Excluded", "yatra")}
7719 </div>
7720 </button>
7721 </nav>
7722 </div>
7723
7724 {/* Tab Content */}
7725 <div className="mt-6">
7726 {tripDetailsTab === "itinerary" && (
7727 <ItinerarySection
7728 formData={formData as any}
7729 isEditMode={isEditMode}
7730 tripId={tripId}
7731 />
7732 )}
7733 {tripDetailsTab === "included" && (
7734 <IncludedSection
7735 formData={formData}
7736 errors={errors}
7737 handleFieldChange={handleFieldChange as any}
7738 />
7739 )}
7740 </div>
7741 </div>
7742 );
7743
7744 case "included":
7745 // Redirect to itinerary with included tab selected
7746 setCurrentSection("itinerary");
7747 setTripDetailsTab("included");
7748 return null;
7749
7750 case "faqs":
7751 // Redirect to seo section (FAQs now merged there)
7752 setCurrentSection("seo");
7753 return null;
7754
7755 // Removed: case 'frontend-tabs' - now part of 'advanced' case
7756
7757 case "seo":
7758 return (
7759 <div className="space-y-6">
7760 <div className="flex flex-wrap items-center gap-2 mb-2">
7761 <Search className="w-5 h-5 text-gray-500" />
7762 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
7763 {__("SEO & Marketing", "yatra")}
7764 </h2>
7765 <Badge
7766 variant="outline"
7767 className="text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-300 border-amber-200 dark:border-amber-800"
7768 >
7769 {__("Recommended", "yatra")}
7770 </Badge>
7771 </div>
7772 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
7773 {__("How will people find & trust this trip?", "yatra")}
7774 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
7775 {__(
7776 "Optimize for search engines and answer common questions to build trust and improve discoverability.",
7777 "yatra",
7778 )}
7779 </span>
7780 </p>
7781
7782 <div className="space-y-6">
7783 {/* SECTION 1: SEO Settings */}
7784 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
7785 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
7786 <Search className="w-4 h-4 text-blue-500" />
7787 {__("Search Engine Optimization", "yatra")}
7788 </h3>
7789 <div className="space-y-4">
7790 <div>
7791 <label
7792 htmlFor="meta_title"
7793 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7794 >
7795 {__("Meta Title", "yatra")}
7796 </label>
7797 <Input
7798 id="meta_title"
7799 type="text"
7800 value={formData.meta_title}
7801 onChange={(e) =>
7802 handleFieldChange("meta_title", e.target.value)
7803 }
7804 placeholder={formData.title || __("Trip Title", "yatra")}
7805 maxLength={60}
7806 />
7807 <HelpText
7808 text={__(
7809 "Best for search engines: 50-60 characters. Include main keywords.",
7810 "yatra",
7811 )}
7812 className="mt-1"
7813 />
7814 </div>
7815 <div>
7816 <label
7817 htmlFor="meta_description"
7818 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7819 >
7820 {__("Meta Description", "yatra")}
7821 </label>
7822 <textarea
7823 id="meta_description"
7824 value={formData.meta_description}
7825 onChange={(e) =>
7826 handleFieldChange("meta_description", e.target.value)
7827 }
7828 rows={3}
7829 maxLength={160}
7830 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
7831 placeholder={__(
7832 "Compelling description that includes key travel terms...",
7833 "yatra",
7834 )}
7835 />
7836 <HelpText
7837 text={__(
7838 "160 characters max. Include location, duration, and key features.",
7839 "yatra",
7840 )}
7841 className="mt-1"
7842 />
7843 </div>
7844 <div>
7845 <label
7846 htmlFor="meta_keywords"
7847 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7848 >
7849 {__("Meta Keywords", "yatra")}
7850 </label>
7851 <Input
7852 id="meta_keywords"
7853 type="text"
7854 value={formData.meta_keywords}
7855 onChange={(e) =>
7856 handleFieldChange("meta_keywords", e.target.value)
7857 }
7858 placeholder={__(
7859 "adventure, travel, tour, guide, experience",
7860 "yatra",
7861 )}
7862 />
7863 <HelpText
7864 text={__(
7865 "Comma-separated keywords. Include location, activities, and travel terms.",
7866 "yatra",
7867 )}
7868 className="mt-1"
7869 />
7870 </div>
7871 </div>
7872
7873 {/* SEO Preview */}
7874 <Card>
7875 <CardHeader>
7876 <CardTitle>{__("SEO Preview", "yatra")}</CardTitle>
7877 <CardDescription>
7878 {__(
7879 "Preview how your trip appears in search results",
7880 "yatra",
7881 )}
7882 </CardDescription>
7883 </CardHeader>
7884 <CardContent className="space-y-4">
7885 <div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
7886 <div className="text-xs text-gray-500 dark:text-gray-400 mb-2">
7887 {__("Google Search Result Preview", "yatra")}
7888 </div>
7889 <div className="space-y-2">
7890 <div className="text-blue-800 dark:text-blue-400 text-sm font-medium">
7891 {sanitizeTextForSEO(
7892 formData.meta_title ||
7893 formData.title ||
7894 "Trip Title",
7895 60,
7896 )}
7897 </div>
7898 <div className="text-green-700 dark:text-green-400 text-sm">
7899 {formData.meta_description
7900 ? sanitizeTextForSEO(formData.meta_description, 150)
7901 : formData.description
7902 ? sanitizeTextForSEO(formData.description, 150)
7903 : "Trip description..."}
7904 </div>
7905 <div className="text-gray-600 dark:text-gray-400 text-xs">
7906 {window.location.hostname}/trip/
7907 {formData.slug || "trip-slug"}
7908 </div>
7909 </div>
7910 </div>
7911 </CardContent>
7912 </Card>
7913 </div>
7914
7915 {/* SECTION 2: FAQs */}
7916 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
7917 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
7918 <HelpCircle className="w-4 h-4 text-blue-500" />
7919 {__("Frequently Asked Questions", "yatra")}
7920 </h3>
7921 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
7922 {__(
7923 "Answer common questions to build trust and reduce support inquiries. FAQs also help with SEO.",
7924 "yatra",
7925 )}
7926 </p>
7927 <div className="space-y-4">
7928 {formData.faqs.map((faq, index) => (
7929 <div
7930 key={index}
7931 className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-900"
7932 >
7933 <div className="flex items-start justify-between mb-3">
7934 <h4 className="text-sm font-medium text-gray-900 dark:text-white">
7935 {__("FAQ", "yatra")} {index + 1}
7936 </h4>
7937 <button
7938 type="button"
7939 onClick={() => handleFAQRemove(index)}
7940 className="text-red-600 hover:text-red-700"
7941 >
7942 <X className="w-4 h-4" />
7943 </button>
7944 </div>
7945 <div className="space-y-3">
7946 <div>
7947 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
7948 {__("Question", "yatra")}
7949 </label>
7950 <Input
7951 type="text"
7952 value={faq.question}
7953 onChange={(e) =>
7954 handleFAQChange(index, "question", e.target.value)
7955 }
7956 placeholder={__("Enter question...", "yatra")}
7957 />
7958 </div>
7959 <div>
7960 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
7961 {__("Answer", "yatra")}
7962 </label>
7963 <textarea
7964 value={faq.answer}
7965 onChange={(e) =>
7966 handleFAQChange(index, "answer", e.target.value)
7967 }
7968 placeholder={__("Enter answer...", "yatra")}
7969 rows={3}
7970 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
7971 />
7972 </div>
7973 </div>
7974 </div>
7975 ))}
7976 <Button
7977 type="button"
7978 variant="outline"
7979 onClick={handleFAQAdd}
7980 className="w-full"
7981 >
7982 <Plus className="w-4 h-4 mr-2" />
7983 {__("Add FAQ", "yatra")}
7984 </Button>
7985 </div>
7986 </div>
7987 </div>
7988 </div>
7989 );
7990
7991 case "advanced":
7992 return (
7993 <div className="space-y-6">
7994 <div className="flex flex-wrap items-center gap-2 mb-2">
7995 <Settings className="w-5 h-5 text-gray-500" />
7996 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
7997 {__("Lifecycle Management", "yatra")}
7998 </h2>
7999 <Badge
8000 variant="outline"
8001 className="text-xs bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-300 border-purple-200 dark:border-purple-800"
8002 >
8003 {__("Power Users", "yatra")}
8004 </Badge>
8005 </div>
8006 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
8007 {__("When should this trip be published or archived?", "yatra")}
8008 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
8009 {__(
8010 "Schedule automatic publishing and unpublishing for seasonal trips. Optional but useful for managing trip lifecycles.",
8011 "yatra",
8012 )}
8013 </span>
8014 </p>
8015
8016 <Card>
8017 <CardHeader>
8018 <CardTitle>{__("Version Control", "yatra")}</CardTitle>
8019 <CardDescription>
8020 {__("Track changes and version history", "yatra")}
8021 </CardDescription>
8022 </CardHeader>
8023 <CardContent className="space-y-4">
8024 <div>
8025 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8026 {__("Version", "yatra")}
8027 </label>
8028 <Input
8029 type="number"
8030 value={formData.version}
8031 onChange={(e) =>
8032 handleFieldChange(
8033 "version",
8034 parseInt(e.target.value) || 1,
8035 )
8036 }
8037 min="1"
8038 readOnly
8039 className="bg-gray-50 dark:bg-gray-800"
8040 />
8041 <HelpText
8042 text={__(
8043 "Version number is automatically incremented when changes are saved",
8044 "yatra",
8045 )}
8046 className="mt-2"
8047 />
8048 </div>
8049 </CardContent>
8050 </Card>
8051
8052 <Card>
8053 <CardHeader>
8054 <CardTitle>{__("Scheduled Publishing", "yatra")}</CardTitle>
8055 <CardDescription>
8056 {__(
8057 "Automatically publish or unpublish your trip on specific dates",
8058 "yatra",
8059 )}
8060 </CardDescription>
8061 </CardHeader>
8062 <CardContent className="space-y-4">
8063 <div>
8064 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8065 {__("Schedule Publish Date", "yatra")}
8066 </label>
8067 <DatePicker
8068 value={formData.scheduled_publish_date}
8069 onChange={(val) =>
8070 handleFieldChange("scheduled_publish_date", val)
8071 }
8072 placeholder={__("Select date", "yatra")}
8073 />
8074 <HelpText
8075 text={__(
8076 "Trip will be automatically published on this date and time",
8077 "yatra",
8078 )}
8079 className="mt-2"
8080 />
8081 </div>
8082 <div>
8083 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8084 {__("Schedule Unpublish Date", "yatra")}
8085 </label>
8086 <DatePicker
8087 value={formData.scheduled_unpublish_date}
8088 onChange={(val) =>
8089 handleFieldChange("scheduled_unpublish_date", val)
8090 }
8091 placeholder={__("Select date", "yatra")}
8092 />
8093 <HelpText
8094 text={__(
8095 "Trip will be automatically unpublished (archived) on this date and time",
8096 "yatra",
8097 )}
8098 className="mt-2"
8099 />
8100 </div>
8101 </CardContent>
8102 </Card>
8103
8104 <Card>
8105 <CardHeader>
8106 <CardTitle>
8107 {__("Seasonal Auto-Enable/Disable", "yatra")}
8108 </CardTitle>
8109 <CardDescription>
8110 {__(
8111 "Automatically enable or disable trip availability based on seasonal dates",
8112 "yatra",
8113 )}
8114 </CardDescription>
8115 </CardHeader>
8116 <CardContent className="space-y-4">
8117 <div className="flex items-center gap-2">
8118 <input
8119 type="checkbox"
8120 id="seasonal_auto_enable"
8121 checked={formData.seasonal_auto_enable}
8122 onChange={(e) =>
8123 handleFieldChange(
8124 "seasonal_auto_enable",
8125 e.target.checked,
8126 )
8127 }
8128 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
8129 />
8130 <label
8131 htmlFor="seasonal_auto_enable"
8132 className="text-sm font-medium text-gray-700 dark:text-gray-300"
8133 >
8134 {__("Enable seasonal auto-management", "yatra")}
8135 </label>
8136 </div>
8137 {formData.seasonal_auto_enable && (
8138 <>
8139 <div>
8140 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8141 {__("Auto-Enable Date", "yatra")}
8142 </label>
8143 <DatePicker
8144 value={formData.seasonal_enable_date}
8145 onChange={(val) =>
8146 handleFieldChange("seasonal_enable_date", val)
8147 }
8148 placeholder={__("Select date", "yatra")}
8149 />
8150 <HelpText
8151 text={__(
8152 "Trip will become available for booking on this date",
8153 "yatra",
8154 )}
8155 className="mt-2"
8156 />
8157 </div>
8158 <div>
8159 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8160 {__("Auto-Disable Date", "yatra")}
8161 </label>
8162 <DatePicker
8163 value={formData.seasonal_disable_date}
8164 onChange={(val) =>
8165 handleFieldChange("seasonal_disable_date", val)
8166 }
8167 placeholder={__("Select date", "yatra")}
8168 />
8169 <HelpText
8170 text={__(
8171 "Trip will become unavailable for booking on this date",
8172 "yatra",
8173 )}
8174 className="mt-2"
8175 />
8176 </div>
8177 </>
8178 )}
8179 </CardContent>
8180 </Card>
8181
8182 {/* Frontend Tabs Management */}
8183 <Card>
8184 <CardHeader>
8185 <CardTitle>{__("Frontend Tabs Management", "yatra")}</CardTitle>
8186 <CardDescription>
8187 {__(
8188 "Manage which tabs appear on the trip single page and in what order. Enable or disable tabs, customize labels, and reorder them.",
8189 "yatra",
8190 )}
8191 </CardDescription>
8192 </CardHeader>
8193 <CardContent>
8194 <div className="space-y-3">
8195 {[...formData.frontend_tabs]
8196 .sort((a, b) => a.order - b.order)
8197 .map((tab, index) => (
8198 <div
8199 key={tab.id}
8200 draggable
8201 onDragStart={(e) => handleDragStart(e, tab.id)}
8202 onDragOver={handleDragOver}
8203 onDragEnter={() => handleDragEnter(tab.id)}
8204 onDragLeave={handleDragLeave}
8205 onDrop={(e) => handleDrop(e, tab.id)}
8206 onDragEnd={handleDragEnd}
8207 className={`p-4 rounded-lg border cursor-move relative ${
8208 tab.enabled
8209 ? "bg-gray-50 dark:bg-gray-850 border-gray-200 dark:border-gray-700 shadow-sm"
8210 : "bg-gray-100 dark:bg-gray-900 border-gray-200 dark:border-gray-700 opacity-60 shadow-sm"
8211 } ${
8212 draggedTab === tab.id
8213 ? "opacity-50 scale-95 shadow-lg"
8214 : ""
8215 } ${
8216 dragOverTab === tab.id && draggedTab !== tab.id
8217 ? "border-blue-400 bg-blue-50 dark:bg-blue-900/20"
8218 : ""
8219 }`}
8220 >
8221 {tab.content_type === "custom" && (
8222 <Button
8223 type="button"
8224 variant="ghost"
8225 size="sm"
8226 onClick={() => handleTabRemove(tab.id)}
8227 className="absolute top-2 right-2 h-10 w-10 p-0 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"
8228 title={__("Delete tab", "yatra")}
8229 >
8230 <Trash2 className="w-5 h-5" />
8231 </Button>
8232 )}
8233 <div className="flex items-center gap-3 mb-3">
8234 <GripVertical className="w-6 h-6 text-gray-400 cursor-grab active:cursor-grabbing" />
8235 <div className="flex-1">
8236 <div className="flex items-center gap-2 mb-2">
8237 <Input
8238 type="text"
8239 value={tab.label}
8240 onChange={(e) =>
8241 handleTabLabelChange(tab.id, e.target.value)
8242 }
8243 className="text-sm font-medium flex-1"
8244 disabled={!tab.enabled}
8245 placeholder={__("Tab Label", "yatra")}
8246 />
8247 <Badge variant="outline" className="text-xs">
8248 {tab.content_type}
8249 </Badge>
8250 </div>
8251 <div className="mb-2">
8252 {tab.enabled ? (
8253 <IconPicker
8254 value={tab.icon || null}
8255 onChange={(value) =>
8256 handleTabIconChange(tab.id, value)
8257 }
8258 label={__("Tab Icon", "yatra")}
8259 helpText={__(
8260 "Select an icon or upload an image for this tab",
8261 "yatra",
8262 )}
8263 size="sm"
8264 />
8265 ) : (
8266 <div className="opacity-50">
8267 <IconPicker
8268 value={tab.icon || null}
8269 onChange={() => {}} // No-op when disabled
8270 label={__("Tab Icon", "yatra")}
8271 helpText={__(
8272 "Enable this tab to select an icon",
8273 "yatra",
8274 )}
8275 size="sm"
8276 />
8277 </div>
8278 )}
8279 </div>
8280 {tab.content_type === "custom" && (
8281 <div className="mt-2">
8282 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
8283 {__("Custom Content", "yatra")}
8284 </label>
8285 <textarea
8286 value={tab.custom_content || ""}
8287 onChange={(e) =>
8288 handleTabCustomContentChange(
8289 tab.id,
8290 e.target.value,
8291 )
8292 }
8293 rows={3}
8294 disabled={!tab.enabled}
8295 placeholder={__(
8296 "Enter custom content for this tab...",
8297 "yatra",
8298 )}
8299 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
8300 />
8301 </div>
8302 )}
8303 {tab.content_type !== "custom" && (
8304 <div className="mt-2">
8305 <Select
8306 value={tab.content_type}
8307 onChange={(e) =>
8308 handleTabContentTypeChange(
8309 tab.id,
8310 e.target
8311 .value as FrontendTab["content_type"],
8312 )
8313 }
8314 disabled={!tab.enabled}
8315 className="text-xs"
8316 >
8317 <option value="overview">
8318 {__("Overview", "yatra")}
8319 </option>
8320 <option value="itinerary">
8321 {__("Itinerary", "yatra")}
8322 </option>
8323 <option value="included_excluded">
8324 {__("Included", "yatra")}
8325 </option>
8326 <option value="location">
8327 {__("Location", "yatra")}
8328 </option>
8329 <option value="important_info">
8330 {__("Important Info", "yatra")}
8331 </option>
8332 <option value="downloads">
8333 {__("Downloads", "yatra")}
8334 </option>
8335 <option value="faq">
8336 {__("FAQ", "yatra")}
8337 </option>
8338 <option value="trip_story">
8339 {__("Story", "yatra")}
8340 </option>
8341 <option value="what_makes_special">
8342 {__("Special", "yatra")}
8343 </option>
8344 <option value="testimonials">
8345 {__("Testimonials", "yatra")}
8346 </option>
8347 <option value="custom">
8348 {__("Custom Content", "yatra")}
8349 </option>
8350 </Select>
8351 </div>
8352 )}
8353 </div>
8354 <div className="flex items-center gap-2">
8355 <Button
8356 type="button"
8357 variant="ghost"
8358 size="sm"
8359 onClick={() => handleTabMove(tab.id, "up")}
8360 disabled={index === 0}
8361 className="h-10 w-10 p-0"
8362 title={__("Move up", "yatra")}
8363 >
8364 <ChevronUp className="w-5 h-5" />
8365 </Button>
8366 <Button
8367 type="button"
8368 variant="ghost"
8369 size="sm"
8370 onClick={() => handleTabMove(tab.id, "down")}
8371 disabled={
8372 index === formData.frontend_tabs.length - 1
8373 }
8374 className="h-10 w-10 p-0"
8375 title={__("Move down", "yatra")}
8376 >
8377 <ChevronDown className="w-5 h-5" />
8378 </Button>
8379 <label className="flex items-center gap-2 cursor-pointer">
8380 <input
8381 type="checkbox"
8382 checked={tab.enabled}
8383 onChange={() => handleTabToggle(tab.id)}
8384 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
8385 />
8386 <span className="text-sm text-gray-700 dark:text-gray-300">
8387 {tab.enabled
8388 ? __("Enabled", "yatra")
8389 : __("Disabled", "yatra")}
8390 </span>
8391 </label>
8392 </div>
8393 </div>
8394 </div>
8395 ))}
8396 </div>
8397 <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
8398 <Button
8399 type="button"
8400 variant="outline"
8401 onClick={handleTabAdd}
8402 className="w-full"
8403 >
8404 <Plus className="w-4 h-4 mr-2" />
8405 {__("Add Custom Tab", "yatra")}
8406 </Button>
8407 </div>
8408 </CardContent>
8409 </Card>
8410 </div>
8411 );
8412
8413 default:
8414 return (
8415 <div className="text-center py-8 text-gray-500">
8416 {__("Section content coming soon...", "yatra")}
8417 </div>
8418 );
8419 }
8420 };
8421
8422 return (
8423 <div className="flex-1 min-h-0 flex flex-col bg-white dark:bg-gray-900 overflow-hidden">
8424 {/* Header */}
8425 <div className="bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-3 flex items-center justify-between">
8426 {/* Left Side - Title, Status, Saved Time */}
8427 <div className="flex items-center gap-4 min-w-0 flex-1">
8428 <div className="min-w-0">
8429 <h1 className="text-base font-semibold text-gray-900 dark:text-white truncate">
8430 {formData.title || __("New Trip", "yatra")}
8431 </h1>
8432 <div className="flex items-center gap-2 mt-1">
8433 <Badge
8434 variant="outline"
8435 className={`bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 ${
8436 formData.status === "publish"
8437 ? "text-green-700 dark:text-green-400 border-green-300 dark:border-green-800"
8438 : formData.status === "review"
8439 ? "text-yellow-700 dark:text-yellow-400 border-yellow-300 dark:border-yellow-800"
8440 : formData.status === "approved"
8441 ? "text-blue-700 dark:text-blue-400 border-blue-300 dark:border-blue-800"
8442 : formData.status === "suspended"
8443 ? "text-orange-700 dark:text-orange-400 border-orange-300 dark:border-orange-800"
8444 : formData.status === "archived"
8445 ? "text-gray-700 dark:text-gray-400 border-gray-300 dark:border-gray-600"
8446 : "text-gray-700 dark:text-gray-300"
8447 }`}
8448 >
8449 {formData.status === "draft"
8450 ? __("Draft", "yatra")
8451 : formData.status === "review"
8452 ? __("Review", "yatra")
8453 : formData.status === "approved"
8454 ? __("Approved", "yatra")
8455 : formData.status === "publish"
8456 ? __("Published", "yatra")
8457 : formData.status === "suspended"
8458 ? __("Suspended", "yatra")
8459 : __("Archived", "yatra")}
8460 </Badge>
8461 </div>
8462 </div>
8463
8464 {/* Quick Start Mode Toggle */}
8465 {!isEditMode && (
8466 <div className="flex items-center gap-2 px-2 py-1 rounded-md bg-gray-50 dark:bg-gray-700/50 flex-shrink-0">
8467 <button
8468 onClick={() => setSimpleMode(!simpleMode)}
8469 className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
8470 title={
8471 simpleMode
8472 ? __("Switch to Advanced Mode", "yatra")
8473 : __("Switch to Simple Mode", "yatra")
8474 }
8475 >
8476 <Lightbulb
8477 className={`w-3.5 h-3.5 ${simpleMode ? "text-yellow-500" : ""}`}
8478 />
8479 <span>
8480 {simpleMode ? __("Simple", "yatra") : __("Advanced", "yatra")}
8481 </span>
8482 </button>
8483 </div>
8484 )}
8485
8486 {/* Guided Tour Button (First Time Users) */}
8487 {!isEditMode && (
8488 <Button
8489 variant="ghost"
8490 size="sm"
8491 onClick={() => {
8492 showToast(
8493 __("Guided tour feature coming soon!", "yatra"),
8494 "info",
8495 );
8496 }}
8497 className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 flex-shrink-0"
8498 >
8499 <BookOpen className="w-3.5 h-3.5 mr-1" />
8500 {__("Take a Tour", "yatra")}
8501 </Button>
8502 )}
8503
8504 {/* Dummy Data Button */}
8505 {!isEditMode && (
8506 <Button
8507 variant="ghost"
8508 size="sm"
8509 onClick={handleFillDummyData}
8510 className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 flex-shrink-0"
8511 title={__("Fill form with dummy data", "yatra")}
8512 >
8513 <Database className="w-3.5 h-3.5 mr-1" />
8514 {__("Fill Dummy Data", "yatra")} ({dummyDataIndex + 1}/
8515 {dummyTripsData.length})
8516 </Button>
8517 )}
8518 </div>
8519
8520 {/* Right Side - Action Buttons */}
8521 <div className="flex items-center gap-2 flex-shrink-0">
8522 {isEditMode && (
8523 <Button
8524 variant="outline"
8525 onClick={() => setShowRevisionsDialog(true)}
8526 className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
8527 >
8528 <History className="w-4 h-4 mr-2" />
8529 {__("Revisions", "yatra")}
8530 </Button>
8531 )}
8532 <Button
8533 variant="outline"
8534 onClick={handlePreview}
8535 className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
8536 disabled={!formData.slug}
8537 >
8538 <Eye className="w-4 h-4 mr-2" />
8539 {__("Preview", "yatra")}
8540 </Button>
8541 <Button
8542 variant="outline"
8543 onClick={handleSaveDraft}
8544 disabled={isSubmitting}
8545 className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
8546 >
8547 <Save className="w-4 h-4 mr-2" />
8548 {isEditMode
8549 ? __("Update Draft", "yatra")
8550 : __("Save Draft", "yatra")}
8551 </Button>
8552 <div className="relative group">
8553 <Button
8554 onClick={handlePublish}
8555 disabled={isSubmitting}
8556 className="bg-blue-600 hover:bg-blue-700 text-white border-0 relative"
8557 >
8558 {isSubmitting ? (
8559 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
8560 ) : (
8561 <>
8562 <Sparkles className="w-4 h-4 mr-2" />
8563 {isEditMode
8564 ? __("Update Trip", "yatra")
8565 : __("Publish Trip", "yatra")}
8566 <ChevronDown className="w-4 h-4 ml-2" />
8567 </>
8568 )}
8569 </Button>
8570 {!isSubmitting && (
8571 <div className="absolute right-0 top-full mt-1 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
8572 <div className="py-1">
8573 <button
8574 type="button"
8575 onClick={() => {
8576 handleFieldChange("status", "draft");
8577 handleSaveDraft();
8578 }}
8579 className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
8580 >
8581 <span>{__("Save as Draft", "yatra")}</span>
8582 </button>
8583 <button
8584 type="button"
8585 onClick={() => {
8586 handleFieldChange("status", "review");
8587 handlePublish();
8588 }}
8589 className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
8590 >
8591 <span>{__("Save for Review", "yatra")}</span>
8592 </button>
8593 <button
8594 type="button"
8595 onClick={() => {
8596 handleFieldChange("status", "approved");
8597 handlePublish();
8598 }}
8599 className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
8600 >
8601 <span>{__("Mark as Approved", "yatra")}</span>
8602 </button>
8603 <button
8604 type="button"
8605 onClick={handlePublish}
8606 className="w-full text-left px-4 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 font-medium flex items-center gap-2"
8607 >
8608 <Sparkles className="w-4 h-4" />
8609 <span>
8610 {isEditMode
8611 ? __("Update & Publish", "yatra")
8612 : __("Publish", "yatra")}
8613 </span>
8614 </button>
8615 <button
8616 type="button"
8617 onClick={() => {
8618 handleFieldChange("status", "suspended");
8619 handlePublish();
8620 }}
8621 className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
8622 >
8623 <span>{__("Suspend", "yatra")}</span>
8624 </button>
8625 <button
8626 type="button"
8627 onClick={() => {
8628 handleFieldChange("status", "archived");
8629 handlePublish();
8630 }}
8631 className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
8632 >
8633 <span>{__("Archive", "yatra")}</span>
8634 </button>
8635 </div>
8636 </div>
8637 )}
8638 </div>
8639 </div>
8640 </div>
8641
8642 {/* Navigation Bar */}
8643 <div className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 px-4 py-2.5">
8644 <div className="flex items-center justify-between gap-4">
8645 {/* Previous Button */}
8646 <Button
8647 variant="outline"
8648 size="default"
8649 onClick={goToPreviousSection}
8650 disabled={currentStepIndex === 0}
8651 className="flex items-center gap-2 text-sm font-semibold border-2 border-blue-400 dark:border-blue-600 bg-blue-50 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/50 hover:border-blue-500 dark:hover:border-blue-500 text-blue-700 dark:text-blue-300 shadow-sm disabled:opacity-40 disabled:cursor-not-allowed flex-shrink-0 px-4 py-2"
8652 >
8653 <ChevronLeft className="w-4 h-4" />
8654 {__("Previous", "yatra")}
8655 </Button>
8656
8657 {/* Next Button */}
8658 <Button
8659 variant="outline"
8660 size="default"
8661 onClick={goToNextSection}
8662 disabled={currentStepIndex >= allSections.length - 1}
8663 className="flex items-center gap-2 text-sm font-semibold border-2 border-blue-400 dark:border-blue-600 bg-blue-50 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/50 hover:border-blue-500 dark:hover:border-blue-500 text-blue-700 dark:text-blue-300 shadow-sm disabled:opacity-40 disabled:cursor-not-allowed flex-shrink-0 px-4 py-2"
8664 >
8665 {__("Next", "yatra")}
8666 <ChevronRight className="w-4 h-4" />
8667 </Button>
8668 </div>
8669 </div>
8670
8671 <div className="flex flex-1 min-h-0 p-0">
8672 {/* Sidebar */}
8673 <div className="w-80 bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 overflow-y-auto overflow-x-hidden flex-shrink-0 min-h-0">
8674 <div className="p-5 pb-8 space-y-6">
8675 {/* PHASE 1: ESSENTIALS */}
8676 <div>
8677 <div className="flex items-center gap-2 mb-3 px-1">
8678 <div className="flex items-center gap-2 flex-1">
8679 <Box className="w-3.5 h-3.5 text-red-500 dark:text-red-400 flex-shrink-0" />
8680 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
8681 {__("PHASE 1: ESSENTIALS", "yatra")}
8682 </h3>
8683 </div>
8684 <Badge
8685 variant="outline"
8686 className="text-[9px] px-1.5 py-0 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800"
8687 >
8688 {__("Must Complete", "yatra")}
8689 </Badge>
8690 </div>
8691 <div className="space-y-0.5">
8692 {essentialsSections.map((section, index) => {
8693 const Icon = section.icon;
8694 const isActive = currentSection === section.id;
8695 const isNext =
8696 !section.completed &&
8697 essentialsSections
8698 .slice(0, index)
8699 .every((s) => s.completed);
8700 return (
8701 <button
8702 key={section.id}
8703 type="button"
8704 onClick={() => setCurrentSection(section.id)}
8705 className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-normal transition-all duration-200 text-left group border relative ${
8706 isActive
8707 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
8708 : isNext
8709 ? "border-amber-200 dark:border-amber-800 bg-amber-50/50 dark:bg-amber-900/10 text-gray-700 dark:text-gray-300 hover:bg-amber-50 dark:hover:bg-amber-900/20"
8710 : "border-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800/50 hover:text-gray-900 dark:hover:text-gray-100"
8711 }`}
8712 >
8713 <div className="flex items-center gap-2 flex-shrink-0">
8714 <span
8715 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
8716 isActive
8717 ? "bg-blue-600 text-white dark:bg-blue-500"
8718 : isNext
8719 ? "bg-amber-500 text-white"
8720 : section.completed
8721 ? "bg-green-500 text-white"
8722 : section.hasErrors
8723 ? "bg-red-500 text-white"
8724 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
8725 }`}
8726 >
8727 {index + 1}
8728 </span>
8729 <Icon
8730 className={`w-4 h-4 transition-colors ${
8731 isActive
8732 ? "text-blue-600 dark:text-blue-400"
8733 : isNext
8734 ? "text-amber-600 dark:text-amber-400"
8735 : section.hasErrors
8736 ? "text-red-600 dark:text-red-400"
8737 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
8738 }`}
8739 />
8740 </div>
8741 <span className="flex-1 min-w-0 break-words leading-snug">
8742 {section.label}
8743 {section.required && (
8744 <span className="ml-1 text-[9px] text-red-500">
8745 *
8746 </span>
8747 )}
8748 </span>
8749 {section.hasErrors && (
8750 <span
8751 className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full"
8752 title={__("This section has errors", "yatra")}
8753 />
8754 )}
8755 </button>
8756 );
8757 })}
8758 </div>
8759 </div>
8760
8761 {/* PHASE 2: DETAILS */}
8762 <div>
8763 <div className="flex items-center gap-2 mb-3 px-1">
8764 <div className="flex items-center gap-2 flex-1">
8765 <BookOpen className="w-3.5 h-3.5 text-blue-500 dark:text-blue-400 flex-shrink-0" />
8766 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
8767 {__("PHASE 2: DETAILS", "yatra")}
8768 </h3>
8769 </div>
8770 <Badge
8771 variant="outline"
8772 className="text-[9px] px-1.5 py-0 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800"
8773 >
8774 {__("Recommended", "yatra")}
8775 </Badge>
8776 </div>
8777 <div className="space-y-0.5">
8778 {detailsSections.map((section, index) => {
8779 const Icon = section.icon;
8780 const isActive = currentSection === section.id;
8781 const sectionNumber = essentialsSections.length + index + 1;
8782 return (
8783 <button
8784 key={section.id}
8785 type="button"
8786 onClick={() => setCurrentSection(section.id)}
8787 className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-normal transition-all duration-200 text-left group border ${
8788 isActive
8789 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
8790 : section.hasErrors
8791 ? "border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-900/10"
8792 : "border-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800/50 hover:text-gray-900 dark:hover:text-gray-100"
8793 }`}
8794 >
8795 <div className="flex items-center gap-2 flex-shrink-0">
8796 <span
8797 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
8798 isActive
8799 ? "bg-blue-600 text-white dark:bg-blue-500"
8800 : section.completed
8801 ? "bg-green-500 text-white"
8802 : section.hasErrors
8803 ? "bg-red-500 text-white"
8804 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
8805 }`}
8806 >
8807 {sectionNumber}
8808 </span>
8809 <Icon
8810 className={`w-4 h-4 transition-colors ${
8811 isActive
8812 ? "text-blue-600 dark:text-blue-400"
8813 : section.hasErrors
8814 ? "text-red-600 dark:text-red-400"
8815 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
8816 }`}
8817 />
8818 </div>
8819 <span className="flex-1 min-w-0 break-words leading-snug">
8820 {section.label}
8821 </span>
8822 {section.hasErrors && (
8823 <span
8824 className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full"
8825 title={__("This section has errors", "yatra")}
8826 />
8827 )}
8828 </button>
8829 );
8830 })}
8831 </div>
8832 </div>
8833
8834 {/* PHASE 3: OPTIMIZATION */}
8835 <div>
8836 <div className="flex items-center gap-2 mb-3 px-1">
8837 <div className="flex items-center gap-2 flex-1">
8838 <BarChart3 className="w-3.5 h-3.5 text-purple-500 dark:text-purple-400 flex-shrink-0" />
8839 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
8840 {__("PHASE 3: OPTIMIZATION", "yatra")}
8841 </h3>
8842 </div>
8843 <Badge
8844 variant="outline"
8845 className="text-[9px] px-1.5 py-0 bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400 border-purple-200 dark:border-purple-800"
8846 >
8847 {__("Optional", "yatra")}
8848 </Badge>
8849 </div>
8850 <div className="space-y-0.5">
8851 {optimizationSections.map((section, index) => {
8852 const Icon = section.icon;
8853 const isActive = currentSection === section.id;
8854 const sectionNumber =
8855 essentialsSections.length +
8856 detailsSections.length +
8857 index +
8858 1;
8859 return (
8860 <button
8861 key={section.id}
8862 type="button"
8863 onClick={() => setCurrentSection(section.id)}
8864 className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-normal transition-all duration-200 text-left group border ${
8865 isActive
8866 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
8867 : section.hasErrors
8868 ? "border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-900/10"
8869 : "border-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800/50 hover:text-gray-900 dark:hover:text-gray-100"
8870 }`}
8871 >
8872 <div className="flex items-center gap-2 flex-shrink-0">
8873 <span
8874 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
8875 isActive
8876 ? "bg-blue-600 text-white dark:bg-blue-500"
8877 : section.completed
8878 ? "bg-green-500 text-white"
8879 : section.hasErrors
8880 ? "bg-red-500 text-white"
8881 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
8882 }`}
8883 >
8884 {sectionNumber}
8885 </span>
8886 <Icon
8887 className={`w-4 h-4 transition-colors ${
8888 isActive
8889 ? "text-blue-600 dark:text-blue-400"
8890 : section.hasErrors
8891 ? "text-red-600 dark:text-red-400"
8892 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
8893 }`}
8894 />
8895 </div>
8896 <span className="flex-1 min-w-0 break-words leading-snug">
8897 {section.label}
8898 </span>
8899 {section.hasErrors && (
8900 <span
8901 className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full"
8902 title={__("This section has errors", "yatra")}
8903 />
8904 )}
8905 </button>
8906 );
8907 })}
8908 </div>
8909 </div>
8910
8911 {/* PHASE 4: ADVANCED */}
8912 <div className="pb-12">
8913 <div className="flex items-center gap-2 mb-3 px-1">
8914 <div className="flex items-center gap-2 flex-1">
8915 <Settings className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 flex-shrink-0" />
8916 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
8917 {__("PHASE 4: ADVANCED", "yatra")}
8918 </h3>
8919 </div>
8920 <Badge
8921 variant="outline"
8922 className="text-[9px] px-1.5 py-0 bg-gray-50 dark:bg-gray-900/20 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800"
8923 >
8924 {__("Power Users", "yatra")}
8925 </Badge>
8926 </div>
8927 <div className="space-y-0.5">
8928 {advancedSections.map((section, index) => {
8929 const Icon = section.icon;
8930 const isActive = currentSection === section.id;
8931 const sectionNumber =
8932 essentialsSections.length +
8933 detailsSections.length +
8934 optimizationSections.length +
8935 index +
8936 1;
8937 return (
8938 <button
8939 key={section.id}
8940 type="button"
8941 onClick={() => setCurrentSection(section.id)}
8942 className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-normal transition-all duration-200 text-left group border ${
8943 isActive
8944 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
8945 : "border-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800/50 hover:text-gray-900 dark:hover:text-gray-100"
8946 }`}
8947 >
8948 <div className="flex items-center gap-2 flex-shrink-0">
8949 <span
8950 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
8951 isActive
8952 ? "bg-blue-600 text-white dark:bg-blue-500"
8953 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
8954 }`}
8955 >
8956 {sectionNumber}
8957 </span>
8958 <Icon
8959 className={`w-4 h-4 transition-colors ${
8960 isActive
8961 ? "text-blue-600 dark:text-blue-400"
8962 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
8963 }`}
8964 />
8965 </div>
8966 <span className="flex-1 min-w-0 break-words leading-snug">
8967 {section.label}
8968 </span>
8969 </button>
8970 );
8971 })}
8972 </div>
8973 </div>
8974 </div>
8975 </div>
8976
8977 {/* Main Content */}
8978 <div className="flex-1 overflow-y-auto bg-white dark:bg-gray-900">
8979 <div className="p-6 pb-24 max-w-4xl">
8980 {errors.submit && (
8981 <Alert variant="error" className="mb-4">
8982 {errors.submit}
8983 </Alert>
8984 )}
8985 {renderSectionContent()}
8986 </div>
8987 </div>
8988 </div>
8989
8990 {/* Modals */}
8991 {/* Highlight Modal */}
8992 {showHighlightModal && (
8993 <div
8994 className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
8995 onClick={() => setShowHighlightModal(false)}
8996 >
8997 <div
8998 className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-xl"
8999 onClick={(e) => e.stopPropagation()}
9000 >
9001 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
9002 {__("Add Highlight", "yatra")}
9003 </h3>
9004 <div className="space-y-4">
9005 <div>
9006 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
9007 {__("Highlight Text", "yatra")}
9008 </label>
9009 <Input
9010 type="text"
9011 value={modalInput.text}
9012 onChange={(e) =>
9013 setModalInput({ ...modalInput, text: e.target.value })
9014 }
9015 placeholder={__("e.g., Private guide included", "yatra")}
9016 autoFocus
9017 onKeyDown={(e) => {
9018 if (e.key === "Enter") {
9019 e.preventDefault();
9020 handleHighlightSave();
9021 }
9022 }}
9023 />
9024 <HelpText
9025 text={__(
9026 'Keep it short and impactful. Examples: "All meals included", "Skip-the-line tickets", "Free airport transfer"',
9027 "yatra",
9028 )}
9029 className="mt-2"
9030 />
9031 </div>
9032 <div className="flex items-center justify-end gap-2">
9033 <Button
9034 variant="outline"
9035 onClick={() => setShowHighlightModal(false)}
9036 >
9037 {__("Cancel", "yatra")}
9038 </Button>
9039 <Button
9040 onClick={handleHighlightSave}
9041 disabled={!modalInput.text.trim()}
9042 >
9043 {__("Add", "yatra")}
9044 </Button>
9045 </div>
9046 </div>
9047 </div>
9048 </div>
9049 )}
9050
9051 {/* Revisions Dialog */}
9052 {showRevisionsDialog && (
9053 <div
9054 className="fixed inset-0 z-[9998] flex items-center justify-center bg-black/50 backdrop-blur-sm"
9055 onClick={() => setShowRevisionsDialog(false)}
9056 >
9057 <Card
9058 className="w-full max-w-2xl max-h-[80vh] mx-4 shadow-xl overflow-hidden flex flex-col"
9059 onClick={(e) => e.stopPropagation()}
9060 >
9061 <CardHeader className="pb-3 flex-shrink-0">
9062 <div className="flex items-center justify-between">
9063 <CardTitle className="text-lg flex items-center gap-2">
9064 <History className="w-5 h-5" />
9065 {__("Trip Revisions", "yatra")}
9066 </CardTitle>
9067 <button
9068 onClick={() => setShowRevisionsDialog(false)}
9069 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
9070 aria-label={__("Close", "yatra")}
9071 >
9072 <X className="w-5 h-5" />
9073 </button>
9074 </div>
9075 </CardHeader>
9076 <CardContent className="flex-1 overflow-y-auto pb-4">
9077 {isLoadingRevisions ? (
9078 <div className="flex items-center justify-center py-8">
9079 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
9080 <span className="ml-2 text-gray-600 dark:text-gray-400">
9081 {__("Loading revisions...", "yatra")}
9082 </span>
9083 </div>
9084 ) : revisions.length === 0 ? (
9085 <div className="text-center py-8 text-gray-500 dark:text-gray-400">
9086 {__("No revisions found", "yatra")}
9087 </div>
9088 ) : (
9089 <div className="space-y-2">
9090 {revisions.map((revision) => (
9091 <div
9092 key={revision.id}
9093 className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors"
9094 onClick={() => handleRevisionClick(revision.id)}
9095 >
9096 <div className="flex items-center justify-between">
9097 <div className="flex-1">
9098 <div className="flex items-center gap-2 mb-1">
9099 <Badge variant="outline">v{revision.version}</Badge>
9100 <span className="text-sm font-medium text-gray-900 dark:text-white">
9101 {__("Revision", "yatra")} {revision.version}
9102 </span>
9103 </div>
9104 <div className="text-xs text-gray-500 dark:text-gray-400">
9105 {__("Created by", "yatra")}{" "}
9106 {revision.created_by_name} •{" "}
9107 {new Date(revision.created_at).toLocaleString()}
9108 </div>
9109 </div>
9110 <Button
9111 variant="outline"
9112 size="sm"
9113 onClick={(e) => {
9114 e.stopPropagation();
9115 handleRevisionClick(revision.id);
9116 }}
9117 >
9118 {__("Use This Revision", "yatra")}
9119 </Button>
9120 </div>
9121 </div>
9122 ))}
9123 </div>
9124 )}
9125 </CardContent>
9126 </Card>
9127 </div>
9128 )}
9129
9130 {/* Revision Confirmation Dialog */}
9131 <ConfirmationDialog
9132 isOpen={showRevisionConfirm}
9133 onClose={() => {
9134 setShowRevisionConfirm(false);
9135 setSelectedRevisionId(null);
9136 }}
9137 onConfirm={handleRevisionConfirm}
9138 title={__("Use This Revision?", "yatra")}
9139 message={__(
9140 "Do you want to use this revision? This will replace all current form data with the revision data.",
9141 "yatra",
9142 )}
9143 confirmText={__("Yes, Use This Revision", "yatra")}
9144 cancelText={__("Cancel", "yatra")}
9145 variant="info"
9146 isLoading={false}
9147 />
9148
9149 {/* Landmark Input Modal */}
9150 <Modal
9151 isOpen={showLandmarkDialog}
9152 onClose={() => {
9153 setShowLandmarkDialog(false);
9154 setLandmarkInput("");
9155 }}
9156 title={__("Add Landmark", "yatra")}
9157 description={__(
9158 "Enter the name of the landmark or point of interest",
9159 "yatra",
9160 )}
9161 size="sm"
9162 hideFooter={false}
9163 footer={
9164 <div className="flex gap-2 justify-end">
9165 <Button
9166 variant="outline"
9167 onClick={() => {
9168 setShowLandmarkDialog(false);
9169 setLandmarkInput("");
9170 }}
9171 >
9172 {__("Cancel", "yatra")}
9173 </Button>
9174 <Button
9175 onClick={() => {
9176 if (landmarkInput.trim()) {
9177 handleFieldChange("landmarks", [
9178 ...formData.landmarks,
9179 landmarkInput.trim(),
9180 ]);
9181 setLandmarkInput("");
9182 setShowLandmarkDialog(false);
9183 }
9184 }}
9185 disabled={!landmarkInput.trim()}
9186 >
9187 <Plus className="w-4 h-4 mr-2" />
9188 {__("Add Landmark", "yatra")}
9189 </Button>
9190 </div>
9191 }
9192 >
9193 <div className="space-y-4">
9194 <div>
9195 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
9196 {__("Landmark Name", "yatra")}
9197 </label>
9198 <Input
9199 type="text"
9200 value={landmarkInput}
9201 onChange={(e) => setLandmarkInput(e.target.value)}
9202 placeholder={__(
9203 "e.g., Eiffel Tower, Central Park, Grand Canyon",
9204 "yatra",
9205 )}
9206 className="w-full"
9207 autoFocus
9208 onKeyDown={(e) => {
9209 if (e.key === "Enter" && landmarkInput.trim()) {
9210 handleFieldChange("landmarks", [
9211 ...formData.landmarks,
9212 landmarkInput.trim(),
9213 ]);
9214 setLandmarkInput("");
9215 setShowLandmarkDialog(false);
9216 }
9217 }}
9218 />
9219 </div>
9220 <div className="text-xs text-gray-500 dark:text-gray-400">
9221 {__(
9222 "Examples: Famous monuments, natural landmarks, historical sites, viewpoints, or points of interest that travelers will visit during this trip.",
9223 "yatra",
9224 )}
9225 </div>
9226 </div>
9227 </Modal>
9228 </div>
9229 );
9230 };
9231
9232 export default TripForm;
9233