| @@ -25,9 +25,29 @@ | ||
| 25 | 25 | } from "../components/ui/card"; |
| 26 | 26 | import { ConditionalRender } from "../components/ui/conditional-render"; |
| 27 | 27 | import { IconPicker, IconPickerValue } from "../components/ui/icon-picker"; |
| 28 | 28 | import { RichTextEditor } from "../components/ui/rich-text-editor"; |
| 29 | +import { | |
| 30 | + ClassificationLandingPageField, | |
| 31 | + fetchPublishedPagePermalink, | |
| 32 | + parseLandingPageIdFromMetadata, | |
| 33 | +} from "../components/classifications/ClassificationLandingPageField"; | |
| 29 | 34 | |
| 35 | +/** DB / REST may send 0|1 as number or string; never use Boolean() on string "0". */ | |
| 36 | +function parseTripCategoryIsFeatured(raw: unknown): boolean { | |
| 37 | + if (raw === true || raw === 1) { | |
| 38 | + return true; | |
| 39 | + } | |
| 40 | + if (raw === false || raw === 0 || raw === null || raw === undefined) { | |
| 41 | + return false; | |
| 42 | + } | |
| 43 | + if (typeof raw === "string") { | |
| 44 | + const t = raw.trim(); | |
| 45 | + return t === "1" || t.toLowerCase() === "true"; | |
| 46 | + } | |
| 47 | + return Number(raw) === 1; | |
| 48 | +} | |
| 49 | + | |
| 30 | 50 | interface CategoryFormData { |
| 31 | 51 | name: string; |
| 32 | 52 | slug: string; |
| 33 | 53 | description: string; |
| @@ -33,11 +53,14 @@ | ||
| 33 | 53 | description: string; |
| 34 | 54 | icon: IconPickerValue | null; |
| 35 | 55 | parent_id: number | ""; |
| 36 | 56 | status: string; |
| 57 | + /** Trip category card "Featured" badge on shortcode / Trip Category block */ | |
| 58 | + is_featured: boolean; | |
| 37 | 59 | seo_title: string; |
| 38 | 60 | seo_description: string; |
| 39 | 61 | seo_keywords: string; |
| 62 | + landing_page_id: number | null; | |
| 40 | 63 | } |
| 41 | 64 | |
| 42 | 65 | interface CategoryOption { |
| 43 | 66 | id: number; |
| @@ -55,11 +78,13 @@ | ||
| 55 | 78 | description: "", |
| 56 | 79 | icon: null, |
| 57 | 80 | parent_id: "", |
| 58 | 81 | status: "publish", |
| 82 | + is_featured: false, | |
| 59 | 83 | seo_title: "", |
| 60 | 84 | seo_description: "", |
| 61 | 85 | seo_keywords: "", |
| 86 | + landing_page_id: null, | |
| 62 | 87 | }); |
| 63 | 88 | const [errors, setErrors] = useState<Record<string, string>>({}); |
| 64 | 89 | const [isSubmitting, setIsSubmitting] = useState(false); |
| 65 | 90 | const [isSlugEditable, setIsSlugEditable] = useState(false); |
| @@ -160,11 +185,17 @@ | ||
| 160 | 185 | description: categoryData.description || "", |
| 161 | 186 | icon: (categoryData.icon as IconPickerValue) || null, |
| 162 | 187 | parent_id: categoryData.parent_id ?? "", |
| 163 | 188 | status: categoryData.status || "publish", |
| 189 | + is_featured: parseTripCategoryIsFeatured( | |
| 190 | + (categoryData as { is_featured?: unknown }).is_featured, | |
| 191 | + ), | |
| 164 | 192 | seo_title: categoryData.metadata?.seo_title || "", |
| 165 | 193 | seo_description: categoryData.metadata?.seo_description || "", |
| 166 | 194 | seo_keywords: categoryData.metadata?.seo_keywords || "", |
| 195 | + landing_page_id: parseLandingPageIdFromMetadata( | |
| 196 | + categoryData.metadata as { landing_page_id?: unknown }, | |
| 197 | + ), | |
| 167 | 198 | }); |
| 168 | 199 | } |
| 169 | 200 | }, [categoryData, isEditMode]); |
| 170 | 201 | |
| @@ -217,11 +248,11 @@ | ||
| 217 | 248 | } |
| 218 | 249 | |
| 219 | 250 | if (!formData.slug.trim()) { |
| 220 | 251 | newErrors.slug = __("Slug is required", "yatra"); |
| 221 | - } else if (!/^[a-z0-9-]+$/.test(formData.slug)) { | |
| 252 | + } else if (!/^[\p{L}\p{N}-]+$/u.test(formData.slug)) { | |
| 222 | 253 | newErrors.slug = __( |
| 223 | - "Slug can only contain lowercase letters, numbers, and hyphens", | |
| 254 | + "Slug can only contain letters, numbers, and hyphens", | |
| 224 | 255 | "yatra", |
| 225 | 256 | ); |
| 226 | 257 | } |
| 227 | 258 | |
| @@ -245,11 +276,13 @@ | ||
| 245 | 276 | description: data.description.trim(), |
| 246 | 277 | icon: data.icon, |
| 247 | 278 | parent_id: data.parent_id === "" ? null : Number(data.parent_id), |
| 248 | 279 | status: data.status, |
| 280 | + is_featured: data.is_featured ? 1 : 0, | |
| 249 | 281 | seo_title: data.seo_title.trim(), |
| 250 | 282 | seo_description: data.seo_description.trim(), |
| 251 | 283 | seo_keywords: data.seo_keywords.trim(), |
| 284 | + landing_page_id: data.landing_page_id ?? null, | |
| 252 | 285 | }; |
| 253 | 286 | |
| 254 | 287 | if (isEditMode && isSlugEditable) { |
| 255 | 288 | payload.preserve_slug = true; |
| @@ -392,9 +425,21 @@ | ||
| 392 | 425 | </Button> |
| 393 | 426 | {formData.slug && ( |
| 394 | 427 | <Button |
| 395 | 428 | variant="outline" |
| 396 | - onClick={() => { | |
| 429 | + onClick={async () => { | |
| 430 | + if ( | |
| 431 | + formData.landing_page_id && | |
| 432 | + window.yatraAdmin?.customLandingPagesModuleEnabled | |
| 433 | + ) { | |
| 434 | + const link = await fetchPublishedPagePermalink( | |
| 435 | + formData.landing_page_id, | |
| 436 | + ); | |
| 437 | + if (link) { | |
| 438 | + window.open(link, "_blank", "noopener,noreferrer"); | |
| 439 | + return; | |
| 440 | + } | |
| 441 | + } | |
| 397 | 442 | const targetUrl = buildYatraSinglePreviewUrl({ |
| 398 | 443 | entity: "category", |
| 399 | 444 | slug: formData.slug || "", |
| 400 | 445 | bases: settings as Record<string, unknown> | null, |
| @@ -515,8 +560,16 @@ | ||
| 515 | 560 | /> |
| 516 | 561 | </div> |
| 517 | 562 | </CardContent> |
| 518 | 563 | </Card> |
| 564 | + | |
| 565 | + <ClassificationLandingPageField | |
| 566 | + selectId="yatra-category-landing-page" | |
| 567 | + value={formData.landing_page_id} | |
| 568 | + onChange={(id) => | |
| 569 | + setFormData((prev) => ({ ...prev, landing_page_id: id })) | |
| 570 | + } | |
| 571 | + /> | |
| 519 | 572 | </div> |
| 520 | 573 | |
| 521 | 574 | <div className="space-y-3"> |
| 522 | 575 | <Card> |
| @@ -590,8 +643,39 @@ | ||
| 590 | 643 | <option value="draft">{__("Draft", "yatra")}</option> |
| 591 | 644 | <option value="publish">{__("Publish", "yatra")}</option> |
| 592 | 645 | <option value="trash">{__("Trash", "yatra")}</option> |
| 593 | 646 | </Select> |
| 647 | + </CardContent> | |
| 648 | + </Card> | |
| 649 | + | |
| 650 | + <Card> | |
| 651 | + <CardHeader className="pb-2"> | |
| 652 | + <CardTitle className="text-base"> | |
| 653 | + {__("Visibility", "yatra")} | |
| 654 | + </CardTitle> | |
| 655 | + </CardHeader> | |
| 656 | + <CardContent> | |
| 657 | + <label className="flex items-start gap-3 cursor-pointer"> | |
| 658 | + <input | |
| 659 | + type="checkbox" | |
| 660 | + className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" | |
| 661 | + checked={formData.is_featured} | |
| 662 | + onChange={(e) => | |
| 663 | + handleFieldChange("is_featured", e.target.checked) | |
| 664 | + } | |
| 665 | + /> | |
| 666 | + <span> | |
| 667 | + <span className="block text-sm font-medium text-gray-900 dark:text-gray-100"> | |
| 668 | + {__("Featured category", "yatra")} | |
| 669 | + </span> | |
| 670 | + <span className="block text-xs text-gray-500 dark:text-gray-400 mt-0.5"> | |
| 671 | + {__( | |
| 672 | + "Shows a “Featured” badge on Trip Category blocks and the [yatra_trip_category] shortcode cards.", | |
| 673 | + "yatra", | |
| 674 | + )} | |
| 675 | + </span> | |
| 676 | + </span> | |
| 677 | + </label> | |
| 594 | 678 | </CardContent> |
| 595 | 679 | </Card> |
| 596 | 680 | |
| 597 | 681 | {/* SEO Settings */} |