Spaces:
Sleeping
Sleeping
| {% extends 'base.html' %} | |
| {% load static %} | |
| {% block title %}Upload Image — Constellar{% endblock %} | |
| {% block extra_css %} | |
| <style> | |
| .dropzone { | |
| border: 1.5px dashed rgba(99,102,241,.35); | |
| border-radius: 14px; | |
| background: rgba(99,102,241,.03); | |
| transition: border-color .2s, background .2s; | |
| cursor: pointer; | |
| } | |
| .dropzone:hover, .dropzone.dragging { | |
| border-color: rgba(99,102,241,.7); | |
| background: rgba(99,102,241,.07); | |
| } | |
| .dropzone.has-file { cursor: default; } | |
| .upload-icon { | |
| width: 64px; height: 64px; | |
| border-radius: 16px; | |
| margin: 0 auto 1.25rem; | |
| display: flex; align-items: center; justify-content: center; | |
| background: rgba(99,102,241,.12); | |
| border: 1px solid rgba(99,102,241,.25); | |
| color: #a5b4fc; | |
| transition: transform .2s; | |
| } | |
| .dropzone:hover .upload-icon { transform: translateY(-3px); } | |
| .tip-card { | |
| display: flex; gap: 1rem; align-items: flex-start; | |
| padding: 1.25rem; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| } | |
| .tip-icon { | |
| width: 38px; height: 38px; flex-shrink: 0; | |
| border-radius: 9px; | |
| display: flex; align-items: center; justify-content: center; | |
| background: rgba(99,102,241,.12); | |
| border: 1px solid rgba(99,102,241,.22); | |
| color: #a5b4fc; | |
| } | |
| .remove-btn { | |
| position: absolute; top: -10px; right: -10px; | |
| width: 28px; height: 28px; | |
| border-radius: 50%; | |
| border: none; cursor: pointer; | |
| background: #ef4444; color: #fff; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: .875rem; line-height: 1; | |
| box-shadow: 0 2px 8px rgba(0,0,0,.4); | |
| transition: background .15s; | |
| } | |
| .remove-btn:hover { background: #dc2626; } | |
| .submit-btn { | |
| font-family: 'Inter', sans-serif; | |
| font-size: .9375rem; font-weight: 600; | |
| color: #fff; padding: .8rem 2.25rem; | |
| border-radius: 8px; border: none; cursor: pointer; | |
| background: var(--accent); | |
| transition: all .18s; | |
| display: inline-flex; align-items: center; gap: .5rem; | |
| } | |
| .submit-btn:hover:not(:disabled) { | |
| background: #818cf8; | |
| box-shadow: 0 0 24px rgba(99,102,241,.4); | |
| transform: translateY(-1px); | |
| } | |
| .submit-btn:disabled { opacity: .45; cursor: not-allowed; } | |
| .spin { | |
| width: 15px; height: 15px; border-radius: 50%; | |
| border: 2px solid rgba(255,255,255,.3); | |
| border-top-color: #fff; | |
| animation: spin .7s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <!-- ════ HEADER ════ --> | |
| <section class="section" style="padding-top:4rem; padding-bottom:2.5rem; text-align:center;"> | |
| <div style="max-width:560px; margin:0 auto;"> | |
| <div class="tag" style="margin-bottom:1.25rem;">Image Analysis</div> | |
| <h1 style="font-family:'Orbitron',monospace; font-size:clamp(1.6rem,3.5vw,2.5rem); font-weight:900; margin:0 0 1rem; letter-spacing:-.01em;"> | |
| Upload a Night Sky Image | |
| </h1> | |
| <p style="font-size:.9375rem; color:var(--muted); line-height:1.7; margin:0;"> | |
| Drop in a photo of the stars and our detection model will identify every constellation it can find. | |
| </p> | |
| </div> | |
| </section> | |
| <!-- ════ UPLOAD FORM ════ --> | |
| <section style="padding:0 1.5rem;"> | |
| <div style="max-width:640px; margin:0 auto;"> | |
| <form id="uploadForm" method="post" enctype="multipart/form-data" action="{% url 'process_upload' %}"> | |
| {% csrf_token %} | |
| <div id="dropZone" class="dropzone" style="padding:3rem 2rem; text-align:center; position:relative;"> | |
| <!-- default state --> | |
| <div id="uploadContent"> | |
| <div class="upload-icon"> | |
| <svg width="26" height="26" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"/> | |
| </svg> | |
| </div> | |
| <h3 style="font-size:1.0625rem; font-weight:600; margin:0 0 .5rem; color:#fff;"> | |
| Drop your image here, or click to browse | |
| </h3> | |
| <p style="font-size:.8125rem; color:var(--muted); margin:0;"> | |
| JPG, JPEG or PNG · up to 10 MB | |
| </p> | |
| </div> | |
| <!-- preview state --> | |
| <div id="previewArea" class="hidden"> | |
| <div style="position:relative; display:inline-block;"> | |
| <img id="previewImage" style="max-width:100%; max-height:320px; border-radius:10px; border:1px solid var(--border);" alt="Selected image preview" /> | |
| <button type="button" id="removeImage" class="remove-btn" aria-label="Remove image">✕</button> | |
| </div> | |
| <p id="fileName" style="font-size:.875rem; font-weight:500; color:#a5b4fc; margin:1rem 0 .125rem;"></p> | |
| <p id="fileSize" style="font-size:.75rem; color:var(--muted); margin:0;"></p> | |
| </div> | |
| <input type="file" id="fileInput" name="image" accept=".jpg,.jpeg,.png" class="hidden" required> | |
| </div> | |
| <div style="text-align:center; margin-top:1.75rem;"> | |
| <button type="submit" id="submitBtn" disabled class="submit-btn"> | |
| <span id="submitText">Select an image first</span> | |
| <span id="loadingSpinner" class="hidden"><span class="spin"></span></span> | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </section> | |
| <!-- ════ TIPS ════ --> | |
| <section class="section" style="padding-top:4.5rem; padding-bottom:2rem;"> | |
| <div style="max-width:820px; margin:0 auto;"> | |
| <div style="text-align:center; margin-bottom:2rem;"> | |
| <p class="section-label" style="justify-content:center;">Better Results</p> | |
| <h2 style="font-family:'Orbitron',monospace; font-size:clamp(1.2rem,2.5vw,1.6rem); font-weight:700; margin:0;"> | |
| Tips for Accurate Detection | |
| </h2> | |
| </div> | |
| <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:.875rem;"> | |
| <div class="tip-card"> | |
| <div class="tip-icon"> | |
| <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <h3 style="font-size:.875rem; font-weight:600; margin:0 0 .375rem;">Clear night sky</h3> | |
| <p style="font-size:.78125rem; color:var(--muted); line-height:1.6; margin:0;">Minimal cloud cover and low light pollution give the model the cleanest star patterns to work with.</p> | |
| </div> | |
| </div> | |
| <div class="tip-card"> | |
| <div class="tip-icon"> | |
| <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"/> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <h3 style="font-size:.875rem; font-weight:600; margin:0 0 .375rem;">High resolution</h3> | |
| <p style="font-size:.78125rem; color:var(--muted); line-height:1.6; margin:0;">Sharper images mean more stars visible to the detector — avoid heavy compression or blur.</p> | |
| </div> | |
| </div> | |
| <div class="tip-card"> | |
| <div class="tip-icon"> | |
| <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <h3 style="font-size:.875rem; font-weight:600; margin:0 0 .375rem;">Wide framing</h3> | |
| <p style="font-size:.78125rem; color:var(--muted); line-height:1.6; margin:0;">Include full star patterns rather than tight crops — constellations need context to be recognized.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ════ OTHER METHODS ════ --> | |
| <section class="section" style="padding-top:1.5rem; text-align:center;"> | |
| <p style="font-size:.8125rem; color:var(--muted); margin:0 0 1rem;">Prefer a different method?</p> | |
| <div style="display:flex; gap:.75rem; justify-content:center; flex-wrap:wrap;"> | |
| <a href="{% url 'detect' %}" class="btn-ghost" style="padding:.65rem 1.375rem;">Live Camera Detection</a> | |
| <a href="{% url 'database' %}" class="btn-ghost" style="padding:.65rem 1.375rem;">Browse Database</a> | |
| </div> | |
| </section> | |
| {% endblock %} | |
| {% block extra_js %} | |
| <script> | |
| const dropZone = document.getElementById('dropZone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const uploadContent = document.getElementById('uploadContent'); | |
| const previewArea = document.getElementById('previewArea'); | |
| const previewImage = document.getElementById('previewImage'); | |
| const fileName = document.getElementById('fileName'); | |
| const fileSize = document.getElementById('fileSize'); | |
| const removeImage = document.getElementById('removeImage'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const submitText = document.getElementById('submitText'); | |
| const loadingSpinner = document.getElementById('loadingSpinner'); | |
| const uploadForm = document.getElementById('uploadForm'); | |
| dropZone.addEventListener('click', () => { | |
| if (!previewArea.classList.contains('hidden')) return; | |
| fileInput.click(); | |
| }); | |
| dropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.add('dragging'); | |
| }); | |
| dropZone.addEventListener('dragleave', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('dragging'); | |
| }); | |
| dropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('dragging'); | |
| if (e.dataTransfer.files.length > 0) handleFile(e.dataTransfer.files[0]); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) handleFile(e.target.files[0]); | |
| }); | |
| function handleFile(file) { | |
| const validTypes = ['image/jpeg', 'image/jpg', 'image/png']; | |
| if (!validTypes.includes(file.type)) { | |
| alert('Please select a valid image file (JPG, JPEG, or PNG)'); | |
| return; | |
| } | |
| if (file.size > 10 * 1024 * 1024) { | |
| alert('File size must be less than 10MB'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| previewImage.src = e.target.result; | |
| fileName.textContent = file.name; | |
| fileSize.textContent = `${(file.size / 1024 / 1024).toFixed(2)} MB`; | |
| uploadContent.classList.add('hidden'); | |
| previewArea.classList.remove('hidden'); | |
| dropZone.classList.add('has-file'); | |
| submitBtn.disabled = false; | |
| submitText.textContent = 'Analyze Constellations'; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| removeImage.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| fileInput.value = ''; | |
| previewArea.classList.add('hidden'); | |
| uploadContent.classList.remove('hidden'); | |
| dropZone.classList.remove('has-file'); | |
| submitBtn.disabled = true; | |
| submitText.textContent = 'Select an image first'; | |
| }); | |
| uploadForm.addEventListener('submit', () => { | |
| submitBtn.disabled = true; | |
| submitText.textContent = 'Analyzing…'; | |
| loadingSpinner.classList.remove('hidden'); | |
| }); | |
| </script> | |
| {% endblock %} | |