Skip to content

Commit ac7da06

Browse files
Allow testimonial photo upload or paste (#94)
- replace the photo URL input with a file picker and paste zone for avatar images - add client-side handling for uploaded or pasted images and update preview rendering - provide placeholder handling and status messaging for image selection methods (not provided) ------ [Codex Task](https://chatgpt.com/codex/tasks/task_e_69453152dd5c8325a1c7d123bdf7d1e9)
1 parent c191d8b commit ac7da06

1 file changed

Lines changed: 69 additions & 6 deletions

File tree

testimonial-card-generator.html

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,22 @@
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
'&': '&amp;',
274301
'<': '&lt;',
@@ -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

Comments
 (0)