| 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(/&/g, "&") |
| 19 |
.replace(/</g, "<") |
| 20 |
.replace(/>/g, ">") |
| 21 |
.replace(/"/g, '"') |
| 22 |
.replace(/'/g, "'") |
| 23 |
.replace(/ /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 |
Send, |
| 38 |
Loader2, |
| 39 |
Sparkles, |
| 40 |
Calendar, |
| 41 |
Clock, |
| 42 |
Image, |
| 43 |
HelpCircle, |
| 44 |
Search, |
| 45 |
Settings, |
| 46 |
CheckCircle2, |
| 47 |
CheckSquare, |
| 48 |
FileText, |
| 49 |
DollarSign, |
| 50 |
AlertCircle, |
| 51 |
Plus, |
| 52 |
Upload, |
| 53 |
MapPin, |
| 54 |
Tag, |
| 55 |
GripVertical, |
| 56 |
ChevronUp, |
| 57 |
ChevronDown, |
| 58 |
ChevronLeft, |
| 59 |
ChevronRight, |
| 60 |
Copy, |
| 61 |
Check, |
| 62 |
BookOpen, |
| 63 |
Download, |
| 64 |
Eye, |
| 65 |
X, |
| 66 |
Trash2, |
| 67 |
Users, |
| 68 |
Box, |
| 69 |
BarChart3, |
| 70 |
Database, |
| 71 |
History, |
| 72 |
Home, |
| 73 |
Car, |
| 74 |
Lightbulb, |
| 75 |
} from "lucide-react"; |
| 76 |
import { RichTextEditor } from "../components/ui/rich-text-editor"; |
| 77 |
import { IconPicker, IconPickerValue } from "../components/ui/icon-picker"; |
| 78 |
import { AiFieldAffordance } from "../components/ai/AiFieldAffordance"; |
| 79 |
import { AutoFillTripModal } from "../components/ai/AutoFillTripModal"; |
| 80 |
import { BuildItineraryModal } from "../components/ai/BuildItineraryModal"; |
| 81 |
import { isAiEligible, isAiModuleEnabled } from "../lib/ai-availability"; |
| 82 |
import { __, sprintf } from "../lib/i18n"; |
| 83 |
import { MEAL_PLAN_SELECT_OPTIONS } from "../lib/meal-plan-labels"; |
| 84 |
import { usePermissions } from "../hooks/usePermissions"; |
| 85 |
import { fetchSettings } from "../api/settings-api"; |
| 86 |
import { apiClient } from "../lib/api-client"; |
| 87 |
import { wpService } from "../lib/api-client"; |
| 88 |
import { Button } from "../components/ui/button"; |
| 89 |
import { Input } from "@/components/ui/input"; |
| 90 |
import { DatePicker } from "@/components/ui/date-picker"; |
| 91 |
import { prepareWordPressMediaFrameOpen } from "../lib/wp-media-open"; |
| 92 |
import { Modal } from "../components/ui/modal"; |
| 93 |
import { Select } from "../components/ui/select"; |
| 94 |
import { Alert } from "../components/ui/alert"; |
| 95 |
import { |
| 96 |
Card, |
| 97 |
CardContent, |
| 98 |
CardHeader, |
| 99 |
CardTitle, |
| 100 |
CardDescription, |
| 101 |
} from "../components/ui/card"; |
| 102 |
import { Badge } from "../components/ui/badge"; |
| 103 |
import TripAttributesSection from "../components/trip-form/TripAttributesSection"; |
| 104 |
import { HelpText } from "../components/ui/help-text"; |
| 105 |
import { getCurrencySymbol } from "../data/currencies"; |
| 106 |
import { ItinerarySection } from "../components/trip-form/sections/ItinerarySection"; |
| 107 |
import { IncludedSection } from "../components/trip-form/sections/IncludedSection"; |
| 108 |
import { ConfirmationDialog } from "../components/ui/confirmation-dialog"; |
| 109 |
import { useToast } from "../components/ui/toast"; |
| 110 |
import { MultiSelect } from "../components/ui/multi-select"; |
| 111 |
import { TestimonialsSelector } from "../components/trip-form/TestimonialsSelector"; |
| 112 |
import { LocationPicker } from "../components/trip-form/LocationPicker"; |
| 113 |
import { ProFeature, ProBadge } from "../components/ProFeature"; |
| 114 |
import { getErrorContext } from "../lib/errors"; |
| 115 |
import { |
| 116 |
buildYatraSinglePublicUrls, |
| 117 |
isWordPressPlainPermalink, |
| 118 |
} from "../lib/frontend-permalink-urls"; |
| 119 |
|
| 120 |
type SectionId = |
| 121 |
| "basic" // 1. Basic Information (title, description, highlights, featured image, trip type, duration) |
| 122 |
| "location" // 2. Location & Geography |
| 123 |
| "duration" // 3. Schedule & Availability (renamed) |
| 124 |
| "pricing" // 4. Pricing & Payment |
| 125 |
| "booking" // 5. Booking Requirements |
| 126 |
| "attributes" // 6. Trip Attributes |
| 127 |
| "itinerary" // 7. Itinerary Builder (includes Included/Excluded) |
| 128 |
| "included" // 8. What's Included/Excluded (deprecated - merged into itinerary) |
| 129 |
| "media" // 9. Media & Content (gallery, video, story, testimonials) |
| 130 |
| "downloads" // 9b. Downloads (Pro module) |
| 131 |
| "categorization" // 10. Categorization & Tags (category, activities, difficulty, tags) |
| 132 |
| "faqs" // 11. FAQs |
| 133 |
| "seo" // 12. SEO Settings |
| 134 |
| "advanced"; // 13. Advanced Settings (status, scheduling, frontend tabs) |
| 135 |
|
| 136 |
interface Section { |
| 137 |
id: SectionId; |
| 138 |
label: string; |
| 139 |
icon: React.ComponentType<{ className?: string }>; |
| 140 |
required: boolean; |
| 141 |
completed: boolean; |
| 142 |
hasErrors?: boolean; |
| 143 |
} |
| 144 |
|
| 145 |
interface FAQ { |
| 146 |
question: string; |
| 147 |
answer: string; |
| 148 |
} |
| 149 |
|
| 150 |
interface TravelerCategory { |
| 151 |
id: number; |
| 152 |
label: string; |
| 153 |
description: string; |
| 154 |
age_min?: number; |
| 155 |
age_max?: number; |
| 156 |
status: "active" | "inactive" | "publish" | "draft"; |
| 157 |
pricing_mode?: "per_person" | "per_group"; |
| 158 |
min_pax?: number | null; |
| 159 |
max_pax?: number | null; |
| 160 |
} |
| 161 |
|
| 162 |
interface PriceType { |
| 163 |
category_id: number; |
| 164 |
original_price: string; |
| 165 |
discounted_price: string; |
| 166 |
is_default?: boolean; |
| 167 |
} |
| 168 |
|
| 169 |
interface TripCategoryOption { |
| 170 |
id: number; |
| 171 |
name: string; |
| 172 |
slug?: string; |
| 173 |
description?: string; |
| 174 |
parent_id: number | null; |
| 175 |
status?: string; |
| 176 |
subcategories?: TripCategoryOption[]; |
| 177 |
} |
| 178 |
|
| 179 |
interface DifficultyLevelOption { |
| 180 |
id: number; |
| 181 |
name: string; |
| 182 |
slug?: string; |
| 183 |
description?: string; |
| 184 |
level_order?: number; |
| 185 |
status?: string; |
| 186 |
} |
| 187 |
|
| 188 |
interface TripAmenityItem { |
| 189 |
title: string; |
| 190 |
description: string; |
| 191 |
} |
| 192 |
|
| 193 |
interface DownloadableItem { |
| 194 |
id?: number | null; |
| 195 |
title: string; |
| 196 |
description: string; |
| 197 |
attachment_id: number | null; |
| 198 |
attachment_url?: string; |
| 199 |
attachment_title?: string; |
| 200 |
visibility: "public" | "logged_in" | "booked_only"; |
| 201 |
enabled: boolean; |
| 202 |
sort_order?: number; |
| 203 |
} |
| 204 |
|
| 205 |
const buildCategoryOptionNodes = ( |
| 206 |
categories: TripCategoryOption[], |
| 207 |
depth = 0, |
| 208 |
): React.ReactNode[] => { |
| 209 |
return categories.flatMap((category) => { |
| 210 |
const optionValue = |
| 211 |
category.slug || category.name || category.id?.toString() || ""; |
| 212 |
const labelPrefix = |
| 213 |
depth > 0 ? `${"-- ".repeat(depth)}${category.name}` : category.name; |
| 214 |
const nodes: React.ReactNode[] = [ |
| 215 |
<option key={`category-option-${category.id}`} value={optionValue}> |
| 216 |
{labelPrefix} |
| 217 |
</option>, |
| 218 |
]; |
| 219 |
|
| 220 |
if ( |
| 221 |
Array.isArray(category.subcategories) && |
| 222 |
category.subcategories.length > 0 |
| 223 |
) { |
| 224 |
nodes.push( |
| 225 |
...buildCategoryOptionNodes(category.subcategories, depth + 1), |
| 226 |
); |
| 227 |
} |
| 228 |
|
| 229 |
return nodes; |
| 230 |
}); |
| 231 |
}; |
| 232 |
|
| 233 |
const extractArrayPayload = (payload: any): any[] => { |
| 234 |
if (!payload) return []; |
| 235 |
if (Array.isArray(payload)) return payload; |
| 236 |
if (Array.isArray(payload.data)) return payload.data; |
| 237 |
if (Array.isArray(payload?.data?.data)) return payload.data.data; |
| 238 |
return []; |
| 239 |
}; |
| 240 |
|
| 241 |
const normalizeAmenityItems = (items: unknown): TripAmenityItem[] => { |
| 242 |
if (!items) return []; |
| 243 |
if (Array.isArray(items)) { |
| 244 |
return items |
| 245 |
.map((item) => { |
| 246 |
if (typeof item === "string") { |
| 247 |
return { title: item, description: "" }; |
| 248 |
} |
| 249 |
if (item && typeof item === "object") { |
| 250 |
const obj = item as Partial<TripAmenityItem>; |
| 251 |
return { |
| 252 |
title: (obj.title ?? "").toString(), |
| 253 |
description: (obj.description ?? "").toString(), |
| 254 |
}; |
| 255 |
} |
| 256 |
return { title: String(item), description: "" }; |
| 257 |
}) |
| 258 |
.filter((item) => item.title.trim().length > 0); |
| 259 |
} |
| 260 |
if (typeof items === "string") { |
| 261 |
try { |
| 262 |
const parsed = JSON.parse(items); |
| 263 |
if (Array.isArray(parsed)) { |
| 264 |
return normalizeAmenityItems(parsed); |
| 265 |
} |
| 266 |
} catch { |
| 267 |
return [{ title: items, description: "" }]; |
| 268 |
} |
| 269 |
} |
| 270 |
return []; |
| 271 |
}; |
| 272 |
|
| 273 |
interface TimeSlot { |
| 274 |
id: string; |
| 275 |
time: string; // HH:MM format |
| 276 |
label: string; |
| 277 |
} |
| 278 |
|
| 279 |
/** |
| 280 |
* Pull a context payload out of the live formData for AI generation |
| 281 |
* calls. Kept narrow on purpose — fewer tokens, no PII, and only the |
| 282 |
* fields the prompt templates actually reference. Helpers stay free of |
| 283 |
* React state so AiFieldAffordance's `buildContext` callback can be |
| 284 |
* invoked from anywhere without re-deriving form deps. |
| 285 |
*/ |
| 286 |
function buildTripAiContext(formData: any): Record<string, unknown> { |
| 287 |
const amenityTitles = (items: any[] | undefined): string[] => |
| 288 |
Array.isArray(items) |
| 289 |
? items |
| 290 |
.map((it) => (typeof it === "string" ? it : it?.title || it?.label)) |
| 291 |
.filter((s): s is string => typeof s === "string" && s.trim() !== "") |
| 292 |
: []; |
| 293 |
|
| 294 |
return { |
| 295 |
name: formData?.title ?? "", |
| 296 |
short_description: formData?.short_description ?? "", |
| 297 |
description: stripHtml(formData?.description ?? ""), |
| 298 |
destinations: namesFromIds(formData?.destinations), |
| 299 |
categories: namesFromIds(formData?.categories), |
| 300 |
activities: namesFromIds(formData?.activities), |
| 301 |
difficulty_level: formData?.difficulty_level ?? "", |
| 302 |
duration_days: formData?.duration_days ?? "", |
| 303 |
duration_nights: formData?.duration_nights ?? "", |
| 304 |
best_season: formData?.best_season ?? "", |
| 305 |
price: formData?.price ?? "", |
| 306 |
deposit_percentage: formData?.deposit_percentage ?? "", |
| 307 |
booking_deadline_hours: formData?.booking_deadline_hours ?? "", |
| 308 |
age_min: formData?.age_min ?? "", |
| 309 |
age_max: formData?.age_max ?? "", |
| 310 |
accommodation_type: formData?.accommodation_type ?? "", |
| 311 |
transportation_included: formData?.transportation_included ?? "", |
| 312 |
included_items: amenityTitles(formData?.included_items), |
| 313 |
excluded_items: amenityTitles(formData?.excluded_items), |
| 314 |
}; |
| 315 |
} |
| 316 |
|
| 317 |
function stripHtml(value: string): string { |
| 318 |
if (!value) return ""; |
| 319 |
return value |
| 320 |
.replace(/<[^>]*>/g, " ") |
| 321 |
.replace(/\s+/g, " ") |
| 322 |
.trim(); |
| 323 |
} |
| 324 |
|
| 325 |
/** Best-effort name resolution for taxonomy fields. The form stores IDs |
| 326 |
* in some places and objects in others; we accept either. */ |
| 327 |
function namesFromIds(value: any): string[] { |
| 328 |
if (!Array.isArray(value)) return []; |
| 329 |
return value |
| 330 |
.map((item) => { |
| 331 |
if (typeof item === "string") return item; |
| 332 |
if (item && typeof item === "object") { |
| 333 |
return item.name || item.label || item.title || ""; |
| 334 |
} |
| 335 |
return ""; |
| 336 |
}) |
| 337 |
.filter((s): s is string => typeof s === "string" && s.trim() !== ""); |
| 338 |
} |
| 339 |
|
| 340 |
interface TripFormData { |
| 341 |
// Overview |
| 342 |
title: string; |
| 343 |
slug: string; |
| 344 |
description: string; |
| 345 |
highlights: string[]; |
| 346 |
trip_details: string; |
| 347 |
short_description: string; |
| 348 |
what_makes_special: string; // What Makes This Trip Special |
| 349 |
trip_story: string; // Trip Story/Narrative |
| 350 |
video_url: string; // Video Embed URL |
| 351 |
virtual_tour_url: string; // 360° Virtual Tour URL |
| 352 |
testimonial_review_ids: number[]; // Array of review IDs to display as testimonials |
| 353 |
|
| 354 |
// Location & Geography |
| 355 |
destinations: number[]; // Array of destination IDs |
| 356 |
starting_location: string; |
| 357 |
ending_location: string; |
| 358 |
countries: string[]; |
| 359 |
regions: string[]; |
| 360 |
starting_latitude: string; |
| 361 |
starting_longitude: string; |
| 362 |
ending_latitude: string; |
| 363 |
ending_longitude: string; |
| 364 |
landmarks: string[]; // Geographic Tags - Landmarks |
| 365 |
|
| 366 |
// Duration & Schedule |
| 367 |
trip_type: "single_day" | "multi_day" | "flexible"; |
| 368 |
duration_days: string; |
| 369 |
duration_nights: string; |
| 370 |
available_from: string; |
| 371 |
available_to: string; |
| 372 |
booking_window_days: string; |
| 373 |
seasonal_availability: string; |
| 374 |
best_season: string; // Best season indicator |
| 375 |
peak_season: string; // Peak season indicator |
| 376 |
off_season: string; // Off-season indicator |
| 377 |
|
| 378 |
// Activity & Category |
| 379 |
activity_types: number[]; // Array of activity IDs (changed from string[]) |
| 380 |
difficulty_level: string; |
| 381 |
trip_category: number[]; // Array of category IDs |
| 382 |
tags: string[]; |
| 383 |
featured_priority: "none" | "featured" | "new" | "limited"; // Featured Priority |
| 384 |
|
| 385 |
// Accommodation |
| 386 |
accommodation_type: string; |
| 387 |
meal_plan: string; |
| 388 |
accommodation_details: string; |
| 389 |
|
| 390 |
// Transportation |
| 391 |
transportation_included: boolean; |
| 392 |
pickup_location: string; |
| 393 |
dropoff_location: string; |
| 394 |
transportation_details: string; |
| 395 |
|
| 396 |
// Pricing |
| 397 |
pricing_type: "regular" | "traveler_based"; |
| 398 |
original_price: string; |
| 399 |
discounted_price: string; |
| 400 |
price_types: PriceType[]; |
| 401 |
deposit_amount: string; |
| 402 |
deposit_percentage: string; |
| 403 |
payment_terms: string; |
| 404 |
max_travelers: string; |
| 405 |
min_travelers: string; |
| 406 |
booking_deadline_hours: string; |
| 407 |
cancellation_policy: string; |
| 408 |
age_min: string; |
| 409 |
age_max: string; |
| 410 |
physical_requirements: string; |
| 411 |
visa_requirements: string; |
| 412 |
vaccination_requirements: string; |
| 413 |
disable_booking: boolean; // Pro: enquiry-only mode (stored in custom_fields) |
| 414 |
|
| 415 |
// Fallback Settings (for trips without availability dates/rules) |
| 416 |
has_default_time_slots: boolean; // For day tours: enable multiple time slots |
| 417 |
default_time_slots: TimeSlot[]; // Array of time slot objects for day tours |
| 418 |
departure_time: string; // Default departure time |
| 419 |
|
| 420 |
// Included/Excluded |
| 421 |
included_items: TripAmenityItem[]; |
| 422 |
excluded_items: TripAmenityItem[]; |
| 423 |
|
| 424 |
// Attributes & Properties |
| 425 |
attributes: Record<number, any>; // attribute_id -> value mapping |
| 426 |
|
| 427 |
// Itinerary |
| 428 |
itinerary_days: ItineraryDay[]; |
| 429 |
|
| 430 |
// Gallery |
| 431 |
gallery_images: Array<{ |
| 432 |
id: number; |
| 433 |
url: string; |
| 434 |
thumbnail_url?: string; |
| 435 |
alt_text?: string; |
| 436 |
caption?: string; |
| 437 |
}>; |
| 438 |
featured_image: number | null; |
| 439 |
|
| 440 |
// Downloads |
| 441 |
downloadable_items: DownloadableItem[]; |
| 442 |
|
| 443 |
// FAQs |
| 444 |
faqs: FAQ[]; |
| 445 |
|
| 446 |
// Frontend Tabs |
| 447 |
frontend_tabs: FrontendTab[]; |
| 448 |
|
| 449 |
// Availability |
| 450 |
availability_dates: AvailabilityDate[]; |
| 451 |
|
| 452 |
// Status & Lifecycle |
| 453 |
status: |
| 454 |
| "draft" |
| 455 |
| "review" |
| 456 |
| "approved" |
| 457 |
| "publish" |
| 458 |
| "archived" |
| 459 |
| "suspended"; |
| 460 |
scheduled_publish_date: string; // Scheduled Publishing |
| 461 |
scheduled_unpublish_date: string; // Scheduled Unpublishing |
| 462 |
version: number; // Version Control |
| 463 |
seasonal_auto_enable: boolean; // Auto-enable/disable based on dates |
| 464 |
seasonal_enable_date: string; // Date to auto-enable |
| 465 |
seasonal_disable_date: string; // Date to auto-disable |
| 466 |
|
| 467 |
// SEO |
| 468 |
meta_title: string; |
| 469 |
meta_description: string; |
| 470 |
meta_keywords: string; |
| 471 |
} |
| 472 |
|
| 473 |
interface FrontendTab { |
| 474 |
id: string; |
| 475 |
label: string; |
| 476 |
enabled: boolean; |
| 477 |
order: number; |
| 478 |
content_type: |
| 479 |
| "overview" |
| 480 |
| "itinerary" |
| 481 |
| "included_excluded" |
| 482 |
| "location" |
| 483 |
| "important_info" |
| 484 |
| "downloads" |
| 485 |
| "faq" |
| 486 |
| "trip_story" |
| 487 |
| "what_makes_special" |
| 488 |
| "testimonials" |
| 489 |
| "reviews" |
| 490 |
| "downloads" |
| 491 |
| "custom" |
| 492 |
| "general" |
| 493 |
| "gallery" |
| 494 |
| "faqs"; |
| 495 |
custom_content?: string; |
| 496 |
icon?: IconPickerValue | null; |
| 497 |
} |
| 498 |
|
| 499 |
interface AvailabilityDate { |
| 500 |
id: string; |
| 501 |
/** Present when availability row is tied to a specific trip */ |
| 502 |
trip_id?: number; |
| 503 |
departure_date: string; |
| 504 |
arrival_date: string; |
| 505 |
seats_remaining: string; |
| 506 |
original_price: string; |
| 507 |
discounted_price: string; |
| 508 |
discount_percentage: string; |
| 509 |
status: "available" | "sold_out" | "limited" | "closed"; |
| 510 |
from_location?: string; |
| 511 |
to_location?: string; |
| 512 |
from_latitude?: string; |
| 513 |
from_longitude?: string; |
| 514 |
to_latitude?: string; |
| 515 |
to_longitude?: string; |
| 516 |
} |
| 517 |
|
| 518 |
interface ItineraryEntry { |
| 519 |
id: string; |
| 520 |
day: number; |
| 521 |
day_title?: string; |
| 522 |
// Use IDs to match ItineraryForm structure |
| 523 |
item_type_id: string; // ID of the item type (Activity, Meal, Accommodation, Transportation, Rest) |
| 524 |
item_id: string; // ID of the specific item (Hiking, Breakfast, Hotel, etc.) |
| 525 |
// Keep legacy fields for backward compatibility |
| 526 |
item_type?: "Meal" | "Activity" | "Accommodation" | "Transportation"; |
| 527 |
item_name?: string; |
| 528 |
item_icon?: string; |
| 529 |
// Entry details |
| 530 |
title: string; |
| 531 |
description: string; |
| 532 |
location?: string; |
| 533 |
location_latitude?: string; |
| 534 |
location_longitude?: string; |
| 535 |
duration?: string; |
| 536 |
start_time: string; |
| 537 |
end_time: string; |
| 538 |
time_type: "exact" | "duration" | "flexible"; |
| 539 |
cost?: string; |
| 540 |
cost_per_person: boolean; |
| 541 |
notes?: string; |
| 542 |
included_items: string[]; |
| 543 |
excluded_items: string[]; |
| 544 |
images: string[]; |
| 545 |
status?: "active" | "inactive"; |
| 546 |
} |
| 547 |
|
| 548 |
interface ItineraryDay { |
| 549 |
day: number; |
| 550 |
day_title?: string; |
| 551 |
entries: ItineraryEntry[]; |
| 552 |
} |
| 553 |
|
| 554 |
// getCurrencySymbol is now imported from '../data/currencies' |
| 555 |
|
| 556 |
const TripForm: React.FC = () => { |
| 557 |
const queryClient = useQueryClient(); |
| 558 |
const { can, isPro } = usePermissions(); |
| 559 |
const { showToast } = useToast(); |
| 560 |
// Downloads is now a FREE feature - always show the UI |
| 561 |
const showDownloadsUI = true; |
| 562 |
|
| 563 |
// AI Assistant — modal state for the "Auto-fill" / "Generate itinerary" |
| 564 |
// workflows. `aiModalMode` controls which preset the modal opens with. |
| 565 |
const [aiModalOpen, setAiModalOpen] = useState(false); |
| 566 |
const [aiModalMode, setAiModalMode] = useState<"all" | "itinerary">("all"); |
| 567 |
// Itinerary-builder modal is the SAME component the standalone |
| 568 |
// Itinerary page uses. Going through it (instead of |
| 569 |
// AutoFillTripModal's itineraryOnly mode) means all three entry |
| 570 |
// points for "build itinerary with AI" — wizard, this trip-form |
| 571 |
// tab, and the Itinerary page — converge on the same agent + |
| 572 |
// applyItinerary persistence path. |
| 573 |
const [itineraryBuildOpen, setItineraryBuildOpen] = useState(false); |
| 574 |
const [featuredImagePreview, setFeaturedImagePreview] = useState<string>(""); |
| 575 |
const [isResolvingFeaturedImage, setIsResolvingFeaturedImage] = |
| 576 |
useState<boolean>(false); |
| 577 |
const featuredImageCache = useRef<Record<number, string>>({}); |
| 578 |
const mediaBaseUrl = useMemo(() => { |
| 579 |
const apiUrl = (window as any)?.yatraAdmin?.apiUrl; |
| 580 |
return apiUrl ? apiUrl.replace(/\/yatra\/v1\/?$/, "") : ""; |
| 581 |
}, []); |
| 582 |
|
| 583 |
// Get section from URL on initial load |
| 584 |
const getInitialSection = (): SectionId => { |
| 585 |
const urlParams = new URLSearchParams(window.location.search); |
| 586 |
const sectionFromUrl = urlParams.get("section") as SectionId | null; |
| 587 |
const validSections: SectionId[] = [ |
| 588 |
"basic", |
| 589 |
"location", |
| 590 |
"duration", |
| 591 |
"pricing", |
| 592 |
"booking", |
| 593 |
"attributes", |
| 594 |
"itinerary", |
| 595 |
"included", |
| 596 |
"media", |
| 597 |
"downloads", |
| 598 |
"categorization", |
| 599 |
"faqs", |
| 600 |
"seo", |
| 601 |
"advanced", |
| 602 |
]; |
| 603 |
if (sectionFromUrl && validSections.includes(sectionFromUrl)) { |
| 604 |
return sectionFromUrl; |
| 605 |
} |
| 606 |
return "basic"; |
| 607 |
}; |
| 608 |
|
| 609 |
const [currentSection, setCurrentSection] = |
| 610 |
useState<SectionId>(getInitialSection); |
| 611 |
const [visitedSections, setVisitedSections] = useState<Set<SectionId>>( |
| 612 |
() => new Set([getInitialSection()]), |
| 613 |
); |
| 614 |
const [showCategorySelector, setShowCategorySelector] = useState(false); |
| 615 |
const [showLandmarkDialog, setShowLandmarkDialog] = useState(false); |
| 616 |
const [landmarkInput, setLandmarkInput] = useState(""); |
| 617 |
|
| 618 |
// Sub-tab state for Trip Details section (itinerary) |
| 619 |
const [tripDetailsTab, setTripDetailsTab] = useState< |
| 620 |
"itinerary" | "included" |
| 621 |
>("itinerary"); |
| 622 |
|
| 623 |
// Track visited sections for lazy loading and update URL |
| 624 |
useEffect(() => { |
| 625 |
setVisitedSections((prev) => new Set([...prev, currentSection])); |
| 626 |
|
| 627 |
// Update URL with current section (without page reload) |
| 628 |
const url = new URL(window.location.href); |
| 629 |
url.searchParams.set("section", currentSection); |
| 630 |
window.history.replaceState({}, "", url.toString()); |
| 631 |
}, [currentSection]); |
| 632 |
|
| 633 |
// Modal states for adding items |
| 634 |
const [showHighlightModal, setShowHighlightModal] = useState(false); |
| 635 |
const [modalInput, setModalInput] = useState({ |
| 636 |
text: "", |
| 637 |
question: "", |
| 638 |
answer: "", |
| 639 |
}); |
| 640 |
|
| 641 |
// Revision states |
| 642 |
const [showRevisionsDialog, setShowRevisionsDialog] = useState(false); |
| 643 |
const [selectedRevisionId, setSelectedRevisionId] = useState<number | null>( |
| 644 |
null, |
| 645 |
); |
| 646 |
const [showRevisionConfirm, setShowRevisionConfirm] = useState(false); |
| 647 |
|
| 648 |
// UI Enhancement states |
| 649 |
const [simpleMode, setSimpleMode] = useState(false); // Quick Start mode |
| 650 |
const [showSlugPreview, setShowSlugPreview] = useState(true); |
| 651 |
const [dummyDataIndex, setDummyDataIndex] = useState(0); // Track which dummy data set to use |
| 652 |
|
| 653 |
// Static/dummy revisions data for UI only |
| 654 |
const dummyRevisions = [ |
| 655 |
{ |
| 656 |
id: 1, |
| 657 |
version: 3, |
| 658 |
created_by_name: "Admin User", |
| 659 |
created_at: new Date().toISOString(), |
| 660 |
}, |
| 661 |
{ |
| 662 |
id: 2, |
| 663 |
version: 2, |
| 664 |
created_by_name: "Admin User", |
| 665 |
created_at: new Date(Date.now() - 86400000).toISOString(), |
| 666 |
}, |
| 667 |
{ |
| 668 |
id: 3, |
| 669 |
version: 1, |
| 670 |
created_by_name: "Admin User", |
| 671 |
created_at: new Date(Date.now() - 172800000).toISOString(), |
| 672 |
}, |
| 673 |
]; |
| 674 |
|
| 675 |
// Comprehensive dummy trip data sets (3 different trips) |
| 676 |
const dummyTripsData: TripFormData[] = [ |
| 677 |
{ |
| 678 |
// Trip 1: Beach Adventure |
| 679 |
title: "7-Day Bali Beach Adventure", |
| 680 |
slug: "7-day-bali-beach-adventure", |
| 681 |
description: |
| 682 |
"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.", |
| 683 |
short_description: |
| 684 |
"Escape to paradise with our 7-day Bali beach adventure featuring pristine beaches, ancient temples, and cultural immersion.", |
| 685 |
highlights: [ |
| 686 |
"Pristine white sand beaches", |
| 687 |
"Ancient temple visits", |
| 688 |
"Traditional spa treatments", |
| 689 |
"Cultural dance performances", |
| 690 |
"Sunset dinners by the ocean", |
| 691 |
], |
| 692 |
trip_details: |
| 693 |
"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.", |
| 694 |
what_makes_special: |
| 695 |
"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.", |
| 696 |
trip_story: |
| 697 |
"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.", |
| 698 |
video_url: "https://www.youtube.com/watch?v=example1", |
| 699 |
virtual_tour_url: "", |
| 700 |
testimonial_review_ids: [], // Will be populated from actual reviews |
| 701 |
destinations: [], // Will be populated based on available destinations |
| 702 |
starting_location: "Ngurah Rai International Airport (DPS)", |
| 703 |
ending_location: "Seminyak Beach Resort", |
| 704 |
countries: ["Indonesia"], |
| 705 |
regions: ["Bali"], |
| 706 |
starting_latitude: "-8.3405", |
| 707 |
starting_longitude: "115.0920", |
| 708 |
ending_latitude: "-8.5069", |
| 709 |
ending_longitude: "115.2625", |
| 710 |
landmarks: [ |
| 711 |
"Tanah Lot Temple", |
| 712 |
"Ubud Monkey Forest", |
| 713 |
"Tegallalang Rice Terrace", |
| 714 |
"Seminyak Beach", |
| 715 |
], |
| 716 |
trip_type: "multi_day", |
| 717 |
duration_days: "7", |
| 718 |
duration_nights: "6", |
| 719 |
available_from: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) |
| 720 |
.toISOString() |
| 721 |
.split("T")[0], // 30 days from now |
| 722 |
available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) |
| 723 |
.toISOString() |
| 724 |
.split("T")[0], // 1 year from now |
| 725 |
booking_window_days: "30", |
| 726 |
seasonal_availability: "Year-round", |
| 727 |
best_season: "April to October", |
| 728 |
peak_season: "July to August", |
| 729 |
off_season: "November to March", |
| 730 |
activity_types: [], // Will be populated based on available activities |
| 731 |
difficulty_level: "", |
| 732 |
trip_category: [], |
| 733 |
tags: ["family-friendly", "beach", "relaxation", "cultural", "spa"], |
| 734 |
featured_priority: "featured", |
| 735 |
accommodation_type: "Resort", |
| 736 |
meal_plan: "breakfast", |
| 737 |
accommodation_details: |
| 738 |
"4-star beachfront resort with private balconies, infinity pool, and spa facilities", |
| 739 |
transportation_included: true, |
| 740 |
pickup_location: "Ngurah Rai International Airport", |
| 741 |
dropoff_location: "Seminyak Beach Resort", |
| 742 |
transportation_details: |
| 743 |
"Private air-conditioned vehicle with professional driver", |
| 744 |
pricing_type: "traveler_based", |
| 745 |
original_price: "", |
| 746 |
discounted_price: "", |
| 747 |
price_types: [ |
| 748 |
{ category_id: 1, original_price: "1250", discounted_price: "" }, |
| 749 |
{ category_id: 2, original_price: "625", discounted_price: "" }, |
| 750 |
{ category_id: 3, original_price: "0", discounted_price: "" }, |
| 751 |
], |
| 752 |
deposit_amount: "300", |
| 753 |
deposit_percentage: "", |
| 754 |
payment_terms: |
| 755 |
"50% deposit required at booking, remaining 50% due 30 days before departure", |
| 756 |
max_travelers: "12", |
| 757 |
min_travelers: "2", |
| 758 |
booking_deadline_hours: "24", |
| 759 |
cancellation_policy: |
| 760 |
"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.", |
| 761 |
age_min: "8", |
| 762 |
age_max: "", |
| 763 |
physical_requirements: |
| 764 |
"Moderate fitness level required. Some walking involved but no strenuous activities.", |
| 765 |
visa_requirements: |
| 766 |
"Visa on arrival available for most nationalities. Valid passport required with at least 6 months validity.", |
| 767 |
vaccination_requirements: |
| 768 |
"No mandatory vaccinations. Recommended: Hepatitis A, Typhoid, and routine vaccinations.", |
| 769 |
disable_booking: false, |
| 770 |
has_default_time_slots: false, |
| 771 |
default_time_slots: [], |
| 772 |
departure_time: "", |
| 773 |
included_items: [ |
| 774 |
{ |
| 775 |
title: "Accommodation", |
| 776 |
description: "6 nights at 4-star beachfront resort", |
| 777 |
}, |
| 778 |
{ title: "Breakfast", description: "Daily breakfast included" }, |
| 779 |
{ |
| 780 |
title: "Airport transfers", |
| 781 |
description: "Private transfers to and from airport", |
| 782 |
}, |
| 783 |
{ |
| 784 |
title: "Temple visits", |
| 785 |
description: "Entrance fees to all temples included", |
| 786 |
}, |
| 787 |
{ |
| 788 |
title: "Cultural performances", |
| 789 |
description: "Traditional dance show tickets", |
| 790 |
}, |
| 791 |
{ |
| 792 |
title: "Professional guide", |
| 793 |
description: "English-speaking local guide", |
| 794 |
}, |
| 795 |
], |
| 796 |
excluded_items: [ |
| 797 |
{ |
| 798 |
title: "International flights", |
| 799 |
description: "Flights to and from Bali not included", |
| 800 |
}, |
| 801 |
{ |
| 802 |
title: "Lunch and dinner", |
| 803 |
description: "Meals other than breakfast", |
| 804 |
}, |
| 805 |
{ |
| 806 |
title: "Travel insurance", |
| 807 |
description: "Travel insurance recommended but not included", |
| 808 |
}, |
| 809 |
{ |
| 810 |
title: "Personal expenses", |
| 811 |
description: "Souvenirs, tips, and personal items", |
| 812 |
}, |
| 813 |
], |
| 814 |
itinerary_days: [], |
| 815 |
gallery_images: [], |
| 816 |
featured_image: null, |
| 817 |
downloadable_items: [], |
| 818 |
faqs: [ |
| 819 |
{ |
| 820 |
question: "What is the best time to visit Bali?", |
| 821 |
answer: |
| 822 |
"The best time to visit Bali is during the dry season from April to October, when you can expect sunny days and minimal rainfall.", |
| 823 |
}, |
| 824 |
{ |
| 825 |
question: "Do I need a visa?", |
| 826 |
answer: |
| 827 |
"Most nationalities can get a visa on arrival at the airport. A valid passport with at least 6 months validity is required.", |
| 828 |
}, |
| 829 |
{ |
| 830 |
question: "What should I pack?", |
| 831 |
answer: |
| 832 |
"Pack light, breathable clothing, swimwear, sunscreen, insect repellent, and comfortable walking shoes. Modest clothing is required for temple visits.", |
| 833 |
}, |
| 834 |
], |
| 835 |
frontend_tabs: [ |
| 836 |
{ |
| 837 |
id: "overview", |
| 838 |
label: "Overview", |
| 839 |
enabled: true, |
| 840 |
order: 1, |
| 841 |
content_type: "general", |
| 842 |
icon: { type: "icon", value: "book-open" }, |
| 843 |
}, |
| 844 |
{ |
| 845 |
id: "itinerary", |
| 846 |
label: "Itinerary", |
| 847 |
enabled: true, |
| 848 |
order: 2, |
| 849 |
content_type: "itinerary", |
| 850 |
icon: { type: "icon", value: "calendar" }, |
| 851 |
}, |
| 852 |
{ |
| 853 |
id: "included", |
| 854 |
label: "Included", |
| 855 |
enabled: true, |
| 856 |
order: 3, |
| 857 |
content_type: "included_excluded", |
| 858 |
icon: { type: "icon", value: "check" }, |
| 859 |
}, |
| 860 |
{ |
| 861 |
id: "location", |
| 862 |
label: "Location", |
| 863 |
enabled: true, |
| 864 |
order: 4, |
| 865 |
content_type: "gallery", |
| 866 |
icon: { type: "icon", value: "map-pin" }, |
| 867 |
}, |
| 868 |
{ |
| 869 |
id: "important_info", |
| 870 |
label: "Important Info", |
| 871 |
enabled: true, |
| 872 |
order: 5, |
| 873 |
content_type: "general", |
| 874 |
icon: { type: "icon", value: "file-text" }, |
| 875 |
}, |
| 876 |
{ |
| 877 |
id: "downloads", |
| 878 |
label: "Downloads", |
| 879 |
enabled: true, |
| 880 |
order: 6, |
| 881 |
content_type: "downloads", |
| 882 |
icon: { type: "icon", value: "download" }, |
| 883 |
}, |
| 884 |
{ |
| 885 |
id: "faq", |
| 886 |
label: "FAQ", |
| 887 |
enabled: true, |
| 888 |
order: 7, |
| 889 |
content_type: "faqs", |
| 890 |
icon: { type: "icon", value: "help-circle" }, |
| 891 |
}, |
| 892 |
{ |
| 893 |
id: "trip_story", |
| 894 |
label: "Story", |
| 895 |
enabled: true, |
| 896 |
order: 8, |
| 897 |
content_type: "custom", |
| 898 |
custom_content: "", |
| 899 |
icon: { type: "icon", value: "book" }, |
| 900 |
}, |
| 901 |
{ |
| 902 |
id: "what_makes_special", |
| 903 |
label: "Special", |
| 904 |
enabled: true, |
| 905 |
order: 9, |
| 906 |
content_type: "custom", |
| 907 |
custom_content: "", |
| 908 |
icon: { type: "icon", value: "star" }, |
| 909 |
}, |
| 910 |
{ |
| 911 |
id: "testimonials", |
| 912 |
label: "Testimonials", |
| 913 |
enabled: true, |
| 914 |
order: 10, |
| 915 |
content_type: "reviews", |
| 916 |
icon: { type: "icon", value: "message-circle" }, |
| 917 |
}, |
| 918 |
], |
| 919 |
availability_dates: [], |
| 920 |
status: "draft", |
| 921 |
scheduled_publish_date: "", |
| 922 |
scheduled_unpublish_date: "", |
| 923 |
version: 1, |
| 924 |
seasonal_auto_enable: false, |
| 925 |
seasonal_enable_date: "", |
| 926 |
seasonal_disable_date: "", |
| 927 |
meta_title: "7-Day Bali Beach Adventure | Luxury Beach Resort Experience", |
| 928 |
meta_description: |
| 929 |
"Experience the best of Bali with our 7-day beach adventure. Pristine beaches, ancient temples, cultural immersion, and world-class spa treatments await.", |
| 930 |
meta_keywords: |
| 931 |
"Bali, beach vacation, cultural tour, spa retreat, Indonesia travel", |
| 932 |
attributes: {}, |
| 933 |
}, |
| 934 |
{ |
| 935 |
// Trip 2: Mountain Trekking Adventure |
| 936 |
title: "Everest Base Camp Trek - 14 Days", |
| 937 |
slug: "everest-base-camp-trek-14-days", |
| 938 |
description: |
| 939 |
"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.", |
| 940 |
short_description: |
| 941 |
"Embark on the adventure of a lifetime with our 14-day Everest Base Camp trek through the heart of the Himalayas.", |
| 942 |
highlights: [ |
| 943 |
"Trek to Everest Base Camp (5,364m)", |
| 944 |
"Visit ancient Buddhist monasteries", |
| 945 |
"Experience Sherpa culture", |
| 946 |
"Breathtaking mountain views", |
| 947 |
"Professional mountain guides", |
| 948 |
], |
| 949 |
trip_details: |
| 950 |
"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.", |
| 951 |
what_makes_special: |
| 952 |
"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.", |
| 953 |
trip_story: |
| 954 |
"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.", |
| 955 |
video_url: "https://www.youtube.com/watch?v=example2", |
| 956 |
virtual_tour_url: "", |
| 957 |
testimonial_review_ids: [], // Will be populated from actual reviews |
| 958 |
destinations: [], // Will be populated |
| 959 |
starting_location: "Kathmandu International Airport", |
| 960 |
ending_location: "Lukla Airport", |
| 961 |
countries: ["Nepal"], |
| 962 |
regions: ["Khumbu Region"], |
| 963 |
starting_latitude: "27.9881", |
| 964 |
starting_longitude: "86.9250", |
| 965 |
ending_latitude: "27.6837", |
| 966 |
ending_longitude: "86.7330", |
| 967 |
landmarks: [ |
| 968 |
"Mount Everest", |
| 969 |
"Namche Bazaar", |
| 970 |
"Tengboche Monastery", |
| 971 |
"Kala Patthar", |
| 972 |
], |
| 973 |
trip_type: "multi_day", |
| 974 |
duration_days: "14", |
| 975 |
duration_nights: "13", |
| 976 |
available_from: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) |
| 977 |
.toISOString() |
| 978 |
.split("T")[0], |
| 979 |
available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) |
| 980 |
.toISOString() |
| 981 |
.split("T")[0], |
| 982 |
booking_window_days: "60", |
| 983 |
seasonal_availability: "March to May, September to November", |
| 984 |
best_season: "October to November", |
| 985 |
peak_season: "October to November", |
| 986 |
off_season: "December to February, June to August", |
| 987 |
activity_types: [], |
| 988 |
difficulty_level: "", |
| 989 |
trip_category: [], |
| 990 |
tags: ["trekking", "mountains", "adventure", "challenging", "everest"], |
| 991 |
featured_priority: "featured", |
| 992 |
accommodation_type: "Teahouse", |
| 993 |
meal_plan: "full_board", |
| 994 |
accommodation_details: |
| 995 |
"Traditional teahouses along the route with basic amenities. Rooms are shared, and facilities become more basic as altitude increases.", |
| 996 |
transportation_included: true, |
| 997 |
pickup_location: "Kathmandu International Airport", |
| 998 |
dropoff_location: "Lukla Airport", |
| 999 |
transportation_details: |
| 1000 |
"Domestic flights Kathmandu-Lukla-Kathmandu included. Airport transfers included.", |
| 1001 |
pricing_type: "regular", |
| 1002 |
original_price: "1899", |
| 1003 |
discounted_price: "1699", |
| 1004 |
deposit_amount: "500", |
| 1005 |
deposit_percentage: "", |
| 1006 |
payment_terms: |
| 1007 |
"50% deposit required at booking, remaining 50% due 60 days before departure", |
| 1008 |
max_travelers: "12", |
| 1009 |
min_travelers: "2", |
| 1010 |
booking_deadline_hours: "24", |
| 1011 |
cancellation_policy: |
| 1012 |
"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.", |
| 1013 |
age_min: "18", |
| 1014 |
age_max: "65", |
| 1015 |
physical_requirements: |
| 1016 |
"Excellent physical fitness required. Previous trekking experience recommended. Must be able to walk 6-8 hours daily at high altitude.", |
| 1017 |
visa_requirements: |
| 1018 |
"Tourist visa required for Nepal. Can be obtained on arrival at airport or in advance. Valid passport required.", |
| 1019 |
vaccination_requirements: |
| 1020 |
"Recommended: Hepatitis A, Typhoid, Japanese Encephalitis, and routine vaccinations. Consult with travel health clinic.", |
| 1021 |
disable_booking: false, |
| 1022 |
has_default_time_slots: false, |
| 1023 |
default_time_slots: [], |
| 1024 |
departure_time: "", |
| 1025 |
included_items: [ |
| 1026 |
{ |
| 1027 |
title: "Accommodation", |
| 1028 |
description: "13 nights in teahouses along the route", |
| 1029 |
}, |
| 1030 |
{ |
| 1031 |
title: "All meals", |
| 1032 |
description: "Breakfast, lunch, and dinner included", |
| 1033 |
}, |
| 1034 |
{ |
| 1035 |
title: "Professional guides", |
| 1036 |
description: "Experienced mountain guides and porters", |
| 1037 |
}, |
| 1038 |
{ |
| 1039 |
title: "Permits", |
| 1040 |
description: "TIMS and Sagarmatha National Park permits", |
| 1041 |
}, |
| 1042 |
{ |
| 1043 |
title: "Domestic flights", |
| 1044 |
description: "Kathmandu-Lukla-Kathmandu flights", |
| 1045 |
}, |
| 1046 |
{ |
| 1047 |
title: "Equipment", |
| 1048 |
description: "Sleeping bag and down jacket rental", |
| 1049 |
}, |
| 1050 |
], |
| 1051 |
excluded_items: [ |
| 1052 |
{ |
| 1053 |
title: "International flights", |
| 1054 |
description: "Flights to and from Kathmandu", |
| 1055 |
}, |
| 1056 |
{ |
| 1057 |
title: "Travel insurance", |
| 1058 |
description: "Comprehensive travel and medical insurance required", |
| 1059 |
}, |
| 1060 |
{ |
| 1061 |
title: "Personal equipment", |
| 1062 |
description: "Trekking boots, clothing, and personal items", |
| 1063 |
}, |
| 1064 |
{ |
| 1065 |
title: "Tips", |
| 1066 |
description: "Tips for guides and porters (recommended)", |
| 1067 |
}, |
| 1068 |
{ |
| 1069 |
title: "Personal expenses", |
| 1070 |
description: "Drinks, snacks, and souvenirs", |
| 1071 |
}, |
| 1072 |
], |
| 1073 |
itinerary_days: [], |
| 1074 |
gallery_images: [], |
| 1075 |
featured_image: null, |
| 1076 |
downloadable_items: [], |
| 1077 |
faqs: [ |
| 1078 |
{ |
| 1079 |
question: "How difficult is the trek?", |
| 1080 |
answer: |
| 1081 |
"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.", |
| 1082 |
}, |
| 1083 |
{ |
| 1084 |
question: "What is the altitude at Base Camp?", |
| 1085 |
answer: |
| 1086 |
"Everest Base Camp is located at 5,364 meters (17,598 feet) above sea level.", |
| 1087 |
}, |
| 1088 |
{ |
| 1089 |
question: "What happens if I get altitude sickness?", |
| 1090 |
answer: |
| 1091 |
"Our guides are trained to recognize and treat altitude sickness. We have proper acclimatization schedules and emergency descent plans in place.", |
| 1092 |
}, |
| 1093 |
], |
| 1094 |
frontend_tabs: [ |
| 1095 |
{ |
| 1096 |
id: "overview", |
| 1097 |
label: "Overview", |
| 1098 |
enabled: true, |
| 1099 |
order: 1, |
| 1100 |
content_type: "general", |
| 1101 |
icon: { type: "icon", value: "book-open" }, |
| 1102 |
}, |
| 1103 |
{ |
| 1104 |
id: "itinerary", |
| 1105 |
label: "Itinerary", |
| 1106 |
enabled: true, |
| 1107 |
order: 2, |
| 1108 |
content_type: "itinerary", |
| 1109 |
icon: { type: "icon", value: "calendar" }, |
| 1110 |
}, |
| 1111 |
{ |
| 1112 |
id: "included", |
| 1113 |
label: "Included", |
| 1114 |
enabled: true, |
| 1115 |
order: 3, |
| 1116 |
content_type: "included_excluded", |
| 1117 |
icon: { type: "icon", value: "check" }, |
| 1118 |
}, |
| 1119 |
{ |
| 1120 |
id: "location", |
| 1121 |
label: "Location", |
| 1122 |
enabled: true, |
| 1123 |
order: 4, |
| 1124 |
content_type: "gallery", |
| 1125 |
icon: { type: "icon", value: "map-pin" }, |
| 1126 |
}, |
| 1127 |
{ |
| 1128 |
id: "important_info", |
| 1129 |
label: "Important Info", |
| 1130 |
enabled: true, |
| 1131 |
order: 5, |
| 1132 |
content_type: "general", |
| 1133 |
icon: { type: "icon", value: "info" }, |
| 1134 |
}, |
| 1135 |
{ |
| 1136 |
id: "downloads", |
| 1137 |
label: "Downloads", |
| 1138 |
enabled: true, |
| 1139 |
order: 6, |
| 1140 |
content_type: "downloads", |
| 1141 |
icon: { type: "icon", value: "download" }, |
| 1142 |
}, |
| 1143 |
{ |
| 1144 |
id: "faq", |
| 1145 |
label: "FAQ", |
| 1146 |
enabled: true, |
| 1147 |
order: 7, |
| 1148 |
content_type: "faqs", |
| 1149 |
icon: { type: "icon", value: "help-circle" }, |
| 1150 |
}, |
| 1151 |
{ |
| 1152 |
id: "trip_story", |
| 1153 |
label: "Story", |
| 1154 |
enabled: true, |
| 1155 |
order: 8, |
| 1156 |
content_type: "custom", |
| 1157 |
custom_content: "", |
| 1158 |
icon: { type: "icon", value: "book" }, |
| 1159 |
}, |
| 1160 |
{ |
| 1161 |
id: "what_makes_special", |
| 1162 |
label: "Special", |
| 1163 |
enabled: true, |
| 1164 |
order: 9, |
| 1165 |
content_type: "custom", |
| 1166 |
custom_content: "", |
| 1167 |
icon: { type: "icon", value: "star" }, |
| 1168 |
}, |
| 1169 |
{ |
| 1170 |
id: "testimonials", |
| 1171 |
label: "Testimonials", |
| 1172 |
enabled: true, |
| 1173 |
order: 10, |
| 1174 |
content_type: "reviews", |
| 1175 |
icon: { type: "icon", value: "message-circle" }, |
| 1176 |
}, |
| 1177 |
], |
| 1178 |
availability_dates: [], |
| 1179 |
status: "draft", |
| 1180 |
scheduled_publish_date: "", |
| 1181 |
scheduled_unpublish_date: "", |
| 1182 |
version: 1, |
| 1183 |
seasonal_auto_enable: false, |
| 1184 |
seasonal_enable_date: "", |
| 1185 |
seasonal_disable_date: "", |
| 1186 |
meta_title: |
| 1187 |
"Everest Base Camp Trek - 14 Days | Ultimate Himalayan Adventure", |
| 1188 |
meta_description: |
| 1189 |
"Embark on the adventure of a lifetime with our 14-day Everest Base Camp trek. Experience Sherpa culture, ancient monasteries, and breathtaking mountain views.", |
| 1190 |
meta_keywords: |
| 1191 |
"Everest Base Camp, trekking, Nepal, Himalayas, adventure travel, mountain trek", |
| 1192 |
attributes: {}, |
| 1193 |
price_types: [], |
| 1194 |
}, |
| 1195 |
{ |
| 1196 |
// Trip 3: European City Tour |
| 1197 |
title: "European Grand Tour - 10 Days", |
| 1198 |
slug: "european-grand-tour-10-days", |
| 1199 |
description: |
| 1200 |
"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.", |
| 1201 |
short_description: |
| 1202 |
"Discover the best of Europe with our 10-day grand tour covering Paris, Rome, and Barcelona.", |
| 1203 |
highlights: [ |
| 1204 |
"Eiffel Tower and Louvre Museum", |
| 1205 |
"Colosseum and Vatican City", |
| 1206 |
"Sagrada Familia and Park Güell", |
| 1207 |
"World-class cuisine", |
| 1208 |
"Professional local guides", |
| 1209 |
], |
| 1210 |
trip_details: |
| 1211 |
"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.", |
| 1212 |
what_makes_special: |
| 1213 |
"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.", |
| 1214 |
trip_story: |
| 1215 |
"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.", |
| 1216 |
video_url: "https://www.youtube.com/watch?v=example3", |
| 1217 |
virtual_tour_url: "", |
| 1218 |
testimonial_review_ids: [], // Will be populated from actual reviews |
| 1219 |
destinations: [], |
| 1220 |
starting_location: "Charles de Gaulle Airport (CDG)", |
| 1221 |
ending_location: "El Prat Airport (BCN)", |
| 1222 |
countries: ["France", "Italy", "Spain"], |
| 1223 |
regions: ["Île-de-France", "Lazio", "Catalonia"], |
| 1224 |
starting_latitude: "48.8566", |
| 1225 |
starting_longitude: "2.3522", |
| 1226 |
ending_latitude: "41.3792", |
| 1227 |
ending_longitude: "2.1281", |
| 1228 |
landmarks: [ |
| 1229 |
"Eiffel Tower", |
| 1230 |
"Colosseum", |
| 1231 |
"Sagrada Familia", |
| 1232 |
"Louvre Museum", |
| 1233 |
"Vatican City", |
| 1234 |
], |
| 1235 |
trip_type: "multi_day", |
| 1236 |
duration_days: "10", |
| 1237 |
duration_nights: "9", |
| 1238 |
available_from: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000) |
| 1239 |
.toISOString() |
| 1240 |
.split("T")[0], |
| 1241 |
available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) |
| 1242 |
.toISOString() |
| 1243 |
.split("T")[0], |
| 1244 |
booking_window_days: "45", |
| 1245 |
seasonal_availability: "Year-round", |
| 1246 |
best_season: "April to June, September to October", |
| 1247 |
peak_season: "June to August", |
| 1248 |
off_season: "November to March", |
| 1249 |
activity_types: [], |
| 1250 |
difficulty_level: "", |
| 1251 |
trip_category: [], |
| 1252 |
tags: ["cultural", "city-tour", "history", "art", "food"], |
| 1253 |
featured_priority: "featured", |
| 1254 |
accommodation_type: "Hotel", |
| 1255 |
meal_plan: "breakfast", |
| 1256 |
accommodation_details: |
| 1257 |
"4-star hotels in city centers with easy access to major attractions", |
| 1258 |
transportation_included: true, |
| 1259 |
pickup_location: "Charles de Gaulle Airport", |
| 1260 |
dropoff_location: "El Prat Airport", |
| 1261 |
transportation_details: |
| 1262 |
"High-speed train between cities, private transfers, and metro passes included", |
| 1263 |
pricing_type: "regular", |
| 1264 |
original_price: "2499", |
| 1265 |
discounted_price: "", |
| 1266 |
price_types: [], |
| 1267 |
deposit_amount: "600", |
| 1268 |
deposit_percentage: "", |
| 1269 |
payment_terms: |
| 1270 |
"40% deposit required at booking, remaining 60% due 45 days before departure", |
| 1271 |
max_travelers: "20", |
| 1272 |
min_travelers: "4", |
| 1273 |
booking_deadline_hours: "24", |
| 1274 |
cancellation_policy: |
| 1275 |
"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.", |
| 1276 |
age_min: "", |
| 1277 |
age_max: "", |
| 1278 |
physical_requirements: |
| 1279 |
"Moderate walking required. Some sites involve stairs and uneven surfaces. Suitable for most fitness levels.", |
| 1280 |
visa_requirements: |
| 1281 |
"Schengen visa required for most non-EU nationals. Apply well in advance as processing can take several weeks.", |
| 1282 |
vaccination_requirements: |
| 1283 |
"No mandatory vaccinations. Routine vaccinations recommended.", |
| 1284 |
disable_booking: false, |
| 1285 |
has_default_time_slots: false, |
| 1286 |
default_time_slots: [], |
| 1287 |
departure_time: "", |
| 1288 |
included_items: [ |
| 1289 |
{ |
| 1290 |
title: "Accommodation", |
| 1291 |
description: "9 nights in 4-star city center hotels", |
| 1292 |
}, |
| 1293 |
{ title: "Breakfast", description: "Daily breakfast included" }, |
| 1294 |
{ |
| 1295 |
title: "Transportation", |
| 1296 |
description: |
| 1297 |
"High-speed trains, airport transfers, and city metro passes", |
| 1298 |
}, |
| 1299 |
{ |
| 1300 |
title: "Guided tours", |
| 1301 |
description: "Professional local guides in each city", |
| 1302 |
}, |
| 1303 |
{ |
| 1304 |
title: "Skip-the-line tickets", |
| 1305 |
description: "Priority access to major attractions", |
| 1306 |
}, |
| 1307 |
{ |
| 1308 |
title: "Welcome dinner", |
| 1309 |
description: "Traditional welcome dinner in Paris", |
| 1310 |
}, |
| 1311 |
], |
| 1312 |
excluded_items: [ |
| 1313 |
{ |
| 1314 |
title: "International flights", |
| 1315 |
description: "Flights to Paris and from Barcelona", |
| 1316 |
}, |
| 1317 |
{ |
| 1318 |
title: "Lunch and dinner", |
| 1319 |
description: "Meals other than breakfast and welcome dinner", |
| 1320 |
}, |
| 1321 |
{ |
| 1322 |
title: "Travel insurance", |
| 1323 |
description: "Travel insurance recommended", |
| 1324 |
}, |
| 1325 |
{ |
| 1326 |
title: "Personal expenses", |
| 1327 |
description: "Souvenirs, tips, and personal items", |
| 1328 |
}, |
| 1329 |
], |
| 1330 |
itinerary_days: [], |
| 1331 |
gallery_images: [], |
| 1332 |
featured_image: null, |
| 1333 |
downloadable_items: [], |
| 1334 |
faqs: [ |
| 1335 |
{ |
| 1336 |
question: "Do I need a visa?", |
| 1337 |
answer: |
| 1338 |
"Most non-EU nationals need a Schengen visa. Apply at the embassy of your first entry country (France) well in advance.", |
| 1339 |
}, |
| 1340 |
{ |
| 1341 |
question: "What languages are spoken?", |
| 1342 |
answer: |
| 1343 |
"English-speaking guides provided. Local languages are French, Italian, and Spanish, but English is widely spoken in tourist areas.", |
| 1344 |
}, |
| 1345 |
{ |
| 1346 |
question: "Is this suitable for families?", |
| 1347 |
answer: |
| 1348 |
"Yes, this tour is family-friendly. However, some museums and sites may have age restrictions for children.", |
| 1349 |
}, |
| 1350 |
], |
| 1351 |
frontend_tabs: [ |
| 1352 |
{ |
| 1353 |
id: "overview", |
| 1354 |
label: "Overview", |
| 1355 |
enabled: true, |
| 1356 |
order: 1, |
| 1357 |
content_type: "general", |
| 1358 |
icon: { type: "icon", value: "book-open" }, |
| 1359 |
}, |
| 1360 |
{ |
| 1361 |
id: "itinerary", |
| 1362 |
label: "Itinerary", |
| 1363 |
enabled: true, |
| 1364 |
order: 2, |
| 1365 |
content_type: "itinerary", |
| 1366 |
icon: { type: "icon", value: "calendar" }, |
| 1367 |
}, |
| 1368 |
{ |
| 1369 |
id: "included", |
| 1370 |
label: "Included", |
| 1371 |
enabled: true, |
| 1372 |
order: 3, |
| 1373 |
content_type: "included_excluded", |
| 1374 |
icon: { type: "icon", value: "check" }, |
| 1375 |
}, |
| 1376 |
{ |
| 1377 |
id: "location", |
| 1378 |
label: "Location", |
| 1379 |
enabled: true, |
| 1380 |
order: 4, |
| 1381 |
content_type: "gallery", |
| 1382 |
icon: { type: "icon", value: "map-pin" }, |
| 1383 |
}, |
| 1384 |
{ |
| 1385 |
id: "important_info", |
| 1386 |
label: "Important Info", |
| 1387 |
enabled: true, |
| 1388 |
order: 5, |
| 1389 |
content_type: "general", |
| 1390 |
icon: { type: "icon", value: "info" }, |
| 1391 |
}, |
| 1392 |
{ |
| 1393 |
id: "downloads", |
| 1394 |
label: "Downloads", |
| 1395 |
enabled: true, |
| 1396 |
order: 6, |
| 1397 |
content_type: "downloads", |
| 1398 |
icon: { type: "icon", value: "download" }, |
| 1399 |
}, |
| 1400 |
{ |
| 1401 |
id: "faq", |
| 1402 |
label: "FAQ", |
| 1403 |
enabled: true, |
| 1404 |
order: 7, |
| 1405 |
content_type: "faqs", |
| 1406 |
icon: { type: "icon", value: "help-circle" }, |
| 1407 |
}, |
| 1408 |
{ |
| 1409 |
id: "trip_story", |
| 1410 |
label: "Story", |
| 1411 |
enabled: true, |
| 1412 |
order: 8, |
| 1413 |
content_type: "custom", |
| 1414 |
custom_content: "", |
| 1415 |
icon: { type: "icon", value: "book" }, |
| 1416 |
}, |
| 1417 |
{ |
| 1418 |
id: "what_makes_special", |
| 1419 |
label: "Special", |
| 1420 |
enabled: true, |
| 1421 |
order: 9, |
| 1422 |
content_type: "custom", |
| 1423 |
custom_content: "", |
| 1424 |
icon: { type: "icon", value: "star" }, |
| 1425 |
}, |
| 1426 |
{ |
| 1427 |
id: "testimonials", |
| 1428 |
label: "Testimonials", |
| 1429 |
enabled: true, |
| 1430 |
order: 10, |
| 1431 |
content_type: "reviews", |
| 1432 |
icon: { type: "icon", value: "message-circle" }, |
| 1433 |
}, |
| 1434 |
], |
| 1435 |
availability_dates: [], |
| 1436 |
status: "draft", |
| 1437 |
scheduled_publish_date: "", |
| 1438 |
scheduled_unpublish_date: "", |
| 1439 |
version: 1, |
| 1440 |
seasonal_auto_enable: false, |
| 1441 |
seasonal_enable_date: "", |
| 1442 |
seasonal_disable_date: "", |
| 1443 |
meta_title: "European Grand Tour - 10 Days | Paris, Rome & Barcelona", |
| 1444 |
meta_description: |
| 1445 |
"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.", |
| 1446 |
meta_keywords: |
| 1447 |
"Europe tour, Paris, Rome, Barcelona, European travel, cultural tour", |
| 1448 |
attributes: {}, |
| 1449 |
}, |
| 1450 |
]; |
| 1451 |
|
| 1452 |
const [formData, setFormData] = useState<TripFormData>({ |
| 1453 |
title: "", |
| 1454 |
slug: "", |
| 1455 |
description: "", |
| 1456 |
highlights: [], |
| 1457 |
trip_details: "", |
| 1458 |
short_description: "", |
| 1459 |
what_makes_special: "", |
| 1460 |
trip_story: "", |
| 1461 |
video_url: "", |
| 1462 |
virtual_tour_url: "", |
| 1463 |
testimonial_review_ids: [], |
| 1464 |
destinations: [], // Array of destination IDs |
| 1465 |
starting_location: "", |
| 1466 |
ending_location: "", |
| 1467 |
countries: [], |
| 1468 |
regions: [], |
| 1469 |
starting_latitude: "", |
| 1470 |
starting_longitude: "", |
| 1471 |
ending_latitude: "", |
| 1472 |
ending_longitude: "", |
| 1473 |
landmarks: [], |
| 1474 |
trip_type: "multi_day", |
| 1475 |
duration_days: "", |
| 1476 |
duration_nights: "", |
| 1477 |
available_from: "", |
| 1478 |
available_to: "", |
| 1479 |
booking_window_days: "", |
| 1480 |
seasonal_availability: "", |
| 1481 |
best_season: "", |
| 1482 |
peak_season: "", |
| 1483 |
off_season: "", |
| 1484 |
activity_types: [], // Array of activity IDs |
| 1485 |
difficulty_level: "", |
| 1486 |
trip_category: [], |
| 1487 |
tags: [], |
| 1488 |
featured_priority: "none", |
| 1489 |
accommodation_type: "", |
| 1490 |
meal_plan: "", |
| 1491 |
accommodation_details: "", |
| 1492 |
transportation_included: false, |
| 1493 |
pickup_location: "", |
| 1494 |
dropoff_location: "", |
| 1495 |
transportation_details: "", |
| 1496 |
pricing_type: "regular", |
| 1497 |
original_price: "", |
| 1498 |
discounted_price: "", |
| 1499 |
price_types: [], |
| 1500 |
deposit_amount: "", |
| 1501 |
deposit_percentage: "", |
| 1502 |
payment_terms: "", |
| 1503 |
max_travelers: "10", |
| 1504 |
min_travelers: "2", |
| 1505 |
booking_deadline_hours: "24", |
| 1506 |
cancellation_policy: "Flexible", |
| 1507 |
age_min: "18", |
| 1508 |
age_max: "65", |
| 1509 |
physical_requirements: "Moderate", |
| 1510 |
visa_requirements: "Schengen visa", |
| 1511 |
vaccination_requirements: "COVID-19 vaccination", |
| 1512 |
disable_booking: false, |
| 1513 |
has_default_time_slots: false, |
| 1514 |
default_time_slots: [], |
| 1515 |
departure_time: "09:00", |
| 1516 |
included_items: [], |
| 1517 |
excluded_items: [], |
| 1518 |
attributes: {}, // attribute_id -> value mapping |
| 1519 |
itinerary_days: [], |
| 1520 |
gallery_images: [], |
| 1521 |
featured_image: null, |
| 1522 |
downloadable_items: [], |
| 1523 |
faqs: [], |
| 1524 |
frontend_tabs: [ |
| 1525 |
{ |
| 1526 |
id: "overview", |
| 1527 |
label: "Overview", |
| 1528 |
enabled: true, |
| 1529 |
order: 1, |
| 1530 |
content_type: "general", |
| 1531 |
icon: { type: "icon", value: "book-open" }, |
| 1532 |
}, |
| 1533 |
{ |
| 1534 |
id: "itinerary", |
| 1535 |
label: "Itinerary", |
| 1536 |
enabled: true, |
| 1537 |
order: 2, |
| 1538 |
content_type: "itinerary", |
| 1539 |
icon: { type: "icon", value: "calendar" }, |
| 1540 |
}, |
| 1541 |
{ |
| 1542 |
id: "included", |
| 1543 |
label: "Included", |
| 1544 |
enabled: true, |
| 1545 |
order: 3, |
| 1546 |
content_type: "included_excluded", |
| 1547 |
icon: { type: "icon", value: "check" }, |
| 1548 |
}, |
| 1549 |
{ |
| 1550 |
id: "location", |
| 1551 |
label: "Location", |
| 1552 |
enabled: true, |
| 1553 |
order: 4, |
| 1554 |
content_type: "gallery", |
| 1555 |
icon: { type: "icon", value: "map-pin" }, |
| 1556 |
}, |
| 1557 |
{ |
| 1558 |
id: "important_info", |
| 1559 |
label: "Important Info", |
| 1560 |
enabled: true, |
| 1561 |
order: 5, |
| 1562 |
content_type: "general", |
| 1563 |
icon: { type: "icon", value: "info" }, |
| 1564 |
}, |
| 1565 |
{ |
| 1566 |
id: "downloads", |
| 1567 |
label: "Downloads", |
| 1568 |
enabled: true, |
| 1569 |
order: 6, |
| 1570 |
content_type: "downloads", |
| 1571 |
icon: { type: "icon", value: "download" }, |
| 1572 |
}, |
| 1573 |
{ |
| 1574 |
id: "faq", |
| 1575 |
label: "FAQ", |
| 1576 |
enabled: true, |
| 1577 |
order: 7, |
| 1578 |
content_type: "faqs", |
| 1579 |
icon: { type: "icon", value: "help-circle" }, |
| 1580 |
}, |
| 1581 |
{ |
| 1582 |
id: "trip_story", |
| 1583 |
label: "Story", |
| 1584 |
enabled: true, |
| 1585 |
order: 8, |
| 1586 |
content_type: "custom", |
| 1587 |
custom_content: "", |
| 1588 |
icon: { type: "icon", value: "book" }, |
| 1589 |
}, |
| 1590 |
{ |
| 1591 |
id: "what_makes_special", |
| 1592 |
label: "Special", |
| 1593 |
enabled: true, |
| 1594 |
order: 9, |
| 1595 |
content_type: "custom", |
| 1596 |
custom_content: "", |
| 1597 |
icon: { type: "icon", value: "star" }, |
| 1598 |
}, |
| 1599 |
{ |
| 1600 |
id: "testimonials", |
| 1601 |
label: "Testimonials", |
| 1602 |
enabled: true, |
| 1603 |
order: 10, |
| 1604 |
content_type: "reviews", |
| 1605 |
icon: { type: "icon", value: "message-circle" }, |
| 1606 |
}, |
| 1607 |
], |
| 1608 |
availability_dates: [], |
| 1609 |
status: "draft", |
| 1610 |
scheduled_publish_date: "", |
| 1611 |
scheduled_unpublish_date: "", |
| 1612 |
version: 1, |
| 1613 |
seasonal_auto_enable: false, |
| 1614 |
seasonal_enable_date: "", |
| 1615 |
seasonal_disable_date: "", |
| 1616 |
meta_title: "", |
| 1617 |
meta_description: "", |
| 1618 |
meta_keywords: "", |
| 1619 |
}); |
| 1620 |
|
| 1621 |
const [errors, setErrors] = useState<Record<string, string>>({}); |
| 1622 |
const [isSubmitting, setIsSubmitting] = useState(false); |
| 1623 |
const [copiedErrorDetails, setCopiedErrorDetails] = useState(false); |
| 1624 |
|
| 1625 |
const copyErrorDetailsToClipboard = (text: string) => { |
| 1626 |
if (!text) return; |
| 1627 |
const fallbackCopy = () => { |
| 1628 |
const textarea = document.createElement("textarea"); |
| 1629 |
textarea.value = text; |
| 1630 |
textarea.style.position = "fixed"; |
| 1631 |
textarea.style.opacity = "0"; |
| 1632 |
document.body.appendChild(textarea); |
| 1633 |
textarea.select(); |
| 1634 |
try { |
| 1635 |
document.execCommand("copy"); |
| 1636 |
} catch (e) { |
| 1637 |
// ignore |
| 1638 |
} |
| 1639 |
document.body.removeChild(textarea); |
| 1640 |
setCopiedErrorDetails(true); |
| 1641 |
setTimeout(() => setCopiedErrorDetails(false), 1500); |
| 1642 |
}; |
| 1643 |
|
| 1644 |
try { |
| 1645 |
if (navigator?.clipboard?.writeText) { |
| 1646 |
navigator.clipboard |
| 1647 |
.writeText(text) |
| 1648 |
.then(() => { |
| 1649 |
setCopiedErrorDetails(true); |
| 1650 |
setTimeout(() => setCopiedErrorDetails(false), 1500); |
| 1651 |
}) |
| 1652 |
.catch(() => fallbackCopy()); |
| 1653 |
} else { |
| 1654 |
fallbackCopy(); |
| 1655 |
} |
| 1656 |
} catch { |
| 1657 |
fallbackCopy(); |
| 1658 |
} |
| 1659 |
}; |
| 1660 |
|
| 1661 |
// Get action and id from URL |
| 1662 |
const action = useMemo(() => { |
| 1663 |
const params = new URLSearchParams(window.location.search); |
| 1664 |
return params.get("action") || "create"; |
| 1665 |
}, []); |
| 1666 |
|
| 1667 |
const tripId = useMemo(() => { |
| 1668 |
const params = new URLSearchParams(window.location.search); |
| 1669 |
return params.get("id") ? parseInt(params.get("id") || "0") : null; |
| 1670 |
}, []); |
| 1671 |
|
| 1672 |
const isEditMode = action === "edit" && tripId !== null; |
| 1673 |
|
| 1674 |
// Fetch traveler categories - LAZY LOAD: only when pricing section is visited |
| 1675 |
const { data: travelerCategoriesData, isLoading: isLoadingCategories } = |
| 1676 |
useQuery({ |
| 1677 |
queryKey: ["traveler-categories"], |
| 1678 |
queryFn: async () => { |
| 1679 |
try { |
| 1680 |
const response = await apiClient.get("/traveler-categories", { |
| 1681 |
params: { |
| 1682 |
per_page: 100, |
| 1683 |
status: "publish", // Only get published categories |
| 1684 |
}, |
| 1685 |
}); |
| 1686 |
const categories = |
| 1687 |
response?.data?.data || response?.data || response || []; |
| 1688 |
return Array.isArray(categories) ? categories : []; |
| 1689 |
} catch (error: any) { |
| 1690 |
showToast( |
| 1691 |
error?.message || __("Failed to load traveler categories", "yatra"), |
| 1692 |
"error", |
| 1693 |
); |
| 1694 |
return []; |
| 1695 |
} |
| 1696 |
}, |
| 1697 |
enabled: |
| 1698 |
can("yatra_view_trips") && |
| 1699 |
(currentSection === "pricing" || visitedSections.has("pricing")), |
| 1700 |
staleTime: 5 * 60 * 1000, // Cache for 5 minutes |
| 1701 |
}); |
| 1702 |
|
| 1703 |
// Get only active/published categories |
| 1704 |
const activeCategories = useMemo(() => { |
| 1705 |
const categories = travelerCategoriesData || []; |
| 1706 |
return categories.filter( |
| 1707 |
(cat: TravelerCategory) => |
| 1708 |
cat.status === "active" || cat.status === "publish", |
| 1709 |
); |
| 1710 |
}, [travelerCategoriesData]); |
| 1711 |
|
| 1712 |
// Fetch settings to get global currency |
| 1713 |
const { data: settingsData } = useQuery({ |
| 1714 |
queryKey: ["settings"], |
| 1715 |
queryFn: async () => { |
| 1716 |
try { |
| 1717 |
const response = await fetchSettings(); |
| 1718 |
return response; |
| 1719 |
} catch (error: any) { |
| 1720 |
// Return default currency if settings fetch fails |
| 1721 |
return { currency: "USD" }; |
| 1722 |
} |
| 1723 |
}, |
| 1724 |
enabled: can("yatra_view_trips"), |
| 1725 |
}); |
| 1726 |
|
| 1727 |
// Get global currency from settings, default to USD |
| 1728 |
const cur = settingsData?.currency; |
| 1729 |
const defCur = settingsData?.default_currency; |
| 1730 |
const globalCurrency = |
| 1731 |
typeof cur === "string" && cur.length > 0 |
| 1732 |
? cur |
| 1733 |
: typeof defCur === "string" && defCur.length > 0 |
| 1734 |
? defCur |
| 1735 |
: "USD"; |
| 1736 |
|
| 1737 |
// Fetch activities from API - LAZY LOAD: only when categorization section is visited |
| 1738 |
const { data: activitiesData } = useQuery({ |
| 1739 |
queryKey: ["activities-published"], |
| 1740 |
queryFn: async () => { |
| 1741 |
try { |
| 1742 |
const response = await apiClient.get("/activities", { |
| 1743 |
params: { |
| 1744 |
per_page: 100, |
| 1745 |
status: "publish", // Only get published activities |
| 1746 |
}, |
| 1747 |
}); |
| 1748 |
return response.data || []; |
| 1749 |
} catch (error: any) { |
| 1750 |
showToast( |
| 1751 |
error?.message || __("Failed to load activities", "yatra"), |
| 1752 |
"error", |
| 1753 |
); |
| 1754 |
return []; |
| 1755 |
} |
| 1756 |
}, |
| 1757 |
enabled: |
| 1758 |
can("yatra_view_trips") && |
| 1759 |
(currentSection === "categorization" || |
| 1760 |
visitedSections.has("categorization")), |
| 1761 |
staleTime: 5 * 60 * 1000, // Cache for 5 minutes |
| 1762 |
}); |
| 1763 |
|
| 1764 |
// LAZY LOAD: only when categorization section is visited |
| 1765 |
const { data: tripCategoriesResponse } = useQuery({ |
| 1766 |
queryKey: ["trip-categories", "published"], |
| 1767 |
queryFn: async () => { |
| 1768 |
try { |
| 1769 |
const response = await apiClient.get("/trip-categories", { |
| 1770 |
params: { |
| 1771 |
per_page: 100, |
| 1772 |
status: "publish", |
| 1773 |
hierarchical: true, |
| 1774 |
orderby: "name", |
| 1775 |
order: "ASC", |
| 1776 |
}, |
| 1777 |
}); |
| 1778 |
return response; |
| 1779 |
} catch (error: any) { |
| 1780 |
showToast( |
| 1781 |
error?.message || __("Failed to load trip categories", "yatra"), |
| 1782 |
"error", |
| 1783 |
); |
| 1784 |
return []; |
| 1785 |
} |
| 1786 |
}, |
| 1787 |
enabled: can("yatra_view_trips"), |
| 1788 |
staleTime: 5 * 60 * 1000, // Cache for 5 minutes |
| 1789 |
}); |
| 1790 |
|
| 1791 |
// LAZY LOAD: only when categorization section is visited |
| 1792 |
const { |
| 1793 |
data: difficultyLevelsResponse, |
| 1794 |
isLoading: isLoadingDifficultyLevels, |
| 1795 |
} = useQuery({ |
| 1796 |
queryKey: ["difficulty-levels", "published"], |
| 1797 |
queryFn: async () => { |
| 1798 |
try { |
| 1799 |
const response = await apiClient.get("/difficulty-levels", { |
| 1800 |
params: { |
| 1801 |
per_page: 100, |
| 1802 |
status: "publish", |
| 1803 |
orderby: "sorting", |
| 1804 |
order: "ASC", |
| 1805 |
}, |
| 1806 |
}); |
| 1807 |
return response.data || []; |
| 1808 |
} catch (error: any) { |
| 1809 |
showToast( |
| 1810 |
error?.message || __("Failed to load difficulty levels", "yatra"), |
| 1811 |
"error", |
| 1812 |
); |
| 1813 |
return []; |
| 1814 |
} |
| 1815 |
}, |
| 1816 |
enabled: |
| 1817 |
can("yatra_view_trips") && |
| 1818 |
(currentSection === "categorization" || |
| 1819 |
visitedSections.has("categorization")), |
| 1820 |
staleTime: 5 * 60 * 1000, // Cache for 5 minutes |
| 1821 |
}); |
| 1822 |
|
| 1823 |
const tripCategories: TripCategoryOption[] = useMemo(() => { |
| 1824 |
const payload = extractArrayPayload(tripCategoriesResponse); |
| 1825 |
return (payload as TripCategoryOption[]).filter((category) => { |
| 1826 |
if (!category) return false; |
| 1827 |
if (typeof category !== "object") return false; |
| 1828 |
if ( |
| 1829 |
"status" in category && |
| 1830 |
category.status && |
| 1831 |
category.status !== "publish" |
| 1832 |
) { |
| 1833 |
return false; |
| 1834 |
} |
| 1835 |
return true; |
| 1836 |
}); |
| 1837 |
}, [tripCategoriesResponse]); |
| 1838 |
|
| 1839 |
const difficultyLevels: DifficultyLevelOption[] = useMemo(() => { |
| 1840 |
const payload = extractArrayPayload(difficultyLevelsResponse); |
| 1841 |
return (payload as DifficultyLevelOption[]).filter((level) => { |
| 1842 |
if (!level) return false; |
| 1843 |
if (typeof level !== "object") return false; |
| 1844 |
if ("status" in level && level.status && level.status !== "publish") { |
| 1845 |
return false; |
| 1846 |
} |
| 1847 |
return true; |
| 1848 |
}); |
| 1849 |
}, [difficultyLevelsResponse]); |
| 1850 |
|
| 1851 |
// Fetch destinations from API - LAZY LOAD: only when location section is visited |
| 1852 |
const { data: destinationsData } = useQuery({ |
| 1853 |
queryKey: ["destinations-published"], |
| 1854 |
queryFn: async () => { |
| 1855 |
try { |
| 1856 |
const response = await apiClient.get("/destinations", { |
| 1857 |
params: { |
| 1858 |
per_page: 100, |
| 1859 |
status: "publish", // Only get published destinations |
| 1860 |
}, |
| 1861 |
}); |
| 1862 |
return response.data || []; |
| 1863 |
} catch (error: any) { |
| 1864 |
showToast( |
| 1865 |
error?.message || __("Failed to load destinations", "yatra"), |
| 1866 |
"error", |
| 1867 |
); |
| 1868 |
return []; |
| 1869 |
} |
| 1870 |
}, |
| 1871 |
enabled: |
| 1872 |
can("yatra_view_trips") && |
| 1873 |
(isEditMode || |
| 1874 |
currentSection === "location" || |
| 1875 |
visitedSections.has("location")), |
| 1876 |
staleTime: 5 * 60 * 1000, // Cache for 5 minutes |
| 1877 |
}); |
| 1878 |
|
| 1879 |
// Fetch trip data if editing |
| 1880 |
const { |
| 1881 |
data: tripData, |
| 1882 |
isLoading: isLoadingTrip, |
| 1883 |
error: tripError, |
| 1884 |
} = useQuery({ |
| 1885 |
queryKey: ["trip", tripId], |
| 1886 |
queryFn: async () => { |
| 1887 |
if (!tripId) return null; |
| 1888 |
try { |
| 1889 |
const response = await apiClient.get(`/trips/${tripId}`); |
| 1890 |
// WordPress REST API returns data directly, but check if it's wrapped |
| 1891 |
// Some endpoints return { data: {...} }, others return {...} directly |
| 1892 |
const tripData = response?.data || response; |
| 1893 |
return tripData; |
| 1894 |
} catch (error: any) { |
| 1895 |
console.error("Error loading trip:", error); |
| 1896 |
showToast( |
| 1897 |
error?.message || __("Failed to load trip data", "yatra"), |
| 1898 |
"error", |
| 1899 |
); |
| 1900 |
throw error; |
| 1901 |
} |
| 1902 |
}, |
| 1903 |
enabled: !!tripId && isEditMode, |
| 1904 |
staleTime: 0, // Always fetch fresh data to ensure downloadable items persist after save |
| 1905 |
}); |
| 1906 |
|
| 1907 |
// Build error context for edit error state |
| 1908 |
const tripErrorContext = useMemo( |
| 1909 |
() => getErrorContext(tripError), |
| 1910 |
[tripError], |
| 1911 |
); |
| 1912 |
|
| 1913 |
// Fetch trip attributes if editing (TripAttributesSection waits until resolved to avoid empty-first-render bug) |
| 1914 |
const tripAttributesQueryEnabled = Boolean(tripId && isEditMode); |
| 1915 |
const { data: tripAttributesData, isPending: isTripAttributesPending } = |
| 1916 |
useQuery({ |
| 1917 |
queryKey: ["trip-attributes", tripId], |
| 1918 |
queryFn: async () => { |
| 1919 |
if (!tripId) return {}; |
| 1920 |
try { |
| 1921 |
const response = await apiClient.get(`/trips/${tripId}/attributes`); |
| 1922 |
|
| 1923 |
// Try different ways to extract the data |
| 1924 |
let payload = response?.data; |
| 1925 |
if (!payload || !Array.isArray(payload)) { |
| 1926 |
payload = response; |
| 1927 |
} |
| 1928 |
if (!payload || !Array.isArray(payload)) { |
| 1929 |
payload = response?.data?.data; |
| 1930 |
} |
| 1931 |
|
| 1932 |
const attributes = Array.isArray(payload) |
| 1933 |
? payload |
| 1934 |
: Array.isArray(payload?.data) |
| 1935 |
? payload.data |
| 1936 |
: []; |
| 1937 |
|
| 1938 |
// Convert to attributeId => value mapping for form |
| 1939 |
const attributesMap: Record<number, any> = {}; |
| 1940 |
attributes.forEach((attr: any) => { |
| 1941 |
const attributeId = Number(attr.attribute_id || attr.id); |
| 1942 |
if (attributeId) { |
| 1943 |
let value = ""; |
| 1944 |
|
| 1945 |
// Read from relationship_metadata (contains complete attribute data) |
| 1946 |
if (attr.relationship_metadata) { |
| 1947 |
try { |
| 1948 |
const metadata = |
| 1949 |
typeof attr.relationship_metadata === "string" |
| 1950 |
? JSON.parse(attr.relationship_metadata) |
| 1951 |
: attr.relationship_metadata; |
| 1952 |
|
| 1953 |
value = metadata.value || ""; |
| 1954 |
} catch (e) { |
| 1955 |
console.warn( |
| 1956 |
"Failed to parse relationship_metadata:", |
| 1957 |
attr.relationship_metadata, |
| 1958 |
e, |
| 1959 |
); |
| 1960 |
value = ""; |
| 1961 |
} |
| 1962 |
} else { |
| 1963 |
// Fallback to old format if metadata is not available |
| 1964 |
value = attr.value || ""; |
| 1965 |
} |
| 1966 |
|
| 1967 |
attributesMap[attributeId] = value; |
| 1968 |
} |
| 1969 |
}); |
| 1970 |
|
| 1971 |
return attributesMap; |
| 1972 |
} catch (error) { |
| 1973 |
console.error("Failed to fetch trip attributes:", error); |
| 1974 |
return {}; |
| 1975 |
} |
| 1976 |
}, |
| 1977 |
enabled: tripAttributesQueryEnabled, |
| 1978 |
staleTime: 5 * 60 * 1000, // Cache for 5 minutes |
| 1979 |
}); |
| 1980 |
const tripAttributesReady = |
| 1981 |
!tripAttributesQueryEnabled || !isTripAttributesPending; |
| 1982 |
|
| 1983 |
// Helper function to normalize array of items to IDs |
| 1984 |
// Helper to normalize highlights (can be array of strings or objects) |
| 1985 |
const normalizeHighlights = (highlights: any): string[] => { |
| 1986 |
if (!highlights) return []; |
| 1987 |
if (Array.isArray(highlights)) { |
| 1988 |
return highlights |
| 1989 |
.map((h: any) => { |
| 1990 |
if (typeof h === "string") return h; |
| 1991 |
if (h && typeof h === "object") { |
| 1992 |
return h.highlight_text || h.text || h.title || String(h); |
| 1993 |
} |
| 1994 |
return String(h); |
| 1995 |
}) |
| 1996 |
.filter((h: string) => h.trim().length > 0); |
| 1997 |
} |
| 1998 |
if (typeof highlights === "string") { |
| 1999 |
try { |
| 2000 |
const parsed = JSON.parse(highlights); |
| 2001 |
return normalizeHighlights(parsed); |
| 2002 |
} catch { |
| 2003 |
return [highlights]; |
| 2004 |
} |
| 2005 |
} |
| 2006 |
return []; |
| 2007 |
}; |
| 2008 |
|
| 2009 |
const normalizeDownloadableItems = (items: any): DownloadableItem[] => { |
| 2010 |
if (!items || !Array.isArray(items)) return []; |
| 2011 |
|
| 2012 |
return items |
| 2013 |
.filter((row: any) => row && typeof row === "object") |
| 2014 |
.map((row: any, idx: number) => { |
| 2015 |
const rawVisibility = (row.visibility ?? "booked_only") as any; |
| 2016 |
const mappedVisibility = |
| 2017 |
rawVisibility === "paid_only" ? "booked_only" : rawVisibility; |
| 2018 |
const safeVisibility: DownloadableItem["visibility"] = [ |
| 2019 |
"public", |
| 2020 |
"logged_in", |
| 2021 |
"booked_only", |
| 2022 |
].includes(mappedVisibility) |
| 2023 |
? mappedVisibility |
| 2024 |
: "booked_only"; |
| 2025 |
|
| 2026 |
const title = ( |
| 2027 |
row.title ?? |
| 2028 |
row.download_title ?? |
| 2029 |
row.downlaod_title ?? |
| 2030 |
"" |
| 2031 |
).toString(); |
| 2032 |
const description = ( |
| 2033 |
row.description ?? |
| 2034 |
row.download_description ?? |
| 2035 |
row.downlaod_description ?? |
| 2036 |
"" |
| 2037 |
).toString(); |
| 2038 |
const attachmentIdRaw = |
| 2039 |
row.attachment_id ?? row.download_file ?? row.downlaod_file; |
| 2040 |
const attachmentUrl = ( |
| 2041 |
row.attachment_url ?? |
| 2042 |
row.content_url ?? |
| 2043 |
"" |
| 2044 |
).toString(); |
| 2045 |
const attachmentTitle = (row.attachment_title ?? "").toString(); |
| 2046 |
const enabledRaw = |
| 2047 |
row.enabled ?? |
| 2048 |
row.is_downloadable ?? |
| 2049 |
row.download_enabled ?? |
| 2050 |
row.downlaod_enabled; |
| 2051 |
|
| 2052 |
return { |
| 2053 |
id: row.id != null ? Number(row.id) : null, |
| 2054 |
title, |
| 2055 |
description, |
| 2056 |
attachment_id: |
| 2057 |
attachmentIdRaw != null ? Number(attachmentIdRaw) : null, |
| 2058 |
attachment_url: attachmentUrl, |
| 2059 |
attachment_title: attachmentTitle, |
| 2060 |
visibility: safeVisibility, |
| 2061 |
enabled: enabledRaw != null ? Boolean(enabledRaw) : true, |
| 2062 |
sort_order: row.sort_order != null ? Number(row.sort_order) : idx + 1, |
| 2063 |
}; |
| 2064 |
}); |
| 2065 |
}; |
| 2066 |
|
| 2067 |
// Helper to extract IDs from mixed arrays |
| 2068 |
const extractIds = (items: any): number[] => { |
| 2069 |
if (!items || !Array.isArray(items)) return []; |
| 2070 |
return items |
| 2071 |
.map((item: any) => { |
| 2072 |
if (typeof item === "number") return item; |
| 2073 |
if (typeof item === "string") return parseInt(item) || 0; |
| 2074 |
if (item && typeof item === "object") { |
| 2075 |
return ( |
| 2076 |
item.id || |
| 2077 |
item.destination_id || |
| 2078 |
item.activity_id || |
| 2079 |
item.category_id || |
| 2080 |
0 |
| 2081 |
); |
| 2082 |
} |
| 2083 |
return 0; |
| 2084 |
}) |
| 2085 |
.filter((id: number) => !isNaN(id) && id > 0); |
| 2086 |
}; |
| 2087 |
|
| 2088 |
// Helper to normalize itinerary days |
| 2089 |
const normalizeItineraryDays = (days: any): any[] => { |
| 2090 |
if (!days) return []; |
| 2091 |
if (Array.isArray(days)) return days; |
| 2092 |
if (typeof days === "string") { |
| 2093 |
try { |
| 2094 |
const parsed = JSON.parse(days); |
| 2095 |
return Array.isArray(parsed) ? parsed : []; |
| 2096 |
} catch { |
| 2097 |
return []; |
| 2098 |
} |
| 2099 |
} |
| 2100 |
return []; |
| 2101 |
}; |
| 2102 |
|
| 2103 |
// Helper to normalize availability dates |
| 2104 |
const normalizeAvailabilityDates = (dates: any): any[] => { |
| 2105 |
if (!dates) return []; |
| 2106 |
if (Array.isArray(dates)) return dates; |
| 2107 |
if (typeof dates === "string") { |
| 2108 |
try { |
| 2109 |
const parsed = JSON.parse(dates); |
| 2110 |
return Array.isArray(parsed) ? parsed : []; |
| 2111 |
} catch { |
| 2112 |
return []; |
| 2113 |
} |
| 2114 |
} |
| 2115 |
return []; |
| 2116 |
}; |
| 2117 |
|
| 2118 |
// Utility function to normalize gallery images |
| 2119 |
const normalizeGalleryImages = ( |
| 2120 |
images: any, |
| 2121 |
): Array<{ |
| 2122 |
id: number; |
| 2123 |
url: string; |
| 2124 |
thumbnail_url?: string; |
| 2125 |
alt_text?: string; |
| 2126 |
caption?: string; |
| 2127 |
}> => { |
| 2128 |
if (!images) return []; |
| 2129 |
if (Array.isArray(images)) { |
| 2130 |
return images |
| 2131 |
.map((img: any) => { |
| 2132 |
if (typeof img === "string") { |
| 2133 |
return { id: 0, url: img }; |
| 2134 |
} |
| 2135 |
if (img && typeof img === "object") { |
| 2136 |
return { |
| 2137 |
id: img.id || img.image_id || 0, |
| 2138 |
url: img.url || img.image_url || img.src || "", |
| 2139 |
thumbnail_url: img.thumbnail_url || img.thumb_url || "", |
| 2140 |
alt_text: img.alt_text || img.alt || "", |
| 2141 |
caption: img.caption || img.title || "", |
| 2142 |
}; |
| 2143 |
} |
| 2144 |
return { id: 0, url: "" }; |
| 2145 |
}) |
| 2146 |
.filter((item: any) => item.url); |
| 2147 |
} |
| 2148 |
if (typeof images === "string") { |
| 2149 |
try { |
| 2150 |
const parsed = JSON.parse(images); |
| 2151 |
return normalizeGalleryImages(parsed); |
| 2152 |
} catch { |
| 2153 |
return [{ id: 0, url: images }]; |
| 2154 |
} |
| 2155 |
} |
| 2156 |
return []; |
| 2157 |
}; |
| 2158 |
|
| 2159 |
// Helper to normalize FAQs |
| 2160 |
const normalizeFaqs = (faqs: any): FAQ[] => { |
| 2161 |
if (!faqs || !Array.isArray(faqs)) return []; |
| 2162 |
return faqs |
| 2163 |
.map((faq: any) => { |
| 2164 |
if (faq && typeof faq === "object") { |
| 2165 |
return { |
| 2166 |
question: faq.question || "", |
| 2167 |
answer: faq.answer || "", |
| 2168 |
}; |
| 2169 |
} |
| 2170 |
return { question: "", answer: "" }; |
| 2171 |
}) |
| 2172 |
.filter((faq: any) => faq.question && faq.answer); |
| 2173 |
}; |
| 2174 |
|
| 2175 |
useEffect(() => { |
| 2176 |
if (!tripData || !isEditMode) { |
| 2177 |
return; |
| 2178 |
} |
| 2179 |
|
| 2180 |
// Preserve current itinerary_days and gallery_images if they exist and have content |
| 2181 |
// This prevents wiping out data when user updates trip in itinerary builder |
| 2182 |
const currentItineraryDays = formData.itinerary_days || []; |
| 2183 |
const currentGalleryImages = formData.gallery_images || []; |
| 2184 |
|
| 2185 |
// Only use database data if current data is empty or if this is initial load |
| 2186 |
const shouldPreserveItinerary = currentItineraryDays.length > 0; |
| 2187 |
const shouldPreserveGallery = currentGalleryImages.length > 0; |
| 2188 |
|
| 2189 |
setFormData({ |
| 2190 |
title: tripData.title || "", |
| 2191 |
slug: tripData.slug || "", |
| 2192 |
description: tripData.description || "", |
| 2193 |
highlights: normalizeHighlights(tripData.highlights), |
| 2194 |
trip_details: tripData.trip_details || "", |
| 2195 |
short_description: tripData.short_description || "", |
| 2196 |
what_makes_special: tripData.what_makes_special || "", |
| 2197 |
trip_story: tripData.trip_story || "", |
| 2198 |
video_url: tripData.video_url || "", |
| 2199 |
virtual_tour_url: tripData.virtual_tour_url || "", |
| 2200 |
testimonial_review_ids: Array.isArray(tripData.testimonial_review_ids) |
| 2201 |
? tripData.testimonial_review_ids |
| 2202 |
: [], |
| 2203 |
destinations: extractIds(tripData.destinations || []), |
| 2204 |
starting_location: tripData.starting_location || "", |
| 2205 |
ending_location: tripData.ending_location || "", |
| 2206 |
countries: Array.isArray(tripData.countries) ? tripData.countries : [], |
| 2207 |
regions: Array.isArray(tripData.regions) ? tripData.regions : [], |
| 2208 |
starting_latitude: tripData.starting_latitude?.toString() || "", |
| 2209 |
starting_longitude: tripData.starting_longitude?.toString() || "", |
| 2210 |
ending_latitude: tripData.ending_latitude?.toString() || "", |
| 2211 |
ending_longitude: tripData.ending_longitude?.toString() || "", |
| 2212 |
landmarks: Array.isArray(tripData.landmarks) ? tripData.landmarks : [], |
| 2213 |
trip_type: (tripData.trip_type || |
| 2214 |
(tripData.duration_days && |
| 2215 |
parseInt(tripData.duration_days?.toString() || "0") === 1 |
| 2216 |
? "single_day" |
| 2217 |
: "multi_day")) as "single_day" | "multi_day" | "flexible", |
| 2218 |
duration_days: tripData.duration_days?.toString() || "", |
| 2219 |
duration_nights: tripData.duration_nights?.toString() || "", |
| 2220 |
available_from: tripData.available_from || "", |
| 2221 |
available_to: tripData.available_to || "", |
| 2222 |
booking_window_days: tripData.booking_window_days?.toString() || "", |
| 2223 |
seasonal_availability: tripData.seasonal_availability || "", |
| 2224 |
best_season: tripData.best_season || "", |
| 2225 |
peak_season: tripData.peak_season || "", |
| 2226 |
off_season: tripData.off_season || "", |
| 2227 |
activity_types: extractIds(tripData.activity_types || []), |
| 2228 |
difficulty_level: tripData.difficulty_level?.toString() || "", |
| 2229 |
trip_category: extractIds(tripData.trip_category || []), |
| 2230 |
tags: Array.isArray(tripData.tags) ? tripData.tags : [], |
| 2231 |
featured_priority: tripData.featured_priority || "none", |
| 2232 |
accommodation_type: tripData.accommodation_type || "", |
| 2233 |
meal_plan: tripData.meal_plan || "", |
| 2234 |
accommodation_details: tripData.accommodation_details || "", |
| 2235 |
transportation_included: tripData.transportation_included || false, |
| 2236 |
pickup_location: tripData.pickup_location || "", |
| 2237 |
dropoff_location: tripData.dropoff_location || "", |
| 2238 |
transportation_details: tripData.transportation_details || "", |
| 2239 |
pricing_type: (tripData.pricing_type || |
| 2240 |
(tripData.price_types && |
| 2241 |
Array.isArray(tripData.price_types) && |
| 2242 |
tripData.price_types.length > 0 |
| 2243 |
? "traveler_based" |
| 2244 |
: "regular")) as "regular" | "traveler_based", |
| 2245 |
original_price: tripData.original_price?.toString() || "", |
| 2246 |
discounted_price: tripData.discounted_price?.toString() || "", |
| 2247 |
price_types: Array.isArray(tripData.price_types) |
| 2248 |
? tripData.price_types.map((pt: any) => ({ |
| 2249 |
category_id: Number(pt.category_id) || 0, |
| 2250 |
original_price: pt.original_price?.toString() || "", |
| 2251 |
discounted_price: pt.discounted_price?.toString() || "", |
| 2252 |
is_default: Boolean(pt.is_default), |
| 2253 |
})) |
| 2254 |
: [], |
| 2255 |
deposit_amount: tripData.deposit_amount?.toString() || "", |
| 2256 |
deposit_percentage: tripData.deposit_percentage?.toString() || "", |
| 2257 |
payment_terms: tripData.payment_terms || "", |
| 2258 |
max_travelers: tripData.max_travelers?.toString() || "", |
| 2259 |
min_travelers: tripData.min_travelers?.toString() || "", |
| 2260 |
booking_deadline_hours: tripData.booking_deadline_hours || "", |
| 2261 |
cancellation_policy: tripData.cancellation_policy || "", |
| 2262 |
age_min: tripData.age_min?.toString() || "", |
| 2263 |
age_max: tripData.age_max?.toString() || "", |
| 2264 |
physical_requirements: tripData.physical_requirements || "", |
| 2265 |
visa_requirements: tripData.visa_requirements || "", |
| 2266 |
vaccination_requirements: tripData.vaccination_requirements || "", |
| 2267 |
disable_booking: Boolean(tripData.custom_fields?.disable_booking), |
| 2268 |
// tinyint(1) columns can serialize from PHP/wpdb as the string "0"/"1". |
| 2269 |
// JS treats "0" as truthy, so a plain `value || false` would leave the |
| 2270 |
// checkbox stuck on after the user un-checked + saved. Coerce explicitly. |
| 2271 |
has_default_time_slots: |
| 2272 |
tripData.has_default_time_slots === true || |
| 2273 |
tripData.has_default_time_slots === 1 || |
| 2274 |
tripData.has_default_time_slots === "1", |
| 2275 |
default_time_slots: Array.isArray(tripData.default_time_slots) |
| 2276 |
? tripData.default_time_slots |
| 2277 |
: tripData.default_time_slots |
| 2278 |
? JSON.parse(tripData.default_time_slots) |
| 2279 |
: [], |
| 2280 |
departure_time: tripData.departure_time || "09:00", |
| 2281 |
included_items: normalizeAmenityItems(tripData.included_items), |
| 2282 |
excluded_items: normalizeAmenityItems(tripData.excluded_items), |
| 2283 |
// Preserve current itinerary data if it exists, otherwise use database data |
| 2284 |
itinerary_days: shouldPreserveItinerary |
| 2285 |
? currentItineraryDays |
| 2286 |
: normalizeItineraryDays(tripData.itinerary_days), |
| 2287 |
// Preserve current gallery data if it exists, otherwise use database data |
| 2288 |
gallery_images: shouldPreserveGallery |
| 2289 |
? currentGalleryImages |
| 2290 |
: normalizeGalleryImages(tripData.gallery_images), |
| 2291 |
featured_image: tripData.featured_image |
| 2292 |
? Number(tripData.featured_image) |
| 2293 |
: null, |
| 2294 |
downloadable_items: normalizeDownloadableItems( |
| 2295 |
tripData.downloadable_items, |
| 2296 |
), |
| 2297 |
faqs: normalizeFaqs(tripData.faqs), |
| 2298 |
frontend_tabs: Array.isArray(tripData.frontend_tabs) |
| 2299 |
? tripData.frontend_tabs |
| 2300 |
: [ |
| 2301 |
// Core sections (always present) - in logical order |
| 2302 |
{ |
| 2303 |
id: "overview", |
| 2304 |
label: "Overview", |
| 2305 |
enabled: true, |
| 2306 |
order: 1, |
| 2307 |
content_type: "overview", |
| 2308 |
icon: { type: "icon", value: "book" }, |
| 2309 |
}, |
| 2310 |
{ |
| 2311 |
id: "itinerary", |
| 2312 |
label: "Itinerary", |
| 2313 |
enabled: true, |
| 2314 |
order: 2, |
| 2315 |
content_type: "itinerary", |
| 2316 |
icon: { type: "icon", value: "calendar" }, |
| 2317 |
}, |
| 2318 |
{ |
| 2319 |
id: "included", |
| 2320 |
label: "Included", |
| 2321 |
enabled: true, |
| 2322 |
order: 3, |
| 2323 |
content_type: "included_excluded", |
| 2324 |
icon: { type: "icon", value: "check" }, |
| 2325 |
}, |
| 2326 |
{ |
| 2327 |
id: "location", |
| 2328 |
label: "Location", |
| 2329 |
enabled: true, |
| 2330 |
order: 4, |
| 2331 |
content_type: "location", |
| 2332 |
icon: { type: "icon", value: "map-pin" }, |
| 2333 |
}, |
| 2334 |
{ |
| 2335 |
id: "important_info", |
| 2336 |
label: "Important Info", |
| 2337 |
enabled: true, |
| 2338 |
order: 5, |
| 2339 |
content_type: "important_info", |
| 2340 |
icon: { type: "icon", value: "info" }, |
| 2341 |
}, |
| 2342 |
|
| 2343 |
// Conditional sections (enabled by default, shown conditionally on frontend) |
| 2344 |
{ |
| 2345 |
id: "downloads", |
| 2346 |
label: "Downloads", |
| 2347 |
enabled: true, |
| 2348 |
order: 6, |
| 2349 |
content_type: "downloads", |
| 2350 |
icon: { type: "icon", value: "download" }, |
| 2351 |
}, |
| 2352 |
{ |
| 2353 |
id: "faq", |
| 2354 |
label: "FAQ", |
| 2355 |
enabled: true, |
| 2356 |
order: 7, |
| 2357 |
content_type: "faq", |
| 2358 |
icon: { type: "icon", value: "help-circle" }, |
| 2359 |
}, |
| 2360 |
{ |
| 2361 |
id: "trip_story", |
| 2362 |
label: "Story", |
| 2363 |
enabled: true, |
| 2364 |
order: 8, |
| 2365 |
content_type: "trip_story", |
| 2366 |
custom_content: "", |
| 2367 |
icon: { type: "icon", value: "book" }, |
| 2368 |
}, |
| 2369 |
{ |
| 2370 |
id: "what_makes_special", |
| 2371 |
label: "Special", |
| 2372 |
enabled: true, |
| 2373 |
order: 9, |
| 2374 |
content_type: "what_makes_special", |
| 2375 |
custom_content: "", |
| 2376 |
icon: { type: "icon", value: "star" }, |
| 2377 |
}, |
| 2378 |
{ |
| 2379 |
id: "testimonials", |
| 2380 |
label: "Testimonials", |
| 2381 |
enabled: true, |
| 2382 |
order: 10, |
| 2383 |
content_type: "testimonials", |
| 2384 |
icon: { type: "icon", value: "message-circle" }, |
| 2385 |
}, |
| 2386 |
], |
| 2387 |
availability_dates: normalizeAvailabilityDates( |
| 2388 |
tripData.availability_dates, |
| 2389 |
), |
| 2390 |
status: (tripData.status || "draft") as |
| 2391 |
| "draft" |
| 2392 |
| "review" |
| 2393 |
| "approved" |
| 2394 |
| "publish" |
| 2395 |
| "archived" |
| 2396 |
| "suspended", |
| 2397 |
scheduled_publish_date: tripData.scheduled_publish_date || "", |
| 2398 |
scheduled_unpublish_date: tripData.scheduled_unpublish_date || "", |
| 2399 |
version: tripData.version || 1, |
| 2400 |
seasonal_auto_enable: tripData.seasonal_auto_enable || false, |
| 2401 |
seasonal_enable_date: tripData.seasonal_enable_date || "", |
| 2402 |
seasonal_disable_date: tripData.seasonal_disable_date || "", |
| 2403 |
meta_title: tripData.meta_title || "", |
| 2404 |
meta_description: tripData.meta_description || "", |
| 2405 |
meta_keywords: tripData.meta_keywords || "", |
| 2406 |
attributes: tripAttributesData || {}, |
| 2407 |
}); |
| 2408 |
|
| 2409 |
if (tripData.featured_image_url && tripData.featured_image) { |
| 2410 |
const numericId = Number(tripData.featured_image) || 0; |
| 2411 |
setFeaturedImagePreview(tripData.featured_image_url); |
| 2412 |
if (numericId > 0) { |
| 2413 |
featuredImageCache.current[numericId] = tripData.featured_image_url; |
| 2414 |
} |
| 2415 |
} else if (!tripData.featured_image) { |
| 2416 |
setFeaturedImagePreview(""); |
| 2417 |
} |
| 2418 |
// eslint-disable-next-line react-hooks/exhaustive-deps |
| 2419 |
}, [tripData, tripAttributesData, destinationsData, isEditMode, tripId]); |
| 2420 |
|
| 2421 |
useEffect(() => { |
| 2422 |
let isMounted = true; |
| 2423 |
|
| 2424 |
const resolveFeaturedImage = async () => { |
| 2425 |
const attachmentId = formData.featured_image; |
| 2426 |
|
| 2427 |
if (!attachmentId) { |
| 2428 |
setFeaturedImagePreview(""); |
| 2429 |
return; |
| 2430 |
} |
| 2431 |
|
| 2432 |
const cachedUrl = featuredImageCache.current[attachmentId]; |
| 2433 |
if (cachedUrl) { |
| 2434 |
setFeaturedImagePreview(cachedUrl); |
| 2435 |
return; |
| 2436 |
} |
| 2437 |
|
| 2438 |
if (!mediaBaseUrl) { |
| 2439 |
setFeaturedImagePreview(""); |
| 2440 |
return; |
| 2441 |
} |
| 2442 |
|
| 2443 |
setIsResolvingFeaturedImage(true); |
| 2444 |
try { |
| 2445 |
const data = await wpService.getMedia(attachmentId); |
| 2446 |
const url = data?.source_url || ""; |
| 2447 |
if (url && isMounted) { |
| 2448 |
featuredImageCache.current[attachmentId] = url; |
| 2449 |
setFeaturedImagePreview(url); |
| 2450 |
} else if (isMounted) { |
| 2451 |
setFeaturedImagePreview(""); |
| 2452 |
} |
| 2453 |
} catch (error) { |
| 2454 |
console.error("Failed to resolve featured image URL:", error); |
| 2455 |
if (isMounted) { |
| 2456 |
setFeaturedImagePreview(""); |
| 2457 |
} |
| 2458 |
} finally { |
| 2459 |
if (isMounted) { |
| 2460 |
setIsResolvingFeaturedImage(false); |
| 2461 |
} |
| 2462 |
} |
| 2463 |
}; |
| 2464 |
|
| 2465 |
resolveFeaturedImage(); |
| 2466 |
|
| 2467 |
return () => { |
| 2468 |
isMounted = false; |
| 2469 |
}; |
| 2470 |
}, [formData.featured_image, mediaBaseUrl]); |
| 2471 |
|
| 2472 |
// Map errors to sections - also check for price_type errors |
| 2473 |
const getSectionErrors = (sectionId: SectionId): string[] => { |
| 2474 |
const errorMap: Record<SectionId, string[]> = { |
| 2475 |
basic: [ |
| 2476 |
"title", |
| 2477 |
"slug", |
| 2478 |
"description", |
| 2479 |
"featured_image", |
| 2480 |
"trip_type", |
| 2481 |
"duration_days", |
| 2482 |
"duration_nights", |
| 2483 |
], |
| 2484 |
location: ["destinations", "starting_location", "ending_location"], |
| 2485 |
duration: ["available_from", "available_to", "booking_window_days"], |
| 2486 |
pricing: ["original_price", "discounted_price", "price_types"], |
| 2487 |
booking: ["min_travelers", "max_travelers", "age_min", "age_max"], |
| 2488 |
attributes: ["attributes"], |
| 2489 |
itinerary: ["itinerary_days"], |
| 2490 |
included: ["included_items", "excluded_items"], |
| 2491 |
media: ["gallery_images", "video_url", "virtual_tour_url"], // Removed featured_image - it's in basic section |
| 2492 |
downloads: ["downloadable_items"], |
| 2493 |
categorization: ["trip_category", "activity_types"], |
| 2494 |
faqs: ["faqs"], |
| 2495 |
seo: ["meta_title", "meta_description"], |
| 2496 |
advanced: [], |
| 2497 |
}; |
| 2498 |
|
| 2499 |
const sectionFields = errorMap[sectionId] || []; |
| 2500 |
const fieldErrors = sectionFields.filter((field) => errors[field]); |
| 2501 |
|
| 2502 |
// Also check for price_type errors (they have dynamic keys like price_type_0_original) |
| 2503 |
if (sectionId === "pricing") { |
| 2504 |
const priceTypeErrors = Object.keys(errors).filter((key) => |
| 2505 |
key.startsWith("price_type_"), |
| 2506 |
); |
| 2507 |
return [...fieldErrors, ...priceTypeErrors]; |
| 2508 |
} |
| 2509 |
|
| 2510 |
return fieldErrors; |
| 2511 |
}; |
| 2512 |
|
| 2513 |
// Define sections - Reorganized for beginner-friendly UX |
| 2514 |
// PHASE 1: ESSENTIALS (Must complete for publishable trip) |
| 2515 |
const essentialsSections: Section[] = [ |
| 2516 |
// 1. Trip Basics - What you're offering |
| 2517 |
{ |
| 2518 |
id: "basic", |
| 2519 |
label: __("Trip Basics", "yatra"), |
| 2520 |
icon: FileText, |
| 2521 |
required: true, |
| 2522 |
completed: !!(formData.title?.trim() && formData.slug?.trim()), |
| 2523 |
hasErrors: getSectionErrors("basic").length > 0, |
| 2524 |
}, |
| 2525 |
|
| 2526 |
// 2. Location & Route - Where it happens |
| 2527 |
{ |
| 2528 |
id: "location", |
| 2529 |
label: __("Location & Route", "yatra"), |
| 2530 |
icon: MapPin, |
| 2531 |
required: false, |
| 2532 |
completed: !!(formData.destinations.length > 0), |
| 2533 |
hasErrors: getSectionErrors("location").length > 0, |
| 2534 |
}, |
| 2535 |
|
| 2536 |
// 3. Pricing - How much it costs |
| 2537 |
{ |
| 2538 |
id: "pricing", |
| 2539 |
label: __("Pricing", "yatra"), |
| 2540 |
icon: DollarSign, |
| 2541 |
required: true, |
| 2542 |
completed: |
| 2543 |
formData.pricing_type === "regular" |
| 2544 |
? !!( |
| 2545 |
formData.original_price && parseFloat(formData.original_price) > 0 |
| 2546 |
) |
| 2547 |
: formData.price_types.some( |
| 2548 |
(pt) => pt.original_price && parseFloat(pt.original_price) > 0, |
| 2549 |
), |
| 2550 |
hasErrors: getSectionErrors("pricing").length > 0, |
| 2551 |
}, |
| 2552 |
|
| 2553 |
// 4. Availability & Booking - When available + booking rules (merged duration + booking) |
| 2554 |
{ |
| 2555 |
id: "duration", |
| 2556 |
label: __("Availability & Booking", "yatra"), |
| 2557 |
icon: Calendar, |
| 2558 |
required: false, |
| 2559 |
completed: !!( |
| 2560 |
formData.available_from || |
| 2561 |
formData.available_to || |
| 2562 |
(formData.min_travelers && formData.max_travelers) |
| 2563 |
), |
| 2564 |
hasErrors: |
| 2565 |
getSectionErrors("duration").length > 0 || |
| 2566 |
getSectionErrors("booking").length > 0, |
| 2567 |
}, |
| 2568 |
]; |
| 2569 |
|
| 2570 |
// PHASE 2: DETAILS (Enhance trip quality) |
| 2571 |
const detailsSections: Section[] = [ |
| 2572 |
// 5. Trip Details - Description + Itinerary + Included/Excluded (merged 3 sections) |
| 2573 |
{ |
| 2574 |
id: "itinerary", |
| 2575 |
label: __("Trip Details", "yatra"), |
| 2576 |
icon: BookOpen, |
| 2577 |
required: false, |
| 2578 |
completed: |
| 2579 |
formData.itinerary_days.length > 0 || |
| 2580 |
formData.included_items.length > 0 || |
| 2581 |
formData.excluded_items.length > 0, |
| 2582 |
hasErrors: |
| 2583 |
getSectionErrors("itinerary").length > 0 || |
| 2584 |
getSectionErrors("included").length > 0, |
| 2585 |
}, |
| 2586 |
]; |
| 2587 |
|
| 2588 |
// PHASE 3: OPTIMIZATION (Improve discoverability) |
| 2589 |
const optimizationSections: Section[] = [ |
| 2590 |
// 6. Media & Gallery - Photos, videos, testimonials |
| 2591 |
{ |
| 2592 |
id: "media", |
| 2593 |
label: __("Media & Gallery", "yatra"), |
| 2594 |
icon: Image, |
| 2595 |
required: false, |
| 2596 |
completed: formData.gallery_images.length > 0 || !!formData.video_url, |
| 2597 |
hasErrors: getSectionErrors("media").length > 0, |
| 2598 |
}, |
| 2599 |
...(showDownloadsUI |
| 2600 |
? ([ |
| 2601 |
{ |
| 2602 |
id: "downloads", |
| 2603 |
label: __("Downloads", "yatra"), |
| 2604 |
icon: Download, |
| 2605 |
required: false, |
| 2606 |
completed: (formData.downloadable_items || []).length > 0, |
| 2607 |
hasErrors: getSectionErrors("downloads").length > 0, |
| 2608 |
}, |
| 2609 |
] as Section[]) |
| 2610 |
: []), |
| 2611 |
|
| 2612 |
// 7. Categories & Attributes - Classification + Custom Attributes (merged 2 sections) |
| 2613 |
{ |
| 2614 |
id: "categorization", |
| 2615 |
label: __("Categories & Attributes", "yatra"), |
| 2616 |
icon: Tag, |
| 2617 |
required: false, |
| 2618 |
completed: !!( |
| 2619 |
formData.trip_category || |
| 2620 |
formData.activity_types.length > 0 || |
| 2621 |
formData.tags.length > 0 || |
| 2622 |
(formData.attributes && Object.keys(formData.attributes).length > 0) |
| 2623 |
), |
| 2624 |
hasErrors: |
| 2625 |
getSectionErrors("categorization").length > 0 || |
| 2626 |
getSectionErrors("attributes").length > 0, |
| 2627 |
}, |
| 2628 |
|
| 2629 |
// 8. SEO & Marketing - SEO + FAQs (merged, fixed duplicate) |
| 2630 |
{ |
| 2631 |
id: "seo", |
| 2632 |
label: __("SEO & Marketing", "yatra"), |
| 2633 |
icon: Search, |
| 2634 |
required: false, |
| 2635 |
completed: !!( |
| 2636 |
formData.meta_title || |
| 2637 |
formData.meta_description || |
| 2638 |
formData.faqs.length > 0 |
| 2639 |
), |
| 2640 |
hasErrors: |
| 2641 |
getSectionErrors("seo").length > 0 || |
| 2642 |
getSectionErrors("faqs").length > 0, |
| 2643 |
}, |
| 2644 |
]; |
| 2645 |
|
| 2646 |
// PHASE 4: ADVANCED (Power users only) |
| 2647 |
const advancedSections: Section[] = [ |
| 2648 |
// 9. Advanced Settings - Publishing, scheduling, technical |
| 2649 |
{ |
| 2650 |
id: "advanced", |
| 2651 |
label: __("Advanced Settings", "yatra"), |
| 2652 |
icon: Settings, |
| 2653 |
required: false, |
| 2654 |
completed: false, |
| 2655 |
hasErrors: false, |
| 2656 |
}, |
| 2657 |
]; |
| 2658 |
|
| 2659 |
const allSections = [ |
| 2660 |
...essentialsSections, |
| 2661 |
...detailsSections, |
| 2662 |
...optimizationSections, |
| 2663 |
...advancedSections, |
| 2664 |
]; |
| 2665 |
const currentStepIndex = allSections.findIndex( |
| 2666 |
(s) => s.id === currentSection, |
| 2667 |
); |
| 2668 |
|
| 2669 |
// Navigation helpers |
| 2670 |
const goToNextSection = () => { |
| 2671 |
if (currentStepIndex < allSections.length - 1) { |
| 2672 |
setCurrentSection(allSections[currentStepIndex + 1].id); |
| 2673 |
} |
| 2674 |
}; |
| 2675 |
|
| 2676 |
const goToPreviousSection = () => { |
| 2677 |
if (currentStepIndex > 0) { |
| 2678 |
setCurrentSection(allSections[currentStepIndex - 1].id); |
| 2679 |
} |
| 2680 |
}; |
| 2681 |
|
| 2682 |
// Smart defaults - Auto-calculate nights from days (only if nights is empty) |
| 2683 |
useEffect(() => { |
| 2684 |
if ( |
| 2685 |
formData.duration_days && |
| 2686 |
formData.trip_type === "multi_day" && |
| 2687 |
(!formData.duration_nights || formData.duration_nights === "") |
| 2688 |
) { |
| 2689 |
const days = parseInt(formData.duration_days); |
| 2690 |
if (days > 0 && !isNaN(days)) { |
| 2691 |
setFormData((prev) => ({ |
| 2692 |
...prev, |
| 2693 |
duration_nights: String(Math.max(0, days - 1)), |
| 2694 |
})); |
| 2695 |
} |
| 2696 |
} |
| 2697 |
// eslint-disable-next-line react-hooks/exhaustive-deps |
| 2698 |
}, [formData.duration_days, formData.trip_type]); |
| 2699 |
|
| 2700 |
// Auto-calculate days from nights (only if days is empty) |
| 2701 |
useEffect(() => { |
| 2702 |
if ( |
| 2703 |
formData.duration_nights && |
| 2704 |
formData.trip_type === "multi_day" && |
| 2705 |
(!formData.duration_days || formData.duration_days === "") |
| 2706 |
) { |
| 2707 |
const nights = parseInt(formData.duration_nights); |
| 2708 |
if (nights > 0 && !isNaN(nights)) { |
| 2709 |
setFormData((prev) => ({ ...prev, duration_days: String(nights + 1) })); |
| 2710 |
} |
| 2711 |
} |
| 2712 |
// eslint-disable-next-line react-hooks/exhaustive-deps |
| 2713 |
}, [formData.duration_nights, formData.trip_type]); |
| 2714 |
|
| 2715 |
// Auto-generate slug from title |
| 2716 |
const generateSlug = (title: string): string => { |
| 2717 |
return title |
| 2718 |
.toLowerCase() |
| 2719 |
.trim() |
| 2720 |
.replace(/[^\w\s-]/g, "") |
| 2721 |
.replace(/[\s_-]+/g, "-") |
| 2722 |
.replace(/^-+|-+$/g, ""); |
| 2723 |
}; |
| 2724 |
|
| 2725 |
const handleTitleChange = (value: string) => { |
| 2726 |
setFormData((prev) => ({ |
| 2727 |
...prev, |
| 2728 |
title: value, |
| 2729 |
slug: prev.slug || generateSlug(value), |
| 2730 |
})); |
| 2731 |
if (errors.title) { |
| 2732 |
setErrors((prev) => ({ ...prev, title: "" })); |
| 2733 |
} |
| 2734 |
}; |
| 2735 |
|
| 2736 |
const handleFieldChange = (field: keyof TripFormData, value: any) => { |
| 2737 |
setFormData((prev) => ({ ...prev, [field]: value })); |
| 2738 |
if (errors[field]) { |
| 2739 |
setErrors((prev) => ({ ...prev, [field]: "" })); |
| 2740 |
} |
| 2741 |
|
| 2742 |
if (field === "featured_image") { |
| 2743 |
// Handle explicit null (when removing image) |
| 2744 |
if (value === null || value === "" || value === undefined) { |
| 2745 |
setFeaturedImagePreview(""); |
| 2746 |
} else { |
| 2747 |
const numericValue = typeof value === "number" ? value : Number(value); |
| 2748 |
if (!numericValue) { |
| 2749 |
setFeaturedImagePreview(""); |
| 2750 |
} else if (featuredImageCache.current[numericValue]) { |
| 2751 |
setFeaturedImagePreview(featuredImageCache.current[numericValue]); |
| 2752 |
} |
| 2753 |
} |
| 2754 |
} |
| 2755 |
}; |
| 2756 |
|
| 2757 |
const handleHighlightAdd = () => { |
| 2758 |
setShowHighlightModal(true); |
| 2759 |
setModalInput({ text: "", question: "", answer: "" }); |
| 2760 |
}; |
| 2761 |
|
| 2762 |
const handleHighlightSave = () => { |
| 2763 |
if (modalInput.text && modalInput.text.trim()) { |
| 2764 |
setFormData((prev) => ({ |
| 2765 |
...prev, |
| 2766 |
highlights: [...prev.highlights, modalInput.text.trim()], |
| 2767 |
})); |
| 2768 |
setShowHighlightModal(false); |
| 2769 |
setModalInput({ text: "", question: "", answer: "" }); |
| 2770 |
} |
| 2771 |
}; |
| 2772 |
|
| 2773 |
const handleHighlightRemove = (index: number) => { |
| 2774 |
setFormData((prev) => ({ |
| 2775 |
...prev, |
| 2776 |
highlights: prev.highlights.filter((_, i) => i !== index), |
| 2777 |
})); |
| 2778 |
}; |
| 2779 |
|
| 2780 |
// Included/Excluded handlers - will be integrated into ItinerarySection component |
| 2781 |
// Keeping for future integration when included/excluded are merged into itinerary section |
| 2782 |
// const handleIncludedAdd = () => { ... }; |
| 2783 |
// const handleIncludedRemove = (index: number) => { ... }; |
| 2784 |
// const handleExcludedAdd = () => { ... }; |
| 2785 |
// const handleExcludedRemove = (index: number) => { ... }; |
| 2786 |
|
| 2787 |
const handleFAQAdd = () => { |
| 2788 |
setFormData((prev) => ({ |
| 2789 |
...prev, |
| 2790 |
faqs: [...prev.faqs, { question: "", answer: "" }], |
| 2791 |
})); |
| 2792 |
}; |
| 2793 |
|
| 2794 |
const handleFAQRemove = (index: number) => { |
| 2795 |
setFormData((prev) => ({ |
| 2796 |
...prev, |
| 2797 |
faqs: prev.faqs.filter((_, i) => i !== index), |
| 2798 |
})); |
| 2799 |
}; |
| 2800 |
|
| 2801 |
const handleFAQChange = ( |
| 2802 |
index: number, |
| 2803 |
field: "question" | "answer", |
| 2804 |
value: string, |
| 2805 |
) => { |
| 2806 |
setFormData((prev) => ({ |
| 2807 |
...prev, |
| 2808 |
faqs: prev.faqs.map((faq, i) => |
| 2809 |
i === index ? { ...faq, [field]: value } : faq, |
| 2810 |
), |
| 2811 |
})); |
| 2812 |
}; |
| 2813 |
|
| 2814 |
const handleGalleryAdd = () => { |
| 2815 |
// Use WordPress media library with multiple selection |
| 2816 |
if (window.wp && window.wp.media) { |
| 2817 |
const mediaUploader = window.wp.media({ |
| 2818 |
title: __("Select Gallery Images", "yatra"), |
| 2819 |
button: { text: __("Add to Gallery", "yatra") }, |
| 2820 |
multiple: true, // Allow multiple image selection |
| 2821 |
library: { type: "image" }, |
| 2822 |
}); |
| 2823 |
|
| 2824 |
mediaUploader.on("select", () => { |
| 2825 |
const selection = mediaUploader.state().get("selection"); |
| 2826 |
const newImages: Array<{ |
| 2827 |
id: number; |
| 2828 |
url: string; |
| 2829 |
thumbnail_url?: string; |
| 2830 |
alt_text?: string; |
| 2831 |
caption?: string; |
| 2832 |
}> = []; |
| 2833 |
|
| 2834 |
selection.each((attachment: any) => { |
| 2835 |
const image = attachment.toJSON(); |
| 2836 |
if (image.url) { |
| 2837 |
newImages.push({ |
| 2838 |
id: image.id || 0, |
| 2839 |
url: image.url, |
| 2840 |
thumbnail_url: |
| 2841 |
image.sizes?.thumbnail?.url || |
| 2842 |
image.sizes?.medium?.url || |
| 2843 |
image.url, |
| 2844 |
alt_text: image.alt || "", |
| 2845 |
caption: image.caption || "", |
| 2846 |
}); |
| 2847 |
} |
| 2848 |
}); |
| 2849 |
|
| 2850 |
if (newImages.length > 0) { |
| 2851 |
setFormData((prev) => ({ |
| 2852 |
...prev, |
| 2853 |
gallery_images: [...prev.gallery_images, ...newImages], |
| 2854 |
})); |
| 2855 |
} |
| 2856 |
}); |
| 2857 |
|
| 2858 |
prepareWordPressMediaFrameOpen(); |
| 2859 |
mediaUploader.open(); |
| 2860 |
} else { |
| 2861 |
// Fallback for when wp.media is not available |
| 2862 |
showToast( |
| 2863 |
__( |
| 2864 |
"Media library not available. Please ensure you are logged in as admin.", |
| 2865 |
"yatra", |
| 2866 |
), |
| 2867 |
"error", |
| 2868 |
); |
| 2869 |
} |
| 2870 |
}; |
| 2871 |
|
| 2872 |
const handleGalleryRemove = (index: number) => { |
| 2873 |
setFormData((prev) => ({ |
| 2874 |
...prev, |
| 2875 |
gallery_images: prev.gallery_images.filter((_, i) => i !== index), |
| 2876 |
})); |
| 2877 |
}; |
| 2878 |
|
| 2879 |
const handleGalleryReorder = (fromIndex: number, toIndex: number) => { |
| 2880 |
setFormData((prev) => { |
| 2881 |
const newImages = [...prev.gallery_images]; |
| 2882 |
const [movedImage] = newImages.splice(fromIndex, 1); |
| 2883 |
newImages.splice(toIndex, 0, movedImage); |
| 2884 |
return { ...prev, gallery_images: newImages }; |
| 2885 |
}); |
| 2886 |
}; |
| 2887 |
|
| 2888 |
const handleDownloadableItemAdd = () => { |
| 2889 |
setFormData((prev) => { |
| 2890 |
const nextOrder = (prev.downloadable_items?.length || 0) + 1; |
| 2891 |
return { |
| 2892 |
...prev, |
| 2893 |
downloadable_items: [ |
| 2894 |
...(prev.downloadable_items || []), |
| 2895 |
{ |
| 2896 |
id: null, |
| 2897 |
title: "", |
| 2898 |
description: "", |
| 2899 |
attachment_id: null, |
| 2900 |
attachment_url: "", |
| 2901 |
attachment_title: "", |
| 2902 |
visibility: "booked_only", |
| 2903 |
enabled: true, |
| 2904 |
sort_order: nextOrder, |
| 2905 |
}, |
| 2906 |
], |
| 2907 |
}; |
| 2908 |
}); |
| 2909 |
}; |
| 2910 |
|
| 2911 |
const handleDownloadableItemRemove = (index: number) => { |
| 2912 |
setFormData((prev) => ({ |
| 2913 |
...prev, |
| 2914 |
downloadable_items: (prev.downloadable_items || []).filter( |
| 2915 |
(_, i) => i !== index, |
| 2916 |
), |
| 2917 |
})); |
| 2918 |
}; |
| 2919 |
|
| 2920 |
const handleDownloadableItemMove = (fromIndex: number, toIndex: number) => { |
| 2921 |
setFormData((prev) => { |
| 2922 |
const items = [...(prev.downloadable_items || [])]; |
| 2923 |
const [moved] = items.splice(fromIndex, 1); |
| 2924 |
items.splice(toIndex, 0, moved); |
| 2925 |
|
| 2926 |
const normalized = items.map((item, idx) => ({ |
| 2927 |
...item, |
| 2928 |
sort_order: idx + 1, |
| 2929 |
})); |
| 2930 |
|
| 2931 |
return { ...prev, downloadable_items: normalized }; |
| 2932 |
}); |
| 2933 |
}; |
| 2934 |
|
| 2935 |
const handleDownloadableItemChange = ( |
| 2936 |
index: number, |
| 2937 |
field: keyof DownloadableItem, |
| 2938 |
value: any, |
| 2939 |
) => { |
| 2940 |
setFormData((prev) => ({ |
| 2941 |
...prev, |
| 2942 |
downloadable_items: (prev.downloadable_items || []).map((item, i) => |
| 2943 |
i === index ? { ...item, [field]: value } : item, |
| 2944 |
), |
| 2945 |
})); |
| 2946 |
}; |
| 2947 |
|
| 2948 |
const handleDownloadableItemSelectFile = (index: number) => { |
| 2949 |
if (window.wp && window.wp.media) { |
| 2950 |
const mediaUploader = window.wp.media({ |
| 2951 |
title: __("Select File", "yatra"), |
| 2952 |
button: { text: __("Use this file", "yatra") }, |
| 2953 |
multiple: false, |
| 2954 |
}); |
| 2955 |
|
| 2956 |
mediaUploader.on("select", () => { |
| 2957 |
const selection = mediaUploader.state().get("selection"); |
| 2958 |
const attachment = selection.first(); |
| 2959 |
if (!attachment) return; |
| 2960 |
const file = attachment.toJSON(); |
| 2961 |
|
| 2962 |
handleDownloadableItemChange(index, "attachment_id", file.id || null); |
| 2963 |
handleDownloadableItemChange(index, "attachment_url", file.url || ""); |
| 2964 |
handleDownloadableItemChange( |
| 2965 |
index, |
| 2966 |
"attachment_title", |
| 2967 |
file.title || file.filename || "", |
| 2968 |
); |
| 2969 |
}); |
| 2970 |
|
| 2971 |
prepareWordPressMediaFrameOpen(); |
| 2972 |
mediaUploader.open(); |
| 2973 |
} else { |
| 2974 |
// Fallback for when wp.media is not available |
| 2975 |
showToast( |
| 2976 |
__( |
| 2977 |
"Media library not available. Please ensure you are logged in as admin.", |
| 2978 |
"yatra", |
| 2979 |
), |
| 2980 |
"error", |
| 2981 |
); |
| 2982 |
} |
| 2983 |
}; |
| 2984 |
|
| 2985 |
const handlePriceTypeAdd = (categoryId: number) => { |
| 2986 |
// Check if category already exists (compare as numbers to handle string/number mismatch) |
| 2987 |
if ( |
| 2988 |
formData.price_types.some( |
| 2989 |
(pt) => Number(pt.category_id) === Number(categoryId), |
| 2990 |
) |
| 2991 |
) { |
| 2992 |
showToast( |
| 2993 |
__("This category already has pricing set", "yatra"), |
| 2994 |
"warning", |
| 2995 |
); |
| 2996 |
return; |
| 2997 |
} |
| 2998 |
setFormData((prev) => ({ |
| 2999 |
...prev, |
| 3000 |
price_types: [ |
| 3001 |
...prev.price_types, |
| 3002 |
{ |
| 3003 |
category_id: categoryId, |
| 3004 |
original_price: "", |
| 3005 |
discounted_price: "", |
| 3006 |
is_default: false, |
| 3007 |
}, |
| 3008 |
], |
| 3009 |
})); |
| 3010 |
}; |
| 3011 |
|
| 3012 |
const handlePriceTypeRemove = (categoryId: number) => { |
| 3013 |
setFormData((prev) => ({ |
| 3014 |
...prev, |
| 3015 |
price_types: prev.price_types.filter( |
| 3016 |
(pt) => Number(pt.category_id) !== Number(categoryId), |
| 3017 |
), |
| 3018 |
})); |
| 3019 |
}; |
| 3020 |
|
| 3021 |
const handlePriceTypeChange = ( |
| 3022 |
categoryId: number, |
| 3023 |
field: "original_price" | "discounted_price", |
| 3024 |
value: string, |
| 3025 |
) => { |
| 3026 |
setFormData((prev) => ({ |
| 3027 |
...prev, |
| 3028 |
price_types: prev.price_types.map((pt) => |
| 3029 |
Number(pt.category_id) === Number(categoryId) |
| 3030 |
? { ...pt, [field]: value } |
| 3031 |
: pt, |
| 3032 |
), |
| 3033 |
})); |
| 3034 |
}; |
| 3035 |
|
| 3036 |
const handlePriceTypeDefaultChange = ( |
| 3037 |
categoryId: number, |
| 3038 |
isDefault: boolean, |
| 3039 |
) => { |
| 3040 |
setFormData((prev) => ({ |
| 3041 |
...prev, |
| 3042 |
price_types: prev.price_types.map((pt) => { |
| 3043 |
if (Number(pt.category_id) === Number(categoryId)) { |
| 3044 |
return { ...pt, is_default: isDefault }; |
| 3045 |
} |
| 3046 |
// Only allow one default at a time. |
| 3047 |
return isDefault ? { ...pt, is_default: false } : pt; |
| 3048 |
}), |
| 3049 |
})); |
| 3050 |
}; |
| 3051 |
|
| 3052 |
// Frontend Tabs Handlers |
| 3053 |
const handleTabToggle = (tabId: string) => { |
| 3054 |
setFormData((prev) => ({ |
| 3055 |
...prev, |
| 3056 |
frontend_tabs: prev.frontend_tabs.map((tab) => |
| 3057 |
tab.id === tabId ? { ...tab, enabled: !tab.enabled } : tab, |
| 3058 |
), |
| 3059 |
})); |
| 3060 |
}; |
| 3061 |
|
| 3062 |
// Handle dummy data fill |
| 3063 |
const handleFillDummyData = () => { |
| 3064 |
// Cycle through dummy data sets |
| 3065 |
const nextIndex = (dummyDataIndex + 1) % dummyTripsData.length; |
| 3066 |
setDummyDataIndex(nextIndex); |
| 3067 |
|
| 3068 |
const dummyData = dummyTripsData[nextIndex]; |
| 3069 |
|
| 3070 |
// Populate form with dummy data |
| 3071 |
setFormData({ |
| 3072 |
...dummyData, |
| 3073 |
// Ensure arrays are properly set |
| 3074 |
destinations: dummyData.destinations || [], |
| 3075 |
activity_types: dummyData.activity_types || [], |
| 3076 |
price_types: dummyData.price_types || [], |
| 3077 |
included_items: dummyData.included_items || [], |
| 3078 |
excluded_items: dummyData.excluded_items || [], |
| 3079 |
highlights: dummyData.highlights || [], |
| 3080 |
faqs: dummyData.faqs || [], |
| 3081 |
gallery_images: dummyData.gallery_images || [], |
| 3082 |
itinerary_days: dummyData.itinerary_days || [], |
| 3083 |
tags: dummyData.tags || [], |
| 3084 |
testimonial_review_ids: dummyData.testimonial_review_ids || [], |
| 3085 |
countries: dummyData.countries || [], |
| 3086 |
regions: dummyData.regions || [], |
| 3087 |
landmarks: dummyData.landmarks || [], |
| 3088 |
availability_dates: dummyData.availability_dates || [], |
| 3089 |
frontend_tabs: dummyData.frontend_tabs || [], |
| 3090 |
has_default_time_slots: dummyData.has_default_time_slots || false, |
| 3091 |
default_time_slots: dummyData.default_time_slots || [], |
| 3092 |
departure_time: dummyData.departure_time || "09:00", |
| 3093 |
downloadable_items: dummyData.downloadable_items || [], |
| 3094 |
}); |
| 3095 |
|
| 3096 |
// Show toast notification |
| 3097 |
showToast( |
| 3098 |
__("Dummy data filled", "yatra") + |
| 3099 |
` (${nextIndex + 1}/${dummyTripsData.length})`, |
| 3100 |
"success", |
| 3101 |
); |
| 3102 |
}; |
| 3103 |
|
| 3104 |
const handleTabLabelChange = (tabId: string, label: string) => { |
| 3105 |
setFormData((prev) => ({ |
| 3106 |
...prev, |
| 3107 |
frontend_tabs: prev.frontend_tabs.map((tab) => |
| 3108 |
tab.id === tabId ? { ...tab, label } : tab, |
| 3109 |
), |
| 3110 |
})); |
| 3111 |
}; |
| 3112 |
|
| 3113 |
const handleTabMove = (tabId: string, direction: "up" | "down") => { |
| 3114 |
setFormData((prev) => { |
| 3115 |
const tabs = [...prev.frontend_tabs]; |
| 3116 |
const index = tabs.findIndex((t) => t.id === tabId); |
| 3117 |
if (index === -1) return prev; |
| 3118 |
|
| 3119 |
const newIndex = direction === "up" ? index - 1 : index + 1; |
| 3120 |
if (newIndex < 0 || newIndex >= tabs.length) return prev; |
| 3121 |
|
| 3122 |
[tabs[index], tabs[newIndex]] = [tabs[newIndex], tabs[index]]; |
| 3123 |
// Update order |
| 3124 |
tabs.forEach((tab, i) => { |
| 3125 |
tab.order = i + 1; |
| 3126 |
}); |
| 3127 |
|
| 3128 |
return { ...prev, frontend_tabs: tabs }; |
| 3129 |
}); |
| 3130 |
}; |
| 3131 |
|
| 3132 |
const handleTabRemove = (tabId: string) => { |
| 3133 |
setFormData((prev) => ({ |
| 3134 |
...prev, |
| 3135 |
frontend_tabs: prev.frontend_tabs |
| 3136 |
.filter((tab) => tab.id !== tabId) |
| 3137 |
.map((tab, i) => ({ ...tab, order: i + 1 })), |
| 3138 |
})); |
| 3139 |
}; |
| 3140 |
|
| 3141 |
const handleTabAdd = () => { |
| 3142 |
const newTabId = `custom_${Date.now()}`; |
| 3143 |
const maxOrder = Math.max(...formData.frontend_tabs.map((t) => t.order), 0); |
| 3144 |
setFormData((prev) => ({ |
| 3145 |
...prev, |
| 3146 |
frontend_tabs: [ |
| 3147 |
...prev.frontend_tabs, |
| 3148 |
{ |
| 3149 |
id: newTabId, |
| 3150 |
label: __("New Tab", "yatra"), |
| 3151 |
enabled: true, |
| 3152 |
order: maxOrder + 1, |
| 3153 |
content_type: "custom", |
| 3154 |
custom_content: "", |
| 3155 |
}, |
| 3156 |
], |
| 3157 |
})); |
| 3158 |
}; |
| 3159 |
|
| 3160 |
const handleTabContentTypeChange = ( |
| 3161 |
tabId: string, |
| 3162 |
contentType: FrontendTab["content_type"], |
| 3163 |
) => { |
| 3164 |
setFormData((prev) => ({ |
| 3165 |
...prev, |
| 3166 |
frontend_tabs: prev.frontend_tabs.map((tab) => |
| 3167 |
tab.id === tabId ? { ...tab, content_type: contentType } : tab, |
| 3168 |
), |
| 3169 |
})); |
| 3170 |
}; |
| 3171 |
|
| 3172 |
const handleTabCustomContentChange = (tabId: string, content: string) => { |
| 3173 |
setFormData((prev) => ({ |
| 3174 |
...prev, |
| 3175 |
frontend_tabs: prev.frontend_tabs.map((tab) => |
| 3176 |
tab.id === tabId ? { ...tab, custom_content: content } : tab, |
| 3177 |
), |
| 3178 |
})); |
| 3179 |
}; |
| 3180 |
|
| 3181 |
const handleTabIconChange = (tabId: string, icon: IconPickerValue | null) => { |
| 3182 |
setFormData((prev) => ({ |
| 3183 |
...prev, |
| 3184 |
frontend_tabs: prev.frontend_tabs.map((tab) => |
| 3185 |
tab.id === tabId ? { ...tab, icon } : tab, |
| 3186 |
), |
| 3187 |
})); |
| 3188 |
}; |
| 3189 |
|
| 3190 |
// Drag and drop handlers |
| 3191 |
const [draggedTab, setDraggedTab] = useState<string | null>(null); |
| 3192 |
const [dragOverTab, setDragOverTab] = useState<string | null>(null); |
| 3193 |
|
| 3194 |
const handleDragStart = (e: React.DragEvent, tabId: string) => { |
| 3195 |
setDraggedTab(tabId); |
| 3196 |
e.dataTransfer.effectAllowed = "move"; |
| 3197 |
}; |
| 3198 |
|
| 3199 |
const handleDragOver = (e: React.DragEvent) => { |
| 3200 |
e.preventDefault(); |
| 3201 |
e.dataTransfer.dropEffect = "move"; |
| 3202 |
}; |
| 3203 |
|
| 3204 |
const handleDragEnter = (tabId: string) => { |
| 3205 |
setDragOverTab(tabId); |
| 3206 |
}; |
| 3207 |
|
| 3208 |
const handleDragLeave = () => { |
| 3209 |
setDragOverTab(null); |
| 3210 |
}; |
| 3211 |
|
| 3212 |
const handleDrop = (e: React.DragEvent, targetTabId: string) => { |
| 3213 |
e.preventDefault(); |
| 3214 |
setDragOverTab(null); |
| 3215 |
|
| 3216 |
if (!draggedTab || draggedTab === targetTabId) { |
| 3217 |
setDraggedTab(null); |
| 3218 |
return; |
| 3219 |
} |
| 3220 |
|
| 3221 |
setFormData((prev) => { |
| 3222 |
const tabs = [...prev.frontend_tabs]; |
| 3223 |
const draggedIndex = tabs.findIndex((t) => t.id === draggedTab); |
| 3224 |
const targetIndex = tabs.findIndex((t) => t.id === targetTabId); |
| 3225 |
|
| 3226 |
if (draggedIndex === -1 || targetIndex === -1) { |
| 3227 |
return prev; |
| 3228 |
} |
| 3229 |
|
| 3230 |
// Remove dragged tab and insert at new position |
| 3231 |
const [draggedTabObj] = tabs.splice(draggedIndex, 1); |
| 3232 |
tabs.splice(targetIndex, 0, draggedTabObj); |
| 3233 |
|
| 3234 |
// Update order values |
| 3235 |
tabs.forEach((tab, i) => { |
| 3236 |
tab.order = i + 1; |
| 3237 |
}); |
| 3238 |
|
| 3239 |
return { ...prev, frontend_tabs: tabs }; |
| 3240 |
}); |
| 3241 |
|
| 3242 |
setDraggedTab(null); |
| 3243 |
}; |
| 3244 |
|
| 3245 |
const handleDragEnd = () => { |
| 3246 |
setDraggedTab(null); |
| 3247 |
setDragOverTab(null); |
| 3248 |
}; |
| 3249 |
|
| 3250 |
const buildEssentialFieldErrors = (): Record<string, string> => { |
| 3251 |
const newErrors: Record<string, string> = {}; |
| 3252 |
|
| 3253 |
if (!formData.title.trim()) { |
| 3254 |
newErrors.title = __("Title is required", "yatra"); |
| 3255 |
} |
| 3256 |
|
| 3257 |
if (!formData.slug.trim()) { |
| 3258 |
newErrors.slug = __("Slug is required", "yatra"); |
| 3259 |
} else if (!/^[\p{L}\p{N}-]+$/u.test(formData.slug)) { |
| 3260 |
newErrors.slug = __( |
| 3261 |
"Slug can only contain letters, numbers, and hyphens", |
| 3262 |
"yatra", |
| 3263 |
); |
| 3264 |
} |
| 3265 |
|
| 3266 |
return newErrors; |
| 3267 |
}; |
| 3268 |
|
| 3269 |
const validateForm = (): boolean => { |
| 3270 |
const newErrors = buildEssentialFieldErrors(); |
| 3271 |
setErrors(newErrors); |
| 3272 |
return Object.keys(newErrors).length === 0; |
| 3273 |
}; |
| 3274 |
|
| 3275 |
// Save mutation |
| 3276 |
const saveMutation = useMutation({ |
| 3277 |
mutationFn: async ( |
| 3278 |
data: TripFormData & { status?: "draft" | "publish" }, |
| 3279 |
) => { |
| 3280 |
const payload = { |
| 3281 |
title: data.title.trim(), |
| 3282 |
slug: data.slug.trim(), |
| 3283 |
description: data.description.trim(), |
| 3284 |
short_description: data.short_description.trim(), |
| 3285 |
highlights: data.highlights, |
| 3286 |
trip_details: data.trip_details.trim(), |
| 3287 |
what_makes_special: data.what_makes_special.trim(), |
| 3288 |
trip_story: data.trip_story.trim(), |
| 3289 |
video_url: data.video_url.trim(), |
| 3290 |
virtual_tour_url: data.virtual_tour_url.trim(), |
| 3291 |
testimonial_review_ids: Array.isArray(data.testimonial_review_ids) |
| 3292 |
? data.testimonial_review_ids.filter( |
| 3293 |
(id): id is number => id !== null && id !== undefined && id > 0, |
| 3294 |
) |
| 3295 |
: [], |
| 3296 |
destinations: data.destinations || [], // Array of destination IDs |
| 3297 |
starting_location: data.starting_location.trim(), |
| 3298 |
ending_location: data.ending_location.trim(), |
| 3299 |
countries: data.countries || [], |
| 3300 |
regions: data.regions || [], |
| 3301 |
starting_latitude: data.starting_latitude |
| 3302 |
? parseFloat(data.starting_latitude) |
| 3303 |
: null, |
| 3304 |
starting_longitude: data.starting_longitude |
| 3305 |
? parseFloat(data.starting_longitude) |
| 3306 |
: null, |
| 3307 |
ending_latitude: data.ending_latitude |
| 3308 |
? parseFloat(data.ending_latitude) |
| 3309 |
: null, |
| 3310 |
ending_longitude: data.ending_longitude |
| 3311 |
? parseFloat(data.ending_longitude) |
| 3312 |
: null, |
| 3313 |
landmarks: data.landmarks || [], |
| 3314 |
trip_type: data.trip_type, |
| 3315 |
duration_days: data.duration_days ? parseInt(data.duration_days) : null, |
| 3316 |
duration_nights: data.duration_nights |
| 3317 |
? parseInt(data.duration_nights) |
| 3318 |
: null, |
| 3319 |
available_from: data.available_from || null, |
| 3320 |
available_to: data.available_to || null, |
| 3321 |
booking_window_days: data.booking_window_days |
| 3322 |
? parseInt(data.booking_window_days) |
| 3323 |
: null, |
| 3324 |
seasonal_availability: data.seasonal_availability || "", |
| 3325 |
best_season: data.best_season.trim(), |
| 3326 |
peak_season: data.peak_season.trim(), |
| 3327 |
off_season: data.off_season.trim(), |
| 3328 |
activity_types: data.activity_types || [], // Array of activity IDs |
| 3329 |
difficulty_level: parseInt(data.difficulty_level) || null, |
| 3330 |
trip_category: (() => { |
| 3331 |
const rawCategories = data.trip_category || []; |
| 3332 |
|
| 3333 |
// If no trip categories available yet, don't filter - send raw data |
| 3334 |
if (tripCategories.length === 0) { |
| 3335 |
return rawCategories; |
| 3336 |
} |
| 3337 |
|
| 3338 |
// Filter valid categories - save exactly what user selects |
| 3339 |
const validCategories = rawCategories.filter((_catId) => { |
| 3340 |
// Always return true - save all selected categories regardless of availability |
| 3341 |
return true; |
| 3342 |
}); |
| 3343 |
|
| 3344 |
return validCategories; |
| 3345 |
})(), |
| 3346 |
tags: data.tags || [], |
| 3347 |
accommodation_type: data.accommodation_type || "", |
| 3348 |
meal_plan: data.meal_plan || "", |
| 3349 |
accommodation_details: data.accommodation_details.trim(), |
| 3350 |
transportation_included: data.transportation_included || false, |
| 3351 |
pickup_location: data.pickup_location.trim(), |
| 3352 |
dropoff_location: data.dropoff_location.trim(), |
| 3353 |
transportation_details: data.transportation_details.trim(), |
| 3354 |
pricing_type: data.pricing_type, |
| 3355 |
original_price: |
| 3356 |
data.pricing_type === "regular" |
| 3357 |
? data.original_price |
| 3358 |
? parseFloat(data.original_price) |
| 3359 |
: null |
| 3360 |
: null, |
| 3361 |
discounted_price: |
| 3362 |
data.pricing_type === "regular" |
| 3363 |
? data.discounted_price |
| 3364 |
? parseFloat(data.discounted_price) |
| 3365 |
: null |
| 3366 |
: null, |
| 3367 |
price_types: |
| 3368 |
data.pricing_type === "traveler_based" |
| 3369 |
? data.price_types.map((pt) => ({ |
| 3370 |
category_id: pt.category_id, |
| 3371 |
is_default: Boolean((pt as any).is_default), |
| 3372 |
original_price: pt.original_price |
| 3373 |
? parseFloat(pt.original_price) |
| 3374 |
: null, |
| 3375 |
discounted_price: pt.discounted_price |
| 3376 |
? parseFloat(pt.discounted_price) |
| 3377 |
: null, |
| 3378 |
})) |
| 3379 |
: [], |
| 3380 |
deposit_amount: data.deposit_amount |
| 3381 |
? parseFloat(data.deposit_amount) |
| 3382 |
: null, |
| 3383 |
deposit_percentage: data.deposit_percentage |
| 3384 |
? parseFloat(data.deposit_percentage) |
| 3385 |
: null, |
| 3386 |
payment_terms: data.payment_terms.trim(), |
| 3387 |
max_travelers: data.max_travelers ? parseInt(data.max_travelers) : null, |
| 3388 |
min_travelers: data.min_travelers ? parseInt(data.min_travelers) : null, |
| 3389 |
booking_deadline_hours: data.booking_deadline_hours || null, |
| 3390 |
cancellation_policy: data.cancellation_policy || "", |
| 3391 |
custom_fields: { disable_booking: data.disable_booking }, |
| 3392 |
age_min: data.age_min ? parseInt(data.age_min) : null, |
| 3393 |
age_max: data.age_max ? parseInt(data.age_max) : null, |
| 3394 |
physical_requirements: data.physical_requirements.trim(), |
| 3395 |
visa_requirements: data.visa_requirements.trim(), |
| 3396 |
vaccination_requirements: data.vaccination_requirements.trim(), |
| 3397 |
has_default_time_slots: data.has_default_time_slots || false, |
| 3398 |
default_time_slots: JSON.stringify(data.default_time_slots || []), |
| 3399 |
departure_time: data.departure_time || "09:00", |
| 3400 |
included_items: (data.included_items || []) |
| 3401 |
.map((item) => ({ |
| 3402 |
title: item.title?.trim() || "", |
| 3403 |
description: item.description?.trim() || "", |
| 3404 |
})) |
| 3405 |
.filter((item) => item.title), |
| 3406 |
excluded_items: (data.excluded_items || []) |
| 3407 |
.map((item) => ({ |
| 3408 |
title: item.title?.trim() || "", |
| 3409 |
description: item.description?.trim() || "", |
| 3410 |
})) |
| 3411 |
.filter((item) => item.title), |
| 3412 |
itinerary_days: data.itinerary_days || [], |
| 3413 |
gallery_images: data.gallery_images || [], |
| 3414 |
featured_image: data.featured_image ?? null, |
| 3415 |
faqs: data.faqs || [], |
| 3416 |
frontend_tabs: data.frontend_tabs.map((tab) => ({ |
| 3417 |
id: tab.id, |
| 3418 |
label: tab.label, |
| 3419 |
enabled: tab.enabled, |
| 3420 |
order: tab.order, |
| 3421 |
content_type: tab.content_type, |
| 3422 |
custom_content: tab.custom_content || null, |
| 3423 |
icon: tab.icon || null, |
| 3424 |
})), |
| 3425 |
availability_dates: data.availability_dates.map((avail) => ({ |
| 3426 |
id: avail.id, |
| 3427 |
departure_date: avail.departure_date || null, |
| 3428 |
arrival_date: avail.arrival_date || null, |
| 3429 |
seats_remaining: avail.seats_remaining || null, |
| 3430 |
original_price: avail.original_price |
| 3431 |
? parseFloat(avail.original_price) |
| 3432 |
: null, |
| 3433 |
discounted_price: avail.discounted_price |
| 3434 |
? parseFloat(avail.discounted_price) |
| 3435 |
: null, |
| 3436 |
discount_percentage: avail.discount_percentage |
| 3437 |
? parseFloat(avail.discount_percentage) |
| 3438 |
: null, |
| 3439 |
status: avail.status || "available", |
| 3440 |
from_location: avail.from_location || null, |
| 3441 |
to_location: avail.to_location || null, |
| 3442 |
from_latitude: avail.from_latitude || null, |
| 3443 |
from_longitude: avail.from_longitude || null, |
| 3444 |
to_latitude: avail.to_latitude || null, |
| 3445 |
to_longitude: avail.to_longitude || null, |
| 3446 |
})), |
| 3447 |
status: data.status === "publish" ? "publish" : "draft", |
| 3448 |
scheduled_publish_date: data.scheduled_publish_date || null, |
| 3449 |
scheduled_unpublish_date: data.scheduled_unpublish_date || null, |
| 3450 |
version: data.version || 1, |
| 3451 |
seasonal_auto_enable: data.seasonal_auto_enable || false, |
| 3452 |
seasonal_enable_date: data.seasonal_enable_date || null, |
| 3453 |
seasonal_disable_date: data.seasonal_disable_date || null, |
| 3454 |
meta_title: data.meta_title || "", |
| 3455 |
meta_description: data.meta_description || "", |
| 3456 |
meta_keywords: data.meta_keywords || "", |
| 3457 |
attributes: data.attributes || {}, |
| 3458 |
featured_priority: data.featured_priority, |
| 3459 |
}; |
| 3460 |
|
| 3461 |
if (showDownloadsUI) { |
| 3462 |
(payload as any).downloadable_items = (data.downloadable_items || []) |
| 3463 |
.map((item, idx) => ({ |
| 3464 |
id: item.id ?? null, |
| 3465 |
title: (item.title || "").trim(), |
| 3466 |
description: item.description || "", |
| 3467 |
attachment_id: item.attachment_id ?? null, |
| 3468 |
visibility: item.visibility || "booked_only", |
| 3469 |
enabled: item.enabled !== false, |
| 3470 |
sort_order: item.sort_order != null ? item.sort_order : idx + 1, |
| 3471 |
attachment_url: item.attachment_url || "", |
| 3472 |
attachment_title: item.attachment_title || "", |
| 3473 |
|
| 3474 |
// Prefixed keys (requested) |
| 3475 |
download_title: (item.title || "").trim(), |
| 3476 |
download_description: item.description || "", |
| 3477 |
download_visibility: item.visibility || "booked_only", |
| 3478 |
download_enabled: item.enabled !== false, |
| 3479 |
download_file: item.attachment_id ?? null, |
| 3480 |
})) |
| 3481 |
.filter((item) => item.title); |
| 3482 |
} |
| 3483 |
|
| 3484 |
if (isEditMode && tripId) { |
| 3485 |
const response = await apiClient.put(`/trips/${tripId}`, payload); |
| 3486 |
return response.data || response; |
| 3487 |
} else { |
| 3488 |
const response = await apiClient.post("/trips", payload); |
| 3489 |
return response.data || response; |
| 3490 |
} |
| 3491 |
}, |
| 3492 |
onSuccess: (data, variables) => { |
| 3493 |
queryClient.invalidateQueries({ queryKey: ["trips"] }); |
| 3494 |
queryClient.invalidateQueries({ queryKey: ["trip", tripId] }); |
| 3495 |
// Force refetch the trip data immediately |
| 3496 |
queryClient.refetchQueries({ queryKey: ["trip", tripId] }); |
| 3497 |
setIsSubmitting(false); |
| 3498 |
|
| 3499 |
// Show success message - different for edit vs create mode |
| 3500 |
let successMessage: string; |
| 3501 |
if (isEditMode) { |
| 3502 |
// Edit mode: updating existing trip |
| 3503 |
successMessage = |
| 3504 |
variables.status === "publish" |
| 3505 |
? __("Trip updated and published successfully", "yatra") |
| 3506 |
: __("Trip updated successfully", "yatra"); |
| 3507 |
} else { |
| 3508 |
// Create mode: creating new trip |
| 3509 |
successMessage = |
| 3510 |
variables.status === "publish" |
| 3511 |
? __("Trip created and published successfully", "yatra") |
| 3512 |
: __("Trip saved as draft successfully", "yatra"); |
| 3513 |
} |
| 3514 |
showToast(successMessage, "success"); |
| 3515 |
|
| 3516 |
// Only redirect when creating a NEW trip - stay on page when editing |
| 3517 |
if (!isEditMode && data?.id) { |
| 3518 |
// If creating new trip, redirect to edit mode so user can continue editing |
| 3519 |
setTimeout(() => { |
| 3520 |
window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&action=edit&id=${data.id}`; |
| 3521 |
}, 1000); |
| 3522 |
} |
| 3523 |
// When editing (isEditMode), always stay on the same page - no redirect |
| 3524 |
}, |
| 3525 |
onError: (error: any) => { |
| 3526 |
const errorMessage = |
| 3527 |
error?.response?.data?.message || |
| 3528 |
error?.message || |
| 3529 |
__("An error occurred while saving", "yatra"); |
| 3530 |
showToast(errorMessage, "error"); |
| 3531 |
setErrors({ submit: errorMessage }); |
| 3532 |
setIsSubmitting(false); |
| 3533 |
}, |
| 3534 |
}); |
| 3535 |
|
| 3536 |
// Light validation for draft saves (only essential fields) |
| 3537 |
const validateDraft = (): boolean => { |
| 3538 |
const newErrors = buildEssentialFieldErrors(); |
| 3539 |
setErrors(newErrors); |
| 3540 |
return Object.keys(newErrors).length === 0; |
| 3541 |
}; |
| 3542 |
|
| 3543 |
const handleSaveDraft = async () => { |
| 3544 |
if (!validateDraft()) { |
| 3545 |
showToast( |
| 3546 |
__("Please add a trip title and URL before saving.", "yatra"), |
| 3547 |
"error", |
| 3548 |
); |
| 3549 |
return; |
| 3550 |
} |
| 3551 |
setIsSubmitting(true); |
| 3552 |
saveMutation.mutate({ ...formData, status: "draft" }); |
| 3553 |
}; |
| 3554 |
|
| 3555 |
// eslint-disable-next-line react-hooks/exhaustive-deps |
| 3556 |
const handlePublish = async () => { |
| 3557 |
if (!validateForm()) { |
| 3558 |
const firstError = Object.keys(errors)[0]; |
| 3559 |
if (firstError) { |
| 3560 |
showToast( |
| 3561 |
__("Trip title and slug are required before publishing.", "yatra"), |
| 3562 |
"error", |
| 3563 |
); |
| 3564 |
const errorElement = document.querySelector( |
| 3565 |
`[name="${firstError}"], #${firstError}`, |
| 3566 |
); |
| 3567 |
if (errorElement) { |
| 3568 |
errorElement.scrollIntoView({ behavior: "smooth", block: "center" }); |
| 3569 |
} |
| 3570 |
} |
| 3571 |
return; |
| 3572 |
} |
| 3573 |
setIsSubmitting(true); |
| 3574 |
saveMutation.mutate({ ...formData, status: "publish" }); |
| 3575 |
}; |
| 3576 |
|
| 3577 |
// Enter should trigger publish/update (skip multiline and interactive controls) |
| 3578 |
useEffect(() => { |
| 3579 |
const onKeyDown = (e: KeyboardEvent) => { |
| 3580 |
if (e.key !== "Enter" || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) |
| 3581 |
return; |
| 3582 |
|
| 3583 |
const target = e.target as HTMLElement | null; |
| 3584 |
if (!target) return; |
| 3585 |
|
| 3586 |
const tag = target.tagName.toLowerCase(); |
| 3587 |
const isTextarea = tag === "textarea"; |
| 3588 |
const isButton = tag === "button"; |
| 3589 |
const isLink = tag === "a"; |
| 3590 |
const isContentEditable = |
| 3591 |
target.getAttribute("contenteditable") === "true"; |
| 3592 |
|
| 3593 |
// Ignore Enter in multiline or interactive elements |
| 3594 |
if (isTextarea || isButton || isLink || isContentEditable) return; |
| 3595 |
|
| 3596 |
const inputType = (target as HTMLInputElement).type; |
| 3597 |
if (["submit", "button", "file"].includes(inputType)) return; |
| 3598 |
|
| 3599 |
e.preventDefault(); |
| 3600 |
if (!isSubmitting) { |
| 3601 |
handlePublish(); |
| 3602 |
} |
| 3603 |
}; |
| 3604 |
|
| 3605 |
window.addEventListener("keydown", onKeyDown); |
| 3606 |
return () => window.removeEventListener("keydown", onKeyDown); |
| 3607 |
}, [isSubmitting, handlePublish]); |
| 3608 |
|
| 3609 |
// Using static revisions data for UI only |
| 3610 |
const revisions = dummyRevisions; |
| 3611 |
const isLoadingRevisions = false; |
| 3612 |
|
| 3613 |
const handlePreview = async () => { |
| 3614 |
const slug = (formData.slug || "").trim(); |
| 3615 |
if (!slug) { |
| 3616 |
showToast( |
| 3617 |
__( |
| 3618 |
"Trip slug is missing. Please add a slug before previewing.", |
| 3619 |
"yatra", |
| 3620 |
), |
| 3621 |
"error", |
| 3622 |
); |
| 3623 |
return; |
| 3624 |
} |
| 3625 |
|
| 3626 |
const siteUrl = (window as any)?.yatraAdmin?.siteUrl || ""; |
| 3627 |
const { plainUrl, prettyUrl } = buildYatraSinglePublicUrls({ |
| 3628 |
entity: "trip", |
| 3629 |
slug, |
| 3630 |
siteUrl, |
| 3631 |
bases: settingsData as Record<string, unknown> | null, |
| 3632 |
}); |
| 3633 |
|
| 3634 |
if (isWordPressPlainPermalink()) { |
| 3635 |
window.open(plainUrl, "_blank", "noopener,noreferrer"); |
| 3636 |
return; |
| 3637 |
} |
| 3638 |
|
| 3639 |
let apiPermalink = (tripData as any)?.permalink || (tripData as any)?.url; |
| 3640 |
|
| 3641 |
if (!apiPermalink && tripId) { |
| 3642 |
try { |
| 3643 |
const detail = await apiClient.get(`/trips/${tripId}`); |
| 3644 |
apiPermalink = |
| 3645 |
(detail as any)?.permalink || (detail as any)?.url || apiPermalink; |
| 3646 |
} catch (error) { |
| 3647 |
// fall through to prettyUrl |
| 3648 |
} |
| 3649 |
} |
| 3650 |
|
| 3651 |
if (apiPermalink) { |
| 3652 |
window.open(apiPermalink, "_blank", "noopener,noreferrer"); |
| 3653 |
return; |
| 3654 |
} |
| 3655 |
|
| 3656 |
window.open(prettyUrl, "_blank", "noopener,noreferrer"); |
| 3657 |
}; |
| 3658 |
|
| 3659 |
const handleRevisionClick = (revisionId: number) => { |
| 3660 |
setSelectedRevisionId(revisionId); |
| 3661 |
setShowRevisionConfirm(true); |
| 3662 |
}; |
| 3663 |
|
| 3664 |
const handleRevisionConfirm = () => { |
| 3665 |
// UI only - no actual functionality |
| 3666 |
if (selectedRevisionId) { |
| 3667 |
showToast(__("Revision feature is coming soon", "yatra"), "info"); |
| 3668 |
} |
| 3669 |
setShowRevisionConfirm(false); |
| 3670 |
setShowRevisionsDialog(false); |
| 3671 |
setSelectedRevisionId(null); |
| 3672 |
}; |
| 3673 |
|
| 3674 |
// Skeleton loader for edit mode |
| 3675 |
if (isEditMode && isLoadingTrip) { |
| 3676 |
return ( |
| 3677 |
<div className="flex-1 min-h-0 flex flex-col bg-white dark:bg-gray-900 overflow-hidden"> |
| 3678 |
{/* Header Skeleton */} |
| 3679 |
<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"> |
| 3680 |
<div className="flex items-center gap-4 min-w-0 flex-1"> |
| 3681 |
<div className="min-w-0"> |
| 3682 |
<div className="h-6 w-64 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2" /> |
| 3683 |
<div className="flex items-center gap-3"> |
| 3684 |
<div className="h-5 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3685 |
<div className="h-6 w-28 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse" /> |
| 3686 |
</div> |
| 3687 |
</div> |
| 3688 |
</div> |
| 3689 |
<div className="flex items-center gap-2"> |
| 3690 |
<div className="h-9 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3691 |
<div className="h-9 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3692 |
<div className="h-9 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3693 |
<div className="h-9 w-36 bg-blue-200 dark:bg-blue-800 rounded animate-pulse" /> |
| 3694 |
</div> |
| 3695 |
</div> |
| 3696 |
|
| 3697 |
{/* Navigation Bar Skeleton */} |
| 3698 |
<div className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 px-4 py-2.5"> |
| 3699 |
<div className="flex items-center justify-between gap-4"> |
| 3700 |
<div className="h-9 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3701 |
<div className="flex-1 flex flex-col items-center gap-1 max-w-md mx-4"> |
| 3702 |
<div className="h-3 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3703 |
<div className="w-full h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse" /> |
| 3704 |
<div className="h-2.5 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3705 |
</div> |
| 3706 |
<div className="h-9 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3707 |
</div> |
| 3708 |
</div> |
| 3709 |
|
| 3710 |
{/* Main Content Skeleton */} |
| 3711 |
<div className="flex-1 flex overflow-hidden"> |
| 3712 |
{/* Sidebar Skeleton */} |
| 3713 |
<div className="w-80 bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 overflow-y-auto"> |
| 3714 |
<div className="p-5 pb-8 space-y-6"> |
| 3715 |
{/* Essentials Section Skeleton */} |
| 3716 |
<div> |
| 3717 |
<div className="h-3.5 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-3 px-1" /> |
| 3718 |
<div className="space-y-0.5"> |
| 3719 |
{[...Array(8)].map((_, i) => ( |
| 3720 |
<div |
| 3721 |
key={`essentials-${i}`} |
| 3722 |
className="flex items-center gap-2 px-3 py-2.5 rounded-md" |
| 3723 |
> |
| 3724 |
<div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" /> |
| 3725 |
<div className="h-4 flex-1 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3726 |
</div> |
| 3727 |
))} |
| 3728 |
</div> |
| 3729 |
</div> |
| 3730 |
|
| 3731 |
{/* Marketing Section Skeleton */} |
| 3732 |
<div> |
| 3733 |
<div className="h-3.5 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-3 px-1" /> |
| 3734 |
<div className="space-y-0.5"> |
| 3735 |
{[...Array(9)].map((_, i) => ( |
| 3736 |
<div |
| 3737 |
key={`marketing-${i}`} |
| 3738 |
className="flex items-center gap-2 px-3 py-2.5 rounded-md" |
| 3739 |
> |
| 3740 |
<div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" /> |
| 3741 |
<div className="h-4 flex-1 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3742 |
</div> |
| 3743 |
))} |
| 3744 |
</div> |
| 3745 |
</div> |
| 3746 |
|
| 3747 |
{/* Lifecycle Section Skeleton */} |
| 3748 |
<div> |
| 3749 |
<div className="flex items-center gap-2 px-3 py-2.5 rounded-md"> |
| 3750 |
<div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" /> |
| 3751 |
<div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3752 |
</div> |
| 3753 |
</div> |
| 3754 |
</div> |
| 3755 |
</div> |
| 3756 |
|
| 3757 |
{/* Main Form Area Skeleton */} |
| 3758 |
<div className="flex-1 overflow-y-auto p-6"> |
| 3759 |
<div className="max-w-4xl mx-auto space-y-6"> |
| 3760 |
{/* Section Header Skeleton */} |
| 3761 |
<div className="flex items-center gap-2 mb-4"> |
| 3762 |
<div className="w-5 h-5 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3763 |
<div className="h-6 w-56 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3764 |
</div> |
| 3765 |
<div className="h-4 w-96 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-6" /> |
| 3766 |
|
| 3767 |
{/* Form Fields Skeleton */} |
| 3768 |
<Card> |
| 3769 |
<CardContent className="p-6 space-y-6"> |
| 3770 |
{/* Title Field */} |
| 3771 |
<div className="space-y-2"> |
| 3772 |
<div className="h-3.5 w-12 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3773 |
<div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3774 |
</div> |
| 3775 |
|
| 3776 |
{/* Slug Field */} |
| 3777 |
<div className="space-y-2"> |
| 3778 |
<div className="h-3.5 w-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3779 |
<div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3780 |
</div> |
| 3781 |
|
| 3782 |
{/* Description Field */} |
| 3783 |
<div className="space-y-2"> |
| 3784 |
<div className="h-3.5 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3785 |
<div className="h-32 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3786 |
</div> |
| 3787 |
|
| 3788 |
{/* Featured Image Skeleton */} |
| 3789 |
<div className="space-y-2"> |
| 3790 |
<div className="h-3.5 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3791 |
<div className="h-48 w-full bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse" /> |
| 3792 |
</div> |
| 3793 |
|
| 3794 |
{/* Grid Fields Skeleton */} |
| 3795 |
<div className="grid grid-cols-2 gap-4"> |
| 3796 |
{[...Array(4)].map((_, i) => ( |
| 3797 |
<div key={`grid-${i}`} className="space-y-3"> |
| 3798 |
<div className="h-3.5 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3799 |
<div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 3800 |
</div> |
| 3801 |
))} |
| 3802 |
</div> |
| 3803 |
</CardContent> |
| 3804 |
</Card> |
| 3805 |
</div> |
| 3806 |
</div> |
| 3807 |
</div> |
| 3808 |
</div> |
| 3809 |
); |
| 3810 |
} |
| 3811 |
|
| 3812 |
if (isEditMode && tripError) { |
| 3813 |
const requestInfo = |
| 3814 |
tripErrorContext.requestInfo || (tripError as any)?.config || {}; |
| 3815 |
const method = (requestInfo.method || "GET").toUpperCase?.() || "GET"; |
| 3816 |
const url = requestInfo.url || (tripError as any)?.config?.url || ""; |
| 3817 |
const payload = |
| 3818 |
requestInfo.payload || (tripError as any)?.config?.data || ""; |
| 3819 |
const composedErrorText = `Method: ${method}\nURL: ${url}\nPayload: ${payload || "N/A"}\n\n${tripErrorContext.details || ""}`; |
| 3820 |
|
| 3821 |
return ( |
| 3822 |
<div className="min-h-screen flex items-center justify-center bg-white"> |
| 3823 |
<div className="w-full max-w-5xl px-6 py-12"> |
| 3824 |
<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"> |
| 3825 |
<div className="w-16 h-16 rounded-2xl bg-white border border-[#ffdede] flex items-center justify-center"> |
| 3826 |
<svg |
| 3827 |
className="w-10 h-10 text-red-500" |
| 3828 |
fill="none" |
| 3829 |
stroke="currentColor" |
| 3830 |
viewBox="0 0 24 24" |
| 3831 |
strokeWidth="1.8" |
| 3832 |
> |
| 3833 |
<path |
| 3834 |
strokeLinecap="round" |
| 3835 |
strokeLinejoin="round" |
| 3836 |
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" |
| 3837 |
/> |
| 3838 |
</svg> |
| 3839 |
</div> |
| 3840 |
<div className="space-y-1"> |
| 3841 |
<h2 className="text-2xl font-semibold text-gray-900"> |
| 3842 |
{__("Error Loading Trips", "yatra")} |
| 3843 |
</h2> |
| 3844 |
<p className="text-sm text-gray-600"> |
| 3845 |
{__( |
| 3846 |
"We couldn’t connect to the trips service. Please refresh or try again shortly.", |
| 3847 |
"yatra", |
| 3848 |
)} |
| 3849 |
</p> |
| 3850 |
</div> |
| 3851 |
<div className="flex flex-wrap gap-2 justify-center"> |
| 3852 |
<Button |
| 3853 |
onClick={() => |
| 3854 |
queryClient.invalidateQueries({ queryKey: ["trip", tripId] }) |
| 3855 |
} |
| 3856 |
> |
| 3857 |
{__("Try again", "yatra")} |
| 3858 |
</Button> |
| 3859 |
<Button |
| 3860 |
variant="outline" |
| 3861 |
onClick={() => { |
| 3862 |
if (typeof window !== "undefined") { |
| 3863 |
window.open( |
| 3864 |
"https://wordpress.org/support/plugin/yatra", |
| 3865 |
"_blank", |
| 3866 |
); |
| 3867 |
} |
| 3868 |
}} |
| 3869 |
> |
| 3870 |
{__("Visit support center", "yatra")} |
| 3871 |
</Button> |
| 3872 |
</div> |
| 3873 |
|
| 3874 |
<div className="w-full mt-10"> |
| 3875 |
<div className="relative w-full text-left rounded-2xl border border-gray-200 bg-white shadow-sm space-y-0"> |
| 3876 |
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-200"> |
| 3877 |
<p className="text-sm font-medium text-gray-800"> |
| 3878 |
{__("Technical details", "yatra")} |
| 3879 |
</p> |
| 3880 |
<Button |
| 3881 |
type="button" |
| 3882 |
size="sm" |
| 3883 |
variant="ghost" |
| 3884 |
className="flex items-center gap-2 text-gray-600 hover:text-gray-900" |
| 3885 |
onClick={() => |
| 3886 |
copyErrorDetailsToClipboard(composedErrorText) |
| 3887 |
} |
| 3888 |
> |
| 3889 |
{copiedErrorDetails ? ( |
| 3890 |
<> |
| 3891 |
<Check className="w-4 h-4" /> |
| 3892 |
{__("Copied", "yatra")} |
| 3893 |
</> |
| 3894 |
) : ( |
| 3895 |
<> |
| 3896 |
<Copy className="w-4 h-4" /> |
| 3897 |
{__("Copy details", "yatra")} |
| 3898 |
</> |
| 3899 |
)} |
| 3900 |
</Button> |
| 3901 |
</div> |
| 3902 |
<div className="px-5 py-3 border-b border-gray-200 space-y-2 text-sm text-left text-gray-800"> |
| 3903 |
<div> |
| 3904 |
<span className="font-medium"> |
| 3905 |
{__("Method:", "yatra")} |
| 3906 |
</span>{" "} |
| 3907 |
<span className="font-mono">{method}</span> |
| 3908 |
</div> |
| 3909 |
<div className="break-all"> |
| 3910 |
<span className="font-medium">{__("URL:", "yatra")}</span>{" "} |
| 3911 |
<span className="font-mono"> |
| 3912 |
{url || __("N/A", "yatra")} |
| 3913 |
</span> |
| 3914 |
</div> |
| 3915 |
{payload && ( |
| 3916 |
<div> |
| 3917 |
<span className="font-medium block mb-1"> |
| 3918 |
{__("Payload:", "yatra")} |
| 3919 |
</span> |
| 3920 |
<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"> |
| 3921 |
{payload} |
| 3922 |
</pre> |
| 3923 |
</div> |
| 3924 |
)} |
| 3925 |
</div> |
| 3926 |
<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"> |
| 3927 |
{tripErrorContext.details || |
| 3928 |
JSON.stringify( |
| 3929 |
{ |
| 3930 |
message: |
| 3931 |
tripError instanceof Error |
| 3932 |
? tripError.message |
| 3933 |
: __("Failed to load trip data", "yatra"), |
| 3934 |
method, |
| 3935 |
url, |
| 3936 |
payload, |
| 3937 |
}, |
| 3938 |
null, |
| 3939 |
2, |
| 3940 |
)} |
| 3941 |
</pre> |
| 3942 |
</div> |
| 3943 |
</div> |
| 3944 |
</div> |
| 3945 |
</div> |
| 3946 |
</div> |
| 3947 |
); |
| 3948 |
} |
| 3949 |
|
| 3950 |
const renderSectionContent = () => { |
| 3951 |
switch (currentSection) { |
| 3952 |
case "basic": |
| 3953 |
return ( |
| 3954 |
<div className="space-y-6"> |
| 3955 |
<div className="flex items-center gap-2 mb-3"> |
| 3956 |
<FileText className="w-5 h-5 text-blue-600 dark:text-blue-400" /> |
| 3957 |
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> |
| 3958 |
{__("Basic Information", "yatra")} |
| 3959 |
</h2> |
| 3960 |
<Badge |
| 3961 |
variant="outline" |
| 3962 |
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" |
| 3963 |
> |
| 3964 |
{__("Start Here", "yatra")} |
| 3965 |
</Badge> |
| 3966 |
</div> |
| 3967 |
<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"> |
| 3968 |
<p className="text-sm text-blue-900 dark:text-blue-100 font-medium mb-1"> |
| 3969 |
{__("💡 Getting Started", "yatra")} |
| 3970 |
</p> |
| 3971 |
<p className="text-xs text-blue-800 dark:text-blue-200"> |
| 3972 |
{__( |
| 3973 |
"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.", |
| 3974 |
"yatra", |
| 3975 |
)} |
| 3976 |
</p> |
| 3977 |
</div> |
| 3978 |
|
| 3979 |
{/* Essential Fields - Single View (No Tabs) */} |
| 3980 |
<div className="space-y-6"> |
| 3981 |
{/* Tour Title */} |
| 3982 |
<div className="mb-4"> |
| 3983 |
<div className="flex items-center justify-between mb-1.5"> |
| 3984 |
<label |
| 3985 |
htmlFor="title" |
| 3986 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5" |
| 3987 |
> |
| 3988 |
{__("Trip Title", "yatra")} |
| 3989 |
<Badge |
| 3990 |
variant="outline" |
| 3991 |
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" |
| 3992 |
> |
| 3993 |
{__("Required", "yatra")} |
| 3994 |
</Badge> |
| 3995 |
<button |
| 3996 |
type="button" |
| 3997 |
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto" |
| 3998 |
title={__( |
| 3999 |
"A catchy title that describes your trip. Recommended: 50-60 characters for best SEO results.", |
| 4000 |
"yatra", |
| 4001 |
)} |
| 4002 |
> |
| 4003 |
<HelpCircle className="w-3.5 h-3.5" /> |
| 4004 |
</button> |
| 4005 |
</label> |
| 4006 |
<span |
| 4007 |
className={`text-xs font-medium ${ |
| 4008 |
formData.title.length > 60 |
| 4009 |
? "text-red-600 dark:text-red-400" |
| 4010 |
: formData.title.length >= 50 |
| 4011 |
? "text-green-600 dark:text-green-400" |
| 4012 |
: "text-gray-500 dark:text-gray-400" |
| 4013 |
}`} |
| 4014 |
> |
| 4015 |
{formData.title.length}/60 |
| 4016 |
</span> |
| 4017 |
</div> |
| 4018 |
<HelpText |
| 4019 |
text={__( |
| 4020 |
'💡 Tip: Include your destination and trip length. Example: "Bali Beach Retreat - 7 Days" or "Paris City Tour - Half Day"', |
| 4021 |
"yatra", |
| 4022 |
)} |
| 4023 |
className="mb-2" |
| 4024 |
/> |
| 4025 |
<Input |
| 4026 |
id="title" |
| 4027 |
type="text" |
| 4028 |
value={formData.title} |
| 4029 |
onChange={(e) => handleTitleChange(e.target.value)} |
| 4030 |
placeholder={__("e.g., Bali Beach Retreat - 7 Days", "yatra")} |
| 4031 |
maxLength={100} |
| 4032 |
className={`${errors.title ? "border-red-500" : formData.title && formData.title.length <= 60 ? "border-green-500" : ""} transition-colors`} |
| 4033 |
required |
| 4034 |
/> |
| 4035 |
{errors.title ? ( |
| 4036 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 4037 |
<AlertCircle className="w-4 h-4" /> |
| 4038 |
{errors.title} |
| 4039 |
</p> |
| 4040 |
) : ( |
| 4041 |
formData.title && |
| 4042 |
formData.title.length > 60 && ( |
| 4043 |
<p className="mt-1.5 text-xs text-yellow-600 dark:text-yellow-400 flex items-center gap-1"> |
| 4044 |
<AlertCircle className="w-3.5 h-3.5" /> |
| 4045 |
{__( |
| 4046 |
"Title is longer than recommended for SEO (60 characters)", |
| 4047 |
"yatra", |
| 4048 |
)} |
| 4049 |
</p> |
| 4050 |
) |
| 4051 |
)} |
| 4052 |
</div> |
| 4053 |
|
| 4054 |
{/* URL Slug */} |
| 4055 |
<div className="mb-4"> |
| 4056 |
<div className="flex items-center justify-between mb-1.5"> |
| 4057 |
<label |
| 4058 |
htmlFor="slug" |
| 4059 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5" |
| 4060 |
> |
| 4061 |
{__("Trip URL", "yatra")} |
| 4062 |
<Badge |
| 4063 |
variant="outline" |
| 4064 |
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" |
| 4065 |
> |
| 4066 |
{__("Required", "yatra")} |
| 4067 |
</Badge> |
| 4068 |
<button |
| 4069 |
type="button" |
| 4070 |
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto" |
| 4071 |
title={__( |
| 4072 |
"The web address for your trip page. Auto-generated from your title, but you can customize it.", |
| 4073 |
"yatra", |
| 4074 |
)} |
| 4075 |
> |
| 4076 |
<HelpCircle className="w-3.5 h-3.5" /> |
| 4077 |
</button> |
| 4078 |
</label> |
| 4079 |
{showSlugPreview && formData.slug && ( |
| 4080 |
<button |
| 4081 |
type="button" |
| 4082 |
onClick={() => setShowSlugPreview(false)} |
| 4083 |
className="text-xs text-blue-600 dark:text-blue-400 hover:underline" |
| 4084 |
> |
| 4085 |
{__("Hide", "yatra")} |
| 4086 |
</button> |
| 4087 |
)} |
| 4088 |
</div> |
| 4089 |
<HelpText |
| 4090 |
text={__( |
| 4091 |
"💡 This is automatically created from your title. You usually don't need to change it unless you want a custom web address.", |
| 4092 |
"yatra", |
| 4093 |
)} |
| 4094 |
className="mb-2" |
| 4095 |
/> |
| 4096 |
<Input |
| 4097 |
id="slug" |
| 4098 |
type="text" |
| 4099 |
value={formData.slug} |
| 4100 |
onChange={(e) => handleFieldChange("slug", e.target.value)} |
| 4101 |
placeholder={__("bali-beach-retreat-7-days", "yatra")} |
| 4102 |
className={`font-mono text-sm ${errors.slug ? "border-red-500" : ""}`} |
| 4103 |
required |
| 4104 |
/> |
| 4105 |
{errors.slug && ( |
| 4106 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 4107 |
<AlertCircle className="w-4 h-4" /> |
| 4108 |
{errors.slug} |
| 4109 |
</p> |
| 4110 |
)} |
| 4111 |
{showSlugPreview && formData.slug && !errors.slug && ( |
| 4112 |
<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"> |
| 4113 |
<div> |
| 4114 |
<span className="text-gray-500 dark:text-gray-400"> |
| 4115 |
{__("Preview URL:", "yatra")}{" "} |
| 4116 |
</span> |
| 4117 |
<span className="font-mono text-blue-600 dark:text-blue-400"> |
| 4118 |
{(window as any).yatraAdmin?.siteUrl || "yoursite.com"}/ |
| 4119 |
{settingsData?.trip_base || "trip"}/{formData.slug} |
| 4120 |
</span> |
| 4121 |
</div> |
| 4122 |
<button |
| 4123 |
type="button" |
| 4124 |
onClick={() => { |
| 4125 |
const tripBase = settingsData?.trip_base || "trip"; |
| 4126 |
const url = `${(window as any).yatraAdmin?.siteUrl || "yoursite.com"}/${tripBase}/${formData.slug}`; |
| 4127 |
navigator.clipboard.writeText(url); |
| 4128 |
showToast( |
| 4129 |
__("URL copied to clipboard", "yatra"), |
| 4130 |
"success", |
| 4131 |
); |
| 4132 |
}} |
| 4133 |
className="ml-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-1" |
| 4134 |
title={__("Copy URL", "yatra")} |
| 4135 |
> |
| 4136 |
<Copy className="w-3.5 h-3.5" /> |
| 4137 |
</button> |
| 4138 |
</div> |
| 4139 |
)} |
| 4140 |
{!showSlugPreview && formData.slug && ( |
| 4141 |
<button |
| 4142 |
type="button" |
| 4143 |
onClick={() => setShowSlugPreview(true)} |
| 4144 |
className="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline" |
| 4145 |
> |
| 4146 |
{__("Show URL Preview", "yatra")} |
| 4147 |
</button> |
| 4148 |
)} |
| 4149 |
</div> |
| 4150 |
|
| 4151 |
{/* Short Description */} |
| 4152 |
<div className="mb-4"> |
| 4153 |
<div className="flex items-center justify-between mb-1.5"> |
| 4154 |
<label className="text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5"> |
| 4155 |
{__("Short Description", "yatra")} |
| 4156 |
<Badge |
| 4157 |
variant="outline" |
| 4158 |
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" |
| 4159 |
> |
| 4160 |
{__("Recommended", "yatra")} |
| 4161 |
</Badge> |
| 4162 |
<button |
| 4163 |
type="button" |
| 4164 |
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" |
| 4165 |
title={__( |
| 4166 |
"A brief summary that appears in listings. Recommended: 100-150 characters for best results.", |
| 4167 |
"yatra", |
| 4168 |
)} |
| 4169 |
> |
| 4170 |
<HelpCircle className="w-3.5 h-3.5" /> |
| 4171 |
</button> |
| 4172 |
</label> |
| 4173 |
<div className="flex items-center gap-2"> |
| 4174 |
<span |
| 4175 |
className={`text-xs font-medium ${ |
| 4176 |
formData.short_description.length > 200 |
| 4177 |
? "text-red-600 dark:text-red-400" |
| 4178 |
: formData.short_description.length >= 100 && |
| 4179 |
formData.short_description.length <= 150 |
| 4180 |
? "text-green-600 dark:text-green-400" |
| 4181 |
: formData.short_description.length > 0 |
| 4182 |
? "text-yellow-600 dark:text-yellow-400" |
| 4183 |
: "text-gray-500 dark:text-gray-400" |
| 4184 |
}`} |
| 4185 |
> |
| 4186 |
{formData.short_description.length}/200 |
| 4187 |
</span> |
| 4188 |
<AiFieldAffordance |
| 4189 |
task="trip-short-description" |
| 4190 |
label={__("Short Description", "yatra")} |
| 4191 |
value={formData.short_description} |
| 4192 |
onAccept={(v) => |
| 4193 |
handleFieldChange("short_description", v) |
| 4194 |
} |
| 4195 |
buildContext={() => buildTripAiContext(formData)} |
| 4196 |
/> |
| 4197 |
</div> |
| 4198 |
</div> |
| 4199 |
<HelpText |
| 4200 |
text={__( |
| 4201 |
'💡 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."', |
| 4202 |
"yatra", |
| 4203 |
)} |
| 4204 |
className="mb-2" |
| 4205 |
/> |
| 4206 |
<RichTextEditor |
| 4207 |
value={formData.short_description} |
| 4208 |
onChange={(value) => |
| 4209 |
handleFieldChange("short_description", value) |
| 4210 |
} |
| 4211 |
placeholder={__( |
| 4212 |
"Escape to paradise with our 7-day Bali beach retreat...", |
| 4213 |
"yatra", |
| 4214 |
)} |
| 4215 |
minHeight={120} |
| 4216 |
/> |
| 4217 |
{formData.short_description.length > 0 && |
| 4218 |
formData.short_description.length < 100 && ( |
| 4219 |
<p className="mt-1.5 text-xs text-yellow-600 dark:text-yellow-400 flex items-center gap-1"> |
| 4220 |
<AlertCircle className="w-3.5 h-3.5" /> |
| 4221 |
{__( |
| 4222 |
"Consider adding more details (recommended: 100-150 characters)", |
| 4223 |
"yatra", |
| 4224 |
)} |
| 4225 |
</p> |
| 4226 |
)} |
| 4227 |
</div> |
| 4228 |
|
| 4229 |
{/* Tour Description */} |
| 4230 |
<div className="mb-4"> |
| 4231 |
<div className="flex items-center justify-between mb-1.5"> |
| 4232 |
<label className="text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5"> |
| 4233 |
{__("Trip Description", "yatra")} |
| 4234 |
<Badge |
| 4235 |
variant="outline" |
| 4236 |
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" |
| 4237 |
> |
| 4238 |
{__("Recommended", "yatra")} |
| 4239 |
</Badge> |
| 4240 |
</label> |
| 4241 |
<AiFieldAffordance |
| 4242 |
task="trip-description" |
| 4243 |
label={__("Description", "yatra")} |
| 4244 |
value={formData.description} |
| 4245 |
onAccept={(v) => handleFieldChange("description", v)} |
| 4246 |
buildContext={() => buildTripAiContext(formData)} |
| 4247 |
/> |
| 4248 |
</div> |
| 4249 |
<HelpText |
| 4250 |
text={__( |
| 4251 |
"💡 Tell travelers what makes your trip special! Describe the experience, highlights, and what they'll see. Write 2-4 paragraphs. Be enthusiastic and detailed!", |
| 4252 |
"💡 Tell travelers what makes your trip special! Describe the experience, highlights, and what they'll see. Write 2-4 paragraphs. Be enthusiastic and detailed!", |
| 4253 |
)} |
| 4254 |
className="mb-2" |
| 4255 |
/> |
| 4256 |
<RichTextEditor |
| 4257 |
value={formData.description} |
| 4258 |
onChange={(value) => handleFieldChange("description", value)} |
| 4259 |
placeholder={__( |
| 4260 |
"Escape to paradise with our 7-day Bali beach retreat... Or describe your single day trip experience...", |
| 4261 |
"yatra", |
| 4262 |
)} |
| 4263 |
minHeight={260} |
| 4264 |
/> |
| 4265 |
{errors.description && ( |
| 4266 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 4267 |
<AlertCircle className="w-4 h-4" /> |
| 4268 |
{errors.description} |
| 4269 |
</p> |
| 4270 |
)} |
| 4271 |
</div> |
| 4272 |
|
| 4273 |
{/* Featured Image */} |
| 4274 |
<div className="mb-6"> |
| 4275 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5 flex items-center gap-1.5"> |
| 4276 |
{__("Featured Image", "yatra")} |
| 4277 |
<Badge |
| 4278 |
variant="outline" |
| 4279 |
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" |
| 4280 |
> |
| 4281 |
{__("Recommended", "yatra")} |
| 4282 |
</Badge> |
| 4283 |
<button |
| 4284 |
type="button" |
| 4285 |
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto" |
| 4286 |
title={__( |
| 4287 |
"The main image that represents your trip. This appears in listings and as the hero image on the trip page.", |
| 4288 |
"yatra", |
| 4289 |
)} |
| 4290 |
> |
| 4291 |
<HelpCircle className="w-3.5 h-3.5" /> |
| 4292 |
</button> |
| 4293 |
</label> |
| 4294 |
<HelpText |
| 4295 |
text={__( |
| 4296 |
"💡 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.", |
| 4297 |
"yatra", |
| 4298 |
)} |
| 4299 |
className="mb-2" |
| 4300 |
/> |
| 4301 |
{formData.featured_image ? ( |
| 4302 |
<div className="relative group"> |
| 4303 |
<div |
| 4304 |
className={`aspect-video bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden border-2 ${ |
| 4305 |
errors.featured_image |
| 4306 |
? "border-red-500 dark:border-red-600" |
| 4307 |
: "border-gray-200 dark:border-gray-700" |
| 4308 |
}`} |
| 4309 |
> |
| 4310 |
{featuredImagePreview ? ( |
| 4311 |
<img |
| 4312 |
src={featuredImagePreview} |
| 4313 |
alt={__("Featured Image", "yatra")} |
| 4314 |
className="w-full h-full object-cover" |
| 4315 |
/> |
| 4316 |
) : ( |
| 4317 |
<div className="flex items-center justify-center w-full h-full text-sm text-gray-500 dark:text-gray-400"> |
| 4318 |
{isResolvingFeaturedImage |
| 4319 |
? __("Loading image...", "yatra") |
| 4320 |
: __("Preview unavailable", "yatra")} |
| 4321 |
</div> |
| 4322 |
)} |
| 4323 |
</div> |
| 4324 |
<button |
| 4325 |
type="button" |
| 4326 |
onClick={() => handleFieldChange("featured_image", null)} |
| 4327 |
className="absolute top-2 right-2 bg-red-600 text-white rounded-full p-1.5 opacity-0 group-hover:opacity-100 transition-opacity" |
| 4328 |
title={__("Remove image", "yatra")} |
| 4329 |
> |
| 4330 |
<X className="w-4 h-4" /> |
| 4331 |
</button> |
| 4332 |
</div> |
| 4333 |
) : ( |
| 4334 |
<button |
| 4335 |
type="button" |
| 4336 |
onClick={() => { |
| 4337 |
// Use WordPress media library |
| 4338 |
if (window.wp && window.wp.media) { |
| 4339 |
const mediaUploader = window.wp.media({ |
| 4340 |
title: __("Select Featured Image", "yatra"), |
| 4341 |
button: { text: __("Use this image", "yatra") }, |
| 4342 |
multiple: false, |
| 4343 |
}); |
| 4344 |
mediaUploader.on("select", () => { |
| 4345 |
const attachment = mediaUploader |
| 4346 |
.state() |
| 4347 |
.get("selection") |
| 4348 |
.first() |
| 4349 |
.toJSON(); |
| 4350 |
handleFieldChange("featured_image", attachment.id); |
| 4351 |
}); |
| 4352 |
prepareWordPressMediaFrameOpen(); |
| 4353 |
mediaUploader.open(); |
| 4354 |
} |
| 4355 |
}} |
| 4356 |
className={`w-full aspect-video border-2 border-dashed rounded-lg flex flex-col items-center justify-center transition-colors ${ |
| 4357 |
errors.featured_image |
| 4358 |
? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20" |
| 4359 |
: "border-gray-300 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-400" |
| 4360 |
}`} |
| 4361 |
> |
| 4362 |
<Upload |
| 4363 |
className={`w-10 h-10 mb-2 ${errors.featured_image ? "text-red-400" : "text-gray-400"}`} |
| 4364 |
/> |
| 4365 |
<span |
| 4366 |
className={`text-sm font-medium ${errors.featured_image ? "text-red-600 dark:text-red-400" : "text-gray-600 dark:text-gray-400"}`} |
| 4367 |
> |
| 4368 |
{__("Upload Featured Image", "yatra")} |
| 4369 |
</span> |
| 4370 |
<span |
| 4371 |
className={`text-xs mt-1 ${errors.featured_image ? "text-red-500 dark:text-red-500" : "text-gray-500 dark:text-gray-500"}`} |
| 4372 |
> |
| 4373 |
{__("Recommended: 1200x800px", "yatra")} |
| 4374 |
</span> |
| 4375 |
</button> |
| 4376 |
)} |
| 4377 |
{errors.featured_image && ( |
| 4378 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 4379 |
<AlertCircle className="w-4 h-4" /> |
| 4380 |
{errors.featured_image} |
| 4381 |
</p> |
| 4382 |
)} |
| 4383 |
</div> |
| 4384 |
|
| 4385 |
{/* Trip Highlights */} |
| 4386 |
<Card className="mt-6"> |
| 4387 |
<CardHeader> |
| 4388 |
<div className="flex items-start justify-between gap-2"> |
| 4389 |
<div className="flex-1"> |
| 4390 |
<CardTitle>{__("Trip Highlights", "yatra")}</CardTitle> |
| 4391 |
<CardDescription> |
| 4392 |
{__( |
| 4393 |
"Add key highlights that make your trip special. These will be displayed prominently on your trip page.", |
| 4394 |
"yatra", |
| 4395 |
)} |
| 4396 |
</CardDescription> |
| 4397 |
</div> |
| 4398 |
{/* AI: generates 5-7 highlight strings. The |
| 4399 |
affordance hands back a multi-line string; |
| 4400 |
we split into the highlights array shape |
| 4401 |
TripForm stores. */} |
| 4402 |
<AiFieldAffordance |
| 4403 |
task="trip-highlights" |
| 4404 |
label={__("Highlights", "yatra")} |
| 4405 |
value={(formData.highlights ?? []).join("\n")} |
| 4406 |
onAccept={(raw) => { |
| 4407 |
const list = raw |
| 4408 |
.split(/\r?\n/) |
| 4409 |
.map((l) => l.replace(/^[\s\-\*•·●]+/, "").trim()) |
| 4410 |
.filter((l) => l !== ""); |
| 4411 |
handleFieldChange("highlights", list as any); |
| 4412 |
}} |
| 4413 |
buildContext={() => buildTripAiContext(formData)} |
| 4414 |
/> |
| 4415 |
</div> |
| 4416 |
</CardHeader> |
| 4417 |
<CardContent className="space-y-3"> |
| 4418 |
{formData.highlights.length > 0 ? ( |
| 4419 |
<div className="space-y-2"> |
| 4420 |
{formData.highlights.map((highlight, index) => ( |
| 4421 |
<div |
| 4422 |
key={index} |
| 4423 |
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" |
| 4424 |
> |
| 4425 |
<CheckCircle2 className="w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0" /> |
| 4426 |
<span className="flex-1 text-sm text-gray-900 dark:text-white"> |
| 4427 |
{highlight} |
| 4428 |
</span> |
| 4429 |
<Button |
| 4430 |
type="button" |
| 4431 |
variant="ghost" |
| 4432 |
size="sm" |
| 4433 |
onClick={() => handleHighlightRemove(index)} |
| 4434 |
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20" |
| 4435 |
> |
| 4436 |
<X className="w-4 h-4" /> |
| 4437 |
</Button> |
| 4438 |
</div> |
| 4439 |
))} |
| 4440 |
</div> |
| 4441 |
) : ( |
| 4442 |
<div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center"> |
| 4443 |
<Sparkles className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" /> |
| 4444 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3"> |
| 4445 |
{__("No highlights added yet", "yatra")} |
| 4446 |
</p> |
| 4447 |
<p className="text-xs text-gray-500 dark:text-gray-500 mb-4"> |
| 4448 |
{__( |
| 4449 |
'Add key selling points like "Private guide", "All meals included", or "Skip-the-line tickets"', |
| 4450 |
"yatra", |
| 4451 |
)} |
| 4452 |
</p> |
| 4453 |
</div> |
| 4454 |
)} |
| 4455 |
<Button |
| 4456 |
type="button" |
| 4457 |
variant="outline" |
| 4458 |
onClick={handleHighlightAdd} |
| 4459 |
className="w-full" |
| 4460 |
> |
| 4461 |
<Plus className="w-4 h-4 mr-2" /> |
| 4462 |
{__("Add Highlight", "yatra")} |
| 4463 |
</Button> |
| 4464 |
</CardContent> |
| 4465 |
</Card> |
| 4466 |
|
| 4467 |
{/* Trip Type & Duration - Moved from Duration section */} |
| 4468 |
<div className="mt-6 space-y-4"> |
| 4469 |
<div className="border-t border-gray-200 dark:border-gray-700 pt-6"> |
| 4470 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4"> |
| 4471 |
{__("Trip Type & Duration", "yatra")} |
| 4472 |
</h3> |
| 4473 |
|
| 4474 |
{/* Trip Type */} |
| 4475 |
<div className="mb-4"> |
| 4476 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-3"> |
| 4477 |
{__("Trip Type", "yatra")} |
| 4478 |
</label> |
| 4479 |
<div |
| 4480 |
className={`grid grid-cols-1 md:grid-cols-2 gap-4 ${ |
| 4481 |
errors.trip_type ? "mb-2" : "" |
| 4482 |
}`} |
| 4483 |
> |
| 4484 |
<label |
| 4485 |
className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${ |
| 4486 |
formData.trip_type === "single_day" |
| 4487 |
? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600" |
| 4488 |
: errors.trip_type |
| 4489 |
? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20" |
| 4490 |
: "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" |
| 4491 |
}`} |
| 4492 |
> |
| 4493 |
<input |
| 4494 |
type="radio" |
| 4495 |
name="trip_type" |
| 4496 |
value="single_day" |
| 4497 |
checked={formData.trip_type === "single_day"} |
| 4498 |
onChange={(e) => { |
| 4499 |
handleFieldChange("trip_type", e.target.value); |
| 4500 |
if (e.target.value === "single_day") { |
| 4501 |
setFormData((prev) => ({ |
| 4502 |
...prev, |
| 4503 |
duration_days: "1", |
| 4504 |
duration_nights: "0", |
| 4505 |
})); |
| 4506 |
} |
| 4507 |
}} |
| 4508 |
className="sr-only" |
| 4509 |
/> |
| 4510 |
<div className="flex flex-1"> |
| 4511 |
<div className="flex flex-col"> |
| 4512 |
<span |
| 4513 |
className={`block text-sm font-semibold ${ |
| 4514 |
formData.trip_type === "single_day" |
| 4515 |
? "text-blue-900 dark:text-blue-300" |
| 4516 |
: "text-gray-900 dark:text-white" |
| 4517 |
}`} |
| 4518 |
> |
| 4519 |
{__("Single Day Trip", "yatra")} |
| 4520 |
</span> |
| 4521 |
<span |
| 4522 |
className={`mt-1 flex items-center text-sm ${ |
| 4523 |
formData.trip_type === "single_day" |
| 4524 |
? "text-blue-700 dark:text-blue-400" |
| 4525 |
: "text-gray-500 dark:text-gray-400" |
| 4526 |
}`} |
| 4527 |
> |
| 4528 |
{__( |
| 4529 |
"Trip completed within one day (no overnight stay)", |
| 4530 |
"yatra", |
| 4531 |
)} |
| 4532 |
</span> |
| 4533 |
</div> |
| 4534 |
</div> |
| 4535 |
{formData.trip_type === "single_day" && ( |
| 4536 |
<div className="absolute top-4 right-4"> |
| 4537 |
<div className="h-4 w-4 rounded-full bg-blue-600"></div> |
| 4538 |
</div> |
| 4539 |
)} |
| 4540 |
</label> |
| 4541 |
|
| 4542 |
<label |
| 4543 |
className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${ |
| 4544 |
formData.trip_type === "multi_day" |
| 4545 |
? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600" |
| 4546 |
: errors.trip_type |
| 4547 |
? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20" |
| 4548 |
: "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" |
| 4549 |
}`} |
| 4550 |
> |
| 4551 |
<input |
| 4552 |
type="radio" |
| 4553 |
name="trip_type" |
| 4554 |
value="multi_day" |
| 4555 |
checked={formData.trip_type === "multi_day"} |
| 4556 |
onChange={(e) => |
| 4557 |
handleFieldChange("trip_type", e.target.value) |
| 4558 |
} |
| 4559 |
className="sr-only" |
| 4560 |
/> |
| 4561 |
<div className="flex flex-1"> |
| 4562 |
<div className="flex flex-col"> |
| 4563 |
<span |
| 4564 |
className={`block text-sm font-semibold ${ |
| 4565 |
formData.trip_type === "multi_day" |
| 4566 |
? "text-blue-900 dark:text-blue-300" |
| 4567 |
: "text-gray-900 dark:text-white" |
| 4568 |
}`} |
| 4569 |
> |
| 4570 |
{__("Multi-Day Trip", "yatra")} |
| 4571 |
</span> |
| 4572 |
<span |
| 4573 |
className={`mt-1 flex items-center text-sm ${ |
| 4574 |
formData.trip_type === "multi_day" |
| 4575 |
? "text-blue-700 dark:text-blue-400" |
| 4576 |
: "text-gray-500 dark:text-gray-400" |
| 4577 |
}`} |
| 4578 |
> |
| 4579 |
{__( |
| 4580 |
"Trip spans multiple days with overnight stays", |
| 4581 |
"yatra", |
| 4582 |
)} |
| 4583 |
</span> |
| 4584 |
</div> |
| 4585 |
</div> |
| 4586 |
{formData.trip_type === "multi_day" && ( |
| 4587 |
<div className="absolute top-4 right-4"> |
| 4588 |
<div className="h-4 w-4 rounded-full bg-blue-600"></div> |
| 4589 |
</div> |
| 4590 |
)} |
| 4591 |
</label> |
| 4592 |
</div> |
| 4593 |
{errors.trip_type && ( |
| 4594 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 4595 |
<AlertCircle className="w-4 h-4" /> |
| 4596 |
{errors.trip_type} |
| 4597 |
</p> |
| 4598 |
)} |
| 4599 |
</div> |
| 4600 |
|
| 4601 |
{/* Duration Days & Nights */} |
| 4602 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 4603 |
<div> |
| 4604 |
<label |
| 4605 |
htmlFor="duration_days" |
| 4606 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 4607 |
> |
| 4608 |
{__("Duration (Days)", "yatra")} |
| 4609 |
</label> |
| 4610 |
<Input |
| 4611 |
id="duration_days" |
| 4612 |
type="number" |
| 4613 |
min="1" |
| 4614 |
value={formData.duration_days} |
| 4615 |
onChange={(e) => { |
| 4616 |
const days = e.target.value; |
| 4617 |
handleFieldChange("duration_days", days); |
| 4618 |
// Auto-set nights based on trip type |
| 4619 |
if (formData.trip_type === "single_day") { |
| 4620 |
setFormData((prev) => ({ |
| 4621 |
...prev, |
| 4622 |
duration_nights: "0", |
| 4623 |
})); |
| 4624 |
} else if (days && parseInt(days) > 1) { |
| 4625 |
// For multi-day, typically nights = days - 1 |
| 4626 |
const nights = Math.max( |
| 4627 |
0, |
| 4628 |
parseInt(days) - 1, |
| 4629 |
).toString(); |
| 4630 |
setFormData((prev) => ({ |
| 4631 |
...prev, |
| 4632 |
duration_nights: nights, |
| 4633 |
})); |
| 4634 |
} |
| 4635 |
}} |
| 4636 |
placeholder={ |
| 4637 |
formData.trip_type === "single_day" |
| 4638 |
? "1" |
| 4639 |
: __("e.g., 7", "yatra") |
| 4640 |
} |
| 4641 |
className={errors.duration_days ? "border-red-500" : ""} |
| 4642 |
disabled={formData.trip_type === "single_day"} |
| 4643 |
/> |
| 4644 |
<p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400"> |
| 4645 |
{formData.trip_type === "single_day" |
| 4646 |
? __("Single day trips are always 1 day", "yatra") |
| 4647 |
: __( |
| 4648 |
"Enter the number of days for your trip", |
| 4649 |
"yatra", |
| 4650 |
)} |
| 4651 |
</p> |
| 4652 |
{errors.duration_days && ( |
| 4653 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 4654 |
<AlertCircle className="w-4 h-4" /> |
| 4655 |
{errors.duration_days} |
| 4656 |
</p> |
| 4657 |
)} |
| 4658 |
</div> |
| 4659 |
<div> |
| 4660 |
<label |
| 4661 |
htmlFor="duration_nights" |
| 4662 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 4663 |
> |
| 4664 |
{__("Duration (Nights)", "yatra")} |
| 4665 |
</label> |
| 4666 |
<Input |
| 4667 |
id="duration_nights" |
| 4668 |
type="number" |
| 4669 |
min="0" |
| 4670 |
value={formData.duration_nights} |
| 4671 |
onChange={(e) => |
| 4672 |
handleFieldChange("duration_nights", e.target.value) |
| 4673 |
} |
| 4674 |
placeholder={ |
| 4675 |
formData.trip_type === "single_day" |
| 4676 |
? "0" |
| 4677 |
: __("e.g., 6", "yatra") |
| 4678 |
} |
| 4679 |
className={ |
| 4680 |
errors.duration_nights ? "border-red-500" : "" |
| 4681 |
} |
| 4682 |
disabled={formData.trip_type === "single_day"} |
| 4683 |
/> |
| 4684 |
<p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400"> |
| 4685 |
{formData.trip_type === "single_day" |
| 4686 |
? __( |
| 4687 |
"Single day trips have 0 nights (no overnight stay)", |
| 4688 |
"yatra", |
| 4689 |
) |
| 4690 |
: __( |
| 4691 |
"Enter the number of nights (typically days - 1)", |
| 4692 |
"yatra", |
| 4693 |
)} |
| 4694 |
</p> |
| 4695 |
{errors.duration_nights && ( |
| 4696 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 4697 |
<AlertCircle className="w-4 h-4" /> |
| 4698 |
{errors.duration_nights} |
| 4699 |
</p> |
| 4700 |
)} |
| 4701 |
</div> |
| 4702 |
</div> |
| 4703 |
</div> |
| 4704 |
</div> |
| 4705 |
</div> |
| 4706 |
</div> |
| 4707 |
); |
| 4708 |
|
| 4709 |
case "location": |
| 4710 |
return ( |
| 4711 |
<div className="space-y-4"> |
| 4712 |
<div className="flex flex-wrap items-center gap-2 mb-2"> |
| 4713 |
<MapPin className="w-5 h-5 text-gray-500" /> |
| 4714 |
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> |
| 4715 |
{__("Location & Geography", "yatra")} |
| 4716 |
</h2> |
| 4717 |
<Badge |
| 4718 |
variant="outline" |
| 4719 |
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" |
| 4720 |
> |
| 4721 |
{__("Recommended", "yatra")} |
| 4722 |
</Badge> |
| 4723 |
</div> |
| 4724 |
<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"> |
| 4725 |
<p className="text-sm font-medium text-amber-900 dark:text-amber-100 mb-1"> |
| 4726 |
{__( |
| 4727 |
"Specify where your trip takes place, including destinations, coordinates, and key landmarks.", |
| 4728 |
"yatra", |
| 4729 |
)} |
| 4730 |
</p> |
| 4731 |
<p className="text-xs text-amber-800 dark:text-amber-200"> |
| 4732 |
{__( |
| 4733 |
"Optional but highly recommended to help travelers understand the experience.", |
| 4734 |
"yatra", |
| 4735 |
)} |
| 4736 |
</p> |
| 4737 |
</div> |
| 4738 |
|
| 4739 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"> |
| 4740 |
{__( |
| 4741 |
"Optimize how your trip appears in search engines and social shares", |
| 4742 |
"yatra", |
| 4743 |
)} |
| 4744 |
<span className="block text-xs text-gray-500 dark:text-gray-400 mt-1"> |
| 4745 |
{__( |
| 4746 |
"Optional, but completing these fields improves SEO and click-through rates.", |
| 4747 |
"yatra", |
| 4748 |
)} |
| 4749 |
</span> |
| 4750 |
</p> |
| 4751 |
<div className="space-y-4"> |
| 4752 |
{/* Destinations - Multiple Selection */} |
| 4753 |
<div> |
| 4754 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 4755 |
{__("Destinations", "yatra")} |
| 4756 |
</label> |
| 4757 |
{destinationsData && destinationsData.length > 0 ? ( |
| 4758 |
<MultiSelect |
| 4759 |
value={formData.destinations.map((id) => id.toString())} |
| 4760 |
onChange={(values) => |
| 4761 |
handleFieldChange( |
| 4762 |
"destinations", |
| 4763 |
values.map((v) => Number(v)), |
| 4764 |
) |
| 4765 |
} |
| 4766 |
options={destinationsData.map((destination: any) => ({ |
| 4767 |
value: destination.id.toString(), |
| 4768 |
label: |
| 4769 |
destination.name || |
| 4770 |
sprintf( |
| 4771 |
// translators: %s: numeric destination ID, used as a fallback when the destination has no name. |
| 4772 |
__("Destination #%s", "yatra"), |
| 4773 |
String(destination.id), |
| 4774 |
), |
| 4775 |
}))} |
| 4776 |
placeholder={__("Select destinations...", "yatra")} |
| 4777 |
searchPlaceholder={__("Search destinations...", "yatra")} |
| 4778 |
error={!!errors.destinations} |
| 4779 |
/> |
| 4780 |
) : ( |
| 4781 |
<div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center"> |
| 4782 |
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3"> |
| 4783 |
{__( |
| 4784 |
"No destinations available. Please create destinations first.", |
| 4785 |
"yatra", |
| 4786 |
)} |
| 4787 |
</p> |
| 4788 |
<div className="space-y-2"> |
| 4789 |
<p className="text-xs text-gray-400 dark:text-gray-500"> |
| 4790 |
{__("To create destinations:", "yatra")} |
| 4791 |
</p> |
| 4792 |
<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"> |
| 4793 |
<li> |
| 4794 |
{__( |
| 4795 |
"Go to Yatra → Destinations in your WordPress admin", |
| 4796 |
"yatra", |
| 4797 |
)} |
| 4798 |
</li> |
| 4799 |
<li>{__('Click "Add New Destination"', "yatra")}</li> |
| 4800 |
<li> |
| 4801 |
{__("Enter destination name and details", "yatra")} |
| 4802 |
</li> |
| 4803 |
<li>{__('Set status to "Published"', "yatra")}</li> |
| 4804 |
</ol> |
| 4805 |
<a |
| 4806 |
href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=destinations&action=create" |
| 4807 |
target="_blank" |
| 4808 |
rel="noopener noreferrer" |
| 4809 |
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" |
| 4810 |
> |
| 4811 |
<svg |
| 4812 |
className="w-3 h-3 mr-1" |
| 4813 |
fill="none" |
| 4814 |
stroke="currentColor" |
| 4815 |
viewBox="0 0 24 24" |
| 4816 |
> |
| 4817 |
<path |
| 4818 |
strokeLinecap="round" |
| 4819 |
strokeLinejoin="round" |
| 4820 |
strokeWidth={2} |
| 4821 |
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" |
| 4822 |
/> |
| 4823 |
</svg> |
| 4824 |
{__("Create Destinations", "yatra")} |
| 4825 |
</a> |
| 4826 |
</div> |
| 4827 |
</div> |
| 4828 |
)} |
| 4829 |
<HelpText |
| 4830 |
text={__( |
| 4831 |
"Select all destinations included in this trip", |
| 4832 |
"yatra", |
| 4833 |
)} |
| 4834 |
className="mt-2" |
| 4835 |
/> |
| 4836 |
{errors.destinations && ( |
| 4837 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 4838 |
<AlertCircle className="w-4 h-4" /> |
| 4839 |
{errors.destinations} |
| 4840 |
</p> |
| 4841 |
)} |
| 4842 |
</div> |
| 4843 |
|
| 4844 |
{/* Locations Section - Senior UI/UX Design */} |
| 4845 |
<div className="space-y-8"> |
| 4846 |
{/* Section Header */} |
| 4847 |
<div className="text-center space-y-2"> |
| 4848 |
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center justify-center gap-2"> |
| 4849 |
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center"> |
| 4850 |
<MapPin className="w-3 h-3 text-white" /> |
| 4851 |
</div> |
| 4852 |
{__("Trip Locations", "yatra")} |
| 4853 |
</h3> |
| 4854 |
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"> |
| 4855 |
{__( |
| 4856 |
"Set precise starting and ending points with location names and GPS coordinates. Use manual entry or visual map selection for maximum accuracy.", |
| 4857 |
"yatra", |
| 4858 |
)} |
| 4859 |
</p> |
| 4860 |
</div> |
| 4861 |
|
| 4862 |
{/* Locations Grid */} |
| 4863 |
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> |
| 4864 |
{/* STARTING LOCATION */} |
| 4865 |
<div className="space-y-6"> |
| 4866 |
{/* Location Header */} |
| 4867 |
<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"> |
| 4868 |
<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"> |
| 4869 |
<MapPin className="w-5 h-5 text-white" /> |
| 4870 |
</div> |
| 4871 |
<div className="flex-1"> |
| 4872 |
<h4 className="text-base font-semibold text-blue-900 dark:text-blue-100"> |
| 4873 |
{__("Starting Point", "yatra")} |
| 4874 |
</h4> |
| 4875 |
<p className="text-xs text-blue-700 dark:text-blue-300"> |
| 4876 |
{__("Where the journey begins", "yatra")} |
| 4877 |
</p> |
| 4878 |
</div> |
| 4879 |
<div className="flex items-center gap-2"> |
| 4880 |
{formData.starting_location && ( |
| 4881 |
<div |
| 4882 |
className="w-2 h-2 bg-green-500 rounded-full animate-pulse" |
| 4883 |
title={__("Location set", "yatra")} |
| 4884 |
></div> |
| 4885 |
)} |
| 4886 |
{formData.starting_latitude && |
| 4887 |
formData.starting_longitude && ( |
| 4888 |
<div |
| 4889 |
className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" |
| 4890 |
title={__("Coordinates set", "yatra")} |
| 4891 |
></div> |
| 4892 |
)} |
| 4893 |
</div> |
| 4894 |
</div> |
| 4895 |
|
| 4896 |
{/* Integrated Location Input with Map */} |
| 4897 |
<div className="space-y-4"> |
| 4898 |
<div className="flex items-center justify-between"> |
| 4899 |
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2"> |
| 4900 |
<div className="w-4 h-4 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center"> |
| 4901 |
<span className="text-xs font-bold text-blue-600 dark:text-blue-400"> |
| 4902 |
1 |
| 4903 |
</span> |
| 4904 |
</div> |
| 4905 |
{__("Starting Location", "yatra")} |
| 4906 |
</label> |
| 4907 |
<div className="flex items-center gap-2"> |
| 4908 |
<span className="text-xs text-gray-500 dark:text-gray-400"> |
| 4909 |
{formData.starting_location |
| 4910 |
? "✓" |
| 4911 |
: __("Required", "yatra")} |
| 4912 |
</span> |
| 4913 |
{formData.starting_latitude && |
| 4914 |
formData.starting_longitude && ( |
| 4915 |
<div className="flex items-center gap-1 text-xs text-green-600"> |
| 4916 |
<div className="w-2 h-2 bg-green-500 rounded-full"></div> |
| 4917 |
{__("Coords Set", "yatra")} |
| 4918 |
</div> |
| 4919 |
)} |
| 4920 |
</div> |
| 4921 |
</div> |
| 4922 |
|
| 4923 |
{/* LocationPicker - Primary Interface */} |
| 4924 |
<div className="space-y-3"> |
| 4925 |
<LocationPicker |
| 4926 |
value={{ |
| 4927 |
name: formData.starting_location, |
| 4928 |
latitude: formData.starting_latitude, |
| 4929 |
longitude: formData.starting_longitude, |
| 4930 |
}} |
| 4931 |
onChange={(locationData) => { |
| 4932 |
handleFieldChange( |
| 4933 |
"starting_location", |
| 4934 |
locationData.name, |
| 4935 |
); |
| 4936 |
handleFieldChange( |
| 4937 |
"starting_latitude", |
| 4938 |
locationData.latitude, |
| 4939 |
); |
| 4940 |
handleFieldChange( |
| 4941 |
"starting_longitude", |
| 4942 |
locationData.longitude, |
| 4943 |
); |
| 4944 |
}} |
| 4945 |
label="" |
| 4946 |
placeholder={__( |
| 4947 |
"Search for starting location...", |
| 4948 |
"yatra", |
| 4949 |
)} |
| 4950 |
helpText="" |
| 4951 |
required={false} |
| 4952 |
defaultMapCenter={ |
| 4953 |
formData.starting_latitude && |
| 4954 |
formData.starting_longitude |
| 4955 |
? [ |
| 4956 |
parseFloat(formData.starting_latitude), |
| 4957 |
parseFloat(formData.starting_longitude), |
| 4958 |
] |
| 4959 |
: [20, 0] |
| 4960 |
} |
| 4961 |
defaultZoom={ |
| 4962 |
formData.starting_latitude && |
| 4963 |
formData.starting_longitude |
| 4964 |
? 13 |
| 4965 |
: 2 |
| 4966 |
} |
| 4967 |
mapHeight="300px" |
| 4968 |
showMapButton={false} |
| 4969 |
searchLimit={8} |
| 4970 |
__={__} |
| 4971 |
className="" |
| 4972 |
mapClassName="rounded-lg" |
| 4973 |
/> |
| 4974 |
</div> |
| 4975 |
</div> |
| 4976 |
|
| 4977 |
{/* GPS Coordinates - Manual Override */} |
| 4978 |
<div className="space-y-4"> |
| 4979 |
<div className="flex items-center justify-between"> |
| 4980 |
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2"> |
| 4981 |
<div className="w-4 h-4 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center"> |
| 4982 |
<span className="text-xs font-bold text-blue-600 dark:text-blue-400"> |
| 4983 |
2 |
| 4984 |
</span> |
| 4985 |
</div> |
| 4986 |
{__("GPS Coordinates", "yatra")} |
| 4987 |
<span className="text-xs text-gray-500 dark:text-gray-400"> |
| 4988 |
({__("Manual override", "yatra")}) |
| 4989 |
</span> |
| 4990 |
</label> |
| 4991 |
<div className="flex items-center gap-2"> |
| 4992 |
<button |
| 4993 |
type="button" |
| 4994 |
onClick={() => { |
| 4995 |
if (navigator.geolocation) { |
| 4996 |
navigator.geolocation.getCurrentPosition( |
| 4997 |
(position) => { |
| 4998 |
handleFieldChange( |
| 4999 |
"starting_latitude", |
| 5000 |
position.coords.latitude.toString(), |
| 5001 |
); |
| 5002 |
handleFieldChange( |
| 5003 |
"starting_longitude", |
| 5004 |
position.coords.longitude.toString(), |
| 5005 |
); |
| 5006 |
}, |
| 5007 |
(error) => { |
| 5008 |
let message = __( |
| 5009 |
"Unable to get your location", |
| 5010 |
"yatra", |
| 5011 |
); |
| 5012 |
let showHttpsNotice = false; |
| 5013 |
|
| 5014 |
switch (error.code) { |
| 5015 |
case 1: // PERMISSION_DENIED |
| 5016 |
if ( |
| 5017 |
error.message.includes( |
| 5018 |
"secure origins", |
| 5019 |
) |
| 5020 |
) { |
| 5021 |
message = __( |
| 5022 |
"Location access requires HTTPS. This feature will work on your live HTTPS site.", |
| 5023 |
"yatra", |
| 5024 |
); |
| 5025 |
showHttpsNotice = true; |
| 5026 |
} else { |
| 5027 |
message = __( |
| 5028 |
"Location access denied. Please allow location access in your browser.", |
| 5029 |
"yatra", |
| 5030 |
); |
| 5031 |
} |
| 5032 |
break; |
| 5033 |
case 2: // POSITION_UNAVAILABLE |
| 5034 |
message = __( |
| 5035 |
"Location information is unavailable. Please try again.", |
| 5036 |
"yatra", |
| 5037 |
); |
| 5038 |
break; |
| 5039 |
case 3: // TIMEOUT |
| 5040 |
message = __( |
| 5041 |
"Location request timed out. Please try again.", |
| 5042 |
"yatra", |
| 5043 |
); |
| 5044 |
break; |
| 5045 |
} |
| 5046 |
|
| 5047 |
if (showHttpsNotice) { |
| 5048 |
alert( |
| 5049 |
message + |
| 5050 |
"\n\n" + |
| 5051 |
__( |
| 5052 |
"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", |
| 5053 |
"yatra", |
| 5054 |
), |
| 5055 |
); |
| 5056 |
} else { |
| 5057 |
alert(message); |
| 5058 |
} |
| 5059 |
}, |
| 5060 |
); |
| 5061 |
} else { |
| 5062 |
alert( |
| 5063 |
__( |
| 5064 |
"Geolocation is not supported by your browser", |
| 5065 |
"yatra", |
| 5066 |
), |
| 5067 |
); |
| 5068 |
} |
| 5069 |
}} |
| 5070 |
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" |
| 5071 |
> |
| 5072 |
<div className="w-3 h-3"> |
| 5073 |
<svg fill="currentColor" viewBox="0 0 20 20"> |
| 5074 |
<path |
| 5075 |
fillRule="evenodd" |
| 5076 |
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" |
| 5077 |
clipRule="evenodd" |
| 5078 |
/> |
| 5079 |
</svg> |
| 5080 |
</div> |
| 5081 |
{__("Use Current Location", "yatra")} |
| 5082 |
</button> |
| 5083 |
</div> |
| 5084 |
</div> |
| 5085 |
|
| 5086 |
<div className="grid grid-cols-2 gap-3"> |
| 5087 |
<div className="space-y-2"> |
| 5088 |
<label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1"> |
| 5089 |
<div className="w-2 h-2 bg-blue-400 rounded-full"></div> |
| 5090 |
{__("Latitude", "yatra")} |
| 5091 |
</label> |
| 5092 |
<Input |
| 5093 |
type="text" |
| 5094 |
value={formData.starting_latitude} |
| 5095 |
onChange={(e) => |
| 5096 |
handleFieldChange( |
| 5097 |
"starting_latitude", |
| 5098 |
e.target.value, |
| 5099 |
) |
| 5100 |
} |
| 5101 |
className="w-full text-sm" |
| 5102 |
/> |
| 5103 |
</div> |
| 5104 |
<div className="space-y-2"> |
| 5105 |
<label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1"> |
| 5106 |
<div className="w-2 h-2 bg-blue-400 rounded-full"></div> |
| 5107 |
{__("Longitude", "yatra")} |
| 5108 |
</label> |
| 5109 |
<Input |
| 5110 |
type="text" |
| 5111 |
value={formData.starting_longitude} |
| 5112 |
onChange={(e) => |
| 5113 |
handleFieldChange( |
| 5114 |
"starting_longitude", |
| 5115 |
e.target.value, |
| 5116 |
) |
| 5117 |
} |
| 5118 |
placeholder={__("e.g., 115.0920", "yatra")} |
| 5119 |
className="w-full text-sm" |
| 5120 |
/> |
| 5121 |
</div> |
| 5122 |
</div> |
| 5123 |
<p className="text-xs text-gray-500 dark:text-gray-400"> |
| 5124 |
{__( |
| 5125 |
"Manual coordinate entry. These will be auto-filled when you select a location from the map above.", |
| 5126 |
"yatra", |
| 5127 |
)} |
| 5128 |
</p> |
| 5129 |
</div> |
| 5130 |
</div> |
| 5131 |
|
| 5132 |
{/* ENDING LOCATION */} |
| 5133 |
<div className="space-y-6"> |
| 5134 |
{/* Location Header */} |
| 5135 |
<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"> |
| 5136 |
<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"> |
| 5137 |
<CheckCircle2 className="w-5 h-5 text-white" /> |
| 5138 |
</div> |
| 5139 |
<div className="flex-1"> |
| 5140 |
<h4 className="text-base font-semibold text-green-900 dark:text-green-100"> |
| 5141 |
{__("Ending Point", "yatra")} |
| 5142 |
</h4> |
| 5143 |
<p className="text-xs text-green-700 dark:text-green-300"> |
| 5144 |
{__("Where the journey concludes", "yatra")} |
| 5145 |
</p> |
| 5146 |
</div> |
| 5147 |
<div className="flex items-center gap-2"> |
| 5148 |
{formData.ending_location && ( |
| 5149 |
<div |
| 5150 |
className="w-2 h-2 bg-green-500 rounded-full animate-pulse" |
| 5151 |
title={__("Location set", "yatra")} |
| 5152 |
></div> |
| 5153 |
)} |
| 5154 |
</div> |
| 5155 |
</div> |
| 5156 |
|
| 5157 |
{/* Integrated Location Input with Map */} |
| 5158 |
<div className="space-y-4"> |
| 5159 |
<div className="flex items-center justify-between"> |
| 5160 |
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2"> |
| 5161 |
<div className="w-4 h-4 bg-green-100 dark:bg-green-900 rounded flex items-center justify-center"> |
| 5162 |
<span className="text-xs font-bold text-green-600 dark:text-green-400"> |
| 5163 |
1 |
| 5164 |
</span> |
| 5165 |
</div> |
| 5166 |
{__("Ending Location", "yatra")} |
| 5167 |
</label> |
| 5168 |
<div className="flex items-center gap-2"> |
| 5169 |
<span className="text-xs text-gray-500 dark:text-gray-400"> |
| 5170 |
{formData.ending_location |
| 5171 |
? "✓" |
| 5172 |
: __("Required", "yatra")} |
| 5173 |
</span> |
| 5174 |
{formData.ending_latitude && |
| 5175 |
formData.ending_longitude && ( |
| 5176 |
<div className="flex items-center gap-1 text-xs text-green-600"> |
| 5177 |
<div className="w-2 h-2 bg-green-500 rounded-full"></div> |
| 5178 |
{__("Coords Set", "yatra")} |
| 5179 |
</div> |
| 5180 |
)} |
| 5181 |
</div> |
| 5182 |
</div> |
| 5183 |
|
| 5184 |
{/* LocationPicker - Primary Interface */} |
| 5185 |
<div className="space-y-3"> |
| 5186 |
<LocationPicker |
| 5187 |
value={{ |
| 5188 |
name: formData.ending_location, |
| 5189 |
latitude: formData.ending_latitude, |
| 5190 |
longitude: formData.ending_longitude, |
| 5191 |
}} |
| 5192 |
onChange={(locationData) => { |
| 5193 |
handleFieldChange( |
| 5194 |
"ending_location", |
| 5195 |
locationData.name, |
| 5196 |
); |
| 5197 |
handleFieldChange( |
| 5198 |
"ending_latitude", |
| 5199 |
locationData.latitude, |
| 5200 |
); |
| 5201 |
handleFieldChange( |
| 5202 |
"ending_longitude", |
| 5203 |
locationData.longitude, |
| 5204 |
); |
| 5205 |
}} |
| 5206 |
label="" |
| 5207 |
placeholder={__( |
| 5208 |
"Search for ending location...", |
| 5209 |
"yatra", |
| 5210 |
)} |
| 5211 |
helpText="" |
| 5212 |
required={false} |
| 5213 |
defaultMapCenter={ |
| 5214 |
formData.ending_latitude && |
| 5215 |
formData.ending_longitude |
| 5216 |
? [ |
| 5217 |
parseFloat(formData.ending_latitude), |
| 5218 |
parseFloat(formData.ending_longitude), |
| 5219 |
] |
| 5220 |
: formData.starting_latitude && |
| 5221 |
formData.starting_longitude |
| 5222 |
? [ |
| 5223 |
parseFloat(formData.starting_latitude), |
| 5224 |
parseFloat(formData.starting_longitude), |
| 5225 |
] |
| 5226 |
: [20, 0] |
| 5227 |
} |
| 5228 |
defaultZoom={ |
| 5229 |
formData.ending_latitude && |
| 5230 |
formData.ending_longitude |
| 5231 |
? 13 |
| 5232 |
: formData.starting_latitude && |
| 5233 |
formData.starting_longitude |
| 5234 |
? 13 |
| 5235 |
: 2 |
| 5236 |
} |
| 5237 |
mapHeight="300px" |
| 5238 |
showMapButton={false} |
| 5239 |
searchLimit={8} |
| 5240 |
__={__} |
| 5241 |
className="" |
| 5242 |
mapClassName="rounded-lg" |
| 5243 |
/> |
| 5244 |
</div> |
| 5245 |
</div> |
| 5246 |
|
| 5247 |
{/* GPS Coordinates - Manual Override */} |
| 5248 |
<div className="space-y-4"> |
| 5249 |
<div className="flex items-center justify-between"> |
| 5250 |
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2"> |
| 5251 |
<div className="w-4 h-4 bg-green-100 dark:bg-green-900 rounded flex items-center justify-center"> |
| 5252 |
<span className="text-xs font-bold text-green-600 dark:text-green-400"> |
| 5253 |
2 |
| 5254 |
</span> |
| 5255 |
</div> |
| 5256 |
{__("GPS Coordinates", "yatra")} |
| 5257 |
<span className="text-xs text-gray-500 dark:text-gray-400"> |
| 5258 |
({__("Optional", "yatra")}) |
| 5259 |
</span> |
| 5260 |
</label> |
| 5261 |
</div> |
| 5262 |
|
| 5263 |
<div className="grid grid-cols-2 gap-3"> |
| 5264 |
<div className="space-y-2"> |
| 5265 |
<label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1"> |
| 5266 |
<div className="w-2 h-2 bg-green-400 rounded-full"></div> |
| 5267 |
{__("Latitude", "yatra")} |
| 5268 |
</label> |
| 5269 |
<Input |
| 5270 |
type="text" |
| 5271 |
value={formData.ending_latitude || ""} |
| 5272 |
onChange={(e) => |
| 5273 |
handleFieldChange( |
| 5274 |
"ending_latitude", |
| 5275 |
e.target.value, |
| 5276 |
) |
| 5277 |
} |
| 5278 |
placeholder={__("e.g., -8.5069", "yatra")} |
| 5279 |
className="w-full text-sm" |
| 5280 |
/> |
| 5281 |
</div> |
| 5282 |
<div className="space-y-2"> |
| 5283 |
<label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1"> |
| 5284 |
<div className="w-2 h-2 bg-green-400 rounded-full"></div> |
| 5285 |
{__("Longitude", "yatra")} |
| 5286 |
</label> |
| 5287 |
<Input |
| 5288 |
type="text" |
| 5289 |
value={formData.ending_longitude || ""} |
| 5290 |
onChange={(e) => |
| 5291 |
handleFieldChange( |
| 5292 |
"ending_longitude", |
| 5293 |
e.target.value, |
| 5294 |
) |
| 5295 |
} |
| 5296 |
placeholder={__("e.g., 115.2625", "yatra")} |
| 5297 |
className="w-full text-sm" |
| 5298 |
/> |
| 5299 |
</div> |
| 5300 |
</div> |
| 5301 |
<p className="text-xs text-gray-500 dark:text-gray-400"> |
| 5302 |
{__( |
| 5303 |
"Optional: Manual coordinate entry. Auto-filled when you select a location from the map above.", |
| 5304 |
"yatra", |
| 5305 |
)} |
| 5306 |
</p> |
| 5307 |
</div> |
| 5308 |
</div> |
| 5309 |
</div> |
| 5310 |
</div> |
| 5311 |
|
| 5312 |
{/* Landmarks */} |
| 5313 |
<div> |
| 5314 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 5315 |
{__("Key Landmarks / Points of Interest", "yatra")} |
| 5316 |
</label> |
| 5317 |
<HelpText |
| 5318 |
text={__( |
| 5319 |
"Add notable landmarks or points of interest visited during this trip", |
| 5320 |
"yatra", |
| 5321 |
)} |
| 5322 |
className="mb-2" |
| 5323 |
/> |
| 5324 |
{formData.landmarks.length > 0 ? ( |
| 5325 |
<div className="space-y-2 mb-2"> |
| 5326 |
{formData.landmarks.map((landmark, index) => ( |
| 5327 |
<div |
| 5328 |
key={index} |
| 5329 |
className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700" |
| 5330 |
> |
| 5331 |
<MapPin className="w-4 h-4 text-gray-400 flex-shrink-0" /> |
| 5332 |
<span className="flex-1 text-sm text-gray-900 dark:text-white"> |
| 5333 |
{landmark} |
| 5334 |
</span> |
| 5335 |
<Button |
| 5336 |
type="button" |
| 5337 |
variant="ghost" |
| 5338 |
size="sm" |
| 5339 |
onClick={() => { |
| 5340 |
const newLandmarks = [...formData.landmarks]; |
| 5341 |
newLandmarks.splice(index, 1); |
| 5342 |
handleFieldChange("landmarks", newLandmarks); |
| 5343 |
}} |
| 5344 |
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20" |
| 5345 |
> |
| 5346 |
<X className="w-4 h-4" /> |
| 5347 |
</Button> |
| 5348 |
</div> |
| 5349 |
))} |
| 5350 |
</div> |
| 5351 |
) : ( |
| 5352 |
<div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center mb-2"> |
| 5353 |
<MapPin className="w-6 h-6 text-gray-300 dark:text-gray-600 mx-auto mb-2" /> |
| 5354 |
<p className="text-xs text-gray-500 dark:text-gray-400"> |
| 5355 |
{__("No landmarks added yet", "yatra")} |
| 5356 |
</p> |
| 5357 |
</div> |
| 5358 |
)} |
| 5359 |
<Button |
| 5360 |
type="button" |
| 5361 |
variant="outline" |
| 5362 |
onClick={() => { |
| 5363 |
setShowLandmarkDialog(true); |
| 5364 |
setLandmarkInput(""); |
| 5365 |
}} |
| 5366 |
className="w-full" |
| 5367 |
> |
| 5368 |
<Plus className="w-4 h-4 mr-2" /> |
| 5369 |
{__("Add Landmark", "yatra")} |
| 5370 |
</Button> |
| 5371 |
</div> |
| 5372 |
</div> |
| 5373 |
</div> |
| 5374 |
); |
| 5375 |
|
| 5376 |
case "duration": |
| 5377 |
return ( |
| 5378 |
<div className="space-y-6"> |
| 5379 |
<div className="flex flex-wrap items-center gap-2 mb-2"> |
| 5380 |
<Calendar className="w-5 h-5 text-gray-500" /> |
| 5381 |
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> |
| 5382 |
{__("Availability & Booking", "yatra")} |
| 5383 |
</h2> |
| 5384 |
<Badge |
| 5385 |
variant="outline" |
| 5386 |
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" |
| 5387 |
> |
| 5388 |
{__("Recommended", "yatra")} |
| 5389 |
</Badge> |
| 5390 |
</div> |
| 5391 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"> |
| 5392 |
{__("When is it available & who can book it?", "yatra")} |
| 5393 |
<span className="block text-xs text-gray-500 dark:text-gray-400 mt-1"> |
| 5394 |
{__( |
| 5395 |
"Set your trip's availability period, capacity limits, and booking requirements. This helps automate bookings and prevent overbooking.", |
| 5396 |
"yatra", |
| 5397 |
)} |
| 5398 |
</span> |
| 5399 |
</p> |
| 5400 |
|
| 5401 |
<div className="space-y-6"> |
| 5402 |
{/* SECTION 1: Availability Period */} |
| 5403 |
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30"> |
| 5404 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> |
| 5405 |
<Calendar className="w-4 h-4 text-blue-500" /> |
| 5406 |
{__("Availability Period", "yatra")} |
| 5407 |
</h3> |
| 5408 |
<div className="space-y-4"> |
| 5409 |
{/* Availability Dates */} |
| 5410 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 5411 |
<div> |
| 5412 |
<label |
| 5413 |
htmlFor="available_from" |
| 5414 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 5415 |
> |
| 5416 |
{__("Available From", "yatra")} |
| 5417 |
</label> |
| 5418 |
<DatePicker |
| 5419 |
value={formData.available_from} |
| 5420 |
onChange={(val) => |
| 5421 |
handleFieldChange("available_from", val) |
| 5422 |
} |
| 5423 |
placeholder={__("Select date", "yatra")} |
| 5424 |
/> |
| 5425 |
<HelpText |
| 5426 |
text={__( |
| 5427 |
"Earliest date this trip becomes available for booking", |
| 5428 |
"yatra", |
| 5429 |
)} |
| 5430 |
className="mt-2" |
| 5431 |
/> |
| 5432 |
</div> |
| 5433 |
<div> |
| 5434 |
<label |
| 5435 |
htmlFor="available_to" |
| 5436 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 5437 |
> |
| 5438 |
{__("Available To", "yatra")} |
| 5439 |
</label> |
| 5440 |
<DatePicker |
| 5441 |
value={formData.available_to} |
| 5442 |
onChange={(val) => |
| 5443 |
handleFieldChange("available_to", val) |
| 5444 |
} |
| 5445 |
placeholder={__("Select date", "yatra")} |
| 5446 |
/> |
| 5447 |
<HelpText |
| 5448 |
text={__( |
| 5449 |
"Latest date this trip is available for booking", |
| 5450 |
"yatra", |
| 5451 |
)} |
| 5452 |
className="mt-2" |
| 5453 |
/> |
| 5454 |
</div> |
| 5455 |
</div> |
| 5456 |
|
| 5457 |
{/* Booking Window */} |
| 5458 |
<div> |
| 5459 |
<label |
| 5460 |
htmlFor="booking_window_days" |
| 5461 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 5462 |
> |
| 5463 |
{__("Booking Window (Days in Advance)", "yatra")} |
| 5464 |
</label> |
| 5465 |
<Input |
| 5466 |
id="booking_window_days" |
| 5467 |
type="number" |
| 5468 |
min="0" |
| 5469 |
value={formData.booking_window_days} |
| 5470 |
onChange={(e) => |
| 5471 |
handleFieldChange("booking_window_days", e.target.value) |
| 5472 |
} |
| 5473 |
placeholder={__("e.g., 30", "yatra")} |
| 5474 |
/> |
| 5475 |
<HelpText |
| 5476 |
text={__( |
| 5477 |
"Minimum days in advance customers can book this trip", |
| 5478 |
"yatra", |
| 5479 |
)} |
| 5480 |
className="mt-2" |
| 5481 |
/> |
| 5482 |
</div> |
| 5483 |
|
| 5484 |
{/* Seasonal Availability */} |
| 5485 |
<div> |
| 5486 |
<label |
| 5487 |
htmlFor="seasonal_availability" |
| 5488 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 5489 |
> |
| 5490 |
{__("Seasonal Availability Notes", "yatra")} |
| 5491 |
</label> |
| 5492 |
<Input |
| 5493 |
id="seasonal_availability" |
| 5494 |
type="text" |
| 5495 |
value={formData.seasonal_availability} |
| 5496 |
onChange={(e) => |
| 5497 |
handleFieldChange( |
| 5498 |
"seasonal_availability", |
| 5499 |
e.target.value, |
| 5500 |
) |
| 5501 |
} |
| 5502 |
placeholder={__( |
| 5503 |
"e.g., Available year-round except monsoon season", |
| 5504 |
"yatra", |
| 5505 |
)} |
| 5506 |
/> |
| 5507 |
<HelpText |
| 5508 |
text={__( |
| 5509 |
"General notes about when this trip is typically available", |
| 5510 |
"yatra", |
| 5511 |
)} |
| 5512 |
className="mt-2" |
| 5513 |
/> |
| 5514 |
</div> |
| 5515 |
</div> |
| 5516 |
</div> |
| 5517 |
|
| 5518 |
{/* SECTION 2: Capacity & Travelers */} |
| 5519 |
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30"> |
| 5520 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> |
| 5521 |
<Users className="w-4 h-4 text-blue-500" /> |
| 5522 |
{__("Capacity & Travelers", "yatra")} |
| 5523 |
</h3> |
| 5524 |
<div className="space-y-4"> |
| 5525 |
{/* Group Size */} |
| 5526 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 5527 |
<div> |
| 5528 |
<label |
| 5529 |
htmlFor="min_travelers" |
| 5530 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 5531 |
> |
| 5532 |
{__("Minimum Travelers", "yatra")} |
| 5533 |
</label> |
| 5534 |
<Input |
| 5535 |
id="min_travelers" |
| 5536 |
type="number" |
| 5537 |
min="1" |
| 5538 |
value={formData.min_travelers} |
| 5539 |
onChange={(e) => |
| 5540 |
handleFieldChange("min_travelers", e.target.value) |
| 5541 |
} |
| 5542 |
className={errors.min_travelers ? "border-red-500" : ""} |
| 5543 |
/> |
| 5544 |
{errors.min_travelers && ( |
| 5545 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 5546 |
<AlertCircle className="w-4 h-4" /> |
| 5547 |
{errors.min_travelers} |
| 5548 |
</p> |
| 5549 |
)} |
| 5550 |
</div> |
| 5551 |
<div> |
| 5552 |
<label |
| 5553 |
htmlFor="max_travelers" |
| 5554 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 5555 |
> |
| 5556 |
{__("Maximum Travelers", "yatra")} |
| 5557 |
</label> |
| 5558 |
<Input |
| 5559 |
id="max_travelers" |
| 5560 |
type="number" |
| 5561 |
min="1" |
| 5562 |
value={formData.max_travelers} |
| 5563 |
onChange={(e) => |
| 5564 |
handleFieldChange("max_travelers", e.target.value) |
| 5565 |
} |
| 5566 |
className={errors.max_travelers ? "border-red-500" : ""} |
| 5567 |
/> |
| 5568 |
{errors.max_travelers && ( |
| 5569 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 5570 |
<AlertCircle className="w-4 h-4" /> |
| 5571 |
{errors.max_travelers} |
| 5572 |
</p> |
| 5573 |
)} |
| 5574 |
</div> |
| 5575 |
</div> |
| 5576 |
|
| 5577 |
{/* Fallback Settings */} |
| 5578 |
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4"> |
| 5579 |
<div className="flex items-start gap-2 mb-3"> |
| 5580 |
<h4 className="text-sm font-medium text-gray-900 dark:text-white"> |
| 5581 |
{__("Fallback Settings", "yatra")} |
| 5582 |
</h4> |
| 5583 |
<span |
| 5584 |
title={__( |
| 5585 |
"These settings are used when the trip has no availability dates or recurring rules set. They provide default values for flexible booking.", |
| 5586 |
"yatra", |
| 5587 |
)} |
| 5588 |
> |
| 5589 |
<HelpCircle className="w-4 h-4 text-gray-400 cursor-help" /> |
| 5590 |
</span> |
| 5591 |
</div> |
| 5592 |
|
| 5593 |
{/* Info Banner */} |
| 5594 |
<div className="mb-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg"> |
| 5595 |
<div className="flex items-start gap-2"> |
| 5596 |
<AlertCircle className="w-4 h-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" /> |
| 5597 |
<div className="text-xs text-amber-800 dark:text-amber-200"> |
| 5598 |
<p className="font-medium mb-1"> |
| 5599 |
{__("When are these settings used?", "yatra")} |
| 5600 |
</p> |
| 5601 |
<p className="text-amber-700 dark:text-amber-300"> |
| 5602 |
{__( |
| 5603 |
"These settings apply ONLY when your trip has ZERO availability dates AND ZERO recurring rules. They provide defaults for flexible booking scenarios.", |
| 5604 |
"yatra", |
| 5605 |
)} |
| 5606 |
</p> |
| 5607 |
</div> |
| 5608 |
</div> |
| 5609 |
</div> |
| 5610 |
|
| 5611 |
{/* Settings Content - Grouped by Trip Type */} |
| 5612 |
<div className="space-y-4"> |
| 5613 |
{/* Day Tour Settings */} |
| 5614 |
{formData.trip_type === "single_day" && ( |
| 5615 |
<div className="p-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700"> |
| 5616 |
<h5 className="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2"> |
| 5617 |
<Clock className="w-4 h-4" /> |
| 5618 |
{__("Day Tour Time Settings", "yatra")} |
| 5619 |
</h5> |
| 5620 |
|
| 5621 |
{/* Enable Multiple Time Slots Toggle */} |
| 5622 |
<div className="mb-4 p-3 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700"> |
| 5623 |
<div className="flex items-start gap-3"> |
| 5624 |
<input |
| 5625 |
type="checkbox" |
| 5626 |
id="has_default_time_slots" |
| 5627 |
checked={formData.has_default_time_slots} |
| 5628 |
onChange={(e) => |
| 5629 |
handleFieldChange( |
| 5630 |
"has_default_time_slots", |
| 5631 |
e.target.checked, |
| 5632 |
) |
| 5633 |
} |
| 5634 |
className="mt-0.5 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" |
| 5635 |
/> |
| 5636 |
<div className="flex-1"> |
| 5637 |
<label |
| 5638 |
htmlFor="has_default_time_slots" |
| 5639 |
className="text-sm font-medium text-gray-900 dark:text-white cursor-pointer" |
| 5640 |
> |
| 5641 |
{__("Enable Multiple Time Slots", "yatra")} |
| 5642 |
</label> |
| 5643 |
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> |
| 5644 |
{__( |
| 5645 |
"Allow customers to select from multiple departure times (e.g., Morning, Afternoon, Evening tours)", |
| 5646 |
"yatra", |
| 5647 |
)} |
| 5648 |
</p> |
| 5649 |
</div> |
| 5650 |
</div> |
| 5651 |
</div> |
| 5652 |
|
| 5653 |
{/* Time Slots Configuration (if enabled) */} |
| 5654 |
{formData.has_default_time_slots && ( |
| 5655 |
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800"> |
| 5656 |
<div className="flex items-center justify-between mb-3"> |
| 5657 |
<div> |
| 5658 |
<label className="block text-sm font-medium text-gray-900 dark:text-white"> |
| 5659 |
{__("Time Slots Configuration", "yatra")} |
| 5660 |
</label> |
| 5661 |
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1"> |
| 5662 |
{__( |
| 5663 |
"Add multiple departure times for customers to choose from.", |
| 5664 |
"yatra", |
| 5665 |
)} |
| 5666 |
</p> |
| 5667 |
</div> |
| 5668 |
<Button |
| 5669 |
type="button" |
| 5670 |
variant="outline" |
| 5671 |
size="sm" |
| 5672 |
onClick={() => { |
| 5673 |
const newSlot: TimeSlot = { |
| 5674 |
id: `slot-${Date.now()}`, |
| 5675 |
time: "09:00", |
| 5676 |
label: "", |
| 5677 |
}; |
| 5678 |
handleFieldChange("default_time_slots", [ |
| 5679 |
...formData.default_time_slots, |
| 5680 |
newSlot, |
| 5681 |
]); |
| 5682 |
}} |
| 5683 |
className="flex items-center gap-1" |
| 5684 |
> |
| 5685 |
<Plus className="w-4 h-4" /> |
| 5686 |
{__("Add Time Slot", "yatra")} |
| 5687 |
</Button> |
| 5688 |
</div> |
| 5689 |
|
| 5690 |
{formData.default_time_slots.length === 0 ? ( |
| 5691 |
<div className="text-center py-8 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg"> |
| 5692 |
<Clock className="w-8 h-8 mx-auto text-gray-400 mb-2" /> |
| 5693 |
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3"> |
| 5694 |
{__("No time slots added yet", "yatra")} |
| 5695 |
</p> |
| 5696 |
<Button |
| 5697 |
type="button" |
| 5698 |
variant="outline" |
| 5699 |
size="sm" |
| 5700 |
onClick={() => { |
| 5701 |
const newSlot: TimeSlot = { |
| 5702 |
id: `slot-${Date.now()}`, |
| 5703 |
time: "09:00", |
| 5704 |
label: __("Morning Tour", "yatra"), |
| 5705 |
}; |
| 5706 |
handleFieldChange("default_time_slots", [ |
| 5707 |
newSlot, |
| 5708 |
]); |
| 5709 |
}} |
| 5710 |
> |
| 5711 |
<Plus className="w-4 h-4 mr-1" /> |
| 5712 |
{__("Add First Time Slot", "yatra")} |
| 5713 |
</Button> |
| 5714 |
</div> |
| 5715 |
) : ( |
| 5716 |
<div className="space-y-3"> |
| 5717 |
{formData.default_time_slots.map( |
| 5718 |
(slot, index) => ( |
| 5719 |
<div |
| 5720 |
key={slot.id} |
| 5721 |
className="flex items-start gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700" |
| 5722 |
> |
| 5723 |
<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"> |
| 5724 |
<span className="text-sm font-semibold text-blue-600 dark:text-blue-400"> |
| 5725 |
{index + 1} |
| 5726 |
</span> |
| 5727 |
</div> |
| 5728 |
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3"> |
| 5729 |
<div> |
| 5730 |
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"> |
| 5731 |
{__("Time", "yatra")} |
| 5732 |
</label> |
| 5733 |
<Input |
| 5734 |
type="time" |
| 5735 |
value={slot.time} |
| 5736 |
onChange={(e) => { |
| 5737 |
const updated = [ |
| 5738 |
...formData.default_time_slots, |
| 5739 |
]; |
| 5740 |
updated[index] = { |
| 5741 |
...slot, |
| 5742 |
time: e.target.value, |
| 5743 |
}; |
| 5744 |
handleFieldChange( |
| 5745 |
"default_time_slots", |
| 5746 |
updated, |
| 5747 |
); |
| 5748 |
}} |
| 5749 |
className="w-full" |
| 5750 |
/> |
| 5751 |
</div> |
| 5752 |
<div> |
| 5753 |
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"> |
| 5754 |
{__("Label", "yatra")} |
| 5755 |
</label> |
| 5756 |
<Input |
| 5757 |
type="text" |
| 5758 |
value={slot.label} |
| 5759 |
onChange={(e) => { |
| 5760 |
const updated = [ |
| 5761 |
...formData.default_time_slots, |
| 5762 |
]; |
| 5763 |
updated[index] = { |
| 5764 |
...slot, |
| 5765 |
label: e.target.value, |
| 5766 |
}; |
| 5767 |
handleFieldChange( |
| 5768 |
"default_time_slots", |
| 5769 |
updated, |
| 5770 |
); |
| 5771 |
}} |
| 5772 |
placeholder={__( |
| 5773 |
"e.g., Morning Tour, Afternoon Tour", |
| 5774 |
"yatra", |
| 5775 |
)} |
| 5776 |
className="w-full" |
| 5777 |
/> |
| 5778 |
</div> |
| 5779 |
</div> |
| 5780 |
<Button |
| 5781 |
type="button" |
| 5782 |
variant="ghost" |
| 5783 |
size="sm" |
| 5784 |
onClick={() => { |
| 5785 |
const updated = |
| 5786 |
formData.default_time_slots.filter( |
| 5787 |
(_, i) => i !== index, |
| 5788 |
); |
| 5789 |
handleFieldChange( |
| 5790 |
"default_time_slots", |
| 5791 |
updated, |
| 5792 |
); |
| 5793 |
}} |
| 5794 |
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 flex-shrink-0 mt-1" |
| 5795 |
title={__( |
| 5796 |
"Remove time slot", |
| 5797 |
"yatra", |
| 5798 |
)} |
| 5799 |
> |
| 5800 |
<X className="w-4 h-4" /> |
| 5801 |
</Button> |
| 5802 |
</div> |
| 5803 |
), |
| 5804 |
)} |
| 5805 |
</div> |
| 5806 |
)} |
| 5807 |
</div> |
| 5808 |
)} |
| 5809 |
|
| 5810 |
{/* Single Departure Time (if multiple slots disabled) */} |
| 5811 |
{!formData.has_default_time_slots && ( |
| 5812 |
<div> |
| 5813 |
<label |
| 5814 |
htmlFor="departure_time_single" |
| 5815 |
className="block text-sm font-medium text-gray-900 dark:text-white mb-2" |
| 5816 |
> |
| 5817 |
{__("Default Departure Time", "yatra")} |
| 5818 |
</label> |
| 5819 |
<Input |
| 5820 |
id="departure_time_single" |
| 5821 |
type="time" |
| 5822 |
value={formData.departure_time} |
| 5823 |
onChange={(e) => |
| 5824 |
handleFieldChange( |
| 5825 |
"departure_time", |
| 5826 |
e.target.value, |
| 5827 |
) |
| 5828 |
} |
| 5829 |
className="max-w-xs" |
| 5830 |
/> |
| 5831 |
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400"> |
| 5832 |
{__( |
| 5833 |
"Single departure time for all bookings when multiple time slots are not enabled.", |
| 5834 |
"yatra", |
| 5835 |
)} |
| 5836 |
</p> |
| 5837 |
</div> |
| 5838 |
)} |
| 5839 |
</div> |
| 5840 |
)} |
| 5841 |
|
| 5842 |
{/* Multi-Day Trip Settings */} |
| 5843 |
{formData.trip_type === "multi_day" && ( |
| 5844 |
<div className="p-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700"> |
| 5845 |
<h5 className="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2"> |
| 5846 |
<Calendar className="w-4 h-4" /> |
| 5847 |
{__("Multi-Day Trip Departure Settings", "yatra")} |
| 5848 |
</h5> |
| 5849 |
|
| 5850 |
<div> |
| 5851 |
<label |
| 5852 |
htmlFor="departure_time_multiday" |
| 5853 |
className="block text-sm font-medium text-gray-900 dark:text-white mb-2" |
| 5854 |
> |
| 5855 |
{__("Default Departure Time", "yatra")} |
| 5856 |
</label> |
| 5857 |
<Input |
| 5858 |
id="departure_time_multiday" |
| 5859 |
type="time" |
| 5860 |
value={formData.departure_time} |
| 5861 |
onChange={(e) => |
| 5862 |
handleFieldChange( |
| 5863 |
"departure_time", |
| 5864 |
e.target.value, |
| 5865 |
) |
| 5866 |
} |
| 5867 |
className="max-w-xs" |
| 5868 |
/> |
| 5869 |
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400"> |
| 5870 |
{__( |
| 5871 |
"Default departure time for trips without specific availability dates.", |
| 5872 |
"yatra", |
| 5873 |
)} |
| 5874 |
</p> |
| 5875 |
</div> |
| 5876 |
</div> |
| 5877 |
)} |
| 5878 |
|
| 5879 |
{/* No Trip Type Selected */} |
| 5880 |
{!formData.trip_type && ( |
| 5881 |
<div className="p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700 text-center"> |
| 5882 |
<p className="text-sm text-gray-500 dark:text-gray-400"> |
| 5883 |
{__( |
| 5884 |
"Please select a trip type (Day Tour or Multi-Day) to configure fallback settings.", |
| 5885 |
"yatra", |
| 5886 |
)} |
| 5887 |
</p> |
| 5888 |
</div> |
| 5889 |
)} |
| 5890 |
</div> |
| 5891 |
</div> |
| 5892 |
</div> |
| 5893 |
</div> |
| 5894 |
|
| 5895 |
{/* SECTION 3: Booking Policies */} |
| 5896 |
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30"> |
| 5897 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> |
| 5898 |
<FileText className="w-4 h-4 text-blue-500" /> |
| 5899 |
{__("Booking Policies", "yatra")} |
| 5900 |
</h3> |
| 5901 |
<div className="space-y-4"> |
| 5902 |
{/* Age Restrictions */} |
| 5903 |
<div> |
| 5904 |
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3"> |
| 5905 |
{__("Age Restrictions", "yatra")} |
| 5906 |
</h4> |
| 5907 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 5908 |
<div> |
| 5909 |
<label |
| 5910 |
htmlFor="age_min" |
| 5911 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 5912 |
> |
| 5913 |
{__("Minimum Age", "yatra")} |
| 5914 |
</label> |
| 5915 |
<Input |
| 5916 |
id="age_min" |
| 5917 |
type="number" |
| 5918 |
min="0" |
| 5919 |
value={formData.age_min} |
| 5920 |
onChange={(e) => |
| 5921 |
handleFieldChange("age_min", e.target.value) |
| 5922 |
} |
| 5923 |
placeholder={__("e.g., 18", "yatra")} |
| 5924 |
className={errors.age_min ? "border-red-500" : ""} |
| 5925 |
/> |
| 5926 |
{errors.age_min && ( |
| 5927 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 5928 |
<AlertCircle className="w-4 h-4" /> |
| 5929 |
{errors.age_min} |
| 5930 |
</p> |
| 5931 |
)} |
| 5932 |
</div> |
| 5933 |
<div> |
| 5934 |
<label |
| 5935 |
htmlFor="age_max" |
| 5936 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 5937 |
> |
| 5938 |
{__("Maximum Age", "yatra")} |
| 5939 |
</label> |
| 5940 |
<Input |
| 5941 |
id="age_max" |
| 5942 |
type="number" |
| 5943 |
min="0" |
| 5944 |
value={formData.age_max} |
| 5945 |
onChange={(e) => |
| 5946 |
handleFieldChange("age_max", e.target.value) |
| 5947 |
} |
| 5948 |
placeholder={__("e.g., 65", "yatra")} |
| 5949 |
className={errors.age_max ? "border-red-500" : ""} |
| 5950 |
/> |
| 5951 |
{errors.age_max && ( |
| 5952 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 5953 |
<AlertCircle className="w-4 h-4" /> |
| 5954 |
{errors.age_max} |
| 5955 |
</p> |
| 5956 |
)} |
| 5957 |
</div> |
| 5958 |
</div> |
| 5959 |
</div> |
| 5960 |
|
| 5961 |
{/* Requirements */} |
| 5962 |
<div> |
| 5963 |
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3"> |
| 5964 |
{__("Trip Requirements", "yatra")} |
| 5965 |
</h4> |
| 5966 |
<div className="space-y-4"> |
| 5967 |
<div> |
| 5968 |
<label |
| 5969 |
htmlFor="physical_requirements" |
| 5970 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 5971 |
> |
| 5972 |
{__("Physical Requirements", "yatra")} |
| 5973 |
</label> |
| 5974 |
<textarea |
| 5975 |
id="physical_requirements" |
| 5976 |
value={formData.physical_requirements} |
| 5977 |
onChange={(e) => |
| 5978 |
handleFieldChange( |
| 5979 |
"physical_requirements", |
| 5980 |
e.target.value, |
| 5981 |
) |
| 5982 |
} |
| 5983 |
placeholder={__( |
| 5984 |
"e.g., Moderate fitness level required. Some walking involved but no strenuous activities.", |
| 5985 |
"yatra", |
| 5986 |
)} |
| 5987 |
rows={3} |
| 5988 |
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" |
| 5989 |
/> |
| 5990 |
</div> |
| 5991 |
<div> |
| 5992 |
<label |
| 5993 |
htmlFor="visa_requirements" |
| 5994 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 5995 |
> |
| 5996 |
{__("Visa Requirements", "yatra")} |
| 5997 |
</label> |
| 5998 |
<textarea |
| 5999 |
id="visa_requirements" |
| 6000 |
value={formData.visa_requirements} |
| 6001 |
onChange={(e) => |
| 6002 |
handleFieldChange( |
| 6003 |
"visa_requirements", |
| 6004 |
e.target.value, |
| 6005 |
) |
| 6006 |
} |
| 6007 |
placeholder={__( |
| 6008 |
"e.g., Tourist visa required. Can be obtained on arrival or in advance.", |
| 6009 |
"yatra", |
| 6010 |
)} |
| 6011 |
rows={3} |
| 6012 |
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" |
| 6013 |
/> |
| 6014 |
</div> |
| 6015 |
<div> |
| 6016 |
<label |
| 6017 |
htmlFor="vaccination_requirements" |
| 6018 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 6019 |
> |
| 6020 |
{__("Vaccination Requirements", "yatra")} |
| 6021 |
</label> |
| 6022 |
<textarea |
| 6023 |
id="vaccination_requirements" |
| 6024 |
value={formData.vaccination_requirements} |
| 6025 |
onChange={(e) => |
| 6026 |
handleFieldChange( |
| 6027 |
"vaccination_requirements", |
| 6028 |
e.target.value, |
| 6029 |
) |
| 6030 |
} |
| 6031 |
placeholder={__( |
| 6032 |
"e.g., No mandatory vaccinations. Recommended: Hepatitis A, Typhoid, and routine vaccinations.", |
| 6033 |
"yatra", |
| 6034 |
)} |
| 6035 |
rows={3} |
| 6036 |
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" |
| 6037 |
/> |
| 6038 |
</div> |
| 6039 |
</div> |
| 6040 |
</div> |
| 6041 |
|
| 6042 |
{/* Cancellation Policy */} |
| 6043 |
<div> |
| 6044 |
<div className="flex items-center justify-between mb-3"> |
| 6045 |
<h4 className="text-sm font-medium text-gray-900 dark:text-white"> |
| 6046 |
{__("Cancellation Policy", "yatra")} |
| 6047 |
</h4> |
| 6048 |
<AiFieldAffordance |
| 6049 |
task="trip-cancellation-policy" |
| 6050 |
label={__("Cancellation Policy", "yatra")} |
| 6051 |
value={formData.cancellation_policy} |
| 6052 |
onAccept={(v) => |
| 6053 |
handleFieldChange("cancellation_policy", v) |
| 6054 |
} |
| 6055 |
buildContext={() => buildTripAiContext(formData)} |
| 6056 |
/> |
| 6057 |
</div> |
| 6058 |
<textarea |
| 6059 |
id="cancellation_policy" |
| 6060 |
value={formData.cancellation_policy} |
| 6061 |
onChange={(e) => |
| 6062 |
handleFieldChange("cancellation_policy", e.target.value) |
| 6063 |
} |
| 6064 |
placeholder={__( |
| 6065 |
"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.", |
| 6066 |
"yatra", |
| 6067 |
)} |
| 6068 |
rows={4} |
| 6069 |
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" |
| 6070 |
/> |
| 6071 |
</div> |
| 6072 |
</div> |
| 6073 |
</div> |
| 6074 |
|
| 6075 |
{/* SECTION 3b: Disable Booking (Pro) */} |
| 6076 |
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30"> |
| 6077 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> |
| 6078 |
<Calendar className="w-4 h-4 text-blue-500" /> |
| 6079 |
{__("Booking Availability", "yatra")} |
| 6080 |
<ProBadge isProActive={isPro} /> |
| 6081 |
</h3> |
| 6082 |
<ProFeature |
| 6083 |
description={__( |
| 6084 |
"disable the booking calendar and make trips enquiry-only", |
| 6085 |
"yatra", |
| 6086 |
)} |
| 6087 |
moduleName={__("Enquiry Only Mode", "yatra")} |
| 6088 |
pricingUrl="https://wpyatra.com/pricing" |
| 6089 |
isProActive={isPro} |
| 6090 |
isModuleEnabled={isPro} |
| 6091 |
> |
| 6092 |
<div className="flex items-start gap-3"> |
| 6093 |
<div className="flex items-center h-5 mt-0.5"> |
| 6094 |
<input |
| 6095 |
id="disable_booking" |
| 6096 |
type="checkbox" |
| 6097 |
checked={formData.disable_booking} |
| 6098 |
onChange={(e) => |
| 6099 |
handleFieldChange("disable_booking", e.target.checked) |
| 6100 |
} |
| 6101 |
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" |
| 6102 |
/> |
| 6103 |
</div> |
| 6104 |
<div> |
| 6105 |
<label |
| 6106 |
htmlFor="disable_booking" |
| 6107 |
className="block text-sm font-medium text-gray-900 dark:text-white cursor-pointer" |
| 6108 |
> |
| 6109 |
{__("Disable Booking Calendar", "yatra")} |
| 6110 |
</label> |
| 6111 |
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> |
| 6112 |
{__( |
| 6113 |
"When enabled, the booking form and date picker will be hidden. Visitors can only send an enquiry for this trip.", |
| 6114 |
"yatra", |
| 6115 |
)} |
| 6116 |
</p> |
| 6117 |
</div> |
| 6118 |
</div> |
| 6119 |
</ProFeature> |
| 6120 |
</div> |
| 6121 |
|
| 6122 |
{/* SECTION 4: Accommodation */} |
| 6123 |
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30"> |
| 6124 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> |
| 6125 |
<Home className="w-4 h-4 text-blue-500" /> |
| 6126 |
{__("Accommodation", "yatra")} |
| 6127 |
</h3> |
| 6128 |
<div className="space-y-4"> |
| 6129 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 6130 |
<div> |
| 6131 |
<label |
| 6132 |
htmlFor="accommodation_type" |
| 6133 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 6134 |
> |
| 6135 |
{__("Accommodation Type", "yatra")} |
| 6136 |
</label> |
| 6137 |
<Input |
| 6138 |
id="accommodation_type" |
| 6139 |
type="text" |
| 6140 |
value={formData.accommodation_type} |
| 6141 |
onChange={(e) => |
| 6142 |
handleFieldChange( |
| 6143 |
"accommodation_type", |
| 6144 |
e.target.value, |
| 6145 |
) |
| 6146 |
} |
| 6147 |
placeholder={__( |
| 6148 |
"e.g., Hotel, Resort, Teahouse, Camping", |
| 6149 |
"yatra", |
| 6150 |
)} |
| 6151 |
/> |
| 6152 |
</div> |
| 6153 |
<div> |
| 6154 |
<label |
| 6155 |
htmlFor="meal_plan" |
| 6156 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 6157 |
> |
| 6158 |
{__("Meal Plan", "yatra")} |
| 6159 |
</label> |
| 6160 |
<Select |
| 6161 |
id="meal_plan" |
| 6162 |
value={formData.meal_plan} |
| 6163 |
onChange={(e) => |
| 6164 |
handleFieldChange("meal_plan", e.target.value) |
| 6165 |
} |
| 6166 |
> |
| 6167 |
{MEAL_PLAN_SELECT_OPTIONS.map(({ value, label }) => ( |
| 6168 |
<option |
| 6169 |
key={value || "meal-plan-empty"} |
| 6170 |
value={value} |
| 6171 |
> |
| 6172 |
{label} |
| 6173 |
</option> |
| 6174 |
))} |
| 6175 |
</Select> |
| 6176 |
<p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400"> |
| 6177 |
{__( |
| 6178 |
"Tells travelers which meals are included with lodging (e.g. breakfast only vs full board).", |
| 6179 |
"yatra", |
| 6180 |
)} |
| 6181 |
</p> |
| 6182 |
</div> |
| 6183 |
</div> |
| 6184 |
<div> |
| 6185 |
<label |
| 6186 |
htmlFor="accommodation_details" |
| 6187 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 6188 |
> |
| 6189 |
{__("Accommodation Details", "yatra")} |
| 6190 |
</label> |
| 6191 |
<textarea |
| 6192 |
id="accommodation_details" |
| 6193 |
value={formData.accommodation_details} |
| 6194 |
onChange={(e) => |
| 6195 |
handleFieldChange( |
| 6196 |
"accommodation_details", |
| 6197 |
e.target.value, |
| 6198 |
) |
| 6199 |
} |
| 6200 |
placeholder={__( |
| 6201 |
"e.g., 4-star beachfront resort with private balconies, infinity pool, and spa facilities", |
| 6202 |
"yatra", |
| 6203 |
)} |
| 6204 |
rows={3} |
| 6205 |
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" |
| 6206 |
/> |
| 6207 |
</div> |
| 6208 |
</div> |
| 6209 |
</div> |
| 6210 |
|
| 6211 |
{/* SECTION 5: Transportation */} |
| 6212 |
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30"> |
| 6213 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> |
| 6214 |
<Car className="w-4 h-4 text-blue-500" /> |
| 6215 |
{__("Transportation", "yatra")} |
| 6216 |
</h3> |
| 6217 |
<div className="space-y-4"> |
| 6218 |
<div className="flex items-center gap-2"> |
| 6219 |
<input |
| 6220 |
type="checkbox" |
| 6221 |
id="transportation_included" |
| 6222 |
checked={formData.transportation_included} |
| 6223 |
onChange={(e) => |
| 6224 |
handleFieldChange( |
| 6225 |
"transportation_included", |
| 6226 |
e.target.checked, |
| 6227 |
) |
| 6228 |
} |
| 6229 |
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" |
| 6230 |
/> |
| 6231 |
<label |
| 6232 |
htmlFor="transportation_included" |
| 6233 |
className="text-sm font-medium text-gray-900 dark:text-white" |
| 6234 |
> |
| 6235 |
{__("Transportation Included", "yatra")} |
| 6236 |
</label> |
| 6237 |
</div> |
| 6238 |
{formData.transportation_included && ( |
| 6239 |
<> |
| 6240 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 6241 |
<div> |
| 6242 |
<label |
| 6243 |
htmlFor="pickup_location" |
| 6244 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 6245 |
> |
| 6246 |
{__("Pickup Location", "yatra")} |
| 6247 |
</label> |
| 6248 |
<Input |
| 6249 |
id="pickup_location" |
| 6250 |
type="text" |
| 6251 |
value={formData.pickup_location} |
| 6252 |
onChange={(e) => |
| 6253 |
handleFieldChange( |
| 6254 |
"pickup_location", |
| 6255 |
e.target.value, |
| 6256 |
) |
| 6257 |
} |
| 6258 |
placeholder={__( |
| 6259 |
"e.g., Airport, Hotel, City Center", |
| 6260 |
"yatra", |
| 6261 |
)} |
| 6262 |
/> |
| 6263 |
</div> |
| 6264 |
<div> |
| 6265 |
<label |
| 6266 |
htmlFor="dropoff_location" |
| 6267 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 6268 |
> |
| 6269 |
{__("Dropoff Location", "yatra")} |
| 6270 |
</label> |
| 6271 |
<Input |
| 6272 |
id="dropoff_location" |
| 6273 |
type="text" |
| 6274 |
value={formData.dropoff_location} |
| 6275 |
onChange={(e) => |
| 6276 |
handleFieldChange( |
| 6277 |
"dropoff_location", |
| 6278 |
e.target.value, |
| 6279 |
) |
| 6280 |
} |
| 6281 |
placeholder={__( |
| 6282 |
"e.g., Airport, Hotel, City Center", |
| 6283 |
"yatra", |
| 6284 |
)} |
| 6285 |
/> |
| 6286 |
</div> |
| 6287 |
</div> |
| 6288 |
<div> |
| 6289 |
<label |
| 6290 |
htmlFor="transportation_details" |
| 6291 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 6292 |
> |
| 6293 |
{__("Transportation Details", "yatra")} |
| 6294 |
</label> |
| 6295 |
<textarea |
| 6296 |
id="transportation_details" |
| 6297 |
value={formData.transportation_details} |
| 6298 |
onChange={(e) => |
| 6299 |
handleFieldChange( |
| 6300 |
"transportation_details", |
| 6301 |
e.target.value, |
| 6302 |
) |
| 6303 |
} |
| 6304 |
placeholder={__( |
| 6305 |
"e.g., Private air-conditioned vehicle with professional driver", |
| 6306 |
"yatra", |
| 6307 |
)} |
| 6308 |
rows={3} |
| 6309 |
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" |
| 6310 |
/> |
| 6311 |
</div> |
| 6312 |
</> |
| 6313 |
)} |
| 6314 |
</div> |
| 6315 |
</div> |
| 6316 |
</div> |
| 6317 |
</div> |
| 6318 |
); |
| 6319 |
|
| 6320 |
case "categorization": |
| 6321 |
return ( |
| 6322 |
<div className="space-y-6"> |
| 6323 |
<div className="flex flex-wrap items-center gap-2 mb-2"> |
| 6324 |
<Tag className="w-5 h-5 text-gray-500" /> |
| 6325 |
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> |
| 6326 |
{__("Categories & Attributes", "yatra")} |
| 6327 |
</h2> |
| 6328 |
<Badge |
| 6329 |
variant="outline" |
| 6330 |
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" |
| 6331 |
> |
| 6332 |
{__("Recommended", "yatra")} |
| 6333 |
</Badge> |
| 6334 |
</div> |
| 6335 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"> |
| 6336 |
{__("How should travelers find this trip?", "yatra")} |
| 6337 |
<span className="block text-xs text-gray-500 dark:text-gray-400 mt-1"> |
| 6338 |
{__( |
| 6339 |
"Organize your trip with categories, activities, and custom attributes to help travelers discover it through search and filters.", |
| 6340 |
"yatra", |
| 6341 |
)} |
| 6342 |
</span> |
| 6343 |
</p> |
| 6344 |
|
| 6345 |
<div className="space-y-6"> |
| 6346 |
{/* SECTION 1: Categories & Classification */} |
| 6347 |
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30"> |
| 6348 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> |
| 6349 |
<Tag className="w-4 h-4 text-blue-500" /> |
| 6350 |
{__("Categories & Classification", "yatra")} |
| 6351 |
</h3> |
| 6352 |
<div className="space-y-4"> |
| 6353 |
{/* Trip Category */} |
| 6354 |
<div> |
| 6355 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 6356 |
{__("Trip Categories", "yatra")} |
| 6357 |
</label> |
| 6358 |
{tripCategories && tripCategories.length > 0 ? ( |
| 6359 |
<MultiSelect |
| 6360 |
value={formData.trip_category} |
| 6361 |
onChange={(values) => |
| 6362 |
handleFieldChange( |
| 6363 |
"trip_category", |
| 6364 |
values.map((v) => Number(v)), |
| 6365 |
) |
| 6366 |
} |
| 6367 |
options={(() => { |
| 6368 |
// Flatten categories with hierarchy indication |
| 6369 |
const flattenCategories = ( |
| 6370 |
cats: any[], |
| 6371 |
prefix = "", |
| 6372 |
): { value: number; label: string }[] => { |
| 6373 |
const result: { value: number; label: string }[] = |
| 6374 |
[]; |
| 6375 |
cats.forEach((cat) => { |
| 6376 |
result.push({ |
| 6377 |
value: cat.id, |
| 6378 |
label: prefix + cat.name, |
| 6379 |
}); |
| 6380 |
if ( |
| 6381 |
cat.subcategories && |
| 6382 |
cat.subcategories.length > 0 |
| 6383 |
) { |
| 6384 |
result.push( |
| 6385 |
...flattenCategories( |
| 6386 |
cat.subcategories, |
| 6387 |
prefix + "— ", |
| 6388 |
), |
| 6389 |
); |
| 6390 |
} |
| 6391 |
}); |
| 6392 |
return result; |
| 6393 |
}; |
| 6394 |
return flattenCategories(tripCategories); |
| 6395 |
})()} |
| 6396 |
placeholder={__("Select categories...", "yatra")} |
| 6397 |
searchPlaceholder={__("Search categories...", "yatra")} |
| 6398 |
/> |
| 6399 |
) : ( |
| 6400 |
<div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center"> |
| 6401 |
<p className="text-sm text-gray-500 dark:text-gray-400"> |
| 6402 |
{__("No categories available. ", "yatra")} |
| 6403 |
<a |
| 6404 |
href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories" |
| 6405 |
target="_blank" |
| 6406 |
rel="noopener noreferrer" |
| 6407 |
className="text-blue-600 hover:text-blue-700 underline" |
| 6408 |
> |
| 6409 |
{__("Create categories here", "yatra")} |
| 6410 |
</a> |
| 6411 |
</p> |
| 6412 |
</div> |
| 6413 |
)} |
| 6414 |
<HelpText |
| 6415 |
text={__( |
| 6416 |
"Select one or more categories for this trip", |
| 6417 |
"yatra", |
| 6418 |
)} |
| 6419 |
className="mt-2" |
| 6420 |
/> |
| 6421 |
</div> |
| 6422 |
|
| 6423 |
{/* Difficulty Level */} |
| 6424 |
<div> |
| 6425 |
<label |
| 6426 |
htmlFor="difficulty_level" |
| 6427 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 6428 |
> |
| 6429 |
{__("Difficulty Level", "yatra")} |
| 6430 |
</label> |
| 6431 |
<Select |
| 6432 |
id="difficulty_level" |
| 6433 |
value={formData.difficulty_level} |
| 6434 |
onChange={(e) => |
| 6435 |
handleFieldChange("difficulty_level", e.target.value) |
| 6436 |
} |
| 6437 |
disabled={isLoadingDifficultyLevels} |
| 6438 |
> |
| 6439 |
<option value=""> |
| 6440 |
{__("Select difficulty", "yatra")} |
| 6441 |
</option> |
| 6442 |
{isLoadingDifficultyLevels && ( |
| 6443 |
<option value="" disabled> |
| 6444 |
{__("Loading difficulty levels...", "yatra")} |
| 6445 |
</option> |
| 6446 |
)} |
| 6447 |
{!isLoadingDifficultyLevels && |
| 6448 |
difficultyLevels.length === 0 && ( |
| 6449 |
<option value="" disabled> |
| 6450 |
{__( |
| 6451 |
"No difficulty levels available - Click below to create", |
| 6452 |
"yatra", |
| 6453 |
)} |
| 6454 |
</option> |
| 6455 |
)} |
| 6456 |
{!isLoadingDifficultyLevels && |
| 6457 |
difficultyLevels.map((level) => ( |
| 6458 |
<option |
| 6459 |
key={`difficulty-${level.id}`} |
| 6460 |
value={level.id?.toString() || ""} |
| 6461 |
> |
| 6462 |
{level.name} |
| 6463 |
</option> |
| 6464 |
))} |
| 6465 |
</Select> |
| 6466 |
{!isLoadingDifficultyLevels && |
| 6467 |
difficultyLevels.length === 0 && ( |
| 6468 |
<div className="mt-3 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-md"> |
| 6469 |
<p className="text-sm text-amber-700 dark:text-amber-300"> |
| 6470 |
{__("No difficulty levels available. ", "yatra")} |
| 6471 |
<a |
| 6472 |
href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=difficulty-levels" |
| 6473 |
target="_blank" |
| 6474 |
rel="noopener noreferrer" |
| 6475 |
className="text-amber-800 hover:text-amber-900 underline font-medium" |
| 6476 |
> |
| 6477 |
{__("Create difficulty levels here", "yatra")} |
| 6478 |
</a> |
| 6479 |
</p> |
| 6480 |
</div> |
| 6481 |
)} |
| 6482 |
<HelpText |
| 6483 |
text={__( |
| 6484 |
"Physical difficulty level required for this trip", |
| 6485 |
"yatra", |
| 6486 |
)} |
| 6487 |
className="mt-2" |
| 6488 |
/> |
| 6489 |
</div> |
| 6490 |
|
| 6491 |
{/* Activity Types */} |
| 6492 |
<div> |
| 6493 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 6494 |
{__("Activity Types", "yatra")} |
| 6495 |
</label> |
| 6496 |
{activitiesData && activitiesData.length > 0 ? ( |
| 6497 |
<MultiSelect |
| 6498 |
value={formData.activity_types} |
| 6499 |
onChange={(values) => |
| 6500 |
handleFieldChange( |
| 6501 |
"activity_types", |
| 6502 |
values.map((v) => Number(v)), |
| 6503 |
) |
| 6504 |
} |
| 6505 |
options={activitiesData.map((activity: any) => ({ |
| 6506 |
value: activity.id, |
| 6507 |
label: activity.name, |
| 6508 |
}))} |
| 6509 |
placeholder={__("Select activities...", "yatra")} |
| 6510 |
searchPlaceholder={__("Search activities...", "yatra")} |
| 6511 |
/> |
| 6512 |
) : ( |
| 6513 |
<div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center"> |
| 6514 |
<p className="text-sm text-gray-500 dark:text-gray-400"> |
| 6515 |
{__("No activities available. ", "yatra")} |
| 6516 |
<a |
| 6517 |
href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=activities" |
| 6518 |
target="_blank" |
| 6519 |
rel="noopener noreferrer" |
| 6520 |
className="text-blue-600 hover:text-blue-700 underline" |
| 6521 |
> |
| 6522 |
{__("Create activities here", "yatra")} |
| 6523 |
</a> |
| 6524 |
</p> |
| 6525 |
</div> |
| 6526 |
)} |
| 6527 |
<HelpText |
| 6528 |
text={__( |
| 6529 |
"Select all activities included in this trip", |
| 6530 |
"yatra", |
| 6531 |
)} |
| 6532 |
className="mt-2" |
| 6533 |
/> |
| 6534 |
</div> |
| 6535 |
|
| 6536 |
{/* Featured Priority */} |
| 6537 |
<div> |
| 6538 |
<label |
| 6539 |
htmlFor="featured_priority" |
| 6540 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 6541 |
> |
| 6542 |
{__("Featured Priority", "yatra")} |
| 6543 |
</label> |
| 6544 |
<Select |
| 6545 |
id="featured_priority" |
| 6546 |
value={formData.featured_priority} |
| 6547 |
onChange={(e) => |
| 6548 |
handleFieldChange( |
| 6549 |
"featured_priority", |
| 6550 |
e.target.value as TripFormData["featured_priority"], |
| 6551 |
) |
| 6552 |
} |
| 6553 |
> |
| 6554 |
<option value="none">{__("None", "yatra")}</option> |
| 6555 |
<option value="featured"> |
| 6556 |
{__("Featured", "yatra")} |
| 6557 |
</option> |
| 6558 |
<option value="new">{__("New", "yatra")}</option> |
| 6559 |
<option value="limited"> |
| 6560 |
{__("Limited Time", "yatra")} |
| 6561 |
</option> |
| 6562 |
</Select> |
| 6563 |
<HelpText |
| 6564 |
text={__( |
| 6565 |
"Special designation for frontend display and promotion", |
| 6566 |
"yatra", |
| 6567 |
)} |
| 6568 |
className="mt-2" |
| 6569 |
/> |
| 6570 |
</div> |
| 6571 |
</div> |
| 6572 |
</div> |
| 6573 |
|
| 6574 |
{/* SECTION 2: Custom Attributes */} |
| 6575 |
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30"> |
| 6576 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> |
| 6577 |
<Settings className="w-4 h-4 text-blue-500" /> |
| 6578 |
{__("Custom Attributes", "yatra")} |
| 6579 |
</h3> |
| 6580 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4"> |
| 6581 |
{__( |
| 6582 |
"Add custom attributes for advanced filtering and search capabilities.", |
| 6583 |
"yatra", |
| 6584 |
)} |
| 6585 |
</p> |
| 6586 |
<TripAttributesSection |
| 6587 |
formData={formData} |
| 6588 |
handleFieldChange={handleFieldChange} |
| 6589 |
tripId={tripId || undefined} |
| 6590 |
isEditMode={isEditMode} |
| 6591 |
tripAttributesData={tripAttributesData} |
| 6592 |
tripAttributesReady={tripAttributesReady} |
| 6593 |
/> |
| 6594 |
</div> |
| 6595 |
</div> |
| 6596 |
</div> |
| 6597 |
); |
| 6598 |
|
| 6599 |
case "media": |
| 6600 |
return ( |
| 6601 |
<div className="space-y-6"> |
| 6602 |
<div className="flex flex-wrap items-center gap-2 mb-2"> |
| 6603 |
<Image className="w-5 h-5 text-gray-500" /> |
| 6604 |
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> |
| 6605 |
{__("Media & Content", "yatra")} |
| 6606 |
</h2> |
| 6607 |
<Badge |
| 6608 |
variant="outline" |
| 6609 |
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" |
| 6610 |
> |
| 6611 |
{__("Recommended", "yatra")} |
| 6612 |
</Badge> |
| 6613 |
</div> |
| 6614 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6"> |
| 6615 |
{__( |
| 6616 |
"Add visual content, videos, stories, and testimonials to showcase your trip", |
| 6617 |
"yatra", |
| 6618 |
)} |
| 6619 |
<span className="block text-xs text-gray-500 dark:text-gray-400 mt-1"> |
| 6620 |
{__( |
| 6621 |
"Optional, but adding rich media greatly increases engagement and conversions.", |
| 6622 |
"yatra", |
| 6623 |
)} |
| 6624 |
</span> |
| 6625 |
</p> |
| 6626 |
|
| 6627 |
<div className="space-y-6"> |
| 6628 |
{/* Photo Gallery */} |
| 6629 |
<Card> |
| 6630 |
<CardHeader> |
| 6631 |
<CardTitle>{__("Photo Gallery", "yatra")}</CardTitle> |
| 6632 |
<CardDescription> |
| 6633 |
{__( |
| 6634 |
"Upload images to showcase your trip. These will be displayed on the trip page.", |
| 6635 |
"yatra", |
| 6636 |
)} |
| 6637 |
</CardDescription> |
| 6638 |
</CardHeader> |
| 6639 |
<CardContent> |
| 6640 |
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4"> |
| 6641 |
{formData.gallery_images.map((image, index) => ( |
| 6642 |
<div key={index} className="relative group"> |
| 6643 |
<div className="aspect-video bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center overflow-hidden"> |
| 6644 |
<img |
| 6645 |
src={image.thumbnail_url || image.url} |
| 6646 |
alt={image.alt_text || `Gallery ${index + 1}`} |
| 6647 |
className="w-full h-full object-cover" |
| 6648 |
/> |
| 6649 |
</div> |
| 6650 |
{/* Reorder buttons */} |
| 6651 |
<div className="absolute top-2 left-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> |
| 6652 |
{index > 0 && ( |
| 6653 |
<button |
| 6654 |
type="button" |
| 6655 |
onClick={() => |
| 6656 |
handleGalleryReorder(index, index - 1) |
| 6657 |
} |
| 6658 |
className="bg-black/60 text-white rounded p-1 hover:bg-black/80" |
| 6659 |
title={__("Move left", "yatra")} |
| 6660 |
> |
| 6661 |
<ChevronLeft className="w-4 h-4" /> |
| 6662 |
</button> |
| 6663 |
)} |
| 6664 |
{index < formData.gallery_images.length - 1 && ( |
| 6665 |
<button |
| 6666 |
type="button" |
| 6667 |
onClick={() => |
| 6668 |
handleGalleryReorder(index, index + 1) |
| 6669 |
} |
| 6670 |
className="bg-black/60 text-white rounded p-1 hover:bg-black/80" |
| 6671 |
title={__("Move right", "yatra")} |
| 6672 |
> |
| 6673 |
<ChevronRight className="w-4 h-4" /> |
| 6674 |
</button> |
| 6675 |
)} |
| 6676 |
</div> |
| 6677 |
{/* Order badge */} |
| 6678 |
<span className="absolute bottom-2 left-2 bg-black/60 text-white text-xs px-1.5 py-0.5 rounded"> |
| 6679 |
{index + 1} |
| 6680 |
</span> |
| 6681 |
{/* Remove button */} |
| 6682 |
<button |
| 6683 |
type="button" |
| 6684 |
onClick={() => handleGalleryRemove(index)} |
| 6685 |
className="absolute top-2 right-2 bg-red-600 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity" |
| 6686 |
> |
| 6687 |
<X className="w-4 h-4" /> |
| 6688 |
</button> |
| 6689 |
</div> |
| 6690 |
))} |
| 6691 |
<button |
| 6692 |
type="button" |
| 6693 |
onClick={handleGalleryAdd} |
| 6694 |
className={`aspect-video border-2 border-dashed rounded-lg flex flex-col items-center justify-center transition-colors ${ |
| 6695 |
errors.gallery_images |
| 6696 |
? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20" |
| 6697 |
: "border-gray-300 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-400" |
| 6698 |
}`} |
| 6699 |
> |
| 6700 |
<Upload |
| 6701 |
className={`w-8 h-8 mb-2 ${errors.gallery_images ? "text-red-400" : "text-gray-400"}`} |
| 6702 |
/> |
| 6703 |
<span |
| 6704 |
className={`text-sm ${errors.gallery_images ? "text-red-600 dark:text-red-400" : "text-gray-600 dark:text-gray-400"}`} |
| 6705 |
> |
| 6706 |
{__("Add Image", "yatra")} |
| 6707 |
</span> |
| 6708 |
</button> |
| 6709 |
</div> |
| 6710 |
{errors.gallery_images && ( |
| 6711 |
<p className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 6712 |
<AlertCircle className="w-4 h-4" /> |
| 6713 |
{errors.gallery_images} |
| 6714 |
</p> |
| 6715 |
)} |
| 6716 |
</CardContent> |
| 6717 |
</Card> |
| 6718 |
{/* Video & Virtual Tour */} |
| 6719 |
<Card> |
| 6720 |
<CardHeader> |
| 6721 |
<CardTitle>{__("Video & Virtual Tour", "yatra")}</CardTitle> |
| 6722 |
<CardDescription> |
| 6723 |
{__( |
| 6724 |
"Add video content to showcase your trip visually", |
| 6725 |
"yatra", |
| 6726 |
)} |
| 6727 |
</CardDescription> |
| 6728 |
</CardHeader> |
| 6729 |
<CardContent className="space-y-4"> |
| 6730 |
<div> |
| 6731 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 6732 |
{__("Video URL", "yatra")} (YouTube/Vimeo) |
| 6733 |
</label> |
| 6734 |
<HelpText |
| 6735 |
text={__( |
| 6736 |
"Paste the full URL from YouTube or Vimeo. The video will be embedded on your trip page.", |
| 6737 |
"yatra", |
| 6738 |
)} |
| 6739 |
className="mb-2" |
| 6740 |
/> |
| 6741 |
<Input |
| 6742 |
type="url" |
| 6743 |
value={formData.video_url} |
| 6744 |
onChange={(e) => |
| 6745 |
handleFieldChange("video_url", e.target.value) |
| 6746 |
} |
| 6747 |
placeholder="https://www.youtube.com/watch?v=..." |
| 6748 |
className={`font-mono text-xs ${errors.video_url ? "border-red-500" : ""}`} |
| 6749 |
/> |
| 6750 |
{errors.video_url && ( |
| 6751 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 6752 |
<AlertCircle className="w-4 h-4" /> |
| 6753 |
{errors.video_url} |
| 6754 |
</p> |
| 6755 |
)} |
| 6756 |
</div> |
| 6757 |
<div> |
| 6758 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 6759 |
{__("360° Virtual Tour URL", "yatra")} |
| 6760 |
</label> |
| 6761 |
<HelpText |
| 6762 |
text={__( |
| 6763 |
"Add a link to an interactive 360° virtual tour if available", |
| 6764 |
"yatra", |
| 6765 |
)} |
| 6766 |
className="mb-2" |
| 6767 |
/> |
| 6768 |
<Input |
| 6769 |
type="url" |
| 6770 |
value={formData.virtual_tour_url} |
| 6771 |
onChange={(e) => |
| 6772 |
handleFieldChange("virtual_tour_url", e.target.value) |
| 6773 |
} |
| 6774 |
placeholder="https://..." |
| 6775 |
className={`font-mono text-xs ${errors.virtual_tour_url ? "border-red-500" : ""}`} |
| 6776 |
/> |
| 6777 |
{errors.virtual_tour_url && ( |
| 6778 |
<p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 6779 |
<AlertCircle className="w-4 h-4" /> |
| 6780 |
{errors.virtual_tour_url} |
| 6781 |
</p> |
| 6782 |
)} |
| 6783 |
</div> |
| 6784 |
</CardContent> |
| 6785 |
</Card> |
| 6786 |
|
| 6787 |
{/* Trip Story & What Makes Special */} |
| 6788 |
<Card> |
| 6789 |
<CardHeader> |
| 6790 |
<CardTitle> |
| 6791 |
{__("Trip Story & Special Features", "yatra")} |
| 6792 |
</CardTitle> |
| 6793 |
<CardDescription> |
| 6794 |
{__( |
| 6795 |
"Tell an engaging story and highlight what makes this trip unique", |
| 6796 |
"yatra", |
| 6797 |
)} |
| 6798 |
</CardDescription> |
| 6799 |
</CardHeader> |
| 6800 |
<CardContent className="space-y-4"> |
| 6801 |
<div> |
| 6802 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 6803 |
{__("What Makes This Trip Special", "yatra")} |
| 6804 |
</label> |
| 6805 |
<HelpText |
| 6806 |
text={__( |
| 6807 |
"Highlight the unique selling points and what sets this trip apart from others", |
| 6808 |
"yatra", |
| 6809 |
)} |
| 6810 |
className="mb-2" |
| 6811 |
/> |
| 6812 |
<textarea |
| 6813 |
value={formData.what_makes_special} |
| 6814 |
onChange={(e) => |
| 6815 |
handleFieldChange("what_makes_special", e.target.value) |
| 6816 |
} |
| 6817 |
placeholder={__( |
| 6818 |
"Describe what makes this trip unique and special...", |
| 6819 |
"yatra", |
| 6820 |
)} |
| 6821 |
rows={5} |
| 6822 |
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" |
| 6823 |
/> |
| 6824 |
</div> |
| 6825 |
<div> |
| 6826 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 6827 |
{__("Trip Story / Narrative", "yatra")} |
| 6828 |
</label> |
| 6829 |
<HelpText |
| 6830 |
text={__( |
| 6831 |
"Tell an engaging story about this trip. Use storytelling to connect with potential travelers emotionally", |
| 6832 |
"yatra", |
| 6833 |
)} |
| 6834 |
className="mb-2" |
| 6835 |
/> |
| 6836 |
<textarea |
| 6837 |
value={formData.trip_story} |
| 6838 |
onChange={(e) => |
| 6839 |
handleFieldChange("trip_story", e.target.value) |
| 6840 |
} |
| 6841 |
placeholder={__( |
| 6842 |
"Write an engaging narrative about this trip...", |
| 6843 |
"yatra", |
| 6844 |
)} |
| 6845 |
rows={8} |
| 6846 |
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" |
| 6847 |
/> |
| 6848 |
</div> |
| 6849 |
</CardContent> |
| 6850 |
</Card> |
| 6851 |
|
| 6852 |
{/* Testimonials - Select from Reviews */} |
| 6853 |
<TestimonialsSelector |
| 6854 |
tripId={tripId} |
| 6855 |
selectedReviewIds={formData.testimonial_review_ids} |
| 6856 |
onChange={(reviewIds) => |
| 6857 |
handleFieldChange("testimonial_review_ids", reviewIds) |
| 6858 |
} |
| 6859 |
/> |
| 6860 |
</div> |
| 6861 |
</div> |
| 6862 |
); |
| 6863 |
|
| 6864 |
case "downloads": |
| 6865 |
if (!showDownloadsUI) { |
| 6866 |
return ( |
| 6867 |
<div className="space-y-6"> |
| 6868 |
<Alert variant="info"> |
| 6869 |
{__("Downloads module is not enabled.", "yatra")} |
| 6870 |
</Alert> |
| 6871 |
</div> |
| 6872 |
); |
| 6873 |
} |
| 6874 |
|
| 6875 |
return ( |
| 6876 |
<div className="space-y-6"> |
| 6877 |
<div className="flex flex-wrap items-center gap-2 mb-2"> |
| 6878 |
<Download className="w-5 h-5 text-gray-500" /> |
| 6879 |
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> |
| 6880 |
{__("Downloads", "yatra")} |
| 6881 |
</h2> |
| 6882 |
</div> |
| 6883 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6"> |
| 6884 |
{__( |
| 6885 |
"Attach files to this trip and control who can access them.", |
| 6886 |
"yatra", |
| 6887 |
)} |
| 6888 |
</p> |
| 6889 |
|
| 6890 |
<Card> |
| 6891 |
<CardHeader> |
| 6892 |
<CardTitle>{__("Downloads", "yatra")}</CardTitle> |
| 6893 |
<CardDescription> |
| 6894 |
{__( |
| 6895 |
"Attach files to this trip and control who can access them.", |
| 6896 |
"yatra", |
| 6897 |
)} |
| 6898 |
</CardDescription> |
| 6899 |
</CardHeader> |
| 6900 |
<CardContent className="space-y-4"> |
| 6901 |
{(formData.downloadable_items || []).length > 0 ? ( |
| 6902 |
<div className="space-y-3"> |
| 6903 |
{(formData.downloadable_items || []).map((item, index) => ( |
| 6904 |
<div |
| 6905 |
key={index} |
| 6906 |
className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg space-y-3" |
| 6907 |
> |
| 6908 |
<div className="flex items-start justify-between gap-2"> |
| 6909 |
<div className="flex-1"> |
| 6910 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 6911 |
{__("Title", "yatra")} |
| 6912 |
</label> |
| 6913 |
<Input |
| 6914 |
value={item.title} |
| 6915 |
onChange={(e) => |
| 6916 |
handleDownloadableItemChange( |
| 6917 |
index, |
| 6918 |
"title", |
| 6919 |
e.target.value, |
| 6920 |
) |
| 6921 |
} |
| 6922 |
placeholder={__( |
| 6923 |
"E.g. Packing list, itinerary PDF, waiver form...", |
| 6924 |
"yatra", |
| 6925 |
)} |
| 6926 |
/> |
| 6927 |
</div> |
| 6928 |
|
| 6929 |
<div className="flex gap-1 pt-7"> |
| 6930 |
{index > 0 && ( |
| 6931 |
<Button |
| 6932 |
type="button" |
| 6933 |
variant="ghost" |
| 6934 |
size="sm" |
| 6935 |
onClick={() => |
| 6936 |
handleDownloadableItemMove(index, index - 1) |
| 6937 |
} |
| 6938 |
title={__("Move up", "yatra")} |
| 6939 |
> |
| 6940 |
<ChevronUp className="w-4 h-4" /> |
| 6941 |
</Button> |
| 6942 |
)} |
| 6943 |
{index < |
| 6944 |
(formData.downloadable_items || []).length - |
| 6945 |
1 && ( |
| 6946 |
<Button |
| 6947 |
type="button" |
| 6948 |
variant="ghost" |
| 6949 |
size="sm" |
| 6950 |
onClick={() => |
| 6951 |
handleDownloadableItemMove(index, index + 1) |
| 6952 |
} |
| 6953 |
title={__("Move down", "yatra")} |
| 6954 |
> |
| 6955 |
<ChevronDown className="w-4 h-4" /> |
| 6956 |
</Button> |
| 6957 |
)} |
| 6958 |
<Button |
| 6959 |
type="button" |
| 6960 |
variant="ghost" |
| 6961 |
size="sm" |
| 6962 |
onClick={() => |
| 6963 |
handleDownloadableItemRemove(index) |
| 6964 |
} |
| 6965 |
className="text-red-600 hover:text-red-700" |
| 6966 |
title={__("Remove", "yatra")} |
| 6967 |
> |
| 6968 |
<X className="w-4 h-4" /> |
| 6969 |
</Button> |
| 6970 |
</div> |
| 6971 |
</div> |
| 6972 |
|
| 6973 |
<div> |
| 6974 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 6975 |
{__("Description", "yatra")} ( |
| 6976 |
{__("Optional", "yatra")}) |
| 6977 |
</label> |
| 6978 |
<textarea |
| 6979 |
value={item.description} |
| 6980 |
onChange={(e) => |
| 6981 |
handleDownloadableItemChange( |
| 6982 |
index, |
| 6983 |
"description", |
| 6984 |
e.target.value, |
| 6985 |
) |
| 6986 |
} |
| 6987 |
rows={3} |
| 6988 |
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" |
| 6989 |
/> |
| 6990 |
</div> |
| 6991 |
|
| 6992 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 6993 |
<div> |
| 6994 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 6995 |
{__("Visibility", "yatra")} |
| 6996 |
</label> |
| 6997 |
<Select |
| 6998 |
value={item.visibility} |
| 6999 |
onChange={(e) => |
| 7000 |
handleDownloadableItemChange( |
| 7001 |
index, |
| 7002 |
"visibility", |
| 7003 |
e.target.value, |
| 7004 |
) |
| 7005 |
} |
| 7006 |
> |
| 7007 |
<option value="public"> |
| 7008 |
{__("Public", "yatra")} |
| 7009 |
</option> |
| 7010 |
<option value="logged_in"> |
| 7011 |
{__("Private (My Account only)", "yatra")} |
| 7012 |
</option> |
| 7013 |
<option value="booked_only"> |
| 7014 |
{__( |
| 7015 |
"Private (Booking confirmation only)", |
| 7016 |
"yatra", |
| 7017 |
)} |
| 7018 |
</option> |
| 7019 |
</Select> |
| 7020 |
</div> |
| 7021 |
<div> |
| 7022 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 7023 |
{__("File", "yatra")} |
| 7024 |
</label> |
| 7025 |
<div className="flex items-center gap-2"> |
| 7026 |
{item.attachment_url ? ( |
| 7027 |
<a |
| 7028 |
href={item.attachment_url} |
| 7029 |
target="_blank" |
| 7030 |
rel="noreferrer" |
| 7031 |
className="flex-shrink-0" |
| 7032 |
title={__("View file", "yatra")} |
| 7033 |
> |
| 7034 |
{/\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test( |
| 7035 |
item.attachment_url, |
| 7036 |
) ? ( |
| 7037 |
<img |
| 7038 |
src={item.attachment_url} |
| 7039 |
alt={ |
| 7040 |
item.attachment_title || |
| 7041 |
__("Selected file", "yatra") |
| 7042 |
} |
| 7043 |
className="w-9 h-9 rounded border border-gray-200 dark:border-gray-700 object-cover bg-white dark:bg-gray-900" |
| 7044 |
/> |
| 7045 |
) : ( |
| 7046 |
<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"> |
| 7047 |
<FileText className="w-4 h-4 text-gray-500 dark:text-gray-400" /> |
| 7048 |
</div> |
| 7049 |
)} |
| 7050 |
</a> |
| 7051 |
) : ( |
| 7052 |
<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"> |
| 7053 |
<FileText className="w-4 h-4 text-gray-400 dark:text-gray-600" /> |
| 7054 |
</div> |
| 7055 |
)} |
| 7056 |
|
| 7057 |
<div className="min-w-0 flex-1"> |
| 7058 |
<div className="flex items-center gap-2 min-w-0"> |
| 7059 |
<Button |
| 7060 |
type="button" |
| 7061 |
variant="outline" |
| 7062 |
onClick={() => |
| 7063 |
handleDownloadableItemSelectFile(index) |
| 7064 |
} |
| 7065 |
className="flex items-center gap-2 flex-shrink-0" |
| 7066 |
> |
| 7067 |
<Upload className="w-4 h-4" /> |
| 7068 |
{item.attachment_id |
| 7069 |
? __("Change File", "yatra") |
| 7070 |
: __("Select File", "yatra")} |
| 7071 |
</Button> |
| 7072 |
<span className="text-xs text-gray-600 dark:text-gray-400 truncate min-w-0"> |
| 7073 |
{item.attachment_title || |
| 7074 |
(item.attachment_id |
| 7075 |
? `#${item.attachment_id}` |
| 7076 |
: __("No file selected", "yatra"))} |
| 7077 |
</span> |
| 7078 |
{item.attachment_url && ( |
| 7079 |
<a |
| 7080 |
href={item.attachment_url} |
| 7081 |
target="_blank" |
| 7082 |
rel="noreferrer" |
| 7083 |
className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1 flex-shrink-0" |
| 7084 |
> |
| 7085 |
<Eye className="w-3 h-3" /> |
| 7086 |
{__("View", "yatra")} |
| 7087 |
</a> |
| 7088 |
)} |
| 7089 |
</div> |
| 7090 |
</div> |
| 7091 |
</div> |
| 7092 |
</div> |
| 7093 |
</div> |
| 7094 |
|
| 7095 |
<div className="flex items-start gap-3"> |
| 7096 |
<div className="flex items-center gap-2 pt-0.5"> |
| 7097 |
<input |
| 7098 |
type="checkbox" |
| 7099 |
checked={item.enabled} |
| 7100 |
onChange={(e) => |
| 7101 |
handleDownloadableItemChange( |
| 7102 |
index, |
| 7103 |
"enabled", |
| 7104 |
e.target.checked, |
| 7105 |
) |
| 7106 |
} |
| 7107 |
className="h-4 w-4" |
| 7108 |
/> |
| 7109 |
<span className="text-sm text-gray-700 dark:text-gray-300"> |
| 7110 |
{__("Enabled", "yatra")} |
| 7111 |
</span> |
| 7112 |
</div> |
| 7113 |
<div className="flex-1"> |
| 7114 |
<p className="text-xs text-gray-500 dark:text-gray-400 leading-relaxed"> |
| 7115 |
{__( |
| 7116 |
"Controls whether this file appears and is available for download on the trip page. When disabled, the file is completely hidden from users.", |
| 7117 |
"yatra", |
| 7118 |
)} |
| 7119 |
</p> |
| 7120 |
</div> |
| 7121 |
</div> |
| 7122 |
</div> |
| 7123 |
))} |
| 7124 |
</div> |
| 7125 |
) : ( |
| 7126 |
<div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center"> |
| 7127 |
<FileText className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" /> |
| 7128 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3"> |
| 7129 |
{__("No downloads added yet", "yatra")} |
| 7130 |
</p> |
| 7131 |
</div> |
| 7132 |
)} |
| 7133 |
|
| 7134 |
<Button |
| 7135 |
type="button" |
| 7136 |
variant="outline" |
| 7137 |
onClick={handleDownloadableItemAdd} |
| 7138 |
className="w-full" |
| 7139 |
> |
| 7140 |
<Plus className="w-4 h-4 mr-2" /> |
| 7141 |
{__("Add Download", "yatra")} |
| 7142 |
</Button> |
| 7143 |
</CardContent> |
| 7144 |
</Card> |
| 7145 |
</div> |
| 7146 |
); |
| 7147 |
|
| 7148 |
case "pricing": |
| 7149 |
return ( |
| 7150 |
<div className="space-y-4"> |
| 7151 |
<div className="flex flex-wrap items-center gap-2 mb-2"> |
| 7152 |
<DollarSign className="w-5 h-5 text-gray-500" /> |
| 7153 |
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> |
| 7154 |
{__("Pricing & Payment", "yatra")} |
| 7155 |
</h2> |
| 7156 |
<Badge |
| 7157 |
variant="outline" |
| 7158 |
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" |
| 7159 |
> |
| 7160 |
{__("Recommended", "yatra")} |
| 7161 |
</Badge> |
| 7162 |
</div> |
| 7163 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"> |
| 7164 |
{__("How much does it cost?", "yatra")} |
| 7165 |
<span className="block text-xs text-gray-500 dark:text-gray-400 mt-1"> |
| 7166 |
{__( |
| 7167 |
"Set pricing for different traveler types and payment options. Optional for drafts, but adding pricing now makes publishing easier.", |
| 7168 |
"yatra", |
| 7169 |
)} |
| 7170 |
</span> |
| 7171 |
</p> |
| 7172 |
|
| 7173 |
<div className="space-y-4"> |
| 7174 |
{/* Pricing Type Selection */} |
| 7175 |
<div> |
| 7176 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-3"> |
| 7177 |
{__("Pricing Type", "yatra")} |
| 7178 |
</label> |
| 7179 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 7180 |
<label |
| 7181 |
className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${ |
| 7182 |
formData.pricing_type === "regular" |
| 7183 |
? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600" |
| 7184 |
: "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" |
| 7185 |
}`} |
| 7186 |
> |
| 7187 |
<input |
| 7188 |
type="radio" |
| 7189 |
name="pricing_type" |
| 7190 |
value="regular" |
| 7191 |
checked={formData.pricing_type === "regular"} |
| 7192 |
onChange={(e) => |
| 7193 |
handleFieldChange("pricing_type", e.target.value) |
| 7194 |
} |
| 7195 |
className="sr-only" |
| 7196 |
/> |
| 7197 |
<div className="flex flex-1"> |
| 7198 |
<div className="flex flex-col"> |
| 7199 |
<span |
| 7200 |
className={`block text-sm font-semibold ${ |
| 7201 |
formData.pricing_type === "regular" |
| 7202 |
? "text-blue-900 dark:text-blue-300" |
| 7203 |
: "text-gray-900 dark:text-white" |
| 7204 |
}`} |
| 7205 |
> |
| 7206 |
{__("Regular Pricing", "yatra")} |
| 7207 |
</span> |
| 7208 |
<span |
| 7209 |
className={`mt-1 flex items-center text-sm ${ |
| 7210 |
formData.pricing_type === "regular" |
| 7211 |
? "text-blue-700 dark:text-blue-400" |
| 7212 |
: "text-gray-500 dark:text-gray-400" |
| 7213 |
}`} |
| 7214 |
> |
| 7215 |
{__("Set a single price for all travelers", "yatra")} |
| 7216 |
</span> |
| 7217 |
</div> |
| 7218 |
</div> |
| 7219 |
{formData.pricing_type === "regular" && ( |
| 7220 |
<div className="absolute top-4 right-4"> |
| 7221 |
<div className="h-4 w-4 rounded-full bg-blue-600"></div> |
| 7222 |
</div> |
| 7223 |
)} |
| 7224 |
</label> |
| 7225 |
|
| 7226 |
<label |
| 7227 |
className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${ |
| 7228 |
formData.pricing_type === "traveler_based" |
| 7229 |
? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600" |
| 7230 |
: "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" |
| 7231 |
}`} |
| 7232 |
> |
| 7233 |
<input |
| 7234 |
type="radio" |
| 7235 |
name="pricing_type" |
| 7236 |
value="traveler_based" |
| 7237 |
checked={formData.pricing_type === "traveler_based"} |
| 7238 |
onChange={(e) => |
| 7239 |
handleFieldChange("pricing_type", e.target.value) |
| 7240 |
} |
| 7241 |
className="sr-only" |
| 7242 |
/> |
| 7243 |
<div className="flex flex-1"> |
| 7244 |
<div className="flex flex-col"> |
| 7245 |
<span |
| 7246 |
className={`block text-sm font-semibold ${ |
| 7247 |
formData.pricing_type === "traveler_based" |
| 7248 |
? "text-blue-900 dark:text-blue-300" |
| 7249 |
: "text-gray-900 dark:text-white" |
| 7250 |
}`} |
| 7251 |
> |
| 7252 |
{__("Traveler-Based Pricing", "yatra")} |
| 7253 |
</span> |
| 7254 |
<span |
| 7255 |
className={`mt-1 flex items-center text-sm ${ |
| 7256 |
formData.pricing_type === "traveler_based" |
| 7257 |
? "text-blue-700 dark:text-blue-400" |
| 7258 |
: "text-gray-500 dark:text-gray-400" |
| 7259 |
}`} |
| 7260 |
> |
| 7261 |
{__( |
| 7262 |
"Set different prices for each traveler category", |
| 7263 |
"yatra", |
| 7264 |
)} |
| 7265 |
</span> |
| 7266 |
</div> |
| 7267 |
</div> |
| 7268 |
{formData.pricing_type === "traveler_based" && ( |
| 7269 |
<div className="absolute top-4 right-4"> |
| 7270 |
<div className="h-4 w-4 rounded-full bg-blue-600"></div> |
| 7271 |
</div> |
| 7272 |
)} |
| 7273 |
</label> |
| 7274 |
</div> |
| 7275 |
</div> |
| 7276 |
|
| 7277 |
{/* Regular Pricing */} |
| 7278 |
{formData.pricing_type === "regular" && ( |
| 7279 |
<div className="space-y-4"> |
| 7280 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 7281 |
<div> |
| 7282 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 7283 |
{__("Original Price", "yatra")} ( |
| 7284 |
{__("Optional", "yatra")}) |
| 7285 |
</label> |
| 7286 |
<div className="relative"> |
| 7287 |
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm"> |
| 7288 |
{getCurrencySymbol(globalCurrency)} |
| 7289 |
</span> |
| 7290 |
<Input |
| 7291 |
type="number" |
| 7292 |
step="0.01" |
| 7293 |
min="0" |
| 7294 |
value={formData.original_price} |
| 7295 |
onChange={(e) => |
| 7296 |
handleFieldChange("original_price", e.target.value) |
| 7297 |
} |
| 7298 |
placeholder="0.00" |
| 7299 |
className={`pl-7 ${errors.original_price ? "border-red-500" : ""}`} |
| 7300 |
/> |
| 7301 |
</div> |
| 7302 |
{errors.original_price && ( |
| 7303 |
<p className="mt-1 text-xs text-red-600"> |
| 7304 |
{errors.original_price} |
| 7305 |
</p> |
| 7306 |
)} |
| 7307 |
</div> |
| 7308 |
|
| 7309 |
<div> |
| 7310 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 7311 |
{__("Discounted Price", "yatra")} ( |
| 7312 |
{__("Optional", "yatra")}) |
| 7313 |
</label> |
| 7314 |
<div className="relative"> |
| 7315 |
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm"> |
| 7316 |
{getCurrencySymbol(globalCurrency)} |
| 7317 |
</span> |
| 7318 |
<Input |
| 7319 |
type="number" |
| 7320 |
step="0.01" |
| 7321 |
min="0" |
| 7322 |
value={formData.discounted_price} |
| 7323 |
onChange={(e) => |
| 7324 |
handleFieldChange( |
| 7325 |
"discounted_price", |
| 7326 |
e.target.value, |
| 7327 |
) |
| 7328 |
} |
| 7329 |
placeholder="0.00" |
| 7330 |
className={`pl-7 ${errors.discounted_price ? "border-red-500" : ""}`} |
| 7331 |
/> |
| 7332 |
</div> |
| 7333 |
{errors.discounted_price && ( |
| 7334 |
<p className="mt-1 text-xs text-red-600"> |
| 7335 |
{errors.discounted_price} |
| 7336 |
</p> |
| 7337 |
)} |
| 7338 |
</div> |
| 7339 |
</div> |
| 7340 |
</div> |
| 7341 |
)} |
| 7342 |
|
| 7343 |
{/* Traveler Category Pricing */} |
| 7344 |
{formData.pricing_type === "traveler_based" && ( |
| 7345 |
<div> |
| 7346 |
<div className="flex items-center justify-between mb-3"> |
| 7347 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400"> |
| 7348 |
{__("Traveler Category Pricing", "yatra")} |
| 7349 |
</label> |
| 7350 |
</div> |
| 7351 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3"> |
| 7352 |
{__( |
| 7353 |
"Add pricing for traveler categories. Categories are managed in Traveler Categories page.", |
| 7354 |
"yatra", |
| 7355 |
)} |
| 7356 |
</p> |
| 7357 |
{isLoadingCategories ? ( |
| 7358 |
<div className="flex items-center justify-center py-8"> |
| 7359 |
<Loader2 className="w-6 h-6 animate-spin text-gray-400" /> |
| 7360 |
</div> |
| 7361 |
) : activeCategories.length === 0 ? ( |
| 7362 |
<div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg text-center"> |
| 7363 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3"> |
| 7364 |
{__("No active traveler categories found.", "yatra")} |
| 7365 |
</p> |
| 7366 |
<Button |
| 7367 |
type="button" |
| 7368 |
variant="outline" |
| 7369 |
onClick={() => |
| 7370 |
(window.location.href = |
| 7371 |
"?page=yatra&subpage=traveler-categories&action=create") |
| 7372 |
} |
| 7373 |
className="flex items-center gap-2 mx-auto" |
| 7374 |
> |
| 7375 |
<Plus className="w-4 h-4" /> |
| 7376 |
{__("Create Category", "yatra")} |
| 7377 |
</Button> |
| 7378 |
</div> |
| 7379 |
) : ( |
| 7380 |
<div className="space-y-4"> |
| 7381 |
{/* Add Pricing Button */} |
| 7382 |
<div className="relative"> |
| 7383 |
<Button |
| 7384 |
type="button" |
| 7385 |
variant="outline" |
| 7386 |
onClick={() => |
| 7387 |
setShowCategorySelector(!showCategorySelector) |
| 7388 |
} |
| 7389 |
className="flex items-center gap-2" |
| 7390 |
disabled={ |
| 7391 |
activeCategories.filter( |
| 7392 |
(cat) => |
| 7393 |
!formData.price_types.some( |
| 7394 |
(pt) => |
| 7395 |
Number(pt.category_id) === Number(cat.id), |
| 7396 |
), |
| 7397 |
).length === 0 |
| 7398 |
} |
| 7399 |
> |
| 7400 |
<Plus className="w-4 h-4" /> |
| 7401 |
{__("Add Pricing", "yatra")} |
| 7402 |
</Button> |
| 7403 |
|
| 7404 |
{/* Category Selection Dropdown */} |
| 7405 |
{showCategorySelector && ( |
| 7406 |
<> |
| 7407 |
<div |
| 7408 |
className="fixed inset-0 z-10" |
| 7409 |
onClick={() => setShowCategorySelector(false)} |
| 7410 |
/> |
| 7411 |
<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"> |
| 7412 |
<div className="p-2"> |
| 7413 |
<div className="text-xs font-medium text-gray-700 dark:text-gray-300 px-3 py-2 mb-1"> |
| 7414 |
{__( |
| 7415 |
"Select a category to add pricing", |
| 7416 |
"yatra", |
| 7417 |
)} |
| 7418 |
</div> |
| 7419 |
{activeCategories.filter( |
| 7420 |
(cat) => |
| 7421 |
!formData.price_types.some( |
| 7422 |
(pt) => |
| 7423 |
Number(pt.category_id) === |
| 7424 |
Number(cat.id), |
| 7425 |
), |
| 7426 |
).length === 0 ? ( |
| 7427 |
<div className="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center"> |
| 7428 |
{__( |
| 7429 |
"All categories have pricing added", |
| 7430 |
"yatra", |
| 7431 |
)} |
| 7432 |
</div> |
| 7433 |
) : ( |
| 7434 |
<div className="space-y-1"> |
| 7435 |
{activeCategories |
| 7436 |
.filter( |
| 7437 |
(cat) => |
| 7438 |
!formData.price_types.some( |
| 7439 |
(pt) => |
| 7440 |
Number(pt.category_id) === |
| 7441 |
Number(cat.id), |
| 7442 |
), |
| 7443 |
) |
| 7444 |
.map((category) => { |
| 7445 |
const ageRange = |
| 7446 |
category.age_min !== undefined || |
| 7447 |
category.age_max !== undefined |
| 7448 |
? category.age_min !== undefined && |
| 7449 |
category.age_max !== undefined |
| 7450 |
? `${category.age_min}-${category.age_max} ${__("years", "yatra")}` |
| 7451 |
: category.age_min !== undefined |
| 7452 |
? `${category.age_min}+ ${__("years", "yatra")}` |
| 7453 |
: category.age_max !== undefined |
| 7454 |
? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}` |
| 7455 |
: "" |
| 7456 |
: null; |
| 7457 |
|
| 7458 |
const pricingMode = |
| 7459 |
category.pricing_mode || "per_person"; |
| 7460 |
const hasMin = |
| 7461 |
category.min_pax !== null && |
| 7462 |
category.min_pax !== undefined; |
| 7463 |
const hasMax = |
| 7464 |
category.max_pax !== null && |
| 7465 |
category.max_pax !== undefined; |
| 7466 |
|
| 7467 |
let pricingLabel = ""; |
| 7468 |
if (pricingMode === "per_group") { |
| 7469 |
if (hasMin && hasMax) { |
| 7470 |
pricingLabel = `${__("Per group", "yatra")} (${category.min_pax}-${category.max_pax})`; |
| 7471 |
} else if (hasMin) { |
| 7472 |
pricingLabel = `${__("Per group", "yatra")} (${__("From", "yatra")} ${category.min_pax})`; |
| 7473 |
} else if (hasMax) { |
| 7474 |
pricingLabel = `${__("Per group", "yatra")} (${__("Up to", "yatra")} ${category.max_pax})`; |
| 7475 |
} else { |
| 7476 |
pricingLabel = __( |
| 7477 |
"Per group", |
| 7478 |
"yatra", |
| 7479 |
); |
| 7480 |
} |
| 7481 |
} else { |
| 7482 |
pricingLabel = __( |
| 7483 |
"Per person", |
| 7484 |
"yatra", |
| 7485 |
); |
| 7486 |
} |
| 7487 |
|
| 7488 |
return ( |
| 7489 |
<button |
| 7490 |
key={category.id} |
| 7491 |
type="button" |
| 7492 |
onClick={() => { |
| 7493 |
handlePriceTypeAdd(category.id); |
| 7494 |
setShowCategorySelector(false); |
| 7495 |
}} |
| 7496 |
className="w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" |
| 7497 |
> |
| 7498 |
<div className="font-medium text-sm text-gray-900 dark:text-white"> |
| 7499 |
{category.label} |
| 7500 |
{ageRange && ( |
| 7501 |
<span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400"> |
| 7502 |
({ageRange}) |
| 7503 |
</span> |
| 7504 |
)} |
| 7505 |
</div> |
| 7506 |
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 7507 |
{category.description} |
| 7508 |
{pricingLabel && ( |
| 7509 |
<span className="ml-1 text-[11px] font-medium text-gray-600 dark:text-gray-300"> |
| 7510 |
• {pricingLabel} |
| 7511 |
</span> |
| 7512 |
)} |
| 7513 |
</div> |
| 7514 |
</button> |
| 7515 |
); |
| 7516 |
})} |
| 7517 |
</div> |
| 7518 |
)} |
| 7519 |
</div> |
| 7520 |
</div> |
| 7521 |
</> |
| 7522 |
)} |
| 7523 |
</div> |
| 7524 |
|
| 7525 |
{/* Added Pricing List */} |
| 7526 |
{formData.price_types.length > 0 && ( |
| 7527 |
<div className="space-y-3"> |
| 7528 |
{formData.price_types.map((priceType, index) => { |
| 7529 |
// Use Number() to ensure consistent comparison (handles string/number mismatch) |
| 7530 |
const category = activeCategories.find( |
| 7531 |
(cat) => |
| 7532 |
Number(cat.id) === |
| 7533 |
Number(priceType.category_id), |
| 7534 |
); |
| 7535 |
if (!category) { |
| 7536 |
return null; |
| 7537 |
} |
| 7538 |
|
| 7539 |
return ( |
| 7540 |
<div |
| 7541 |
key={priceType.category_id} |
| 7542 |
className="p-4 border border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/20 rounded-lg" |
| 7543 |
> |
| 7544 |
<div className="flex items-start justify-between mb-3"> |
| 7545 |
<div className="flex-1"> |
| 7546 |
<div className="flex items-center gap-2 mb-1"> |
| 7547 |
<h4 className="text-sm font-semibold text-gray-900 dark:text-white"> |
| 7548 |
{category.label} |
| 7549 |
{(category.age_min !== undefined || |
| 7550 |
category.age_max !== undefined) && ( |
| 7551 |
<span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400"> |
| 7552 |
( |
| 7553 |
{category.age_min !== undefined && |
| 7554 |
category.age_max !== undefined |
| 7555 |
? `${category.age_min}-${category.age_max} ${__("years", "yatra")}` |
| 7556 |
: category.age_min !== undefined |
| 7557 |
? `${category.age_min}+ ${__("years", "yatra")}` |
| 7558 |
: category.age_max !== undefined |
| 7559 |
? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}` |
| 7560 |
: ""} |
| 7561 |
) |
| 7562 |
</span> |
| 7563 |
)} |
| 7564 |
|
| 7565 |
{/* Pricing mode & group size */} |
| 7566 |
<span className="ml-2 inline-flex items-center gap-1 text-[11px] font-medium text-gray-600 dark:text-gray-300"> |
| 7567 |
{(() => { |
| 7568 |
const pricingMode = |
| 7569 |
category.pricing_mode || |
| 7570 |
"per_person"; |
| 7571 |
const hasMin = |
| 7572 |
category.min_pax !== null && |
| 7573 |
category.min_pax !== undefined; |
| 7574 |
const hasMax = |
| 7575 |
category.max_pax !== null && |
| 7576 |
category.max_pax !== undefined; |
| 7577 |
|
| 7578 |
if (pricingMode === "per_group") { |
| 7579 |
if (hasMin && hasMax) { |
| 7580 |
return `${__("Per group", "yatra")} (${category.min_pax}-${category.max_pax})`; |
| 7581 |
} |
| 7582 |
if (hasMin) { |
| 7583 |
return `${__("Per group", "yatra")} (${__("From", "yatra")} ${category.min_pax})`; |
| 7584 |
} |
| 7585 |
if (hasMax) { |
| 7586 |
return `${__("Per group", "yatra")} (${__("Up to", "yatra")} ${category.max_pax})`; |
| 7587 |
} |
| 7588 |
return __("Per group", "yatra"); |
| 7589 |
} |
| 7590 |
|
| 7591 |
return __("Per person", "yatra"); |
| 7592 |
})()} |
| 7593 |
</span> |
| 7594 |
</h4> |
| 7595 |
</div> |
| 7596 |
<p className="text-xs text-gray-600 dark:text-gray-400"> |
| 7597 |
{category.description} |
| 7598 |
</p> |
| 7599 |
</div> |
| 7600 |
<button |
| 7601 |
type="button" |
| 7602 |
onClick={() => |
| 7603 |
handlePriceTypeRemove(category.id) |
| 7604 |
} |
| 7605 |
className="p-1 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors" |
| 7606 |
title={__("Remove Pricing", "yatra")} |
| 7607 |
> |
| 7608 |
<X className="w-4 h-4" /> |
| 7609 |
</button> |
| 7610 |
</div> |
| 7611 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> |
| 7612 |
<div> |
| 7613 |
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5"> |
| 7614 |
{__("Original Price", "yatra")} ( |
| 7615 |
{__("Optional", "yatra")}) |
| 7616 |
</label> |
| 7617 |
<div className="relative"> |
| 7618 |
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm"> |
| 7619 |
{getCurrencySymbol(globalCurrency)} |
| 7620 |
</span> |
| 7621 |
<Input |
| 7622 |
type="number" |
| 7623 |
step="0.01" |
| 7624 |
min="0" |
| 7625 |
value={priceType.original_price} |
| 7626 |
onChange={(e) => |
| 7627 |
handlePriceTypeChange( |
| 7628 |
category.id, |
| 7629 |
"original_price", |
| 7630 |
e.target.value, |
| 7631 |
) |
| 7632 |
} |
| 7633 |
placeholder="0.00" |
| 7634 |
className={`pl-7 ${errors[`price_type_${index}_original`] ? "border-red-500" : ""}`} |
| 7635 |
/> |
| 7636 |
</div> |
| 7637 |
{errors[`price_type_${index}_original`] && ( |
| 7638 |
<p className="mt-1 text-xs text-red-600 dark:text-red-400"> |
| 7639 |
{errors[`price_type_${index}_original`]} |
| 7640 |
</p> |
| 7641 |
)} |
| 7642 |
</div> |
| 7643 |
<div> |
| 7644 |
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5"> |
| 7645 |
{__("Discounted Price", "yatra")} ( |
| 7646 |
{__("Optional", "yatra")}) |
| 7647 |
</label> |
| 7648 |
<div className="relative"> |
| 7649 |
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm"> |
| 7650 |
{getCurrencySymbol(globalCurrency)} |
| 7651 |
</span> |
| 7652 |
<Input |
| 7653 |
type="number" |
| 7654 |
step="0.01" |
| 7655 |
min="0" |
| 7656 |
value={priceType.discounted_price} |
| 7657 |
onChange={(e) => |
| 7658 |
handlePriceTypeChange( |
| 7659 |
category.id, |
| 7660 |
"discounted_price", |
| 7661 |
e.target.value, |
| 7662 |
) |
| 7663 |
} |
| 7664 |
placeholder="0.00" |
| 7665 |
className={`pl-7 ${errors[`price_type_${index}_discounted`] ? "border-red-500" : ""}`} |
| 7666 |
/> |
| 7667 |
</div> |
| 7668 |
{errors[ |
| 7669 |
`price_type_${index}_discounted` |
| 7670 |
] && ( |
| 7671 |
<p className="mt-1 text-xs text-red-600 dark:text-red-400"> |
| 7672 |
{ |
| 7673 |
errors[ |
| 7674 |
`price_type_${index}_discounted` |
| 7675 |
] |
| 7676 |
} |
| 7677 |
</p> |
| 7678 |
)} |
| 7679 |
</div> |
| 7680 |
</div> |
| 7681 |
|
| 7682 |
<div className="mt-3 flex items-center justify-between"> |
| 7683 |
<label className="inline-flex items-center gap-2 text-xs font-medium text-gray-700 dark:text-gray-300"> |
| 7684 |
<input |
| 7685 |
type="checkbox" |
| 7686 |
checked={Boolean(priceType.is_default)} |
| 7687 |
onChange={(e) => |
| 7688 |
handlePriceTypeDefaultChange( |
| 7689 |
category.id, |
| 7690 |
e.target.checked, |
| 7691 |
) |
| 7692 |
} |
| 7693 |
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600" |
| 7694 |
/> |
| 7695 |
{__( |
| 7696 |
"Default price (used on page load)", |
| 7697 |
"yatra", |
| 7698 |
)} |
| 7699 |
</label> |
| 7700 |
<span className="text-[11px] text-gray-500 dark:text-gray-400"> |
| 7701 |
{__("Traveler-based pricing only", "yatra")} |
| 7702 |
</span> |
| 7703 |
</div> |
| 7704 |
</div> |
| 7705 |
); |
| 7706 |
})} |
| 7707 |
</div> |
| 7708 |
)} |
| 7709 |
|
| 7710 |
{/* Empty State */} |
| 7711 |
{formData.price_types.length === 0 && ( |
| 7712 |
<div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center"> |
| 7713 |
<Tag className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" /> |
| 7714 |
<p className="text-sm text-gray-600 dark:text-gray-400"> |
| 7715 |
{__( |
| 7716 |
"No pricing added yet. Select a category above to add pricing.", |
| 7717 |
"yatra", |
| 7718 |
)} |
| 7719 |
</p> |
| 7720 |
</div> |
| 7721 |
)} |
| 7722 |
</div> |
| 7723 |
)} |
| 7724 |
{errors.price_types && ( |
| 7725 |
<p className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> |
| 7726 |
<AlertCircle className="w-4 h-4" /> |
| 7727 |
{errors.price_types} |
| 7728 |
</p> |
| 7729 |
)} |
| 7730 |
</div> |
| 7731 |
)} |
| 7732 |
|
| 7733 |
{/* |
| 7734 |
Deposit & Payment Terms — Pro feature (FlexiblePayments module). |
| 7735 |
The DB columns live in the free TripsTable schema so existing data |
| 7736 |
survives toggling Pro on/off. Editing is gated on |
| 7737 |
window.yatraAdmin.flexiblePaymentsEnabled (set true by Pro's |
| 7738 |
FlexiblePaymentsModule::addAdminData() on yatra_admin_localized_data). |
| 7739 |
|
| 7740 |
We deliberately render the section in all three states (free / Pro |
| 7741 |
installed-but-module-off / Pro+module-on) so the feature is |
| 7742 |
discoverable in the free plugin. When unlocked → editable. When |
| 7743 |
locked → fields shown disabled inside a <fieldset disabled>, with a |
| 7744 |
PRO badge on the heading and a context-aware info banner above: |
| 7745 |
- free: "Upgrade to Pro" link |
| 7746 |
- Pro installed but module disabled: "Activate Module" link |
| 7747 |
*/} |
| 7748 |
{(() => { |
| 7749 |
const flexiblePaymentsEnabled = Boolean( |
| 7750 |
(window as any)?.yatraAdmin?.flexiblePaymentsEnabled, |
| 7751 |
); |
| 7752 |
const isLocked = !flexiblePaymentsEnabled; |
| 7753 |
const siteUrl = |
| 7754 |
(window as any)?.yatraAdmin?.siteUrl || |
| 7755 |
"/wp-admin/admin.php?page=yatra"; |
| 7756 |
const modulesPageUrl = `${siteUrl}/wp-admin/admin.php?page=yatra&subpage=modules`; |
| 7757 |
const pricingUrl = "https://mantrabrain.com/yatra-pricing/"; |
| 7758 |
|
| 7759 |
return ( |
| 7760 |
<div className="border-t border-gray-200 dark:border-gray-700 pt-4"> |
| 7761 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center"> |
| 7762 |
{__("Deposit & Payment Terms", "yatra")} |
| 7763 |
{isLocked && <ProBadge isProActive={isPro} />} |
| 7764 |
</h3> |
| 7765 |
|
| 7766 |
{isLocked && ( |
| 7767 |
<div className="mb-3 p-3 bg-gradient-to-r from-purple-50 to-blue-50 dark:from-purple-900/20 dark:to-blue-900/20 border border-purple-200 dark:border-purple-800 rounded-md text-xs text-purple-900 dark:text-purple-100 flex items-start gap-2"> |
| 7768 |
<svg |
| 7769 |
className="w-4 h-4 mt-0.5 text-purple-500 flex-shrink-0" |
| 7770 |
fill="none" |
| 7771 |
stroke="currentColor" |
| 7772 |
viewBox="0 0 24 24" |
| 7773 |
aria-hidden="true" |
| 7774 |
> |
| 7775 |
<path |
| 7776 |
strokeLinecap="round" |
| 7777 |
strokeLinejoin="round" |
| 7778 |
strokeWidth={2} |
| 7779 |
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" |
| 7780 |
/> |
| 7781 |
</svg> |
| 7782 |
<div className="flex-1"> |
| 7783 |
{isPro ? ( |
| 7784 |
<> |
| 7785 |
{__( |
| 7786 |
"Per-trip deposits need the Flexible Payments module.", |
| 7787 |
"yatra", |
| 7788 |
)}{" "} |
| 7789 |
<a |
| 7790 |
href={modulesPageUrl} |
| 7791 |
target="_blank" |
| 7792 |
rel="noopener noreferrer" |
| 7793 |
className="underline font-medium" |
| 7794 |
> |
| 7795 |
{__("Activate module", "yatra")} |
| 7796 |
</a> |
| 7797 |
</> |
| 7798 |
) : ( |
| 7799 |
<> |
| 7800 |
{__( |
| 7801 |
"Per-trip deposit amount, percentage and custom payment terms are a Yatra Pro feature.", |
| 7802 |
"yatra", |
| 7803 |
)}{" "} |
| 7804 |
<a |
| 7805 |
href={pricingUrl} |
| 7806 |
target="_blank" |
| 7807 |
rel="noopener noreferrer" |
| 7808 |
className="underline font-medium" |
| 7809 |
> |
| 7810 |
{__("Upgrade to Pro", "yatra")} |
| 7811 |
</a> |
| 7812 |
</> |
| 7813 |
)} |
| 7814 |
</div> |
| 7815 |
</div> |
| 7816 |
)} |
| 7817 |
|
| 7818 |
{/* |
| 7819 |
<fieldset disabled> is the HTML-standard way to make every |
| 7820 |
form control inside it non-interactive in one go — works |
| 7821 |
for our <Input> and <textarea> without per-field disabled props. |
| 7822 |
*/} |
| 7823 |
<fieldset |
| 7824 |
disabled={isLocked} |
| 7825 |
className={ |
| 7826 |
isLocked |
| 7827 |
? "opacity-60 cursor-not-allowed select-none" |
| 7828 |
: "" |
| 7829 |
} |
| 7830 |
aria-label={ |
| 7831 |
isLocked |
| 7832 |
? __( |
| 7833 |
"Deposit & Payment Terms (Pro feature, disabled)", |
| 7834 |
"yatra", |
| 7835 |
) |
| 7836 |
: undefined |
| 7837 |
} |
| 7838 |
> |
| 7839 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 7840 |
<div> |
| 7841 |
<label |
| 7842 |
htmlFor="deposit_amount" |
| 7843 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 7844 |
> |
| 7845 |
{__("Deposit Amount", "yatra")} |
| 7846 |
</label> |
| 7847 |
<div className="relative"> |
| 7848 |
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500"> |
| 7849 |
{getCurrencySymbol(globalCurrency)} |
| 7850 |
</span> |
| 7851 |
<Input |
| 7852 |
id="deposit_amount" |
| 7853 |
type="number" |
| 7854 |
step="0.01" |
| 7855 |
min="0" |
| 7856 |
value={formData.deposit_amount} |
| 7857 |
onChange={(e) => |
| 7858 |
handleFieldChange( |
| 7859 |
"deposit_amount", |
| 7860 |
e.target.value, |
| 7861 |
) |
| 7862 |
} |
| 7863 |
placeholder="0.00" |
| 7864 |
className="pl-7" |
| 7865 |
/> |
| 7866 |
</div> |
| 7867 |
</div> |
| 7868 |
<div> |
| 7869 |
<label |
| 7870 |
htmlFor="deposit_percentage" |
| 7871 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 7872 |
> |
| 7873 |
{__("Deposit Percentage", "yatra")} |
| 7874 |
</label> |
| 7875 |
<div className="relative"> |
| 7876 |
<Input |
| 7877 |
id="deposit_percentage" |
| 7878 |
type="number" |
| 7879 |
step="0.01" |
| 7880 |
min="0" |
| 7881 |
max="100" |
| 7882 |
value={formData.deposit_percentage} |
| 7883 |
onChange={(e) => |
| 7884 |
handleFieldChange( |
| 7885 |
"deposit_percentage", |
| 7886 |
e.target.value, |
| 7887 |
) |
| 7888 |
} |
| 7889 |
placeholder="0" |
| 7890 |
className="pr-7" |
| 7891 |
/> |
| 7892 |
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500"> |
| 7893 |
% |
| 7894 |
</span> |
| 7895 |
</div> |
| 7896 |
</div> |
| 7897 |
</div> |
| 7898 |
<div className="mt-4"> |
| 7899 |
<label |
| 7900 |
htmlFor="payment_terms" |
| 7901 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 7902 |
> |
| 7903 |
{__("Payment Terms", "yatra")} |
| 7904 |
</label> |
| 7905 |
<textarea |
| 7906 |
id="payment_terms" |
| 7907 |
value={formData.payment_terms} |
| 7908 |
onChange={(e) => |
| 7909 |
handleFieldChange("payment_terms", e.target.value) |
| 7910 |
} |
| 7911 |
placeholder={__( |
| 7912 |
"e.g., 50% deposit required at booking, balance due 30 days before departure", |
| 7913 |
"yatra", |
| 7914 |
)} |
| 7915 |
rows={2} |
| 7916 |
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" |
| 7917 |
/> |
| 7918 |
</div> |
| 7919 |
</fieldset> |
| 7920 |
</div> |
| 7921 |
); |
| 7922 |
})()} |
| 7923 |
|
| 7924 |
{/* Group Pricing */} |
| 7925 |
{/* Removed */} |
| 7926 |
</div> |
| 7927 |
</div> |
| 7928 |
); |
| 7929 |
|
| 7930 |
case "attributes": |
| 7931 |
// Redirect to categorization section (attributes now merged there) |
| 7932 |
setCurrentSection("categorization"); |
| 7933 |
return null; |
| 7934 |
|
| 7935 |
case "itinerary": |
| 7936 |
return ( |
| 7937 |
<div className="space-y-6"> |
| 7938 |
{/* Header */} |
| 7939 |
<div className="flex flex-wrap items-center gap-2 mb-2"> |
| 7940 |
<BookOpen className="w-5 h-5 text-gray-500" /> |
| 7941 |
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> |
| 7942 |
{__("Trip Details", "yatra")} |
| 7943 |
</h2> |
| 7944 |
<Badge |
| 7945 |
variant="outline" |
| 7946 |
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" |
| 7947 |
> |
| 7948 |
{__("Recommended", "yatra")} |
| 7949 |
</Badge> |
| 7950 |
</div> |
| 7951 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"> |
| 7952 |
{__("What will travelers experience?", "yatra")} |
| 7953 |
<span className="block text-xs text-gray-500 dark:text-gray-400 mt-1"> |
| 7954 |
{__( |
| 7955 |
"Build your day-by-day itinerary and list what's included/excluded to set clear expectations.", |
| 7956 |
"yatra", |
| 7957 |
)} |
| 7958 |
</span> |
| 7959 |
</p> |
| 7960 |
|
| 7961 |
{/* Sub-tabs */} |
| 7962 |
<div className="border-b border-gray-200 dark:border-gray-700"> |
| 7963 |
<nav className="flex gap-6" aria-label="Trip Details Tabs"> |
| 7964 |
<button |
| 7965 |
onClick={() => setTripDetailsTab("itinerary")} |
| 7966 |
className={`pb-3 px-1 border-b-2 font-medium text-sm transition-colors ${ |
| 7967 |
tripDetailsTab === "itinerary" |
| 7968 |
? "border-blue-500 text-blue-600 dark:text-blue-400" |
| 7969 |
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300" |
| 7970 |
}`} |
| 7971 |
> |
| 7972 |
<div className="flex items-center gap-2"> |
| 7973 |
<Calendar className="w-4 h-4" /> |
| 7974 |
{__("Itinerary", "yatra")} |
| 7975 |
</div> |
| 7976 |
</button> |
| 7977 |
<button |
| 7978 |
onClick={() => setTripDetailsTab("included")} |
| 7979 |
className={`pb-3 px-1 border-b-2 font-medium text-sm transition-colors ${ |
| 7980 |
tripDetailsTab === "included" |
| 7981 |
? "border-blue-500 text-blue-600 dark:text-blue-400" |
| 7982 |
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300" |
| 7983 |
}`} |
| 7984 |
> |
| 7985 |
<div className="flex items-center gap-2"> |
| 7986 |
<CheckSquare className="w-4 h-4" /> |
| 7987 |
{__("Included/Excluded", "yatra")} |
| 7988 |
</div> |
| 7989 |
</button> |
| 7990 |
</nav> |
| 7991 |
</div> |
| 7992 |
|
| 7993 |
{/* Tab Content */} |
| 7994 |
<div className="mt-6"> |
| 7995 |
{tripDetailsTab === "itinerary" && ( |
| 7996 |
<> |
| 7997 |
{isAiEligible() && isAiModuleEnabled() && ( |
| 7998 |
<div className="mb-4 flex items-center justify-between rounded-md border border-blue-200 bg-blue-50/40 px-3 py-2 dark:border-blue-500/40 dark:bg-blue-900/20"> |
| 7999 |
<div className="text-xs text-blue-900 dark:text-blue-200"> |
| 8000 |
<Sparkles className="mr-1 inline h-3.5 w-3.5" /> |
| 8001 |
{isEditMode |
| 8002 |
? __( |
| 8003 |
"Let AI draft a day-by-day plan with real activity blocks — you can edit each day after.", |
| 8004 |
"yatra", |
| 8005 |
) |
| 8006 |
: __( |
| 8007 |
"Save the trip first to enable AI itinerary generation — the agent needs a trip ID to attach days to.", |
| 8008 |
"yatra", |
| 8009 |
)} |
| 8010 |
</div> |
| 8011 |
<Button |
| 8012 |
type="button" |
| 8013 |
size="sm" |
| 8014 |
variant="outline" |
| 8015 |
className="border-blue-300 text-blue-700 hover:bg-blue-100 dark:border-blue-500/60 dark:text-blue-200" |
| 8016 |
disabled={!isEditMode || !tripId} |
| 8017 |
onClick={() => { |
| 8018 |
// Same modal the Itinerary page uses, so all |
| 8019 |
// three entry points share one agent + one |
| 8020 |
// persistence path. |
| 8021 |
if (isEditMode && tripId) { |
| 8022 |
setItineraryBuildOpen(true); |
| 8023 |
} |
| 8024 |
}} |
| 8025 |
> |
| 8026 |
<Sparkles className="mr-1 h-3.5 w-3.5" /> |
| 8027 |
{__("Build with AI", "yatra")} |
| 8028 |
</Button> |
| 8029 |
</div> |
| 8030 |
)} |
| 8031 |
<ItinerarySection |
| 8032 |
formData={formData as any} |
| 8033 |
isEditMode={isEditMode} |
| 8034 |
tripId={tripId} |
| 8035 |
/> |
| 8036 |
</> |
| 8037 |
)} |
| 8038 |
{tripDetailsTab === "included" && ( |
| 8039 |
<IncludedSection |
| 8040 |
formData={formData} |
| 8041 |
errors={errors} |
| 8042 |
handleFieldChange={handleFieldChange as any} |
| 8043 |
/> |
| 8044 |
)} |
| 8045 |
</div> |
| 8046 |
</div> |
| 8047 |
); |
| 8048 |
|
| 8049 |
case "included": |
| 8050 |
// Redirect to itinerary with included tab selected |
| 8051 |
setCurrentSection("itinerary"); |
| 8052 |
setTripDetailsTab("included"); |
| 8053 |
return null; |
| 8054 |
|
| 8055 |
case "faqs": |
| 8056 |
// Redirect to seo section (FAQs now merged there) |
| 8057 |
setCurrentSection("seo"); |
| 8058 |
return null; |
| 8059 |
|
| 8060 |
// Removed: case 'frontend-tabs' - now part of 'advanced' case |
| 8061 |
|
| 8062 |
case "seo": |
| 8063 |
return ( |
| 8064 |
<div className="space-y-6"> |
| 8065 |
<div className="flex flex-wrap items-center gap-2 mb-2"> |
| 8066 |
<Search className="w-5 h-5 text-gray-500" /> |
| 8067 |
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> |
| 8068 |
{__("SEO & Marketing", "yatra")} |
| 8069 |
</h2> |
| 8070 |
<Badge |
| 8071 |
variant="outline" |
| 8072 |
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" |
| 8073 |
> |
| 8074 |
{__("Recommended", "yatra")} |
| 8075 |
</Badge> |
| 8076 |
</div> |
| 8077 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"> |
| 8078 |
{__("How will people find & trust this trip?", "yatra")} |
| 8079 |
<span className="block text-xs text-gray-500 dark:text-gray-400 mt-1"> |
| 8080 |
{__( |
| 8081 |
"Optimize for search engines and answer common questions to build trust and improve discoverability.", |
| 8082 |
"yatra", |
| 8083 |
)} |
| 8084 |
</span> |
| 8085 |
</p> |
| 8086 |
|
| 8087 |
<div className="space-y-6"> |
| 8088 |
{/* SECTION 1: SEO Settings */} |
| 8089 |
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30"> |
| 8090 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> |
| 8091 |
<Search className="w-4 h-4 text-blue-500" /> |
| 8092 |
{__("Search Engine Optimization", "yatra")} |
| 8093 |
</h3> |
| 8094 |
<div className="space-y-4"> |
| 8095 |
<div> |
| 8096 |
<div className="flex items-center justify-between mb-1.5"> |
| 8097 |
<label |
| 8098 |
htmlFor="meta_title" |
| 8099 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400" |
| 8100 |
> |
| 8101 |
{__("Meta Title", "yatra")} |
| 8102 |
</label> |
| 8103 |
<AiFieldAffordance |
| 8104 |
task="seo-meta-title" |
| 8105 |
label={__("Meta Title", "yatra")} |
| 8106 |
value={formData.meta_title} |
| 8107 |
onAccept={(v) => |
| 8108 |
handleFieldChange("meta_title", v.slice(0, 60)) |
| 8109 |
} |
| 8110 |
buildContext={() => buildTripAiContext(formData)} |
| 8111 |
/> |
| 8112 |
</div> |
| 8113 |
<Input |
| 8114 |
id="meta_title" |
| 8115 |
type="text" |
| 8116 |
value={formData.meta_title} |
| 8117 |
onChange={(e) => |
| 8118 |
handleFieldChange("meta_title", e.target.value) |
| 8119 |
} |
| 8120 |
placeholder={formData.title || __("Trip Title", "yatra")} |
| 8121 |
maxLength={60} |
| 8122 |
/> |
| 8123 |
<HelpText |
| 8124 |
text={__( |
| 8125 |
"Best for search engines: 50-60 characters. Include main keywords.", |
| 8126 |
"yatra", |
| 8127 |
)} |
| 8128 |
className="mt-1" |
| 8129 |
/> |
| 8130 |
</div> |
| 8131 |
<div> |
| 8132 |
<div className="flex items-center justify-between mb-1.5"> |
| 8133 |
<label |
| 8134 |
htmlFor="meta_description" |
| 8135 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400" |
| 8136 |
> |
| 8137 |
{__("Meta Description", "yatra")} |
| 8138 |
</label> |
| 8139 |
<AiFieldAffordance |
| 8140 |
task="seo-meta-description" |
| 8141 |
label={__("Meta Description", "yatra")} |
| 8142 |
value={formData.meta_description} |
| 8143 |
onAccept={(v) => |
| 8144 |
handleFieldChange("meta_description", v.slice(0, 160)) |
| 8145 |
} |
| 8146 |
buildContext={() => buildTripAiContext(formData)} |
| 8147 |
/> |
| 8148 |
</div> |
| 8149 |
<textarea |
| 8150 |
id="meta_description" |
| 8151 |
value={formData.meta_description} |
| 8152 |
onChange={(e) => |
| 8153 |
handleFieldChange("meta_description", e.target.value) |
| 8154 |
} |
| 8155 |
rows={3} |
| 8156 |
maxLength={160} |
| 8157 |
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" |
| 8158 |
placeholder={__( |
| 8159 |
"Compelling description that includes key travel terms...", |
| 8160 |
"yatra", |
| 8161 |
)} |
| 8162 |
/> |
| 8163 |
<HelpText |
| 8164 |
text={__( |
| 8165 |
"160 characters max. Include location, duration, and key features.", |
| 8166 |
"yatra", |
| 8167 |
)} |
| 8168 |
className="mt-1" |
| 8169 |
/> |
| 8170 |
</div> |
| 8171 |
<div> |
| 8172 |
<label |
| 8173 |
htmlFor="meta_keywords" |
| 8174 |
className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" |
| 8175 |
> |
| 8176 |
{__("Meta Keywords", "yatra")} |
| 8177 |
</label> |
| 8178 |
<Input |
| 8179 |
id="meta_keywords" |
| 8180 |
type="text" |
| 8181 |
value={formData.meta_keywords} |
| 8182 |
onChange={(e) => |
| 8183 |
handleFieldChange("meta_keywords", e.target.value) |
| 8184 |
} |
| 8185 |
placeholder={__( |
| 8186 |
"adventure, travel, tour, guide, experience", |
| 8187 |
"yatra", |
| 8188 |
)} |
| 8189 |
/> |
| 8190 |
<HelpText |
| 8191 |
text={__( |
| 8192 |
"Comma-separated keywords. Include location, activities, and travel terms.", |
| 8193 |
"yatra", |
| 8194 |
)} |
| 8195 |
className="mt-1" |
| 8196 |
/> |
| 8197 |
</div> |
| 8198 |
</div> |
| 8199 |
|
| 8200 |
{/* SEO Preview */} |
| 8201 |
<Card> |
| 8202 |
<CardHeader> |
| 8203 |
<CardTitle>{__("SEO Preview", "yatra")}</CardTitle> |
| 8204 |
<CardDescription> |
| 8205 |
{__( |
| 8206 |
"Preview how your trip appears in search results", |
| 8207 |
"yatra", |
| 8208 |
)} |
| 8209 |
</CardDescription> |
| 8210 |
</CardHeader> |
| 8211 |
<CardContent className="space-y-4"> |
| 8212 |
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg"> |
| 8213 |
<div className="text-xs text-gray-500 dark:text-gray-400 mb-2"> |
| 8214 |
{__("Google Search Result Preview", "yatra")} |
| 8215 |
</div> |
| 8216 |
<div className="space-y-2"> |
| 8217 |
<div className="text-blue-800 dark:text-blue-400 text-sm font-medium"> |
| 8218 |
{sanitizeTextForSEO( |
| 8219 |
formData.meta_title || |
| 8220 |
formData.title || |
| 8221 |
"Trip Title", |
| 8222 |
60, |
| 8223 |
)} |
| 8224 |
</div> |
| 8225 |
<div className="text-green-700 dark:text-green-400 text-sm"> |
| 8226 |
{formData.meta_description |
| 8227 |
? sanitizeTextForSEO(formData.meta_description, 150) |
| 8228 |
: formData.description |
| 8229 |
? sanitizeTextForSEO(formData.description, 150) |
| 8230 |
: "Trip description..."} |
| 8231 |
</div> |
| 8232 |
<div className="text-gray-600 dark:text-gray-400 text-xs"> |
| 8233 |
{window.location.hostname}/trip/ |
| 8234 |
{formData.slug || "trip-slug"} |
| 8235 |
</div> |
| 8236 |
</div> |
| 8237 |
</div> |
| 8238 |
</CardContent> |
| 8239 |
</Card> |
| 8240 |
</div> |
| 8241 |
|
| 8242 |
{/* SECTION 2: FAQs */} |
| 8243 |
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30"> |
| 8244 |
<div className="mb-4 flex items-center justify-between gap-2"> |
| 8245 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-2"> |
| 8246 |
<HelpCircle className="w-4 h-4 text-blue-500" /> |
| 8247 |
{__("Frequently Asked Questions", "yatra")} |
| 8248 |
</h3> |
| 8249 |
{/* AI: generates 5-7 Q&A pairs. Result text is |
| 8250 |
parsed from "Q: …\nA: …" blocks separated by |
| 8251 |
blank lines — same shape as the trip-faq |
| 8252 |
prompt's documented output. Accept replaces |
| 8253 |
the existing FAQ list wholesale. */} |
| 8254 |
<AiFieldAffordance |
| 8255 |
task="trip-faq" |
| 8256 |
label={__("FAQ", "yatra")} |
| 8257 |
value={(formData.faqs ?? []) |
| 8258 |
.map((f: any) => `Q: ${f.question}\nA: ${f.answer}`) |
| 8259 |
.join("\n\n")} |
| 8260 |
onAccept={(raw) => { |
| 8261 |
const parsed: Array<{ |
| 8262 |
question: string; |
| 8263 |
answer: string; |
| 8264 |
}> = []; |
| 8265 |
const blocks = raw |
| 8266 |
.replace(/\r\n?/g, "\n") |
| 8267 |
.split(/\n{2,}/); |
| 8268 |
for (const block of blocks) { |
| 8269 |
const q = block.match(/^\s*Q[:\.\-]\s*(.+)/im); |
| 8270 |
const a = block.match(/A[:\.\-]\s*([\s\S]+)/im); |
| 8271 |
if (q) { |
| 8272 |
parsed.push({ |
| 8273 |
question: q[1].trim(), |
| 8274 |
answer: a ? a[1].trim() : "", |
| 8275 |
}); |
| 8276 |
} |
| 8277 |
} |
| 8278 |
if (parsed.length > 0) { |
| 8279 |
handleFieldChange("faqs", parsed as any); |
| 8280 |
} |
| 8281 |
}} |
| 8282 |
buildContext={() => buildTripAiContext(formData)} |
| 8283 |
/> |
| 8284 |
</div> |
| 8285 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4"> |
| 8286 |
{__( |
| 8287 |
"Answer common questions to build trust and reduce support inquiries. FAQs also help with SEO.", |
| 8288 |
"yatra", |
| 8289 |
)} |
| 8290 |
</p> |
| 8291 |
<div className="space-y-4"> |
| 8292 |
{formData.faqs.map((faq, index) => ( |
| 8293 |
<div |
| 8294 |
key={index} |
| 8295 |
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-900" |
| 8296 |
> |
| 8297 |
<div className="flex items-start justify-between mb-3"> |
| 8298 |
<h4 className="text-sm font-medium text-gray-900 dark:text-white"> |
| 8299 |
{__("FAQ", "yatra")} {index + 1} |
| 8300 |
</h4> |
| 8301 |
<button |
| 8302 |
type="button" |
| 8303 |
onClick={() => handleFAQRemove(index)} |
| 8304 |
className="text-red-600 hover:text-red-700" |
| 8305 |
> |
| 8306 |
<X className="w-4 h-4" /> |
| 8307 |
</button> |
| 8308 |
</div> |
| 8309 |
<div className="space-y-3"> |
| 8310 |
<div> |
| 8311 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 8312 |
{__("Question", "yatra")} |
| 8313 |
</label> |
| 8314 |
<Input |
| 8315 |
type="text" |
| 8316 |
value={faq.question} |
| 8317 |
onChange={(e) => |
| 8318 |
handleFAQChange(index, "question", e.target.value) |
| 8319 |
} |
| 8320 |
placeholder={__("Enter question...", "yatra")} |
| 8321 |
/> |
| 8322 |
</div> |
| 8323 |
<div> |
| 8324 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 8325 |
{__("Answer", "yatra")} |
| 8326 |
</label> |
| 8327 |
<textarea |
| 8328 |
value={faq.answer} |
| 8329 |
onChange={(e) => |
| 8330 |
handleFAQChange(index, "answer", e.target.value) |
| 8331 |
} |
| 8332 |
placeholder={__("Enter answer...", "yatra")} |
| 8333 |
rows={3} |
| 8334 |
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" |
| 8335 |
/> |
| 8336 |
</div> |
| 8337 |
</div> |
| 8338 |
</div> |
| 8339 |
))} |
| 8340 |
<Button |
| 8341 |
type="button" |
| 8342 |
variant="outline" |
| 8343 |
onClick={handleFAQAdd} |
| 8344 |
className="w-full" |
| 8345 |
> |
| 8346 |
<Plus className="w-4 h-4 mr-2" /> |
| 8347 |
{__("Add FAQ", "yatra")} |
| 8348 |
</Button> |
| 8349 |
</div> |
| 8350 |
</div> |
| 8351 |
</div> |
| 8352 |
</div> |
| 8353 |
); |
| 8354 |
|
| 8355 |
case "advanced": |
| 8356 |
return ( |
| 8357 |
<div className="space-y-6"> |
| 8358 |
<div className="flex flex-wrap items-center gap-2 mb-2"> |
| 8359 |
<Settings className="w-5 h-5 text-gray-500" /> |
| 8360 |
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> |
| 8361 |
{__("Lifecycle Management", "yatra")} |
| 8362 |
</h2> |
| 8363 |
<Badge |
| 8364 |
variant="outline" |
| 8365 |
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" |
| 8366 |
> |
| 8367 |
{__("Power Users", "yatra")} |
| 8368 |
</Badge> |
| 8369 |
</div> |
| 8370 |
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"> |
| 8371 |
{__("When should this trip be published or archived?", "yatra")} |
| 8372 |
<span className="block text-xs text-gray-500 dark:text-gray-400 mt-1"> |
| 8373 |
{__( |
| 8374 |
"Schedule automatic publishing and unpublishing for seasonal trips. Optional but useful for managing trip lifecycles.", |
| 8375 |
"yatra", |
| 8376 |
)} |
| 8377 |
</span> |
| 8378 |
</p> |
| 8379 |
|
| 8380 |
<Card> |
| 8381 |
<CardHeader> |
| 8382 |
<CardTitle>{__("Version Control", "yatra")}</CardTitle> |
| 8383 |
<CardDescription> |
| 8384 |
{__("Track changes and version history", "yatra")} |
| 8385 |
</CardDescription> |
| 8386 |
</CardHeader> |
| 8387 |
<CardContent className="space-y-4"> |
| 8388 |
<div> |
| 8389 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 8390 |
{__("Version", "yatra")} |
| 8391 |
</label> |
| 8392 |
<Input |
| 8393 |
type="number" |
| 8394 |
value={formData.version} |
| 8395 |
onChange={(e) => |
| 8396 |
handleFieldChange( |
| 8397 |
"version", |
| 8398 |
parseInt(e.target.value) || 1, |
| 8399 |
) |
| 8400 |
} |
| 8401 |
min="1" |
| 8402 |
readOnly |
| 8403 |
className="bg-gray-50 dark:bg-gray-800" |
| 8404 |
/> |
| 8405 |
<HelpText |
| 8406 |
text={__( |
| 8407 |
"Version number is automatically incremented when changes are saved", |
| 8408 |
"yatra", |
| 8409 |
)} |
| 8410 |
className="mt-2" |
| 8411 |
/> |
| 8412 |
</div> |
| 8413 |
</CardContent> |
| 8414 |
</Card> |
| 8415 |
|
| 8416 |
<Card> |
| 8417 |
<CardHeader> |
| 8418 |
<CardTitle>{__("Scheduled Publishing", "yatra")}</CardTitle> |
| 8419 |
<CardDescription> |
| 8420 |
{__( |
| 8421 |
"Automatically publish or unpublish your trip on specific dates", |
| 8422 |
"yatra", |
| 8423 |
)} |
| 8424 |
</CardDescription> |
| 8425 |
</CardHeader> |
| 8426 |
<CardContent className="space-y-4"> |
| 8427 |
<div> |
| 8428 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 8429 |
{__("Schedule Publish Date", "yatra")} |
| 8430 |
</label> |
| 8431 |
<DatePicker |
| 8432 |
value={formData.scheduled_publish_date} |
| 8433 |
onChange={(val) => |
| 8434 |
handleFieldChange("scheduled_publish_date", val) |
| 8435 |
} |
| 8436 |
placeholder={__("Select date", "yatra")} |
| 8437 |
/> |
| 8438 |
<HelpText |
| 8439 |
text={__( |
| 8440 |
"Trip will be automatically published on this date and time", |
| 8441 |
"yatra", |
| 8442 |
)} |
| 8443 |
className="mt-2" |
| 8444 |
/> |
| 8445 |
</div> |
| 8446 |
<div> |
| 8447 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 8448 |
{__("Schedule Unpublish Date", "yatra")} |
| 8449 |
</label> |
| 8450 |
<DatePicker |
| 8451 |
value={formData.scheduled_unpublish_date} |
| 8452 |
onChange={(val) => |
| 8453 |
handleFieldChange("scheduled_unpublish_date", val) |
| 8454 |
} |
| 8455 |
placeholder={__("Select date", "yatra")} |
| 8456 |
/> |
| 8457 |
<HelpText |
| 8458 |
text={__( |
| 8459 |
"Trip will be automatically unpublished (archived) on this date and time", |
| 8460 |
"yatra", |
| 8461 |
)} |
| 8462 |
className="mt-2" |
| 8463 |
/> |
| 8464 |
</div> |
| 8465 |
</CardContent> |
| 8466 |
</Card> |
| 8467 |
|
| 8468 |
<Card> |
| 8469 |
<CardHeader> |
| 8470 |
<CardTitle> |
| 8471 |
{__("Seasonal Auto-Enable/Disable", "yatra")} |
| 8472 |
</CardTitle> |
| 8473 |
<CardDescription> |
| 8474 |
{__( |
| 8475 |
"Automatically enable or disable trip availability based on seasonal dates", |
| 8476 |
"yatra", |
| 8477 |
)} |
| 8478 |
</CardDescription> |
| 8479 |
</CardHeader> |
| 8480 |
<CardContent className="space-y-4"> |
| 8481 |
<div className="flex items-center gap-2"> |
| 8482 |
<input |
| 8483 |
type="checkbox" |
| 8484 |
id="seasonal_auto_enable" |
| 8485 |
checked={formData.seasonal_auto_enable} |
| 8486 |
onChange={(e) => |
| 8487 |
handleFieldChange( |
| 8488 |
"seasonal_auto_enable", |
| 8489 |
e.target.checked, |
| 8490 |
) |
| 8491 |
} |
| 8492 |
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" |
| 8493 |
/> |
| 8494 |
<label |
| 8495 |
htmlFor="seasonal_auto_enable" |
| 8496 |
className="text-sm font-medium text-gray-700 dark:text-gray-300" |
| 8497 |
> |
| 8498 |
{__("Enable seasonal auto-management", "yatra")} |
| 8499 |
</label> |
| 8500 |
</div> |
| 8501 |
{formData.seasonal_auto_enable && ( |
| 8502 |
<> |
| 8503 |
<div> |
| 8504 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 8505 |
{__("Auto-Enable Date", "yatra")} |
| 8506 |
</label> |
| 8507 |
<DatePicker |
| 8508 |
value={formData.seasonal_enable_date} |
| 8509 |
onChange={(val) => |
| 8510 |
handleFieldChange("seasonal_enable_date", val) |
| 8511 |
} |
| 8512 |
placeholder={__("Select date", "yatra")} |
| 8513 |
/> |
| 8514 |
<HelpText |
| 8515 |
text={__( |
| 8516 |
"Trip will become available for booking on this date", |
| 8517 |
"yatra", |
| 8518 |
)} |
| 8519 |
className="mt-2" |
| 8520 |
/> |
| 8521 |
</div> |
| 8522 |
<div> |
| 8523 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 8524 |
{__("Auto-Disable Date", "yatra")} |
| 8525 |
</label> |
| 8526 |
<DatePicker |
| 8527 |
value={formData.seasonal_disable_date} |
| 8528 |
onChange={(val) => |
| 8529 |
handleFieldChange("seasonal_disable_date", val) |
| 8530 |
} |
| 8531 |
placeholder={__("Select date", "yatra")} |
| 8532 |
/> |
| 8533 |
<HelpText |
| 8534 |
text={__( |
| 8535 |
"Trip will become unavailable for booking on this date", |
| 8536 |
"yatra", |
| 8537 |
)} |
| 8538 |
className="mt-2" |
| 8539 |
/> |
| 8540 |
</div> |
| 8541 |
</> |
| 8542 |
)} |
| 8543 |
</CardContent> |
| 8544 |
</Card> |
| 8545 |
|
| 8546 |
{/* Frontend Tabs Management */} |
| 8547 |
<Card> |
| 8548 |
<CardHeader> |
| 8549 |
<CardTitle>{__("Frontend Tabs Management", "yatra")}</CardTitle> |
| 8550 |
<CardDescription> |
| 8551 |
{__( |
| 8552 |
"Manage which tabs appear on the trip single page and in what order. Enable or disable tabs, customize labels, and reorder them.", |
| 8553 |
"yatra", |
| 8554 |
)} |
| 8555 |
</CardDescription> |
| 8556 |
</CardHeader> |
| 8557 |
<CardContent> |
| 8558 |
<div className="space-y-3"> |
| 8559 |
{[...formData.frontend_tabs] |
| 8560 |
.sort((a, b) => a.order - b.order) |
| 8561 |
.map((tab, index) => ( |
| 8562 |
<div |
| 8563 |
key={tab.id} |
| 8564 |
draggable |
| 8565 |
onDragStart={(e) => handleDragStart(e, tab.id)} |
| 8566 |
onDragOver={handleDragOver} |
| 8567 |
onDragEnter={() => handleDragEnter(tab.id)} |
| 8568 |
onDragLeave={handleDragLeave} |
| 8569 |
onDrop={(e) => handleDrop(e, tab.id)} |
| 8570 |
onDragEnd={handleDragEnd} |
| 8571 |
className={`p-4 rounded-lg border cursor-move relative ${ |
| 8572 |
tab.enabled |
| 8573 |
? "bg-gray-50 dark:bg-gray-850 border-gray-200 dark:border-gray-700 shadow-sm" |
| 8574 |
: "bg-gray-100 dark:bg-gray-900 border-gray-200 dark:border-gray-700 opacity-60 shadow-sm" |
| 8575 |
} ${ |
| 8576 |
draggedTab === tab.id |
| 8577 |
? "opacity-50 scale-95 shadow-lg" |
| 8578 |
: "" |
| 8579 |
} ${ |
| 8580 |
dragOverTab === tab.id && draggedTab !== tab.id |
| 8581 |
? "border-blue-400 bg-blue-50 dark:bg-blue-900/20" |
| 8582 |
: "" |
| 8583 |
}`} |
| 8584 |
> |
| 8585 |
{tab.content_type === "custom" && ( |
| 8586 |
<Button |
| 8587 |
type="button" |
| 8588 |
variant="ghost" |
| 8589 |
size="sm" |
| 8590 |
onClick={() => handleTabRemove(tab.id)} |
| 8591 |
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" |
| 8592 |
title={__("Delete tab", "yatra")} |
| 8593 |
> |
| 8594 |
<Trash2 className="w-5 h-5" /> |
| 8595 |
</Button> |
| 8596 |
)} |
| 8597 |
<div className="flex items-center gap-3 mb-3"> |
| 8598 |
<GripVertical className="w-6 h-6 text-gray-400 cursor-grab active:cursor-grabbing" /> |
| 8599 |
<div className="flex-1"> |
| 8600 |
<div className="flex items-center gap-2 mb-2"> |
| 8601 |
<Input |
| 8602 |
type="text" |
| 8603 |
value={tab.label} |
| 8604 |
onChange={(e) => |
| 8605 |
handleTabLabelChange(tab.id, e.target.value) |
| 8606 |
} |
| 8607 |
className="text-sm font-medium flex-1" |
| 8608 |
disabled={!tab.enabled} |
| 8609 |
placeholder={__("Tab Label", "yatra")} |
| 8610 |
/> |
| 8611 |
<Badge variant="outline" className="text-xs"> |
| 8612 |
{tab.content_type} |
| 8613 |
</Badge> |
| 8614 |
</div> |
| 8615 |
<div className="mb-2"> |
| 8616 |
{tab.enabled ? ( |
| 8617 |
<IconPicker |
| 8618 |
value={tab.icon || null} |
| 8619 |
onChange={(value) => |
| 8620 |
handleTabIconChange(tab.id, value) |
| 8621 |
} |
| 8622 |
label={__("Tab Icon", "yatra")} |
| 8623 |
helpText={__( |
| 8624 |
"Select an icon or upload an image for this tab", |
| 8625 |
"yatra", |
| 8626 |
)} |
| 8627 |
size="sm" |
| 8628 |
/> |
| 8629 |
) : ( |
| 8630 |
<div className="opacity-50"> |
| 8631 |
<IconPicker |
| 8632 |
value={tab.icon || null} |
| 8633 |
onChange={() => {}} // No-op when disabled |
| 8634 |
label={__("Tab Icon", "yatra")} |
| 8635 |
helpText={__( |
| 8636 |
"Enable this tab to select an icon", |
| 8637 |
"yatra", |
| 8638 |
)} |
| 8639 |
size="sm" |
| 8640 |
/> |
| 8641 |
</div> |
| 8642 |
)} |
| 8643 |
</div> |
| 8644 |
{tab.content_type === "custom" && ( |
| 8645 |
<div className="mt-2"> |
| 8646 |
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5"> |
| 8647 |
{__("Custom Content", "yatra")} |
| 8648 |
</label> |
| 8649 |
<textarea |
| 8650 |
value={tab.custom_content || ""} |
| 8651 |
onChange={(e) => |
| 8652 |
handleTabCustomContentChange( |
| 8653 |
tab.id, |
| 8654 |
e.target.value, |
| 8655 |
) |
| 8656 |
} |
| 8657 |
rows={3} |
| 8658 |
disabled={!tab.enabled} |
| 8659 |
placeholder={__( |
| 8660 |
"Enter custom content for this tab...", |
| 8661 |
"yatra", |
| 8662 |
)} |
| 8663 |
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" |
| 8664 |
/> |
| 8665 |
</div> |
| 8666 |
)} |
| 8667 |
{tab.content_type !== "custom" && ( |
| 8668 |
<div className="mt-2"> |
| 8669 |
<Select |
| 8670 |
value={tab.content_type} |
| 8671 |
onChange={(e) => |
| 8672 |
handleTabContentTypeChange( |
| 8673 |
tab.id, |
| 8674 |
e.target |
| 8675 |
.value as FrontendTab["content_type"], |
| 8676 |
) |
| 8677 |
} |
| 8678 |
disabled={!tab.enabled} |
| 8679 |
className="text-xs" |
| 8680 |
> |
| 8681 |
<option value="overview"> |
| 8682 |
{__("Overview", "yatra")} |
| 8683 |
</option> |
| 8684 |
<option value="itinerary"> |
| 8685 |
{__("Itinerary", "yatra")} |
| 8686 |
</option> |
| 8687 |
<option value="included_excluded"> |
| 8688 |
{__("Included", "yatra")} |
| 8689 |
</option> |
| 8690 |
<option value="location"> |
| 8691 |
{__("Location", "yatra")} |
| 8692 |
</option> |
| 8693 |
<option value="important_info"> |
| 8694 |
{__("Important Info", "yatra")} |
| 8695 |
</option> |
| 8696 |
<option value="downloads"> |
| 8697 |
{__("Downloads", "yatra")} |
| 8698 |
</option> |
| 8699 |
<option value="faq"> |
| 8700 |
{__("FAQ", "yatra")} |
| 8701 |
</option> |
| 8702 |
<option value="trip_story"> |
| 8703 |
{__("Story", "yatra")} |
| 8704 |
</option> |
| 8705 |
<option value="what_makes_special"> |
| 8706 |
{__("Special", "yatra")} |
| 8707 |
</option> |
| 8708 |
<option value="testimonials"> |
| 8709 |
{__("Testimonials", "yatra")} |
| 8710 |
</option> |
| 8711 |
<option value="custom"> |
| 8712 |
{__("Custom Content", "yatra")} |
| 8713 |
</option> |
| 8714 |
</Select> |
| 8715 |
</div> |
| 8716 |
)} |
| 8717 |
</div> |
| 8718 |
<div className="flex items-center gap-2"> |
| 8719 |
<Button |
| 8720 |
type="button" |
| 8721 |
variant="ghost" |
| 8722 |
size="sm" |
| 8723 |
onClick={() => handleTabMove(tab.id, "up")} |
| 8724 |
disabled={index === 0} |
| 8725 |
className="h-10 w-10 p-0" |
| 8726 |
title={__("Move up", "yatra")} |
| 8727 |
> |
| 8728 |
<ChevronUp className="w-5 h-5" /> |
| 8729 |
</Button> |
| 8730 |
<Button |
| 8731 |
type="button" |
| 8732 |
variant="ghost" |
| 8733 |
size="sm" |
| 8734 |
onClick={() => handleTabMove(tab.id, "down")} |
| 8735 |
disabled={ |
| 8736 |
index === formData.frontend_tabs.length - 1 |
| 8737 |
} |
| 8738 |
className="h-10 w-10 p-0" |
| 8739 |
title={__("Move down", "yatra")} |
| 8740 |
> |
| 8741 |
<ChevronDown className="w-5 h-5" /> |
| 8742 |
</Button> |
| 8743 |
<label className="flex items-center gap-2 cursor-pointer"> |
| 8744 |
<input |
| 8745 |
type="checkbox" |
| 8746 |
checked={tab.enabled} |
| 8747 |
onChange={() => handleTabToggle(tab.id)} |
| 8748 |
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" |
| 8749 |
/> |
| 8750 |
<span className="text-sm text-gray-700 dark:text-gray-300"> |
| 8751 |
{tab.enabled |
| 8752 |
? __("Enabled", "yatra") |
| 8753 |
: __("Disabled", "yatra")} |
| 8754 |
</span> |
| 8755 |
</label> |
| 8756 |
</div> |
| 8757 |
</div> |
| 8758 |
</div> |
| 8759 |
))} |
| 8760 |
</div> |
| 8761 |
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700"> |
| 8762 |
<Button |
| 8763 |
type="button" |
| 8764 |
variant="outline" |
| 8765 |
onClick={handleTabAdd} |
| 8766 |
className="w-full" |
| 8767 |
> |
| 8768 |
<Plus className="w-4 h-4 mr-2" /> |
| 8769 |
{__("Add Custom Tab", "yatra")} |
| 8770 |
</Button> |
| 8771 |
</div> |
| 8772 |
</CardContent> |
| 8773 |
</Card> |
| 8774 |
</div> |
| 8775 |
); |
| 8776 |
|
| 8777 |
default: |
| 8778 |
return ( |
| 8779 |
<div className="text-center py-8 text-gray-500"> |
| 8780 |
{__("Section content coming soon...", "yatra")} |
| 8781 |
</div> |
| 8782 |
); |
| 8783 |
} |
| 8784 |
}; |
| 8785 |
|
| 8786 |
return ( |
| 8787 |
<div className="flex-1 min-h-0 flex flex-col bg-white dark:bg-gray-900 overflow-hidden"> |
| 8788 |
{/* Header */} |
| 8789 |
<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"> |
| 8790 |
{/* Left Side - Title, Status, Saved Time */} |
| 8791 |
<div className="flex items-center gap-4 min-w-0 flex-1"> |
| 8792 |
<div className="min-w-0"> |
| 8793 |
<h1 className="text-base font-semibold text-gray-900 dark:text-white truncate"> |
| 8794 |
{formData.title || __("New Trip", "yatra")} |
| 8795 |
</h1> |
| 8796 |
<div className="flex items-center gap-2 mt-1"> |
| 8797 |
<Badge |
| 8798 |
variant="outline" |
| 8799 |
className={`bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 ${ |
| 8800 |
formData.status === "publish" |
| 8801 |
? "text-green-700 dark:text-green-400 border-green-300 dark:border-green-800" |
| 8802 |
: formData.status === "review" |
| 8803 |
? "text-yellow-700 dark:text-yellow-400 border-yellow-300 dark:border-yellow-800" |
| 8804 |
: formData.status === "approved" |
| 8805 |
? "text-blue-700 dark:text-blue-400 border-blue-300 dark:border-blue-800" |
| 8806 |
: formData.status === "suspended" |
| 8807 |
? "text-orange-700 dark:text-orange-400 border-orange-300 dark:border-orange-800" |
| 8808 |
: formData.status === "archived" |
| 8809 |
? "text-gray-700 dark:text-gray-400 border-gray-300 dark:border-gray-600" |
| 8810 |
: "text-gray-700 dark:text-gray-300" |
| 8811 |
}`} |
| 8812 |
> |
| 8813 |
{formData.status === "draft" |
| 8814 |
? __("Draft", "yatra") |
| 8815 |
: formData.status === "review" |
| 8816 |
? __("Review", "yatra") |
| 8817 |
: formData.status === "approved" |
| 8818 |
? __("Approved", "yatra") |
| 8819 |
: formData.status === "publish" |
| 8820 |
? __("Published", "yatra") |
| 8821 |
: formData.status === "suspended" |
| 8822 |
? __("Suspended", "yatra") |
| 8823 |
: __("Archived", "yatra")} |
| 8824 |
</Badge> |
| 8825 |
</div> |
| 8826 |
</div> |
| 8827 |
|
| 8828 |
{/* Quick Start Mode Toggle */} |
| 8829 |
{!isEditMode && ( |
| 8830 |
<div className="flex items-center gap-2 px-2 py-1 rounded-md bg-gray-50 dark:bg-gray-700/50 flex-shrink-0"> |
| 8831 |
<button |
| 8832 |
onClick={() => setSimpleMode(!simpleMode)} |
| 8833 |
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" |
| 8834 |
title={ |
| 8835 |
simpleMode |
| 8836 |
? __("Switch to Advanced Mode", "yatra") |
| 8837 |
: __("Switch to Simple Mode", "yatra") |
| 8838 |
} |
| 8839 |
> |
| 8840 |
<Lightbulb |
| 8841 |
className={`w-3.5 h-3.5 ${simpleMode ? "text-yellow-500" : ""}`} |
| 8842 |
/> |
| 8843 |
<span> |
| 8844 |
{simpleMode ? __("Simple", "yatra") : __("Advanced", "yatra")} |
| 8845 |
</span> |
| 8846 |
</button> |
| 8847 |
</div> |
| 8848 |
)} |
| 8849 |
|
| 8850 |
{/* Guided Tour Button (First Time Users) */} |
| 8851 |
{!isEditMode && ( |
| 8852 |
<Button |
| 8853 |
variant="ghost" |
| 8854 |
size="sm" |
| 8855 |
onClick={() => { |
| 8856 |
showToast( |
| 8857 |
__("Guided tour feature coming soon!", "yatra"), |
| 8858 |
"info", |
| 8859 |
); |
| 8860 |
}} |
| 8861 |
className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 flex-shrink-0" |
| 8862 |
> |
| 8863 |
<BookOpen className="w-3.5 h-3.5 mr-1" /> |
| 8864 |
{__("Take a Tour", "yatra")} |
| 8865 |
</Button> |
| 8866 |
)} |
| 8867 |
|
| 8868 |
{/* Dummy Data Button */} |
| 8869 |
{!isEditMode && ( |
| 8870 |
<Button |
| 8871 |
variant="ghost" |
| 8872 |
size="sm" |
| 8873 |
onClick={handleFillDummyData} |
| 8874 |
className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 flex-shrink-0" |
| 8875 |
title={__("Fill form with dummy data", "yatra")} |
| 8876 |
> |
| 8877 |
<Database className="w-3.5 h-3.5 mr-1" /> |
| 8878 |
{__("Fill Dummy Data", "yatra")} ({dummyDataIndex + 1}/ |
| 8879 |
{dummyTripsData.length}) |
| 8880 |
</Button> |
| 8881 |
)} |
| 8882 |
</div> |
| 8883 |
|
| 8884 |
{/* Right Side - Action Buttons */} |
| 8885 |
<div className="flex items-center gap-2 flex-shrink-0"> |
| 8886 |
{isEditMode && ( |
| 8887 |
<Button |
| 8888 |
variant="outline" |
| 8889 |
onClick={() => setShowRevisionsDialog(true)} |
| 8890 |
className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700" |
| 8891 |
> |
| 8892 |
<History className="w-4 h-4 mr-2" /> |
| 8893 |
{__("Revisions", "yatra")} |
| 8894 |
</Button> |
| 8895 |
)} |
| 8896 |
{isAiEligible() && isAiModuleEnabled() && ( |
| 8897 |
<Button |
| 8898 |
type="button" |
| 8899 |
onClick={() => { |
| 8900 |
setAiModalMode("all"); |
| 8901 |
setAiModalOpen(true); |
| 8902 |
}} |
| 8903 |
className="bg-gradient-to-r from-purple-600 to-indigo-600 text-white border-0 hover:from-purple-700 hover:to-indigo-700" |
| 8904 |
title={__( |
| 8905 |
"Generate description, itinerary, included items, SEO meta, and more — from this trip's facts.", |
| 8906 |
"yatra", |
| 8907 |
)} |
| 8908 |
> |
| 8909 |
<Sparkles className="w-4 h-4 mr-2" /> |
| 8910 |
{__("Auto-fill with AI", "yatra")} |
| 8911 |
</Button> |
| 8912 |
)} |
| 8913 |
<Button |
| 8914 |
variant="outline" |
| 8915 |
onClick={handlePreview} |
| 8916 |
className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700" |
| 8917 |
disabled={!formData.slug} |
| 8918 |
> |
| 8919 |
<Eye className="w-4 h-4 mr-2" /> |
| 8920 |
{__("Preview", "yatra")} |
| 8921 |
</Button> |
| 8922 |
<Button |
| 8923 |
variant="outline" |
| 8924 |
onClick={handleSaveDraft} |
| 8925 |
disabled={isSubmitting} |
| 8926 |
className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700" |
| 8927 |
> |
| 8928 |
<Save className="w-4 h-4 mr-2" /> |
| 8929 |
{isEditMode |
| 8930 |
? __("Update Draft", "yatra") |
| 8931 |
: __("Save Draft", "yatra")} |
| 8932 |
</Button> |
| 8933 |
<div className="relative group"> |
| 8934 |
<Button |
| 8935 |
onClick={handlePublish} |
| 8936 |
disabled={isSubmitting} |
| 8937 |
className="bg-blue-600 hover:bg-blue-700 text-white border-0 relative" |
| 8938 |
> |
| 8939 |
{isSubmitting ? ( |
| 8940 |
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> |
| 8941 |
) : ( |
| 8942 |
<> |
| 8943 |
{/* Send icon (paper-plane) — used to be Sparkles, |
| 8944 |
which made it look like an AI feature; replaced |
| 8945 |
so the publish button reads as a regular submit |
| 8946 |
action, distinct from the actual AI affordances |
| 8947 |
sprinkled across the form. */} |
| 8948 |
<Send className="w-4 h-4 mr-2" /> |
| 8949 |
{isEditMode |
| 8950 |
? __("Update Trip", "yatra") |
| 8951 |
: __("Publish Trip", "yatra")} |
| 8952 |
<ChevronDown className="w-4 h-4 ml-2" /> |
| 8953 |
</> |
| 8954 |
)} |
| 8955 |
</Button> |
| 8956 |
{!isSubmitting && ( |
| 8957 |
<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"> |
| 8958 |
<div className="py-1"> |
| 8959 |
<button |
| 8960 |
type="button" |
| 8961 |
onClick={() => { |
| 8962 |
handleFieldChange("status", "draft"); |
| 8963 |
handleSaveDraft(); |
| 8964 |
}} |
| 8965 |
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" |
| 8966 |
> |
| 8967 |
<span>{__("Save as Draft", "yatra")}</span> |
| 8968 |
</button> |
| 8969 |
<button |
| 8970 |
type="button" |
| 8971 |
onClick={() => { |
| 8972 |
handleFieldChange("status", "review"); |
| 8973 |
handlePublish(); |
| 8974 |
}} |
| 8975 |
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" |
| 8976 |
> |
| 8977 |
<span>{__("Save for Review", "yatra")}</span> |
| 8978 |
</button> |
| 8979 |
<button |
| 8980 |
type="button" |
| 8981 |
onClick={() => { |
| 8982 |
handleFieldChange("status", "approved"); |
| 8983 |
handlePublish(); |
| 8984 |
}} |
| 8985 |
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" |
| 8986 |
> |
| 8987 |
<span>{__("Mark as Approved", "yatra")}</span> |
| 8988 |
</button> |
| 8989 |
<button |
| 8990 |
type="button" |
| 8991 |
onClick={handlePublish} |
| 8992 |
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" |
| 8993 |
> |
| 8994 |
<Sparkles className="w-4 h-4" /> |
| 8995 |
<span> |
| 8996 |
{isEditMode |
| 8997 |
? __("Update & Publish", "yatra") |
| 8998 |
: __("Publish", "yatra")} |
| 8999 |
</span> |
| 9000 |
</button> |
| 9001 |
<button |
| 9002 |
type="button" |
| 9003 |
onClick={() => { |
| 9004 |
handleFieldChange("status", "suspended"); |
| 9005 |
handlePublish(); |
| 9006 |
}} |
| 9007 |
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" |
| 9008 |
> |
| 9009 |
<span>{__("Suspend", "yatra")}</span> |
| 9010 |
</button> |
| 9011 |
<button |
| 9012 |
type="button" |
| 9013 |
onClick={() => { |
| 9014 |
handleFieldChange("status", "archived"); |
| 9015 |
handlePublish(); |
| 9016 |
}} |
| 9017 |
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" |
| 9018 |
> |
| 9019 |
<span>{__("Archive", "yatra")}</span> |
| 9020 |
</button> |
| 9021 |
</div> |
| 9022 |
</div> |
| 9023 |
)} |
| 9024 |
</div> |
| 9025 |
</div> |
| 9026 |
</div> |
| 9027 |
|
| 9028 |
{/* Navigation Bar */} |
| 9029 |
<div className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 px-4 py-2.5"> |
| 9030 |
<div className="flex items-center justify-between gap-4"> |
| 9031 |
{/* Previous Button */} |
| 9032 |
<Button |
| 9033 |
variant="outline" |
| 9034 |
size="default" |
| 9035 |
onClick={goToPreviousSection} |
| 9036 |
disabled={currentStepIndex === 0} |
| 9037 |
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" |
| 9038 |
> |
| 9039 |
<ChevronLeft className="w-4 h-4" /> |
| 9040 |
{__("Previous", "yatra")} |
| 9041 |
</Button> |
| 9042 |
|
| 9043 |
{/* Next Button */} |
| 9044 |
<Button |
| 9045 |
variant="outline" |
| 9046 |
size="default" |
| 9047 |
onClick={goToNextSection} |
| 9048 |
disabled={currentStepIndex >= allSections.length - 1} |
| 9049 |
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" |
| 9050 |
> |
| 9051 |
{__("Next", "yatra")} |
| 9052 |
<ChevronRight className="w-4 h-4" /> |
| 9053 |
</Button> |
| 9054 |
</div> |
| 9055 |
</div> |
| 9056 |
|
| 9057 |
<div className="flex flex-1 min-h-0 p-0"> |
| 9058 |
{/* Sidebar */} |
| 9059 |
<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"> |
| 9060 |
<div className="p-5 pb-8 space-y-6"> |
| 9061 |
{/* PHASE 1: ESSENTIALS */} |
| 9062 |
<div> |
| 9063 |
<div className="flex items-center gap-2 mb-3 px-1"> |
| 9064 |
<div className="flex items-center gap-2 flex-1"> |
| 9065 |
<Box className="w-3.5 h-3.5 text-red-500 dark:text-red-400 flex-shrink-0" /> |
| 9066 |
<h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest"> |
| 9067 |
{__("PHASE 1: ESSENTIALS", "yatra")} |
| 9068 |
</h3> |
| 9069 |
</div> |
| 9070 |
<Badge |
| 9071 |
variant="outline" |
| 9072 |
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" |
| 9073 |
> |
| 9074 |
{__("Must Complete", "yatra")} |
| 9075 |
</Badge> |
| 9076 |
</div> |
| 9077 |
<div className="space-y-0.5"> |
| 9078 |
{essentialsSections.map((section, index) => { |
| 9079 |
const Icon = section.icon; |
| 9080 |
const isActive = currentSection === section.id; |
| 9081 |
const isNext = |
| 9082 |
!section.completed && |
| 9083 |
essentialsSections |
| 9084 |
.slice(0, index) |
| 9085 |
.every((s) => s.completed); |
| 9086 |
return ( |
| 9087 |
<button |
| 9088 |
key={section.id} |
| 9089 |
type="button" |
| 9090 |
onClick={() => setCurrentSection(section.id)} |
| 9091 |
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 ${ |
| 9092 |
isActive |
| 9093 |
? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800" |
| 9094 |
: isNext |
| 9095 |
? "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" |
| 9096 |
: "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" |
| 9097 |
}`} |
| 9098 |
> |
| 9099 |
<div className="flex items-center gap-2 flex-shrink-0"> |
| 9100 |
<span |
| 9101 |
className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${ |
| 9102 |
isActive |
| 9103 |
? "bg-blue-600 text-white dark:bg-blue-500" |
| 9104 |
: isNext |
| 9105 |
? "bg-amber-500 text-white" |
| 9106 |
: section.completed |
| 9107 |
? "bg-green-500 text-white" |
| 9108 |
: section.hasErrors |
| 9109 |
? "bg-red-500 text-white" |
| 9110 |
: "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400" |
| 9111 |
}`} |
| 9112 |
> |
| 9113 |
{index + 1} |
| 9114 |
</span> |
| 9115 |
<Icon |
| 9116 |
className={`w-4 h-4 transition-colors ${ |
| 9117 |
isActive |
| 9118 |
? "text-blue-600 dark:text-blue-400" |
| 9119 |
: isNext |
| 9120 |
? "text-amber-600 dark:text-amber-400" |
| 9121 |
: section.hasErrors |
| 9122 |
? "text-red-600 dark:text-red-400" |
| 9123 |
: "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400" |
| 9124 |
}`} |
| 9125 |
/> |
| 9126 |
</div> |
| 9127 |
<span className="flex-1 min-w-0 break-words leading-snug"> |
| 9128 |
{section.label} |
| 9129 |
{section.required && ( |
| 9130 |
<span className="ml-1 text-[9px] text-red-500"> |
| 9131 |
* |
| 9132 |
</span> |
| 9133 |
)} |
| 9134 |
</span> |
| 9135 |
{section.hasErrors && ( |
| 9136 |
<span |
| 9137 |
className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full" |
| 9138 |
title={__("This section has errors", "yatra")} |
| 9139 |
/> |
| 9140 |
)} |
| 9141 |
</button> |
| 9142 |
); |
| 9143 |
})} |
| 9144 |
</div> |
| 9145 |
</div> |
| 9146 |
|
| 9147 |
{/* PHASE 2: DETAILS */} |
| 9148 |
<div> |
| 9149 |
<div className="flex items-center gap-2 mb-3 px-1"> |
| 9150 |
<div className="flex items-center gap-2 flex-1"> |
| 9151 |
<BookOpen className="w-3.5 h-3.5 text-blue-500 dark:text-blue-400 flex-shrink-0" /> |
| 9152 |
<h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest"> |
| 9153 |
{__("PHASE 2: DETAILS", "yatra")} |
| 9154 |
</h3> |
| 9155 |
</div> |
| 9156 |
<Badge |
| 9157 |
variant="outline" |
| 9158 |
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" |
| 9159 |
> |
| 9160 |
{__("Recommended", "yatra")} |
| 9161 |
</Badge> |
| 9162 |
</div> |
| 9163 |
<div className="space-y-0.5"> |
| 9164 |
{detailsSections.map((section, index) => { |
| 9165 |
const Icon = section.icon; |
| 9166 |
const isActive = currentSection === section.id; |
| 9167 |
const sectionNumber = essentialsSections.length + index + 1; |
| 9168 |
return ( |
| 9169 |
<button |
| 9170 |
key={section.id} |
| 9171 |
type="button" |
| 9172 |
onClick={() => setCurrentSection(section.id)} |
| 9173 |
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 ${ |
| 9174 |
isActive |
| 9175 |
? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800" |
| 9176 |
: section.hasErrors |
| 9177 |
? "border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-900/10" |
| 9178 |
: "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" |
| 9179 |
}`} |
| 9180 |
> |
| 9181 |
<div className="flex items-center gap-2 flex-shrink-0"> |
| 9182 |
<span |
| 9183 |
className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${ |
| 9184 |
isActive |
| 9185 |
? "bg-blue-600 text-white dark:bg-blue-500" |
| 9186 |
: section.completed |
| 9187 |
? "bg-green-500 text-white" |
| 9188 |
: section.hasErrors |
| 9189 |
? "bg-red-500 text-white" |
| 9190 |
: "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400" |
| 9191 |
}`} |
| 9192 |
> |
| 9193 |
{sectionNumber} |
| 9194 |
</span> |
| 9195 |
<Icon |
| 9196 |
className={`w-4 h-4 transition-colors ${ |
| 9197 |
isActive |
| 9198 |
? "text-blue-600 dark:text-blue-400" |
| 9199 |
: section.hasErrors |
| 9200 |
? "text-red-600 dark:text-red-400" |
| 9201 |
: "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400" |
| 9202 |
}`} |
| 9203 |
/> |
| 9204 |
</div> |
| 9205 |
<span className="flex-1 min-w-0 break-words leading-snug"> |
| 9206 |
{section.label} |
| 9207 |
</span> |
| 9208 |
{section.hasErrors && ( |
| 9209 |
<span |
| 9210 |
className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full" |
| 9211 |
title={__("This section has errors", "yatra")} |
| 9212 |
/> |
| 9213 |
)} |
| 9214 |
</button> |
| 9215 |
); |
| 9216 |
})} |
| 9217 |
</div> |
| 9218 |
</div> |
| 9219 |
|
| 9220 |
{/* PHASE 3: OPTIMIZATION */} |
| 9221 |
<div> |
| 9222 |
<div className="flex items-center gap-2 mb-3 px-1"> |
| 9223 |
<div className="flex items-center gap-2 flex-1"> |
| 9224 |
<BarChart3 className="w-3.5 h-3.5 text-purple-500 dark:text-purple-400 flex-shrink-0" /> |
| 9225 |
<h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest"> |
| 9226 |
{__("PHASE 3: OPTIMIZATION", "yatra")} |
| 9227 |
</h3> |
| 9228 |
</div> |
| 9229 |
<Badge |
| 9230 |
variant="outline" |
| 9231 |
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" |
| 9232 |
> |
| 9233 |
{__("Optional", "yatra")} |
| 9234 |
</Badge> |
| 9235 |
</div> |
| 9236 |
<div className="space-y-0.5"> |
| 9237 |
{optimizationSections.map((section, index) => { |
| 9238 |
const Icon = section.icon; |
| 9239 |
const isActive = currentSection === section.id; |
| 9240 |
const sectionNumber = |
| 9241 |
essentialsSections.length + |
| 9242 |
detailsSections.length + |
| 9243 |
index + |
| 9244 |
1; |
| 9245 |
return ( |
| 9246 |
<button |
| 9247 |
key={section.id} |
| 9248 |
type="button" |
| 9249 |
onClick={() => setCurrentSection(section.id)} |
| 9250 |
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 ${ |
| 9251 |
isActive |
| 9252 |
? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800" |
| 9253 |
: section.hasErrors |
| 9254 |
? "border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-900/10" |
| 9255 |
: "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" |
| 9256 |
}`} |
| 9257 |
> |
| 9258 |
<div className="flex items-center gap-2 flex-shrink-0"> |
| 9259 |
<span |
| 9260 |
className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${ |
| 9261 |
isActive |
| 9262 |
? "bg-blue-600 text-white dark:bg-blue-500" |
| 9263 |
: section.completed |
| 9264 |
? "bg-green-500 text-white" |
| 9265 |
: section.hasErrors |
| 9266 |
? "bg-red-500 text-white" |
| 9267 |
: "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400" |
| 9268 |
}`} |
| 9269 |
> |
| 9270 |
{sectionNumber} |
| 9271 |
</span> |
| 9272 |
<Icon |
| 9273 |
className={`w-4 h-4 transition-colors ${ |
| 9274 |
isActive |
| 9275 |
? "text-blue-600 dark:text-blue-400" |
| 9276 |
: section.hasErrors |
| 9277 |
? "text-red-600 dark:text-red-400" |
| 9278 |
: "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400" |
| 9279 |
}`} |
| 9280 |
/> |
| 9281 |
</div> |
| 9282 |
<span className="flex-1 min-w-0 break-words leading-snug"> |
| 9283 |
{section.label} |
| 9284 |
</span> |
| 9285 |
{section.hasErrors && ( |
| 9286 |
<span |
| 9287 |
className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full" |
| 9288 |
title={__("This section has errors", "yatra")} |
| 9289 |
/> |
| 9290 |
)} |
| 9291 |
</button> |
| 9292 |
); |
| 9293 |
})} |
| 9294 |
</div> |
| 9295 |
</div> |
| 9296 |
|
| 9297 |
{/* PHASE 4: ADVANCED */} |
| 9298 |
<div className="pb-12"> |
| 9299 |
<div className="flex items-center gap-2 mb-3 px-1"> |
| 9300 |
<div className="flex items-center gap-2 flex-1"> |
| 9301 |
<Settings className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 flex-shrink-0" /> |
| 9302 |
<h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest"> |
| 9303 |
{__("PHASE 4: ADVANCED", "yatra")} |
| 9304 |
</h3> |
| 9305 |
</div> |
| 9306 |
<Badge |
| 9307 |
variant="outline" |
| 9308 |
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" |
| 9309 |
> |
| 9310 |
{__("Power Users", "yatra")} |
| 9311 |
</Badge> |
| 9312 |
</div> |
| 9313 |
<div className="space-y-0.5"> |
| 9314 |
{advancedSections.map((section, index) => { |
| 9315 |
const Icon = section.icon; |
| 9316 |
const isActive = currentSection === section.id; |
| 9317 |
const sectionNumber = |
| 9318 |
essentialsSections.length + |
| 9319 |
detailsSections.length + |
| 9320 |
optimizationSections.length + |
| 9321 |
index + |
| 9322 |
1; |
| 9323 |
return ( |
| 9324 |
<button |
| 9325 |
key={section.id} |
| 9326 |
type="button" |
| 9327 |
onClick={() => setCurrentSection(section.id)} |
| 9328 |
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 ${ |
| 9329 |
isActive |
| 9330 |
? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800" |
| 9331 |
: "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" |
| 9332 |
}`} |
| 9333 |
> |
| 9334 |
<div className="flex items-center gap-2 flex-shrink-0"> |
| 9335 |
<span |
| 9336 |
className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${ |
| 9337 |
isActive |
| 9338 |
? "bg-blue-600 text-white dark:bg-blue-500" |
| 9339 |
: "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400" |
| 9340 |
}`} |
| 9341 |
> |
| 9342 |
{sectionNumber} |
| 9343 |
</span> |
| 9344 |
<Icon |
| 9345 |
className={`w-4 h-4 transition-colors ${ |
| 9346 |
isActive |
| 9347 |
? "text-blue-600 dark:text-blue-400" |
| 9348 |
: "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400" |
| 9349 |
}`} |
| 9350 |
/> |
| 9351 |
</div> |
| 9352 |
<span className="flex-1 min-w-0 break-words leading-snug"> |
| 9353 |
{section.label} |
| 9354 |
</span> |
| 9355 |
</button> |
| 9356 |
); |
| 9357 |
})} |
| 9358 |
</div> |
| 9359 |
</div> |
| 9360 |
</div> |
| 9361 |
</div> |
| 9362 |
|
| 9363 |
{/* Main Content */} |
| 9364 |
<div className="flex-1 overflow-y-auto bg-white dark:bg-gray-900"> |
| 9365 |
<div className="p-6 pb-24 max-w-4xl"> |
| 9366 |
{errors.submit && ( |
| 9367 |
<Alert variant="error" className="mb-4"> |
| 9368 |
{errors.submit} |
| 9369 |
</Alert> |
| 9370 |
)} |
| 9371 |
{renderSectionContent()} |
| 9372 |
</div> |
| 9373 |
</div> |
| 9374 |
</div> |
| 9375 |
|
| 9376 |
{/* Modals */} |
| 9377 |
{/* Highlight Modal */} |
| 9378 |
{showHighlightModal && ( |
| 9379 |
<div |
| 9380 |
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" |
| 9381 |
onClick={() => setShowHighlightModal(false)} |
| 9382 |
> |
| 9383 |
<div |
| 9384 |
className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-xl" |
| 9385 |
onClick={(e) => e.stopPropagation()} |
| 9386 |
> |
| 9387 |
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4"> |
| 9388 |
{__("Add Highlight", "yatra")} |
| 9389 |
</h3> |
| 9390 |
<div className="space-y-4"> |
| 9391 |
<div> |
| 9392 |
<label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"> |
| 9393 |
{__("Highlight Text", "yatra")} |
| 9394 |
</label> |
| 9395 |
<Input |
| 9396 |
type="text" |
| 9397 |
value={modalInput.text} |
| 9398 |
onChange={(e) => |
| 9399 |
setModalInput({ ...modalInput, text: e.target.value }) |
| 9400 |
} |
| 9401 |
placeholder={__("e.g., Private guide included", "yatra")} |
| 9402 |
autoFocus |
| 9403 |
onKeyDown={(e) => { |
| 9404 |
if (e.key === "Enter") { |
| 9405 |
e.preventDefault(); |
| 9406 |
handleHighlightSave(); |
| 9407 |
} |
| 9408 |
}} |
| 9409 |
/> |
| 9410 |
<HelpText |
| 9411 |
text={__( |
| 9412 |
'Keep it short and impactful. Examples: "All meals included", "Skip-the-line tickets", "Free airport transfer"', |
| 9413 |
"yatra", |
| 9414 |
)} |
| 9415 |
className="mt-2" |
| 9416 |
/> |
| 9417 |
</div> |
| 9418 |
<div className="flex items-center justify-end gap-2"> |
| 9419 |
<Button |
| 9420 |
variant="outline" |
| 9421 |
onClick={() => setShowHighlightModal(false)} |
| 9422 |
> |
| 9423 |
{__("Cancel", "yatra")} |
| 9424 |
</Button> |
| 9425 |
<Button |
| 9426 |
onClick={handleHighlightSave} |
| 9427 |
disabled={!modalInput.text.trim()} |
| 9428 |
> |
| 9429 |
{__("Add", "yatra")} |
| 9430 |
</Button> |
| 9431 |
</div> |
| 9432 |
</div> |
| 9433 |
</div> |
| 9434 |
</div> |
| 9435 |
)} |
| 9436 |
|
| 9437 |
{/* Revisions Dialog */} |
| 9438 |
{showRevisionsDialog && ( |
| 9439 |
<div |
| 9440 |
className="fixed inset-0 z-[9998] flex items-center justify-center bg-black/50 backdrop-blur-sm" |
| 9441 |
onClick={() => setShowRevisionsDialog(false)} |
| 9442 |
> |
| 9443 |
<Card |
| 9444 |
className="w-full max-w-2xl max-h-[80vh] mx-4 shadow-xl overflow-hidden flex flex-col" |
| 9445 |
onClick={(e) => e.stopPropagation()} |
| 9446 |
> |
| 9447 |
<CardHeader className="pb-3 flex-shrink-0"> |
| 9448 |
<div className="flex items-center justify-between"> |
| 9449 |
<CardTitle className="text-lg flex items-center gap-2"> |
| 9450 |
<History className="w-5 h-5" /> |
| 9451 |
{__("Trip Revisions", "yatra")} |
| 9452 |
</CardTitle> |
| 9453 |
<button |
| 9454 |
onClick={() => setShowRevisionsDialog(false)} |
| 9455 |
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors" |
| 9456 |
aria-label={__("Close", "yatra")} |
| 9457 |
> |
| 9458 |
<X className="w-5 h-5" /> |
| 9459 |
</button> |
| 9460 |
</div> |
| 9461 |
</CardHeader> |
| 9462 |
<CardContent className="flex-1 overflow-y-auto pb-4"> |
| 9463 |
{isLoadingRevisions ? ( |
| 9464 |
<div className="flex items-center justify-center py-8"> |
| 9465 |
<Loader2 className="w-6 h-6 animate-spin text-gray-400" /> |
| 9466 |
<span className="ml-2 text-gray-600 dark:text-gray-400"> |
| 9467 |
{__("Loading revisions...", "yatra")} |
| 9468 |
</span> |
| 9469 |
</div> |
| 9470 |
) : revisions.length === 0 ? ( |
| 9471 |
<div className="text-center py-8 text-gray-500 dark:text-gray-400"> |
| 9472 |
{__("No revisions found", "yatra")} |
| 9473 |
</div> |
| 9474 |
) : ( |
| 9475 |
<div className="space-y-2"> |
| 9476 |
{revisions.map((revision) => ( |
| 9477 |
<div |
| 9478 |
key={revision.id} |
| 9479 |
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" |
| 9480 |
onClick={() => handleRevisionClick(revision.id)} |
| 9481 |
> |
| 9482 |
<div className="flex items-center justify-between"> |
| 9483 |
<div className="flex-1"> |
| 9484 |
<div className="flex items-center gap-2 mb-1"> |
| 9485 |
<Badge variant="outline">v{revision.version}</Badge> |
| 9486 |
<span className="text-sm font-medium text-gray-900 dark:text-white"> |
| 9487 |
{__("Revision", "yatra")} {revision.version} |
| 9488 |
</span> |
| 9489 |
</div> |
| 9490 |
<div className="text-xs text-gray-500 dark:text-gray-400"> |
| 9491 |
{__("Created by", "yatra")}{" "} |
| 9492 |
{revision.created_by_name} •{" "} |
| 9493 |
{new Date(revision.created_at).toLocaleString()} |
| 9494 |
</div> |
| 9495 |
</div> |
| 9496 |
<Button |
| 9497 |
variant="outline" |
| 9498 |
size="sm" |
| 9499 |
onClick={(e) => { |
| 9500 |
e.stopPropagation(); |
| 9501 |
handleRevisionClick(revision.id); |
| 9502 |
}} |
| 9503 |
> |
| 9504 |
{__("Use This Revision", "yatra")} |
| 9505 |
</Button> |
| 9506 |
</div> |
| 9507 |
</div> |
| 9508 |
))} |
| 9509 |
</div> |
| 9510 |
)} |
| 9511 |
</CardContent> |
| 9512 |
</Card> |
| 9513 |
</div> |
| 9514 |
)} |
| 9515 |
|
| 9516 |
{/* Revision Confirmation Dialog */} |
| 9517 |
<ConfirmationDialog |
| 9518 |
isOpen={showRevisionConfirm} |
| 9519 |
onClose={() => { |
| 9520 |
setShowRevisionConfirm(false); |
| 9521 |
setSelectedRevisionId(null); |
| 9522 |
}} |
| 9523 |
onConfirm={handleRevisionConfirm} |
| 9524 |
title={__("Use This Revision?", "yatra")} |
| 9525 |
message={__( |
| 9526 |
"Do you want to use this revision? This will replace all current form data with the revision data.", |
| 9527 |
"yatra", |
| 9528 |
)} |
| 9529 |
confirmText={__("Yes, Use This Revision", "yatra")} |
| 9530 |
cancelText={__("Cancel", "yatra")} |
| 9531 |
variant="info" |
| 9532 |
isLoading={false} |
| 9533 |
/> |
| 9534 |
|
| 9535 |
{/* Landmark Input Modal */} |
| 9536 |
<Modal |
| 9537 |
isOpen={showLandmarkDialog} |
| 9538 |
onClose={() => { |
| 9539 |
setShowLandmarkDialog(false); |
| 9540 |
setLandmarkInput(""); |
| 9541 |
}} |
| 9542 |
title={__("Add Landmark", "yatra")} |
| 9543 |
description={__( |
| 9544 |
"Enter the name of the landmark or point of interest", |
| 9545 |
"yatra", |
| 9546 |
)} |
| 9547 |
size="sm" |
| 9548 |
hideFooter={false} |
| 9549 |
footer={ |
| 9550 |
<div className="flex gap-2 justify-end"> |
| 9551 |
<Button |
| 9552 |
variant="outline" |
| 9553 |
onClick={() => { |
| 9554 |
setShowLandmarkDialog(false); |
| 9555 |
setLandmarkInput(""); |
| 9556 |
}} |
| 9557 |
> |
| 9558 |
{__("Cancel", "yatra")} |
| 9559 |
</Button> |
| 9560 |
<Button |
| 9561 |
onClick={() => { |
| 9562 |
if (landmarkInput.trim()) { |
| 9563 |
handleFieldChange("landmarks", [ |
| 9564 |
...formData.landmarks, |
| 9565 |
landmarkInput.trim(), |
| 9566 |
]); |
| 9567 |
setLandmarkInput(""); |
| 9568 |
setShowLandmarkDialog(false); |
| 9569 |
} |
| 9570 |
}} |
| 9571 |
disabled={!landmarkInput.trim()} |
| 9572 |
> |
| 9573 |
<Plus className="w-4 h-4 mr-2" /> |
| 9574 |
{__("Add Landmark", "yatra")} |
| 9575 |
</Button> |
| 9576 |
</div> |
| 9577 |
} |
| 9578 |
> |
| 9579 |
<div className="space-y-4"> |
| 9580 |
<div> |
| 9581 |
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> |
| 9582 |
{__("Landmark Name", "yatra")} |
| 9583 |
</label> |
| 9584 |
<Input |
| 9585 |
type="text" |
| 9586 |
value={landmarkInput} |
| 9587 |
onChange={(e) => setLandmarkInput(e.target.value)} |
| 9588 |
placeholder={__( |
| 9589 |
"e.g., Eiffel Tower, Central Park, Grand Canyon", |
| 9590 |
"yatra", |
| 9591 |
)} |
| 9592 |
className="w-full" |
| 9593 |
autoFocus |
| 9594 |
onKeyDown={(e) => { |
| 9595 |
if (e.key === "Enter" && landmarkInput.trim()) { |
| 9596 |
handleFieldChange("landmarks", [ |
| 9597 |
...formData.landmarks, |
| 9598 |
landmarkInput.trim(), |
| 9599 |
]); |
| 9600 |
setLandmarkInput(""); |
| 9601 |
setShowLandmarkDialog(false); |
| 9602 |
} |
| 9603 |
}} |
| 9604 |
/> |
| 9605 |
</div> |
| 9606 |
<div className="text-xs text-gray-500 dark:text-gray-400"> |
| 9607 |
{__( |
| 9608 |
"Examples: Famous monuments, natural landmarks, historical sites, viewpoints, or points of interest that travelers will visit during this trip.", |
| 9609 |
"yatra", |
| 9610 |
)} |
| 9611 |
</div> |
| 9612 |
</div> |
| 9613 |
</Modal> |
| 9614 |
{/* AI Assistant — auto-fill / itinerary modal. Mounted at the root |
| 9615 |
so it overlays the whole form regardless of which section is |
| 9616 |
currently active. */} |
| 9617 |
<AutoFillTripModal |
| 9618 |
open={aiModalOpen} |
| 9619 |
onClose={() => setAiModalOpen(false)} |
| 9620 |
buildContext={() => buildTripAiContext(formData)} |
| 9621 |
itineraryOnly={aiModalMode === "itinerary"} |
| 9622 |
onFieldsAccepted={(updates) => { |
| 9623 |
for (const [key, value] of Object.entries(updates)) { |
| 9624 |
handleFieldChange(key as any, value as any); |
| 9625 |
} |
| 9626 |
}} |
| 9627 |
/> |
| 9628 |
{/* Same modal as the standalone Itinerary page. We mount it |
| 9629 |
here so the Itinerary tab's "Build with AI" button can drive |
| 9630 |
the SAME agent + applyItinerary flow that the other two |
| 9631 |
surfaces use, instead of forking through AutoFillTripModal's |
| 9632 |
single-shot legacy parser. */} |
| 9633 |
{isEditMode && tripId && ( |
| 9634 |
<BuildItineraryModal |
| 9635 |
open={itineraryBuildOpen} |
| 9636 |
onClose={() => setItineraryBuildOpen(false)} |
| 9637 |
tripId={tripId} |
| 9638 |
tripName={(formData as any)?.title || __("Trip", "yatra")} |
| 9639 |
tripDurationDays={Number((formData as any)?.duration_days) || 0} |
| 9640 |
onApplied={() => { |
| 9641 |
// Persistence happened server-side via applyItinerary; the |
| 9642 |
// trip form's local itinerary state needs to refresh so the |
| 9643 |
// newly-created day rows show up. The TripForm reloads |
| 9644 |
// itinerary data on a routing event — we just close the |
| 9645 |
// modal and let the user navigate / save normally. |
| 9646 |
setItineraryBuildOpen(false); |
| 9647 |
}} |
| 9648 |
/> |
| 9649 |
)} |
| 9650 |
</div> |
| 9651 |
); |
| 9652 |
}; |
| 9653 |
|
| 9654 |
export default TripForm; |
| 9655 |
|