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 }); } } }">

Trends NZ

Oakridge Cork Coaster Round - Set of 4

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

Set of four stylish natural cork coasters that provide protection from heat and spills. When cork is harvested only the outer bark is removed from the tree. This allows the tree to continue growing and replenishing the bark. It also has the benefit of a comfortable soft-touch texture while being very durable. This set is nicely presented in a natural cardboard gift box with a window to show off the brand. Features:

  • Set of four stylish natural cork coasters
  • Features a soft touch texture
  • Protects surfaces from heat, moisture and spills
  • Presentation: Natural kraft gift box

Dimensions

D 4

Packaging

Individual Gift Box.

Branding Options

Colourflex Transfer: 80mm circle Laser Engraving: 80mm circle Digital Packaging Print: 80mm circle DigiFlex Transfer: 80mm circle

Shape

Round

Included

x4 Coasters

Laser Engraving Finish

Natural Etch

Country of Origin

China

Impact Aware collection

This product is part of the Impact Aware collection because it is made with natural materials.

You might also like...

More in Home & Living