patx/voldservices

improve uplaod form

Commit cccb43b · patx · 2026-08-16T17:27:24-04:00

Changeset
cccb43bd56318159ed03126598c68a9bb593b040
Parents
865a93fce5b038c1db430601977b7656932acf09

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/docs/CNAME b/docs/CNAME
deleted file mode 100644
index 397a2cb..0000000
--- a/docs/CNAME
+++ /dev/null
@@ -1 +0,0 @@
-voldservices.com
\ No newline at end of file
diff --git a/docs/index.html b/docs/index.html
index fc92245..f921509 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -7,7 +7,7 @@
   <title>Pressure Washing Broward County | Paver Sealing &amp; Roof Cleaning | Vold Services</title>
   <meta name="description" content="Pressure washing, paver sealing and repair, and soft wash roof cleaning across Broward, Miami-Dade, and Palm Beach counties. Call or text (954) 401-3301." />
   <meta name="robots" content="index, follow, max-image-preview:large" />
-  <link rel="canonical" href="https://voldservices.com/" />
+  <link rel="canonical" href="https://patx.gitman.io/voldservices/" />
 
   <meta name="geo.region" content="US-FL" />
   <meta name="geo.placename" content="Fort Lauderdale, Florida" />
@@ -18,9 +18,9 @@
   <meta property="og:site_name" content="Vold Services" />
   <meta property="og:title" content="Pressure Washing, Paver Sealing &amp; Roof Cleaning | Vold Services" />
   <meta property="og:description" content="Pressure washing, paver sealing and repair, and roof cleaning for homes and commercial properties across South Florida. Call or text (954) 401-3301." />
-  <meta property="og:url" content="https://voldservices.com/" />
-  <meta property="og:image" content="https://gitman.io/patx/demo/raw/docs/voldservices/og-card-v2.jpg" />
-  <meta property="og:image:secure_url" content="https://gitman.io/patx/demo/raw/docs/voldservices/og-card-v2.jpg" />
+  <meta property="og:url" content="https://patx.gitman.io/voldservices/" />
+  <meta property="og:image" content="https://patx.gitman.io/voldservices/og-card-v2.jpg" />
+  <meta property="og:image:secure_url" content="https://patx.gitman.io/voldservices/og-card-v2.jpg" />
   <meta property="og:image:type" content="image/jpeg" />
   <meta property="og:image:width" content="1200" />
   <meta property="og:image:height" content="630" />
@@ -29,17 +29,17 @@
   <meta name="twitter:card" content="summary_large_image" />
   <meta name="twitter:title" content="Pressure Washing, Paver Sealing &amp; Roof Cleaning | Vold Services" />
   <meta name="twitter:description" content="Pressure washing, paver sealing and repair, and roof cleaning across South Florida. Call or text (954) 401-3301." />
-  <meta name="twitter:image" content="https://gitman.io/patx/demo/raw/docs/voldservices/og-card-v2.jpg" />
+  <meta name="twitter:image" content="https://patx.gitman.io/voldservices/og-card-v2.jpg" />
   <meta name="twitter:image:alt" content="Vold Services — pressure washing, paver sealing, and roof cleaning in South Florida" />
 
   <meta name="theme-color" content="#080808" />
-  <link rel="icon" href="https://gitman.io/patx/demo/raw/docs/voldservices/favicon.png" type="image/png" />
-  <link rel="apple-touch-icon" href="https://gitman.io/patx/demo/raw/docs/voldservices/favicon.png" />
+  <link rel="icon" href="https://patx.gitman.io/voldservices/favicon.png" type="image/png" />
+  <link rel="apple-touch-icon" href="https://patx.gitman.io/voldservices/favicon.png" />
 
   <link rel="preconnect" href="https://fonts.googleapis.com" />
   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
   <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600&family=Roboto+Mono:wght@400;500&family=Teko:wght@500;600&display=swap" rel="stylesheet" />
-  <link rel="preload" as="image" href="https://gitman.io/patx/demo/raw/docs/voldservices/img/hero-poster-v2.jpg" />
+  <link rel="preload" as="image" href="https://patx.gitman.io/voldservices/img/hero-poster-v2.jpg" />
 
   <style>
     /* True charcoal and concrete neutrals, with the logo blue reserved
@@ -1015,15 +1015,41 @@
     .field select option { background: var(--ink-2); color: var(--bone); }
     .field textarea { min-height: 122px; resize: vertical; }
 
-    .field input[type="file"] {
-      padding: 7px;
+    .field input.photo-input {
+      position: absolute;
+      width: 1px;
+      height: 1px;
+      padding: 0;
+      margin: -1px;
+      overflow: hidden;
+      clip: rect(0, 0, 0, 0);
+      white-space: nowrap;
+      border: 0;
+    }
+    .field .photo-picker {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 14px;
+      min-height: 54px;
+      margin: 0;
+      padding: 8px 10px 8px 8px;
+      border: 1px solid var(--line);
+      border-radius: 3px;
+      background: rgba(8, 8, 8, .64);
       color: var(--muted);
+      font-family: inherit;
+      font-size: .78rem;
+      letter-spacing: 0;
+      text-transform: none;
       cursor: pointer;
+      transition: border-color .18s ease, background .18s ease;
     }
-    .field input[type="file"]::file-selector-button {
-      min-height: 40px;
-      margin-right: 13px;
-      padding: 0 16px;
+    .photo-picker__action {
+      display: inline-grid;
+      place-items: center;
+      min-height: 38px;
+      padding: 0 15px;
       border: 1px solid var(--line);
       border-radius: 2px;
       background: rgba(7, 142, 209, .14);
@@ -1032,14 +1058,24 @@
       font-size: .72rem;
       letter-spacing: .08em;
       text-transform: uppercase;
-      cursor: pointer;
       transition: border-color .18s ease, background .18s ease, color .18s ease;
     }
-    .field input[type="file"]:hover::file-selector-button {
+    .photo-picker__note { text-align: right; }
+    .field .photo-picker:hover,
+    .field .photo-input:focus-visible + .photo-picker {
+      border-color: var(--lift);
+      background: rgba(7, 142, 209, .08);
+    }
+    .field .photo-picker:hover .photo-picker__action,
+    .field .photo-input:focus-visible + .photo-picker .photo-picker__action {
       border-color: var(--lift);
       background: var(--brand);
       color: #fff;
     }
+    .field .photo-input:focus-visible + .photo-picker {
+      outline: 2px solid var(--lift);
+      outline-offset: 2px;
+    }
     .field-help, .photo-status {
       margin: 7px 0 0;
       font-family: var(--mono);
@@ -1048,6 +1084,59 @@
       color: var(--muted-dim);
     }
     .photo-status:not(:empty) { color: var(--muted); }
+    .photo-list {
+      display: grid;
+      gap: 6px;
+      margin: 9px 0 0;
+      padding: 0;
+      list-style: none;
+    }
+    .photo-list[hidden] { display: none; }
+    .photo-item {
+      display: grid;
+      grid-template-columns: minmax(0, 1fr) auto;
+      align-items: center;
+      gap: 12px;
+      padding: 9px 10px 9px 12px;
+      border-left: 2px solid var(--brand);
+      border-radius: 2px;
+      background: rgba(255, 255, 255, .035);
+    }
+    .photo-file { min-width: 0; }
+    .photo-name {
+      display: block;
+      overflow: hidden;
+      color: var(--bone);
+      font-size: .82rem;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    .photo-size {
+      display: block;
+      margin-top: 2px;
+      color: var(--muted-dim);
+      font-family: var(--mono);
+      font-size: .65rem;
+      letter-spacing: .06em;
+      text-transform: uppercase;
+    }
+    .photo-remove {
+      padding: 6px 8px;
+      border: 1px solid transparent;
+      border-radius: 2px;
+      background: transparent;
+      color: var(--muted);
+      font-family: var(--mono);
+      font-size: .66rem;
+      letter-spacing: .07em;
+      text-transform: uppercase;
+      cursor: pointer;
+    }
+    .photo-remove:hover, .photo-remove:focus-visible {
+      border-color: rgba(255, 132, 111, .55);
+      color: #ffd8d1;
+      outline: none;
+    }
 
     .form-note {
       font-family: var(--mono);
@@ -1320,7 +1409,7 @@
   <header class="nav" id="nav">
     <div class="wrap nav-inner">
       <a class="brand" href="#top" aria-label="Vold Services home">
-        <img src="https://gitman.io/patx/demo/raw/docs/voldservices/logo.png" alt="Vold Services" width="188" height="97" />
+        <img src="https://patx.gitman.io/voldservices/logo.png" alt="Vold Services" width="188" height="97" />
       </a>
 
       <div class="nav-actions">
@@ -1350,16 +1439,16 @@
     <section class="hero" id="top">
       <div class="hero-sticky" id="hero-sticky">
         <div class="hero-media">
-          <img class="hero-fallback" src="https://gitman.io/patx/demo/raw/docs/voldservices/img/hero-scrub-poster.jpg" alt="" aria-hidden="true" />
+          <img class="hero-fallback" src="https://patx.gitman.io/voldservices/img/hero-scrub-poster.jpg" alt="" aria-hidden="true" />
           <video
             id="hero-video"
-            poster="https://gitman.io/patx/demo/raw/docs/voldservices/img/hero-scrub-poster.jpg"
+            poster="https://patx.gitman.io/voldservices/img/hero-scrub-poster.jpg"
             muted playsinline webkit-playsinline preload="auto"
             controlslist="nodownload nofullscreen noremoteplayback"
             disablepictureinpicture disableremoteplayback
             aria-hidden="true" tabindex="-1">
-            <source media="(max-width: 720px)" src="https://gitman.io/patx/demo/raw/docs/voldservices/hero-scrub-mobile.mp4" type="video/mp4" />
-            <source src="https://gitman.io/patx/demo/raw/docs/voldservices/hero-scrub.mp4" type="video/mp4" />
+            <source media="(max-width: 720px)" src="https://patx.gitman.io/voldservices/hero-scrub-mobile.mp4" type="video/mp4" />
+            <source src="https://patx.gitman.io/voldservices/hero-scrub.mp4" type="video/mp4" />
           </video>
         </div>
         <div class="hero-grain" aria-hidden="true"></div>
@@ -1518,12 +1607,12 @@
           <article class="wand-project reveal">
             <div class="wand">
               <div class="wand-layer">
-                <img src="https://gitman.io/patx/demo/raw/docs/voldservices/img/before-driveway.jpg"
+                <img src="https://patx.gitman.io/voldservices/img/before-driveway.jpg"
                      alt="Travertine paver driveway dulled by dirt and organic growth before cleaning"
                      loading="lazy" decoding="async" width="746" height="829" />
               </div>
               <div class="wand-layer wand-layer--after">
-                <img src="https://gitman.io/patx/demo/raw/docs/voldservices/img/after-driveway.jpg"
+                <img src="https://patx.gitman.io/voldservices/img/after-driveway.jpg"
                      alt="The same driveway after pressure washing, re-sanding, and sealing"
                      loading="lazy" decoding="async" width="746" height="829" />
               </div>
@@ -1546,12 +1635,12 @@
           <article class="wand-project reveal">
             <div class="wand">
               <div class="wand-layer">
-                <img src="https://gitman.io/patx/demo/raw/docs/voldservices/img/before-pool-deck.jpg"
+                <img src="https://patx.gitman.io/voldservices/img/before-pool-deck.jpg"
                      alt="Gold travertine pool deck greyed by weathering and organic staining before cleaning"
                      loading="lazy" decoding="async" width="746" height="829" />
               </div>
               <div class="wand-layer wand-layer--after">
-                <img src="https://gitman.io/patx/demo/raw/docs/voldservices/img/after-pool-deck.jpg"
+                <img src="https://patx.gitman.io/voldservices/img/after-pool-deck.jpg"
                      alt="The same pool deck after cleaning and sealing, with the stone color brought back"
                      loading="lazy" decoding="async" width="746" height="829" />
               </div>
@@ -1593,27 +1682,27 @@
 
         <div class="work-grid reveal">
           <button class="work-tile" type="button">
-            <img src="https://gitman.io/patx/demo/raw/docs/voldservices/img/after-driveway.jpg" alt="Travertine paver driveway cleaned and sealed at a two-storey South Florida home" loading="lazy" decoding="async" width="746" height="829" />
+            <img src="https://patx.gitman.io/voldservices/img/after-driveway.jpg" alt="Travertine paver driveway cleaned and sealed at a two-storey South Florida home" loading="lazy" decoding="async" width="746" height="829" />
             <span class="work-cap"><b>Driveway wash &amp; seal</b><span>Paver sealing</span></span>
           </button>
           <button class="work-tile" type="button">
-            <img src="https://gitman.io/patx/demo/raw/docs/voldservices/img/after-pool-deck.jpg" alt="Gold travertine pool deck sealed to a wet look beside a screened pool" loading="lazy" decoding="async" width="746" height="829" />
+            <img src="https://patx.gitman.io/voldservices/img/after-pool-deck.jpg" alt="Gold travertine pool deck sealed to a wet look beside a screened pool" loading="lazy" decoding="async" width="746" height="829" />
             <span class="work-cap"><b>Pool deck seal</b><span>Paver sealing</span></span>
           </button>
           <button class="work-tile" type="button">
-            <img src="https://gitman.io/patx/demo/raw/docs/voldservices/img/after-lanai.jpg" alt="Screened lanai with travertine pavers cleaned back to a warm tone" loading="lazy" decoding="async" width="746" height="829" />
+            <img src="https://patx.gitman.io/voldservices/img/after-lanai.jpg" alt="Screened lanai with travertine pavers cleaned back to a warm tone" loading="lazy" decoding="async" width="746" height="829" />
             <span class="work-cap"><b>Screened lanai</b><span>Pressure washing</span></span>
           </button>
           <button class="work-tile" type="button">
-            <img src="https://gitman.io/patx/demo/raw/docs/voldservices/img/after-patio.jpg" alt="Front paver patio and walkway cleaned, weeded, and re-sanded" loading="lazy" decoding="async" width="746" height="829" />
+            <img src="https://patx.gitman.io/voldservices/img/after-patio.jpg" alt="Front paver patio and walkway cleaned, weeded, and re-sanded" loading="lazy" decoding="async" width="746" height="829" />
             <span class="work-cap"><b>Patio &amp; walkway</b><span>Pressure washing</span></span>
           </button>
           <button class="work-tile" type="button">
-            <img src="https://gitman.io/patx/demo/raw/docs/voldservices/img/work-driveway-travertine.jpg" alt="Finished travertine paver driveway taped off after sealing" loading="lazy" decoding="async" width="746" height="829" />
+            <img src="https://patx.gitman.io/voldservices/img/work-driveway-travertine.jpg" alt="Finished travertine paver driveway taped off after sealing" loading="lazy" decoding="async" width="746" height="829" />
             <span class="work-cap"><b>Travertine driveway</b><span>Sealed &amp; taped off</span></span>
           </button>
           <button class="work-tile" type="button">
-            <img src="https://gitman.io/patx/demo/raw/docs/voldservices/img/work-driveway-install.jpg" alt="Paver driveway restoration in progress, with new pavers being laid over the old concrete" loading="lazy" decoding="async" width="746" height="829" />
+            <img src="https://patx.gitman.io/voldservices/img/work-driveway-install.jpg" alt="Paver driveway restoration in progress, with new pavers being laid over the old concrete" loading="lazy" decoding="async" width="746" height="829" />
             <span class="work-cap"><b>Driveway restoration</b><span>In progress</span></span>
           </button>
         </div>
@@ -1804,9 +1893,14 @@
               </div>
               <div class="field">
                 <label for="q-photos">Show us the surface <span aria-hidden="true">·</span> Optional</label>
-                <input id="q-photos" name="photos" type="file" accept="image/*" multiple
+                <input class="photo-input" id="q-photos" name="photos" type="file" accept="image/*" multiple
                        aria-describedby="q-photos-help q-photos-status" />
-                <p class="field-help" id="q-photos-help">Add up to 5 photos. Maximum 25 MB each and 90 MB total.</p>
+                <label class="photo-picker" for="q-photos">
+                  <span class="photo-picker__action">Add photos</span>
+                  <span class="photo-picker__note">One at a time or several together</span>
+                </label>
+                <p class="field-help" id="q-photos-help">Add up to 5 photos. You can open the picker again to add more. Maximum 25 MB each and 90 MB total.</p>
+                <ul class="photo-list" id="q-photo-list" aria-label="Selected photos" hidden></ul>
                 <p class="photo-status" id="q-photos-status" aria-live="polite"></p>
               </div>
               <div class="form-error" id="form-error" role="alert" hidden></div>
@@ -1837,7 +1931,7 @@
     <div class="wrap">
       <div class="footer-inner">
         <div>
-          <img src="https://gitman.io/patx/demo/raw/docs/voldservices/logo.png" alt="Vold Services" width="168" height="87" />
+          <img src="https://patx.gitman.io/voldservices/logo.png" alt="Vold Services" width="168" height="87" />
           <p>
             Exterior cleaning for homes, HOAs, and commercial properties across Broward,
             Miami-Dade, and Palm Beach counties.
@@ -1912,13 +2006,13 @@
     "@graph": [
       {
         "@type": ["LocalBusiness", "HomeAndConstructionBusiness"],
-        "@id": "https://voldservices.com/#business",
+        "@id": "https://patx.gitman.io/voldservices/#business",
         "name": "Vold Services",
         "description": "Pressure washing, paver sealing and repair, and roof cleaning for residential and commercial properties in Broward, Miami-Dade, and Palm Beach counties, Florida.",
-        "url": "https://voldservices.com/",
+        "url": "https://patx.gitman.io/voldservices/",
         "telephone": "+1-954-401-3301",
-        "image": "https://gitman.io/patx/demo/raw/docs/voldservices/og-card-v2.jpg",
-        "logo": "https://gitman.io/patx/demo/raw/docs/voldservices/logo.png",
+        "image": "https://patx.gitman.io/voldservices/og-card-v2.jpg",
+        "logo": "https://patx.gitman.io/voldservices/logo.png",
         "sameAs": [
           "https://www.instagram.com/voldservices/",
           "https://www.facebook.com/p/VOLD-Services-61553817020348/",
@@ -2009,10 +2103,10 @@
       },
       {
         "@type": "WebSite",
-        "@id": "https://voldservices.com/#website",
-        "url": "https://voldservices.com/",
+        "@id": "https://patx.gitman.io/voldservices/#website",
+        "url": "https://patx.gitman.io/voldservices/",
         "name": "Vold Services",
-        "publisher": { "@id": "https://voldservices.com/#business" },
+        "publisher": { "@id": "https://patx.gitman.io/voldservices/#business" },
         "inLanguage": "en-US"
       }
     ]
@@ -2384,10 +2478,12 @@
       var formError = document.getElementById('form-error');
       var submitButton = document.getElementById('quote-submit');
       var photoInput = document.getElementById('q-photos');
+      var photoList = document.getElementById('q-photo-list');
       var photoStatus = document.getElementById('q-photos-status');
       var maxPhotos = 5;
       var maxPhotoBytes = 25 * 1024 * 1024;
       var maxTotalPhotoBytes = 90 * 1024 * 1024;
+      var selectedPhotos = [];
 
       function clearFormError() {
         formError.hidden = true;
@@ -2401,36 +2497,79 @@
 
       function formatPhotoStatus(files) {
         if (!files.length) return '';
-        var names = files.map(function (file) { return file.name; }).join(', ');
-        return files.length + (files.length === 1 ? ' photo selected: ' : ' photos selected: ') + names;
+        return files.length + ' of ' + maxPhotos + (files.length === 1 ? ' photo is' : ' photos are') + ' ready to send.';
       }
 
-      function validatePhotos() {
-        var files = Array.prototype.slice.call(photoInput.files || []);
+      function photoValidationMessage(files) {
         var totalBytes = files.reduce(function (total, file) { return total + file.size; }, 0);
         var oversizedPhoto = files.find(function (file) { return file.size > maxPhotoBytes; });
         var nonPhoto = files.find(function (file) {
           return file.type && file.type.indexOf('image/') !== 0;
         });
 
-        photoInput.setCustomValidity('');
-
         if (files.length > maxPhotos) {
-          photoInput.setCustomValidity('Choose no more than 5 photos.');
-        } else if (oversizedPhoto) {
-          photoInput.setCustomValidity(oversizedPhoto.name + ' is larger than 25 MB.');
-        } else if (totalBytes > maxTotalPhotoBytes) {
-          photoInput.setCustomValidity('Keep the combined photo size under 90 MB.');
-        } else if (nonPhoto) {
-          photoInput.setCustomValidity('Choose image files only.');
+          return 'Choose no more than 5 photos. Remove one before adding another.';
+        }
+        if (oversizedPhoto) {
+          return oversizedPhoto.name + ' is larger than 25 MB and was not added.';
         }
+        if (totalBytes > maxTotalPhotoBytes) {
+          return 'Keep the combined photo size under 90 MB. Those photos were not added.';
+        }
+        if (nonPhoto) return 'Choose image files only.';
+        return '';
+      }
+
+      function formatPhotoSize(bytes) {
+        if (bytes < 1024 * 1024) return Math.max(1, Math.round(bytes / 1024)) + ' KB';
+        return (bytes / (1024 * 1024)).toFixed(1).replace('.0', '') + ' MB';
+      }
+
+      function photoKey(file) {
+        return [file.name, file.size, file.lastModified, file.type].join('|');
+      }
+
+      function renderPhotos(message) {
+        photoList.textContent = '';
+        selectedPhotos.forEach(function (file, index) {
+          var item = document.createElement('li');
+          var fileDetails = document.createElement('span');
+          var fileName = document.createElement('span');
+          var fileSize = document.createElement('span');
+          var removeButton = document.createElement('button');
+
+          item.className = 'photo-item';
+          fileDetails.className = 'photo-file';
+          fileName.className = 'photo-name';
+          fileName.textContent = file.name;
+          fileSize.className = 'photo-size';
+          fileSize.textContent = 'Photo ' + (index + 1) + ' · ' + formatPhotoSize(file.size);
+          removeButton.className = 'photo-remove';
+          removeButton.type = 'button';
+          removeButton.dataset.photoIndex = index;
+          removeButton.setAttribute('aria-label', 'Remove ' + file.name);
+          removeButton.textContent = 'Remove';
+
+          fileDetails.appendChild(fileName);
+          fileDetails.appendChild(fileSize);
+          item.appendChild(fileDetails);
+          item.appendChild(removeButton);
+          photoList.appendChild(item);
+        });
 
-        if (!photoInput.checkValidity()) {
-          photoStatus.textContent = photoInput.validationMessage;
+        photoList.hidden = selectedPhotos.length === 0;
+        photoStatus.textContent = message || formatPhotoStatus(selectedPhotos);
+      }
+
+      function validatePhotos() {
+        var message = photoValidationMessage(selectedPhotos);
+        photoInput.setCustomValidity(message);
+        if (message) {
+          photoStatus.textContent = message;
           return false;
         }
 
-        photoStatus.textContent = formatPhotoStatus(files);
+        photoStatus.textContent = formatPhotoStatus(selectedPhotos);
         return true;
       }
 
@@ -2458,7 +2597,47 @@
 
       photoInput.addEventListener('change', function () {
         clearFormError();
-        validatePhotos();
+        var incomingPhotos = Array.prototype.slice.call(photoInput.files || []);
+        var selectedKeys = selectedPhotos.map(photoKey);
+        var duplicateCount = 0;
+
+        photoInput.value = '';
+        incomingPhotos = incomingPhotos.filter(function (file) {
+          var key = photoKey(file);
+          if (selectedKeys.indexOf(key) !== -1) {
+            duplicateCount += 1;
+            return false;
+          }
+          selectedKeys.push(key);
+          return true;
+        });
+
+        var candidatePhotos = selectedPhotos.concat(incomingPhotos);
+        var message = photoValidationMessage(candidatePhotos);
+        photoInput.setCustomValidity('');
+
+        if (message) {
+          renderPhotos(message);
+          return;
+        }
+
+        selectedPhotos = candidatePhotos;
+        renderPhotos(duplicateCount
+          ? (duplicateCount === 1 ? 'That photo is already selected. ' : 'Those photos are already selected. ') + formatPhotoStatus(selectedPhotos)
+          : '');
+      });
+
+      photoList.addEventListener('click', function (e) {
+        var removeButton = e.target.closest('.photo-remove');
+        if (!removeButton) return;
+
+        var index = Number(removeButton.dataset.photoIndex);
+        if (!Number.isInteger(index) || !selectedPhotos[index]) return;
+
+        selectedPhotos.splice(index, 1);
+        photoInput.setCustomValidity('');
+        clearFormError();
+        renderPhotos();
       });
 
       form.addEventListener('submit', async function (e) {
@@ -2477,9 +2656,15 @@
         submitButton.textContent = 'Sending request…';
 
         try {
+          var formData = new FormData(form);
+          formData.delete(photoInput.name);
+          selectedPhotos.forEach(function (file) {
+            formData.append(photoInput.name, file, file.name);
+          });
+
           var response = await fetch(form.action, {
             method: form.method,
-            body: new FormData(form),
+            body: formData,
             headers: { 'Accept': 'application/json' }
           });
           var data = await response.json().catch(function () { return null; });
@@ -2490,7 +2675,8 @@
 
           successName.textContent = submittedName || 'we’ve got it';
           form.reset();
-          photoStatus.textContent = '';
+          selectedPhotos = [];
+          renderPhotos();
           form.hidden = true;
           success.hidden = false;
           success.focus();