diff --git a/Pipfile b/Pipfile index 4d46502..c43b36d 100644 --- a/Pipfile +++ b/Pipfile @@ -5,6 +5,7 @@ name = "pypi" [packages] django = "<5.0,>=4.2" +whitenoise = "<7.0,>=6.0" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index ff0cd9e..25ecfca 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bd4cb91114d0c833ac47abdad00410896c666f4553053870c3c7655342c703a0" + "sha256": "faf9d6b8d73f937fa430b92dadbef77a6921f9675d5e8e3131b663391e308135" }, "pipfile-spec": 6, "requires": { @@ -18,28 +18,37 @@ "default": { "asgiref": { "hashes": [ - "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", - "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e" + "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", + "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d" ], "markers": "python_version >= '3.9'", - "version": "==3.10.0" + "version": "==3.11.0" }, "django": { "hashes": [ - "sha256:9398e487bcb55e3f142cb56d19fbd9a83e15bb03a97edc31f408361ee76d9d7a", - "sha256:c96e64fc3c359d051a6306871bd26243db1bd02317472a62ffdbe6c3cae14280" + "sha256:b865fbe0f4a3d1ee36594c5efa42b20db3c8bbb10dff0736face1c6e4bda5b92", + "sha256:f393a394053713e7d213984555c5b7d3caeee78b2ccb729888a0774dff6c11a8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.26" + "version": "==4.2.27" }, "sqlparse": { "hashes": [ - "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", - "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca" + "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e", + "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb" ], "markers": "python_version >= '3.8'", - "version": "==0.5.3" + "version": "==0.5.4" + }, + "whitenoise": { + "hashes": [ + "sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f", + "sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==6.11.0" } }, "develop": {} diff --git a/seduttomachineworks_project/quote_submit.py b/seduttomachineworks_project/quote_submit.py new file mode 100644 index 0000000..a0b2e24 --- /dev/null +++ b/seduttomachineworks_project/quote_submit.py @@ -0,0 +1,81 @@ +import json +import time +from pathlib import Path +from django.http import StreamingHttpResponse, JsonResponse +from django.conf import settings +from django.views.decorators.clickjacking import xframe_options_exempt +from django.views.decorators.csrf import csrf_exempt + + +def send_progress_update(step, message, progress): + """Helper to send a progress update as JSON""" + return json.dumps({"step": step, "message": message, "progress": progress}) + "\n" + + +@xframe_options_exempt +@csrf_exempt +def submit_quote(request): + """ + Handle quote submission with progress updates. + Steps: + 1. Upload files + 2. Save files + 3. Send email + 4. Verify email sent + """ + if request.method != "POST": + return JsonResponse({"error": "Method not allowed"}, status=405) + + # Read form data first (files need to be read from request) + email = request.POST.get("email", "").strip() + notes = request.POST.get("notes", "").strip() + files = request.FILES.getlist("files") + + def process_quote(): + try: + # Step 1: Upload files + yield send_progress_update(1, "Uploading files...", 25) + # Files are already uploaded at this point, just acknowledge + uploaded_files = [] + for file in files: + uploaded_files.append({"name": file.name, "size": file.size}) + + # Step 2: Save files + yield send_progress_update(2, "Saving files...", 50) + # Save files to disk/storage + saved_paths = [] + for file in files: + # Create store directory if it doesn't exist + store_dir = Path(settings.MEDIA_ROOT) / "uploads" + store_dir.mkdir(parents=True, exist_ok=True) + + # Save file + file_path = store_dir / file.name + with open(file_path, "wb") as f: + for chunk in file.chunks(): + f.write(chunk) + saved_paths.append(str(file_path)) + + # Step 3: Send emails + yield send_progress_update(3, "Sending emails 1/2...", 70) + time.sleep(0.5) + # TODO: Actually send emails + yield send_progress_update(4, "Sending emails 2/2...", 75) + time.sleep(0.5) + # TODO: Actually send emails + emails_sent = True # Placeholder + + # Complete + yield send_progress_update( + 5, + "Quote request submitted successfully! Check your email for confirmation.", + 100, + ) + + except Exception as e: + yield send_progress_update("error", f"An error occurred: {str(e)}", 0) + + response = StreamingHttpResponse(process_quote(), content_type="text/event-stream") + response["Cache-Control"] = "no-cache" + response["X-Accel-Buffering"] = "no" + return response diff --git a/seduttomachineworks_project/urls.py b/seduttomachineworks_project/urls.py index 2787d86..f5a2d1c 100644 --- a/seduttomachineworks_project/urls.py +++ b/seduttomachineworks_project/urls.py @@ -4,11 +4,13 @@ URL configuration for seduttomotorsports project. from django.urls import path from . import views +from . import quote_submit from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path("", views.quote_upload, name="quote_upload"), + path("submit-quote/", quote_submit.submit_quote, name="submit_quote"), ] if settings.DEBUG: diff --git a/static/css/quote_upload.css b/static/css/quote_upload.css index f6683f3..2d0c5f9 100644 --- a/static/css/quote_upload.css +++ b/static/css/quote_upload.css @@ -30,6 +30,47 @@ h2 { color: #333; } +.form-field { + margin-bottom: 20px; +} + +.form-field label { + display: block; + margin-bottom: 8px; + color: #333; + font-weight: 500; + font-size: 14px; +} + +.form-field input[type="email"], +.form-field textarea { + width: 100%; + padding: 12px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + font-family: Arial, sans-serif; + transition: border-color 0.3s; +} + +.form-field input[type="email"]:focus, +.form-field textarea:focus { + outline: none; + border-color: #0066cc; +} + +.form-field input[type="email"]:disabled, +.form-field textarea:disabled { + background: #f5f5f5; + cursor: not-allowed; + opacity: 0.7; +} + +.form-field textarea { + resize: vertical; + min-height: 80px; +} + .drop-area { border: 2px dashed #ccc; border-radius: 4px; @@ -49,6 +90,11 @@ h2 { background: #e6f2ff; } +.drop-area.disabled { + opacity: 0.6; + cursor: not-allowed; +} + .drop-area p + p { font-size: 14px; color: #666; @@ -96,14 +142,34 @@ button:disabled { cursor: not-allowed; } -.todo { +.progress-container { margin-top: 20px; - padding: 10px; - background: #fff3cd; - border: 1px solid #ffc107; + padding: 16px; + background: #f9f9f9; border-radius: 4px; +} + +.progress-bar { + width: 100%; + height: 24px; + background: #e0e0e0; + border-radius: 12px; + overflow: hidden; + margin-bottom: 10px; +} + +.progress-fill { + height: 100%; + background: #0066cc; + border-radius: 12px; + transition: width 0.3s ease; + width: 0%; +} + +.progress-message { font-size: 14px; - color: #856404; + color: #666; + text-align: center; } footer { diff --git a/static/js/quote_upload.js b/static/js/quote_upload.js new file mode 100644 index 0000000..c7b11b3 --- /dev/null +++ b/static/js/quote_upload.js @@ -0,0 +1,181 @@ +const dropArea = document.getElementById('dropArea'); +const fileInput = document.getElementById('fileInput'); +const fileList = document.getElementById('fileList'); +const submitBtn = document.getElementById('submitBtn'); +const emailInput = document.getElementById('emailInput'); +const notesInput = document.getElementById('notesInput'); +let selectedFiles = []; + +function checkCanSubmit() { + const hasEmail = emailInput.value.trim().length > 0; + const hasNotes = notesInput.value.trim().length > 0; + const hasFiles = selectedFiles.length > 0; + submitBtn.disabled = !(hasEmail || hasNotes || hasFiles); +} + +emailInput.addEventListener('input', checkCanSubmit); +notesInput.addEventListener('input', checkCanSubmit); + +dropArea.addEventListener('click', () => fileInput.click()); + +dropArea.addEventListener('dragover', (e) => { + e.preventDefault(); + dropArea.classList.add('dragover'); +}); + +dropArea.addEventListener('dragleave', () => { + dropArea.classList.remove('dragover'); +}); + +dropArea.addEventListener('drop', (e) => { + e.preventDefault(); + dropArea.classList.remove('dragover'); + handleFiles(e.dataTransfer.files); +}); + +fileInput.addEventListener('change', (e) => { + handleFiles(e.target.files); +}); + +function handleFiles(files) { + selectedFiles = Array.from(files); + displayFiles(); + checkCanSubmit(); +} + +function displayFiles() { + if (selectedFiles.length === 0) { + fileList.classList.remove('show'); + return; + } + fileList.classList.add('show'); + fileList.innerHTML = selectedFiles.map(file => + `
Drag & drop files here
+Drag & drop drawings, CAD models, images or anything else here
or click to browse