| @@ -4,8 +4,9 @@ | ||
| 4 | 4 | Search, |
| 5 | 5 | Filter, |
| 6 | 6 | ArrowUpDown, |
| 7 | 7 | ExternalLink, |
| 8 | + ArrowUpRight, | |
| 8 | 9 | } from "lucide-react"; |
| 9 | 10 | import { __ } from "../lib/i18n"; |
| 10 | 11 | import { PageHeader } from "../components/common/PageHeader"; |
| 11 | 12 | import { |
| @@ -28,8 +29,25 @@ | ||
| 28 | 29 | } from "../hooks/useModules"; |
| 29 | 30 | import { usePermissions } from "../hooks/usePermissions"; |
| 30 | 31 | import { PremiumUpgradeDialog } from "../components/modules/PremiumUpgradeDialog"; |
| 31 | 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 | + | |
| 32 | 50 | const Modules: React.FC = () => { |
| 33 | 51 | const { can } = usePermissions(); |
| 34 | 52 | const canManageModules = can("yatra_edit_trips"); |
| 35 | 53 | const { data: modules = [], isLoading, error } = useModulesQuery(); |
| @@ -67,13 +85,16 @@ | ||
| 67 | 85 | const order: ModulePlan[] = ["free", "personal", "growth", "agency"]; |
| 68 | 86 | return order.filter((tier) => set.has(tier)); |
| 69 | 87 | }, [modules]); |
| 70 | 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. | |
| 71 | 92 | const planLabels: Record<ModulePlan, string> = { |
| 72 | 93 | free: __("Free", "yatra"), |
| 73 | - personal: __("Personal", "yatra"), | |
| 94 | + personal: __("Starter", "yatra"), | |
| 74 | 95 | growth: __("Growth", "yatra"), |
| 75 | - agency: __("Agency", "yatra"), | |
| 96 | + agency: __("Scale", "yatra"), | |
| 76 | 97 | }; |
| 77 | 98 | |
| 78 | 99 | const filteredModules = useMemo(() => { |
| 79 | 100 | let list = modules; |
| @@ -524,17 +545,18 @@ | ||
| 524 | 545 | {module.name} |
| 525 | 546 | {module.is_core && ( |
| 526 | 547 | <Badge variant="outline">{__("Core", "yatra")}</Badge> |
| 527 | 548 | )} |
| 528 | - {/* Plan badge — Personal for any Pro module, Agency | |
| 549 | + {/* Plan badge — Starter for any Pro module, Scale | |
| 529 | 550 | for white-label-tier modules. Always visible so |
| 530 | - customers know which plan unlocks the module. */} | |
| 551 | + customers know which plan unlocks the module. | |
| 552 | + (Slug stays 'agency'; label is the new "Scale".) */} | |
| 531 | 553 | {module.plan === "agency" && ( |
| 532 | 554 | <Badge |
| 533 | 555 | variant="outline" |
| 534 | 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" |
| 535 | 557 | > |
| 536 | - {__("Agency", "yatra")} | |
| 558 | + {__("Scale", "yatra")} | |
| 537 | 559 | </Badge> |
| 538 | 560 | )} |
| 539 | 561 | {/* Growth tier — AI Assistant + any future module |
| 540 | 562 | requiring the Growth-or-Agency band. Without |
| @@ -553,9 +575,9 @@ | ||
| 553 | 575 | <Badge |
| 554 | 576 | variant="outline" |
| 555 | 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" |
| 556 | 578 | > |
| 557 | - {__("Personal", "yatra")} | |
| 579 | + {__("Starter", "yatra")} | |
| 558 | 580 | </Badge> |
| 559 | 581 | )} |
| 560 | 582 | </CardTitle> |
| 561 | 583 | <CardDescription className="mt-1 text-sm line-clamp-3"> |
| @@ -572,9 +594,12 @@ | ||
| 572 | 594 | </div> |
| 573 | 595 | </div> |
| 574 | 596 | </CardHeader> |
| 575 | 597 | <CardContent className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"> |
| 576 | - <div className="flex items-center gap-2"> | |
| 598 | + <div | |
| 599 | + className="flex items-center gap-2" | |
| 600 | + onClick={(e) => e.stopPropagation()} | |
| 601 | + > | |
| 577 | 602 | {module.video_url && ( |
| 578 | 603 | <a |
| 579 | 604 | href={module.video_url} |
| 580 | 605 | target="_blank" |
| @@ -596,9 +621,12 @@ | ||
| 596 | 621 | {__("Video", "yatra")} |
| 597 | 622 | </a> |
| 598 | 623 | )} |
| 599 | 624 | </div> |
| 600 | - <div className="flex items-center gap-2"> | |
| 625 | + <div | |
| 626 | + className="flex items-center gap-2" | |
| 627 | + onClick={(e) => e.stopPropagation()} | |
| 628 | + > | |
| 601 | 629 | {module.slug === "dynamic_form_field" && module.enabled && ( |
| 602 | 630 | <a |
| 603 | 631 | href={`${window.location.origin}/wp-admin/admin.php?page=yatra&subpage=settings`} |
| 604 | 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" |
| @@ -616,8 +644,21 @@ | ||
| 616 | 644 | <circle cx="12" cy="12" r="3"></circle> |
| 617 | 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> |
| 618 | 646 | </svg> |
| 619 | 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")} | |
| 620 | 661 | </a> |
| 621 | 662 | )} |
| 622 | 663 | {module.docs_url && ( |
| 623 | 664 | <a |