381 lines
20 KiB
JavaScript
381 lines
20 KiB
JavaScript
/* ===== Package detail modals ===== */
|
||
(function () {
|
||
var DETAILS = {
|
||
'first-spark': {
|
||
emoji: '🌱', name: 'The First Spark', price: 'From £250',
|
||
lead: 'A clean, professional first website to get your business online quickly and affordably.',
|
||
low: { amt: '£250', desc: 'A tidy 1–3 page, template-based site with your branding, a contact form and one round of revisions.' },
|
||
high: { amt: '~£450', desc: 'Up to 5 pages with light customisation, basic SEO setup and a second round of revisions.' },
|
||
specs: [
|
||
{ i: 'fa-file-lines', k: 'Pages', v: 'Up to 5' },
|
||
{ i: 'fa-rotate', k: 'Revisions', v: '2 rounds' },
|
||
{ i: 'fa-clock', k: 'Typical turnaround', v: '1–2 weeks' },
|
||
{ i: 'fa-wand-magic-sparkles', k: 'Platform', v: 'WordPress or Squarespace' }
|
||
]
|
||
},
|
||
'enchanted-workshop': {
|
||
emoji: '✨', name: 'The Enchanted Workshop', price: '£750 – £1,500',
|
||
lead: 'A custom-designed site for businesses ready to stand out and grow their audience.',
|
||
low: { amt: '£750', desc: '5–7 pages with a custom design, a blog or news section and social media links.' },
|
||
high: { amt: '£1,500', desc: 'Up to 10 pages with photo galleries, analytics and full search-engine optimisation.' },
|
||
specs: [
|
||
{ i: 'fa-file-lines', k: 'Pages', v: '5 – 10' },
|
||
{ i: 'fa-rotate', k: 'Revisions', v: '3 rounds' },
|
||
{ i: 'fa-clock', k: 'Typical turnaround', v: '2–4 weeks' },
|
||
{ i: 'fa-wand-magic-sparkles', k: 'Platform', v: 'WordPress or Squarespace' }
|
||
]
|
||
},
|
||
'arcane-forge': {
|
||
emoji: '🔮', name: 'The Arcane Forge', price: '£1,500 – £3,000',
|
||
lead: 'Advanced layouts and bespoke functionality for businesses with bigger ambitions.',
|
||
low: { amt: '£1,500', desc: '8–10 pages with advanced layouts, enhanced forms and a tailored design.' },
|
||
high: { amt: '£3,000', desc: '12+ pages with custom functionality, third-party integrations, performance tuning and training.' },
|
||
specs: [
|
||
{ i: 'fa-file-lines', k: 'Pages', v: '10+' },
|
||
{ i: 'fa-rotate', k: 'Revisions', v: 'Unlimited in scope' },
|
||
{ i: 'fa-clock', k: 'Typical turnaround', v: '3–5 weeks' },
|
||
{ i: 'fa-plug', k: 'Integrations', v: 'Included' }
|
||
]
|
||
},
|
||
'merchants-portal': {
|
||
emoji: '🛒', name: "The Merchant's Portal", price: '£3,000 – £5,000',
|
||
lead: 'Everything you need to open a fully working online shop.',
|
||
low: { amt: '£3,000', desc: 'Shopify or WooCommerce setup with up to ~50 products, payment gateways and shipping.' },
|
||
high: { amt: '£5,000', desc: 'Up to ~200 products with a custom store design, customer accounts and store-management training.' },
|
||
specs: [
|
||
{ i: 'fa-box', k: 'Products', v: 'Up to ~200' },
|
||
{ i: 'fa-credit-card', k: 'Payments', v: 'Gateways configured' },
|
||
{ i: 'fa-clock', k: 'Typical turnaround', v: '4–6 weeks' },
|
||
{ i: 'fa-store', k: 'Platform', v: 'Shopify / WooCommerce' }
|
||
]
|
||
},
|
||
'grand-emporium': {
|
||
emoji: '⚡', name: 'The Grand Emporium', price: '£5,000 – £7,500+',
|
||
lead: 'A digital flagship for established and high-volume retailers.',
|
||
low: { amt: '£5,000', desc: 'Multi-category store with subscription products and advanced customer accounts.' },
|
||
high: { amt: '£7,500+', desc: 'Automation and workflows, marketing integrations, bespoke functionality and full launch support.' },
|
||
specs: [
|
||
{ i: 'fa-box', k: 'Products', v: 'Unlimited' },
|
||
{ i: 'fa-robot', k: 'Automation', v: 'Workflows & marketing' },
|
||
{ i: 'fa-clock', k: 'Typical turnaround', v: '6–10 weeks' },
|
||
{ i: 'fa-rocket', k: 'Launch support', v: 'Included' }
|
||
]
|
||
},
|
||
'wizards-bench': {
|
||
emoji: '🛠️', name: "The Wizard's Bench", price: '£40 <span>/ hour</span>',
|
||
lead: 'Pay-as-you-go technical support for websites, email and office tech. Minimum one hour; remote sessions carry no call-out charge.',
|
||
specs: [
|
||
{ i: 'fa-bolt', k: 'What an hour covers', v: 'Fixes, updates, email & DNS tweaks' },
|
||
{ i: 'fa-hourglass-half', k: 'Minimum', v: '1 hour' },
|
||
{ i: 'fa-house-laptop', k: 'Remote', v: 'No call-out charge' },
|
||
{ i: 'fa-location-dot', k: 'On-site', v: 'Oxfordshire area' },
|
||
{ i: 'fa-file-invoice', k: 'Larger jobs', v: 'Quoted upfront' }
|
||
]
|
||
},
|
||
'minor-ward': {
|
||
emoji: '🛡️', name: 'Minor Ward', price: '£10 <span>/ month</span>',
|
||
lead: 'Essential upkeep to keep a simple WordPress site safe and current.',
|
||
specs: [
|
||
{ i: 'fa-arrows-rotate', k: 'Updates', v: 'WordPress core & plugins' },
|
||
{ i: 'fa-shield-halved', k: 'Security', v: 'Monitoring' },
|
||
{ i: 'fa-database', k: 'Backups', v: 'Monthly' },
|
||
{ i: 'fa-headset', k: 'Support time', v: 'Billed at bench rate' },
|
||
{ i: 'fa-circle-check', k: 'Best for', v: 'Brochure sites' }
|
||
]
|
||
},
|
||
'greater-ward': {
|
||
emoji: '🛡️', name: 'Greater Ward', price: '£25 <span>/ month</span>',
|
||
lead: 'Everything in Minor Ward, plus proactive checks and priority support for active business sites.',
|
||
specs: [
|
||
{ i: 'fa-stethoscope', k: 'Health checks', v: 'Monthly' },
|
||
{ i: 'fa-bolt', k: 'Support', v: 'Priority email' },
|
||
{ i: 'fa-gauge-high', k: 'Performance', v: 'Monitored' },
|
||
{ i: 'fa-clock', k: 'Support allowance', v: '~30 mins / month' },
|
||
{ i: 'fa-circle-check', k: 'Best for', v: 'Active business sites' }
|
||
]
|
||
},
|
||
'merchants-ward': {
|
||
emoji: '🛡️', name: "Merchant's Ward", price: '£50 <span>/ month</span>',
|
||
lead: 'Everything in Greater Ward, plus ecommerce-grade monitoring and maintenance for online stores.',
|
||
specs: [
|
||
{ i: 'fa-cart-shopping', k: 'Ecommerce', v: 'Store monitoring' },
|
||
{ i: 'fa-screwdriver-wrench', k: 'Maintenance', v: 'Store upkeep' },
|
||
{ i: 'fa-bolt', k: 'Response', v: 'Priority' },
|
||
{ i: 'fa-clock', k: 'Support allowance', v: '~1 hour / month' },
|
||
{ i: 'fa-circle-check', k: 'Best for', v: 'Online stores' }
|
||
]
|
||
},
|
||
'the-cantrip': {
|
||
emoji: '⚙️', name: 'The Cantrip', price: '£750 – £2,000',
|
||
lead: 'A focused web tool or mini-app, built to handle one job brilliantly.',
|
||
low: { amt: '£750', desc: 'A single-purpose tool with a form, a database and a simple admin view.' },
|
||
high: { amt: '£2,000', desc: 'A richer tool with several features, user logins and a tidy dashboard.' },
|
||
specs: [
|
||
{ i: 'fa-screwdriver-wrench', k: 'Type', v: 'Web app / tool' },
|
||
{ i: 'fa-user', k: 'Accounts', v: 'Optional logins' },
|
||
{ i: 'fa-clock', k: 'Typical turnaround', v: '2–5 weeks' },
|
||
{ i: 'fa-server', k: 'Hosting', v: 'Set up for you' }
|
||
]
|
||
},
|
||
'grand-working': {
|
||
emoji: '🏗️', name: 'The Grand Working', price: '£3,000 – £10,000+',
|
||
lead: 'A full custom web application, designed and built around your business.',
|
||
low: { amt: '£3,000', desc: 'A focused custom app with accounts, a database and your core features.' },
|
||
high: { amt: '£10,000+', desc: 'A larger platform with integrations, user roles and ongoing support.' },
|
||
specs: [
|
||
{ i: 'fa-gears', k: 'Type', v: 'Full custom app' },
|
||
{ i: 'fa-users-gear', k: 'Accounts', v: 'Roles & permissions' },
|
||
{ i: 'fa-plug', k: 'Integrations', v: 'APIs included' },
|
||
{ i: 'fa-clock', k: 'Typical turnaround', v: '6–12 weeks' },
|
||
{ i: 'fa-headset', k: 'Support', v: 'Available after launch' }
|
||
]
|
||
},
|
||
'sigil-workshop': {
|
||
emoji: '🎨', name: 'The Sigil Workshop', price: 'From £150',
|
||
lead: 'Logos, brand kits and graphics that make your business instantly recognisable.',
|
||
low: { amt: '£150', desc: 'A standalone logo with two concepts and the final files.' },
|
||
high: { amt: '~£900', desc: 'A full brand kit: logo suite, colour and font system, templates and stationery.' },
|
||
specs: [
|
||
{ i: 'fa-lightbulb', k: 'Concepts', v: '2 – 3' },
|
||
{ i: 'fa-rotate', k: 'Revisions', v: '3 rounds' },
|
||
{ i: 'fa-clock', k: 'Typical turnaround', v: '1–2 weeks' },
|
||
{ i: 'fa-file-export', k: 'Formats', v: 'Vector, web & print' }
|
||
]
|
||
},
|
||
'moving-picture': {
|
||
emoji: '🎬', name: 'The Moving Picture', price: 'From £350',
|
||
lead: 'Filming and editing for promos, events and social media.',
|
||
low: { amt: '£350', desc: 'A short edit from footage you provide, with titles and music.' },
|
||
high: { amt: '£1,500+', desc: 'A full filming day on location, plus edit, colour grade and social cut-downs.' },
|
||
specs: [
|
||
{ i: 'fa-video', k: 'Filming', v: 'Half or full day' },
|
||
{ i: 'fa-rotate', k: 'Revisions', v: '2 rounds' },
|
||
{ i: 'fa-clock', k: 'Typical turnaround', v: '1–3 weeks' },
|
||
{ i: 'fa-share-nodes', k: 'Delivery', v: 'Web & social formats' }
|
||
]
|
||
},
|
||
'third-dimension': {
|
||
emoji: '🧊', name: 'The Third Dimension', price: 'From £300',
|
||
lead: '3D models, product renders and animation for the web and video.',
|
||
low: { amt: '£300', desc: 'A single 3D model or a polished product render.' },
|
||
high: { amt: '£2,000+', desc: 'A set of models with animated sequences and high-resolution renders.' },
|
||
specs: [
|
||
{ i: 'fa-cube', k: 'Output', v: 'Models, renders, animation' },
|
||
{ i: 'fa-rotate', k: 'Revisions', v: '2 – 3 rounds' },
|
||
{ i: 'fa-clock', k: 'Typical turnaround', v: '1–4 weeks' },
|
||
{ i: 'fa-file-export', k: 'Formats', v: 'Web, video & print' }
|
||
]
|
||
},
|
||
'sonic-alchemy': {
|
||
emoji: '🎙️', name: 'Sonic Alchemy', price: 'From £200',
|
||
lead: 'Editing, mixing and mastering for podcasts, video and adverts.',
|
||
low: { amt: '£200', desc: 'Editing and cleanup of a single recording or podcast episode.' },
|
||
high: { amt: '£800+', desc: 'Full podcast or advert production with music, mixing and mastering.' },
|
||
specs: [
|
||
{ i: 'fa-sliders', k: 'Services', v: 'Edit, mix, master' },
|
||
{ i: 'fa-rotate', k: 'Revisions', v: '2 rounds' },
|
||
{ i: 'fa-clock', k: 'Typical turnaround', v: '3–7 days' },
|
||
{ i: 'fa-music', k: 'Delivery', v: 'Broadcast-ready files' }
|
||
]
|
||
}
|
||
};
|
||
|
||
// Build the modal shell once
|
||
var overlay = document.createElement('div');
|
||
overlay.className = 'pkg-modal-overlay';
|
||
overlay.innerHTML =
|
||
'<div class="pkg-modal" role="dialog" aria-modal="true" aria-labelledby="pkgModalName">' +
|
||
'<button class="pkg-modal-close" type="button" aria-label="Close">×</button>' +
|
||
'<div class="pkg-modal-emoji"></div>' +
|
||
'<div class="pkg-modal-name" id="pkgModalName"></div>' +
|
||
'<div class="pkg-modal-price"></div>' +
|
||
'<p class="pkg-modal-lead"></p>' +
|
||
'<div class="pkg-modal-body"></div>' +
|
||
'<a class="btn btn-accent pricing-enquire pkg-modal-enquire"><i class="fas fa-envelope"></i> Enquire about this</a>' +
|
||
'</div>';
|
||
document.body.appendChild(overlay);
|
||
|
||
var modalBox = overlay.querySelector('.pkg-modal');
|
||
var elEmoji = overlay.querySelector('.pkg-modal-emoji');
|
||
var elName = overlay.querySelector('.pkg-modal-name');
|
||
var elPrice = overlay.querySelector('.pkg-modal-price');
|
||
var elLead = overlay.querySelector('.pkg-modal-lead');
|
||
var elBody = overlay.querySelector('.pkg-modal-body');
|
||
var elEnquire = overlay.querySelector('.pkg-modal-enquire');
|
||
var elClose = overlay.querySelector('.pkg-modal-close');
|
||
var lastFocus = null;
|
||
|
||
var REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
var SPARKLE_CHARS = ['✦', '✧', '✶', '✩', '✨'];
|
||
var SPARKLE_COLORS = ['rgba(255,255,255,1)', 'rgba(251,191,36,1)', 'rgba(253,224,120,1)', 'rgba(249,115,22,0.95)'];
|
||
|
||
function dropSparkle(x, y) {
|
||
var sp = document.createElement('span');
|
||
sp.className = 'pkg-sparkle';
|
||
var size = (0.5 + Math.random() * 0.9).toFixed(2);
|
||
var col = SPARKLE_COLORS[Math.floor(Math.random() * SPARKLE_COLORS.length)];
|
||
sp.textContent = SPARKLE_CHARS[Math.floor(Math.random() * SPARKLE_CHARS.length)];
|
||
sp.style.left = x + 'px';
|
||
sp.style.top = y + 'px';
|
||
sp.style.fontSize = size + 'rem';
|
||
sp.style.color = col;
|
||
document.body.appendChild(sp);
|
||
var da = Math.random() * Math.PI * 2;
|
||
var dd = 8 + Math.random() * 22;
|
||
var ddx = (Math.cos(da) * dd).toFixed(0);
|
||
var ddy = (Math.sin(da) * dd).toFixed(0);
|
||
sp.animate([
|
||
{ opacity: 1, transform: 'translate(-50%, -50%) scale(' + size + ') rotate(0deg)' },
|
||
{ opacity: 0, transform: 'translate(calc(-50% + ' + ddx + 'px), calc(-50% + ' + ddy + 'px)) scale(' + (size * 0.25).toFixed(2) + ') rotate(120deg)' }
|
||
], { duration: 460 + Math.random() * 300, easing: 'ease-out' }).onfinish = function () { sp.remove(); };
|
||
}
|
||
|
||
function edgeSparkles(box, duration) {
|
||
var start = null;
|
||
function frame(ts) {
|
||
if (!start) start = ts;
|
||
var p = Math.min((ts - start) / duration, 1);
|
||
var r = box.getBoundingClientRect();
|
||
for (var i = 0; i < 2; i++) {
|
||
var side = Math.random(), x, y;
|
||
if (side < 0.25) { x = r.left + Math.random() * r.width; y = r.top; }
|
||
else if (side < 0.5) { x = r.left + Math.random() * r.width; y = r.bottom; }
|
||
else if (side < 0.75) { x = r.left; y = r.top + Math.random() * r.height; }
|
||
else { x = r.right; y = r.top + Math.random() * r.height; }
|
||
dropSparkle(x, y);
|
||
}
|
||
if (p < 1) requestAnimationFrame(frame);
|
||
}
|
||
requestAnimationFrame(frame);
|
||
}
|
||
|
||
function openModal(slug, ox, oy) {
|
||
var d = DETAILS[slug];
|
||
if (!d) return;
|
||
|
||
elEmoji.textContent = d.emoji;
|
||
elName.textContent = d.name;
|
||
elPrice.innerHTML = d.price;
|
||
elLead.textContent = d.lead;
|
||
|
||
var html = '';
|
||
if (d.low && d.high) {
|
||
html += '<div class="pkg-range">' +
|
||
'<div class="pkg-range-col"><h4>Lower end</h4><div class="amt">' + d.low.amt + '</div><p>' + d.low.desc + '</p></div>' +
|
||
'<div class="pkg-range-col"><h4>Upper end</h4><div class="amt">' + d.high.amt + '</div><p>' + d.high.desc + '</p></div>' +
|
||
'</div>';
|
||
}
|
||
html += '<hr class="pkg-modal-divider"><div class="pkg-specs">';
|
||
d.specs.forEach(function (s) {
|
||
html += '<div class="pkg-spec"><span class="k"><i class="fas ' + s.i + '"></i> ' + s.k + '</span><span class="v">' + s.v + '</span></div>';
|
||
});
|
||
html += '</div>';
|
||
elBody.innerHTML = html;
|
||
|
||
elEnquire.setAttribute('href', 'index.php?package=' + slug + '#contact');
|
||
|
||
lastFocus = document.activeElement;
|
||
overlay.classList.add('open');
|
||
document.documentElement.classList.add('modal-open');
|
||
modalBox.scrollTop = 0;
|
||
|
||
if (!REDUCED) {
|
||
var rect = modalBox.getBoundingClientRect();
|
||
var cx = rect.left + rect.width / 2;
|
||
var cy = rect.top + rect.height / 2;
|
||
if (typeof ox !== 'number') { ox = cx; oy = cy; }
|
||
var tx = ox - cx, ty = oy - cy;
|
||
var dist = Math.hypot(tx, ty);
|
||
var startAng = Math.atan2(ty, tx);
|
||
var maxR = Math.max(55, Math.min(dist * 0.5, 120));
|
||
var turns = 1.25;
|
||
var duration = 1000;
|
||
|
||
var STEPS = 44, frames = [];
|
||
for (var i = 0; i <= STEPS; i++) {
|
||
var t = i / STEPS;
|
||
var g = Math.pow(t, 1.4);
|
||
var baseX = tx * (1 - g);
|
||
var baseY = ty * (1 - g);
|
||
var amp = maxR * Math.sin(Math.PI * Math.pow(t, 0.7));
|
||
var ang = startAng + turns * 2 * Math.PI * t;
|
||
var px = baseX + Math.cos(ang) * amp;
|
||
var py = baseY + Math.sin(ang) * amp;
|
||
var sc = 0.05 + 0.95 * g;
|
||
frames.push({
|
||
transform: 'translate(' + px.toFixed(1) + 'px,' + py.toFixed(1) + 'px) scale(' + sc.toFixed(3) + ')',
|
||
opacity: t < 0.08 ? (t / 0.08).toFixed(2) : 1
|
||
});
|
||
}
|
||
modalBox.animate(frames, { duration: duration, easing: 'linear' });
|
||
|
||
edgeSparkles(modalBox, duration);
|
||
}
|
||
|
||
elClose.focus();
|
||
}
|
||
|
||
function perimeterBurst(r, n) {
|
||
var per = 2 * (r.width + r.height);
|
||
for (var i = 0; i < n; i++) {
|
||
var d = (i / n) * per + (Math.random() - 0.5) * (per / n);
|
||
var x, y;
|
||
if (d < r.width) { x = r.left + d; y = r.top; }
|
||
else if (d < r.width + r.height) { x = r.right; y = r.top + (d - r.width); }
|
||
else if (d < 2 * r.width + r.height){ x = r.right - (d - r.width - r.height); y = r.bottom; }
|
||
else { x = r.left; y = r.bottom - (d - 2 * r.width - r.height); }
|
||
dropSparkle(x, y);
|
||
}
|
||
}
|
||
|
||
function closeModal() {
|
||
if (!REDUCED && overlay.classList.contains('open')) {
|
||
perimeterBurst(modalBox.getBoundingClientRect(), 48);
|
||
}
|
||
overlay.classList.remove('open');
|
||
document.documentElement.classList.remove('modal-open');
|
||
if (lastFocus && lastFocus.focus) lastFocus.focus();
|
||
}
|
||
|
||
elClose.addEventListener('click', closeModal);
|
||
overlay.addEventListener('click', function (e) {
|
||
if (e.target === overlay) closeModal();
|
||
});
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape' && overlay.classList.contains('open')) closeModal();
|
||
});
|
||
|
||
function clickPoint(e, el) {
|
||
if (e && e.clientX) return [e.clientX, e.clientY];
|
||
var r = el.getBoundingClientRect();
|
||
return [r.left + r.width / 2, r.top + r.height / 2];
|
||
}
|
||
|
||
document.querySelectorAll('a.pricing-enquire, a.bench-enquire, a.ward-enquire').forEach(function (link) {
|
||
var query = ((link.getAttribute('href') || '').split('?')[1] || '').split('#')[0];
|
||
var slug = new URLSearchParams(query).get('package');
|
||
if (!slug || !DETAILS[slug]) return;
|
||
|
||
var btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'pkg-details-btn';
|
||
btn.innerHTML = '<i class="fas fa-circle-info"></i> See full details';
|
||
btn.addEventListener('click', function (e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
var pt = clickPoint(e, btn);
|
||
openModal(slug, pt[0], pt[1]);
|
||
});
|
||
link.parentNode.insertBefore(btn, link);
|
||
|
||
var card = link.closest('.pricing-card, .bench-card, .ward-card');
|
||
if (card) {
|
||
card.addEventListener('click', function (e) {
|
||
if (e.target.closest('a, button')) return;
|
||
var pt = clickPoint(e, card);
|
||
openModal(slug, pt[0], pt[1]);
|
||
});
|
||
}
|
||
});
|
||
})();
|