178178 color : var (--tx-2 );
179179 }
180180
181+ .paste-zone {
182+ border : 1px dashed var (--ui-3 );
183+ border-radius : var (--radius-md );
184+ padding : 0.75rem ;
185+ background : var (--bg );
186+ display : grid;
187+ gap : 0.35rem ;
188+ align-items : center;
189+ cursor : pointer;
190+ }
191+
192+ .paste-zone : focus-visible {
193+ outline : 2px solid var (--link );
194+ outline-offset : 2px ;
195+ }
196+
181197 </ style >
182198 < script src ="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.13/html-to-image.min.js "
183199 integrity ="sha512-iZ2ORl595Wx6miw+GuadDet4WQbdSWS3JLMoNfY8cRGoEFy6oT3G9IbcrBeL6AfkgpA51ETt/faX6yLV+/gFJg== "
@@ -217,8 +233,15 @@ <h1>Testimonial Card Image Generator</h1>
217233 < input type ="text " id ="affiliation " placeholder ="Company, role, or project ">
218234 </ div >
219235 < div class ="field ">
220- < label for ="photo "> Photo URL</ label >
221- < input type ="url " id ="photo " placeholder ="https://example.com/photo.jpg ">
236+ < label for ="photoUpload "> Photo</ label >
237+ < p class ="helper "> Upload an image file or paste one from your clipboard (Ctrl/Cmd + V). Images
238+ stay on your device.</ p >
239+ < input type ="file " id ="photoUpload " accept ="image/* ">
240+ < div class ="paste-zone " id ="pasteZone " tabindex ="0 " role ="button "
241+ aria-label ="Click or focus and paste an image for the avatar ">
242+ < strong > Click to choose a file, then paste an image here.</ strong >
243+ < p class ="helper "> Pasting will replace the current photo preview.</ p >
244+ </ div >
222245 </ div >
223246 </ div >
224247
@@ -256,7 +279,8 @@ <h1>Testimonial Card Image Generator</h1>
256279 const testimonialInput = document . getElementById ( 'testimonial' ) ;
257280 const nameInput = document . getElementById ( 'name' ) ;
258281 const affiliationInput = document . getElementById ( 'affiliation' ) ;
259- const photoInput = document . getElementById ( 'photo' ) ;
282+ const photoUploadInput = document . getElementById ( 'photoUpload' ) ;
283+ const pasteZone = document . getElementById ( 'pasteZone' ) ;
260284 const quote = document . getElementById ( 'quote' ) ;
261285 const previewName = document . getElementById ( 'previewName' ) ;
262286 const previewAffiliation = document . getElementById ( 'previewAffiliation' ) ;
@@ -267,8 +291,11 @@ <h1>Testimonial Card Image Generator</h1>
267291 const highlightChips = document . getElementById ( 'highlightChips' ) ;
268292 const downloadButton = document . getElementById ( 'download' ) ;
269293 const renderSurface = document . getElementById ( 'renderSurface' ) ;
294+ const placeholderImage = 'https://placehold.co/128x128?text=Photo' ;
270295
271296 let highlightPhrases = [ ] ;
297+ let uploadedPhotoUrl = '' ;
298+ let objectUrl = '' ;
272299 const escapeHtml = ( str ) => str . replace ( / [ & < > " ' ] / g, ( match ) => ( {
273300 '&' : '&' ,
274301 '<' : '<' ,
@@ -295,8 +322,8 @@ <h1>Testimonial Card Image Generator</h1>
295322 quote . innerHTML = testimonialText ? applyHighlights ( testimonialText ) : 'Add your testimonial to see a preview.' ;
296323 previewName . textContent = nameInput . value . trim ( ) || 'Name' ;
297324 previewAffiliation . textContent = affiliationInput . value . trim ( ) || 'Affiliation' ;
298- const photo = photoInput . value . trim ( ) ;
299- avatar . src = photo || 'https://placehold.co/128x128?text=Photo' ;
325+ const photo = uploadedPhotoUrl ;
326+ avatar . src = photo || placeholderImage ;
300327 avatar . alt = photo ? `${ previewName . textContent } photo` : 'Placeholder avatar' ;
301328 status . textContent = '' ;
302329 }
@@ -379,10 +406,32 @@ <h1>Testimonial Card Image Generator</h1>
379406 }
380407 }
381408
409+ function setPhotoFromFile ( file , source ) {
410+ if ( ! file ) return ;
411+ if ( ! file . type . startsWith ( 'image/' ) ) {
412+ status . textContent = 'Please choose an image file.' ;
413+ return ;
414+ }
415+ if ( objectUrl ) {
416+ URL . revokeObjectURL ( objectUrl ) ;
417+ }
418+ objectUrl = URL . createObjectURL ( file ) ;
419+ uploadedPhotoUrl = objectUrl ;
420+ renderPreview ( ) ;
421+ status . textContent = source ;
422+ }
423+
424+ function handlePaste ( event ) {
425+ const files = event . clipboardData ?. files || [ ] ;
426+ const imageFile = Array . from ( files ) . find ( ( file ) => file . type . startsWith ( 'image/' ) ) ;
427+ if ( ! imageFile ) return ;
428+ event . preventDefault ( ) ;
429+ setPhotoFromFile ( imageFile , 'Photo added from pasted image.' ) ;
430+ }
431+
382432 testimonialInput . addEventListener ( 'input' , renderPreview ) ;
383433 nameInput . addEventListener ( 'input' , renderPreview ) ;
384434 affiliationInput . addEventListener ( 'input' , renderPreview ) ;
385- photoInput . addEventListener ( 'input' , renderPreview ) ;
386435 highlightButton . addEventListener ( 'click' , addHighlightFromSelection ) ;
387436 clearHighlightsButton . addEventListener ( 'click' , ( ) => {
388437 highlightPhrases = [ ] ;
@@ -391,6 +440,20 @@ <h1>Testimonial Card Image Generator</h1>
391440 status . textContent = 'Highlights cleared.' ;
392441 } ) ;
393442 downloadButton . addEventListener ( 'click' , downloadImage ) ;
443+ photoUploadInput . addEventListener ( 'change' , ( event ) => {
444+ const [ file ] = event . target . files ;
445+ if ( ! file ) return ;
446+ setPhotoFromFile ( file , 'Photo added from file upload.' ) ;
447+ } ) ;
448+ pasteZone . addEventListener ( 'click' , ( ) => photoUploadInput . click ( ) ) ;
449+ pasteZone . addEventListener ( 'keydown' , ( event ) => {
450+ if ( event . key === 'Enter' || event . key === ' ' ) {
451+ event . preventDefault ( ) ;
452+ photoUploadInput . click ( ) ;
453+ }
454+ } ) ;
455+ pasteZone . addEventListener ( 'paste' , handlePaste ) ;
456+ document . addEventListener ( 'paste' , handlePaste ) ;
394457
395458 renderPreview ( ) ;
396459 </ script >
0 commit comments