// Yoto Up Covers Web App
class YotoCoversApp {
constructor() {
this.images = [];
this.selectedImageIndex = -1;
this.zoomLevel = 1.0;
this.currentTemplate = 'classic';
this.currentTitleStyle = 'folded';
this.printLayout = {
cardsPerRow: 2,
cardsPerColumn: 5,
pageMargin: 10,
cardSpacing: 5
};
this.templateSettings = {
title: '',
footer: '',
accentColor: '#f1c40f',
titleColor: '#111111',
fullBleed: true,
titleShadow: false,
titleFontSize: null,
footerFontSize: null
};
this.cardVariations = [];
this.init();
}
init() {
this.bindEvents();
this.updateUI();
this.updateCardVariations();
}
bindEvents() {
// Print layout settings
document.getElementById('cards-per-row').addEventListener('change', (e) => {
this.printLayout.cardsPerRow = parseInt(e.target.value);
this.generatePreview();
});
document.getElementById('cards-per-column').addEventListener('change', (e) => {
this.printLayout.cardsPerColumn = parseInt(e.target.value);
this.generatePreview();
});
document.getElementById('page-margin').addEventListener('input', (e) => {
this.printLayout.pageMargin = parseInt(e.target.value);
this.generatePreview();
});
document.getElementById('card-spacing').addEventListener('input', (e) => {
this.printLayout.cardSpacing = parseInt(e.target.value);
this.generatePreview();
});
// Template settings
document.getElementById('template-select').addEventListener('change', (e) => {
this.currentTemplate = e.target.value;
this.generatePreview();
});
document.getElementById('title-input').addEventListener('input', (e) => {
this.templateSettings.title = e.target.value;
this.generatePreview();
});
document.getElementById('footer-input').addEventListener('input', (e) => {
this.templateSettings.footer = e.target.value;
this.generatePreview();
});
// Title style selection
document.querySelectorAll('.title-style-option').forEach(option => {
option.addEventListener('click', () => {
document.querySelectorAll('.title-style-option').forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
this.currentTitleStyle = option.dataset.style;
this.generatePreview();
});
});
// Color pickers
document.getElementById('accent-color').addEventListener('input', (e) => {
this.templateSettings.accentColor = e.target.value;
document.getElementById('accent-color-text').value = e.target.value;
this.generatePreview();
});
document.getElementById('title-color').addEventListener('input', (e) => {
this.templateSettings.titleColor = e.target.value;
document.getElementById('title-color-text').value = e.target.value;
this.generatePreview();
});
// Checkboxes
document.getElementById('full-bleed').addEventListener('change', (e) => {
this.templateSettings.fullBleed = e.target.checked;
this.generatePreview();
});
document.getElementById('title-shadow').addEventListener('change', (e) => {
this.templateSettings.titleShadow = e.target.checked;
this.generatePreview();
});
// Image upload
const imageUpload = document.getElementById('image-upload');
const fileInputLabel = document.querySelector('.file-input-label');
imageUpload.addEventListener('change', (e) => this.handleImageUpload(e.target.files));
imageUpload.addEventListener('dragover', (e) => {
e.preventDefault();
fileInputLabel.classList.add('dragover');
});
imageUpload.addEventListener('dragleave', (e) => {
e.preventDefault();
fileInputLabel.classList.remove('dragover');
});
imageUpload.addEventListener('drop', (e) => {
e.preventDefault();
fileInputLabel.classList.remove('dragover');
this.handleImageUpload(e.dataTransfer.files);
});
// Image management
document.getElementById('clear-images').addEventListener('click', () => this.clearAllImages());
// Actions
document.getElementById('generate-preview').addEventListener('click', () => this.generatePreview());
document.getElementById('download-html').addEventListener('click', () => this.downloadHTML());
document.getElementById('open-preview').addEventListener('click', () => this.openPreviewInNewTab());
document.getElementById('print-preview').addEventListener('click', () => this.printPreview());
// Zoom controls
document.getElementById('zoom-in').addEventListener('click', () => this.zoomIn());
document.getElementById('zoom-out').addEventListener('click', () => this.zoomOut());
document.getElementById('zoom-reset').addEventListener('click', () => this.resetZoom());
// Image editor
document.getElementById('fit-mode').addEventListener('change', (e) => {
if (this.selectedImageIndex >= 0) {
this.images[this.selectedImageIndex].fitMode = e.target.value;
this.generatePreview();
}
});
document.getElementById('crop-position').addEventListener('change', (e) => {
if (this.selectedImageIndex >= 0) {
this.images[this.selectedImageIndex].cropPosition = e.target.value;
this.generatePreview();
}
});
document.getElementById('crop-offset-x').addEventListener('input', (e) => {
if (this.selectedImageIndex >= 0) {
this.images[this.selectedImageIndex].cropOffsetX = parseFloat(e.target.value);
document.getElementById('crop-offset-x-value').textContent = e.target.value;
this.generatePreview();
}
});
document.getElementById('crop-offset-y').addEventListener('input', (e) => {
if (this.selectedImageIndex >= 0) {
this.images[this.selectedImageIndex].cropOffsetY = parseFloat(e.target.value);
document.getElementById('crop-offset-y-value').textContent = e.target.value;
this.generatePreview();
}
});
document.getElementById('add-text-overlay').addEventListener('click', () => this.addTextOverlay());
document.getElementById('add-card-variation').addEventListener('click', () => this.addCardVariation());
// Mobile toggles
document.getElementById('toggle-settings').addEventListener('click', () => {
const panel = document.querySelector('.settings-panel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
});
document.getElementById('toggle-editor').addEventListener('click', () => {
const panel = document.querySelector('.edit-panel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
});
}
async handleImageUpload(files) {
for (const file of files) {
if (file.type.startsWith('image/')) {
try {
const imageData = await this.readFileAsDataURL(file);
const coverImage = new CoverImage(file.name, imageData);
this.images.push(coverImage);
} catch (error) {
this.showStatusMessage(`Error loading ${file.name}: ${error.message}`, 'error');
}
}
}
this.updateImageList();
this.updateCardVariations();
this.generatePreview();
}
readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsDataURL(file);
});
}
updateImageList() {
const imageList = document.getElementById('image-list');
imageList.innerHTML = '';
this.images.forEach((image, index) => {
const item = document.createElement('div');
item.className = 'image-item';
item.innerHTML = `
${image.name}
${image.width || 0} × ${image.height || 0}px
`;
imageList.appendChild(item);
});
}
selectImage(index) {
this.selectedImageIndex = index;
this.updateImageEditor();
}
removeImage(index) {
this.images.splice(index, 1);
if (this.selectedImageIndex === index) {
this.selectedImageIndex = -1;
} else if (this.selectedImageIndex > index) {
this.selectedImageIndex--;
}
this.updateImageList();
this.updateImageEditor();
this.generatePreview();
}
clearAllImages() {
this.images = [];
this.selectedImageIndex = -1;
this.updateImageList();
this.updateImageEditor();
this.updateCardVariations();
this.generatePreview();
}
updateImageEditor() {
const editor = document.getElementById('image-editor');
const noSelection = document.getElementById('no-image-selected');
if (this.selectedImageIndex >= 0) {
const image = this.images[this.selectedImageIndex];
editor.style.display = 'block';
noSelection.style.display = 'none';
document.getElementById('fit-mode').value = image.fitMode;
document.getElementById('crop-position').value = image.cropPosition;
document.getElementById('crop-offset-x').value = image.cropOffsetX;
document.getElementById('crop-offset-x-value').textContent = image.cropOffsetX;
document.getElementById('crop-offset-y').value = image.cropOffsetY;
document.getElementById('crop-offset-y-value').textContent = image.cropOffsetY;
this.updateTextOverlays();
} else {
editor.style.display = 'none';
noSelection.style.display = 'block';
}
}
updateTextOverlays() {
const container = document.getElementById('text-overlays');
container.innerHTML = '';
if (this.selectedImageIndex >= 0) {
const image = this.images[this.selectedImageIndex];
image.textOverlays.forEach((overlay, index) => {
const item = document.createElement('div');
item.className = 'text-overlay-item';
item.innerHTML = `
`;
container.appendChild(item);
});
}
}
addTextOverlay() {
if (this.selectedImageIndex >= 0) {
const image = this.images[this.selectedImageIndex];
image.textOverlays.push({
text: 'New Text',
color: '#ffffff',
fontSize: 24,
x: 50,
y: 50
});
this.updateTextOverlays();
this.generatePreview();
}
}
updateTextOverlay(index, property, value) {
if (this.selectedImageIndex >= 0) {
const image = this.images[this.selectedImageIndex];
if (property === 'fontSize') {
value = parseInt(value);
}
image.textOverlays[index][property] = value;
this.generatePreview();
}
}
removeTextOverlay(index) {
if (this.selectedImageIndex >= 0) {
const image = this.images[this.selectedImageIndex];
image.textOverlays.splice(index, 1);
this.updateTextOverlays();
this.generatePreview();
}
}
addCardVariation() {
const variation = {
title: '',
footer: '',
imageIndex: -1
};
this.cardVariations.push(variation);
this.updateCardVariations();
this.generatePreview();
}
updateCardVariation(index, field, value) {
this.cardVariations[index][field] = value;
this.generatePreview();
}
removeCardVariation(index) {
this.cardVariations.splice(index, 1);
this.updateCardVariations();
this.generatePreview();
}
updateCardVariations() {
const container = document.getElementById('card-variations');
container.innerHTML = '';
this.cardVariations.forEach((variation, index) => {
const item = document.createElement('div');
item.className = 'card-variation-item';
item.innerHTML = `
Card ${index + 1}:
`;
container.appendChild(item);
});
}
async generatePreview() {
const placeholder = document.getElementById('preview-placeholder');
const iframe = document.getElementById('preview-iframe');
if (this.images.length === 0 && !this.templateSettings.title) {
placeholder.textContent = 'Add an image or enter a title to generate preview';
placeholder.style.display = 'flex';
iframe.style.display = 'none';
return;
}
placeholder.innerHTML = ' Generating preview...';
placeholder.style.display = 'flex';
iframe.style.display = 'none';
try {
const html = await this.generateHTML();
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
iframe.src = url;
iframe.style.display = 'block';
placeholder.style.display = 'none';
// Apply zoom after iframe loads
iframe.onload = () => {
this.applyZoomToPreview();
};
// Clean up the previous blob URL
if (iframe.dataset.previousUrl) {
URL.revokeObjectURL(iframe.dataset.previousUrl);
}
iframe.dataset.previousUrl = url;
} catch (error) {
placeholder.textContent = `Error generating preview: ${error.message}`;
this.showStatusMessage(`Preview generation failed: ${error.message}`, 'error');
}
}
async generateHTML() {
// Determine number of cards to generate
let totalCards;
if (this.cardVariations.length > 0) {
totalCards = this.cardVariations.length;
} else {
totalCards = 1; // Single card if no variations
}
// For print sheet, arrange cards in grid, but don't exceed available space
const cardsPerPage = this.printLayout.cardsPerRow * this.printLayout.cardsPerColumn;
const pagesNeeded = Math.ceil(totalCards / cardsPerPage);
const totalCardsToGenerate = pagesNeeded * cardsPerPage; // Fill pages completely, but we'll hide empty slots
const cards = [];
// Generate cards
for (let i = 0; i < totalCardsToGenerate; i++) {
if (i < totalCards) {
// Use variation or default
const variation = this.cardVariations[i];
const imageIndex = variation && variation.imageIndex >= 0 ? variation.imageIndex : 0;
const imageUrl = this.images.length > 0 ? this.images[imageIndex].dataUrl : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
cards.push({
imageUrl,
title: (variation && variation.title) || this.templateSettings.title || `Card ${i + 1}`,
footer: (variation && variation.footer) || this.templateSettings.footer,
isVisible: true
});
} else {
// Empty card for filling the grid
cards.push({
imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
title: '',
footer: '',
isVisible: false
});
}
}
// Fixed card dimensions: 54mm x 85.6mm
const cardWidthMm = 54;
const cardHeightMm = 85.6;
// Generate HTML for print sheet
const html = `
Yoto Covers Print Sheet
${cards.map((card, index) => `
${this.escapeHtml(card.title)}
${!this.templateSettings.fullBleed ? `
` : ''}
`).join('')}
`;
return html;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
zoomIn() {
this.zoomLevel = Math.min(this.zoomLevel * 1.2, 3.0);
this.updateZoomDisplay();
this.applyZoomToPreview();
}
zoomOut() {
this.zoomLevel = Math.max(this.zoomLevel / 1.2, 0.3);
this.updateZoomDisplay();
this.applyZoomToPreview();
}
resetZoom() {
this.zoomLevel = 1.0;
this.updateZoomDisplay();
this.applyZoomToPreview();
}
updateZoomDisplay() {
const zoomPercent = Math.round(this.zoomLevel * 100);
document.getElementById('zoom-level').textContent = zoomPercent === 100 ? 'Fit' : zoomPercent + '%';
}
applyZoomToPreview() {
const iframe = document.getElementById('preview-iframe');
if (iframe.contentDocument && iframe.contentDocument.body) {
const printSheet = iframe.contentDocument.querySelector('.print-sheet');
if (printSheet) {
printSheet.style.transform = `scale(${this.zoomLevel})`;
printSheet.style.transformOrigin = 'top center';
}
}
}
openPreviewInNewTab() {
const iframe = document.getElementById('preview-iframe');
if (iframe.src) {
window.open(iframe.src, '_blank');
} else {
this.showStatusMessage('No preview available. Generate a preview first.', 'error');
}
}
printPreview() {
const iframe = document.getElementById('preview-iframe');
if (iframe.contentWindow) {
iframe.contentWindow.print();
} else {
this.showStatusMessage('No preview available to print.', 'error');
}
}
downloadHTML() {
if (!this.templateSettings.title && this.images.length === 0) {
this.showStatusMessage('Add content before downloading.', 'error');
return;
}
this.generateHTML().then(html => {
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `yoto-cover-${Date.now()}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showStatusMessage('HTML file downloaded successfully!', 'success');
}).catch(error => {
this.showStatusMessage(`Download failed: ${error.message}`, 'error');
});
}
showStatusMessage(message, type = 'info') {
const statusDiv = document.getElementById('status-message');
statusDiv.textContent = message;
statusDiv.className = `status-message status-${type}`;
statusDiv.style.display = 'block';
setTimeout(() => {
statusDiv.style.display = 'none';
}, 5000);
}
updateUI() {
this.updateZoomDisplay();
// Initialize print layout controls
document.getElementById('cards-per-row').value = this.printLayout.cardsPerRow;
document.getElementById('cards-per-column').value = this.printLayout.cardsPerColumn;
document.getElementById('page-margin').value = this.printLayout.pageMargin;
document.getElementById('card-spacing').value = this.printLayout.cardSpacing;
}
}
class CoverImage {
constructor(name, dataUrl) {
this.name = name;
this.dataUrl = dataUrl;
this.fitMode = 'scale';
this.cropPosition = 'center';
this.cropOffsetX = 0;
this.cropOffsetY = 0;
this.textOverlays = [];
this.width = 0;
this.height = 0;
// Get image dimensions
this.getImageDimensions();
}
async getImageDimensions() {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
this.width = img.naturalWidth;
this.height = img.naturalHeight;
resolve();
};
img.src = this.dataUrl;
});
}
}
// Initialize the app
const app = new YotoCoversApp();