PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
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.6, at resources/js/pages/TripForm.tsx

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