@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
package/src/main/faq.tsx
CHANGED
|
@@ -1,61 +1,85 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
+
}
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
47
|
+
{data.description}
|
|
32
48
|
</p>
|
|
33
49
|
<div className="space-y-6">
|
|
34
|
-
{items.map((item
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
}
|
package/src/main/features.tsx
CHANGED
|
@@ -1,42 +1,71 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
53
|
+
{data.description}
|
|
26
54
|
</p>
|
|
27
55
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 gap-y-12">
|
|
28
|
-
{
|
|
56
|
+
{data.items.map((feature) => {
|
|
29
57
|
const Icon = getGlobalIcon(feature.iconKey);
|
|
30
58
|
return (
|
|
31
59
|
<div
|
|
32
|
-
key={
|
|
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">{
|
|
68
|
+
<p className="text-gray-700 dark:text-gray-300">{feature.description}</p>
|
|
40
69
|
</div>
|
|
41
70
|
)
|
|
42
71
|
})}
|
package/src/main/footer.tsx
CHANGED
|
@@ -1,45 +1,56 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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>{
|
|
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>{
|
|
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
|
-
{
|
|
41
|
+
{data.email}
|
|
28
42
|
</div>
|
|
29
43
|
<a
|
|
30
|
-
href={`mailto:${
|
|
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>{
|
|
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
|
+
}
|
package/src/main/gallery.tsx
CHANGED
|
@@ -1,135 +1,86 @@
|
|
|
1
|
-
|
|
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 {
|
|
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 =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
108
|
-
<div
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
129
|
-
<
|
|
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'
|