@@ -2,10 +2,10 @@ import {memo, useCallback, useEffect, useRef, useState} from 'react';
2
2
3
3
import APAvatar from '@src/components/global/APAvatar' ;
4
4
import DotsPattern from './DotsPattern' ;
5
+ import html2canvas from 'html2canvas-objectfit-fix' ;
5
6
import { Account } from '@src/api/activitypub' ;
6
7
import { Button , H2 , LoadingIndicator , LucideIcon , Skeleton , ToggleGroup , ToggleGroupItem , Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from '@tryghost/shade' ;
7
8
import { imageUrlToDataUrl } from '@src/utils/image' ;
8
- import { takeScreenshot } from '@src/utils/screenshot' ;
9
9
import { toast } from 'sonner' ;
10
10
import { useBrowseSite } from '@tryghost/admin-x-framework/api/site' ;
11
11
import { useFeatureFlags } from '@src/lib/feature-flags' ;
@@ -80,8 +80,8 @@ const ProfileCard: React.FC<ProfileCardProps> = memo(({
80
80
const margin = isScreenshot ? 'm-4' : 'm-16' ;
81
81
const borderClass = isScreenshot ? backgroundColor === 'light' ? 'border border-gray-200' : '' : 'shadow-xl' ;
82
82
83
- const cardWidth = format === 'square' ? 'w-[392px ]' : 'w-[316px]' ;
84
- const cardHeight = 'h-[392px ]' ;
83
+ const cardWidth = format === 'square' ? 'w-[422px ]' : 'w-[316px]' ;
84
+ const cardHeight = 'h-[422px ]' ;
85
85
86
86
const bannerImageSrc = isScreenshot && bannerDataUrl ? bannerDataUrl : ( account ?. bannerImageUrl || coverImage ) ;
87
87
const avatarImageSrc = isScreenshot && avatarDataUrl ? avatarDataUrl : ( account ?. avatarUrl || publicationIcon ) ;
@@ -119,7 +119,7 @@ const ProfileCard: React.FC<ProfileCardProps> = memo(({
119
119
</ div >
120
120
< div className = { `flex grow flex-col items-center p-6 ${ ( account ?. avatarUrl || publicationIcon ) ? 'pt-9' : 'pt-3' } text-center ${ format === 'square' ? 'flex-1 justify-center' : '' } ` } >
121
121
< H2 className = { `${ isScreenshot && 'tracking-normal' } ` } style = { { color : textColor } } > { ! isLoading ? account ?. name : < Skeleton className = 'w-32' /> } </ H2 >
122
- < span className = { `mt-0 .5 text-lg ${ isScreenshot && 'tracking-normal' } ` } style = { { color : textColor } } > { ! isLoading ? 'Now on the Social Web! ' : < Skeleton className = 'w-28' /> } </ span >
122
+ < span className = { `mt-1 .5 leading-7 ${ isScreenshot && 'tracking-normal' } ` } style = { { color : textColor } } > { ! isLoading ? 'Available on Ghost, Flipboard, Threads, Bluesky, Mastodon, or wherever you get your social web feeds. ' : < Skeleton className = 'w-28' /> } </ span >
123
123
< div
124
124
className = { `mt-auto flex max-h-[60px] min-h-12 w-full items-center justify-center rounded-full border px-4 py-2 font-medium leading-7 ${ isScreenshot && 'tracking-normal' } ` }
125
125
style = { {
@@ -146,11 +146,10 @@ const Profile: React.FC<ProfileProps> = ({account, isLoading}) => {
146
146
const profileCardRef = useRef < HTMLDivElement > ( null ) ;
147
147
const [ backgroundColor , setBackgroundColor ] = useState < 'light' | 'dark' | 'accent' > ( 'light' ) ;
148
148
const [ cardFormat , setCardFormat ] = useState < 'vertical' | 'square' > ( 'vertical' ) ;
149
- const [ isAltKeyHeld , setIsAltKeyHeld ] = useState ( false ) ;
150
149
const [ isProcessing , setIsProcessing ] = useState ( false ) ;
151
150
const [ bannerDataUrl , setBannerDataUrl ] = useState < string | null > ( null ) ;
152
151
const [ avatarDataUrl , setAvatarDataUrl ] = useState < string | null > ( null ) ;
153
- const shareText = `You can now follow ${ account ?. name } on the social web, on ${ account ?. handle } ` ;
152
+ const shareText = `${ account ?. name } is now available across the social web, on ${ account ?. handle } ` ;
154
153
155
154
const convertImagesToDataUrls = useCallback ( async ( ) => {
156
155
if ( account ?. bannerImageUrl || coverImage ) {
@@ -186,35 +185,6 @@ const Profile: React.FC<ProfileProps> = ({account, isLoading}) => {
186
185
} ;
187
186
} , [ convertImagesToDataUrls ] ) ;
188
187
189
- // Listen for Alt key press/release
190
- useEffect ( ( ) => {
191
- const handleKeyDown = ( event : KeyboardEvent ) => {
192
- if ( event . altKey && ! isAltKeyHeld ) {
193
- setIsAltKeyHeld ( true ) ;
194
- }
195
- } ;
196
-
197
- const handleKeyUp = ( event : KeyboardEvent ) => {
198
- if ( ! event . altKey && isAltKeyHeld ) {
199
- setIsAltKeyHeld ( false ) ;
200
- }
201
- } ;
202
-
203
- const handleWindowBlur = ( ) => {
204
- setIsAltKeyHeld ( false ) ;
205
- } ;
206
-
207
- window . addEventListener ( 'keydown' , handleKeyDown ) ;
208
- window . addEventListener ( 'keyup' , handleKeyUp ) ;
209
- window . addEventListener ( 'blur' , handleWindowBlur ) ;
210
-
211
- return ( ) => {
212
- window . removeEventListener ( 'keydown' , handleKeyDown ) ;
213
- window . removeEventListener ( 'keyup' , handleKeyUp ) ;
214
- window . removeEventListener ( 'blur' , handleWindowBlur ) ;
215
- } ;
216
- } , [ isAltKeyHeld ] ) ;
217
-
218
188
const getGradient = ( ) => {
219
189
switch ( backgroundColor ) {
220
190
case 'light' :
@@ -241,38 +211,61 @@ const Profile: React.FC<ProfileProps> = ({account, isLoading}) => {
241
211
}
242
212
} ;
243
213
244
- const handleDownload = async ( event : React . MouseEvent ) => {
214
+ const handleCopy = async ( ) => {
245
215
if ( ! profileCardRef . current || isProcessing ) {
246
216
return ;
247
217
}
248
218
249
219
setIsProcessing ( true ) ;
250
220
251
- await new Promise < void > ( ( resolve ) => {
252
- setTimeout ( ( ) => resolve ( ) , 100 ) ;
221
+ // Wait for the next frame to ensure the loading indicator is painted
222
+ await new Promise ( ( resolve ) => {
223
+ requestAnimationFrame ( ( ) => {
224
+ requestAnimationFrame ( resolve ) ;
225
+ } ) ;
253
226
} ) ;
254
227
255
- const shouldCopyToClipboard = event . altKey ;
256
-
257
228
try {
258
- await takeScreenshot ( profileCardRef . current , {
259
- filename : `${ account ?. handle } .png` ,
260
- backgroundColor : 'transparent' ,
261
- copyToClipboard : shouldCopyToClipboard
262
- } ) ;
229
+ if ( ! navigator . clipboard || ! ( 'write' in navigator . clipboard ) || typeof ClipboardItem === 'undefined' ) {
230
+ throw new Error ( 'Clipboard API not supported in this browser' ) ;
231
+ }
263
232
264
- if ( shouldCopyToClipboard ) {
233
+ try {
234
+ const blobPromise = new Promise < Blob > ( async ( resolve , reject ) => {
235
+ try {
236
+ const canvas = await html2canvas ( profileCardRef . current ! , {
237
+ backgroundColor : 'transparent' ,
238
+ scale : 2 ,
239
+ logging : false ,
240
+ useCORS : true ,
241
+ allowTaint : true ,
242
+ imageTimeout : 0
243
+ } ) ;
244
+
245
+ canvas . toBlob ( ( blob ) => {
246
+ if ( blob ) {
247
+ resolve ( blob ) ;
248
+ } else {
249
+ reject ( new Error ( 'Failed to create blob' ) ) ;
250
+ }
251
+ } , 'image/png' ) ;
252
+ } catch ( error ) {
253
+ reject ( error ) ;
254
+ }
255
+ } ) ;
256
+
257
+ const clipboardItem = new ClipboardItem ( {
258
+ 'image/png' : blobPromise
259
+ } ) ;
260
+
261
+ await navigator . clipboard . write ( [ clipboardItem ] ) ;
265
262
toast . success ( 'Image copied to clipboard' ) ;
266
- } else {
267
- toast . success ( 'Image downloaded ') ;
263
+ } catch ( error ) {
264
+ toast . error ( 'Failed to copy image ') ;
268
265
}
266
+ setIsProcessing ( false ) ;
269
267
} catch ( error ) {
270
- if ( shouldCopyToClipboard ) {
271
- toast . error ( `Failed to copy image` ) ;
272
- } else {
273
- toast . error ( `Failed to download image` ) ;
274
- }
275
- } finally {
268
+ toast . error ( 'Failed to copy image' ) ;
276
269
setIsProcessing ( false ) ;
277
270
}
278
271
} ;
@@ -386,15 +379,10 @@ const Profile: React.FC<ProfileProps> = ({account, isLoading}) => {
386
379
< svg fill = "none" viewBox = "0 0 16 16" > < g clipPath = "url(#social-linkedin_svg__clip0_537_833)" > < path className = "social-linkedin_svg__linkedin" clipRule = "evenodd" d = "M1.778 16h12.444c.982 0 1.778-.796 1.778-1.778V1.778C16 .796 15.204 0 14.222 0H1.778C.796 0 0 .796 0 1.778v12.444C0 15.204.796 16 1.778 16z" fill = "#007ebb" fillRule = "evenodd" > </ path > < path clipRule = "evenodd" d = "M13.778 13.778h-2.374V9.734c0-1.109-.421-1.729-1.299-1.729-.955 0-1.453.645-1.453 1.729v4.044H6.363V6.074h2.289v1.038s.688-1.273 2.322-1.273c1.634 0 2.804.997 2.804 3.061v4.878zM3.634 5.065c-.78 0-1.411-.636-1.411-1.421s.631-1.422 1.41-1.422c.78 0 1.411.637 1.411 1.422 0 .785-.631 1.421-1.41 1.421zm-1.182 8.713h2.386V6.074H2.452v7.704z" fill = "#fff" fillRule = "evenodd" > </ path > </ g > < defs > < clipPath id = "social-linkedin_svg__clip0_537_833" > < path d = "M0 0h16v16H0z" fill = "#fff" > </ path > </ clipPath > </ defs > </ svg >
387
380
</ a >
388
381
</ div >
389
- < Tooltip >
390
- < TooltipTrigger >
391
- < Button className = { `min-w-[160px] dark:bg-black dark:text-white dark:hover:bg-black/90 ${ backgroundColor === 'dark' && 'bg-white text-black hover:bg-gray-50 dark:bg-white dark:text-black dark:hover:bg-gray-50/90' } ` } onClick = { handleDownload } >
392
- { isProcessing ? < LoadingIndicator color = { `${ backgroundColor === 'dark' ? 'dark' : 'light' } ` } size = 'sm' /> : isAltKeyHeld ? < LucideIcon . Copy /> : < LucideIcon . Download /> }
393
- { ! isProcessing && ( isAltKeyHeld ? 'Copy image' : 'Download image' ) }
394
- </ Button >
395
- </ TooltipTrigger >
396
- < TooltipContent > Hold Alt/Option to copy to clipboard</ TooltipContent >
397
- </ Tooltip >
382
+ < Button className = { `min-w-[160px] dark:bg-black dark:text-white dark:hover:bg-black/90 ${ backgroundColor === 'dark' && 'bg-white text-black hover:bg-gray-50 dark:bg-white dark:text-black dark:hover:bg-gray-50/90' } ` } onClick = { handleCopy } >
383
+ { isProcessing ? < LoadingIndicator color = { `${ backgroundColor === 'dark' ? 'dark' : 'light' } ` } size = 'sm' /> : < LucideIcon . Copy /> }
384
+ { ! isProcessing && 'Copy image' }
385
+ </ Button >
398
386
</ div >
399
387
{ ( account ?. bannerImageUrl || coverImage ) &&
400
388
< DotsPattern className = { `absolute left-1/2 top-1/2 h-[600px] w-[598px] -translate-x-1/2 -translate-y-1/2 ${ backgroundColor === 'dark' && 'z-10' } ` } style = { { color : getDotsPatternColor ( ) } } />
@@ -407,7 +395,7 @@ const Profile: React.FC<ProfileProps> = ({account, isLoading}) => {
407
395
ref = { profileCardRef }
408
396
className = 'fixed left-[-9999px] top-0 z-[-1] flex w-fit justify-center overflow-hidden rounded-2xl bg-gray-50'
409
397
style = { {
410
- width : cardFormat === 'square' ? '424px ' : '348px' ,
398
+ width : cardFormat === 'square' ? '454px ' : '348px' ,
411
399
fontFamily : 'system-ui'
412
400
} }
413
401
>
0 commit comments