Created
March 11, 2026 17:22
-
-
Save andrast0th/e1584b1de59613850464bbcca4c7ecde to your computer and use it in GitHub Desktop.
Photo upload using phone
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Camera Upload</title> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: system-ui, sans-serif; | |
| background: #111; | |
| color: #eee; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 2rem 1rem; | |
| gap: 1.5rem; | |
| } | |
| h1 { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| letter-spacing: 0.02em; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 1rem; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| button { | |
| padding: 0.6rem 1.4rem; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: opacity 0.15s; | |
| } | |
| button:hover { | |
| opacity: 0.85; | |
| } | |
| button:disabled { | |
| opacity: 0.35; | |
| cursor: default; | |
| } | |
| #btnCapture { | |
| background: #2563eb; | |
| color: #fff; | |
| } | |
| #btnFile { | |
| background: #059669; | |
| color: #fff; | |
| } | |
| #btnClear { | |
| background: #dc2626; | |
| color: #fff; | |
| } | |
| /* hidden file input wired to camera */ | |
| #fileInput { | |
| display: none; | |
| } | |
| .gallery { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 1rem; | |
| width: 100%; | |
| max-width: 900px; | |
| } | |
| .photo-card { | |
| position: relative; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| background: #222; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .photo-card .img-wrap { | |
| position: relative; | |
| aspect-ratio: 1 / 1; | |
| overflow: hidden; | |
| } | |
| .photo-card img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| display: block; | |
| } | |
| .photo-card .mime-label { | |
| font-size: 0.72rem; | |
| color: #aaa; | |
| background: #1a1a1a; | |
| padding: 0.35rem 0.6rem; | |
| letter-spacing: 0.03em; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .photo-card .img-wrap .remove-btn { | |
| position: absolute; | |
| top: 6px; | |
| right: 6px; | |
| background: rgba(0, 0, 0, 0.6); | |
| color: #fff; | |
| border: none; | |
| border-radius: 50%; | |
| width: 28px; | |
| height: 28px; | |
| font-size: 1rem; | |
| line-height: 1; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 0; | |
| } | |
| .photo-card .remove-btn:hover { | |
| background: rgba(220, 38, 38, 0.85); | |
| } | |
| .empty { | |
| color: #555; | |
| font-size: 0.95rem; | |
| text-align: center; | |
| } | |
| #count { | |
| font-size: 0.85rem; | |
| color: #888; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Camera Upload</h1> | |
| <div class="controls"> | |
| <!-- capture="environment" prefers the rear camera on mobile --> | |
| <input | |
| id="fileInput" | |
| type="file" | |
| accept="image/*" | |
| capture="environment" | |
| multiple | |
| /> | |
| <button id="btnCapture">Take Photo</button> | |
| <button id="btnFile">Choose from Library</button> | |
| <button id="btnClear" disabled>Clear All</button> | |
| </div> | |
| <span id="count">0 photos in memory</span> | |
| <div class="gallery" id="gallery"> | |
| <p class="empty" id="empty">No photos yet.</p> | |
| </div> | |
| <script> | |
| const fileInput = document.getElementById("fileInput"); | |
| const btnCapture = document.getElementById("btnCapture"); | |
| const btnFile = document.getElementById("btnFile"); | |
| const btnClear = document.getElementById("btnClear"); | |
| const gallery = document.getElementById("gallery"); | |
| const countEl = document.getElementById("count"); | |
| const emptyEl = document.getElementById("empty"); | |
| // Track object URLs so we can revoke them later | |
| const photos = []; // { objectUrl, file } | |
| // "Take Photo" → open file picker with camera capture | |
| btnCapture.addEventListener("click", () => { | |
| fileInput.setAttribute("capture", "environment"); | |
| fileInput.removeAttribute("multiple"); | |
| fileInput.click(); | |
| }); | |
| // "Choose from Library" → open file picker without capture | |
| btnFile.addEventListener("click", () => { | |
| fileInput.removeAttribute("capture"); | |
| fileInput.setAttribute("multiple", ""); | |
| fileInput.click(); | |
| }); | |
| fileInput.addEventListener("change", () => { | |
| const files = Array.from(fileInput.files); | |
| if (!files.length) return; | |
| files.forEach(addPhoto); | |
| // Reset so the same file can be chosen again | |
| fileInput.value = ""; | |
| }); | |
| function addPhoto(file) { | |
| const objectUrl = URL.createObjectURL(file); | |
| const entry = { objectUrl, file }; | |
| photos.push(entry); | |
| // Build card | |
| const card = document.createElement("div"); | |
| card.className = "photo-card"; | |
| const img = document.createElement("img"); | |
| img.src = objectUrl; | |
| img.alt = file.name; | |
| const removeBtn = document.createElement("button"); | |
| removeBtn.className = "remove-btn"; | |
| removeBtn.title = "Remove"; | |
| removeBtn.textContent = "×"; | |
| removeBtn.addEventListener("click", () => removePhoto(entry, card)); | |
| const imgWrap = document.createElement("div"); | |
| imgWrap.className = "img-wrap"; | |
| imgWrap.appendChild(img); | |
| imgWrap.appendChild(removeBtn); | |
| const mimeLabel = document.createElement("div"); | |
| mimeLabel.className = "mime-label"; | |
| mimeLabel.textContent = file.type || "unknown"; | |
| card.appendChild(imgWrap); | |
| card.appendChild(mimeLabel); | |
| gallery.appendChild(card); | |
| updateUI(); | |
| } | |
| function removePhoto(entry, card) { | |
| URL.revokeObjectURL(entry.objectUrl); | |
| const idx = photos.indexOf(entry); | |
| if (idx !== -1) photos.splice(idx, 1); | |
| card.remove(); | |
| updateUI(); | |
| } | |
| btnClear.addEventListener("click", () => { | |
| photos.forEach((e) => URL.revokeObjectURL(e.objectUrl)); | |
| photos.length = 0; | |
| // Remove all cards except the empty message | |
| gallery.querySelectorAll(".photo-card").forEach((c) => c.remove()); | |
| updateUI(); | |
| }); | |
| function updateUI() { | |
| const n = photos.length; | |
| countEl.textContent = `${n} photo${n !== 1 ? "s" : ""} in memory`; | |
| emptyEl.style.display = n === 0 ? "" : "none"; | |
| btnClear.disabled = n === 0; | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment