This commit is contained in:
2025-09-27 17:07:58 +01:00
commit bfdcee8602
2663 changed files with 517832 additions and 0 deletions

199
static/js/donate.js Normal file
View File

@@ -0,0 +1,199 @@
(async () => {
const cfg = await fetch("/config").then(r => r.json());
const stripe = Stripe(cfg.publishableKey);
const form = document.getElementById("donation-form");
const result = document.getElementById("result");
const amountInput = document.getElementById("amount");
const currencyInput = document.getElementById("currency");
let elements, expressCheckoutElement, cardElement, paymentIntent;
// Initialize payment elements
async function initializePayment() {
const amount = parseFloat(amountInput.value) || 10;
const currency = currencyInput.value;
const supporterName = document.getElementById('supporter-name').value;
// Create payment intent
const res = await fetch("/create-payment-intent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount, currency, supporterName })
});
const { clientSecret } = await res.json();
// Create elements with appearance customization
elements = stripe.elements({
clientSecret,
appearance: {
theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'night' : 'stripe'
}
});
// Create and mount express checkout element (Google Pay, Apple Pay, etc.)
expressCheckoutElement = elements.create("expressCheckout", {
buttonType: {
googlePay: "donate",
applePay: "donate"
},
paymentMethods: {
link: "never" // Disable Link payment method
}
});
expressCheckoutElement.mount("#express-checkout-element");
// Listen for express checkout readiness
expressCheckoutElement.on('ready', (event) => {
const paymentDivider = document.querySelector('.payment-divider');
if (event.availablePaymentMethods && Object.keys(event.availablePaymentMethods).length > 0) {
// Express methods are available, show the divider
paymentDivider.style.display = 'block';
} else {
// No express methods available, hide the divider on desktop but keep on mobile
if (window.innerWidth <= 768) {
paymentDivider.style.display = 'block';
paymentDivider.querySelector('span').textContent = 'Pay with card';
} else {
paymentDivider.style.display = 'none';
}
}
});
// Create and mount card element
cardElement = elements.create("card", {
style: {
base: {
fontSize: '16px',
color: getComputedStyle(document.documentElement).getPropertyValue('--text-primary'),
'::placeholder': {
color: getComputedStyle(document.documentElement).getPropertyValue('--text-secondary'),
},
},
}
});
cardElement.mount("#card-element");
return clientSecret;
}
// Handle express checkout (Google Pay, Apple Pay, etc.)
async function handleExpressPayment() {
result.textContent = "Processing...";
try {
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: window.location.href,
},
redirect: "if_required"
});
if (error) {
result.textContent = "❌ " + error.message;
} else {
result.textContent = "✅ Thank you! Support received successfully.";
showSuccessAnimation();
addToSupporterWall();
}
} catch (err) {
result.textContent = "❌ Payment failed. Please try again.";
}
}
// Handle card payment
async function handleCardPayment(e) {
e.preventDefault();
result.textContent = "Processing...";
try {
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: window.location.href,
},
redirect: "if_required"
});
if (error) {
result.textContent = "❌ " + error.message;
} else {
result.textContent = "✅ Thank you! Support received successfully.";
showSuccessAnimation();
addToSupporterWall();
}
} catch (err) {
result.textContent = "❌ Payment failed. Please try again.";
}
}
// Success animation
function showSuccessAnimation() {
result.style.background = 'linear-gradient(135deg, #28a745, #20c997)';
result.style.color = 'white';
result.style.padding = '12px';
result.style.borderRadius = '8px';
result.style.animation = 'bounce 0.6s ease-out';
}
// Add to supporter wall
async function addToSupporterWall() {
const supporterName = document.getElementById('supporter-name').value.trim();
const amount = parseFloat(amountInput.value) || 0;
const currency = currencyInput.value;
if (supporterName && window.supporterWall) {
await window.supporterWall.addSupporter(supporterName, amount * 100, currency);
}
}
// Reinitialize when amount or currency changes
async function reinitialize() {
if (elements) {
elements.destroy();
}
const amount = parseFloat(amountInput.value);
if (amount && amount > 0) {
await initializePayment();
// Set up event listeners
if (expressCheckoutElement) {
expressCheckoutElement.on('click', handleExpressPayment);
}
}
}
// Event listeners
form.addEventListener("submit", handleCardPayment);
amountInput.addEventListener("blur", reinitialize);
currencyInput.addEventListener("change", reinitialize);
// Initialize on page load
await initializePayment();
// Set up express checkout event listener
if (expressCheckoutElement) {
expressCheckoutElement.on('click', handleExpressPayment);
}
// Handle responsive payment divider visibility
window.addEventListener('resize', () => {
const paymentDivider = document.querySelector('.payment-divider');
if (window.innerWidth <= 768) {
// Always show on mobile
paymentDivider.style.display = 'block';
}
});
// Update elements theme when theme changes
if (window.themeManager) {
const originalSetTheme = window.themeManager.setTheme;
window.themeManager.setTheme = function(theme) {
originalSetTheme.call(this, theme);
// Reinitialize elements with new theme
setTimeout(reinitialize, 100);
};
}
})();

188
static/js/logo.js Normal file
View File

@@ -0,0 +1,188 @@
// Draggable Logo with Click-to-Navigate Functionality
class DraggableLogo {
constructor() {
this.logo = document.querySelector('.logo');
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
this.position = { x: 0, y: 0 };
this.clickStartTime = 0;
this.startPosition = { x: 0, y: 0 };
this.clickThreshold = 200; // ms
this.website = 'https://computertech.dev';
if (this.logo) {
this.init();
}
}
init() {
this.loadPosition();
this.setupEventListeners();
}
setupEventListeners() {
// Mouse events
this.logo.addEventListener('mousedown', this.handleStart.bind(this));
document.addEventListener('mousemove', this.handleMove.bind(this));
document.addEventListener('mouseup', this.handleEnd.bind(this));
// Touch events for mobile
this.logo.addEventListener('touchstart', this.handleStart.bind(this), { passive: false });
document.addEventListener('touchmove', this.handleMove.bind(this), { passive: false });
this.logo.addEventListener('touchend', this.handleEnd.bind(this));
// Prevent context menu
this.logo.addEventListener('contextmenu', (e) => e.preventDefault());
// Double-click to snap to corner
this.logo.addEventListener('dblclick', (e) => {
e.preventDefault();
this.snapToNearestCorner();
});
// Click handler (fallback)
this.logo.addEventListener('click', (e) => {
if (!this.isDragging) {
e.preventDefault();
window.open(this.website, '_blank');
}
});
}
handleStart(e) {
e.preventDefault();
this.clickStartTime = Date.now();
this.isDragging = false;
this.startPosition = { x: 0, y: 0 };
const rect = this.logo.getBoundingClientRect();
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
this.dragOffset.x = clientX - rect.left;
this.dragOffset.y = clientY - rect.top;
this.startPosition.x = clientX;
this.startPosition.y = clientY;
this.logo.classList.add('dragging');
this.logo.classList.remove('clickable');
}
handleMove(e) {
if (!this.logo.classList.contains('dragging')) return;
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
const deltaX = Math.abs(clientX - this.startPosition.x);
const deltaY = Math.abs(clientY - this.startPosition.y);
const dragThreshold = 5;
if (deltaX > dragThreshold || deltaY > dragThreshold) {
this.isDragging = true;
e.preventDefault();
this.position.x = clientX - this.dragOffset.x;
this.position.y = clientY - this.dragOffset.y;
// Keep within viewport bounds
const logoRect = this.logo.getBoundingClientRect();
const margin = 10;
this.position.x = Math.max(margin, Math.min(window.innerWidth - logoRect.width - margin, this.position.x));
this.position.y = Math.max(margin, Math.min(window.innerHeight - logoRect.height - margin, this.position.y));
this.logo.style.left = this.position.x + 'px';
this.logo.style.top = this.position.y + 'px';
}
}
handleEnd(e) {
this.logo.classList.remove('dragging');
const clickDuration = Date.now() - this.clickStartTime;
if (!this.isDragging && clickDuration < this.clickThreshold) {
// It was a click, navigate to website
this.logo.classList.add('clickable');
setTimeout(() => {
window.open(this.website, '_blank');
}, 100);
} else if (this.isDragging) {
// It was a drag, save position
this.savePosition();
this.addBounceEffect();
}
// Reset dragging state with small delay
setTimeout(() => {
this.isDragging = false;
this.logo.classList.remove('clickable');
}, 50);
}
savePosition() {
localStorage.setItem('logoPosition', JSON.stringify(this.position));
// Update CSS variables for immediate positioning on reload
document.documentElement.style.setProperty('--logo-x', this.position.x + 'px');
document.documentElement.style.setProperty('--logo-y', this.position.y + 'px');
}
loadPosition() {
const savedPosition = localStorage.getItem('logoPosition');
if (savedPosition) {
this.position = JSON.parse(savedPosition);
this.logo.style.left = this.position.x + 'px';
this.logo.style.top = this.position.y + 'px';
}
}
snapToNearestCorner() {
const logoRect = this.logo.getBoundingClientRect();
const margin = 20;
const corners = [
{ x: margin, y: margin },
{ x: window.innerWidth - logoRect.width - margin, y: margin },
{ x: margin, y: window.innerHeight - logoRect.height - margin },
{ x: window.innerWidth - logoRect.width - margin, y: window.innerHeight - logoRect.height - margin }
];
let nearestCorner = corners[0];
let minDistance = Infinity;
corners.forEach(corner => {
const distance = Math.sqrt(
Math.pow(corner.x - this.position.x, 2) +
Math.pow(corner.y - this.position.y, 2)
);
if (distance < minDistance) {
minDistance = distance;
nearestCorner = corner;
}
});
this.position = nearestCorner;
this.logo.style.transition = 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
this.logo.style.left = nearestCorner.x + 'px';
this.logo.style.top = nearestCorner.y + 'px';
setTimeout(() => {
this.logo.style.transition = '';
this.savePosition();
}, 500);
}
addBounceEffect() {
this.logo.style.animation = 'bounce 0.6s ease-out';
setTimeout(() => {
this.logo.style.animation = '';
}, 600);
}
}
// Initialize draggable logo when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.draggableLogo = new DraggableLogo();
});

142
static/js/supporters.js Normal file
View File

@@ -0,0 +1,142 @@
// Supporter Wall Functionality
class SupporterWall {
constructor() {
this.supportersList = document.getElementById('supporters-list');
this.supporterWall = document.getElementById('supporter-wall');
this.supporters = [];
if (this.supportersList) {
this.init();
}
}
init() {
this.loadSupporters();
// Refresh supporters every 30 seconds
setInterval(() => this.loadSupporters(), 30000);
}
async loadSupporters() {
try {
const response = await fetch('/supporters');
const data = await response.json();
if (data.supporters && data.supporters.length > 0) {
this.updateSupportersList(data.supporters);
} else {
this.showEmptyState();
}
} catch (error) {
console.error('Error loading supporters:', error);
this.showEmptyState();
}
}
updateSupportersList(newSupporters) {
// Clear loading message
this.supportersList.innerHTML = '';
// Check for new supporters since last update
const newSupporterIds = newSupporters.map(s => s.timestamp);
const currentSupporterIds = this.supporters.map(s => s.timestamp);
const hasNewSupporters = newSupporterIds.some(id => !currentSupporterIds.includes(id));
this.supporters = newSupporters;
// Create supporter elements
newSupporters.forEach((supporter, index) => {
const supporterElement = this.createSupporterElement(supporter);
// Add 'new' class to recent supporters
if (hasNewSupporters && index === 0 && supporter.time_ago === 'just now') {
supporterElement.classList.add('new');
setTimeout(() => supporterElement.classList.remove('new'), 2000);
}
this.supportersList.appendChild(supporterElement);
});
}
createSupporterElement(supporter) {
const element = document.createElement('div');
element.className = 'supporter-item';
const currencySymbols = {
'GBP': '£',
'EUR': '€',
'USD': '$'
};
const symbol = currencySymbols[supporter.currency] || supporter.currency;
const amount = (supporter.amount / 100).toFixed(2);
element.innerHTML = `
<span class="name">${this.escapeHtml(supporter.name)}</span>
<span class="time">${supporter.time_ago}</span>
<div style="clear: both; margin-top: 2px; font-size: 11px; color: var(--text-secondary);">
${symbol}${amount}
</div>
`;
return element;
}
showEmptyState() {
this.supportersList.innerHTML = `
<div class="supporter-item" style="text-align: center; color: var(--text-secondary);">
Be the first to support! 💜
</div>
`;
}
async addSupporter(name, amount, currency) {
if (!name || name.trim() === '') return;
try {
const response = await fetch('/add-supporter', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name.trim(),
amount: amount,
currency: currency
})
});
const result = await response.json();
if (result.success) {
// Reload supporters to show the new one
setTimeout(() => this.loadSupporters(), 1000);
return true;
}
} catch (error) {
console.error('Error adding supporter:', error);
}
return false;
}
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Method to hide/show supporter wall
toggle() {
if (this.supporterWall) {
this.supporterWall.style.display =
this.supporterWall.style.display === 'none' ? 'block' : 'none';
}
}
}
// Initialize supporter wall when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.supporterWall = new SupporterWall();
});

266
static/js/theme.js Normal file
View File

@@ -0,0 +1,266 @@
// Theme Management with Draggable Fun Button
class ThemeManager {
constructor() {
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
this.position = { x: 0, y: 0 };
this.clickStartTime = 0;
this.clickThreshold = 200; // ms
this.init();
}
init() {
// Load saved theme or detect OS preference
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Set initial theme
if (savedTheme) {
this.setTheme(savedTheme);
} else if (prefersDark) {
this.setTheme('dark');
} else {
this.setTheme('light');
}
// Listen for OS theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
// Set up toggle button with drag functionality
this.setupToggleButton();
this.loadPosition();
}
setTheme(theme) {
// Add smooth transition class temporarily
document.body.classList.add('theme-transitioning');
document.documentElement.setAttribute('data-theme', theme);
this.updateToggleButton(theme);
this.addPulseEffect();
// Remove transition class after animation completes
setTimeout(() => {
document.body.classList.remove('theme-transitioning');
}, 400);
}
toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
this.setTheme(newTheme);
localStorage.setItem('theme', newTheme);
this.addBounceEffect();
}
setupToggleButton() {
const toggleButton = document.getElementById('theme-toggle');
if (!toggleButton) return;
// Simple click handler as fallback
toggleButton.addEventListener('click', (e) => {
// Only handle click if we haven't detected dragging
if (!this.isDragging) {
e.preventDefault();
e.stopPropagation();
this.toggleTheme();
}
});
// Mouse events for drag functionality
toggleButton.addEventListener('mousedown', this.handleStart.bind(this));
document.addEventListener('mousemove', this.handleMove.bind(this));
document.addEventListener('mouseup', this.handleEnd.bind(this));
// Touch events for mobile
toggleButton.addEventListener('touchstart', this.handleStart.bind(this), { passive: false });
document.addEventListener('touchmove', this.handleMove.bind(this), { passive: false });
document.addEventListener('touchend', this.handleEnd.bind(this));
// Prevent context menu on long press
toggleButton.addEventListener('contextmenu', (e) => e.preventDefault());
// Double-click for fun bounce effect
toggleButton.addEventListener('dblclick', (e) => {
e.preventDefault();
this.addBounceEffect();
this.snapToNearestCorner();
});
}
handleStart(e) {
e.preventDefault();
this.clickStartTime = Date.now();
this.isDragging = false;
this.startPosition = { x: 0, y: 0 };
const toggleButton = document.getElementById('theme-toggle');
const rect = toggleButton.getBoundingClientRect();
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
this.dragOffset.x = clientX - rect.left;
this.dragOffset.y = clientY - rect.top;
this.startPosition.x = clientX;
this.startPosition.y = clientY;
toggleButton.classList.add('dragging');
}
handleMove(e) {
const toggleButton = document.getElementById('theme-toggle');
if (!toggleButton.classList.contains('dragging')) return;
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
// Calculate distance moved from start position
const deltaX = Math.abs(clientX - this.startPosition.x);
const deltaY = Math.abs(clientY - this.startPosition.y);
const dragThreshold = 5; // pixels
// Only start dragging if moved more than threshold
if (deltaX > dragThreshold || deltaY > dragThreshold) {
this.isDragging = true;
e.preventDefault();
this.position.x = clientX - this.dragOffset.x;
this.position.y = clientY - this.dragOffset.y;
// Keep button within viewport bounds
const buttonSize = 60;
const margin = 10;
this.position.x = Math.max(margin, Math.min(window.innerWidth - buttonSize - margin, this.position.x));
this.position.y = Math.max(margin, Math.min(window.innerHeight - buttonSize - margin, this.position.y));
toggleButton.style.left = this.position.x + 'px';
toggleButton.style.top = this.position.y + 'px';
toggleButton.style.right = 'auto';
}
}
handleEnd(e) {
const toggleButton = document.getElementById('theme-toggle');
toggleButton.classList.remove('dragging');
if (this.isDragging) {
// It was a drag, save position and add bounce effect
this.savePosition();
this.addBounceEffect();
// Prevent click event from firing after drag
setTimeout(() => {
this.isDragging = false;
}, 50);
} else {
this.isDragging = false;
}
}
savePosition() {
localStorage.setItem('themeButtonPosition', JSON.stringify(this.position));
// Update CSS variables for immediate positioning on reload
document.documentElement.style.setProperty('--theme-button-x', this.position.x + 'px');
document.documentElement.style.setProperty('--theme-button-y', this.position.y + 'px');
const toggleButton = document.getElementById('theme-toggle');
if (toggleButton) {
toggleButton.classList.add('positioned');
}
}
loadPosition() {
const savedPosition = localStorage.getItem('themeButtonPosition');
if (savedPosition) {
this.position = JSON.parse(savedPosition);
const toggleButton = document.getElementById('theme-toggle');
if (toggleButton) {
// Position is already set by CSS variables, just ensure the class is added
toggleButton.classList.add('positioned');
toggleButton.style.left = this.position.x + 'px';
toggleButton.style.top = this.position.y + 'px';
toggleButton.style.right = 'auto';
}
}
}
snapToNearestCorner() {
const toggleButton = document.getElementById('theme-toggle');
const buttonSize = 60;
const margin = 20;
const corners = [
{ x: margin, y: margin }, // top-left
{ x: window.innerWidth - buttonSize - margin, y: margin }, // top-right
{ x: margin, y: window.innerHeight - buttonSize - margin }, // bottom-left
{ x: window.innerWidth - buttonSize - margin, y: window.innerHeight - buttonSize - margin } // bottom-right
];
// Find nearest corner
let nearestCorner = corners[0];
let minDistance = Infinity;
corners.forEach(corner => {
const distance = Math.sqrt(
Math.pow(corner.x - this.position.x, 2) +
Math.pow(corner.y - this.position.y, 2)
);
if (distance < minDistance) {
minDistance = distance;
nearestCorner = corner;
}
});
// Animate to nearest corner
this.position = nearestCorner;
toggleButton.style.transition = 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
toggleButton.style.left = nearestCorner.x + 'px';
toggleButton.style.top = nearestCorner.y + 'px';
setTimeout(() => {
toggleButton.style.transition = '';
this.savePosition();
}, 500);
}
addBounceEffect() {
const toggleButton = document.getElementById('theme-toggle');
toggleButton.classList.remove('bounce');
// Trigger reflow
toggleButton.offsetHeight;
toggleButton.classList.add('bounce');
setTimeout(() => toggleButton.classList.remove('bounce'), 600);
}
addPulseEffect() {
const toggleButton = document.getElementById('theme-toggle');
toggleButton.classList.remove('pulse');
// Trigger reflow
toggleButton.offsetHeight;
toggleButton.classList.add('pulse');
setTimeout(() => toggleButton.classList.remove('pulse'), 1000);
}
updateToggleButton(theme) {
const toggleButton = document.getElementById('theme-toggle');
if (toggleButton) {
toggleButton.innerHTML = theme === 'dark' ? '☀️' : '🌙';
toggleButton.setAttribute('aria-label', `Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`);
toggleButton.setAttribute('title', `Click to switch to ${theme === 'dark' ? 'light' : 'dark'} mode • Drag to move • Double-click to snap to corner`);
}
}
getCurrentTheme() {
return document.documentElement.getAttribute('data-theme');
}
}
// Initialize theme manager when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.themeManager = new ThemeManager();
});

295
static/js/typewriter.js Normal file
View File

@@ -0,0 +1,295 @@
// Typewriter Effect for Info Box with Draggable Functionality
class TypewriterEffect {
constructor(elementId, messages) {
this.element = document.getElementById(elementId);
this.infoBox = this.element ? this.element.closest('.info-box') : null;
this.messages = messages;
this.currentMessageIndex = 0;
this.currentCharIndex = 0;
this.isTyping = false;
this.isDeleting = false;
this.typingSpeed = 80; // ms per character
this.deletingSpeed = 40; // ms per character when deleting
this.pauseTime = 1500; // ms to pause at end of message
this.deleteDelay = 800; // ms to wait before starting to delete
// Draggable properties
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
this.position = { x: 0, y: 0 };
this.clickStartTime = 0;
this.startPosition = { x: 0, y: 0 };
if (this.element) {
this.init();
this.setupDraggable();
}
}
init() {
// Clear initial content and start typing
this.element.textContent = '';
this.loadPosition();
setTimeout(() => this.type(), 500); // Small delay before starting
}
setupDraggable() {
if (!this.infoBox) return;
// Mouse events
this.infoBox.addEventListener('mousedown', this.handleStart.bind(this));
document.addEventListener('mousemove', this.handleMove.bind(this));
document.addEventListener('mouseup', this.handleEnd.bind(this));
// Touch events for mobile
this.infoBox.addEventListener('touchstart', this.handleStart.bind(this), { passive: false });
document.addEventListener('touchmove', this.handleMove.bind(this), { passive: false });
document.addEventListener('touchend', this.handleEnd.bind(this));
// Prevent default drag behavior on images and links
this.infoBox.addEventListener('dragstart', (e) => e.preventDefault());
// Add visual feedback
this.infoBox.style.cursor = 'grab';
this.infoBox.style.userSelect = 'none';
this.infoBox.style.webkitUserSelect = 'none';
this.infoBox.style.mozUserSelect = 'none';
this.infoBox.style.msUserSelect = 'none';
}
handleStart(e) {
this.isDragging = true;
this.clickStartTime = Date.now();
const clientX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
const clientY = e.type === 'touchstart' ? e.touches[0].clientY : e.clientY;
this.startPosition = { x: clientX, y: clientY };
const rect = this.infoBox.getBoundingClientRect();
this.dragOffset = {
x: clientX - rect.left,
y: clientY - rect.top
};
this.infoBox.style.cursor = 'grabbing';
this.infoBox.classList.add('dragging');
e.preventDefault();
}
handleMove(e) {
if (!this.isDragging) return;
e.preventDefault();
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY;
this.position = {
x: clientX - this.dragOffset.x,
y: clientY - this.dragOffset.y
};
// Constrain to viewport
const maxX = window.innerWidth - this.infoBox.offsetWidth;
const maxY = window.innerHeight - this.infoBox.offsetHeight;
this.position.x = Math.max(0, Math.min(this.position.x, maxX));
this.position.y = Math.max(0, Math.min(this.position.y, maxY));
this.updatePosition();
}
handleEnd(e) {
if (!this.isDragging) return;
this.isDragging = false;
this.infoBox.style.cursor = 'grab';
this.infoBox.classList.remove('dragging');
// Save position
this.savePosition();
// Check if it was a click vs drag
const clickDuration = Date.now() - this.clickStartTime;
const clientX = e.type === 'touchend' ? e.changedTouches[0].clientX : e.clientX;
const clientY = e.type === 'touchend' ? e.changedTouches[0].clientY : e.clientY;
const distance = Math.sqrt(
Math.pow(clientX - this.startPosition.x, 2) +
Math.pow(clientY - this.startPosition.y, 2)
);
// If it was a quick click with minimal movement, trigger click behavior
if (clickDuration < 300 && distance < 10) {
this.handleClick();
}
}
handleClick() {
// Skip to next message on click
this.nextMessage();
}
updatePosition() {
if (this.infoBox) {
this.infoBox.style.left = `${this.position.x}px`;
this.infoBox.style.top = `${this.position.y}px`;
}
}
savePosition() {
localStorage.setItem('typewriterPosition', JSON.stringify(this.position));
// Update CSS variables for immediate positioning on reload
document.documentElement.style.setProperty('--typewriter-x', this.position.x + 'px');
document.documentElement.style.setProperty('--typewriter-y', this.position.y + 'px');
}
loadPosition() {
const saved = localStorage.getItem('typewriterPosition');
if (saved && this.infoBox) {
try {
this.position = JSON.parse(saved);
// Ensure position is still valid for current viewport
const maxX = window.innerWidth - this.infoBox.offsetWidth;
const maxY = window.innerHeight - this.infoBox.offsetHeight;
this.position.x = Math.max(0, Math.min(this.position.x, maxX));
this.position.y = Math.max(0, Math.min(this.position.y, maxY));
this.updatePosition();
} catch (e) {
// If parsing fails, use default position
this.position = { x: 20, y: 110 };
}
} else {
this.position = { x: 20, y: 110 };
}
}
type() {
if (this.isDeleting) {
this.deleteText();
} else {
this.addText();
}
}
addText() {
if (this.currentCharIndex < this.messages[this.currentMessageIndex].length) {
const currentMessage = this.messages[this.currentMessageIndex];
this.element.textContent = currentMessage.substring(0, this.currentCharIndex + 1);
this.currentCharIndex++;
this.element.classList.add('typing');
setTimeout(() => this.type(), this.typingSpeed);
} else {
// Finished typing current message
this.element.classList.remove('typing');
this.element.classList.add('paused');
setTimeout(() => {
this.element.classList.remove('paused');
this.isDeleting = true;
setTimeout(() => this.type(), this.deleteDelay);
}, this.pauseTime);
}
}
deleteText() {
if (this.currentCharIndex > 0) {
const currentMessage = this.messages[this.currentMessageIndex];
this.element.textContent = currentMessage.substring(0, this.currentCharIndex - 1);
this.currentCharIndex--;
setTimeout(() => this.type(), this.deletingSpeed);
} else {
// Finished deleting, move to next message
this.isDeleting = false;
this.nextMessage();
setTimeout(() => this.type(), this.typingSpeed);
}
}
nextMessage() {
if (this.messages && this.messages.length > 0) {
this.currentMessageIndex = (this.currentMessageIndex + 1) % this.messages.length;
}
}
// Method to add new messages dynamically
addMessage(message) {
this.messages.push(message);
}
// Method to update messages array
updateMessages(newMessages) {
this.messages = newMessages;
this.currentMessageIndex = 0;
this.currentCharIndex = 0;
this.isDeleting = false;
}
// Method to pause/resume typing
pause() {
this.isPaused = true;
}
resume() {
this.isPaused = false;
this.type();
}
}
// Preload emojis to prevent loading issues
function preloadEmojis() {
const emojis = ['🇺🇸', '🇬🇧', '🇮🇪', '🇫🇷', '🇪🇸', '🇩🇪', '🇮🇹', '🇧🇷', '🇯🇵', '🇨🇳', '🇷🇺', '🇵🇱', '🇸🇪', '🇳🇱', '🇫🇮', '🇳🇴', '🇭🇺', '🇬🇷', '🇮🇱', '🇸🇦', '🇮🇳', '🇰🇷', '🇻🇳', '🇵🇭', '🇮🇩', '🇹🇭', '❤️'];
const testDiv = document.createElement('div');
testDiv.style.position = 'absolute';
testDiv.style.left = '-9999px';
testDiv.style.fontSize = '1px';
testDiv.innerHTML = emojis.join('');
document.body.appendChild(testDiv);
setTimeout(() => document.body.removeChild(testDiv), 100);
}
// Initialize typewriter when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// Preload emojis first
preloadEmojis();
// Array of multilingual "thanks" messages with flags
const messages = [
"🇺🇸🇬🇧 Thank you!",
"🇮🇪 Go raibh maith agat!",
"🇫🇷 Merci!",
"🇪🇸 ¡Gracias!",
"🇩🇪 Danke!",
"🇮🇹 Grazie!",
"🇧🇷 Obrigado!",
"🇯🇵 ありがとう!",
"🇨🇳 谢谢!",
"🇷🇺 Спасибо!",
"🇵🇱 Tak!",
"🇸🇪 Tack!",
"🇳🇱 Dank je!",
"🇫🇮 Kiitos!",
"🇳🇴 Takk!",
"🇵🇱 Dziękuję!",
"🇭🇺 Köszönöm!",
"🇬🇷 Ευχαριστώ!",
"🇮🇱 תודה!",
"🇸🇦 شكرا!",
"🇮🇳 धन्यवाद!",
"🇰🇷 감사합니다!",
"🇻🇳 Cảm ơn!",
"🇵🇭 Salamat!",
"🇮🇩 Terima kasih!",
"🇹🇭 ขอบคุณ!",
"❤️ Much love!"
];
// Initialize the typewriter effect with a small delay to ensure emojis are loaded
setTimeout(() => {
window.typewriter = new TypewriterEffect('typewriter-text', messages);
}, 200);
});