@windrun-huaiin/third-ui 5.11.5 → 5.12.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "5.11.5",
3
+ "version": "5.12.1",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -1,37 +1,89 @@
1
1
  'use client'
2
2
 
3
3
  import { globalLucideIcons as icons } from "@base-ui/components/global-icon"
4
+ import { cn } from '@lib/utils'
4
5
  import { useTranslations } from 'next-intl'
5
6
  import Image from "next/image"
6
- import { GradientButton } from "@third-ui/fuma/mdx/gradient-button"
7
- import { cn } from '@lib/utils';
8
- import { useState } from 'react';
7
+ import { useState } from 'react'
9
8
 
10
9
  interface GalleryItem {
11
10
  url: string;
12
11
  altMsg: string;
13
12
  }
14
13
 
15
- export function Gallery({ sectionClassName }: { sectionClassName?: string }) {
14
+ interface GalleryProps {
15
+ sectionClassName?: string;
16
+ button?: React.ReactNode;
17
+ }
18
+
19
+ export function Gallery({ sectionClassName, button }: GalleryProps) {
16
20
  const t = useTranslations('gallery');
17
21
  const galleryItems = t.raw('prompts') as GalleryItem[];
18
22
  const defaultImgUrl = t.raw('defaultImgUrl') as string;
19
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!;
20
26
 
21
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
+
22
36
  try {
23
- const response = await fetch(item.url);
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
24
61
  const blob = await response.blob();
25
- const url = window.URL.createObjectURL(blob);
62
+ const blobUrl = URL.createObjectURL(blob);
63
+
64
+ // create download link and trigger download
26
65
  const a = document.createElement('a');
27
- a.href = url;
28
- a.download = `reve-image-${index + 1}.webp`;
66
+ a.href = blobUrl;
67
+ a.download = `${downloadPrefix}-${index + 1}${extension}`;
68
+ a.style.display = 'none';
29
69
  document.body.appendChild(a);
30
70
  a.click();
31
- window.URL.revokeObjectURL(url);
32
- document.body.removeChild(a);
71
+
72
+ // clean up DOM elements and blob URL
73
+ setTimeout(() => {
74
+ document.body.removeChild(a);
75
+ URL.revokeObjectURL(blobUrl);
76
+ }, 100);
77
+
33
78
  } catch (error) {
34
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
+ });
35
87
  }
36
88
  };
37
89
 
@@ -65,22 +117,29 @@ export function Gallery({ sectionClassName }: { sectionClassName?: string }) {
65
117
  <div className="absolute inset-0 flex items-end justify-end p-4 opacity-0 group-hover:opacity-100 transition duration-300">
66
118
  <button
67
119
  onClick={() => handleDownload(item, index)}
68
- className="bg-black/50 hover:bg-black/70 p-2 rounded-full text-white/80 hover:text-white transition-all duration-300"
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
+ )}
69
127
  >
70
- <icons.Download className="h-5 w-5 text-white" />
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
+ )}
71
133
  </button>
72
134
  </div>
73
135
  </div>
74
136
  ))}
75
137
  </div>
76
- <div className="text-center mt-12">
77
- <GradientButton
78
- title={t('button')}
79
- href="https://preview.reve.art/"
80
- align="center"
81
- />
82
- </div>
138
+ {button && (
139
+ <div className="text-center mt-12">
140
+ {button}
141
+ </div>
142
+ )}
83
143
  </section>
84
144
  )
85
145
  }
86
-