Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/hxmz-axfn07/qr-printing-sfw/llms.txt

Use this file to discover all available pages before exploring further.

The upload form at /upload is where customers build their print order. It is a single-page form (served from client/upload.html, driven by client/upload.js) that lets a customer add multiple files in one submission, configure independent print settings for each file, preview a live order summary, and send everything to the server in one click. No account is required.

Customer Details

The top section of the form collects contact information for the order.
customer_name
string
required
The customer’s full name. Used by staff to identify the order on the dashboard and call the customer when their prints are ready. Defaults to "Walk-in customer" on the server if left blank.
phone
string
Phone number. Optional but recommended so staff can contact the customer. Accepts any format — no validation is applied client-side.
notes
string
Free-form order-level notes visible to staff. Use this for instructions that apply to the entire order, for example "Need urgently" or "Staple all files together".

File Cart

Below the contact fields is the File cart section. It starts with one blank file slot and grows as the customer adds more documents.

Adding documents

When the page loads, upload.js fetches GET /api/config to retrieve the available print options, then immediately calls addDocument() to render the first file slot:
fetch("/api/config")
  .then((response) => response.json())
  .then((payload) => {
    config = payload;
    addDocument(); // renders the first blank document card
  });
Tapping the Add file button calls addDocument() again for each additional document. Each call clones the <template id="documentTemplate"> element and appends the resulting card to the #documents container. The color mode and print style <select> menus are populated from the live config response, so the options always match what the shop has configured.
function addDocument() {
  const clone = template.content.firstElementChild.cloneNode(true);

  // Populate color mode options from config (e.g. bw / color)
  optionList(config.options?.color_modes || { bw: "Black & white", color: "Color" })
    .forEach(([value, label]) => {
      clone.querySelector('[data-field="color_mode"]')
        .insertAdjacentHTML("beforeend", `<option value="${value}">${label}</option>`);
    });

  // Populate print style options from config (e.g. single / double)
  optionList(config.options?.print_styles || { single: "Single side", double: "Double side" })
    .forEach(([value, label]) => {
      clone.querySelector('[data-field="print_style"]')
        .insertAdjacentHTML("beforeend", `<option value="${value}">${label}</option>`);
    });

  documentsRoot.appendChild(clone);
  refreshNumbers();
}
To remove a document the customer taps the Remove button on that card. The minimum is one file — the remove button does nothing when only one slot remains.

Per-Document Print Options

Each file card contains its own set of print options. These are configured independently per file, so a customer can print one document in color and another in black and white within the same order.
file
file
required
The document to print. Accepted formats include PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, images, and other common print file types as listed in shop.supported_file_types. The file is uploaded as part of the documents multipart field.
color_mode
string
required
Controls whether the document is printed in black and white or color.
ValueLabel
bwBlack & white
colorColor
Options are drawn from options.color_modes in config.yml.
print_style
string
required
Controls single-sided or double-sided (duplex) printing.
ValueLabel
singleSingle side
doubleDouble side
Options are drawn from options.print_styles in config.yml.
copies
integer
required
Number of copies to print. Must be between 1 and 99 (enforced both client-side via min="1" max="99" and server-side with max(1, min(99, int(...)))). Defaults to 1.
document_notes
string
Per-file freeform instruction for staff. For example: "Print pages 1-10 only" or "Use thick paper".

Live Order Review

As the customer fills in the form, a live Order review panel updates automatically below the file cards. Every change and input event on any document card triggers refreshReview():
function refreshReview() {
  const rows = [...documentsRoot.children].map((card, index) => {
    const file = card.querySelector('[data-field="file"]').files[0];
    const color = card.querySelector('[data-field="color_mode"]');
    const style = card.querySelector('[data-field="print_style"]');
    const copies = card.querySelector('[data-field="copies"]').value || 1;
    return `<li>
      <strong>${file ? file.name : `File ${index + 1}`}</strong>
      <span>${color.selectedOptions[0]?.textContent || ""}</span>
      <span>${style.selectedOptions[0]?.textContent || ""}</span>
      <span>${copies} copies</span>
    </li>`;
  }).join("");
  review.innerHTML = `<h2>Order review</h2><ul>${rows}</ul>
    <p>Estimated cost appears automatically for PDFs.
       Other files are estimated after staff review.</p>`;
}
The panel shows the filename (or a placeholder like “File 1” before a file is chosen), the human-readable color mode and print style labels, and the copy count. For PDF files the server automatically detects the page count on upload and calculates an estimated cost using the configured pricing table. For other file types the cost estimate is completed by staff during review.

Submitting the Order

When the customer taps Submit order, the form’s submit event is intercepted by upload.js. The buildFormData() function assembles a multipart/form-data payload and POSTs it to POST /api/orders:
form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const response = await fetch("/api/orders", {
    method: "POST",
    body: buildFormData(),
  });
  const payload = await response.json();
  if (!response.ok) throw new Error(payload.error || "Upload failed");
  location.href = `/success?id=${encodeURIComponent(payload.order.id)}`;
});
On success (HTTP 201 Created) the browser redirects to /success?id=<order-id>, where the UUID order ID is displayed for the customer’s reference. If an error occurs (e.g. a file exceeds the size limit, or no file was attached) an inline error message is shown in red beneath the form without navigating away.

Multipart field naming convention

buildFormData() uses the following field names. Per-document fields are zero-indexed to match the order each file was added:
customer_name           # string  -- customer's full name
phone                   # string  -- customer's phone number
notes                   # string  -- order-level notes

documents               # file    -- repeatable; one entry per file in the cart

color_mode_0            # string  -- color mode for document 0 (bw / color)
color_mode_1            # string  -- color mode for document 1
...

print_style_0           # string  -- print style for document 0 (single / double)
print_style_1           # string  -- print style for document 1
...

copies_0                # integer -- copy count for document 0 (1-99)
copies_1                # integer -- copy count for document 1
...

document_notes_0        # string  -- per-file notes for document 0
document_notes_1        # string  -- per-file notes for document 1
...
The server reads each per-document field using the corresponding index (fields.get(f"color_mode_{index}"), etc.) to match options back to the right uploaded file.
The upload form does not include a paper size selector. The server assigns every document a paper size of A4 by default (fields.get(f"paper_size_{index}") or "A4"). Staff can adjust the paper size from the admin dashboard during order review.
The default file size limit is 50 MB per file. This can be raised or lowered by setting shop.max_upload_mb in config.yml (or via the admin settings panel). Submitting a file that exceeds the limit returns an error message naming the offending file; all other files in the order are discarded and the customer must re-submit.

Build docs developers (and LLMs) love