@windrun-huaiin/third-ui 5.14.2 → 6.1.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.
Files changed (69) hide show
  1. package/dist/clerk/index.d.mts +2 -21
  2. package/dist/clerk/index.d.ts +2 -21
  3. package/dist/clerk/index.js +5 -2884
  4. package/dist/clerk/index.js.map +1 -1
  5. package/dist/clerk/index.mjs +3 -2872
  6. package/dist/clerk/index.mjs.map +1 -1
  7. package/dist/clerk/server.d.mts +28 -0
  8. package/dist/clerk/server.d.ts +28 -0
  9. package/dist/clerk/server.js +3025 -0
  10. package/dist/clerk/server.js.map +1 -0
  11. package/dist/clerk/server.mjs +2991 -0
  12. package/dist/clerk/server.mjs.map +1 -0
  13. package/dist/fuma/mdx/index.d.mts +1 -12
  14. package/dist/fuma/mdx/index.d.ts +1 -12
  15. package/dist/fuma/mdx/index.js +47 -262
  16. package/dist/fuma/mdx/index.js.map +1 -1
  17. package/dist/fuma/mdx/index.mjs +48 -261
  18. package/dist/fuma/mdx/index.mjs.map +1 -1
  19. package/dist/fuma/server.d.mts +15 -2
  20. package/dist/fuma/server.d.ts +15 -2
  21. package/dist/fuma/server.js +234 -49
  22. package/dist/fuma/server.js.map +1 -1
  23. package/dist/fuma/server.mjs +231 -48
  24. package/dist/fuma/server.mjs.map +1 -1
  25. package/dist/lib/server.d.mts +509 -465
  26. package/dist/lib/server.d.ts +509 -465
  27. package/dist/main/index.d.mts +5 -56
  28. package/dist/main/index.d.ts +5 -56
  29. package/dist/main/index.js +646 -1322
  30. package/dist/main/index.js.map +1 -1
  31. package/dist/main/index.mjs +675 -1342
  32. package/dist/main/index.mjs.map +1 -1
  33. package/dist/main/server.d.mts +64 -0
  34. package/dist/main/server.d.ts +64 -0
  35. package/dist/main/server.js +4166 -0
  36. package/dist/main/server.js.map +1 -0
  37. package/dist/main/server.mjs +4128 -0
  38. package/dist/main/server.mjs.map +1 -0
  39. package/package.json +12 -2
  40. package/src/clerk/clerk-organization-client.tsx +50 -0
  41. package/src/clerk/clerk-organization.tsx +21 -38
  42. package/src/clerk/clerk-page-generator.tsx +0 -2
  43. package/src/clerk/clerk-provider-client.tsx +1 -1
  44. package/src/clerk/clerk-user-client.tsx +64 -0
  45. package/src/clerk/clerk-user.tsx +29 -58
  46. package/src/clerk/index.ts +1 -4
  47. package/src/clerk/server.ts +3 -0
  48. package/src/fuma/{mdx/fuma-banner-suit.tsx → fuma-banner-suit.tsx} +5 -7
  49. package/src/fuma/mdx/banner.tsx +51 -52
  50. package/src/fuma/mdx/index.ts +0 -2
  51. package/src/fuma/mdx/toc-footer-wrapper.tsx +1 -0
  52. package/src/fuma/mdx/zia-file.tsx +0 -1
  53. package/src/fuma/server.ts +3 -1
  54. package/src/fuma/{mdx/site-x.tsx → site-x.tsx} +4 -5
  55. package/src/main/cta.tsx +33 -10
  56. package/src/main/faq-interactive.tsx +68 -0
  57. package/src/main/faq.tsx +62 -38
  58. package/src/main/features.tsx +40 -11
  59. package/src/main/footer.tsx +27 -16
  60. package/src/main/gallery-interactive.tsx +171 -0
  61. package/src/main/gallery.tsx +54 -101
  62. package/src/main/index.ts +1 -10
  63. package/src/main/language-detector.tsx +175 -0
  64. package/src/main/price-plan-interactive.tsx +273 -0
  65. package/src/main/price-plan.tsx +112 -129
  66. package/src/main/seo-content.tsx +46 -13
  67. package/src/main/server.ts +10 -0
  68. package/src/main/tips.tsx +48 -22
  69. package/src/main/usage.tsx +43 -11
@@ -1,14 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- 'use client';
3
-
4
- import React, { useState } from 'react'
5
- import { useTranslations } from 'next-intl'
6
2
  import { cn } from '@lib/utils';
7
- import { globalLucideIcons as icons } from '@base-ui/components/global-icon'
8
- import { useRouter } from 'next/navigation';
3
+ import { getTranslations } from 'next-intl/server';
4
+ import { PricePlanInteractive } from './price-plan-interactive';
9
5
 
10
6
  export interface PricePlanProps {
11
- // default is $
7
+ locale: string
12
8
  currency?: string
13
9
  pricePlanConfig: PricePlanAppConfig
14
10
  sectionClassName?: string
@@ -30,9 +26,10 @@ export interface PricePlanAppConfig {
30
26
  minPlanFeaturesCount: number
31
27
  }
32
28
 
33
- export function PricePlan({ currency = '$', pricePlanConfig, sectionClassName }: PricePlanProps) {
34
- const t = useTranslations('pricePlan')
35
- const billingSwitch = t.raw('billingSwitch') as {
29
+ interface PricePlanData {
30
+ title: string;
31
+ subtitle: string;
32
+ billingSwitch: {
36
33
  options: Array<{
37
34
  key: string
38
35
  name: string
@@ -41,175 +38,172 @@ export function PricePlan({ currency = '$', pricePlanConfig, sectionClassName }:
41
38
  subTitle?: string
42
39
  }>
43
40
  defaultKey: string
44
- }
45
- const plans = t.raw('plans') as Array<any>
46
- const router = useRouter();
47
-
48
- // price plan config
49
- const billingOptions = pricePlanConfig.billingOptions
50
- const prices = pricePlanConfig.prices
51
- const minPlanFeaturesCount = pricePlanConfig.minPlanFeaturesCount
52
-
53
- // current billing key
54
- const [billingKey, setBillingKey] = useState(billingSwitch.defaultKey)
41
+ };
42
+ plans: Array<any>;
43
+ currency: string;
44
+ pricePlanConfig: PricePlanAppConfig;
45
+ }
55
46
 
56
- // tooltip state
57
- const [tooltip, setTooltip] = useState<{
58
- show: boolean
59
- content: string
60
- x: number
61
- y: number
62
- }>({ show: false, content: '', x: 0, y: 0 })
47
+ export async function PricePlan({
48
+ locale,
49
+ currency = '$',
50
+ pricePlanConfig,
51
+ sectionClassName
52
+ }: PricePlanProps) {
53
+ const t = await getTranslations({ locale, namespace: 'pricePlan' })
54
+
55
+ const data: PricePlanData = {
56
+ title: t('title'),
57
+ subtitle: t('subtitle'),
58
+ billingSwitch: t.raw('billingSwitch') as {
59
+ options: Array<{
60
+ key: string
61
+ name: string
62
+ unit: string
63
+ discountText: string
64
+ subTitle?: string
65
+ }>
66
+ defaultKey: string
67
+ },
68
+ plans: t.raw('plans') as Array<any>,
69
+ currency,
70
+ pricePlanConfig
71
+ };
63
72
 
64
- // get current billing config and display config
65
- const currentBilling = billingOptions.find((opt: any) => opt.key === billingKey) || billingOptions[0]
66
- const currentBillingDisplay = billingSwitch.options.find((opt: any) => opt.key === billingKey) || billingSwitch.options[0]
73
+ // Static data processing for server-side rendering
74
+ const billingOptions = data.pricePlanConfig.billingOptions;
75
+ const prices = data.pricePlanConfig.prices;
76
+ const minPlanFeaturesCount = data.pricePlanConfig.minPlanFeaturesCount;
77
+
78
+ // Use default billing for static rendering
79
+ const defaultBilling = billingOptions.find((opt: any) => opt.key === data.billingSwitch.defaultKey) || billingOptions[0];
80
+ const defaultBillingDisplay = data.billingSwitch.options.find((opt: any) => opt.key === data.billingSwitch.defaultKey) || data.billingSwitch.options[0];
67
81
 
68
- // calculate features count
82
+ // Calculate features count
69
83
  const maxFeaturesCount = Math.max(
70
- ...plans.map((plan: any) => plan.features?.length || 0),
84
+ ...data.plans.map((plan: any) => plan.features?.length || 0),
71
85
  minPlanFeaturesCount || 0
72
- )
86
+ );
73
87
 
74
- // handle card height alignment
88
+ // Handle card height alignment
75
89
  const getFeatureRows = (plan: any) => {
76
- const features = plan.features || []
77
- const filled = [...features]
78
- while (filled.length < maxFeaturesCount) filled.push(null)
79
- return filled
80
- }
90
+ const features = plan.features || [];
91
+ const filled = [...features];
92
+ while (filled.length < maxFeaturesCount) filled.push(null);
93
+ return filled;
94
+ };
81
95
 
82
- // price render logic
83
- function renderPrice(plan: any) {
96
+ // Static price render logic for default billing
97
+ function renderPrice(plan: any, billingKey = data.billingSwitch.defaultKey) {
84
98
  const priceValue = prices[plan.key];
85
- // current billing subTitle
86
- const billingSubTitle = billingSwitch.options.find((opt: any) => opt.key === billingKey)?.subTitle || '';
87
- // non-numeric (like 'Custom') directly display
99
+ const currentBilling = billingOptions.find((opt: any) => opt.key === billingKey) || defaultBilling;
100
+ const currentBillingDisplay = data.billingSwitch.options.find((opt: any) => opt.key === billingKey) || defaultBillingDisplay;
101
+ const billingSubTitle = currentBillingDisplay?.subTitle || '';
102
+
103
+ // Non-numeric (like 'Custom') directly display
88
104
  if (typeof priceValue !== 'number' || isNaN(priceValue)) {
89
105
  return (
90
- <div className="flex flex-col items-start w-full">
106
+ <div className="flex flex-col items-start w-full" data-price-container={plan.key}>
91
107
  <div className="flex items-end gap-2">
92
- <span className="text-4xl font-extrabold text-gray-900 dark:text-gray-100">{priceValue}</span>
108
+ <span className="text-4xl font-extrabold text-gray-900 dark:text-gray-100" data-price-value={plan.key}>{priceValue}</span>
93
109
  </div>
94
110
  <div className="flex items-center gap-2 min-h-[24px] mt-1">
95
- <span className={cn('text-xs text-gray-700 dark:text-gray-300 font-medium', plan.showBillingSubTitle === false && 'opacity-0 select-none')}>
111
+ <span className={cn('text-xs text-gray-700 dark:text-gray-300 font-medium', plan.showBillingSubTitle === false && 'opacity-0 select-none')} data-price-subtitle={plan.key}>
96
112
  {plan.showBillingSubTitle === false ? '' : billingSubTitle}
97
113
  </span>
98
114
  </div>
99
115
  </div>
100
116
  );
101
117
  }
102
- // numeric price logic
103
- const originValue = Number(priceValue)
104
- const discount = currentBilling.discount
105
- const hasDiscount = discount !== 0
106
- const saleValue = originValue * (1 - discount)
107
- // format price, keep 2 decimal places but remove trailing 0
108
- const formatPrice = (v: number) => Number(v.toFixed(2)).toString()
109
- const unit = currentBillingDisplay.unit || ''
110
- let discountText = ''
118
+
119
+ // Numeric price logic
120
+ const originValue = Number(priceValue);
121
+ const discount = currentBilling.discount;
122
+ const hasDiscount = discount !== 0;
123
+ const saleValue = originValue * (1 - discount);
124
+ const formatPrice = (v: number) => Number(v.toFixed(2)).toString();
125
+ const unit = currentBillingDisplay.unit || '';
126
+ let discountText = '';
111
127
  if (hasDiscount && currentBillingDisplay.discountText) {
112
- discountText = currentBillingDisplay.discountText.replace('{percent}', String(Math.round(Math.abs(discount) * 100)))
128
+ discountText = currentBillingDisplay.discountText.replace('{percent}', String(Math.round(Math.abs(discount) * 100)));
113
129
  }
114
- // show NaN when price is negative
115
- const showNaN = saleValue < 0
130
+ const showNaN = saleValue < 0;
131
+
116
132
  return (
117
- <div className="flex flex-col items-start w-full">
133
+ <div className="flex flex-col items-start w-full" data-price-container={plan.key}>
118
134
  <div className="flex items-end gap-2">
119
- <span className="text-4xl font-extrabold text-gray-900 dark:text-gray-100">
120
- {currency}{showNaN ? 'NaN' : (hasDiscount ? formatPrice(saleValue) : formatPrice(originValue))}
135
+ <span className="text-4xl font-extrabold text-gray-900 dark:text-gray-100" data-price-value={plan.key}>
136
+ {data.currency}{showNaN ? 'NaN' : (hasDiscount ? formatPrice(saleValue) : formatPrice(originValue))}
121
137
  </span>
122
- <span className="text-lg text-gray-700 dark:text-gray-300 font-medium mb-1">{unit}</span>
138
+ <span className="text-lg text-gray-700 dark:text-gray-300 font-medium mb-1" data-price-unit={plan.key}>{unit}</span>
123
139
  </div>
124
- {/* sub title row, always take place */}
125
140
  <div className="flex items-center gap-2 min-h-[24px] mt-1">
126
141
  {hasDiscount && (
127
142
  <>
128
- <span className="text-base text-gray-400 line-through">{currency}{showNaN ? 'NaN' : formatPrice(originValue)}</span>
143
+ <span className="text-base text-gray-400 line-through" data-price-original={plan.key}>
144
+ {data.currency}{showNaN ? 'NaN' : formatPrice(originValue)}
145
+ </span>
129
146
  {discountText && (
130
- <span className="px-2 py-0.5 text-xs rounded bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 font-semibold align-middle">{discountText}</span>
147
+ <span className="px-2 py-0.5 text-xs rounded bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 font-semibold align-middle" data-price-discount={plan.key}>
148
+ {discountText}
149
+ </span>
131
150
  )}
132
151
  </>
133
152
  )}
134
- <span className={cn('text-xs text-gray-700 dark:text-gray-300 font-medium', plan.showBillingSubTitle === false && 'opacity-0 select-none')}>
153
+ <span className={cn('text-xs text-gray-700 dark:text-gray-300 font-medium', plan.showBillingSubTitle === false && 'opacity-0 select-none')} data-price-subtitle={plan.key}>
135
154
  {plan.showBillingSubTitle === false ? '' : billingSubTitle}
136
155
  </span>
137
156
  </div>
138
157
  </div>
139
- )
140
- }
141
-
142
- // tooltip component
143
- const Tooltip = ({ show, content, x, y }: typeof tooltip) => {
144
- if (!show) return null
145
- // simple boundary handling, prevent overflow
146
- const style: React.CSSProperties = {
147
- position: 'fixed',
148
- left: Math.max(8, x),
149
- top: Math.max(8, y),
150
- zIndex: 9999,
151
- maxWidth: 200,
152
- transform: 'translateY(-50%)',
153
- pointerEvents: 'none',
154
- whiteSpace: 'pre-line',
155
- }
156
- return (
157
- <div
158
- style={style}
159
- 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"
160
- >
161
- {content}
162
- </div>
163
- )
158
+ );
164
159
  }
165
160
 
166
161
  return (
167
162
  <section id="pricing" className={cn("px-4 py-10 md:px-16 md:py-16 mx-auto max-w-7xl scroll-mt-10", sectionClassName)}>
168
163
  {/* title and subtitle */}
169
164
  <h2 className="text-3xl md:text-4xl font-bold text-center mb-3">
170
- {t('title')}
165
+ {data.title}
171
166
  </h2>
172
167
  <p className="text-center text-gray-600 dark:text-gray-400 mb-8 text-base md:text-lg mx-auto">
173
- {t('subtitle')}
168
+ {data.subtitle}
174
169
  </p>
175
170
 
176
171
  {/* billing switch button */}
177
172
  <div className="flex flex-col items-center">
178
- {/* Binary toggle buttons */}
179
173
  <div className="flex items-center relative mb-3">
180
- <div className="flex bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-full p-1">
174
+ <div className="flex bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-full p-1" data-billing-switch>
181
175
  <button
182
176
  className={cn(
183
177
  'min-w-[120px] px-6 py-2 font-medium transition text-lg relative',
184
- billingKey === 'monthly'
178
+ data.billingSwitch.defaultKey === 'monthly'
185
179
  ? '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'
186
180
  : 'text-gray-800 dark:text-gray-200 hover:text-gray-900 dark:hover:text-gray-100 rounded-full'
187
181
  )}
188
- onClick={() => setBillingKey('monthly')}
182
+ data-billing-button="monthly"
189
183
  type="button"
190
184
  >
191
- {(billingSwitch.options.find((opt: any) => opt.key === 'monthly')?.name) || 'Monthly'}
185
+ {(data.billingSwitch.options.find((opt: any) => opt.key === 'monthly')?.name) || 'Monthly'}
192
186
  </button>
193
187
  <button
194
188
  className={cn(
195
189
  'min-w-[120px] px-6 py-2 font-medium transition text-lg relative',
196
- billingKey === 'yearly'
190
+ data.billingSwitch.defaultKey === 'yearly'
197
191
  ? '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'
198
192
  : 'text-gray-800 dark:text-gray-200 hover:text-gray-900 dark:hover:text-gray-100 rounded-full'
199
193
  )}
200
- onClick={() => setBillingKey('yearly')}
194
+ data-billing-button="yearly"
201
195
  type="button"
202
196
  >
203
- {(billingSwitch.options.find((opt: any) => opt.key === 'yearly')?.name) || 'Yearly'}
197
+ {(data.billingSwitch.options.find((opt: any) => opt.key === 'yearly')?.name) || 'Yearly'}
204
198
  </button>
205
199
  </div>
206
200
  </div>
207
201
 
208
- {/* Discount info - fixed position between buttons and cards */}
209
- <div className="h-8 flex items-center justify-center mb-3">
202
+ {/* Discount info - static for default billing */}
203
+ <div className="h-8 flex items-center justify-center mb-3" data-discount-info>
210
204
  {(() => {
211
- const opt = billingSwitch.options.find((opt: any) => opt.key === billingKey);
212
- const bOpt = billingOptions.find((opt: any) => opt.key === billingKey);
205
+ const opt = data.billingSwitch.options.find((opt: any) => opt.key === data.billingSwitch.defaultKey);
206
+ const bOpt = billingOptions.find((opt: any) => opt.key === data.billingSwitch.defaultKey);
213
207
  if (!(opt && bOpt && opt.discountText && bOpt.discount !== 0)) return null;
214
208
  return (
215
209
  <span className="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">
@@ -225,9 +219,10 @@ export function PricePlan({ currency = '$', pricePlanConfig, sectionClassName }:
225
219
 
226
220
  {/* price card area */}
227
221
  <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
228
- {plans.map((plan: any, _idx: number) => (
222
+ {data.plans.map((plan: any, _idx: number) => (
229
223
  <div
230
224
  key={plan.key}
225
+ data-price-plan={plan.key}
231
226
  className={cn(
232
227
  'flex flex-col bg-white dark:bg-gray-800/60 rounded-2xl border border-gray-300 dark:border-[#7c3aed40] transition p-8 h-full shadow-sm dark:shadow-none',
233
228
  'hover:border-2 hover:border-purple-500',
@@ -247,7 +242,7 @@ export function PricePlan({ currency = '$', pricePlanConfig, sectionClassName }:
247
242
  {/* feature list */}
248
243
  <ul className="flex-1 mb-6 mt-4">
249
244
  {getFeatureRows(plan).map((feature: any, i: number) => (
250
- <li key={i} className="flex items-center gap-2 mb-2 min-h-[28px]">
245
+ <li key={i} className="flex items-center gap-2 mb-2 min-h-[28px]" data-feature-item={`${plan.key}-${i}`}>
251
246
  {/* icon */}
252
247
  {feature ? (
253
248
  <span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200 mr-1">
@@ -267,20 +262,12 @@ export function PricePlan({ currency = '$', pricePlanConfig, sectionClassName }:
267
262
  {feature.tooltip && (
268
263
  <span
269
264
  className="ml-1 align-middle inline-flex"
270
- onMouseEnter={e => {
271
- setTooltip({
272
- show: true,
273
- content: feature.tooltip,
274
- x: e.clientX,
275
- y: e.clientY
276
- })
277
- }}
278
- onMouseMove={e => {
279
- setTooltip(t => ({ ...t, x: e.clientX, y: e.clientY }))
280
- }}
281
- onMouseLeave={() => setTooltip(t => ({ ...t, show: false }))}
265
+ data-tooltip-trigger={`${plan.key}-${i}`}
266
+ data-tooltip-content={feature.tooltip}
282
267
  >
283
- <icons.FAQ className="w-4 h-4" />
268
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
269
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
270
+ </svg>
284
271
  </span>
285
272
  )}
286
273
  </span>
@@ -302,19 +289,15 @@ export function PricePlan({ currency = '$', pricePlanConfig, sectionClassName }:
302
289
  )}
303
290
  disabled={plan.button?.disabled}
304
291
  type="button"
305
- onClick={() => {
306
- if (!plan.button?.disabled) {
307
- router.push('/');
308
- }
309
- }}
292
+ data-plan-button={plan.key}
310
293
  >
311
294
  {plan.button?.text || '--'}
312
295
  </button>
313
296
  </div>
314
297
  ))}
315
298
  </div>
316
- {/* tooltip hover msg */}
317
- <Tooltip {...tooltip} />
299
+
300
+ <PricePlanInteractive data={data} />
318
301
  </section>
319
302
  )
320
303
  }
@@ -1,42 +1,75 @@
1
1
  /* eslint-disable react/no-unescaped-entities */
2
- 'use client'
3
-
4
- import { useTranslations } from 'next-intl'
2
+ import { getTranslations } from 'next-intl/server';
5
3
  import { cn } from '@lib/utils';
6
4
  import { richText } from '@third-ui/main/rich-text-expert';
7
5
 
8
- interface Section {
6
+ interface SeoSection {
7
+ id: string;
9
8
  title: string;
10
9
  content: string;
11
10
  }
12
11
 
13
- export function SeoContent({ sectionClassName }: { sectionClassName?: string }) {
14
- const t = useTranslations('seoContent');
12
+ interface SeoContentData {
13
+ title: string;
14
+ eyesOn: string;
15
+ description: string;
16
+ intro: string;
17
+ sections: SeoSection[];
18
+ conclusion: string;
19
+ }
20
+
21
+ export async function SeoContent({
22
+ locale,
23
+ sectionClassName
24
+ }: {
25
+ locale: string;
26
+ sectionClassName?: string;
27
+ }) {
28
+ const t = await getTranslations({ locale, namespace: 'seoContent' });
29
+
30
+ // Process translation data
31
+ const rawSections = t.raw('sections') as Array<{
32
+ title: string;
33
+ content: string;
34
+ }>;
35
+
36
+ const data: SeoContentData = {
37
+ title: t('title'),
38
+ eyesOn: t('eyesOn'),
39
+ description: t('description'),
40
+ intro: richText(t, 'intro'),
41
+ sections: rawSections.map((section, index) => ({
42
+ id: `seo-section-${index}`,
43
+ title: section.title,
44
+ content: richText(t, `sections.${index}.content`)
45
+ })),
46
+ conclusion: richText(t, 'conclusion')
47
+ };
15
48
 
16
49
  return (
17
50
  <section id="seo" className={cn("px-16 py-10 mx-16 md:mx-32 scroll-mt-20", sectionClassName)}>
18
51
  <h2 className="text-3xl md:text-4xl font-bold text-center mb-8">
19
- {t('title')} <span className="text-purple-500">{t('eyesOn')}</span>
52
+ {data.title} <span className="text-purple-500">{data.eyesOn}</span>
20
53
  </h2>
21
54
  <h3 className="text-center text-gray-600 dark:text-gray-400 mb-12 text-lg">
22
- {t('description')}
55
+ {data.description}
23
56
  </h3>
24
57
  <div className="bg-gray-50 dark:bg-gray-800/60 border border-gray-200 dark:border-gray-700 rounded-2xl p-8 md:p-12 shadow-sm dark:shadow-none">
25
58
  <div className="space-y-10">
26
59
  <p className="text-gray-600 dark:text-gray-400 text-lg">
27
- {richText(t, 'intro')}
60
+ {data.intro}
28
61
  </p>
29
- {t.raw('sections').map((section: Section, index: number) => (
30
- <div key={index}>
62
+ {data.sections.map((section) => (
63
+ <div key={section.id} data-seo-section={section.id}>
31
64
  <h2 className="text-xl font-semibold mb-3 text-gray-900 dark:text-gray-100 flex items-center">
32
65
  {section.title}
33
66
  </h2>
34
- <p className="text-gray-700 dark:text-gray-300">{richText(t, `sections.${index}.content`)}</p>
67
+ <p className="text-gray-700 dark:text-gray-300">{section.content}</p>
35
68
  </div>
36
69
  ))}
37
70
  </div>
38
71
  <p className="mt-10 text-gray-600 dark:text-gray-400 text-lg">
39
- {richText(t, 'conclusion')}
72
+ {data.conclusion}
40
73
  </p>
41
74
  </div>
42
75
  </section>
@@ -0,0 +1,10 @@
1
+ // Main application Server components
2
+ export * from './gallery';
3
+ export * from './usage';
4
+ export * from './features';
5
+ export * from './tips';
6
+ export * from './faq';
7
+ export * from './seo-content';
8
+ export * from './cta';
9
+ export * from './footer';
10
+ export * from './price-plan';
package/src/main/tips.tsx CHANGED
@@ -1,40 +1,66 @@
1
- 'use client'
2
-
3
- import { useTranslations } from 'next-intl'
1
+ import { getTranslations } from 'next-intl/server';
4
2
  import { cn } from '@lib/utils';
5
3
  import { richText } from '@third-ui/main/rich-text-expert';
6
4
 
7
- interface Tip {
5
+ interface TipSection {
6
+ id: string;
8
7
  title: string;
9
8
  description: string;
10
9
  }
11
10
 
12
- export function Tips({ sectionClassName }: { sectionClassName?: string }) {
13
- const t = useTranslations('tips');
14
- const sections = t.raw('sections') as Tip[];
11
+ interface TipsData {
12
+ title: string;
13
+ eyesOn: string;
14
+ leftColumn: TipSection[];
15
+ rightColumn: TipSection[];
16
+ }
17
+
18
+ export async function Tips({
19
+ locale,
20
+ sectionClassName
21
+ }: {
22
+ locale: string;
23
+ sectionClassName?: string;
24
+ }) {
25
+ const t = await getTranslations({ locale, namespace: 'tips' });
26
+
27
+ // Process translation data
28
+ const sections = t.raw('sections') as Array<{
29
+ title: string;
30
+ description: string;
31
+ }>;
32
+
33
+ const processedSections = sections.map((section, index) => ({
34
+ id: `tip-section-${index}`,
35
+ title: section.title,
36
+ description: richText(t, `sections.${index}.description`)
37
+ }));
38
+
39
+ const midPoint = Math.ceil(processedSections.length / 2);
40
+ const leftColumn = processedSections.slice(0, midPoint);
41
+ const rightColumn = processedSections.slice(midPoint);
15
42
 
16
- const midPoint = Math.ceil(sections.length / 2);
17
- const leftColumn = sections.slice(0, midPoint);
18
- const rightColumn = sections.slice(midPoint);
43
+ const data: TipsData = {
44
+ title: t('title'),
45
+ eyesOn: t('eyesOn'),
46
+ leftColumn,
47
+ rightColumn
48
+ };
19
49
 
20
50
  return (
21
51
  <section id="tips" className={cn("px-16 py-10 mx-16 md:mx-32 scroll-mt-20", sectionClassName)}>
22
52
  <h2 className="text-3xl md:text-4xl font-bold text-center mb-16">
23
- {t('title')} <span className="text-purple-500">{t('eyesOn')}</span>
53
+ {data.title} <span className="text-purple-500">{data.eyesOn}</span>
24
54
  </h2>
25
55
  <div className="grid grid-cols-1 md:grid-cols-2 gap-12 bg-gray-50 dark:bg-gray-800/60 border border-gray-200 dark:border-gray-700 rounded-2xl p-8 md:p-12 shadow-sm dark:shadow-none">
26
- {[leftColumn, rightColumn].map((column: Tip[], colIndex) => (
56
+ {[data.leftColumn, data.rightColumn].map((column: TipSection[], colIndex) => (
27
57
  <div key={colIndex} className="space-y-8">
28
- {column.map((tip: Tip, tipIndex) => {
29
- // calculate the actual index in the original array
30
- const actualIndex = colIndex === 0 ? tipIndex : tipIndex + midPoint;
31
- return (
32
- <div key={tipIndex} className="space-y-4">
33
- <h3 className="text-2xl font-semibold">{tip.title}</h3>
34
- <p className="">{richText(t, `sections.${actualIndex}.description`)}</p>
35
- </div>
36
- );
37
- })}
58
+ {column.map((tip: TipSection) => (
59
+ <div key={tip.id} data-tip-id={tip.id} className="space-y-4">
60
+ <h3 className="text-2xl font-semibold">{tip.title}</h3>
61
+ <p className="">{tip.description}</p>
62
+ </div>
63
+ ))}
38
64
  </div>
39
65
  ))}
40
66
  </div>
@@ -1,40 +1,72 @@
1
- 'use client'
2
-
3
- import { useTranslations } from 'next-intl'
1
+ import { getTranslations } from 'next-intl/server';
4
2
  import { cn } from '@lib/utils';
5
3
  import { globalLucideIcons as icons, getGlobalIcon } from '@base-ui/components/global-icon'
6
4
  import { richText } from '@third-ui/main/rich-text-expert';
7
5
 
8
- export function Usage({ sectionClassName }: { sectionClassName?: string }) {
9
- const t = useTranslations('usage');
6
+ interface UsageData {
7
+ title: string;
8
+ eyesOn: string;
9
+ description: string;
10
+ steps: Array<{
11
+ id: string;
12
+ title: string;
13
+ description: string;
14
+ iconKey: keyof typeof icons;
15
+ stepNumber: number;
16
+ }>;
17
+ }
18
+
19
+ export async function Usage({
20
+ locale,
21
+ sectionClassName
22
+ }: {
23
+ locale: string;
24
+ sectionClassName?: string;
25
+ }) {
26
+ const t = await getTranslations({ locale, namespace: 'usage' });
27
+
28
+ // Process translation data
10
29
  const steps = t.raw('steps') as Array<{
11
30
  title: string;
12
31
  description: string;
13
32
  iconKey: keyof typeof icons;
14
33
  }>;
34
+
35
+ const data: UsageData = {
36
+ title: t('title'),
37
+ eyesOn: t('eyesOn'),
38
+ description: richText(t, 'description'),
39
+ steps: steps.map((step, index) => ({
40
+ id: `usage-step-${index}`,
41
+ title: step.title,
42
+ description: richText(t, `steps.${index}.description`),
43
+ iconKey: step.iconKey,
44
+ stepNumber: index + 1
45
+ }))
46
+ };
15
47
 
16
48
  return (
17
49
  <section id="usage" className={cn("px-16 py-10 mx-16 md:mx-32 scroll-mt-20", sectionClassName)}>
18
50
  <h2 className="text-3xl md:text-4xl font-bold text-center mb-4">
19
- {t('title')} <span className="text-purple-500">{t('eyesOn')}</span>
51
+ {data.title} <span className="text-purple-500">{data.eyesOn}</span>
20
52
  </h2>
21
53
  <p className="text-center text-gray-600 dark:text-gray-400 mb-12 text-base md:text-lg mx-auto whitespace-nowrap">
22
- {richText(t, 'description')}
54
+ {data.description}
23
55
  </p>
24
56
  <div className="bg-gray-50 dark:bg-gray-800/60 border border-gray-200 dark:border-gray-700 rounded-2xl p-8 md:p-12 shadow-sm dark:shadow-none">
25
57
  <div className="grid grid-cols-1 md:grid-cols-3 gap-8 gap-y-12">
26
- {steps.map((step, idx) => {
58
+ {data.steps.map((step) => {
27
59
  const Icon = getGlobalIcon(step.iconKey);
28
60
  return (
29
- <div key={idx} className="flex items-start">
61
+ <div key={step.id} data-usage-step={step.id} className="flex items-start">
30
62
  <div className="flex-shrink-0 mr-4">
31
63
  <Icon className="w-8 h-8 text-purple-500" />
32
64
  </div>
33
65
  <div>
34
66
  <h3 className="text-xl font-semibold mb-3 text-gray-900 dark:text-gray-100 flex items-center">
35
- {`${idx + 1}. ${step.title}`}
67
+ {`${step.stepNumber}. ${step.title}`}
36
68
  </h3>
37
- <p className="text-gray-700 dark:text-gray-300">{richText(t, `steps.${idx}.description`)}</p>
69
+ <p className="text-gray-700 dark:text-gray-300">{step.description}</p>
38
70
  </div>
39
71
  </div>
40
72
  )