commit 3d66c2a53a55ea6d3aa4c17bbda3693193e16a92 Author: Wartana Date: Sat Feb 28 17:16:50 2026 +0800 feat: add initial web-based label printer application including UI, logic, styles, and Nginx configuration. diff --git a/app.js b/app.js new file mode 100644 index 0000000..c5bb000 --- /dev/null +++ b/app.js @@ -0,0 +1,39 @@ +// DOM Elements +const labelForm = document.getElementById('labelForm'); +const printBtn = document.getElementById('printBtn'); + +// Inputs +const itemName = document.getElementById('itemName'); +const itemPrice = document.getElementById('itemPrice'); +const itemNotes = document.getElementById('itemNotes'); + +// Preview Elements +const previewName = document.getElementById('previewName'); +const previewPrice = document.getElementById('previewPrice'); +const previewNotes = document.getElementById('previewNotes'); + +// Live Preview Updater +const updatePreview = () => { + previewName.textContent = itemName.value || 'ITEM NAME'; + previewPrice.textContent = itemPrice.value || 'Rp 0'; + previewNotes.textContent = itemNotes.value || 'Notes here'; +}; + +itemName.addEventListener('input', updatePreview); +itemPrice.addEventListener('input', updatePreview); +itemNotes.addEventListener('input', updatePreview); + +// Print Logic (System Print) +labelForm.addEventListener('submit', (e) => { + e.preventDefault(); + + // Quick validation + if (!itemName.value || !itemPrice.value) { + alert("Please fill name and price."); + return; + } + + // Call the browser's native print dialog + // The CSS @media print rules will hide everything except the label wrapper + window.print(); +}); diff --git a/index.css b/index.css new file mode 100644 index 0000000..155be79 --- /dev/null +++ b/index.css @@ -0,0 +1,402 @@ +:root { + --bg-color: #f0f4f8; + --card-bg: rgba(255, 255, 255, 0.85); + --primary: #4F46E5; + --primary-hover: #4338CA; + --text: #1F2937; + --text-muted: #6B7280; + --border: rgba(229, 231, 235, 0.5); + --success: #10B981; + --danger: #EF4444; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Inter', sans-serif; + color: var(--text); + background-color: var(--bg-color); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + position: relative; + overflow: hidden; +} + +.app-background { + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle at 50% 50%, #e0c3fc 0%, #8ec5fc 100%); + z-index: -1; + animation: drift 20s ease-in-out infinite alternate; +} + +@keyframes drift { + 0% { + transform: translate(0, 0) scale(1); + } + + 100% { + transform: translate(-2%, 2%) scale(1.05); + } +} + +.container { + background: var(--card-bg); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 24px; + padding: 32px; + width: 100%; + max-width: 420px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(40px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.app-header { + text-align: center; + margin-bottom: 24px; +} + +.icon-container { + background: linear-gradient(135deg, var(--primary), #8B5CF6); + width: 64px; + height: 64px; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + color: white; + margin: 0 auto 16px; + box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.3); +} + +h1 { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 4px; +} + +.app-header p { + color: var(--text-muted); + font-size: 0.9rem; +} + +.device-section { + display: flex; + align-items: center; + justify-content: space-between; + background: rgba(255, 255, 255, 0.5); + padding: 16px; + border-radius: 12px; + margin-bottom: 24px; + border: 1px solid var(--border); +} + +.status-indicator { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.9rem; + font-weight: 500; +} + +.dot { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.disconnected { + background-color: var(--danger); + box-shadow: 0 0 8px rgba(239, 68, 68, 0.5); +} + +.connected { + background-color: var(--success); + box-shadow: 0 0 8px rgba(16, 185, 129, 0.5); +} + +button { + cursor: pointer; + font-family: inherit; + border: none; + outline: none; + border-radius: 8px; + font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all 0.2s ease; +} + +.primary-btn { + background: var(--primary); + color: white; + padding: 8px 16px; + font-size: 0.9rem; +} + +.primary-btn:hover { + background: var(--primary-hover); + transform: translateY(-1px); +} + +.action-btn { + width: 100%; + padding: 14px; + background: linear-gradient(135deg, var(--success), #059669); + color: white; + font-size: 1.1rem; + border-radius: 12px; + margin-top: 16px; + box-shadow: 0 4px 6px -1px rgba(16, 185, 129, 0.3); +} + +.action-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 10px 15px -3px rgba(16, 185, 129, 0.4); +} + +.action-btn:disabled { + background: #D1D5DB; + cursor: not-allowed; + box-shadow: none; + transform: none; +} + +h2 { + font-size: 1.1rem; + margin-bottom: 16px; + border-bottom: 2px solid var(--primary); + padding-bottom: 8px; + display: inline-block; +} + +.form-group { + margin-bottom: 16px; +} + +.form-row { + display: flex; + gap: 16px; +} + +.form-row .form-group { + flex: 1; +} + +label { + display: block; + font-size: 0.85rem; + color: var(--text-muted); + margin-bottom: 6px; + font-weight: 500; +} + +input { + width: 100%; + padding: 10px 12px; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 8px; + font-family: inherit; + font-size: 1rem; + background: rgba(255, 255, 255, 0.8); + transition: all 0.2s; +} + +input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); + background: white; +} + +.preview-container { + background: #fff; + border: 1px dashed var(--primary); + border-radius: 8px; + padding: 16px; + margin-top: 24px; + margin-bottom: 16px; + text-align: center; +} + +.preview-container label { + margin-bottom: 12px; +} + +.label-preview { + /* Simulating 58mm printer aspect/width constraints (approx 384 dots width = 48 chars) */ + width: 250px; + margin: 0 auto; + font-family: monospace; + color: black; + text-align: center; + padding: 10px; + background-color: white; + border: 1px solid #eee; +} + +.preview-name { + font-weight: bold; + font-size: 1.2rem; + margin-bottom: 8px; + text-transform: uppercase; +} + +.preview-price { + font-size: 1.1rem; + margin-bottom: 8px; + font-weight: bold; +} + +.preview-notes { + font-size: 0.85rem; + color: #444; +} + +.brand-header { + font-size: 1.1rem; + font-weight: bold; + margin-bottom: 12px; +} + +.divider { + font-size: 0.8rem; + color: #000; + margin: 4px 0; + letter-spacing: -1px; +} + +footer { + text-align: center; + margin-top: 24px; + font-size: 0.8rem; + color: var(--text-muted); +} + +/* ========================================= + PRINT STYLES FOR 58MM THERMAL PRINTER + ========================================= */ +@media print { + + /* Hide Everything Except Printable Area */ + body { + margin: 0; + padding: 0; + background: white !important; + } + + /* Hide all child elements of body except the container */ + body>*:not(#appContainer) { + display: none !important; + } + + /* Hide background elements, headers, footers */ + #appBackground, + header, + .designer-section h2, + .device-section, + footer, + /* Hide the form inputs and labels */ + #labelForm>.form-group, + #labelForm>.form-row, + #printBtn, + .preview-container>label { + display: none !important; + } + + /* Remove styles from structural wrappers so it sits top-left */ + #appContainer, + #labelForm, + .preview-container { + box-shadow: none !important; + border: none !important; + background: transparent !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + padding: 0 !important; + max-width: none !important; + animation: none !important; + margin: 0 !important; + position: static !important; + } + + /* Print Setup */ + @page { + margin: 0; + size: 58mm auto; + /* Basic generic 58mm paper size */ + } + + .printable-area { + position: absolute; + left: 0; + top: 0; + width: 48mm; + /* Safe margin from 58mm total width */ + margin: 0 auto; + padding: 0; + text-align: center; + border: none !important; + background: white !important; + font-family: 'Courier New', Courier, monospace !important; + color: #000 !important; + } + + /* Format Adjustments for Print Quality */ + .printable-area .brand-header { + font-size: 14pt !important; + margin-bottom: 8px !important; + } + + .printable-area .preview-name { + font-size: 16pt !important; + margin-bottom: 4px !important; + } + + .printable-area .preview-price { + font-size: 14pt !important; + margin-bottom: 4px !important; + } + + .printable-area .preview-notes { + font-size: 10pt !important; + margin-top: 4px !important; + } + + .printable-area .divider { + font-size: 10pt !important; + } + + /* Extra padding at bottom for tear-off */ + .printable-area::after { + content: ""; + display: block; + height: 20mm; + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..dfe7435 --- /dev/null +++ b/index.html @@ -0,0 +1,92 @@ + + + + + + + Bluetooth Label Printer App + + + + + + +
+ +
+
+
+ + + + + +
+

Smart Label Printer

+

58mm Thermal Bluetooth Print

+
+ +
+
+
+ Browser Print Ready +
+

Use the browser's standard Print dialog. Set Paper Size to 58mm & + Margins to None.

+
+ +
+

Design Label

+
+
+ + +
+ +
+
+ + +
+
+ +
+ + +
+ +
+ +
+
LABEL STORE
+
Item Name
+
====================
+
Rp 0
+
====================
+
Notes here
+
+
+ + +
+
+ +
+

Built with Vanilla JS & Web Bluetooth API

+
+
+ + + + + \ No newline at end of file diff --git a/label.app.oncloud.my.id.conf b/label.app.oncloud.my.id.conf new file mode 100644 index 0000000..0ac9c78 --- /dev/null +++ b/label.app.oncloud.my.id.conf @@ -0,0 +1,13 @@ +server { + listen 80; + server_name label.app.oncloud.my.id; + root /home/wartana/myApp/label; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + # Enable CORS if needed + add_header Access-Control-Allow-Origin *; +}