| @@ -47,9 +47,9 @@ | ||
| 47 | 47 | BarChart3, |
| 48 | 48 | Palette, |
| 49 | 49 | Percent, |
| 50 | 50 | } from "lucide-react"; |
| 51 | -import { __ } from "../lib/i18n"; | |
| 51 | +import { __, sprintf } from "../lib/i18n"; | |
| 52 | 52 | import { prepareWordPressMediaFrameOpen } from "../lib/wp-media-open"; |
| 53 | 53 | import { usePermissions } from "../hooks/usePermissions"; |
| 54 | 54 | import { useToast } from "../components/ui/toast"; |
| 55 | 55 | import { navigateMenu } from "../hooks/useNavigate"; |
| @@ -92,8 +92,20 @@ | ||
| 92 | 92 | } from "../lib/frontend-permalink-urls"; |
| 93 | 93 | import { getCurrencyOptions } from "../data/currencies"; |
| 94 | 94 | import { SearchableSelect } from "../components/ui/searchable-select"; |
| 95 | 95 | import { MultiSelect, MultiSelectOption } from "../components/ui/multi-select"; |
| 96 | +import type { | |
| 97 | + BookingFormConfig, | |
| 98 | + FormCondition, | |
| 99 | + FormFieldConfig, | |
| 100 | + FormSectionConfig, | |
| 101 | +} from "../components/settings/booking-form-types"; | |
| 102 | +import { BookingFormFieldsEditor } from "../components/settings/BookingFormFieldsEditor"; | |
| 103 | +import { | |
| 104 | + BookingFormConditionsModal, | |
| 105 | + describeTargets, | |
| 106 | + useConditionTargets, | |
| 107 | +} from "../components/settings/BookingFormConditionsModal"; | |
| 96 | 108 | |
| 97 | 109 | // Multiple Taxes Editor Component |
| 98 | 110 | const MultipleTaxesEditor = React.memo( |
| 99 | 111 | ({ |
| @@ -725,58 +737,10 @@ | ||
| 725 | 737 | | "seo" |
| 726 | 738 | | "advanced"; |
| 727 | 739 | |
| 728 | 740 | // Form Builder Types |
| 729 | -interface FormFieldConfig { | |
| 730 | - id: string; | |
| 731 | - type: | |
| 732 | - | "text" | |
| 733 | - | "email" | |
| 734 | - | "tel" | |
| 735 | - | "date" | |
| 736 | - | "select" | |
| 737 | - | "country" | |
| 738 | - | "textarea" | |
| 739 | - | "checkbox" | |
| 740 | - | "number" | |
| 741 | - | "text_block"; | |
| 742 | - label: string; | |
| 743 | - placeholder: string; | |
| 744 | - required: boolean; | |
| 745 | - enabled: boolean; | |
| 746 | - order: number; | |
| 747 | - width: "full" | "half" | "third"; | |
| 748 | - section?: string; | |
| 749 | - options?: { value: string; label: string }[]; | |
| 750 | - content?: string; // For text_block: the display-only content (safe HTML) | |
| 751 | - locked?: boolean; // If true, field cannot be deleted and required cannot be changed | |
| 752 | - /** | |
| 753 | - * Traveler-section only: which travelers this field is shown to. | |
| 754 | - * "all" (default) = every traveler; "lead" = the lead traveler (Traveler 1) only. | |
| 755 | - * Absent is treated as "all" for backward compatibility. | |
| 756 | - */ | |
| 757 | - applies_to?: "all" | "lead"; | |
| 758 | - /** | |
| 759 | - * Phone (tel) fields only: show the international country-code selector | |
| 760 | - * (flag + dial code) on the input. Absent is treated as `true` (on) for | |
| 761 | - * backward compatibility, so existing forms keep the widget without a re-save. | |
| 762 | - */ | |
| 763 | - show_country_code?: boolean; | |
| 764 | -} | |
| 765 | - | |
| 766 | -interface FormSectionConfig { | |
| 767 | - title: string; | |
| 768 | - description: string; | |
| 769 | - enabled?: boolean; | |
| 770 | - fields: FormFieldConfig[]; | |
| 771 | -} | |
| 772 | - | |
| 773 | -interface BookingFormConfig { | |
| 774 | - contact_form: FormSectionConfig; | |
| 775 | - emergency_contact_form: FormSectionConfig; | |
| 776 | - traveler_form: FormSectionConfig; | |
| 777 | -} | |
| 778 | - | |
| 741 | +// Booking form builder types live in components/settings/booking-form-types.ts | |
| 742 | +// (shared with the per-trip Conditions popup). | |
| 779 | 743 | interface PaymentGatewayConfig { |
| 780 | 744 | enabled: boolean; |
| 781 | 745 | icon?: string; |
| 782 | 746 | title?: string; |
| @@ -836,8 +800,10 @@ | ||
| 836 | 800 | /** Hex brand color for public trip/booking/listing UI (buttons, links, accents) */ |
| 837 | 801 | frontend_primary_color: string; |
| 838 | 802 | /** CSS max width for Yatra containers; empty = follow theme / theme.json */ |
| 839 | 803 | frontend_container_max_width: string; |
| 804 | + /** Trip listing card density: "standard" | "compact_mobile" | "compact_all" */ | |
| 805 | + frontend_listing_card_layout: string; | |
| 840 | 806 | |
| 841 | 807 | // Google Calendar Settings |
| 842 | 808 | google_calendar_client_id?: string; |
| 843 | 809 | google_calendar_client_secret?: string; |
| @@ -852,13 +818,15 @@ | ||
| 852 | 818 | search_show_destination: boolean; |
| 853 | 819 | search_show_activities: boolean; |
| 854 | 820 | search_show_duration: boolean; |
| 855 | 821 | search_show_budget: boolean; |
| 822 | + search_show_date: boolean; | |
| 856 | 823 | collapse_filters_on_mobile: boolean; |
| 857 | 824 | |
| 858 | 825 | // Booking Settings |
| 859 | 826 | booking_confirmation: boolean; |
| 860 | 827 | auto_confirm_bookings: boolean; |
| 828 | + auto_confirm_mode: "none" | "online" | "all"; | |
| 861 | 829 | require_login: boolean; |
| 862 | 830 | allow_guest_checkout: boolean; |
| 863 | 831 | require_guest_email_verification: boolean; |
| 864 | 832 | booking_expiry_hours: number; |
| @@ -882,8 +850,10 @@ | ||
| 882 | 850 | scheduled_payment_days: number; |
| 883 | 851 | scheduled_payment_installments: number; |
| 884 | 852 | scheduled_payment_interval: number; |
| 885 | 853 | scheduled_payment_reminder_days: number; |
| 854 | + balance_anchor: string; | |
| 855 | + balance_due_days: number; | |
| 886 | 856 | gateway_configs: Record<string, PaymentGatewayConfig>; |
| 887 | 857 | gateway_order?: string[]; |
| 888 | 858 | |
| 889 | 859 | // Pricing Settings |
| @@ -991,8 +961,10 @@ | ||
| 991 | 961 | /** Wishlist / saved trips (Yatra Pro); toggled in Customer settings */ |
| 992 | 962 | enable_wishlist?: boolean; |
| 993 | 963 | /** Keep sold-out departure dates visible on the storefront (default true) */ |
| 994 | 964 | show_sold_out?: boolean; |
| 965 | + /** How many months ahead customers can see and book (1–36, default 12) */ | |
| 966 | + availability_horizon_months?: number; | |
| 995 | 967 | |
| 996 | 968 | // Review Settings |
| 997 | 969 | enable_reviews: boolean; |
| 998 | 970 | require_booking: boolean; |
| @@ -1115,8 +1087,10 @@ | ||
| 1115 | 1087 | seo_trip_meta_description: string; |
| 1116 | 1088 | seo_trip_meta_keywords: string; |
| 1117 | 1089 | seo_trip_meta_image: number; |
| 1118 | 1090 | enable_sitemap: boolean; |
| 1091 | + sitemap_types: string[]; | |
| 1092 | + sitemap_noindex_excluded: boolean; | |
| 1119 | 1093 | } |
| 1120 | 1094 | |
| 1121 | 1095 | // Form Builder Component |
| 1122 | 1096 | type BookingFormSubTab = |
| @@ -1151,11 +1125,17 @@ | ||
| 1151 | 1125 | }) => { |
| 1152 | 1126 | const [activeFormTab, setActiveFormTab] = |
| 1153 | 1127 | useState<BookingFormSubTab>(getInitialFormSubTab); |
| 1154 | 1128 | |
| 1155 | - // Check if Dynamic Form Field module is enabled via modules API | |
| 1129 | + // Check if Dynamic Form Field module is enabled via modules API. | |
| 1130 | + // NOTE: the key must differ from useModulesQuery()'s ["modules"] — this | |
| 1131 | + // query caches the RAW { success, data } envelope, and the Modules page | |
| 1132 | + // reads the same cache entry expecting a plain array. Sharing the key made | |
| 1133 | + // Modules crash with "modules.forEach is not a function" after visiting | |
| 1134 | + // Settings → Booking Form. The "modules" prefix keeps it invalidated by the | |
| 1135 | + // module toggles. | |
| 1156 | 1136 | const { data: modulesData } = useQuery({ |
| 1157 | - queryKey: ["modules"], | |
| 1137 | + queryKey: ["modules", "raw-envelope"], | |
| 1158 | 1138 | queryFn: async () => { |
| 1159 | 1139 | const response = await apiService.getModules(); |
| 1160 | 1140 | return response; |
| 1161 | 1141 | }, |
| @@ -1177,8 +1157,27 @@ | ||
| 1177 | 1157 | // Discount Stacking tab visibility earlier this session. |
| 1178 | 1158 | return !!window.yatraAdmin?.dynamicFormFieldEnabled; |
| 1179 | 1159 | }, [modulesData]); |
| 1180 | 1160 | |
| 1161 | + // Per-trip "Conditions" popup for the active form. Needs the module on AND a | |
| 1162 | + // Pro build that resolves conditions at checkout (older Pro builds set the | |
| 1163 | + // module flag but not this one — offering the popup there would save rules | |
| 1164 | + // that never apply). Target names (trips / categories) are only fetched when | |
| 1165 | + // there is something to label. | |
| 1166 | + const conditionsSupported = | |
| 1167 | + isDynamicFormFieldEnabled && | |
| 1168 | + !!window.yatraAdmin?.bookingFormConditionsEnabled; | |
| 1169 | + const [conditionsOpen, setConditionsOpen] = useState(false); | |
| 1170 | + const hasAnyConditions = React.useMemo(() => { | |
| 1171 | + const cfg = formData?.booking_form_config; | |
| 1172 | + return !!cfg && Object.values(cfg).some( | |
| 1173 | + (s: any) => Array.isArray(s?.conditions) && s.conditions.length > 0, | |
| 1174 | + ); | |
| 1175 | + }, [formData?.booking_form_config]); | |
| 1176 | + const { options: conditionTargetOptions } = useConditionTargets( | |
| 1177 | + conditionsSupported && hasAnyConditions, | |
| 1178 | + ); | |
| 1179 | + | |
| 1181 | 1180 | // Save sub-tab to localStorage when it changes |
| 1182 | 1181 | const handleSubTabChange = (tab: BookingFormSubTab) => { |
| 1183 | 1182 | setActiveFormTab(tab); |
| 1184 | 1183 | if (typeof window !== "undefined") { |
| @@ -1184,32 +1183,8 @@ | ||
| 1184 | 1183 | if (typeof window !== "undefined") { |
| 1185 | 1184 | localStorage.setItem("yatra_settings_booking_form_subtab", tab); |
| 1186 | 1185 | } |
| 1187 | 1186 | }; |
| 1188 | - const [editingField, setEditingField] = useState<string | null>(null); | |
| 1189 | - // Draft for the Field ID input while editing an existing field. The field's | |
| 1190 | - // `id` is its React key AND the value `editingField` tracks, so mutating it on | |
| 1191 | - // every keystroke remounts the row (focus loss) and breaks the open-editor | |
| 1192 | - // match (editor closes). We edit a local draft and commit once on blur/Enter. | |
| 1193 | - const [fieldIdDraft, setFieldIdDraft] = useState<string | null>(null); | |
| 1194 | - // Clear any pending Field ID draft whenever the edited field changes | |
| 1195 | - // (open / close / switch) so a draft can never leak onto another field. | |
| 1196 | - useEffect(() => { | |
| 1197 | - setFieldIdDraft(null); | |
| 1198 | - }, [editingField]); | |
| 1199 | - const [showAddField, setShowAddField] = useState(false); | |
| 1200 | - const [newField, setNewField] = useState<Partial<FormFieldConfig>>({ | |
| 1201 | - id: "", | |
| 1202 | - type: "text", | |
| 1203 | - label: "", | |
| 1204 | - placeholder: "", | |
| 1205 | - required: false, | |
| 1206 | - enabled: true, | |
| 1207 | - width: "full", | |
| 1208 | - options: [], | |
| 1209 | - applies_to: "all", | |
| 1210 | - }); | |
| 1211 | - | |
| 1212 | 1187 | // Traveler-section "Applies to" choices (only shown on the Traveler tab). |
| 1213 | 1188 | const appliesToOptions: { value: "all" | "lead"; label: string }[] = [ |
| 1214 | 1189 | { value: "all", label: __("All travelers", "yatra") }, |
| 1215 | 1190 | { value: "lead", label: __("Lead traveler only", "yatra") }, |
| @@ -1214,53 +1189,9 @@ | ||
| 1214 | 1189 | { value: "all", label: __("All travelers", "yatra") }, |
| 1215 | 1190 | { value: "lead", label: __("Lead traveler only", "yatra") }, |
| 1216 | 1191 | ]; |
| 1217 | 1192 | |
| 1218 | - // Delete confirmation state | |
| 1219 | - const [deleteConfirm, setDeleteConfirm] = useState<{ | |
| 1220 | - isOpen: boolean; | |
| 1221 | - fieldId: string | null; | |
| 1222 | - fieldLabel: string; | |
| 1223 | - }>({ | |
| 1224 | - isOpen: false, | |
| 1225 | - fieldId: null, | |
| 1226 | - fieldLabel: "", | |
| 1227 | - }); | |
| 1228 | 1193 | |
| 1229 | - // Drag and drop state | |
| 1230 | - const [draggedFieldId, setDraggedFieldId] = useState<string | null>(null); | |
| 1231 | - const [dragOverFieldId, setDragOverFieldId] = useState<string | null>(null); | |
| 1232 | - | |
| 1233 | - // Helper to generate ID from label | |
| 1234 | - const generateIdFromLabel = (label: string): string => { | |
| 1235 | - return label | |
| 1236 | - .toLowerCase() | |
| 1237 | - .replace(/\s+/g, "_") | |
| 1238 | - .replace(/[^a-z0-9_]/g, ""); | |
| 1239 | - }; | |
| 1240 | - | |
| 1241 | - // Helper to sanitize ID input | |
| 1242 | - const sanitizeId = (id: string): string => { | |
| 1243 | - return id | |
| 1244 | - .toLowerCase() | |
| 1245 | - .replace(/\s+/g, "_") | |
| 1246 | - .replace(/[^a-z0-9_]/g, ""); | |
| 1247 | - }; | |
| 1248 | - | |
| 1249 | - // Handle label change with auto ID generation | |
| 1250 | - const handleNewFieldLabelChange = (label: string) => { | |
| 1251 | - const autoId = generateIdFromLabel(label); | |
| 1252 | - setNewField((prev) => ({ | |
| 1253 | - ...prev, | |
| 1254 | - label, | |
| 1255 | - // Only auto-generate ID if it hasn't been manually edited | |
| 1256 | - id: | |
| 1257 | - prev.id === "" || prev.id === generateIdFromLabel(prev.label || "") | |
| 1258 | - ? autoId | |
| 1259 | - : prev.id, | |
| 1260 | - })); | |
| 1261 | - }; | |
| 1262 | - | |
| 1263 | 1194 | const formTabs = [ |
| 1264 | 1195 | { |
| 1265 | 1196 | id: "contact_form" as const, |
| 1266 | 1197 | label: __("Contact Form", "yatra"), |
| @@ -1342,191 +1273,9 @@ | ||
| 1342 | 1273 | }; |
| 1343 | 1274 | }); |
| 1344 | 1275 | }; |
| 1345 | 1276 | |
| 1346 | - const updateField = (fieldId: string, updates: Partial<FormFieldConfig>) => { | |
| 1347 | - const currentConfig = getCurrentFormConfig(); | |
| 1348 | - const updatedFields = currentConfig.fields.map((field) => | |
| 1349 | - field.id === fieldId ? { ...field, ...updates } : field, | |
| 1350 | - ); | |
| 1351 | - updateFormConfig({ fields: updatedFields }); | |
| 1352 | - }; | |
| 1353 | 1277 | |
| 1354 | - const toggleFieldEnabled = (fieldId: string) => { | |
| 1355 | - const currentConfig = getCurrentFormConfig(); | |
| 1356 | - const field = currentConfig.fields.find((f) => f.id === fieldId); | |
| 1357 | - // Locked fields cannot be disabled | |
| 1358 | - if (field && !field.locked) { | |
| 1359 | - updateField(fieldId, { enabled: !field.enabled }); | |
| 1360 | - } | |
| 1361 | - }; | |
| 1362 | - | |
| 1363 | - const toggleFieldRequired = (fieldId: string) => { | |
| 1364 | - const currentConfig = getCurrentFormConfig(); | |
| 1365 | - const field = currentConfig.fields.find((f) => f.id === fieldId); | |
| 1366 | - if (field) { | |
| 1367 | - updateField(fieldId, { required: !field.required }); | |
| 1368 | - } | |
| 1369 | - }; | |
| 1370 | - | |
| 1371 | - const moveField = (fieldId: string, direction: "up" | "down") => { | |
| 1372 | - const currentConfig = getCurrentFormConfig(); | |
| 1373 | - const fields = [...currentConfig.fields]; | |
| 1374 | - const index = fields.findIndex((f) => f.id === fieldId); | |
| 1375 | - | |
| 1376 | - if (direction === "up" && index > 0) { | |
| 1377 | - [fields[index - 1], fields[index]] = [fields[index], fields[index - 1]]; | |
| 1378 | - } else if (direction === "down" && index < fields.length - 1) { | |
| 1379 | - [fields[index], fields[index + 1]] = [fields[index + 1], fields[index]]; | |
| 1380 | - } | |
| 1381 | - | |
| 1382 | - // Update order values | |
| 1383 | - fields.forEach((field, i) => { | |
| 1384 | - field.order = i + 1; | |
| 1385 | - }); | |
| 1386 | - | |
| 1387 | - updateFormConfig({ fields }); | |
| 1388 | - }; | |
| 1389 | - | |
| 1390 | - // Drag and Drop handlers | |
| 1391 | - const handleDragStart = (e: React.DragEvent, fieldId: string) => { | |
| 1392 | - setDraggedFieldId(fieldId); | |
| 1393 | - e.dataTransfer.effectAllowed = "move"; | |
| 1394 | - }; | |
| 1395 | - | |
| 1396 | - const handleDragOver = (e: React.DragEvent, fieldId: string) => { | |
| 1397 | - e.preventDefault(); | |
| 1398 | - e.dataTransfer.dropEffect = "move"; | |
| 1399 | - if (fieldId !== draggedFieldId) { | |
| 1400 | - setDragOverFieldId(fieldId); | |
| 1401 | - } | |
| 1402 | - }; | |
| 1403 | - | |
| 1404 | - const handleDragLeave = () => { | |
| 1405 | - setDragOverFieldId(null); | |
| 1406 | - }; | |
| 1407 | - | |
| 1408 | - const handleDrop = (e: React.DragEvent, targetFieldId: string) => { | |
| 1409 | - e.preventDefault(); | |
| 1410 | - if (!draggedFieldId || draggedFieldId === targetFieldId) { | |
| 1411 | - setDraggedFieldId(null); | |
| 1412 | - setDragOverFieldId(null); | |
| 1413 | - return; | |
| 1414 | - } | |
| 1415 | - | |
| 1416 | - const currentConfig = getCurrentFormConfig(); | |
| 1417 | - const fields = [...currentConfig.fields]; | |
| 1418 | - const draggedIndex = fields.findIndex((f) => f.id === draggedFieldId); | |
| 1419 | - const targetIndex = fields.findIndex((f) => f.id === targetFieldId); | |
| 1420 | - | |
| 1421 | - if (draggedIndex !== -1 && targetIndex !== -1) { | |
| 1422 | - const [draggedField] = fields.splice(draggedIndex, 1); | |
| 1423 | - fields.splice(targetIndex, 0, draggedField); | |
| 1424 | - | |
| 1425 | - // Update order values | |
| 1426 | - fields.forEach((field, i) => { | |
| 1427 | - field.order = i + 1; | |
| 1428 | - }); | |
| 1429 | - | |
| 1430 | - updateFormConfig({ fields }); | |
| 1431 | - } | |
| 1432 | - | |
| 1433 | - setDraggedFieldId(null); | |
| 1434 | - setDragOverFieldId(null); | |
| 1435 | - }; | |
| 1436 | - | |
| 1437 | - const handleDragEnd = () => { | |
| 1438 | - setDraggedFieldId(null); | |
| 1439 | - setDragOverFieldId(null); | |
| 1440 | - }; | |
| 1441 | - | |
| 1442 | - const deleteField = (fieldId: string) => { | |
| 1443 | - const currentConfig = getCurrentFormConfig(); | |
| 1444 | - const updatedFields = currentConfig.fields.filter((f) => f.id !== fieldId); | |
| 1445 | - updatedFields.forEach((field, i) => { | |
| 1446 | - field.order = i + 1; | |
| 1447 | - }); | |
| 1448 | - updateFormConfig({ fields: updatedFields }); | |
| 1449 | - setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" }); | |
| 1450 | - }; | |
| 1451 | - | |
| 1452 | - const addNewField = () => { | |
| 1453 | - const isTextBlock = newField.type === "text_block"; | |
| 1454 | - const currentConfig = getCurrentFormConfig(); | |
| 1455 | - | |
| 1456 | - let fieldId: string; | |
| 1457 | - if (isTextBlock) { | |
| 1458 | - // Display-only block: the admin supplies only content — no label or id. | |
| 1459 | - // Generate a unique internal id so it can be ordered/stored like any field. | |
| 1460 | - if (!newField.content || !newField.content.trim()) return; | |
| 1461 | - let n = 1; | |
| 1462 | - while (currentConfig.fields.some((f) => f.id === `text_block_${n}`)) n++; | |
| 1463 | - fieldId = `text_block_${n}`; | |
| 1464 | - } else { | |
| 1465 | - if (!newField.label || !newField.id) return; | |
| 1466 | - fieldId = sanitizeId(newField.id); | |
| 1467 | - } | |
| 1468 | - | |
| 1469 | - // Check if ID already exists | |
| 1470 | - if (currentConfig.fields.some((f) => f.id === fieldId)) { | |
| 1471 | - // TODO: Replace with toast notification when refactoring this nested component | |
| 1472 | - window.alert( | |
| 1473 | - "A field with this ID already exists. Please use a different ID.", | |
| 1474 | - ); | |
| 1475 | - return; | |
| 1476 | - } | |
| 1477 | - | |
| 1478 | - const newFieldConfig: FormFieldConfig = { | |
| 1479 | - id: fieldId, | |
| 1480 | - type: (newField.type as FormFieldConfig["type"]) || "text", | |
| 1481 | - label: newField.label || "", | |
| 1482 | - placeholder: newField.placeholder || "", | |
| 1483 | - required: newField.required || false, | |
| 1484 | - enabled: true, | |
| 1485 | - order: currentConfig.fields.length + 1, | |
| 1486 | - width: (newField.width as FormFieldConfig["width"]) || "full", | |
| 1487 | - // Per-traveler targeting only applies to the Traveler section. | |
| 1488 | - ...(activeFormTab === "traveler_form" | |
| 1489 | - ? { applies_to: newField.applies_to || "all" } | |
| 1490 | - : {}), | |
| 1491 | - // Phone fields carry the country-code toggle (default on). | |
| 1492 | - ...(newField.type === "tel" | |
| 1493 | - ? { show_country_code: newField.show_country_code !== false } | |
| 1494 | - : {}), | |
| 1495 | - }; | |
| 1496 | - | |
| 1497 | - // Add options if field type is select | |
| 1498 | - if ( | |
| 1499 | - newField.type === "select" && | |
| 1500 | - newField.options && | |
| 1501 | - newField.options.length > 0 | |
| 1502 | - ) { | |
| 1503 | - newFieldConfig.options = newField.options.filter( | |
| 1504 | - (opt) => opt.value && opt.label, | |
| 1505 | - ); | |
| 1506 | - } | |
| 1507 | - | |
| 1508 | - // Text block: keep its display content; it can never be a required input. | |
| 1509 | - if (isTextBlock) { | |
| 1510 | - newFieldConfig.content = newField.content || ""; | |
| 1511 | - newFieldConfig.required = false; | |
| 1512 | - } | |
| 1513 | - | |
| 1514 | - updateFormConfig({ fields: [...currentConfig.fields, newFieldConfig] }); | |
| 1515 | - setNewField({ | |
| 1516 | - id: "", | |
| 1517 | - type: "text", | |
| 1518 | - label: "", | |
| 1519 | - placeholder: "", | |
| 1520 | - required: false, | |
| 1521 | - enabled: true, | |
| 1522 | - width: "full", | |
| 1523 | - options: [], | |
| 1524 | - content: "", | |
| 1525 | - }); | |
| 1526 | - setShowAddField(false); | |
| 1527 | - }; | |
| 1528 | - | |
| 1529 | 1278 | const currentConfig = getCurrentFormConfig(); |
| 1530 | 1279 | |
| 1531 | 1280 | return ( |
| 1532 | 1281 | <div className="space-y-6"> |
| @@ -1575,11 +1324,35 @@ | ||
| 1575 | 1324 | |
| 1576 | 1325 | {/* Form Section Settings */} |
| 1577 | 1326 | <Card> |
| 1578 | 1327 | <CardHeader> |
| 1579 | - <CardTitle className="text-base"> | |
| 1580 | - {__("Form Section Settings", "yatra")} | |
| 1581 | - </CardTitle> | |
| 1328 | + <div className="flex flex-wrap items-center justify-between gap-2"> | |
| 1329 | + <CardTitle className="text-base"> | |
| 1330 | + {__("Form Section Settings", "yatra")} | |
| 1331 | + </CardTitle> | |
| 1332 | + {conditionsSupported && ( | |
| 1333 | + <Button | |
| 1334 | + type="button" | |
| 1335 | + variant="outline" | |
| 1336 | + size="sm" | |
| 1337 | + onClick={() => setConditionsOpen(true)} | |
| 1338 | + className="border-purple-300 text-purple-700 hover:bg-purple-50 dark:border-purple-700 dark:text-purple-300 dark:hover:bg-purple-900/20" | |
| 1339 | + title={__( | |
| 1340 | + "Give some trips their own version of this form", | |
| 1341 | + "yatra", | |
| 1342 | + )} | |
| 1343 | + data-testid="open-conditions" | |
| 1344 | + > | |
| 1345 | + <SlidersHorizontal className="w-4 h-4 mr-1.5" /> | |
| 1346 | + {__("Conditions", "yatra")} | |
| 1347 | + {(currentConfig.conditions?.length ?? 0) > 0 && ( | |
| 1348 | + <span className="ml-1.5 text-[11px] px-1.5 py-0.5 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300 font-semibold"> | |
| 1349 | + {currentConfig.conditions?.length} | |
| 1350 | + </span> | |
| 1351 | + )} | |
| 1352 | + </Button> | |
| 1353 | + )} | |
| 1354 | + </div> | |
| 1582 | 1355 | </CardHeader> |
| 1583 | 1356 | <CardContent className="space-y-4"> |
| 1584 | 1357 | <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 1585 | 1358 | <input |
| @@ -1626,849 +1399,85 @@ | ||
| 1626 | 1399 | className="mt-1" |
| 1627 | 1400 | /> |
| 1628 | 1401 | </div> |
| 1629 | 1402 | </div> |
| 1630 | - </CardContent> | |
| 1631 | - </Card> | |
| 1632 | 1403 | |
| 1633 | - {/* Form Fields */} | |
| 1634 | - <Card> | |
| 1635 | - <CardHeader className="flex flex-row items-center justify-between"> | |
| 1636 | - <CardTitle className="text-base"> | |
| 1637 | - {__("Form Fields", "yatra")} | |
| 1638 | - </CardTitle> | |
| 1639 | - <Button | |
| 1640 | - variant="outline" | |
| 1641 | - size="sm" | |
| 1642 | - onClick={() => setShowAddField(!showAddField)} | |
| 1643 | - className="flex items-center gap-2" | |
| 1644 | - > | |
| 1645 | - <Plus className="w-4 h-4" /> | |
| 1646 | - {__("Add Field", "yatra")} | |
| 1647 | - </Button> | |
| 1648 | - </CardHeader> | |
| 1649 | - <CardContent className="space-y-3"> | |
| 1650 | - {/* Add New Field Form */} | |
| 1651 | - {showAddField && ( | |
| 1652 | - <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 mb-4"> | |
| 1653 | - <h4 className="font-medium text-sm mb-3"> | |
| 1654 | - {__("Add New Field", "yatra")} | |
| 1655 | - </h4> | |
| 1656 | - <div className="grid grid-cols-1 md:grid-cols-5 gap-3"> | |
| 1657 | - <div> | |
| 1658 | - <Label className="text-xs"> | |
| 1659 | - {__("Field Type", "yatra")} | |
| 1660 | - </Label> | |
| 1661 | - <Select | |
| 1662 | - value={newField.type || "text"} | |
| 1663 | - onChange={(e) => | |
| 1664 | - setNewField((prev) => ({ | |
| 1665 | - ...prev, | |
| 1666 | - type: e.target.value as FormFieldConfig["type"], | |
| 1667 | - })) | |
| 1668 | - } | |
| 1669 | - className="mt-1" | |
| 1404 | + {/* Per-trip conditions summary (Pro) */} | |
| 1405 | + {conditionsSupported && | |
| 1406 | + (currentConfig.conditions?.length ?? 0) > 0 && ( | |
| 1407 | + <div className="space-y-2" data-testid="conditions-summary"> | |
| 1408 | + {currentConfig.conditions?.map((c, i) => ( | |
| 1409 | + <div | |
| 1410 | + key={c.id || i} | |
| 1411 | + className="flex flex-wrap items-center gap-2 px-3 py-2 rounded-md border border-dashed border-gray-300 dark:border-gray-600 text-sm" | |
| 1670 | 1412 | > |
| 1671 | - {fieldTypes.map((type) => ( | |
| 1672 | - <option key={type.value} value={type.value}> | |
| 1673 | - {type.label} | |
| 1674 | - </option> | |
| 1675 | - ))} | |
| 1676 | - </Select> | |
| 1677 | - </div> | |
| 1678 | - {newField.type !== "text_block" && ( | |
| 1679 | - <div> | |
| 1680 | - <Label className="text-xs"> | |
| 1681 | - {__("Label", "yatra")} * | |
| 1682 | - </Label> | |
| 1683 | - <Input | |
| 1684 | - value={newField.label || ""} | |
| 1685 | - onChange={(e) => | |
| 1686 | - handleNewFieldLabelChange(e.target.value) | |
| 1687 | - } | |
| 1688 | - placeholder="Field label" | |
| 1689 | - className="mt-1" | |
| 1690 | - /> | |
| 1691 | - </div> | |
| 1692 | - )} | |
| 1693 | - {newField.type !== "text_block" && ( | |
| 1694 | - <div> | |
| 1695 | - <Label className="text-xs"> | |
| 1696 | - {__("Field ID", "yatra")} * | |
| 1697 | - </Label> | |
| 1698 | - <Input | |
| 1699 | - value={newField.id || ""} | |
| 1700 | - onChange={(e) => | |
| 1701 | - setNewField((prev) => ({ | |
| 1702 | - ...prev, | |
| 1703 | - id: sanitizeId(e.target.value), | |
| 1704 | - })) | |
| 1705 | - } | |
| 1706 | - placeholder="field_id" | |
| 1707 | - className="mt-1 font-mono text-xs" | |
| 1708 | - /> | |
| 1709 | - <p className="text-[10px] text-gray-400 mt-0.5"> | |
| 1710 | - {__("Lowercase, no spaces", "yatra")} | |
| 1711 | - </p> | |
| 1712 | - </div> | |
| 1713 | - )} | |
| 1714 | - {newField.type !== "text_block" && ( | |
| 1715 | - <div> | |
| 1716 | - <Label className="text-xs"> | |
| 1717 | - {__("Placeholder", "yatra")} | |
| 1718 | - </Label> | |
| 1719 | - <Input | |
| 1720 | - value={newField.placeholder || ""} | |
| 1721 | - onChange={(e) => | |
| 1722 | - setNewField((prev) => ({ | |
| 1723 | - ...prev, | |
| 1724 | - placeholder: e.target.value, | |
| 1725 | - })) | |
| 1726 | - } | |
| 1727 | - placeholder="Placeholder text" | |
| 1728 | - className="mt-1" | |
| 1729 | - /> | |
| 1730 | - </div> | |
| 1731 | - )} | |
| 1732 | - <div> | |
| 1733 | - <Label className="text-xs">{__("Width", "yatra")}</Label> | |
| 1734 | - <Select | |
| 1735 | - value={newField.width || "full"} | |
| 1736 | - onChange={(e) => | |
| 1737 | - setNewField((prev) => ({ | |
| 1738 | - ...prev, | |
| 1739 | - width: e.target.value as FormFieldConfig["width"], | |
| 1740 | - })) | |
| 1741 | - } | |
| 1742 | - className="mt-1" | |
| 1743 | - > | |
| 1744 | - {widthOptions.map((w) => ( | |
| 1745 | - <option key={w.value} value={w.value}> | |
| 1746 | - {w.label} | |
| 1747 | - </option> | |
| 1748 | - ))} | |
| 1749 | - </Select> | |
| 1750 | - </div> | |
| 1751 | - {activeFormTab === "traveler_form" && ( | |
| 1752 | - <div> | |
| 1753 | - <Label className="text-xs"> | |
| 1754 | - {__("Applies to", "yatra")} | |
| 1755 | - </Label> | |
| 1756 | - <Select | |
| 1757 | - value={newField.applies_to || "all"} | |
| 1758 | - onChange={(e) => | |
| 1759 | - setNewField((prev) => ({ | |
| 1760 | - ...prev, | |
| 1761 | - applies_to: e.target | |
| 1762 | - .value as FormFieldConfig["applies_to"], | |
| 1763 | - })) | |
| 1764 | - } | |
| 1765 | - className="mt-1" | |
| 1766 | - > | |
| 1767 | - {appliesToOptions.map((a) => ( | |
| 1768 | - <option key={a.value} value={a.value}> | |
| 1769 | - {a.label} | |
| 1770 | - </option> | |
| 1771 | - ))} | |
| 1772 | - </Select> | |
| 1773 | - </div> | |
| 1774 | - )} | |
| 1775 | - </div> | |
| 1776 | - {/* Content editor for text blocks (display-only) */} | |
| 1777 | - {newField.type === "text_block" && ( | |
| 1778 | - <div className="mt-3"> | |
| 1779 | - <Label className="text-xs font-medium"> | |
| 1780 | - {__("Content", "yatra")} | |
| 1781 | - </Label> | |
| 1782 | - <textarea | |
| 1783 | - value={newField.content || ""} | |
| 1784 | - onChange={(e) => | |
| 1785 | - setNewField((prev) => ({ | |
| 1786 | - ...prev, | |
| 1787 | - content: e.target.value, | |
| 1788 | - })) | |
| 1789 | - } | |
| 1790 | - rows={4} | |
| 1791 | - placeholder={__( | |
| 1792 | - "Text shown to customers between fields. Basic HTML (bold, links, lists) is allowed.", | |
| 1793 | - "yatra", | |
| 1794 | - )} | |
| 1795 | - className="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm" | |
| 1796 | - /> | |
| 1797 | - <p className="text-[10px] text-gray-400 mt-0.5"> | |
| 1798 | - {__( | |
| 1799 | - "Display only — this is not an input and is not submitted by the customer.", | |
| 1800 | - "yatra", | |
| 1801 | - )} | |
| 1802 | - </p> | |
| 1803 | - </div> | |
| 1804 | - )} | |
| 1805 | - | |
| 1806 | - {/* Options editor for select fields */} | |
| 1807 | - {newField.type === "select" && ( | |
| 1808 | - <div className="mt-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-blue-100 dark:border-blue-900"> | |
| 1809 | - <div className="flex items-center justify-between mb-2"> | |
| 1810 | - <Label className="text-xs font-medium"> | |
| 1811 | - {__("Dropdown Options", "yatra")} | |
| 1812 | - </Label> | |
| 1413 | + <span className="text-gray-500">{__("On", "yatra")}</span> | |
| 1414 | + <span className="font-medium"> | |
| 1415 | + {describeTargets(c.targets, conditionTargetOptions) || | |
| 1416 | + __("(no trips selected)", "yatra")} | |
| 1417 | + </span> | |
| 1418 | + <span className="text-gray-500"> | |
| 1419 | + {sprintf( | |
| 1420 | + __("→ “%1$s” · %2$d fields", "yatra"), | |
| 1421 | + c.title, | |
| 1422 | + c.fields.filter((f) => f.enabled !== false).length, | |
| 1423 | + )} | |
| 1424 | + </span> | |
| 1425 | + <span className="flex-1" /> | |
| 1813 | 1426 | <Button |
| 1814 | 1427 | type="button" |
| 1815 | 1428 | variant="ghost" |
| 1816 | 1429 | size="sm" |
| 1817 | - onClick={() => { | |
| 1818 | - const currentOptions = newField.options || []; | |
| 1819 | - setNewField((prev) => ({ | |
| 1820 | - ...prev, | |
| 1821 | - options: [ | |
| 1822 | - ...currentOptions, | |
| 1823 | - { value: "", label: "" }, | |
| 1824 | - ], | |
| 1825 | - })); | |
| 1826 | - }} | |
| 1827 | - className="h-6 text-xs" | |
| 1430 | + onClick={() => setConditionsOpen(true)} | |
| 1828 | 1431 | > |
| 1829 | - <Plus className="w-3 h-3 mr-1" /> | |
| 1830 | - {__("Add Option", "yatra")} | |
| 1432 | + {__("Edit", "yatra")} | |
| 1831 | 1433 | </Button> |
| 1832 | 1434 | </div> |
| 1833 | - <div className="space-y-2"> | |
| 1834 | - {(newField.options || []).map((option, optIndex) => ( | |
| 1835 | - <div key={optIndex} className="flex items-center gap-2"> | |
| 1836 | - <Input | |
| 1837 | - value={option.value} | |
| 1838 | - onChange={(e) => { | |
| 1839 | - const newOptions = [...(newField.options || [])]; | |
| 1840 | - newOptions[optIndex] = { | |
| 1841 | - ...newOptions[optIndex], | |
| 1842 | - value: e.target.value, | |
| 1843 | - }; | |
| 1844 | - setNewField((prev) => ({ | |
| 1845 | - ...prev, | |
| 1846 | - options: newOptions, | |
| 1847 | - })); | |
| 1848 | - }} | |
| 1849 | - placeholder="Value (e.g., option1)" | |
| 1850 | - className="text-xs flex-1" | |
| 1851 | - /> | |
| 1852 | - <Input | |
| 1853 | - value={option.label} | |
| 1854 | - onChange={(e) => { | |
| 1855 | - const newOptions = [...(newField.options || [])]; | |
| 1856 | - newOptions[optIndex] = { | |
| 1857 | - ...newOptions[optIndex], | |
| 1858 | - label: e.target.value, | |
| 1859 | - }; | |
| 1860 | - setNewField((prev) => ({ | |
| 1861 | - ...prev, | |
| 1862 | - options: newOptions, | |
| 1863 | - })); | |
| 1864 | - }} | |
| 1865 | - placeholder="Label (e.g., Option 1)" | |
| 1866 | - className="text-xs flex-1" | |
| 1867 | - /> | |
| 1868 | - <button | |
| 1869 | - type="button" | |
| 1870 | - onClick={() => { | |
| 1871 | - const newOptions = ( | |
| 1872 | - newField.options || [] | |
| 1873 | - ).filter((_, i) => i !== optIndex); | |
| 1874 | - setNewField((prev) => ({ | |
| 1875 | - ...prev, | |
| 1876 | - options: newOptions, | |
| 1877 | - })); | |
| 1878 | - }} | |
| 1879 | - className="p-1 text-gray-400 hover:text-red-500" | |
| 1880 | - > | |
| 1881 | - <Trash2 className="w-3 h-3" /> | |
| 1882 | - </button> | |
| 1883 | - </div> | |
| 1884 | - ))} | |
| 1885 | - {(!newField.options || newField.options.length === 0) && ( | |
| 1886 | - <p className="text-xs text-gray-400 italic"> | |
| 1887 | - {__( | |
| 1888 | - 'Click "Add Option" to add dropdown choices.', | |
| 1889 | - "yatra", | |
| 1890 | - )} | |
| 1891 | - </p> | |
| 1892 | - )} | |
| 1893 | - </div> | |
| 1435 | + ))} | |
| 1436 | + <div className="flex flex-wrap items-center gap-2 px-3 py-2 rounded-md bg-gray-50 dark:bg-gray-800 text-sm"> | |
| 1437 | + <span className="font-medium"> | |
| 1438 | + {__("All other trips", "yatra")} | |
| 1439 | + </span> | |
| 1440 | + <span className="text-gray-500"> | |
| 1441 | + {__("use this form (below)", "yatra")} | |
| 1442 | + </span> | |
| 1894 | 1443 | </div> |
| 1895 | - )} | |
| 1896 | - | |
| 1897 | - <div className="flex items-center gap-4 mt-3"> | |
| 1898 | - {newField.type !== "text_block" && ( | |
| 1899 | - <label className="flex items-center gap-2 text-sm"> | |
| 1900 | - <input | |
| 1901 | - type="checkbox" | |
| 1902 | - checked={newField.required || false} | |
| 1903 | - onChange={(e) => | |
| 1904 | - setNewField((prev) => ({ | |
| 1905 | - ...prev, | |
| 1906 | - required: e.target.checked, | |
| 1907 | - })) | |
| 1908 | - } | |
| 1909 | - className="w-4 h-4 rounded border-gray-300 text-blue-600" | |
| 1910 | - /> | |
| 1911 | - {__("Required", "yatra")} | |
| 1912 | - </label> | |
| 1913 | - )} | |
| 1914 | - {newField.type === "tel" && ( | |
| 1915 | - <label className="flex items-center gap-2 text-sm"> | |
| 1916 | - <input | |
| 1917 | - type="checkbox" | |
| 1918 | - checked={newField.show_country_code !== false} | |
| 1919 | - onChange={(e) => | |
| 1920 | - setNewField((prev) => ({ | |
| 1921 | - ...prev, | |
| 1922 | - show_country_code: e.target.checked, | |
| 1923 | - })) | |
| 1924 | - } | |
| 1925 | - className="w-4 h-4 rounded border-gray-300 text-blue-600" | |
| 1926 | - /> | |
| 1927 | - {__("Show country code", "yatra")} | |
| 1928 | - </label> | |
| 1929 | - )} | |
| 1930 | - <div className="flex-1"></div> | |
| 1931 | - <Button | |
| 1932 | - variant="ghost" | |
| 1933 | - size="sm" | |
| 1934 | - onClick={() => setShowAddField(false)} | |
| 1935 | - > | |
| 1936 | - {__("Cancel", "yatra")} | |
| 1937 | - </Button> | |
| 1938 | - <Button | |
| 1939 | - size="sm" | |
| 1940 | - onClick={addNewField} | |
| 1941 | - disabled={ | |
| 1942 | - newField.type === "text_block" | |
| 1943 | - ? !newField.content || !newField.content.trim() | |
| 1944 | - : !newField.id || !newField.label | |
| 1945 | - } | |
| 1946 | - > | |
| 1947 | - {__("Add Field", "yatra")} | |
| 1948 | - </Button> | |
| 1949 | 1444 | </div> |
| 1950 | - </div> | |
| 1951 | - )} | |
| 1952 | - | |
| 1953 | - {/* Field List */} | |
| 1954 | - {currentConfig.fields?.length === 0 ? ( | |
| 1955 | - <div className="text-center py-8 text-gray-500 dark:text-gray-400"> | |
| 1956 | - <p> | |
| 1957 | - {__( | |
| 1958 | - 'No fields configured. Click "Add Field" to get started.', | |
| 1959 | - "yatra", | |
| 1960 | - )} | |
| 1961 | - </p> | |
| 1962 | - </div> | |
| 1963 | - ) : ( | |
| 1964 | - <div className="space-y-2"> | |
| 1965 | - {currentConfig.fields?.map((field, index) => ( | |
| 1966 | - <div | |
| 1967 | - key={field.id} | |
| 1968 | - draggable | |
| 1969 | - onDragStart={(e) => handleDragStart(e, field.id)} | |
| 1970 | - onDragOver={(e) => handleDragOver(e, field.id)} | |
| 1971 | - onDragLeave={handleDragLeave} | |
| 1972 | - onDrop={(e) => handleDrop(e, field.id)} | |
| 1973 | - onDragEnd={handleDragEnd} | |
| 1974 | - className={`flex items-center gap-3 p-3 rounded-lg border transition-all cursor-grab active:cursor-grabbing ${ | |
| 1975 | - draggedFieldId === field.id | |
| 1976 | - ? "opacity-50 border-dashed" | |
| 1977 | - : "" | |
| 1978 | - } ${ | |
| 1979 | - dragOverFieldId === field.id | |
| 1980 | - ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20" | |
| 1981 | - : "" | |
| 1982 | - } ${ | |
| 1983 | - field.enabled | |
| 1984 | - ? "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700" | |
| 1985 | - : "bg-gray-50 dark:bg-gray-900 border-gray-100 dark:border-gray-800 opacity-60" | |
| 1986 | - }`} | |
| 1987 | - > | |
| 1988 | - {/* Drag Handle & Order */} | |
| 1989 | - <div className="flex items-center gap-1"> | |
| 1990 | - <GripVertical className="w-4 h-4 text-gray-400" /> | |
| 1991 | - <div className="flex flex-col gap-0.5"> | |
| 1992 | - <button | |
| 1993 | - type="button" | |
| 1994 | - onClick={() => moveField(field.id, "up")} | |
| 1995 | - disabled={index === 0} | |
| 1996 | - className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed" | |
| 1997 | - > | |
| 1998 | - <ArrowUp className="w-3 h-3" /> | |
| 1999 | - </button> | |
| 2000 | - <button | |
| 2001 | - type="button" | |
| 2002 | - onClick={() => moveField(field.id, "down")} | |
| 2003 | - disabled={index === currentConfig.fields.length - 1} | |
| 2004 | - className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed" | |
| 2005 | - > | |
| 2006 | - <ArrowDown className="w-3 h-3" /> | |
| 2007 | - </button> | |
| 2008 | - </div> | |
| 2009 | - </div> | |
| 2010 | - | |
| 2011 | - {/* Field Info */} | |
| 2012 | - <div className="flex-1 min-w-0"> | |
| 2013 | - {editingField === field.id ? ( | |
| 2014 | - <div className="space-y-3"> | |
| 2015 | - <div className="grid grid-cols-4 gap-2"> | |
| 2016 | - {field.type !== "text_block" && ( | |
| 2017 | - <> | |
| 2018 | - <Input | |
| 2019 | - value={field.label} | |
| 2020 | - onChange={(e) => | |
| 2021 | - updateField(field.id, { | |
| 2022 | - label: e.target.value, | |
| 2023 | - }) | |
| 2024 | - } | |
| 2025 | - placeholder="Label" | |
| 2026 | - className="text-sm" | |
| 2027 | - /> | |
| 2028 | - <Input | |
| 2029 | - value={field.placeholder} | |
| 2030 | - onChange={(e) => | |
| 2031 | - updateField(field.id, { | |
| 2032 | - placeholder: e.target.value, | |
| 2033 | - }) | |
| 2034 | - } | |
| 2035 | - placeholder="Placeholder" | |
| 2036 | - className="text-sm" | |
| 2037 | - /> | |
| 2038 | - </> | |
| 2039 | - )} | |
| 2040 | - <Select | |
| 2041 | - value={field.type} | |
| 2042 | - onChange={(e) => | |
| 2043 | - !field.locked && | |
| 2044 | - updateField(field.id, { | |
| 2045 | - type: e.target | |
| 2046 | - .value as FormFieldConfig["type"], | |
| 2047 | - }) | |
| 2048 | - } | |
| 2049 | - disabled={field.locked} | |
| 2050 | - title={ | |
| 2051 | - field.locked | |
| 2052 | - ? "Locked fields cannot change type" | |
| 2053 | - : undefined | |
| 2054 | - } | |
| 2055 | - className="text-sm" | |
| 2056 | - > | |
| 2057 | - {fieldTypes.map((type) => ( | |
| 2058 | - <option key={type.value} value={type.value}> | |
| 2059 | - {type.label} | |
| 2060 | - </option> | |
| 2061 | - ))} | |
| 2062 | - </Select> | |
| 2063 | - <Select | |
| 2064 | - value={field.width} | |
| 2065 | - onChange={(e) => | |
| 2066 | - updateField(field.id, { | |
| 2067 | - width: e.target | |
| 2068 | - .value as FormFieldConfig["width"], | |
| 2069 | - }) | |
| 2070 | - } | |
| 2071 | - className="text-sm" | |
| 2072 | - > | |
| 2073 | - {widthOptions.map((w) => ( | |
| 2074 | - <option key={w.value} value={w.value}> | |
| 2075 | - {w.label} | |
| 2076 | - </option> | |
| 2077 | - ))} | |
| 2078 | - </Select> | |
| 2079 | - {activeFormTab === "traveler_form" && ( | |
| 2080 | - <Select | |
| 2081 | - value={field.applies_to || "all"} | |
| 2082 | - onChange={(e) => | |
| 2083 | - updateField(field.id, { | |
| 2084 | - applies_to: e.target | |
| 2085 | - .value as FormFieldConfig["applies_to"], | |
| 2086 | - }) | |
| 2087 | - } | |
| 2088 | - className="text-sm" | |
| 2089 | - title={__( | |
| 2090 | - "Which travelers see this field", | |
| 2091 | - "yatra", | |
| 2092 | - )} | |
| 2093 | - > | |
| 2094 | - {appliesToOptions.map((a) => ( | |
| 2095 | - <option key={a.value} value={a.value}> | |
| 2096 | - {a.label} | |
| 2097 | - </option> | |
| 2098 | - ))} | |
| 2099 | - </Select> | |
| 2100 | - )} | |
| 2101 | - {field.type === "tel" && ( | |
| 2102 | - <label | |
| 2103 | - className="flex items-center gap-2 text-sm whitespace-nowrap px-1" | |
| 2104 | - title={__( | |
| 2105 | - "Show the international country flag + dial code selector on this phone field", | |
| 2106 | - "yatra", | |
| 2107 | - )} | |
| 2108 | - > | |
| 2109 | - <input | |
| 2110 | - type="checkbox" | |
| 2111 | - checked={field.show_country_code !== false} | |
| 2112 | - onChange={(e) => | |
| 2113 | - updateField(field.id, { | |
| 2114 | - show_country_code: e.target.checked, | |
| 2115 | - }) | |
| 2116 | - } | |
| 2117 | - className="w-4 h-4 rounded border-gray-300 text-blue-600" | |
| 2118 | - /> | |
| 2119 | - {__("Show country code", "yatra")} | |
| 2120 | - </label> | |
| 2121 | - )} | |
| 2122 | - </div> | |
| 2123 | - | |
| 2124 | - {/* Content editor for text blocks (display-only) */} | |
| 2125 | - {field.type === "text_block" && ( | |
| 2126 | - <div> | |
| 2127 | - <Label className="text-xs font-medium"> | |
| 2128 | - {__("Content", "yatra")} | |
| 2129 | - </Label> | |
| 2130 | - <textarea | |
| 2131 | - value={field.content || ""} | |
| 2132 | - onChange={(e) => | |
| 2133 | - updateField(field.id, { | |
| 2134 | - content: e.target.value, | |
| 2135 | - }) | |
| 2136 | - } | |
| 2137 | - rows={4} | |
| 2138 | - placeholder={__( | |
| 2139 | - "Text shown to customers between fields. Basic HTML (bold, links, lists) is allowed.", | |
| 2140 | - "yatra", | |
| 2141 | - )} | |
| 2142 | - className="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm" | |
| 2143 | - /> | |
| 2144 | - <p className="text-[10px] text-gray-400 mt-0.5"> | |
| 2145 | - {__( | |
| 2146 | - "Display only — this is not an input and is not submitted by the customer.", | |
| 2147 | - "yatra", | |
| 2148 | - )} | |
| 2149 | - </p> | |
| 2150 | - </div> | |
| 2151 | - )} | |
| 2152 | - | |
| 2153 | - {/* Field ID - Below other fields (input fields only) */} | |
| 2154 | - {field.type !== "text_block" && ( | |
| 2155 | - <div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-900 rounded"> | |
| 2156 | - <Label className="text-xs text-gray-500 whitespace-nowrap"> | |
| 2157 | - {__("Field ID:", "yatra")} | |
| 2158 | - </Label> | |
| 2159 | - <Input | |
| 2160 | - value={ | |
| 2161 | - fieldIdDraft !== null | |
| 2162 | - ? fieldIdDraft | |
| 2163 | - : field.id | |
| 2164 | - } | |
| 2165 | - onChange={(e) => { | |
| 2166 | - if (!field.locked) { | |
| 2167 | - // Local draft only — do NOT mutate field.id | |
| 2168 | - // here, or the row remounts (focus loss) and | |
| 2169 | - // the editor closes on every keystroke. | |
| 2170 | - setFieldIdDraft(sanitizeId(e.target.value)); | |
| 2171 | - } | |
| 2172 | - }} | |
| 2173 | - onBlur={() => { | |
| 2174 | - if (fieldIdDraft === null) return; | |
| 2175 | - const next = fieldIdDraft; | |
| 2176 | - setFieldIdDraft(null); | |
| 2177 | - if ( | |
| 2178 | - !field.locked && | |
| 2179 | - next !== "" && | |
| 2180 | - next !== field.id | |
| 2181 | - ) { | |
| 2182 | - updateField(field.id, { id: next }); | |
| 2183 | - // Keep the editor open on the renamed field. | |
| 2184 | - setEditingField((cur) => | |
| 2185 | - cur === field.id ? next : cur, | |
| 2186 | - ); | |
| 2187 | - } | |
| 2188 | - }} | |
| 2189 | - onKeyDown={(e) => { | |
| 2190 | - if (e.key === "Enter") { | |
| 2191 | - e.preventDefault(); | |
| 2192 | - ( | |
| 2193 | - e.currentTarget as HTMLInputElement | |
| 2194 | - ).blur(); | |
| 2195 | - } | |
| 2196 | - }} | |
| 2197 | - placeholder="field_id" | |
| 2198 | - className="text-sm font-mono flex-1 max-w-xs" | |
| 2199 | - disabled={field.locked} | |
| 2200 | - title={ | |
| 2201 | - field.locked | |
| 2202 | - ? "Locked fields cannot change ID" | |
| 2203 | - : "Field ID (lowercase, no spaces)" | |
| 2204 | - } | |
| 2205 | - /> | |
| 2206 | - {field.locked && ( | |
| 2207 | - <span className="text-xs text-amber-600"> | |
| 2208 | - {__("(locked)", "yatra")} | |
| 2209 | - </span> | |
| 2210 | - )} | |
| 2211 | - </div> | |
| 2212 | - )} | |
| 2213 | - | |
| 2214 | - {/* Dropdown Options Editor */} | |
| 2215 | - {field.type === "select" && ( | |
| 2216 | - <div className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700"> | |
| 2217 | - <div className="flex items-center justify-between mb-2"> | |
| 2218 | - <Label className="text-xs font-medium"> | |
| 2219 | - {__("Dropdown Options", "yatra")} | |
| 2220 | - </Label> | |
| 2221 | - <Button | |
| 2222 | - type="button" | |
| 2223 | - variant="ghost" | |
| 2224 | - size="sm" | |
| 2225 | - onClick={() => { | |
| 2226 | - const newOptions = [ | |
| 2227 | - ...(field.options || []), | |
| 2228 | - { value: "", label: "" }, | |
| 2229 | - ]; | |
| 2230 | - updateField(field.id, { | |
| 2231 | - options: newOptions, | |
| 2232 | - }); | |
| 2233 | - }} | |
| 2234 | - className="h-6 text-xs" | |
| 2235 | - > | |
| 2236 | - <Plus className="w-3 h-3 mr-1" /> | |
| 2237 | - {__("Add Option", "yatra")} | |
| 2238 | - </Button> | |
| 2239 | - </div> | |
| 2240 | - <div className="space-y-2"> | |
| 2241 | - {(field.options || []).map( | |
| 2242 | - (option, optIndex) => ( | |
| 2243 | - <div | |
| 2244 | - key={optIndex} | |
| 2245 | - className="flex items-center gap-2" | |
| 2246 | - > | |
| 2247 | - <Input | |
| 2248 | - value={option.value} | |
| 2249 | - onChange={(e) => { | |
| 2250 | - const newOptions = [ | |
| 2251 | - ...(field.options || []), | |
| 2252 | - ]; | |
| 2253 | - newOptions[optIndex] = { | |
| 2254 | - ...newOptions[optIndex], | |
| 2255 | - value: e.target.value, | |
| 2256 | - }; | |
| 2257 | - updateField(field.id, { | |
| 2258 | - options: newOptions, | |
| 2259 | - }); | |
| 2260 | - }} | |
| 2261 | - placeholder="Value (e.g., spouse)" | |
| 2262 | - className="text-xs flex-1" | |
| 2263 | - /> | |
| 2264 | - <Input | |
| 2265 | - value={option.label} | |
| 2266 | - onChange={(e) => { | |
| 2267 | - const newOptions = [ | |
| 2268 | - ...(field.options || []), | |
| 2269 | - ]; | |
| 2270 | - newOptions[optIndex] = { | |
| 2271 | - ...newOptions[optIndex], | |
| 2272 | - label: e.target.value, | |
| 2273 | - }; | |
| 2274 | - updateField(field.id, { | |
| 2275 | - options: newOptions, | |
| 2276 | - }); | |
| 2277 | - }} | |
| 2278 | - placeholder="Label (e.g., Spouse/Partner)" | |
| 2279 | - className="text-xs flex-1" | |
| 2280 | - /> | |
| 2281 | - <button | |
| 2282 | - type="button" | |
| 2283 | - onClick={() => { | |
| 2284 | - const newOptions = ( | |
| 2285 | - field.options || [] | |
| 2286 | - ).filter((_, i) => i !== optIndex); | |
| 2287 | - updateField(field.id, { | |
| 2288 | - options: newOptions, | |
| 2289 | - }); | |
| 2290 | - }} | |
| 2291 | - className="p-1 text-gray-400 hover:text-red-500" | |
| 2292 | - > | |
| 2293 | - <Trash2 className="w-3 h-3" /> | |
| 2294 | - </button> | |
| 2295 | - </div> | |
| 2296 | - ), | |
| 2297 | - )} | |
| 2298 | - {(!field.options || | |
| 2299 | - field.options.length === 0) && ( | |
| 2300 | - <p className="text-xs text-gray-400 italic"> | |
| 2301 | - {__( | |
| 2302 | - 'No options. Click "Add Option" to add dropdown choices.', | |
| 2303 | - "yatra", | |
| 2304 | - )} | |
| 2305 | - </p> | |
| 2306 | - )} | |
| 2307 | - </div> | |
| 2308 | - </div> | |
| 2309 | - )} | |
| 2310 | - </div> | |
| 2311 | - ) : ( | |
| 2312 | - <div className="flex items-center gap-3 flex-wrap"> | |
| 2313 | - <span className="font-medium text-sm"> | |
| 2314 | - {field.type === "text_block" | |
| 2315 | - ? __("Text Block", "yatra") | |
| 2316 | - : field.label} | |
| 2317 | - </span> | |
| 2318 | - {field.type !== "text_block" && ( | |
| 2319 | - <code className="text-xs text-gray-500 dark:text-gray-400 px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded font-mono"> | |
| 2320 | - {field.id} | |
| 2321 | - </code> | |
| 2322 | - )} | |
| 2323 | - <span className="text-xs text-gray-400 dark:text-gray-500 px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded"> | |
| 2324 | - {fieldTypes.find((t) => t.value === field.type) | |
| 2325 | - ?.label || field.type} | |
| 2326 | - </span> | |
| 2327 | - <span className="text-xs text-gray-400 dark:text-gray-500"> | |
| 2328 | - {widthOptions.find((w) => w.value === field.width) | |
| 2329 | - ?.label || "Full"} | |
| 2330 | - </span> | |
| 2331 | - {field.type === "text_block" && field.content && ( | |
| 2332 | - <span className="text-xs text-gray-500 dark:text-gray-400 italic truncate max-w-[16rem]"> | |
| 2333 | - " | |
| 2334 | - {field.content | |
| 2335 | - .replace(/<[^>]*>/g, "") | |
| 2336 | - .slice(0, 60)} | |
| 2337 | - " | |
| 2338 | - </span> | |
| 2339 | - )} | |
| 2340 | - {field.type === "select" && | |
| 2341 | - field.options && | |
| 2342 | - field.options.length > 0 && ( | |
| 2343 | - <span className="text-xs text-blue-500 dark:text-blue-400"> | |
| 2344 | - ({field.options.length}{" "} | |
| 2345 | - {field.options.length === 1 | |
| 2346 | - ? "option" | |
| 2347 | - : "options"} | |
| 2348 | - ) | |
| 2349 | - </span> | |
| 2350 | - )} | |
| 2351 | - {field.required && ( | |
| 2352 | - <span className="text-xs text-red-500 font-medium"> | |
| 2353 | - {__("Required", "yatra")} | |
| 2354 | - </span> | |
| 2355 | - )} | |
| 2356 | - {activeFormTab === "traveler_form" && | |
| 2357 | - field.applies_to === "lead" && ( | |
| 2358 | - <span className="text-xs px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 font-medium"> | |
| 2359 | - {__("Lead only", "yatra")} | |
| 2360 | - </span> | |
| 2361 | - )} | |
| 2362 | - {field.locked && ( | |
| 2363 | - <span | |
| 2364 | - className="inline-flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400 px-1.5 py-0.5 bg-amber-50 dark:bg-amber-900/20 rounded" | |
| 2365 | - title="This field is protected and cannot be deleted" | |
| 2366 | - > | |
| 2367 | - <Lock className="w-3 h-3" /> | |
| 2368 | - Locked | |
| 2369 | - </span> | |
| 2370 | - )} | |
| 2371 | - </div> | |
| 2372 | - )} | |
| 2373 | - </div> | |
| 2374 | - | |
| 2375 | - {/* Actions */} | |
| 2376 | - <div className="flex items-center gap-1"> | |
| 2377 | - {field.type !== "text_block" && ( | |
| 2378 | - <button | |
| 2379 | - type="button" | |
| 2380 | - onClick={() => | |
| 2381 | - !field.locked && toggleFieldRequired(field.id) | |
| 2382 | - } | |
| 2383 | - disabled={field.locked} | |
| 2384 | - className={`p-1.5 rounded ${field.locked ? "cursor-not-allowed opacity-50" : ""} ${field.required ? "text-red-500 bg-red-50 dark:bg-red-900/20" : "text-gray-400 hover:text-gray-600"}`} | |
| 2385 | - title={ | |
| 2386 | - field.locked | |
| 2387 | - ? "This field is required and cannot be changed" | |
| 2388 | - : field.required | |
| 2389 | - ? "Make optional" | |
| 2390 | - : "Make required" | |
| 2391 | - } | |
| 2392 | - > | |
| 2393 | - <Star | |
| 2394 | - className="w-4 h-4" | |
| 2395 | - fill={field.required ? "currentColor" : "none"} | |
| 2396 | - /> | |
| 2397 | - </button> | |
| 2398 | - )} | |
| 2399 | - {/* Show enable/disable toggle only for non-locked fields */} | |
| 2400 | - {!field.locked && ( | |
| 2401 | - <button | |
| 2402 | - type="button" | |
| 2403 | - onClick={() => toggleFieldEnabled(field.id)} | |
| 2404 | - className={`p-1.5 rounded ${field.enabled ? "text-green-500" : "text-gray-400"}`} | |
| 2405 | - title={ | |
| 2406 | - field.enabled ? "Disable field" : "Enable field" | |
| 2407 | - } | |
| 2408 | - > | |
| 2409 | - {field.enabled ? ( | |
| 2410 | - <Eye className="w-4 h-4" /> | |
| 2411 | - ) : ( | |
| 2412 | - <EyeOff className="w-4 h-4" /> | |
| 2413 | - )} | |
| 2414 | - </button> | |
| 2415 | - )} | |
| 2416 | - <button | |
| 2417 | - type="button" | |
| 2418 | - onClick={() => | |
| 2419 | - setEditingField( | |
| 2420 | - editingField === field.id ? null : field.id, | |
| 2421 | - ) | |
| 2422 | - } | |
| 2423 | - className="p-1.5 rounded text-gray-400 hover:text-blue-500" | |
| 2424 | - title="Edit field" | |
| 2425 | - > | |
| 2426 | - <Edit2 className="w-4 h-4" /> | |
| 2427 | - </button> | |
| 2428 | - {/* Show delete button only for non-locked fields */} | |
| 2429 | - {!field.locked && ( | |
| 2430 | - <button | |
| 2431 | - type="button" | |
| 2432 | - onClick={() => | |
| 2433 | - setDeleteConfirm({ | |
| 2434 | - isOpen: true, | |
| 2435 | - fieldId: field.id, | |
| 2436 | - fieldLabel: field.label, | |
| 2437 | - }) | |
| 2438 | - } | |
| 2439 | - className="p-1.5 rounded text-gray-400 hover:text-red-500" | |
| 2440 | - title="Delete field" | |
| 2441 | - > | |
| 2442 | - <Trash2 className="w-4 h-4" /> | |
| 2443 | - </button> | |
| 2444 | - )} | |
| 2445 | - </div> | |
| 2446 | - </div> | |
| 2447 | - ))} | |
| 2448 | - </div> | |
| 2449 | - )} | |
| 1445 | + )} | |
| 2450 | 1446 | </CardContent> |
| 2451 | 1447 | </Card> |
| 2452 | 1448 | |
| 2453 | - {/* Delete Confirmation Dialog */} | |
| 2454 | - <ConfirmationDialog | |
| 2455 | - isOpen={deleteConfirm.isOpen} | |
| 2456 | - onClose={() => | |
| 2457 | - setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" }) | |
| 2458 | - } | |
| 2459 | - onConfirm={() => { | |
| 2460 | - if (deleteConfirm.fieldId) { | |
| 2461 | - deleteField(deleteConfirm.fieldId); | |
| 1449 | + {conditionsSupported && ( | |
| 1450 | + <BookingFormConditionsModal | |
| 1451 | + isOpen={conditionsOpen} | |
| 1452 | + onClose={() => setConditionsOpen(false)} | |
| 1453 | + formType={activeFormTab} | |
| 1454 | + formLabel={ | |
| 1455 | + formTabs.find((t) => t.id === activeFormTab)?.label || "" | |
| 2462 | 1456 | } |
| 2463 | - }} | |
| 2464 | - title={__("Delete Field", "yatra")} | |
| 2465 | - message={`Are you sure you want to delete the field "${deleteConfirm.fieldLabel}"? This action cannot be undone.`} | |
| 2466 | - confirmText={__("Delete", "yatra")} | |
| 2467 | - cancelText={__("Cancel", "yatra")} | |
| 2468 | - variant="danger" | |
| 1457 | + section={currentConfig} | |
| 1458 | + onSave={(conditions) => | |
| 1459 | + updateFormConfig({ | |
| 1460 | + conditions: conditions.length > 0 ? conditions : undefined, | |
| 1461 | + }) | |
| 1462 | + } | |
| 1463 | + fieldTypes={fieldTypes} | |
| 1464 | + widthOptions={widthOptions} | |
| 1465 | + appliesToOptions={appliesToOptions} | |
| 1466 | + /> | |
| 1467 | + )} | |
| 1468 | + | |
| 1469 | + {/* Form Fields — shared builder (also used by the Conditions popup) */} | |
| 1470 | + <BookingFormFieldsEditor | |
| 1471 | + formType={activeFormTab} | |
| 1472 | + fields={currentConfig.fields} | |
| 1473 | + onChange={(fields) => updateFormConfig({ fields })} | |
| 1474 | + fieldTypes={fieldTypes} | |
| 1475 | + widthOptions={widthOptions} | |
| 1476 | + appliesToOptions={appliesToOptions} | |
| 2469 | 1477 | /> |
| 2470 | 1478 | |
| 1479 | + | |
| 2471 | 1480 | {/* Preview */} |
| 2472 | 1481 | <Card> |
| 2473 | 1482 | <CardHeader> |
| 2474 | 1483 | <CardTitle className="text-base"> |
| @@ -2726,8 +1735,9 @@ | ||
| 2726 | 1735 | date_format: "Y-m-d", |
| 2727 | 1736 | time_format: "H:i", |
| 2728 | 1737 | frontend_primary_color: "#3b82f6", |
| 2729 | 1738 | frontend_container_max_width: "", |
| 1739 | + frontend_listing_card_layout: "standard", | |
| 2730 | 1740 | // Search & Listing — defaults match current behaviour (all fields shown, |
| 2731 | 1741 | // mobile filters expanded) so existing sites are unchanged. |
| 2732 | 1742 | search_show_keyword: true, |
| 2733 | 1743 | search_show_destination: true, |
| @@ -2733,11 +1743,13 @@ | ||
| 2733 | 1743 | search_show_destination: true, |
| 2734 | 1744 | search_show_activities: true, |
| 2735 | 1745 | search_show_duration: true, |
| 2736 | 1746 | search_show_budget: true, |
| 1747 | + search_show_date: false, | |
| 2737 | 1748 | collapse_filters_on_mobile: false, |
| 2738 | 1749 | booking_confirmation: true, |
| 2739 | 1750 | auto_confirm_bookings: false, |
| 1751 | + auto_confirm_mode: "online", | |
| 2740 | 1752 | require_login: false, |
| 2741 | 1753 | allow_guest_checkout: true, |
| 2742 | 1754 | require_guest_email_verification: false, |
| 2743 | 1755 | booking_expiry_hours: 24, |
| @@ -2761,8 +1773,10 @@ | ||
| 2761 | 1773 | scheduled_payment_installments: 1, |
| 2762 | 1774 | discount_stacking_mode: "both", |
| 2763 | 1775 | scheduled_payment_interval: 30, |
| 2764 | 1776 | scheduled_payment_reminder_days: 3, |
| 1777 | + balance_anchor: "booking", | |
| 1778 | + balance_due_days: 14, | |
| 2765 | 1779 | gateway_configs: { |
| 2766 | 1780 | stripe: { |
| 2767 | 1781 | enabled: true, |
| 2768 | 1782 | title: "Stripe", |
| @@ -2969,8 +1983,9 @@ | ||
| 2969 | 1983 | trip_category_base: "trip-category", |
| 2970 | 1984 | booking_base: "book", |
| 2971 | 1985 | enable_wishlist: false, |
| 2972 | 1986 | show_sold_out: true, |
| 1987 | + availability_horizon_months: 12, | |
| 2973 | 1988 | use_booking_page: false, |
| 2974 | 1989 | booking_page_id: 0, |
| 2975 | 1990 | terms_page_id: 0, |
| 2976 | 1991 | privacy_policy_page_id: 0, |
| @@ -3211,8 +2226,10 @@ | ||
| 3211 | 2226 | seo_trip_meta_description: "", |
| 3212 | 2227 | seo_trip_meta_keywords: "", |
| 3213 | 2228 | seo_trip_meta_image: 0, |
| 3214 | 2229 | enable_sitemap: true, |
| 2230 | + sitemap_types: ["archive", "trip", "destination", "activity", "category"], | |
| 2231 | + sitemap_noindex_excluded: false, | |
| 3215 | 2232 | }), |
| 3216 | 2233 | [], |
| 3217 | 2234 | ); |
| 3218 | 2235 | |
| @@ -4546,8 +3563,152 @@ | ||
| 4546 | 3563 | spellCheck={false} |
| 4547 | 3564 | autoComplete="off" |
| 4548 | 3565 | /> |
| 4549 | 3566 | </FormField> |
| 3567 | + | |
| 3568 | + <FormField | |
| 3569 | + id="frontend_listing_card_layout" | |
| 3570 | + label={__("Listing card layout", "yatra")} | |
| 3571 | + description={__( | |
| 3572 | + "How trip cards appear in listings and search results. Compact shows a dense card — image, title, price and a View Details button only — so several trips fit per screen. Ideal for phones. Existing sites keep the Standard card.", | |
| 3573 | + "yatra", | |
| 3574 | + )} | |
| 3575 | + > | |
| 3576 | + <div | |
| 3577 | + role="radiogroup" | |
| 3578 | + aria-label={__("Listing card layout", "yatra")} | |
| 3579 | + className="grid grid-cols-1 sm:grid-cols-3 gap-3 max-w-2xl" | |
| 3580 | + > | |
| 3581 | + {( | |
| 3582 | + [ | |
| 3583 | + { | |
| 3584 | + value: "standard", | |
| 3585 | + label: __("Standard", "yatra"), | |
| 3586 | + sub: __("Full card (default)", "yatra"), | |
| 3587 | + svg: ( | |
| 3588 | + <svg | |
| 3589 | + viewBox="0 0 160 100" | |
| 3590 | + className="w-full h-full" | |
| 3591 | + fill="none" | |
| 3592 | + > | |
| 3593 | + <rect x="46" y="6" width="68" height="88" rx="6" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" /> | |
| 3594 | + <rect x="52" y="12" width="56" height="30" rx="3" fill="#60a5fa" /> | |
| 3595 | + <rect x="52" y="49" width="44" height="5" rx="2.5" fill="#94a3b8" /> | |
| 3596 | + <rect x="52" y="59" width="34" height="3" rx="1.5" fill="#cbd5e1" /> | |
| 3597 | + <rect x="52" y="66" width="50" height="3" rx="1.5" fill="#e2e8f0" /> | |
| 3598 | + <rect x="52" y="73" width="50" height="3" rx="1.5" fill="#e2e8f0" /> | |
| 3599 | + <rect x="52" y="82" width="18" height="6" rx="2" fill="#475569" /> | |
| 3600 | + <rect x="86" y="81" width="22" height="8" rx="3" fill="#3b82f6" /> | |
| 3601 | + </svg> | |
| 3602 | + ), | |
| 3603 | + }, | |
| 3604 | + { | |
| 3605 | + value: "compact_mobile", | |
| 3606 | + label: __("Compact — mobile", "yatra"), | |
| 3607 | + sub: __("Dense on phones · desktop unchanged", "yatra"), | |
| 3608 | + svg: ( | |
| 3609 | + <svg | |
| 3610 | + viewBox="0 0 160 100" | |
| 3611 | + className="w-full h-full" | |
| 3612 | + fill="none" | |
| 3613 | + > | |
| 3614 | + <rect x="50" y="4" width="60" height="92" rx="9" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" /> | |
| 3615 | + <rect x="72" y="8" width="16" height="3" rx="1.5" fill="#e2e8f0" /> | |
| 3616 | + {[0, 26, 52].map((dy) => ( | |
| 3617 | + <g key={dy} transform={`translate(0,${dy})`}> | |
| 3618 | + <rect x="56" y="16" width="48" height="22" rx="3" fill="#f1f5f9" /> | |
| 3619 | + <rect x="59" y="19" width="16" height="16" rx="2" fill="#60a5fa" /> | |
| 3620 | + <rect x="79" y="21" width="20" height="3" rx="1.5" fill="#94a3b8" /> | |
| 3621 | + <rect x="79" y="27" width="12" height="2.5" rx="1.25" fill="#cbd5e1" /> | |
| 3622 | + <rect x="79" y="32" width="9" height="3" rx="1.5" fill="#475569" /> | |
| 3623 | + <rect x="93" y="31.5" width="6" height="4" rx="1.5" fill="#3b82f6" /> | |
| 3624 | + </g> | |
| 3625 | + ))} | |
| 3626 | + </svg> | |
| 3627 | + ), | |
| 3628 | + }, | |
| 3629 | + { | |
| 3630 | + value: "compact_all", | |
| 3631 | + label: __("Compact — everywhere", "yatra"), | |
| 3632 | + sub: __("Dense on mobile & desktop", "yatra"), | |
| 3633 | + svg: ( | |
| 3634 | + <svg | |
| 3635 | + viewBox="0 0 160 100" | |
| 3636 | + className="w-full h-full" | |
| 3637 | + fill="none" | |
| 3638 | + > | |
| 3639 | + <rect x="10" y="12" width="98" height="58" rx="4" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" /> | |
| 3640 | + <rect x="51" y="70" width="16" height="8" fill="#cbd5e1" /> | |
| 3641 | + <rect x="39" y="78" width="40" height="4" rx="2" fill="#cbd5e1" /> | |
| 3642 | + {[0, 24].map((dy) => ( | |
| 3643 | + <g key={dy} transform={`translate(0,${dy})`}> | |
| 3644 | + <rect x="16" y="18" width="86" height="20" rx="3" fill="#f1f5f9" /> | |
| 3645 | + <rect x="19" y="21" width="14" height="14" rx="2" fill="#60a5fa" /> | |
| 3646 | + <rect x="37" y="23" width="34" height="3" rx="1.5" fill="#94a3b8" /> | |
| 3647 | + <rect x="37" y="29" width="20" height="2.5" rx="1.25" fill="#cbd5e1" /> | |
| 3648 | + <rect x="86" y="27" width="12" height="6" rx="2" fill="#3b82f6" /> | |
| 3649 | + </g> | |
| 3650 | + ))} | |
| 3651 | + <rect x="120" y="26" width="30" height="62" rx="5" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" /> | |
| 3652 | + {[0, 18, 36].map((dy) => ( | |
| 3653 | + <g key={dy} transform={`translate(0,${dy})`}> | |
| 3654 | + <rect x="124" y="32" width="22" height="14" rx="2" fill="#f1f5f9" /> | |
| 3655 | + <rect x="126" y="34" width="10" height="10" rx="1.5" fill="#60a5fa" /> | |
| 3656 | + <rect x="138" y="36" width="7" height="2" rx="1" fill="#94a3b8" /> | |
| 3657 | + <rect x="138" y="40" width="5" height="2" rx="1" fill="#cbd5e1" /> | |
| 3658 | + </g> | |
| 3659 | + ))} | |
| 3660 | + </svg> | |
| 3661 | + ), | |
| 3662 | + }, | |
| 3663 | + ] as const | |
| 3664 | + ).map((opt) => { | |
| 3665 | + const selected = | |
| 3666 | + (formData.frontend_listing_card_layout || "standard") === | |
| 3667 | + opt.value; | |
| 3668 | + return ( | |
| 3669 | + <button | |
| 3670 | + key={opt.value} | |
| 3671 | + type="button" | |
| 3672 | + role="radio" | |
| 3673 | + aria-checked={selected} | |
| 3674 | + onClick={() => | |
| 3675 | + handleFieldChange({ | |
| 3676 | + target: { | |
| 3677 | + name: "frontend_listing_card_layout", | |
| 3678 | + id: "frontend_listing_card_layout", | |
| 3679 | + value: opt.value, | |
| 3680 | + type: "text", | |
| 3681 | + }, | |
| 3682 | + } as React.ChangeEvent<HTMLInputElement>) | |
| 3683 | + } | |
| 3684 | + className={`relative rounded-lg border-2 p-2.5 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${ | |
| 3685 | + selected | |
| 3686 | + ? "border-blue-600 ring-1 ring-blue-600 bg-blue-50/60 dark:bg-blue-900/20" | |
| 3687 | + : "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600" | |
| 3688 | + }`} | |
| 3689 | + > | |
| 3690 | + <div className="aspect-[8/5] w-full overflow-hidden rounded-md bg-gray-50 dark:bg-gray-800/60"> | |
| 3691 | + {opt.svg} | |
| 3692 | + </div> | |
| 3693 | + <div className="mt-2 text-sm font-medium text-gray-900 dark:text-white"> | |
| 3694 | + {opt.label} | |
| 3695 | + </div> | |
| 3696 | + <div className="text-xs text-gray-500 dark:text-gray-400"> | |
| 3697 | + {opt.sub} | |
| 3698 | + </div> | |
| 3699 | + {selected && ( | |
| 3700 | + <span className="absolute top-2 right-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-blue-600 text-white"> | |
| 3701 | + <svg width="12" height="12" viewBox="0 0 20 20" fill="currentColor"> | |
| 3702 | + <path d="M16.7 5.3a1 1 0 010 1.4l-7 7a1 1 0 01-1.4 0l-3-3a1 1 0 011.4-1.4L9 11.6l6.3-6.3a1 1 0 011.4 0z" /> | |
| 3703 | + </svg> | |
| 3704 | + </span> | |
| 3705 | + )} | |
| 3706 | + </button> | |
| 3707 | + ); | |
| 3708 | + })} | |
| 3709 | + </div> | |
| 3710 | + </FormField> | |
| 4550 | 3711 | </div> |
| 4551 | 3712 | </div> |
| 4552 | 3713 | ); |
| 4553 | 3714 | |
| @@ -4742,8 +3903,53 @@ | ||
| 4742 | 3903 | |
| 4743 | 3904 | {formData.enable_scheduled_payments && ( |
| 4744 | 3905 | <> |
| 4745 | 3906 | <FormField |
| 3907 | + id="balance_anchor" | |
| 3908 | + label={__("Balance schedule", "yatra")} | |
| 3909 | + description={__( | |
| 3910 | + "Whether the remaining balance is scheduled a fixed number of days after the deposit, or relative to the tour start date.", | |
| 3911 | + "yatra", | |
| 3912 | + )} | |
| 3913 | + > | |
| 3914 | + <Select | |
| 3915 | + id="balance_anchor" | |
| 3916 | + name="balance_anchor" | |
| 3917 | + value={formData.balance_anchor} | |
| 3918 | + onChange={handleFieldChange} | |
| 3919 | + > | |
| 3920 | + <option value="booking"> | |
| 3921 | + {__("A fixed number of days after the deposit", "yatra")} | |
| 3922 | + </option> | |
| 3923 | + <option value="tour"> | |
| 3924 | + {__("A number of days before the tour date", "yatra")} | |
| 3925 | + </option> | |
| 3926 | + </Select> | |
| 3927 | + </FormField> | |
| 3928 | + | |
| 3929 | + {formData.balance_anchor === "tour" && ( | |
| 3930 | + <FormField | |
| 3931 | + id="balance_due_days" | |
| 3932 | + label={__("Days before tour", "yatra")} | |
| 3933 | + description={__( | |
| 3934 | + "The balance is collected this many days before the tour starts. Bookings made inside this window must pay in full up front.", | |
| 3935 | + "yatra", | |
| 3936 | + )} | |
| 3937 | + > | |
| 3938 | + <Input | |
| 3939 | + id="balance_due_days" | |
| 3940 | + type="number" | |
| 3941 | + min={1} | |
| 3942 | + max={365} | |
| 3943 | + value={formData.balance_due_days} | |
| 3944 | + name="balance_due_days" | |
| 3945 | + onChange={handleFieldChange} | |
| 3946 | + className="max-w-xs" | |
| 3947 | + /> | |
| 3948 | + </FormField> | |
| 3949 | + )} | |
| 3950 | + | |
| 3951 | + <FormField | |
| 4746 | 3952 | id="scheduled_payment_type" |
| 4747 | 3953 | label={__("Schedule type", "yatra")} |
| 4748 | 3954 | description={__( |
| 4749 | 3955 | "Single final charge on a date, or multiple installments.", |
| @@ -4764,27 +3970,29 @@ | ||
| 4764 | 3970 | </option> |
| 4765 | 3971 | </Select> |
| 4766 | 3972 | </FormField> |
| 4767 | 3973 | |
| 4768 | - <FormField | |
| 4769 | - id="scheduled_payment_days" | |
| 4770 | - label={__("Days until first charge", "yatra")} | |
| 4771 | - description={__( | |
| 4772 | - "How many days after the initial payment to schedule the first balance collection.", | |
| 4773 | - "yatra", | |
| 4774 | - )} | |
| 4775 | - > | |
| 4776 | - <Input | |
| 3974 | + {formData.balance_anchor !== "tour" && ( | |
| 3975 | + <FormField | |
| 4777 | 3976 | id="scheduled_payment_days" |
| 4778 | - type="number" | |
| 4779 | - min={0} | |
| 4780 | - max={365} | |
| 4781 | - value={formData.scheduled_payment_days} | |
| 4782 | - name="scheduled_payment_days" | |
| 4783 | - onChange={handleFieldChange} | |
| 4784 | - className="max-w-xs" | |
| 4785 | - /> | |
| 4786 | - </FormField> | |
| 3977 | + label={__("Days until first charge", "yatra")} | |
| 3978 | + description={__( | |
| 3979 | + "How many days after the initial payment to schedule the first balance collection.", | |
| 3980 | + "yatra", | |
| 3981 | + )} | |
| 3982 | + > | |
| 3983 | + <Input | |
| 3984 | + id="scheduled_payment_days" | |
| 3985 | + type="number" | |
| 3986 | + min={0} | |
| 3987 | + max={365} | |
| 3988 | + value={formData.scheduled_payment_days} | |
| 3989 | + name="scheduled_payment_days" | |
| 3990 | + onChange={handleFieldChange} | |
| 3991 | + className="max-w-xs" | |
| 3992 | + /> | |
| 3993 | + </FormField> | |
| 3994 | + )} | |
| 4787 | 3995 | |
| 4788 | 3996 | {formData.scheduled_payment_type === "installments" && ( |
| 4789 | 3997 | <> |
| 4790 | 3998 | <FormField |
| @@ -5808,8 +5016,16 @@ | ||
| 5808 | 5016 | "Dropdown to filter trips by price range.", |
| 5809 | 5017 | "yatra", |
| 5810 | 5018 | ), |
| 5811 | 5019 | }, |
| 5020 | + { | |
| 5021 | + key: "search_show_date" as const, | |
| 5022 | + label: __("Date", "yatra"), | |
| 5023 | + description: __( | |
| 5024 | + "Date picker to find trips with a departure on a specific date.", | |
| 5025 | + "yatra", | |
| 5026 | + ), | |
| 5027 | + }, | |
| 5812 | 5028 | ].map((row) => ( |
| 5813 | 5029 | <div |
| 5814 | 5030 | key={row.key} |
| 5815 | 5031 | className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md" |
| @@ -5905,31 +5121,83 @@ | ||
| 5905 | 5121 | </p> |
| 5906 | 5122 | </div> |
| 5907 | 5123 | </div> |
| 5908 | 5124 | |
| 5909 | - <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> | |
| 5910 | - <input | |
| 5911 | - type="checkbox" | |
| 5912 | - id="auto_confirm_bookings" | |
| 5913 | - checked={formData.auto_confirm_bookings} | |
| 5914 | - name="auto_confirm_bookings" | |
| 5915 | - onChange={handleFieldChange} | |
| 5916 | - className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" | |
| 5917 | - /> | |
| 5918 | - <div className="flex-1"> | |
| 5919 | - <Label | |
| 5920 | - htmlFor="auto_confirm_bookings" | |
| 5921 | - className="font-medium cursor-pointer" | |
| 5922 | - > | |
| 5923 | - {__("Auto-Confirm Bookings", "yatra")} | |
| 5924 | - </Label> | |
| 5925 | - <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> | |
| 5926 | - {__( | |
| 5927 | - "Automatically confirm bookings without manual approval", | |
| 5928 | - "yatra", | |
| 5929 | - )} | |
| 5930 | - </p> | |
| 5125 | + <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> | |
| 5126 | + <div className="font-medium"> | |
| 5127 | + {__("Auto-Confirm Bookings", "yatra")} | |
| 5931 | 5128 | </div> |
| 5129 | + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 mb-2"> | |
| 5130 | + {__( | |
| 5131 | + "Choose when a booking is automatically set to Confirmed. Offline methods (bank transfer, pay later) stay pending until you confirm them.", | |
| 5132 | + "yatra", | |
| 5133 | + )} | |
| 5134 | + </p> | |
| 5135 | + <div role="radiogroup" className="flex flex-col gap-2"> | |
| 5136 | + {[ | |
| 5137 | + { | |
| 5138 | + value: "none", | |
| 5139 | + label: __("Don't auto-confirm", "yatra"), | |
| 5140 | + sub: __( | |
| 5141 | + "Every booking stays Pending until you confirm it manually.", | |
| 5142 | + "yatra", | |
| 5143 | + ), | |
| 5144 | + }, | |
| 5145 | + { | |
| 5146 | + value: "online", | |
| 5147 | + label: __("Auto-confirm online payments only", "yatra"), | |
| 5148 | + sub: __( | |
| 5149 | + "When an online payment (Stripe, PayPal, Razorpay…) pays the balance in full, the booking is confirmed. Deposits/partial payments and offline methods stay Pending.", | |
| 5150 | + "yatra", | |
| 5151 | + ), | |
| 5152 | + }, | |
| 5153 | + { | |
| 5154 | + value: "all", | |
| 5155 | + label: __("Auto-confirm all bookings", "yatra"), | |
| 5156 | + sub: __( | |
| 5157 | + "Every booking is confirmed at checkout, whether or not it was paid.", | |
| 5158 | + "yatra", | |
| 5159 | + ), | |
| 5160 | + }, | |
| 5161 | + ].map((opt) => ( | |
| 5162 | + <label | |
| 5163 | + key={opt.value} | |
| 5164 | + htmlFor={`auto_confirm_mode_${opt.value}`} | |
| 5165 | + className="flex items-start gap-2 cursor-pointer" | |
| 5166 | + > | |
| 5167 | + <input | |
| 5168 | + type="radio" | |
| 5169 | + id={`auto_confirm_mode_${opt.value}`} | |
| 5170 | + name="auto_confirm_mode" | |
| 5171 | + value={opt.value} | |
| 5172 | + checked={ | |
| 5173 | + (formData.auto_confirm_mode || "none") === opt.value | |
| 5174 | + } | |
| 5175 | + onChange={() => | |
| 5176 | + setFormData((prev) => | |
| 5177 | + prev | |
| 5178 | + ? { | |
| 5179 | + ...prev, | |
| 5180 | + auto_confirm_mode: | |
| 5181 | + opt.value as SettingsData["auto_confirm_mode"], | |
| 5182 | + // Keep the legacy boolean in sync for any | |
| 5183 | + // external reader (all → true, otherwise false). | |
| 5184 | + auto_confirm_bookings: opt.value === "all", | |
| 5185 | + } | |
| 5186 | + : prev, | |
| 5187 | + ) | |
| 5188 | + } | |
| 5189 | + className="mt-0.5 w-4 h-4 border-gray-300 text-blue-600 focus:ring-blue-500" | |
| 5190 | + /> | |
| 5191 | + <div className="flex-1"> | |
| 5192 | + <span className="font-medium text-sm">{opt.label}</span> | |
| 5193 | + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> | |
| 5194 | + {opt.sub} | |
| 5195 | + </p> | |
| 5196 | + </div> | |
| 5197 | + </label> | |
| 5198 | + ))} | |
| 5199 | + </div> | |
| 5932 | 5200 | </div> |
| 5933 | 5201 | |
| 5934 | 5202 | <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 5935 | 5203 | <input |
| @@ -6042,8 +5310,29 @@ | ||
| 6042 | 5310 | </p> |
| 6043 | 5311 | </div> |
| 6044 | 5312 | </div> |
| 6045 | 5313 | |
| 5314 | + <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> | |
| 5315 | + <FormField | |
| 5316 | + id="availability_horizon_months" | |
| 5317 | + label={__("Booking horizon (months)", "yatra")} | |
| 5318 | + description={__( | |
| 5319 | + "How far ahead customers can see and book dates on the storefront calendar — 1 to 36 months (default 12). A trip whose Available To date is earlier stops there. Long horizons make flexible-booking trips (no dates or rules configured) load more dates per page.", | |
| 5320 | + "yatra", | |
| 5321 | + )} | |
| 5322 | + > | |
| 5323 | + <Input | |
| 5324 | + id="availability_horizon_months" | |
| 5325 | + type="number" | |
| 5326 | + value={formData.availability_horizon_months ?? 12} | |
| 5327 | + name="availability_horizon_months" | |
| 5328 | + onChange={handleFieldChange} | |
| 5329 | + min="1" | |
| 5330 | + max="36" | |
| 5331 | + /> | |
| 5332 | + </FormField> | |
| 5333 | + </div> | |
| 5334 | + | |
| 6046 | 5335 | <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6047 | 5336 | <input |
| 6048 | 5337 | type="checkbox" |
| 6049 | 5338 | id="allow_waitlist" |
| @@ -9319,8 +8608,95 @@ | ||
| 9319 | 8608 | {__( |
| 9320 | 8609 | "Optional: submit this URL in Google Search Console or Bing Webmaster Tools. It is already listed in robots.txt, so search engines find it automatically.", |
| 9321 | 8610 | "yatra", |
| 9322 | 8611 | )} |
| 8612 | + | |
| 8613 | + {formData.enable_sitemap && ( | |
| 8614 | + <div className="p-3 border border-gray-200 dark:border-gray-700 rounded-md space-y-3"> | |
| 8615 | + <div> | |
| 8616 | + <Label className="font-medium"> | |
| 8617 | + {__("Content in the sitemap", "yatra")} | |
| 8618 | + </Label> | |
| 8619 | + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> | |
| 8620 | + {__( | |
| 8621 | + "Uncheck a type to keep it out of /yatra-sitemap.xml.", | |
| 8622 | + "yatra", | |
| 8623 | + )} | |
| 8624 | + </p> | |
| 8625 | + </div> | |
| 8626 | + <div className="grid grid-cols-2 sm:grid-cols-3 gap-2"> | |
| 8627 | + {[ | |
| 8628 | + { key: "trip", label: __("Trips", "yatra") }, | |
| 8629 | + { key: "destination", label: __("Destinations", "yatra") }, | |
| 8630 | + { key: "activity", label: __("Activities", "yatra") }, | |
| 8631 | + { key: "category", label: __("Trip categories", "yatra") }, | |
| 8632 | + { key: "archive", label: __("Listing pages", "yatra") }, | |
| 8633 | + ].map((t) => { | |
| 8634 | + const selected = formData.sitemap_types ?? []; | |
| 8635 | + const checked = selected.includes(t.key); | |
| 8636 | + return ( | |
| 8637 | + <label | |
| 8638 | + key={t.key} | |
| 8639 | + className="flex items-center gap-2 text-sm cursor-pointer" | |
| 8640 | + > | |
| 8641 | + <input | |
| 8642 | + type="checkbox" | |
| 8643 | + checked={checked} | |
| 8644 | + onChange={() => | |
| 8645 | + setFormData((prev) => { | |
| 8646 | + if (!prev) return prev; | |
| 8647 | + return { | |
| 8648 | + ...prev, | |
| 8649 | + sitemap_types: checked | |
| 8650 | + ? (prev.sitemap_types ?? []).filter( | |
| 8651 | + (x: string) => x !== t.key, | |
| 8652 | + ) | |
| 8653 | + : [...(prev.sitemap_types ?? []), t.key], | |
| 8654 | + }; | |
| 8655 | + }) | |
| 8656 | + } | |
| 8657 | + className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" | |
| 8658 | + /> | |
| 8659 | + <span>{t.label}</span> | |
| 8660 | + </label> | |
| 8661 | + ); | |
| 8662 | + })} | |
| 8663 | + </div> | |
| 8664 | + | |
| 8665 | + <div className="flex items-start gap-2 pt-2 border-t border-gray-200 dark:border-gray-700"> | |
| 8666 | + <input | |
| 8667 | + type="checkbox" | |
| 8668 | + id="sitemap_noindex_excluded" | |
| 8669 | + name="sitemap_noindex_excluded" | |
| 8670 | + checked={formData.sitemap_noindex_excluded} | |
| 8671 | + onChange={(e) => | |
| 8672 | + setFormData((prev) => { | |
| 8673 | + if (!prev) return prev; | |
| 8674 | + return { | |
| 8675 | + ...prev, | |
| 8676 | + sitemap_noindex_excluded: e.target.checked, | |
| 8677 | + }; | |
| 8678 | + }) | |
| 8679 | + } | |
| 8680 | + className="mt-0.5 w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" | |
| 8681 | + /> | |
| 8682 | + <div className="flex-1"> | |
| 8683 | + <Label | |
| 8684 | + htmlFor="sitemap_noindex_excluded" | |
| 8685 | + className="font-medium cursor-pointer" | |
| 8686 | + > | |
| 8687 | + {__("Also add noindex to excluded types", "yatra")} | |
| 8688 | + </Label> | |
| 8689 | + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> | |
| 8690 | + {__( | |
| 8691 | + "Leaving a type out of the sitemap does not stop Google indexing it. Turn this on to send noindex on those pages as well — they will drop out of search results, so only do this for content you do not want found.", | |
| 8692 | + "yatra", | |
| 8693 | + )} | |
| 8694 | + </p> | |
| 8695 | + </div> | |
| 8696 | + </div> | |
| 8697 | + </div> | |
| 8698 | + )} | |
| 9323 | 8699 | </p> |
| 9324 | 8700 | </div> |
| 9325 | 8701 | )} |
| 9326 | 8702 | </div> |