@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/src/main/faq.tsx CHANGED
@@ -1,61 +1,85 @@
1
- 'use client';
2
-
3
- import { useState } from "react";
4
- import { useTranslations } from 'next-intl';
5
- import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
1
+ import { getTranslations } from 'next-intl/server';
6
2
  import { cn } from '@lib/utils';
7
3
  import { richText } from '@third-ui/main/rich-text-expert';
4
+ import { FAQInteractive } from './faq-interactive';
8
5
 
9
- export function FAQ({ sectionClassName }: { sectionClassName?: string }) {
10
- const t = useTranslations('faq');
11
- const items = t.raw('items') as Array<{
6
+ interface FAQData {
7
+ title: string;
8
+ description: string;
9
+ items: Array<{
10
+ id: string;
12
11
  question: string;
13
12
  answer: string;
14
13
  }>;
15
- const [openArr, setOpenArr] = useState<boolean[]>(() => items.map(() => false));
14
+ }
16
15
 
17
- const handleToggle = (idx: number) => {
18
- setOpenArr(prev => {
19
- const next = [...prev];
20
- next[idx] = !next[idx];
21
- return next;
22
- });
16
+ export async function FAQ({
17
+ locale,
18
+ sectionClassName
19
+ }: {
20
+ locale: string;
21
+ sectionClassName?: string;
22
+ }) {
23
+ const t = await getTranslations({ locale, namespace: 'faq' });
24
+
25
+ // Process translation data
26
+ const rawItems = t.raw('items') as Array<{
27
+ question: string;
28
+ answer: string;
29
+ }>;
30
+
31
+ const data: FAQData = {
32
+ title: t('title'),
33
+ description: richText(t, 'description'),
34
+ items: rawItems.map((item, index) => ({
35
+ id: `faq-item-${index}`,
36
+ question: item.question,
37
+ answer: richText(t, `items.${index}.answer`)
38
+ }))
23
39
  };
24
40
 
25
41
  return (
26
42
  <section id="faq" className={cn("px-16 py-10 mx-16 md:mx-32 scroll-mt-20", sectionClassName)}>
27
43
  <h2 className="text-3xl md:text-4xl font-bold text-center mb-4">
28
- {t('title')}
44
+ {data.title}
29
45
  </h2>
30
46
  <p className="text-center text-gray-600 dark:text-gray-400 mb-12 text-base md:text-lg mx-auto">
31
- {richText(t, 'description')}
47
+ {data.description}
32
48
  </p>
33
49
  <div className="space-y-6">
34
- {items.map((item, idx) => {
35
- const isOpen = openArr[idx];
36
- const Icon = isOpen ? icons.ChevronDown : icons.ChevronRight;
37
- return (
38
- <div
39
- key={idx}
40
- className="bg-white dark:bg-gray-800/60 p-6 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-purple-300 dark:hover:border-purple-500/50 transition shadow-sm dark:shadow-none"
50
+ {data.items.map((item) => (
51
+ <div
52
+ key={item.id}
53
+ data-faq-id={item.id}
54
+ className="bg-white dark:bg-gray-800/60 p-6 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-purple-300 dark:hover:border-purple-500/50 transition shadow-sm dark:shadow-none"
55
+ >
56
+ <button
57
+ className="w-full flex items-center justify-between text-left focus:outline-none"
58
+ data-faq-toggle={item.id}
59
+ aria-expanded="false"
41
60
  >
42
- <button
43
- className="w-full flex items-center justify-between text-left focus:outline-none"
44
- onClick={() => handleToggle(idx)}
45
- aria-expanded={isOpen}
61
+ <span className="text-lg font-semibold text-gray-900 dark:text-gray-100">{item.question}</span>
62
+ <svg
63
+ className="w-6 h-6 text-gray-400 ml-2 transition-transform duration-200"
64
+ data-faq-icon={item.id}
65
+ fill="none"
66
+ stroke="currentColor"
67
+ viewBox="0 0 24 24"
46
68
  >
47
- <span className="text-lg font-semibold text-gray-900 dark:text-gray-100">{item.question}</span>
48
- <Icon className="w-6 h-6 text-gray-400 ml-2 transition-transform duration-200" />
49
- </button>
50
- {isOpen && (
51
- <div className="mt-4 text-gray-700 dark:text-gray-300 text-base">
52
- {richText(t, `items.${idx}.answer`)}
53
- </div>
54
- )}
69
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
70
+ </svg>
71
+ </button>
72
+ <div
73
+ className="mt-4 text-gray-700 dark:text-gray-300 text-base hidden"
74
+ data-faq-content={item.id}
75
+ >
76
+ {item.answer}
55
77
  </div>
56
- );
57
- })}
78
+ </div>
79
+ ))}
58
80
  </div>
81
+
82
+ <FAQInteractive data={data} />
59
83
  </section>
60
84
  );
61
85
  }
@@ -1,42 +1,71 @@
1
- 'use client'
2
-
1
+ import { getTranslations } from 'next-intl/server';
3
2
  import { getGlobalIcon } from '@base-ui/components/global-icon';
4
- import { useTranslations } from 'next-intl'
5
3
  import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
6
4
  import { cn } from '@lib/utils';
7
5
  import { richText } from '@third-ui/main/rich-text-expert';
8
6
 
9
- export function Features({ sectionClassName }: { sectionClassName?: string }) {
10
- const t = useTranslations('features');
7
+ interface FeaturesData {
8
+ title: string;
9
+ eyesOn: string;
10
+ description: string;
11
+ items: Array<{
12
+ id: string;
13
+ title: string;
14
+ description: string;
15
+ iconKey: keyof typeof icons;
16
+ }>;
17
+ }
18
+
19
+ export async function Features({
20
+ locale,
21
+ sectionClassName
22
+ }: {
23
+ locale: string;
24
+ sectionClassName?: string;
25
+ }) {
26
+ const t = await getTranslations({ locale, namespace: 'features' });
11
27
 
12
- // 直接从翻译文件获取特性列表
28
+ // Process translation data
13
29
  const featureItems = t.raw('items') as Array<{
14
30
  title: string;
15
31
  description: string;
16
32
  iconKey: keyof typeof icons;
17
33
  }>;
34
+
35
+ const data: FeaturesData = {
36
+ title: t('title'),
37
+ eyesOn: t('eyesOn'),
38
+ description: richText(t, 'description'),
39
+ items: featureItems.map((feature, index) => ({
40
+ id: `feature-item-${index}`,
41
+ title: feature.title,
42
+ description: richText(t, `items.${index}.description`),
43
+ iconKey: feature.iconKey
44
+ }))
45
+ };
18
46
 
19
47
  return (
20
48
  <section id="features" className={cn("px-16 py-10 mx-16 md:mx-32 scroll-mt-18", sectionClassName)}>
21
49
  <h2 className="text-3xl md:text-4xl font-bold text-center mb-4">
22
- {t('title')} <span className="text-purple-500">{t('eyesOn')}</span>
50
+ {data.title} <span className="text-purple-500">{data.eyesOn}</span>
23
51
  </h2>
24
52
  <p className="text-center text-gray-600 dark:text-gray-400 mb-12 text-base md:text-lg mx-auto whitespace-nowrap">
25
- {richText(t, 'description')}
53
+ {data.description}
26
54
  </p>
27
55
  <div className="grid grid-cols-1 md:grid-cols-3 gap-8 gap-y-12">
28
- {featureItems.map((feature, index) => {
56
+ {data.items.map((feature) => {
29
57
  const Icon = getGlobalIcon(feature.iconKey);
30
58
  return (
31
59
  <div
32
- key={index}
60
+ key={feature.id}
61
+ data-feature-id={feature.id}
33
62
  className="bg-white dark:bg-gray-800/60 p-8 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-purple-300 dark:hover:border-purple-500/50 transition shadow-sm dark:shadow-none"
34
63
  >
35
64
  <div className="text-4xl mb-4 flex items-center justify-start">
36
65
  <Icon className="w-8 h-8" />
37
66
  </div>
38
67
  <h3 className="text-xl font-semibold mb-3 text-gray-900 dark:text-gray-100">{feature.title}</h3>
39
- <p className="text-gray-700 dark:text-gray-300">{richText(t, `items.${index}.description`)}</p>
68
+ <p className="text-gray-700 dark:text-gray-300">{feature.description}</p>
40
69
  </div>
41
70
  )
42
71
  })}
@@ -1,45 +1,56 @@
1
- 'use client'
2
-
1
+ import { getTranslations } from 'next-intl/server';
3
2
  import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
4
- import { useLocale, useTranslations } from 'next-intl';
5
3
  import Link from "next/link";
6
4
 
7
- export function Footer() {
8
- const tFooter = useTranslations('footer');
9
- const locale = useLocale();
5
+ interface FooterData {
6
+ terms: string;
7
+ privacy: string;
8
+ contactUs: string;
9
+ email: string;
10
+ copyright: string;
11
+ company: string;
12
+ }
13
+
14
+ export async function Footer({ locale }: { locale: string }) {
15
+ const tFooter = await getTranslations({ locale, namespace: 'footer' });
16
+
17
+ const data: FooterData = {
18
+ terms: tFooter('terms', { defaultValue: 'Terms of Service' }),
19
+ privacy: tFooter('privacy', { defaultValue: 'Privacy Policy' }),
20
+ contactUs: tFooter('contactUs', { defaultValue: 'Contact Us' }),
21
+ email: tFooter('email'),
22
+ company: tFooter('company'),
23
+ copyright: tFooter('copyright', { year: new Date().getFullYear(), name: tFooter('company') })
24
+ };
10
25
 
11
26
  return (
12
27
  <div className="mb-10 w-full mx-auto border-t-purple-700/80 border-t-1">
13
28
  <footer>
14
29
  <div className="w-full flex flex-col items-center justify-center px-4 py-8 space-y-3">
15
- {/* 第一行:居中icon跳转链接 */}
16
30
  <div className="flex items-center justify-center space-x-6 text-xs">
17
31
  <Link href={`/${locale}/legal/terms`} className="flex items-center space-x-1 hover:underline">
18
32
  <icons.ReceiptText className="h-3.5 w-3.5"/>
19
- <span>{tFooter('terms', { defaultValue: 'Terms of Service' })}</span>
33
+ <span>{data.terms}</span>
20
34
  </Link>
21
35
  <Link href={`/${locale}/legal/privacy`} className="flex items-center space-x-1 hover:underline">
22
36
  <icons.ShieldUser className="h-3.5 w-3.5"/>
23
- <span>{tFooter('privacy', { defaultValue: 'Privacy Policy' })}</span>
37
+ <span>{data.privacy}</span>
24
38
  </Link>
25
39
  <div className="relative group">
26
40
  <div className="absolute left-2/3 -translate-x-1/4 bottom-full mb-1 hidden group-hover:block bg-zinc-600 text-white text-xs rounded px-3 py-1 whitespace-nowrap z-10 shadow-lg">
27
- {tFooter('email')}
41
+ {data.email}
28
42
  </div>
29
43
  <a
30
- href={`mailto:${tFooter('email')}`}
44
+ href={`mailto:${data.email}`}
31
45
  className="flex items-center space-x-1 underline cursor-pointer px-2"
32
46
  >
33
47
  <icons.Mail className="h-3.5 w-3.5"/>
34
- <span>{tFooter('contactUs', { defaultValue: 'Contact Us' })}</span>
48
+ <span>{data.contactUs}</span>
35
49
  </a>
36
50
  </div>
37
51
  </div>
38
- {/* 第二行:版权声明 */}
39
52
  <div className="text-xs text-center">
40
- <span>
41
- {tFooter('copyright', { year: new Date().getFullYear(), name: tFooter('company') })}
42
- </span>
53
+ <span>{data.copyright}</span>
43
54
  </div>
44
55
  </div>
45
56
  </footer>
@@ -0,0 +1,171 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+
5
+ interface GalleryItem {
6
+ id: string;
7
+ url: string;
8
+ altMsg: string;
9
+ }
10
+
11
+ interface GalleryData {
12
+ titleL: string;
13
+ eyesOn: string;
14
+ titleR: string;
15
+ description: string;
16
+ items: GalleryItem[];
17
+ defaultImgUrl: string;
18
+ downloadPrefix: string;
19
+ }
20
+
21
+ export function GalleryInteractive({ data }: { data: GalleryData }) {
22
+ const [imageErrors, setImageErrors] = useState<Set<string>>(new Set());
23
+ const [downloadingItems, setDownloadingItems] = useState<Set<string>>(new Set());
24
+
25
+ // Get CDN proxy URL from environment
26
+ const cdnProxyUrl = process.env.NEXT_PUBLIC_STYLE_CDN_PROXY_URL;
27
+
28
+ useEffect(() => {
29
+ // Progressive enhancement: Add download functionality and error handling
30
+ data.items.forEach((item, index) => {
31
+ const downloadButton = document.querySelector(`[data-gallery-download="${item.id}"]`) as HTMLButtonElement;
32
+ const imageElement = document.querySelector(`[data-gallery-image="${item.id}"]`) as HTMLImageElement;
33
+
34
+ if (downloadButton) {
35
+ const handleDownload = async () => {
36
+ // Prevent duplicate clicks
37
+ if (downloadingItems.has(item.id)) {
38
+ return;
39
+ }
40
+
41
+ // Set download status
42
+ setDownloadingItems(prev => new Set(prev).add(item.id));
43
+
44
+ try {
45
+ if (!cdnProxyUrl) {
46
+ throw new Error('CDN proxy URL not configured');
47
+ }
48
+
49
+ // Use R2 proxy to download directly
50
+ const originalUrl = new URL(item.url);
51
+ const filename = originalUrl.pathname.substring(1);
52
+
53
+ // Build proxy download URL
54
+ const proxyUrl = `${cdnProxyUrl}/${encodeURIComponent(filename)}`;
55
+
56
+ // Extract file extension from URL
57
+ const urlExtension = item.url.split('.').pop()?.toLowerCase();
58
+ let extension = '.webp';
59
+ if (urlExtension && ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(urlExtension)) {
60
+ extension = `.${urlExtension}`;
61
+ }
62
+
63
+ // Fetch file from proxy
64
+ const response = await fetch(proxyUrl);
65
+
66
+ if (!response.ok) {
67
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
68
+ }
69
+
70
+ // Convert to blob
71
+ const blob = await response.blob();
72
+ const blobUrl = URL.createObjectURL(blob);
73
+
74
+ // Create download link and trigger download
75
+ const a = document.createElement('a');
76
+ a.href = blobUrl;
77
+ a.download = `${data.downloadPrefix}-${index + 1}${extension}`;
78
+ a.style.display = 'none';
79
+ document.body.appendChild(a);
80
+ a.click();
81
+
82
+ // Clean up DOM elements and blob URL
83
+ setTimeout(() => {
84
+ document.body.removeChild(a);
85
+ URL.revokeObjectURL(blobUrl);
86
+ }, 100);
87
+
88
+ } catch (error) {
89
+ console.error('Download failed:', error);
90
+ } finally {
91
+ // Clear download status
92
+ setDownloadingItems(prev => {
93
+ const newSet = new Set(prev);
94
+ newSet.delete(item.id);
95
+ return newSet;
96
+ });
97
+ }
98
+ };
99
+
100
+ downloadButton.addEventListener('click', handleDownload);
101
+ }
102
+
103
+ if (imageElement) {
104
+ const handleImageError = () => {
105
+ setImageErrors(prev => new Set(prev).add(item.id));
106
+ // Update image src to default
107
+ imageElement.src = data.defaultImgUrl;
108
+ };
109
+
110
+ imageElement.addEventListener('error', handleImageError);
111
+ }
112
+ });
113
+
114
+ // Update download button states based on downloading status
115
+ const updateDownloadStates = () => {
116
+ data.items.forEach((item) => {
117
+ const downloadButton = document.querySelector(`[data-gallery-download="${item.id}"]`) as HTMLButtonElement;
118
+ if (downloadButton) {
119
+ const isDownloading = downloadingItems.has(item.id);
120
+
121
+ if (isDownloading) {
122
+ downloadButton.disabled = true;
123
+ downloadButton.classList.add('bg-black/30', 'text-white/50');
124
+ downloadButton.classList.remove('bg-black/50', 'hover:bg-black/70', 'text-white/80', 'hover:text-white');
125
+
126
+ // Replace icon with spinner
127
+ downloadButton.innerHTML = `
128
+ <svg class="h-5 w-5 text-white animate-spin" fill="none" viewBox="0 0 24 24">
129
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
130
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
131
+ </svg>
132
+ `;
133
+ } else {
134
+ downloadButton.disabled = false;
135
+ downloadButton.classList.remove('bg-black/30', 'text-white/50');
136
+ downloadButton.classList.add('bg-black/50', 'hover:bg-black/70', 'text-white/80', 'hover:text-white');
137
+
138
+ // Reset to download icon
139
+ downloadButton.innerHTML = `
140
+ <svg class="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
141
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
142
+ </svg>
143
+ `;
144
+ }
145
+ }
146
+ });
147
+ };
148
+
149
+ updateDownloadStates();
150
+
151
+ // Cleanup event listeners
152
+ return () => {
153
+ data.items.forEach((item) => {
154
+ const downloadButton = document.querySelector(`[data-gallery-download="${item.id}"]`) as HTMLButtonElement;
155
+ const imageElement = document.querySelector(`[data-gallery-image="${item.id}"]`) as HTMLImageElement;
156
+
157
+ if (downloadButton) {
158
+ const newButton = downloadButton.cloneNode(true);
159
+ downloadButton.parentNode?.replaceChild(newButton, downloadButton);
160
+ }
161
+
162
+ if (imageElement) {
163
+ const newImage = imageElement.cloneNode(true);
164
+ imageElement.parentNode?.replaceChild(newImage, imageElement);
165
+ }
166
+ });
167
+ };
168
+ }, [data, downloadingItems, imageErrors, cdnProxyUrl]);
169
+
170
+ return null; // Progressive enhancement - no additional DOM rendering
171
+ }
@@ -1,135 +1,86 @@
1
- 'use client'
2
-
3
- import { globalLucideIcons as icons } from "@base-ui/components/global-icon"
1
+ import { getTranslations } from 'next-intl/server';
4
2
  import { cn } from '@lib/utils'
5
- import { useTranslations } from 'next-intl'
6
3
  import Image from "next/image"
7
- import { useState } from 'react'
4
+ import { GalleryInteractive } from './gallery-interactive';
8
5
 
9
6
  interface GalleryItem {
7
+ id: string;
10
8
  url: string;
11
9
  altMsg: string;
12
10
  }
13
11
 
12
+ interface GalleryData {
13
+ titleL: string;
14
+ eyesOn: string;
15
+ titleR: string;
16
+ description: string;
17
+ items: GalleryItem[];
18
+ defaultImgUrl: string;
19
+ downloadPrefix: string;
20
+ }
21
+
14
22
  interface GalleryProps {
23
+ locale: string;
15
24
  sectionClassName?: string;
16
25
  button?: React.ReactNode;
17
26
  }
18
27
 
19
- export function Gallery({ sectionClassName, button }: GalleryProps) {
20
- const t = useTranslations('gallery');
21
- const galleryItems = t.raw('prompts') as GalleryItem[];
22
- const defaultImgUrl = t.raw('defaultImgUrl') as string;
23
- const [imageErrors, setImageErrors] = useState<Set<number>>(new Set());
24
- const [downloadingItems, setDownloadingItems] = useState<Set<number>>(new Set());
25
- const cdnProxyUrl = process.env.NEXT_PUBLIC_STYLE_CDN_PROXY_URL!;
26
-
27
- const handleDownload = async (item: GalleryItem, index: number) => {
28
- // prevent duplicate clicks
29
- if (downloadingItems.has(index)) {
30
- return;
31
- }
32
-
33
- // set download status
34
- setDownloadingItems(prev => new Set(prev).add(index));
35
-
36
- try {
37
- // use R2 proxy to download directly, no need to fetch
38
- // convert original R2 URL to proxy URL
39
- const originalUrl = new URL(item.url);
40
- const filename = originalUrl.pathname.substring(1);
41
-
42
- // build proxy download URL
43
- const proxyUrl = `${cdnProxyUrl}/${encodeURIComponent(filename)}`;
44
-
45
- // extract file extension from URL
46
- const urlExtension = item.url.split('.').pop()?.toLowerCase();
47
- let extension = '.webp';
48
- if (urlExtension && ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(urlExtension)) {
49
- extension = `.${urlExtension}`;
50
- }
51
- const downloadPrefix = t('downloadPrefix');
52
-
53
- // fetch file from proxy
54
- const response = await fetch(proxyUrl);
55
-
56
- if (!response.ok) {
57
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
58
- }
59
-
60
- // convert to blob
61
- const blob = await response.blob();
62
- const blobUrl = URL.createObjectURL(blob);
63
-
64
- // create download link and trigger download
65
- const a = document.createElement('a');
66
- a.href = blobUrl;
67
- a.download = `${downloadPrefix}-${index + 1}${extension}`;
68
- a.style.display = 'none';
69
- document.body.appendChild(a);
70
- a.click();
71
-
72
- // clean up DOM elements and blob URL
73
- setTimeout(() => {
74
- document.body.removeChild(a);
75
- URL.revokeObjectURL(blobUrl);
76
- }, 100);
77
-
78
- } catch (error) {
79
- console.error('Download failed:', error);
80
- } finally {
81
- // clear download status
82
- setDownloadingItems(prev => {
83
- const newSet = new Set(prev);
84
- newSet.delete(index);
85
- return newSet;
86
- });
87
- }
88
- };
89
-
90
- const handleImageError = (index: number) => {
91
- setImageErrors(prev => new Set(prev).add(index));
92
- };
93
-
94
- const getImageSrc = (item: GalleryItem, index: number) => {
95
- return imageErrors.has(index) ? defaultImgUrl : item.url;
28
+ export async function Gallery({ locale, sectionClassName, button }: GalleryProps) {
29
+ const t = await getTranslations({ locale, namespace: 'gallery' });
30
+
31
+ // Process translation data
32
+ const galleryItems = t.raw('prompts') as Array<{
33
+ url: string;
34
+ altMsg: string;
35
+ }>;
36
+
37
+ const data: GalleryData = {
38
+ titleL: t('titleL'),
39
+ eyesOn: t('eyesOn'),
40
+ titleR: t('titleR'),
41
+ description: t('description'),
42
+ items: galleryItems.map((item, index) => ({
43
+ id: `gallery-item-${index}`,
44
+ url: item.url,
45
+ altMsg: item.altMsg
46
+ })),
47
+ defaultImgUrl: t.raw('defaultImgUrl') as string,
48
+ downloadPrefix: t('downloadPrefix')
96
49
  };
97
50
 
98
51
  return (
99
52
  <section id="gallery" className={cn("container mx-auto px-4 py-20 scroll-mt-20", sectionClassName)}>
100
53
  <h2 className="text-3xl md:text-4xl font-bold text-center mb-6">
101
- {t('titleL')} <span className="text-purple-500">{t('eyesOn')}</span> {t('titleR')}
54
+ {data.titleL} <span className="text-purple-500">{data.eyesOn}</span> {data.titleR}
102
55
  </h2>
103
56
  <p className="text-center max-w-2xl mx-auto mb-16">
104
- {t('description')}
57
+ {data.description}
105
58
  </p>
106
59
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
107
- {galleryItems.map((item, index) => (
108
- <div key={index} className="group relative overflow-hidden rounded-xl">
60
+ {data.items.map((item, index) => (
61
+ <div
62
+ key={item.id}
63
+ className="group relative overflow-hidden rounded-xl"
64
+ data-gallery-item={item.id}
65
+ data-gallery-index={index}
66
+ >
109
67
  <Image
110
- src={getImageSrc(item, index)}
68
+ src={item.url}
111
69
  alt={item.altMsg}
112
70
  width={600}
113
71
  height={600}
114
72
  className="w-full h-80 object-cover transition duration-300 group-hover:scale-105"
115
- onError={() => handleImageError(index)}
73
+ data-gallery-image={item.id}
116
74
  />
117
75
  <div className="absolute inset-0 flex items-end justify-end p-4 opacity-0 group-hover:opacity-100 transition duration-300">
118
76
  <button
119
- onClick={() => handleDownload(item, index)}
120
- disabled={downloadingItems.has(index)}
121
- className={cn(
122
- "p-2 rounded-full transition-all duration-300",
123
- downloadingItems.has(index)
124
- ? "bg-black/30 text-white/50"
125
- : "bg-black/50 hover:bg-black/70 text-white/80 hover:text-white"
126
- )}
77
+ className="p-2 rounded-full bg-black/50 hover:bg-black/70 text-white/80 hover:text-white transition-all duration-300"
78
+ data-gallery-download={item.id}
79
+ aria-label={`Download ${item.altMsg}`}
127
80
  >
128
- {downloadingItems.has(index) ? (
129
- <icons.Loader2 className="h-5 w-5 text-white animate-spin" />
130
- ) : (
131
- <icons.Download className="h-5 w-5 text-white" />
132
- )}
81
+ <svg className="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
82
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
83
+ </svg>
133
84
  </button>
134
85
  </div>
135
86
  </div>
@@ -140,6 +91,8 @@ export function Gallery({ sectionClassName, button }: GalleryProps) {
140
91
  {button}
141
92
  </div>
142
93
  )}
94
+
95
+ <GalleryInteractive data={data} />
143
96
  </section>
144
97
  )
145
98
  }
package/src/main/index.ts CHANGED
@@ -1,18 +1,9 @@
1
1
  'use client';
2
2
 
3
- // Main application components
4
- export * from './gallery';
5
- export * from './usage';
6
- export * from './features';
7
- export * from './tips';
8
- export * from './faq';
9
- export * from './seo-content';
10
- export * from './cta';
11
- export * from './footer';
3
+ // Main application Client components
12
4
  export * from './go-to-top';
13
5
  export * from './loading';
14
6
  export * from './nprogress-bar';
15
- export * from './price-plan';
16
7
  export * from './ads-alert-dialog';
17
8
  export * from './x-button'
18
9
  export * from './ai-prompt-textarea'