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

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