@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.
Files changed (70) 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 +49 -263
  16. package/dist/fuma/mdx/index.js.map +1 -1
  17. package/dist/fuma/mdx/index.mjs +50 -262
  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} +3 -6
  49. package/src/fuma/mdx/banner.tsx +0 -1
  50. package/src/fuma/mdx/index.ts +0 -2
  51. package/src/fuma/mdx/mermaid.tsx +3 -1
  52. package/src/fuma/mdx/toc-footer-wrapper.tsx +1 -0
  53. package/src/fuma/mdx/zia-file.tsx +0 -1
  54. package/src/fuma/server.ts +3 -1
  55. package/src/fuma/{mdx/site-x.tsx → site-x.tsx} +4 -5
  56. package/src/main/cta.tsx +33 -10
  57. package/src/main/faq-interactive.tsx +68 -0
  58. package/src/main/faq.tsx +62 -38
  59. package/src/main/features.tsx +40 -11
  60. package/src/main/footer.tsx +27 -16
  61. package/src/main/gallery-interactive.tsx +171 -0
  62. package/src/main/gallery.tsx +54 -101
  63. package/src/main/index.ts +1 -10
  64. package/src/main/language-detector.tsx +175 -0
  65. package/src/main/price-plan-interactive.tsx +273 -0
  66. package/src/main/price-plan.tsx +112 -129
  67. package/src/main/seo-content.tsx +46 -13
  68. package/src/main/server.ts +10 -0
  69. package/src/main/tips.tsx +48 -22
  70. package/src/main/usage.tsx +43 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "5.14.1",
3
+ "version": "6.0.0",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -11,11 +11,21 @@
11
11
  "import": "./dist/clerk/index.mjs",
12
12
  "require": "./dist/clerk/index.js"
13
13
  },
14
+ "./clerk/server": {
15
+ "types": "./dist/clerk/server.d.ts",
16
+ "import": "./dist/clerk/server.mjs",
17
+ "require": "./dist/clerk/server.js"
18
+ },
14
19
  "./main": {
15
20
  "types": "./dist/main/index.d.ts",
16
21
  "import": "./dist/main/index.mjs",
17
22
  "require": "./dist/main/index.js"
18
23
  },
24
+ "./main/server": {
25
+ "types": "./dist/main/server.d.ts",
26
+ "import": "./dist/main/server.mjs",
27
+ "require": "./dist/main/server.js"
28
+ },
19
29
  "./fuma/server": {
20
30
  "types": "./dist/fuma/server.d.ts",
21
31
  "import": "./dist/fuma/server.mjs",
@@ -54,7 +64,7 @@
54
64
  "mermaid": "^11.6.0",
55
65
  "react-medium-image-zoom": "^5.2.14",
56
66
  "zod": "^3.22.4",
57
- "@windrun-huaiin/base-ui": "^6.0.3"
67
+ "@windrun-huaiin/base-ui": "^7.0.0"
58
68
  },
59
69
  "peerDependencies": {
60
70
  "react": "19.1.0",
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ import { OrganizationSwitcher } from '@clerk/nextjs';
4
+ import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
5
+
6
+ interface ClerkOrganizationData {
7
+ homepage: string;
8
+ terms: string;
9
+ privacy: string;
10
+ locale: string;
11
+ className: string;
12
+ }
13
+
14
+ export function ClerkOrganizationClient({ data }: { data: ClerkOrganizationData }) {
15
+ return (
16
+ <div className={` ms-3 me-2 flex items-center h-10 rounded-full border shadow-lg ${data.className}`}>
17
+ <div className="flex items-center gap-x-4 w-full min-w-40">
18
+ <OrganizationSwitcher
19
+ appearance={{
20
+ elements: {
21
+ organizationSwitcherTrigger:
22
+ "w-40 h-10 border !rounded-full bg-transparent flex items-center justify-between box-border",
23
+ organizationSwitcherTriggerIcon: "",
24
+ userButtonAvatarBox: "w-8 h-8 border rounded-full",
25
+ },
26
+ }}
27
+ >
28
+ <OrganizationSwitcher.OrganizationProfilePage
29
+ label={data.homepage}
30
+ url="/"
31
+ labelIcon={<icons.D8 />}
32
+ />
33
+ <OrganizationSwitcher.OrganizationProfilePage
34
+ labelIcon={<icons.ReceiptText />}
35
+ label={data.terms}
36
+ url={`/${data.locale}/legal/terms`}
37
+ >
38
+ </OrganizationSwitcher.OrganizationProfilePage>
39
+
40
+ <OrganizationSwitcher.OrganizationProfilePage
41
+ labelIcon={<icons.ShieldUser />}
42
+ label={data.privacy}
43
+ url={`/${data.locale}/legal/privacy`}
44
+ >
45
+ </OrganizationSwitcher.OrganizationProfilePage>
46
+ </OrganizationSwitcher>
47
+ </div>
48
+ </div>
49
+ );
50
+ }
@@ -1,49 +1,32 @@
1
- 'use client';
1
+ import { getTranslations } from 'next-intl/server';
2
+ import { ClerkOrganizationClient } from './clerk-organization-client';
2
3
 
3
- import { OrganizationSwitcher } from '@clerk/nextjs';
4
- import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
5
4
  interface ClerkOrganizationProps {
6
5
  className?: string;
7
6
  locale: string;
8
7
  }
9
8
 
10
- export default function ClerkOrganization({
9
+ interface ClerkOrganizationData {
10
+ homepage: string;
11
+ terms: string;
12
+ privacy: string;
13
+ locale: string;
14
+ className: string;
15
+ }
16
+
17
+ export async function ClerkOrganization({
11
18
  locale,
12
19
  className = '',
13
20
  }: ClerkOrganizationProps) {
14
- return (
15
- <div className={` ms-3 me-2 flex items-center h-10 rounded-full border shadow-lg ${className}`}>
16
- <div className="flex items-center gap-x-4 w-full min-w-40">
17
- <OrganizationSwitcher
18
- appearance={{
19
- elements: {
20
- organizationSwitcherTrigger:
21
- "w-40 h-10 border !rounded-full bg-transparent flex items-center justify-between box-border",
22
- organizationSwitcherTriggerIcon: "",
23
- userButtonAvatarBox: "w-8 h-8 border rounded-full",
24
- },
25
- }}
26
- >
27
- <OrganizationSwitcher.OrganizationProfilePage
28
- label="Homepage"
29
- url="/"
30
- labelIcon={<icons.D8 />}
31
- />
32
- <OrganizationSwitcher.OrganizationProfilePage
33
- labelIcon={<icons.ReceiptText />}
34
- label="服务"
35
- url={`/${locale}/legal/terms`}
36
- >
37
- </OrganizationSwitcher.OrganizationProfilePage>
21
+ const t = await getTranslations({ locale, namespace: 'footer' });
22
+
23
+ const data: ClerkOrganizationData = {
24
+ homepage: 'Homepage',
25
+ terms: t('terms'),
26
+ privacy: t('privacy'),
27
+ locale,
28
+ className
29
+ };
38
30
 
39
- <OrganizationSwitcher.OrganizationProfilePage
40
- labelIcon={<icons.ShieldUser />}
41
- label="隐私"
42
- url={`/${locale}/legal/privacy`}
43
- >
44
- </OrganizationSwitcher.OrganizationProfilePage>
45
- </OrganizationSwitcher>
46
- </div>
47
- </div>
48
- );
31
+ return <ClerkOrganizationClient data={data} />;
49
32
  }
@@ -7,8 +7,6 @@
7
7
  * LICENSE file in the root directory of this source tree.
8
8
  */
9
9
 
10
- 'use client';
11
-
12
10
  import { SignIn, SignUp, Waitlist } from '@clerk/nextjs';
13
11
 
14
12
  export function createSignInPage() {
@@ -30,7 +30,7 @@ export function ClerkProviderClient({
30
30
  localization: currentLocalization,
31
31
  };
32
32
 
33
- // 只有传入参数非空时才拼接 URL 并添加对应的属性
33
+ // Only add URL when the parameter is not empty
34
34
  if (signInUrl) {
35
35
  clerkProviderProps.signInUrl = `/${locale}${signInUrl}`;
36
36
  }
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
4
+ import { ClerkLoaded, ClerkLoading, SignedIn, SignedOut, SignInButton, SignUpButton, UserButton } from "@clerk/nextjs";
5
+
6
+ interface ClerkUserData {
7
+ signIn: string;
8
+ signUp: string;
9
+ terms: string;
10
+ privacy: string;
11
+ locale: string;
12
+ clerkAuthInModal: boolean;
13
+ showSignUp: boolean;
14
+ }
15
+
16
+ export function ClerkUserClient({ data }: { data: ClerkUserData }) {
17
+ return (
18
+ <div className="ms-1.5 flex items-center gap-2 h-10 me-3">
19
+ <ClerkLoading>
20
+ <div className="w-20 h-9 px-2 border border-gray-300 rounded-full hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800 text-center text-sm"></div>
21
+ </ClerkLoading>
22
+ <ClerkLoaded>
23
+ <SignedOut>
24
+ <SignInButton mode={data.clerkAuthInModal ? 'modal' : 'redirect'}>
25
+ <button className="w-20 h-9 px-2 border border-gray-300 rounded-full hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800 text-center text-sm">
26
+ {data.signIn}
27
+ </button>
28
+ </SignInButton>
29
+ {data.showSignUp && (
30
+ <SignUpButton mode={data.clerkAuthInModal ? 'modal' : 'redirect'}>
31
+ <button className="w-20 h-9 px-2 border border-gray-300 rounded-full hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800 text-center text-sm">
32
+ {data.signUp}
33
+ </button>
34
+ </SignUpButton>
35
+ )}
36
+ </SignedOut>
37
+ <SignedIn>
38
+ <UserButton
39
+ appearance={{
40
+ elements: {
41
+ userButtonAvatarBox: "w-8 h-8 border",
42
+ }
43
+ }}
44
+ >
45
+ <UserButton.MenuItems>
46
+ <UserButton.Action label="manageAccount" />
47
+ {<UserButton.Link
48
+ labelIcon={<icons.ReceiptText className="size-4 fill-none stroke-[var(--clerk-icon-stroke-color)]" />}
49
+ label={data.terms}
50
+ href={`/${data.locale}/legal/terms`}>
51
+ </UserButton.Link>}
52
+ {<UserButton.Link
53
+ labelIcon={<icons.ShieldUser className="size-4 fill-none stroke-[var(--clerk-icon-stroke-color)]" />}
54
+ label={data.privacy}
55
+ href={`/${data.locale}/legal/privacy`}>
56
+ </UserButton.Link>}
57
+ <UserButton.Action label="signOut" />
58
+ </UserButton.MenuItems>
59
+ </UserButton>
60
+ </SignedIn>
61
+ </ClerkLoaded>
62
+ </div>
63
+ );
64
+ }
@@ -1,69 +1,40 @@
1
- 'use client';
2
-
3
- import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
4
- import { ClerkLoaded, ClerkLoading, SignedIn, SignedOut, SignInButton, SignUpButton, UserButton } from "@clerk/nextjs";
5
- import { useTranslations } from 'next-intl';
6
- import { type JSX } from 'react';
1
+ import { getTranslations } from 'next-intl/server';
2
+ import { ClerkUserClient } from './clerk-user-client';
7
3
 
8
4
  interface ClerkUserProps {
9
5
  locale: string;
10
- // default as true, cause Clerk direct is not well, so just use model for sign-in/sign-up
6
+ // default as true, 'cause Clerk direct is not well, so just use model for sign-in/sign-up
11
7
  clerkAuthInModal?: boolean;
12
8
  showSignUp?: boolean;
13
9
  }
14
10
 
15
- export function ClerkUser({
11
+ interface ClerkUserData {
12
+ signIn: string;
13
+ signUp: string;
14
+ terms: string;
15
+ privacy: string;
16
+ locale: string;
17
+ clerkAuthInModal: boolean;
18
+ showSignUp: boolean;
19
+ }
20
+
21
+ export async function ClerkUser({
16
22
  locale,
17
23
  clerkAuthInModal = true,
18
24
  showSignUp = true
19
- }: ClerkUserProps): JSX.Element {
20
- const t = useTranslations('clerk');
21
- const t2 = useTranslations('footer');
22
- return (
23
- <div className="ms-1.5 flex items-center gap-2 h-10 me-3">
24
- <ClerkLoading>
25
- <div className="w-20 h-9 px-2 border border-gray-300 rounded-full hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800 text-center text-sm"></div>
26
- </ClerkLoading>
27
- <ClerkLoaded>
28
- <SignedOut>
29
- <SignInButton mode={clerkAuthInModal ? 'modal' : 'redirect'}>
30
- <button className="w-20 h-9 px-2 border border-gray-300 rounded-full hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800 text-center text-sm">
31
- {t('signIn')}
32
- </button>
33
- </SignInButton>
34
- {showSignUp && (
35
- <SignUpButton mode={clerkAuthInModal ? 'modal' : 'redirect'}>
36
- <button className="w-20 h-9 px-2 border border-gray-300 rounded-full hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800 text-center text-sm">
37
- {t('signUp')}
38
- </button>
39
- </SignUpButton>
40
- )}
41
- </SignedOut>
42
- <SignedIn>
43
- <UserButton
44
- appearance={{
45
- elements: {
46
- userButtonAvatarBox: "w-8 h-8 border",
47
- }
48
- }}
49
- >
50
- <UserButton.MenuItems>
51
- <UserButton.Action label="manageAccount" />
52
- {<UserButton.Link
53
- labelIcon={<icons.ReceiptText className="size-4 fill-none stroke-[var(--clerk-icon-stroke-color)]" />}
54
- label={t2('terms')}
55
- href={`/${locale}/legal/terms`}>
56
- </UserButton.Link>}
57
- {<UserButton.Link
58
- labelIcon={<icons.ShieldUser className="size-4 fill-none stroke-[var(--clerk-icon-stroke-color)]" />}
59
- label={t2('privacy')}
60
- href={`/${locale}/legal/privacy`}>
61
- </UserButton.Link>}
62
- <UserButton.Action label="signOut" />
63
- </UserButton.MenuItems>
64
- </UserButton>
65
- </SignedIn>
66
- </ClerkLoaded>
67
- </div>
68
- );
25
+ }: ClerkUserProps) {
26
+ const t = await getTranslations({ locale, namespace: 'clerk' });
27
+ const t2 = await getTranslations({ locale, namespace: 'footer' });
28
+
29
+ const data: ClerkUserData = {
30
+ signIn: t('signIn'),
31
+ signUp: t('signUp'),
32
+ terms: t2('terms'),
33
+ privacy: t2('privacy'),
34
+ locale,
35
+ clerkAuthInModal,
36
+ showSignUp
37
+ };
38
+
39
+ return <ClerkUserClient data={data} />;
69
40
  }
@@ -1,6 +1,3 @@
1
1
  'use client';
2
2
  // Clerk related components
3
- export * from './clerk-organization';
4
- export * from './clerk-provider-client';
5
- export * from './clerk-user';
6
- export * from './clerk-page-generator';
3
+ export * from './clerk-provider-client';
@@ -0,0 +1,3 @@
1
+ export * from './clerk-page-generator';
2
+ export * from './clerk-user';
3
+ export * from './clerk-organization';
@@ -1,15 +1,12 @@
1
- 'use client'
2
-
3
- import { useTranslations } from 'next-intl';
1
+ import { getTranslations } from 'next-intl/server';
4
2
  import { Banner } from '@third-ui/fuma/mdx/banner';
5
3
 
6
- export function FumaBannerSuit({ showBanner }: { showBanner: boolean }) {
7
- const t = useTranslations('home');
4
+ export async function FumaBannerSuit({ locale, showBanner }: { locale: string, showBanner: boolean }) {
5
+ const t = await getTranslations({ locale, namespace: 'home' });
8
6
  const heightValue = showBanner ? 3 : 0.5;
9
7
  const height= `${heightValue}rem`;
10
8
  return (
11
9
  <>
12
- {/* 设置 header 的 top 位置为 Banner 的底部,避免间隙 */}
13
10
  {showBanner ? (
14
11
  <Banner variant="rainbow" changeLayout={true} height={heightValue}>
15
12
  <p className="text-xl">{t('banner')}</p>
@@ -53,7 +53,6 @@ export function Banner({
53
53
  const [open, setOpen] = useState(true);
54
54
  const globalKey = id ? `nd-banner-${id}` : null;
55
55
  const bannerHeight = `${height}rem`;
56
- const headerStartHeight = `${height + 5.5}rem`;
57
56
 
58
57
  useEffect(() => {
59
58
  if (globalKey) setOpen(localStorage.getItem(globalKey) !== 'true');
@@ -7,8 +7,6 @@ export * from './image-grid';
7
7
  export * from './zia-card';
8
8
  export * from './gradient-button';
9
9
  export * from './toc-base';
10
- export * from './fuma-banner-suit';
11
10
  export * from './fuma-github-info';
12
- export * from './site-x';
13
11
  export * from './zia-file';
14
12
  export * from './toc-footer-wrapper';
@@ -85,8 +85,10 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
85
85
  const delta = e.deltaY > 0 ? -0.1 : 0.1;
86
86
  setScale((prev) => clamp(prev + delta, 0.25, 10));
87
87
  } else {
88
+ // two-finger pan on touchpad: support both horizontal (deltaX) and vertical (deltaY)
89
+ e.preventDefault();
88
90
  e.stopPropagation();
89
- setTranslate((prev) => ({ x: prev.x, y: prev.y - e.deltaY }));
91
+ setTranslate((prev) => ({ x: prev.x - e.deltaX, y: prev.y - e.deltaY }));
90
92
  }
91
93
  }, []);
92
94
 
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+
2
3
  import { EditOnGitHub, LastUpdatedDate } from '@third-ui/fuma/mdx/toc-base';
3
4
  import React from 'react';
4
5
 
@@ -11,7 +11,6 @@ import Link from 'next/link';
11
11
 
12
12
  const itemVariants = 'flex flex-row items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-fd-accent hover:text-fd-accent-foreground [&_svg]:size-4';
13
13
 
14
- // 注解样式:色彩突出且主题适配
15
14
  const anotionClass = 'ms-2 px-2 py-0.5 rounded text-xs font-semibold bg-fd-accent/80 text-fd-accent-foreground dark:bg-white/20 dark:text-white';
16
15
 
17
16
  export interface ZiaFileProps extends HTMLAttributes<HTMLDivElement> {
@@ -1,3 +1,5 @@
1
1
  // Fumadocs related components, this is a server component!
2
2
  export * from './fuma-page-genarator';
3
- export * from './llm-copy-handler';
3
+ export * from './llm-copy-handler';
4
+ export * from './fuma-banner-suit';
5
+ export * from './site-x';
@@ -1,16 +1,15 @@
1
- 'use client';
2
-
1
+ import { getTranslations } from 'next-intl/server';
3
2
  import { cn } from '@lib/utils';
4
3
  import type { HTMLAttributes } from 'react';
5
- import { useTranslations } from 'next-intl';
6
4
 
7
5
  export type SiteXProps = Omit<HTMLAttributes<HTMLSpanElement>, 'type'> & {
6
+ locale: string;
8
7
  type: 'site' | 'email';
9
8
  namespace?: string;
10
9
  tKey?: string;
11
10
  };
12
11
 
13
- export function SiteX({ type, namespace, tKey, className, ...props }: SiteXProps) {
12
+ export async function SiteX({ locale, type, namespace, tKey, className, ...props }: SiteXProps) {
14
13
  // 默认命名空间和key
15
14
  let ns = namespace;
16
15
  let key = tKey;
@@ -20,7 +19,7 @@ export function SiteX({ type, namespace, tKey, className, ...props }: SiteXProps
20
19
  if (!key) {
21
20
  key = type === 'site' ? 'title' : 'email';
22
21
  }
23
- const t = useTranslations(ns);
22
+ const t = await getTranslations({ locale, namespace: ns });
24
23
  const text = t(key, { defaultValue: type === 'site' ? 'Site----' : '----@example.com' });
25
24
 
26
25
  if (type === 'site') {
package/src/main/cta.tsx CHANGED
@@ -1,12 +1,35 @@
1
- 'use client'
2
-
1
+ import { getTranslations } from 'next-intl/server';
3
2
  import { GradientButton } from "@third-ui/fuma/mdx/gradient-button";
4
- import { useTranslations } from 'next-intl';
5
3
  import { cn } from '@lib/utils';
6
4
  import { richText } from '@third-ui/main/rich-text-expert';
7
5
 
8
- export function CTA({ sectionClassName }: { sectionClassName?: string }) {
9
- const t = useTranslations('cta');
6
+ interface CTAData {
7
+ title: string;
8
+ eyesOn: string;
9
+ description1: string;
10
+ description2: string;
11
+ button: string;
12
+ url: string;
13
+ }
14
+
15
+ export async function CTA({
16
+ locale,
17
+ sectionClassName
18
+ }: {
19
+ locale: string;
20
+ sectionClassName?: string;
21
+ }) {
22
+ const t = await getTranslations({ locale, namespace: 'cta' });
23
+
24
+ const data: CTAData = {
25
+ title: t('title'),
26
+ eyesOn: t('eyesOn'),
27
+ description1: richText(t, 'description1'),
28
+ description2: t('description2'),
29
+ button: t('button'),
30
+ url: t('url')
31
+ };
32
+
10
33
  return (
11
34
  <section id="cta" className={cn("px-16 py-20 mx-16 md:mx-32 scroll-mt-20", sectionClassName)}>
12
35
  <div className="
@@ -16,16 +39,16 @@ export function CTA({ sectionClassName }: { sectionClassName?: string }) {
16
39
  bg-[length:200%_auto] animate-cta-gradient-wave
17
40
  ">
18
41
  <h2 className="text-3xl md:text-4xl font-bold mb-6">
19
- {t('title')} <span className="text-purple-400">{t('eyesOn')}</span>?
42
+ {data.title} <span className="text-purple-400">{data.eyesOn}</span>?
20
43
  </h2>
21
44
  <p className="text-2xl mx-auto mb-8">
22
- {richText(t, 'description1')}
45
+ {data.description1}
23
46
  <br />
24
- <span className="text-purple-400">{t('description2')}</span>
47
+ <span className="text-purple-400">{data.description2}</span>
25
48
  </p>
26
49
  <GradientButton
27
- title={t('button')}
28
- href={t('url')}
50
+ title={data.button}
51
+ href={data.url}
29
52
  align="center"
30
53
  />
31
54
  </div>
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+
5
+ interface FAQData {
6
+ title: string;
7
+ description: string;
8
+ items: Array<{
9
+ id: string;
10
+ question: string;
11
+ answer: string;
12
+ }>;
13
+ }
14
+
15
+ export function FAQInteractive({ data }: { data: FAQData }) {
16
+ const [openStates, setOpenStates] = useState<Record<string, boolean>>({});
17
+
18
+ useEffect(() => {
19
+ // Progressive enhancement: Add interactivity to existing DOM elements
20
+ data.items.forEach((item) => {
21
+ const toggleButton = document.querySelector(`[data-faq-toggle="${item.id}"]`) as HTMLButtonElement;
22
+ const contentDiv = document.querySelector(`[data-faq-content="${item.id}"]`) as HTMLDivElement;
23
+ const iconSvg = document.querySelector(`[data-faq-icon="${item.id}"]`) as SVGElement;
24
+
25
+ if (toggleButton && contentDiv && iconSvg) {
26
+ const handleClick = () => {
27
+ const isOpen = openStates[item.id] || false;
28
+ const newOpenState = !isOpen;
29
+
30
+ // Update state
31
+ setOpenStates(prev => ({
32
+ ...prev,
33
+ [item.id]: newOpenState
34
+ }));
35
+
36
+ // Update DOM
37
+ if (newOpenState) {
38
+ contentDiv.classList.remove('hidden');
39
+ toggleButton.setAttribute('aria-expanded', 'true');
40
+ iconSvg.style.transform = 'rotate(90deg)';
41
+ } else {
42
+ contentDiv.classList.add('hidden');
43
+ toggleButton.setAttribute('aria-expanded', 'false');
44
+ iconSvg.style.transform = 'rotate(0deg)';
45
+ }
46
+ };
47
+
48
+ toggleButton.addEventListener('click', handleClick);
49
+
50
+ // Cleanup function will be handled by the effect cleanup
51
+ }
52
+ });
53
+
54
+ // Cleanup event listeners
55
+ return () => {
56
+ data.items.forEach((item) => {
57
+ const toggleButton = document.querySelector(`[data-faq-toggle="${item.id}"]`) as HTMLButtonElement;
58
+ if (toggleButton) {
59
+ // Remove all event listeners by cloning the element
60
+ const newButton = toggleButton.cloneNode(true);
61
+ toggleButton.parentNode?.replaceChild(newButton, toggleButton);
62
+ }
63
+ });
64
+ };
65
+ }, [data, openStates]);
66
+
67
+ return null; // Progressive enhancement - no additional DOM rendering
68
+ }