@windrun-huaiin/third-ui 5.14.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clerk/index.d.mts +2 -21
- package/dist/clerk/index.d.ts +2 -21
- package/dist/clerk/index.js +5 -2884
- package/dist/clerk/index.js.map +1 -1
- package/dist/clerk/index.mjs +3 -2872
- package/dist/clerk/index.mjs.map +1 -1
- package/dist/clerk/server.d.mts +28 -0
- package/dist/clerk/server.d.ts +28 -0
- package/dist/clerk/server.js +3025 -0
- package/dist/clerk/server.js.map +1 -0
- package/dist/clerk/server.mjs +2991 -0
- package/dist/clerk/server.mjs.map +1 -0
- package/dist/fuma/mdx/index.d.mts +1 -12
- package/dist/fuma/mdx/index.d.ts +1 -12
- package/dist/fuma/mdx/index.js +49 -263
- package/dist/fuma/mdx/index.js.map +1 -1
- package/dist/fuma/mdx/index.mjs +50 -262
- package/dist/fuma/mdx/index.mjs.map +1 -1
- package/dist/fuma/server.d.mts +15 -2
- package/dist/fuma/server.d.ts +15 -2
- package/dist/fuma/server.js +234 -49
- package/dist/fuma/server.js.map +1 -1
- package/dist/fuma/server.mjs +231 -48
- package/dist/fuma/server.mjs.map +1 -1
- package/dist/lib/server.d.mts +509 -465
- package/dist/lib/server.d.ts +509 -465
- package/dist/main/index.d.mts +5 -56
- package/dist/main/index.d.ts +5 -56
- package/dist/main/index.js +646 -1322
- package/dist/main/index.js.map +1 -1
- package/dist/main/index.mjs +675 -1342
- package/dist/main/index.mjs.map +1 -1
- package/dist/main/server.d.mts +64 -0
- package/dist/main/server.d.ts +64 -0
- package/dist/main/server.js +4166 -0
- package/dist/main/server.js.map +1 -0
- package/dist/main/server.mjs +4128 -0
- package/dist/main/server.mjs.map +1 -0
- package/package.json +12 -2
- package/src/clerk/clerk-organization-client.tsx +50 -0
- package/src/clerk/clerk-organization.tsx +21 -38
- package/src/clerk/clerk-page-generator.tsx +0 -2
- package/src/clerk/clerk-provider-client.tsx +1 -1
- package/src/clerk/clerk-user-client.tsx +64 -0
- package/src/clerk/clerk-user.tsx +29 -58
- package/src/clerk/index.ts +1 -4
- package/src/clerk/server.ts +3 -0
- package/src/fuma/{mdx/fuma-banner-suit.tsx → fuma-banner-suit.tsx} +3 -6
- package/src/fuma/mdx/banner.tsx +0 -1
- package/src/fuma/mdx/index.ts +0 -2
- package/src/fuma/mdx/mermaid.tsx +3 -1
- package/src/fuma/mdx/toc-footer-wrapper.tsx +1 -0
- package/src/fuma/mdx/zia-file.tsx +0 -1
- package/src/fuma/server.ts +3 -1
- package/src/fuma/{mdx/site-x.tsx → site-x.tsx} +4 -5
- package/src/main/cta.tsx +33 -10
- package/src/main/faq-interactive.tsx +68 -0
- package/src/main/faq.tsx +62 -38
- package/src/main/features.tsx +40 -11
- package/src/main/footer.tsx +27 -16
- package/src/main/gallery-interactive.tsx +171 -0
- package/src/main/gallery.tsx +54 -101
- package/src/main/index.ts +1 -10
- package/src/main/language-detector.tsx +175 -0
- package/src/main/price-plan-interactive.tsx +273 -0
- package/src/main/price-plan.tsx +112 -129
- package/src/main/seo-content.tsx +46 -13
- package/src/main/server.ts +10 -0
- package/src/main/tips.tsx +48 -22
- package/src/main/usage.tsx +43 -11
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* MIT License
|
|
4
|
+
* Copyright (c) 2025 D8ger
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*/
|
|
9
|
+
'use client'
|
|
10
|
+
|
|
11
|
+
import { globalLucideIcons as icons } from "@base-ui/components/global-icon"
|
|
12
|
+
import { useLocale, useTranslations } from 'next-intl'
|
|
13
|
+
import { useRouter } from 'next/navigation'
|
|
14
|
+
import { useEffect, useState } from 'react'
|
|
15
|
+
|
|
16
|
+
type I18nConfig = {
|
|
17
|
+
locales: readonly string[];
|
|
18
|
+
detector: {
|
|
19
|
+
storagePrefix: string;
|
|
20
|
+
storageKey: string;
|
|
21
|
+
autoCloseTimeout: number;
|
|
22
|
+
expirationDays: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
interface LanguageDetectorProps {
|
|
27
|
+
i18nConfig: I18nConfig;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Locale = string;
|
|
31
|
+
|
|
32
|
+
interface LanguagePreference {
|
|
33
|
+
locale: string;
|
|
34
|
+
status: 'accepted' | 'rejected';
|
|
35
|
+
timestamp: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function LanguageDetector({ i18nConfig }: LanguageDetectorProps) {
|
|
39
|
+
const [show, setShow] = useState(false)
|
|
40
|
+
const [detectedLocale, setDetectedLocale] = useState<Locale | null>(null)
|
|
41
|
+
const currentLocale = useLocale()
|
|
42
|
+
const router = useRouter()
|
|
43
|
+
const t = useTranslations('languageDetection')
|
|
44
|
+
|
|
45
|
+
// Get the storage key from the configuration
|
|
46
|
+
const LANGUAGE_PREFERENCE_KEY = `${i18nConfig.detector.storagePrefix}-${i18nConfig.detector.storageKey}`
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
// Get the browser language
|
|
50
|
+
const browserLang = navigator.language.split('-')[0] as Locale
|
|
51
|
+
|
|
52
|
+
// Get the language preference from localStorage
|
|
53
|
+
const savedPreference = localStorage.getItem(LANGUAGE_PREFERENCE_KEY)
|
|
54
|
+
const preference: LanguagePreference | null = savedPreference
|
|
55
|
+
? JSON.parse(savedPreference)
|
|
56
|
+
: null
|
|
57
|
+
|
|
58
|
+
// Check if the language detection box should be displayed
|
|
59
|
+
const shouldShowDetector = () => {
|
|
60
|
+
if (!preference) return true;
|
|
61
|
+
|
|
62
|
+
// If the stored language is the same as the current language, do not display the detection box
|
|
63
|
+
if (preference.locale === currentLocale) return false;
|
|
64
|
+
|
|
65
|
+
// If the user has previously rejected switching to this language, do not display the detection box
|
|
66
|
+
if (preference.status === 'rejected' && preference.locale === browserLang) return false;
|
|
67
|
+
|
|
68
|
+
// If the user has previously accepted switching to this language, do not display the detection box
|
|
69
|
+
if (preference.status === 'accepted' && preference.locale === currentLocale) return false;
|
|
70
|
+
|
|
71
|
+
// Use the expiration time from the configuration
|
|
72
|
+
const expirationMs = i18nConfig.detector.expirationDays * 24 * 60 * 60 * 1000;
|
|
73
|
+
if (Date.now() - preference.timestamp < expirationMs) return false;
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if the browser language is in the supported language list and needs to display the detection box
|
|
79
|
+
if ((i18nConfig.locales as string[]).includes(browserLang) &&
|
|
80
|
+
browserLang !== currentLocale &&
|
|
81
|
+
shouldShowDetector()) {
|
|
82
|
+
setDetectedLocale(browserLang)
|
|
83
|
+
setShow(true)
|
|
84
|
+
|
|
85
|
+
// Use the automatic closing time from the configuration
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
console.log('[LanguageDetector] Auto closing after timeout')
|
|
88
|
+
setShow(false)
|
|
89
|
+
// Save the rejected state when the automatic closing occurs
|
|
90
|
+
savePreference(browserLang, 'rejected')
|
|
91
|
+
}, i18nConfig.detector.autoCloseTimeout)
|
|
92
|
+
|
|
93
|
+
return () => clearTimeout(timer)
|
|
94
|
+
}
|
|
95
|
+
}, [currentLocale])
|
|
96
|
+
|
|
97
|
+
// Save the language preference to localStorage
|
|
98
|
+
const savePreference = (locale: string, status: 'accepted' | 'rejected') => {
|
|
99
|
+
const preference: LanguagePreference = {
|
|
100
|
+
locale,
|
|
101
|
+
status,
|
|
102
|
+
timestamp: Date.now()
|
|
103
|
+
}
|
|
104
|
+
localStorage.setItem(LANGUAGE_PREFERENCE_KEY, JSON.stringify(preference))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const handleLanguageChange = () => {
|
|
108
|
+
if (detectedLocale) {
|
|
109
|
+
// Save the accepted state
|
|
110
|
+
savePreference(detectedLocale, 'accepted')
|
|
111
|
+
|
|
112
|
+
// Get the current path
|
|
113
|
+
const pathname = window.location.pathname
|
|
114
|
+
// Replace the language part
|
|
115
|
+
const newPathname = pathname.replace(`/${currentLocale}`, `/${detectedLocale}`)
|
|
116
|
+
// Redirect to the new path
|
|
117
|
+
router.push(newPathname)
|
|
118
|
+
setShow(false)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const handleClose = () => {
|
|
123
|
+
if (detectedLocale) {
|
|
124
|
+
// Save the rejected state
|
|
125
|
+
savePreference(detectedLocale, 'rejected')
|
|
126
|
+
}
|
|
127
|
+
setShow(false)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!detectedLocale || !show) return null
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="fixed top-16 right-4 z-40 w-[420px]">
|
|
134
|
+
<div className={`shadow-lg rounded-lg transition-all duration-300 ${show ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'}
|
|
135
|
+
bg-linear-to-r from-purple-100/95 via-white/95 to-purple-100/95 backdrop-blur-xs
|
|
136
|
+
animate-gradient-x`}>
|
|
137
|
+
<div className="relative px-6 py-4 overflow-hidden">
|
|
138
|
+
<div className="relative z-10 flex flex-col gap-3">
|
|
139
|
+
<div className="flex items-start justify-between gap-4">
|
|
140
|
+
<div className="flex flex-col gap-1.5">
|
|
141
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
|
142
|
+
{t('title')}
|
|
143
|
+
</h3>
|
|
144
|
+
<p className="text-base text-gray-600">
|
|
145
|
+
{t('description')} <span className="text-purple-500 font-semibold">{detectedLocale === 'zh' ? '中文' : 'English'}</span>?
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
<button
|
|
149
|
+
onClick={handleClose}
|
|
150
|
+
className="text-gray-500 hover:text-gray-700"
|
|
151
|
+
>
|
|
152
|
+
<icons.X className="h-5 w-5" />
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
<div className="flex items-center gap-3">
|
|
156
|
+
<button
|
|
157
|
+
onClick={handleClose}
|
|
158
|
+
className="flex-1 px-4 py-2 text-base bg-gray-100 text-gray-600 rounded-md hover:bg-gray-200"
|
|
159
|
+
>
|
|
160
|
+
{t('close')}
|
|
161
|
+
</button>
|
|
162
|
+
<button
|
|
163
|
+
onClick={handleLanguageChange}
|
|
164
|
+
className="flex-1 px-4 py-2 text-base bg-purple-500 text-white rounded-md hover:bg-purple-600"
|
|
165
|
+
>
|
|
166
|
+
{t('changeAction')}
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="absolute inset-0 bg-linear-to-r from-transparent via-purple-200/30 to-transparent animate-shimmer" />
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect } from 'react';
|
|
5
|
+
import { useRouter } from 'next/navigation';
|
|
6
|
+
import { cn } from '@lib/utils';
|
|
7
|
+
|
|
8
|
+
// Import interfaces from the main component
|
|
9
|
+
interface BillingOption {
|
|
10
|
+
key: string;
|
|
11
|
+
discount: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Prices {
|
|
15
|
+
[key: string]: number | string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PricePlanAppConfig {
|
|
19
|
+
billingOptions: BillingOption[];
|
|
20
|
+
prices: Prices;
|
|
21
|
+
minPlanFeaturesCount: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PricePlanData {
|
|
25
|
+
title: string;
|
|
26
|
+
subtitle: string;
|
|
27
|
+
billingSwitch: {
|
|
28
|
+
options: Array<{
|
|
29
|
+
key: string;
|
|
30
|
+
name: string;
|
|
31
|
+
unit: string;
|
|
32
|
+
discountText: string;
|
|
33
|
+
subTitle?: string;
|
|
34
|
+
}>;
|
|
35
|
+
defaultKey: string;
|
|
36
|
+
};
|
|
37
|
+
plans: Array<any>;
|
|
38
|
+
currency: string;
|
|
39
|
+
pricePlanConfig: PricePlanAppConfig;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function PricePlanInteractive({ data }: { data: PricePlanData }) {
|
|
43
|
+
const [billingKey, setBillingKey] = useState(data.billingSwitch.defaultKey);
|
|
44
|
+
const [tooltip, setTooltip] = useState<{
|
|
45
|
+
show: boolean;
|
|
46
|
+
content: string;
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
}>({ show: false, content: '', x: 0, y: 0 });
|
|
50
|
+
|
|
51
|
+
const router = useRouter();
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
// Progressive enhancement: Add billing switch functionality
|
|
55
|
+
const monthlyButton = document.querySelector('[data-billing-button="monthly"]') as HTMLButtonElement;
|
|
56
|
+
const yearlyButton = document.querySelector('[data-billing-button="yearly"]') as HTMLButtonElement;
|
|
57
|
+
|
|
58
|
+
const handleBillingSwitch = (newBillingKey: string) => {
|
|
59
|
+
setBillingKey(newBillingKey);
|
|
60
|
+
updatePrices(newBillingKey);
|
|
61
|
+
updateDiscountInfo(newBillingKey);
|
|
62
|
+
updateButtonStyles(newBillingKey);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (monthlyButton) {
|
|
66
|
+
monthlyButton.addEventListener('click', () => handleBillingSwitch('monthly'));
|
|
67
|
+
}
|
|
68
|
+
if (yearlyButton) {
|
|
69
|
+
yearlyButton.addEventListener('click', () => handleBillingSwitch('yearly'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add tooltip functionality
|
|
73
|
+
data.plans.forEach((plan: any) => {
|
|
74
|
+
plan.features?.forEach((feature: any, i: number) => {
|
|
75
|
+
if (feature?.tooltip) {
|
|
76
|
+
const tooltipTrigger = document.querySelector(`[data-tooltip-trigger="${plan.key}-${i}"]`) as HTMLElement;
|
|
77
|
+
if (tooltipTrigger) {
|
|
78
|
+
const handleMouseEnter = (e: MouseEvent) => {
|
|
79
|
+
setTooltip({
|
|
80
|
+
show: true,
|
|
81
|
+
content: feature.tooltip,
|
|
82
|
+
x: e.clientX,
|
|
83
|
+
y: e.clientY
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
88
|
+
setTooltip(prev => ({ ...prev, x: e.clientX, y: e.clientY }));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleMouseLeave = () => {
|
|
92
|
+
setTooltip(prev => ({ ...prev, show: false }));
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
tooltipTrigger.addEventListener('mouseenter', handleMouseEnter);
|
|
96
|
+
tooltipTrigger.addEventListener('mousemove', handleMouseMove);
|
|
97
|
+
tooltipTrigger.addEventListener('mouseleave', handleMouseLeave);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Add plan button functionality
|
|
104
|
+
data.plans.forEach((plan: any) => {
|
|
105
|
+
const planButton = document.querySelector(`[data-plan-button="${plan.key}"]`) as HTMLButtonElement;
|
|
106
|
+
if (planButton && !plan.button?.disabled) {
|
|
107
|
+
planButton.addEventListener('click', () => {
|
|
108
|
+
router.push('/');
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Cleanup
|
|
114
|
+
return () => {
|
|
115
|
+
if (monthlyButton) {
|
|
116
|
+
const newButton = monthlyButton.cloneNode(true);
|
|
117
|
+
monthlyButton.parentNode?.replaceChild(newButton, monthlyButton);
|
|
118
|
+
}
|
|
119
|
+
if (yearlyButton) {
|
|
120
|
+
const newButton = yearlyButton.cloneNode(true);
|
|
121
|
+
yearlyButton.parentNode?.replaceChild(newButton, yearlyButton);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Cleanup tooltip events
|
|
125
|
+
data.plans.forEach((plan: any) => {
|
|
126
|
+
plan.features?.forEach((_feature: any, i: number) => {
|
|
127
|
+
const tooltipTrigger = document.querySelector(`[data-tooltip-trigger="${plan.key}-${i}"]`) as HTMLElement;
|
|
128
|
+
if (tooltipTrigger) {
|
|
129
|
+
const newTrigger = tooltipTrigger.cloneNode(true);
|
|
130
|
+
tooltipTrigger.parentNode?.replaceChild(newTrigger, tooltipTrigger);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
}, [data, router]);
|
|
136
|
+
|
|
137
|
+
const updatePrices = (newBillingKey: string) => {
|
|
138
|
+
const currentBilling = data.pricePlanConfig.billingOptions.find((opt: any) => opt.key === newBillingKey) || data.pricePlanConfig.billingOptions[0];
|
|
139
|
+
const currentBillingDisplay = data.billingSwitch.options.find((opt: any) => opt.key === newBillingKey) || data.billingSwitch.options[0];
|
|
140
|
+
|
|
141
|
+
data.plans.forEach((plan: any) => {
|
|
142
|
+
const priceContainer = document.querySelector(`[data-price-container="${plan.key}"]`) as HTMLElement;
|
|
143
|
+
const priceValue = data.pricePlanConfig.prices[plan.key];
|
|
144
|
+
|
|
145
|
+
if (priceContainer) {
|
|
146
|
+
// Update price display based on new billing
|
|
147
|
+
const priceValueElement = document.querySelector(`[data-price-value="${plan.key}"]`) as HTMLElement;
|
|
148
|
+
const priceUnitElement = document.querySelector(`[data-price-unit="${plan.key}"]`) as HTMLElement;
|
|
149
|
+
const priceOriginalElement = document.querySelector(`[data-price-original="${plan.key}"]`) as HTMLElement;
|
|
150
|
+
const priceDiscountElement = document.querySelector(`[data-price-discount="${plan.key}"]`) as HTMLElement;
|
|
151
|
+
const priceSubtitleElement = document.querySelector(`[data-price-subtitle="${plan.key}"]`) as HTMLElement;
|
|
152
|
+
|
|
153
|
+
if (typeof priceValue !== 'number' || isNaN(priceValue)) {
|
|
154
|
+
// Non-numeric price
|
|
155
|
+
if (priceValueElement) priceValueElement.textContent = String(priceValue);
|
|
156
|
+
if (priceSubtitleElement) priceSubtitleElement.textContent = plan.showBillingSubTitle === false ? '' : (currentBillingDisplay?.subTitle || '');
|
|
157
|
+
} else {
|
|
158
|
+
// Numeric price
|
|
159
|
+
const originValue = Number(priceValue);
|
|
160
|
+
const discount = currentBilling.discount;
|
|
161
|
+
const hasDiscount = discount !== 0;
|
|
162
|
+
const saleValue = originValue * (1 - discount);
|
|
163
|
+
const formatPrice = (v: number) => Number(v.toFixed(2)).toString();
|
|
164
|
+
const showNaN = saleValue < 0;
|
|
165
|
+
|
|
166
|
+
if (priceValueElement) {
|
|
167
|
+
priceValueElement.textContent = `${data.currency}${showNaN ? 'NaN' : (hasDiscount ? formatPrice(saleValue) : formatPrice(originValue))}`;
|
|
168
|
+
}
|
|
169
|
+
if (priceUnitElement) {
|
|
170
|
+
priceUnitElement.textContent = currentBillingDisplay.unit || '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle discount display
|
|
174
|
+
if (hasDiscount) {
|
|
175
|
+
if (priceOriginalElement) {
|
|
176
|
+
priceOriginalElement.textContent = `${data.currency}${showNaN ? 'NaN' : formatPrice(originValue)}`;
|
|
177
|
+
priceOriginalElement.style.display = 'inline';
|
|
178
|
+
}
|
|
179
|
+
if (priceDiscountElement && currentBillingDisplay.discountText) {
|
|
180
|
+
const discountText = currentBillingDisplay.discountText.replace('{percent}', String(Math.round(Math.abs(discount) * 100)));
|
|
181
|
+
priceDiscountElement.textContent = discountText;
|
|
182
|
+
priceDiscountElement.style.display = 'inline';
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
if (priceOriginalElement) priceOriginalElement.style.display = 'none';
|
|
186
|
+
if (priceDiscountElement) priceDiscountElement.style.display = 'none';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (priceSubtitleElement) {
|
|
190
|
+
priceSubtitleElement.textContent = plan.showBillingSubTitle === false ? '' : (currentBillingDisplay?.subTitle || '');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const updateDiscountInfo = (newBillingKey: string) => {
|
|
198
|
+
const discountInfoElement = document.querySelector('[data-discount-info]') as HTMLElement;
|
|
199
|
+
if (discountInfoElement) {
|
|
200
|
+
const opt = data.billingSwitch.options.find((opt: any) => opt.key === newBillingKey);
|
|
201
|
+
const bOpt = data.pricePlanConfig.billingOptions.find((opt: any) => opt.key === newBillingKey);
|
|
202
|
+
|
|
203
|
+
if (opt && bOpt && opt.discountText && bOpt.discount !== 0) {
|
|
204
|
+
const discountText = opt.discountText.replace('{percent}', String(Math.round(Math.abs(bOpt.discount) * 100)));
|
|
205
|
+
discountInfoElement.innerHTML = `
|
|
206
|
+
<span class="px-2 py-1 text-xs rounded bg-yellow-100 text-yellow-800 font-semibold align-middle text-center inline-flex items-center justify-center whitespace-nowrap">
|
|
207
|
+
${discountText}
|
|
208
|
+
</span>
|
|
209
|
+
`;
|
|
210
|
+
} else {
|
|
211
|
+
discountInfoElement.innerHTML = '';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const updateButtonStyles = (newBillingKey: string) => {
|
|
217
|
+
const monthlyButton = document.querySelector('[data-billing-button="monthly"]') as HTMLElement;
|
|
218
|
+
const yearlyButton = document.querySelector('[data-billing-button="yearly"]') as HTMLElement;
|
|
219
|
+
|
|
220
|
+
if (monthlyButton) {
|
|
221
|
+
if (newBillingKey === 'monthly') {
|
|
222
|
+
monthlyButton.className = cn(
|
|
223
|
+
'min-w-[120px] px-6 py-2 font-medium transition text-lg relative',
|
|
224
|
+
'text-white bg-gradient-to-r from-purple-400 to-pink-500 hover:from-purple-500 hover:to-pink-600 dark:from-purple-500 dark:to-pink-600 dark:hover:from-purple-600 rounded-full shadow-sm'
|
|
225
|
+
);
|
|
226
|
+
} else {
|
|
227
|
+
monthlyButton.className = cn(
|
|
228
|
+
'min-w-[120px] px-6 py-2 font-medium transition text-lg relative',
|
|
229
|
+
'text-gray-800 dark:text-gray-200 hover:text-gray-900 dark:hover:text-gray-100 rounded-full'
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (yearlyButton) {
|
|
235
|
+
if (newBillingKey === 'yearly') {
|
|
236
|
+
yearlyButton.className = cn(
|
|
237
|
+
'min-w-[120px] px-6 py-2 font-medium transition text-lg relative',
|
|
238
|
+
'text-white bg-gradient-to-r from-purple-400 to-pink-500 hover:from-purple-500 hover:to-pink-600 dark:from-purple-500 dark:to-pink-600 dark:hover:from-purple-600 rounded-full shadow-sm'
|
|
239
|
+
);
|
|
240
|
+
} else {
|
|
241
|
+
yearlyButton.className = cn(
|
|
242
|
+
'min-w-[120px] px-6 py-2 font-medium transition text-lg relative',
|
|
243
|
+
'text-gray-800 dark:text-gray-200 hover:text-gray-900 dark:hover:text-gray-100 rounded-full'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Tooltip component
|
|
250
|
+
const Tooltip = ({ show, content, x, y }: typeof tooltip) => {
|
|
251
|
+
if (!show) return null;
|
|
252
|
+
const style: React.CSSProperties = {
|
|
253
|
+
position: 'fixed',
|
|
254
|
+
left: Math.max(8, x),
|
|
255
|
+
top: Math.max(8, y),
|
|
256
|
+
zIndex: 9999,
|
|
257
|
+
maxWidth: 200,
|
|
258
|
+
transform: 'translateY(-50%)',
|
|
259
|
+
pointerEvents: 'none',
|
|
260
|
+
whiteSpace: 'pre-line',
|
|
261
|
+
};
|
|
262
|
+
return (
|
|
263
|
+
<div
|
|
264
|
+
style={style}
|
|
265
|
+
className="bg-gray-700 dark:bg-gray-200 text-gray-100 dark:text-gray-800 text-xs leading-relaxed px-3 py-2 rounded-lg shadow-lg border border-gray-300 dark:border-gray-600 backdrop-blur-sm"
|
|
266
|
+
>
|
|
267
|
+
{content}
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return <Tooltip {...tooltip} />;
|
|
273
|
+
}
|