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
});
}
}
}">
Prime Source
Deflector Perfect Hat
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
Deflector Perfect Hat 4008A Endorsed by the Cancer Society, $0.50 of each hat sold goes to the Cancer Society. UPF rating 50 "Excellent Protection" Meets NZ Standard AS/NZS 4399 Cancer Society information ladel attached Fade Resistant Poly Viscose Knitted Elastic sweatband for comfort and secure wear 45 degree tilted hat brim to shade face, nose, ears and neck Light weight and packable Sizes: XS (53cm), S (55cm), M (58cm), L (60cm)
usage
Beach/Summer, Picnic/BBQ, School, Uniforms
brand
Legend
sizes
L, M, S, XS