190 lines
12 KiB
HTML
190 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Submit a print — hamprint{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
/* Show/hide source-input panes based on the radio selection. The radios
|
|
live inside a grid <div> while the panes are siblings of the grid, so a
|
|
plain `~` combinator never matches. `fieldset:has(...)` reaches across
|
|
the grid and selects the right pane regardless of nesting depth. */
|
|
.src-pane { display: none; }
|
|
fieldset:has(#src-upload:checked) .src-pane[data-pane="upload"],
|
|
fieldset:has(#src-printables:checked) .src-pane[data-pane="url"],
|
|
fieldset:has(#src-makerworld:checked) .src-pane[data-pane="url"],
|
|
fieldset:has(#src-thingiverse:checked) .src-pane[data-pane="url"] { display: block; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-3xl mx-auto">
|
|
<header class="mb-6">
|
|
<h1 class="text-2xl font-bold tracking-tight">Submit a print</h1>
|
|
<p class="text-slate-600 mt-1">Anyone can submit a job. We'll send you a codename to follow it on the dashboard.</p>
|
|
</header>
|
|
|
|
{% if user.is_authenticated %}
|
|
<div class="rounded-lg border border-emerald-200 bg-emerald-50 p-4 mb-6 flex items-start gap-3 text-sm">
|
|
<svg class="w-5 h-5 text-emerald-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd"/></svg>
|
|
<p class="text-emerald-900">Signed in as <span class="font-medium">{{ user.email|default:user.get_username }}</span>. No email confirmation needed.</p>
|
|
</div>
|
|
{% else %}
|
|
<div class="rounded-lg border border-slate-200 bg-white p-4 mb-6 flex items-start gap-3 text-sm">
|
|
<span class="px-2 py-0.5 rounded-md bg-slate-100 text-slate-700 text-xs font-medium uppercase tracking-wide">Guest</span>
|
|
<p class="text-slate-700">You're submitting anonymously. We'll email you a confirmation link — click it within <span class="font-medium">24 hours</span> to put your print in the queue. <a href="{% url 'account_login' %}" class="text-amber-700 font-medium hover:underline">Sign in with Google</a> to skip this step.</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="post" enctype="multipart/form-data" class="space-y-6 bg-white border border-slate-200 rounded-lg p-6">
|
|
{% csrf_token %}
|
|
|
|
{% if form.non_field_errors %}
|
|
<div class="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800">
|
|
{{ form.non_field_errors|join:" " }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% comment %}
|
|
Source-type radio cards. Hardcoded (not looped) so Tailwind's scanner
|
|
sees the literal `peer/<name>` and `peer-checked/<name>:` class strings
|
|
and emits the matching CSS rules.
|
|
{% endcomment %}
|
|
<fieldset>
|
|
<legend class="block text-sm font-medium text-slate-900 mb-2">Where is the model coming from? <span class="text-red-500">*</span></legend>
|
|
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 mb-4">
|
|
<input type="radio" name="{{ form.source_type.html_name }}" id="src-upload" value="upload" class="hidden peer/upload" {% if form.source_type.value == "upload" or not form.source_type.value %}checked{% endif %}>
|
|
<label for="src-upload" class="cursor-pointer border border-slate-300 rounded-md px-3 py-2 text-center text-sm hover:border-amber-500 peer-checked/upload:border-amber-500 peer-checked/upload:bg-amber-50 peer-checked/upload:text-amber-900 peer-checked/upload:font-medium">.stl file</label>
|
|
|
|
<input type="radio" name="{{ form.source_type.html_name }}" id="src-printables" value="printables" class="hidden peer/printables" {% if form.source_type.value == "printables" %}checked{% endif %}>
|
|
<label for="src-printables" class="cursor-pointer border border-slate-300 rounded-md px-3 py-2 text-center text-sm hover:border-amber-500 peer-checked/printables:border-amber-500 peer-checked/printables:bg-amber-50 peer-checked/printables:text-amber-900 peer-checked/printables:font-medium">Printables</label>
|
|
|
|
<input type="radio" name="{{ form.source_type.html_name }}" id="src-makerworld" value="makerworld" class="hidden peer/makerworld" {% if form.source_type.value == "makerworld" %}checked{% endif %}>
|
|
<label for="src-makerworld" class="cursor-pointer border border-slate-300 rounded-md px-3 py-2 text-center text-sm hover:border-amber-500 peer-checked/makerworld:border-amber-500 peer-checked/makerworld:bg-amber-50 peer-checked/makerworld:text-amber-900 peer-checked/makerworld:font-medium">MakerWorld</label>
|
|
|
|
<input type="radio" name="{{ form.source_type.html_name }}" id="src-thingiverse" value="thingiverse" class="hidden peer/thingiverse" {% if form.source_type.value == "thingiverse" %}checked{% endif %}>
|
|
<label for="src-thingiverse" class="cursor-pointer border border-slate-300 rounded-md px-3 py-2 text-center text-sm hover:border-amber-500 peer-checked/thingiverse:border-amber-500 peer-checked/thingiverse:bg-amber-50 peer-checked/thingiverse:text-amber-900 peer-checked/thingiverse:font-medium">Thingiverse</label>
|
|
</div>
|
|
|
|
{% comment %}
|
|
Upload pane. The native file input stays sr-only and is triggered by
|
|
the wrapping <label> dropzone. After selection, the prompt text is
|
|
swapped for the filename + size via the inline script below.
|
|
{% endcomment %}
|
|
<div class="src-pane" data-pane="upload">
|
|
<label class="block cursor-pointer">
|
|
<div class="border-2 border-dashed border-slate-300 rounded-md p-6 text-center hover:border-amber-500 hover:bg-amber-50/40 transition">
|
|
<svg class="w-8 h-8 mx-auto text-slate-400" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0L9 12.75M12 9.75l3 3M3 17.25V18a2.25 2.25 0 002.25 2.25h13.5A2.25 2.25 0 0021 18v-.75"/></svg>
|
|
<p data-stl-prompt class="mt-2 text-sm font-medium text-slate-700">Drop your .stl here or click to browse</p>
|
|
<p data-stl-filename class="hidden mt-2 text-sm font-medium text-amber-700"></p>
|
|
<p class="text-xs text-slate-500 mt-1">Raw <span class="mono">.stl</span> only, max 4 MB. No <span class="mono">.3mf</span>, <span class="mono">.gcode</span>, <span class="mono">.zip</span>, etc.</p>
|
|
</div>
|
|
{{ form.stl_file }}
|
|
</label>
|
|
{% if form.stl_file.errors %}<p class="text-xs text-red-600 mt-1">{{ form.stl_file.errors|join:" " }}</p>{% endif %}
|
|
</div>
|
|
|
|
{% comment %}
|
|
Shared URL pane. The label, placeholder, and help text are rewritten
|
|
per source-type by the inline script below so the user gets host-
|
|
specific guidance even though there's only one real <input>.
|
|
{% endcomment %}
|
|
<div class="src-pane" data-pane="url">
|
|
<label for="{{ form.source_url.id_for_label }}" data-url-label class="block text-sm font-medium text-slate-700 mb-1">Model URL</label>
|
|
{{ form.source_url }}
|
|
<p data-url-help class="text-xs text-slate-500 mt-1">Must be a link on <span class="mono">printables.com</span>, <span class="mono">makerworld.com</span>, or <span class="mono">thingiverse.com</span> matching your selection above.</p>
|
|
{% if form.source_url.errors %}<p class="text-xs text-red-600 mt-1">{{ form.source_url.errors|join:" " }}</p>{% endif %}
|
|
</div>
|
|
|
|
<p class="text-xs text-slate-500 mt-3">Got something from elsewhere? Download the <span class="mono">.stl</span> and upload it as a file.</p>
|
|
{% if form.source_type.errors %}<p class="text-xs text-red-600 mt-1">{{ form.source_type.errors|join:" " }}</p>{% endif %}
|
|
</fieldset>
|
|
|
|
{# Filament #}
|
|
<div>
|
|
<label for="{{ form.requested_filament.id_for_label }}" class="block text-sm font-medium text-slate-900 mb-1">Filament</label>
|
|
{{ form.requested_filament }}
|
|
<p class="text-xs text-slate-500 mt-1">Only filaments currently loaded at hamlab.lt are listed. The list is curated by operators — out-of-stock options are hidden.</p>
|
|
{% if form.requested_filament.errors %}<p class="text-xs text-red-600 mt-1">{{ form.requested_filament.errors|join:" " }}</p>{% endif %}
|
|
</div>
|
|
|
|
{# Notes (private) #}
|
|
<div>
|
|
<label for="{{ form.notes_for_op.id_for_label }}" class="block text-sm font-medium text-slate-900 mb-1">
|
|
Notes for the operator
|
|
<span class="ml-1 inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-slate-100 text-slate-600 text-[10px] font-medium uppercase tracking-wide align-middle">
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"/></svg>
|
|
Private
|
|
</span>
|
|
</label>
|
|
{{ form.notes_for_op }}
|
|
<p class="text-xs text-slate-500 mt-1">Only the operator sees these — they're not shown on your public submission page.</p>
|
|
{% if form.notes_for_op.errors %}<p class="text-xs text-red-600 mt-1">{{ form.notes_for_op.errors|join:" " }}</p>{% endif %}
|
|
</div>
|
|
|
|
{% if form.guest_email %}
|
|
<div class="border-t border-slate-200 pt-6">
|
|
<label for="{{ form.guest_email.id_for_label }}" class="block text-sm font-medium text-slate-900 mb-1">Your email <span class="text-red-500">*</span></label>
|
|
{{ form.guest_email }}
|
|
<p class="text-xs text-slate-500 mt-1">We'll send a confirmation link. The submission disappears in 24 hours if not confirmed. Never shown publicly.</p>
|
|
{% if form.guest_email.errors %}<p class="text-xs text-red-600 mt-1">{{ form.guest_email.errors|join:" " }}</p>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="flex items-center justify-end gap-3 border-t border-slate-200 pt-6">
|
|
<a href="{% url 'dashboard:index' %}" class="px-4 py-2 text-sm rounded-md text-slate-700 hover:bg-slate-100">Cancel</a>
|
|
<button type="submit" class="px-4 py-2 text-sm rounded-md bg-amber-500 text-white hover:bg-amber-600 font-medium">Submit print</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
// Filename feedback: swap "Drop your .stl here" for the selected file's name
|
|
// + size as soon as the user picks something via the hidden file input.
|
|
(function() {
|
|
var input = document.getElementById("{{ form.stl_file.id_for_label }}");
|
|
var prompt = document.querySelector("[data-stl-prompt]");
|
|
var filename = document.querySelector("[data-stl-filename]");
|
|
if (!input || !prompt || !filename) return;
|
|
input.addEventListener("change", function(e) {
|
|
var file = e.target.files && e.target.files[0];
|
|
if (file) {
|
|
prompt.classList.add("hidden");
|
|
filename.classList.remove("hidden");
|
|
filename.textContent = "✓ " + file.name + " (" + Math.round(file.size / 1024) + " KB)";
|
|
} else {
|
|
prompt.classList.remove("hidden");
|
|
filename.classList.add("hidden");
|
|
filename.textContent = "";
|
|
}
|
|
});
|
|
})();
|
|
|
|
// URL-pane copy: rewrite the label, the input's placeholder, and the help
|
|
// text per source-type so the user sees host-specific guidance.
|
|
(function() {
|
|
var COPY = {
|
|
printables: { label: "Printables.com URL", ph: "https://www.printables.com/model/…", help: "Must be a printables.com link." },
|
|
makerworld: { label: "MakerWorld URL", ph: "https://makerworld.com/en/models/…", help: "Must be a makerworld.com link." },
|
|
thingiverse: { label: "Thingiverse URL", ph: "https://www.thingiverse.com/thing:…", help: "Must be a thingiverse.com link." }
|
|
};
|
|
var labelEl = document.querySelector("[data-url-label]");
|
|
var inputEl = document.getElementById("{{ form.source_url.id_for_label }}");
|
|
var helpEl = document.querySelector("[data-url-help]");
|
|
|
|
function apply(sourceType) {
|
|
var c = COPY[sourceType];
|
|
if (!c) return;
|
|
if (labelEl) labelEl.textContent = c.label;
|
|
if (inputEl) inputEl.placeholder = c.ph;
|
|
if (helpEl) helpEl.textContent = c.help;
|
|
}
|
|
|
|
document.querySelectorAll('input[name="{{ form.source_type.html_name }}"]').forEach(function(radio) {
|
|
radio.addEventListener("change", function(e) { apply(e.target.value); });
|
|
if (radio.checked) apply(radio.value);
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|