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
Desk Top Flags
Colour:
Quantity
Branding Logo or Images
Accepted file types: PDF, AI, EPS, JPG, PNG | Max file size: 10MB | Max files: 1
check_circle
error
Product Description expand_more
Add a professional finishing touch to your event or display with our Table Top Flags, perfect for trade shows, conferences, presentations, and table settings. Featuring vibrant full-colour printing on polyester fabric, these flags are ideal for branding, wayfinding, or table numbering—with the option for different prints on each flag. Choose from plastic or metal poles and bases, including options to hold one, two, or three flags for maximum versatility.
Product Features
Desk Top Flag S/S with Plastic Pole
Product Materials
Plastic Pole - 110 GSM Knitted Polyester Flag
Product Item Size
Flag Size 140mm x 210mm /270mm Plastic Pole
Product Packaging Inner
Plastic Bag