search
f.name)); if (this.uploadedFiles.length + files.length > 5) { alert('You can only upload up to 5 branding files.'); return; } files.forEach(file => { const ext = file.name.split('.').pop().toLowerCase(); if (!['pdf', 'ai', 'eps', 'jpg', 'jpeg', 'png'].includes(ext)) { alert('Only PDF, AI, EPS, JPG, and PNG files are accepted.'); return; } if (file.size > 10 * 1024 * 1024) { alert('File size exceeds the 10MB limit.'); return; } this.uploadFile(file); }); }, updateFileState(filename, data) { const fileObj = this.uploadedFiles.find(f => f.name === filename); if (fileObj) { Object.assign(fileObj, data); this.uploadedFiles = [...this.uploadedFiles]; // Force Alpine reactivity } }, uploadFile(file) { this.uploadedFiles.push({ name: file.name, progress: 0, hashed_name: null, error: null, status: 'uploading' }); this.isUploading = true; this.uploadedFiles = [...this.uploadedFiles]; // Force immediate display of progress bar console.log('uploadFile: Preparing payload for:', file.name); const formData = new FormData(); formData.append('file', file); const csrfToken = document.querySelector('meta[name=csrf-token]')?.getAttribute('content') || ''; console.log('uploadFile: CSRF token lookup result:', csrfToken ? 'Token Found' : 'Token Missing!'); const xhr = new XMLHttpRequest(); xhr.open('POST', '/upload-quote-file'); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); if (csrfToken) { xhr.setRequestHeader('X-CSRF-TOKEN', csrfToken); } xhr.upload.addEventListener('progress', e => { if (e.lengthComputable) { const percentage = Math.round((e.loaded / e.total) * 100); console.log('uploadFile progress (' + file.name + '): ' + percentage + '%'); this.updateFileState(file.name, { progress: percentage }); } }); xhr.addEventListener('load', () => { console.log('uploadFile load complete (' + file.name + '). Status code:', xhr.status); if (xhr.status >= 200 && xhr.status < 300) { try { const response = JSON.parse(xhr.responseText); console.log('uploadFile response (' + file.name + '):', response); if (response.success) { this.updateFileState(file.name, { hashed_name: response.hashed_name, status: 'success', progress: 100 }); } else { this.updateFileState(file.name, { error: response.message || 'Upload failed.', status: 'error' }); } } catch(err) { console.error('JSON parse exception during load handler:', err); this.updateFileState(file.name, { error: 'Invalid server response.', status: 'error' }); } } else { let msg = 'Upload failed.'; try { const err = JSON.parse(xhr.responseText); msg = err.message || msg; } catch(e) {} this.updateFileState(file.name, { error: msg, status: 'error' }); } this.checkUploadStatus(); }); xhr.addEventListener('error', () => { console.error('uploadFile net error (' + file.name + ')'); this.updateFileState(file.name, { error: 'Network error occurred.', status: 'error' }); this.checkUploadStatus(); }); xhr.send(formData); console.log('uploadFile: Sent payload for:', file.name); }, checkUploadStatus() { this.isUploading = this.uploadedFiles.some(f => f.status === 'uploading'); }, removeUploadedFile(index) { this.uploadedFiles.splice(index, 1); this.checkUploadStatus(); }, getBrandingFilesPayload() { return this.uploadedFiles .filter(f => f.status === 'success') .map(f => ({ original_name: f.name, hashed_name: f.hashed_name })); }, getAddIsDisabled(id) { const cartElement = document.querySelector('[x-data=\'shoppingCart\']'); const cart = Alpine.$data(cartElement); return cart.isInCart(id, this.getColourTitle()); }, init() { // Clean up any leading '?&' from the search query string and parse parameters let search = window.location.search; search = search.replace(/^\?&/, '?'); const params = new URLSearchParams(search); // Automatically set the Quantity const quantityParam = params.get('quantity') || params.get('qty'); if (quantityParam) { const qty = parseInt(quantityParam, 10); if (!isNaN(qty) && qty > 0) { this._quantity = qty; } } // Automatically select the Color const colourParam = params.get('colour') || params.get('color'); if (colourParam) { this.$nextTick(() => { const decodedColour = decodeURIComponent(colourParam).trim().toLowerCase(); const buttons = Array.from(document.querySelectorAll('.product-details button[title]')); const match = buttons.find(b => b.getAttribute('title').trim().toLowerCase() === decodedColour); if (match) match.click(); // Triggers the Alpine click handler to select color and update corresponding image }); } } }">

Banners Ink NZ

Pennants 210mm x 300mm

Colour:

Quantity

Branding Logo or Images

Accepted file types: PDF, AI, EPS, JPG, PNG | Max file size: 10MB | Max files: 1

Product Description expand_more

Boost Your Brand or Celebrate in Style with Custom Pennants. Pennants aren’t just decorative—they’re versatile promotional tools perfect for sports teams, community clubs, RSLs, and more. Any design can be easily printed on high-quality fabric, giving you limitless possibilities for customisation. Made for full-colour printing, these pennants provide a professional canvas for logos, awards, or commemorative designs. Stand out, celebrate achievements, and leave a lasting impression with custom pennants designed just for you!

Product Features

Ideal for sports clubs, Schools,Universities etc

Product Materials

130GSM satin drill

Product Item Size

210 x 300mm with 20mm tassels at bottom

Product Packaging Inner

Plastic Bag

You might also like...

More in Exhibitions & Events