| @@ -4,10 +4,9 @@ | ||
| 4 | 4 | Search, |
| 5 | 5 | Filter, |
| 6 | 6 | ArrowUpDown, |
| 7 | 7 | ExternalLink, |
| 8 | - Crown, | |
| 9 | - Sparkles, | |
| 8 | + ArrowUpRight, | |
| 10 | 9 | } from "lucide-react"; |
| 11 | 10 | import { __ } from "../lib/i18n"; |
| 12 | 11 | import { PageHeader } from "../components/common/PageHeader"; |
| 13 | 12 | import { |
| @@ -30,8 +29,25 @@ | ||
| 30 | 29 | } from "../hooks/useModules"; |
| 31 | 30 | import { usePermissions } from "../hooks/usePermissions"; |
| 32 | 31 | import { PremiumUpgradeDialog } from "../components/modules/PremiumUpgradeDialog"; |
| 33 | 32 | |
| 33 | +/** | |
| 34 | + * Build the "Learn more" link to a module's dedicated feature page on | |
| 35 | + * wpyatra.com (e.g. https://wpyatra.com/features/dynamic-pricing/), tagged with | |
| 36 | + * UTM params. The feature-page slug matches the docs slug, so we derive it from | |
| 37 | + * the module's docs_url last segment (defined once per module on the backend). | |
| 38 | + * Returns null when there's no docs_url to derive a slug from. | |
| 39 | + */ | |
| 40 | +const featureUrlFromDocs = (docsUrl?: string): string | null => { | |
| 41 | + if (!docsUrl) return null; | |
| 42 | + const slug = docsUrl.replace(/\/+$/, "").split("/").pop(); | |
| 43 | + if (!slug) return null; | |
| 44 | + const params = | |
| 45 | + "utm_source=plugin&utm_medium=modules&utm_campaign=learn-more&utm_content=" + | |
| 46 | + encodeURIComponent(slug); | |
| 47 | + return `https://wpyatra.com/features/${slug}/?${params}`; | |
| 48 | +}; | |
| 49 | + | |
| 34 | 50 | const Modules: React.FC = () => { |
| 35 | 51 | const { can } = usePermissions(); |
| 36 | 52 | const canManageModules = can("yatra_edit_trips"); |
| 37 | 53 | const { data: modules = [], isLoading, error } = useModulesQuery(); |
| @@ -69,13 +85,16 @@ | ||
| 69 | 85 | const order: ModulePlan[] = ["free", "personal", "growth", "agency"]; |
| 70 | 86 | return order.filter((tier) => set.has(tier)); |
| 71 | 87 | }, [modules]); |
| 72 | 88 | |
| 89 | + // 2026 plan rename: display labels changed (Personal→Starter, Agency→Scale) | |
| 90 | + // but the plan *slugs* ('personal'/'agency') stay — they mirror the Pro | |
| 91 | + // internal tier slugs and keep existing-user feature gating intact. | |
| 73 | 92 | const planLabels: Record<ModulePlan, string> = { |
| 74 | 93 | free: __("Free", "yatra"), |
| 75 | - personal: __("Personal", "yatra"), | |
| 94 | + personal: __("Starter", "yatra"), | |
| 76 | 95 | growth: __("Growth", "yatra"), |
| 77 | - agency: __("Agency", "yatra"), | |
| 96 | + agency: __("Scale", "yatra"), | |
| 78 | 97 | }; |
| 79 | 98 | |
| 80 | 99 | const filteredModules = useMemo(() => { |
| 81 | 100 | let list = modules; |
| @@ -526,18 +545,18 @@ | ||
| 526 | 545 | {module.name} |
| 527 | 546 | {module.is_core && ( |
| 528 | 547 | <Badge variant="outline">{__("Core", "yatra")}</Badge> |
| 529 | 548 | )} |
| 530 | - {/* Plan badge — Personal for any Pro module, Agency | |
| 549 | + {/* Plan badge — Starter for any Pro module, Scale | |
| 531 | 550 | for white-label-tier modules. Always visible so |
| 532 | - customers know which plan unlocks the module. */} | |
| 551 | + customers know which plan unlocks the module. | |
| 552 | + (Slug stays 'agency'; label is the new "Scale".) */} | |
| 533 | 553 | {module.plan === "agency" && ( |
| 534 | 554 | <Badge |
| 535 | 555 | variant="outline" |
| 536 | - className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-gradient-to-r from-purple-600 to-indigo-500 border-purple-700 text-white shadow-sm dark:from-purple-500 dark:to-indigo-400 dark:border-purple-400 dark:text-white" | |
| 556 | + className="px-2 py-0.5 rounded-full bg-gradient-to-r from-purple-600 to-indigo-500 border-purple-700 text-white shadow-sm dark:from-purple-500 dark:to-indigo-400 dark:border-purple-400 dark:text-white" | |
| 537 | 557 | > |
| 538 | - <Crown className="w-3 h-3" /> | |
| 539 | - {__("Agency", "yatra")} | |
| 558 | + {__("Scale", "yatra")} | |
| 540 | 559 | </Badge> |
| 541 | 560 | )} |
| 542 | 561 | {/* Growth tier — AI Assistant + any future module |
| 543 | 562 | requiring the Growth-or-Agency band. Without |
| @@ -546,11 +565,10 @@ | ||
| 546 | 565 | unlocks AI features. */} |
| 547 | 566 | {module.plan === "growth" && ( |
| 548 | 567 | <Badge |
| 549 | 568 | variant="outline" |
| 550 | - className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-gradient-to-r from-emerald-500 to-teal-500 border-emerald-600 text-white shadow-sm dark:from-emerald-500 dark:to-teal-400 dark:border-emerald-400 dark:text-white" | |
| 569 | + className="px-2 py-0.5 rounded-full bg-gradient-to-r from-emerald-500 to-teal-500 border-emerald-600 text-white shadow-sm dark:from-emerald-500 dark:to-teal-400 dark:border-emerald-400 dark:text-white" | |
| 551 | 570 | > |
| 552 | - <Sparkles className="w-3 h-3" /> | |
| 553 | 571 | {__("Growth", "yatra")} |
| 554 | 572 | </Badge> |
| 555 | 573 | )} |
| 556 | 574 | {module.plan === "personal" && ( |
| @@ -557,21 +575,11 @@ | ||
| 557 | 575 | <Badge |
| 558 | 576 | variant="outline" |
| 559 | 577 | className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-blue-50 border-blue-200 text-blue-700 dark:bg-blue-900/30 dark:border-blue-500/40 dark:text-blue-200" |
| 560 | 578 | > |
| 561 | - {__("Personal", "yatra")} | |
| 579 | + {__("Starter", "yatra")} | |
| 562 | 580 | </Badge> |
| 563 | 581 | )} |
| 564 | - {/* Only show Premium badge if module is premium AND not available (Pro not active) */} | |
| 565 | - {module.is_premium && !module.is_available && ( | |
| 566 | - <Badge | |
| 567 | - variant="outline" | |
| 568 | - className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-gradient-to-r from-orange-500 to-orange-400 border-orange-600 text-white shadow-sm dark:bg-orange-600/60 dark:border-orange-500 dark:text-orange-50" | |
| 569 | - > | |
| 570 | - <Crown className="w-3 h-3" /> | |
| 571 | - {__("Premium", "yatra")} | |
| 572 | - </Badge> | |
| 573 | - )} | |
| 574 | 582 | </CardTitle> |
| 575 | 583 | <CardDescription className="mt-1 text-sm line-clamp-3"> |
| 576 | 584 | {module.description} |
| 577 | 585 | </CardDescription> |
| @@ -586,9 +594,12 @@ | ||
| 586 | 594 | </div> |
| 587 | 595 | </div> |
| 588 | 596 | </CardHeader> |
| 589 | 597 | <CardContent className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"> |
| 590 | - <div className="flex items-center gap-2"> | |
| 598 | + <div | |
| 599 | + className="flex items-center gap-2" | |
| 600 | + onClick={(e) => e.stopPropagation()} | |
| 601 | + > | |
| 591 | 602 | {module.video_url && ( |
| 592 | 603 | <a |
| 593 | 604 | href={module.video_url} |
| 594 | 605 | target="_blank" |
| @@ -610,9 +621,12 @@ | ||
| 610 | 621 | {__("Video", "yatra")} |
| 611 | 622 | </a> |
| 612 | 623 | )} |
| 613 | 624 | </div> |
| 614 | - <div className="flex items-center gap-2"> | |
| 625 | + <div | |
| 626 | + className="flex items-center gap-2" | |
| 627 | + onClick={(e) => e.stopPropagation()} | |
| 628 | + > | |
| 615 | 629 | {module.slug === "dynamic_form_field" && module.enabled && ( |
| 616 | 630 | <a |
| 617 | 631 | href={`${window.location.origin}/wp-admin/admin.php?page=yatra&subpage=settings`} |
| 618 | 632 | className="flex items-center gap-1 text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200 text-xs font-medium" |
| @@ -630,8 +644,21 @@ | ||
| 630 | 644 | <circle cx="12" cy="12" r="3"></circle> |
| 631 | 645 | <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> |
| 632 | 646 | </svg> |
| 633 | 647 | {__("Settings", "yatra")} |
| 648 | + </a> | |
| 649 | + )} | |
| 650 | + {featureUrlFromDocs(module.docs_url) && ( | |
| 651 | + <a | |
| 652 | + href={featureUrlFromDocs(module.docs_url) as string} | |
| 653 | + target="_blank" | |
| 654 | + rel="noopener noreferrer" | |
| 655 | + onClick={(e) => e.stopPropagation()} | |
| 656 | + className="flex items-center gap-1 text-blue-600 hover:text-blue-500 dark:text-blue-400 text-xs font-medium" | |
| 657 | + title={__("Learn more about this feature", "yatra")} | |
| 658 | + > | |
| 659 | + <ArrowUpRight className="w-3 h-3" /> | |
| 660 | + {__("Learn more", "yatra")} | |
| 634 | 661 | </a> |
| 635 | 662 | )} |
| 636 | 663 | {module.docs_url && ( |
| 637 | 664 | <a |