We provide man and van and removals services across Manchester and the wider Greater Manchester area. Whether you're moving from a city centre apartment, relocating from a family home in the suburbs, or need help with furniture delivery, we connect you to local drivers who know the city and can help with moves of all sizes.
Get quotes from our removalists, or use our man and van booking form below for an instant price and to get booked in straight away. It only takes 30 seconds!
We provide man and van and removals services across Manchester and the wider Greater Manchester area. Whether you're moving from a city centre apartment, relocating from a family home in the suburbs, or need help with furniture delivery, we connect you to local drivers who know the city and can help with moves of all sizes.
Find our rates, van services, or use our quote estimator below for an instant price and to get booked in straight away. It only takes 30 seconds!
1. Enter your postcodes and details below
Pick up and drop off
2. Choose your driver and van size
Find a driver near you
3. Get an instant quote and book in
Get booked in – get stuff moved
1. Enter postcodes
2. Select a driver
3. Book in the job
Select A Moving Service
Service selection: choose the move you need to get started.
Getting a quote for: Man & Van
Estimating your loading time:
Studio flat / few items: 30-60 mins
1-bed flat: 60-90 mins
2-bed house: 90-120 mins
Add 15-30 mins per floor for stairs
If additional time is needed, it's charged at the hourly rate.
Example: 30 mins to load + 30 mins to unload = select 1 hour
Step 1 of 3
Estimate
Enter details and click “Get a quote”.
Compare van sizes
How your booking & deposit works
Getting a quote for: Full Removals
Submitting Your Request
Please wait while we upload your files...
Step 1 of 3
so CSS rules can't
// fail to apply and the modal is guaranteed to render. Returns a Promise:
// true → customer tapped Continue, form submits (oversized videos skipped)
// false → customer tapped Cancel, handler returns so they can edit
function confirmOversizedVideos(oversized){
return new Promise(function(resolve){
if (!oversized.length) return resolve(true);
const WA_URL = 'https://wa.me/443300430885?text=Hi%2C%20my%20removals%20video%20was%20too%20large%20to%20upload%20through%20your%20form%20%E2%80%94%20sending%20it%20here.';
// Backdrop
const backdrop = document.createElement('div');
backdrop.setAttribute('role','dialog');
backdrop.setAttribute('aria-modal','true');
backdrop.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:99999;display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;font-family:Poppins,system-ui,sans-serif;';
// Box
const box = document.createElement('div');
box.style.cssText = 'background:#fff;padding:28px 28px 24px;border-radius:12px;max-width:440px;width:100%;box-sizing:border-box;box-shadow:0 20px 60px rgba(0,0,0,0.3);text-align:center;';
// Title
const title = document.createElement('h3');
title.textContent = "Your video's too large to upload here";
title.style.cssText = 'margin:0 0 14px;font-size:18px;font-weight:600;color:#111;';
box.appendChild(title);
// Lead text
const lead = document.createElement('p');
lead.style.cssText = 'margin:0 0 12px;font-size:14px;line-height:1.5;color:#444;';
lead.textContent = "Videos up to 50 MB can go through the form. Anything bigger needs to come over WhatsApp — we'll attach it to your enquiry.";
box.appendChild(lead);
// File list
const list = document.createElement('div');
list.style.cssText = 'margin:14px 0;padding:10px 12px;background:#f7f7f9;border-radius:8px;font-size:13px;color:#555;text-align:left;';
oversized.forEach(function(f){
const row = document.createElement('div');
const mb = (f.size/1048576).toFixed(1);
row.textContent = f.name + ' — ' + mb + ' MB';
row.style.cssText = 'margin:2px 0;word-break:break-word;';
list.appendChild(row);
});
box.appendChild(list);
// Sub text — built from textContent to avoid any HTML/JS parse issues
// when the file goes through WordPress / WPCode snippet processing.
const sub = document.createElement('p');
sub.style.cssText = 'margin:0 0 18px;font-size:14px;line-height:1.5;color:#444;';
sub.textContent = "Tap Open WhatsApp below to send your video to us, then come back and tap Continue to send your enquiry. Your details and photos go through either way.";
box.appendChild(sub);
// Actions
const actions = document.createElement('div');
actions.style.cssText = 'display:flex;flex-direction:column;gap:10px;';
const waBtn = document.createElement('a');
waBtn.href = WA_URL;
waBtn.target = '_blank';
waBtn.rel = 'noopener';
waBtn.textContent = 'Open WhatsApp — 0330 043 0885';
waBtn.style.cssText = 'background:#25D366;color:#fff;padding:12px 18px;border-radius:8px;text-decoration:none;font-weight:600;font-size:14px;display:block;text-align:center;';
const continueBtn = document.createElement('button');
continueBtn.type = 'button';
continueBtn.textContent = 'Continue — send my enquiry';
continueBtn.style.cssText = 'background:#DF1AD1;color:#fff;border:0;padding:12px 18px;border-radius:8px;cursor:pointer;font-weight:600;font-size:14px;';
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.textContent = 'Cancel — let me edit';
cancelBtn.style.cssText = 'background:transparent;border:1px solid #d1d5db;color:#444;padding:10px 18px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;';
actions.appendChild(waBtn);
actions.appendChild(continueBtn);
actions.appendChild(cancelBtn);
box.appendChild(actions);
backdrop.appendChild(box);
document.body.appendChild(backdrop);
function close(result){
try { document.body.removeChild(backdrop); } catch(e){}
resolve(result);
}
continueBtn.addEventListener('click', function(){ close(true); });
cancelBtn.addEventListener('click', function(){ close(false); });
// Click outside the box also cancels
backdrop.addEventListener('click', function(e){
if (e.target === backdrop) close(false);
});
});
}
if (uploadBox) {
['dragenter','dragover'].forEach(evt => {
uploadBox.addEventListener(evt, (e) => {
e.preventDefault();
e.stopPropagation();
uploadBox.classList.add('dragover');
});
});
['dragleave','drop'].forEach(evt => {
uploadBox.addEventListener(evt, (e) => {
e.preventDefault();
e.stopPropagation();
uploadBox.classList.remove('dragover');
});
});
uploadBox.addEventListener('drop', (e) => {
if (e.dataTransfer && e.dataTransfer.files.length) {
addMedia(e.dataTransfer.files);
}
});
}
function showStep(step) {
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
const activeStep = document.getElementById('step' + step);
activeStep.classList.add('active');
const errorMsg = document.getElementById('rmvErrorMsg');
if (errorMsg) errorMsg.classList.remove('show');
const progress = (step / 3) * 100;
document.getElementById('rmvProgressFill').style.width = progress + '%';
document.getElementById('rmvProgressLabel').textContent = `Step ${step} of 3`;
currentStep = step;
// Smart scroll - scroll to service selector panel (like Man and Van scrolls to summaryEl)
// On main site: scroll to tvmcRemovalsPanel or tvmcSelectorPanel container
// In iframe: scroll to progressBar
const inIframe = window.parent !== window;
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
// Find scroll target: on main site use the panel container, in iframe use progressBar
const scrollTarget = inIframe
? document.getElementById('rmvProgressBar')
: (document.getElementById('tvmcRemovalsPanel') || document.getElementById('tvmcSelectorPanel') || document.getElementById('rmvProgressBar'));
if (scrollTarget) {
// Calculate absolute position from top of document
let absTop = 0;
let el = scrollTarget;
while(el) { absTop += el.offsetTop; el = el.offsetParent; }
// Always try scrollIntoView first
try {
scrollTarget.scrollIntoView({ behavior: isMobile ? 'instant' : 'smooth', block: 'start' });
} catch(e) {
scrollTarget.scrollIntoView(true);
}
if (inIframe) {
// In iframe: tell parent to scroll
const sendScroll = () => {
try {
window.parent.postMessage({ type: 'tvmc-scroll-to', offsetY: absTop }, '*');
} catch(e) {}
};
sendScroll();
setTimeout(sendScroll, 50);
setTimeout(sendScroll, 100);
setTimeout(sendScroll, 200);
setTimeout(sendScroll, 400);
setTimeout(sendScroll, 800);
} else if (!window.PARTNER_ID) {
// Main site: scroll again with header offset
setTimeout(() => {
window.scrollTo({ top: absTop - 150, behavior: 'smooth' });
}, 50);
}
}
}
function clearFieldError(field) {
if (!field) return;
field.classList.remove('has-error');
const error = field.querySelector('.req-error');
if (error && error.dataset.default) {
error.textContent = error.dataset.default;
}
}
function setFieldError(field, message) {
if (!field) return;
field.classList.add('has-error');
const error = field.querySelector('.req-error');
if (error) {
const defaultMsg = error.dataset.default || error.textContent;
error.textContent = message || defaultMsg || 'Please fill in this field.';
}
}
function bindClearOnInput(field, selectors = 'input, select, textarea') {
if (!field) return;
field.querySelectorAll(selectors).forEach(el => {
el.addEventListener('input', () => clearFieldError(field));
el.addEventListener('change', () => clearFieldError(field));
});
}
document.querySelectorAll('.field').forEach(field => bindClearOnInput(field));
document.getElementById('step1Next').addEventListener('click', () => {
const selectedMoveType = document.querySelector('input[name="moveType"]:checked');
let hasError = false;
if (!selectedMoveType) {
if (moveTypeField) setFieldError(moveTypeField);
hasError = true;
}
const packingService = document.querySelector('input[name="packingService"]:checked');
const packingServiceField = document.querySelector('input[name="packingService"]')?.closest('.field');
if (!packingService) {
setFieldError(packingServiceField);
hasError = true;
}
if (packingService && packingService.value === 'Yes') {
const packingMaterials = document.querySelector('input[name="packingMaterials"]:checked');
const packingMaterialsField = document.getElementById('packingDetailsGroup');
if (!packingMaterials) {
setFieldError(packingMaterialsField);
hasError = true;
}
}
const boxEstimate = document.getElementById('boxEstimate');
if (!boxEstimate.value) {
setFieldError(boxEstimate.closest('.field'));
hasError = true;
}
const bulkyItems = document.getElementById('bulkyItems');
if (!bulkyItems.value.trim()) {
setFieldError(bulkyItems.closest('.field'));
hasError = true;
}
if (hasError) {
showError('Please fill in the required fields below.');
return;
}
showStep(2);
});
document.getElementById('step2Back').addEventListener('click', () => showStep(1));
document.getElementById('step2Next').addEventListener('click', () => {
const requiredFields = ['fromPropertySize', 'fromPostcode', 'toPropertySize', 'toPostcode'];
let hasError = false;
for (const id of requiredFields) {
const el = document.getElementById(id);
if (!el.value.trim()) {
setFieldError(el.closest('.field'));
hasError = true;
}
}
const fromPropertyType = document.querySelector('input[name="fromPropertyType"]:checked');
const toPropertyType = document.querySelector('input[name="toPropertyType"]:checked');
if (!fromPropertyType) {
setFieldError(document.querySelector('input[name="fromPropertyType"]')?.closest('.field'));
hasError = true;
}
if (!toPropertyType) {
setFieldError(document.querySelector('input[name="toPropertyType"]')?.closest('.field'));
hasError = true;
}
const fromNeedsLift = fromPropertyType && fromPropertyType.value === 'Apartment/Flat';
if (fromNeedsLift) {
const fromLift = document.querySelector('input[name="fromLift"]:checked');
const fromFloor = document.getElementById('fromFloor');
if (!fromLift) {
setFieldError(document.querySelector('input[name="fromLift"]')?.closest('.field'));
hasError = true;
}
if (!fromFloor.value.trim()) {
setFieldError(fromFloor.closest('.field'));
hasError = true;
}
}
const toNeedsLift = toPropertyType && toPropertyType.value === 'Apartment/Flat';
if (toNeedsLift) {
const toLift = document.querySelector('input[name="toLift"]:checked');
const toFloor = document.getElementById('toFloor');
if (!toLift) {
setFieldError(document.querySelector('input[name="toLift"]')?.closest('.field'));
hasError = true;
}
if (!toFloor.value.trim()) {
setFieldError(toFloor.closest('.field'));
hasError = true;
}
}
const dateType = document.querySelector('input[name="movingDateType"]:checked');
if (dateType && dateType.value === 'Specific') {
const specificDate = document.getElementById('specificDate');
if (!specificDate.value.trim()) {
setFieldError(specificDate.closest('.field'));
hasError = true;
}
}
if (dateType && dateType.value === 'Flexible') {
const flexibleFrom = document.getElementById('flexibleDateFrom');
const flexibleTo = document.getElementById('flexibleDateTo');
if (!flexibleFrom.value.trim() || !flexibleTo.value.trim()) {
if (!flexibleFrom.value.trim()) {
setFieldError(flexibleFrom.closest('.field'));
}
if (!flexibleTo.value.trim()) {
setFieldError(flexibleTo.closest('.field'));
}
hasError = true;
}
}
if (hasError) {
showError('Please fill in the required fields below.');
return;
}
showStep(3);
});
document.getElementById('step3Back').addEventListener('click', () => showStep(2));
function togglePropertyFields(prefix) {
const propertyType = document.querySelector(`input[name="${prefix}PropertyType"]:checked`);
const liftGroup = document.getElementById(`${prefix}LiftGroup`);
const floorGroup = document.getElementById(`${prefix}FloorGroup`);
const liftInputs = document.querySelectorAll(`input[name="${prefix}Lift"]`);
const floorInput = document.getElementById(`${prefix}Floor`);
if (!propertyType || propertyType.value !== 'Apartment/Flat') {
if (liftGroup) liftGroup.style.display = 'none';
if (floorGroup) floorGroup.style.display = 'none';
liftInputs.forEach(input => input.checked = false);
if (floorInput) floorInput.value = '';
return;
}
if (liftGroup) liftGroup.style.display = 'flex';
if (floorGroup) floorGroup.style.display = 'flex';
}
['from', 'to'].forEach(prefix => {
const inputs = document.querySelectorAll(`input[name="${prefix}PropertyType"]`);
inputs.forEach(input => {
input.addEventListener('change', () => togglePropertyFields(prefix));
});
});
// togglePackingDetails moved above moveTypeCards handler — see earlier in file
const packingServiceInputs2 = document.querySelectorAll('input[name="packingService"]');
packingServiceInputs2.forEach(input => input.addEventListener('change', togglePackingDetails));
const dateTypeInputs = document.querySelectorAll('input[name="movingDateType"]');
const specificDateField = document.getElementById('specificDateField');
const flexibleDateField = document.getElementById('flexibleDateField');
function toggleDateFields() {
const selected = document.querySelector('input[name="movingDateType"]:checked');
const isFlexible = selected && selected.value === 'Flexible';
const isNotSure = selected && selected.value === 'Not sure';
if (specificDateField) specificDateField.style.display = isFlexible || isNotSure ? 'none' : 'block';
if (flexibleDateField) flexibleDateField.style.display = isFlexible && !isNotSure ? 'block' : 'none';
}
dateTypeInputs.forEach(input => input.addEventListener('change', toggleDateFields));
toggleDateFields();
function initDatePicker(){
if (window.flatpickr) {
flatpickr("#specificDate", { altInput:true, altFormat:"d/m/Y", dateFormat:"d/m/Y", disableMobile:true });
flatpickr("#flexibleDateFrom", { altInput:true, altFormat:"d/m/Y", dateFormat:"d/m/Y", disableMobile:true });
flatpickr("#flexibleDateTo", { altInput:true, altFormat:"d/m/Y", dateFormat:"d/m/Y", disableMobile:true });
} else {
setTimeout(initDatePicker, 40);
}
}
initDatePicker();
document.getElementById('removalsForm').addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
const successMsg = document.getElementById('rmvSuccessMsg');
const errorMsg = document.getElementById('rmvErrorMsg');
const thankYouMsg = document.getElementById('rmvThankYou');
const uploadingOverlay = document.getElementById('uploadingOverlay');
successMsg.classList.remove('show');
errorMsg.classList.remove('show');
thankYouMsg.classList.remove('show');
const nameField = document.getElementById('customerName');
const emailField = document.getElementById('customerEmail');
const phoneField = document.getElementById('customerPhone');
let hasError = false;
if (!nameField.value.trim()) {
setFieldError(nameField.closest('.field'));
hasError = true;
}
if (!emailField.value.trim() || !emailField.checkValidity()) {
setFieldError(emailField.closest('.field'), 'Please enter a valid email address.');
hasError = true;
}
if (!phoneField.value.trim()) {
setFieldError(phoneField.closest('.field'));
hasError = true;
}
if (hasError) {
showError('Please fill in the required fields below.');
return;
}
// Submit-time gate: if any picked video exceeds the 50 MB Supabase Free
// cap, show the custom modal with an Open-WhatsApp button. Lead is never
// blocked silently — Continue submits (oversized skipped during upload);
// Cancel returns to the form so the customer can edit.
console.log('[Removals] Submit handler started');
const oversizedAtSubmit = mediaFiles.filter(isOversizedVideo);
if (oversizedAtSubmit.length) {
const proceed = await confirmOversizedVideos(oversizedAtSubmit);
if (!proceed) {
return;
}
}
submitBtn.disabled = true;
submitBtn.textContent = 'Processing...';
// Show uploading overlay
uploadingOverlay.classList.add('show');
try {
const moveTypeInput = document.querySelector('input[name="moveType"]:checked');
const moveTypeLabel = moveTypeInput ? moveTypeInput.closest('.crew-card').querySelector('.crew-title').textContent.trim() : 'Not specified';
// Generate a unique submission ID. Used as the Supabase folder name AND as
// the lookup key on the Airtable record from the photo/video-uploaded webhooks.
const submissionUuid = (window.crypto && crypto.randomUUID)
? crypto.randomUUID()
: 'sub-' + Date.now() + '-' + Math.random().toString(36).slice(2, 10);
// Split media: BOTH videos and photos now go to Supabase via TUS (resumable,
// reliable on bad networks). Multipart was unreliable on poor connections —
// a 58MB-video + 5MB-photos test from Vietnam lost ALL media because the
// multipart body upload timed out before n8n could ack.
// Oversized videos (> 50 MB Supabase Free cap) are excluded from the upload
// queue — they're shown inline with a WhatsApp fallback so the lead is never
// blocked by a video the platform can't accept.
const allVideos = mediaFiles.filter(isVideoFile);
const videos = allVideos.filter(function(f){ return !isOversizedVideo(f); });
const oversizedVideos = allVideos.filter(isOversizedVideo);
const photos = mediaFiles.filter(function(f){ return !isVideoFile(f); });
const hasVideo = videos.length > 0;
const hasPhotos = photos.length > 0;
const hasOversizedVideo = oversizedVideos.length > 0;
const movingDateType = (document.querySelector('input[name="movingDateType"]:checked') || {}).value || '';
// Build a tiny JSON body. ~5KB max — no file bytes. Files upload separately
// to Supabase via TUS after the lead is safely captured in Airtable.
const body = {
booking_type: 'Removal',
source: 'removals_public',
timestamp: new Date().toISOString(),
page_url: window.location.href,
gclid: window.__tvmc_gclid || '',
// Partner tracking (if form loaded in partner widget)
partner_source: window.PARTNER_ID || '',
partner_name: (window.PARTNER_CONFIG && window.PARTNER_CONFIG.company_name) || '',
partner_stripe_account: (window.PARTNER_CONFIG && window.PARTNER_CONFIG.stripe_account_id) || '',
partner_commission_type: (window.PARTNER_CONFIG && window.PARTNER_CONFIG.commission_type) || '',
partner_commission_value: (window.PARTNER_CONFIG && window.PARTNER_CONFIG.commission_value) || '',
// Customer details
name: document.getElementById('customerName').value.trim(),
email: document.getElementById('customerEmail').value.trim(),
phone: document.getElementById('customerPhone').value.trim(),
// Move details
move_type: moveTypeLabel,
service_selected: moveTypeLabel,
packing_service: (document.querySelector('input[name="packingService"]:checked') || {}).value || '',
packing_materials: (document.querySelector('input[name="packingMaterials"]:checked') || {}).value || '',
packing_details: document.getElementById('packingDetails').value.trim(),
from_property_type: (document.querySelector('input[name="fromPropertyType"]:checked') || {}).value || '',
from_lift: (document.querySelector('input[name="fromLift"]:checked') || {}).value || '',
from_floor: document.getElementById('fromFloor').value.trim(),
from_property_size: document.getElementById('fromPropertySize').value,
from_postcode: document.getElementById('fromPostcode').value.trim(),
to_property_type: (document.querySelector('input[name="toPropertyType"]:checked') || {}).value || '',
to_lift: (document.querySelector('input[name="toLift"]:checked') || {}).value || '',
to_floor: document.getElementById('toFloor').value.trim(),
to_property_size: document.getElementById('toPropertySize').value,
to_postcode: document.getElementById('toPostcode').value.trim(),
moving_date_type: movingDateType,
specific_date: movingDateType === 'Specific' ? document.getElementById('specificDate').value : '',
flexible_from: movingDateType === 'Flexible' ? document.getElementById('flexibleDateFrom').value : '',
flexible_to: movingDateType === 'Flexible' ? document.getElementById('flexibleDateTo').value : '',
bulky_items: document.getElementById('bulkyItems').value.trim(),
box_estimate: document.getElementById('boxEstimate').value.trim(),
moving_from_notes: document.getElementById('fromNotes').value.trim(),
moving_to_notes: document.getElementById('toNotes').value.trim(),
// Submission tracking — used by photo/video-uploaded webhooks to find the
// Airtable record by submission_uuid and mirror Supabase files into Drive.
submission_uuid: submissionUuid,
has_video: hasVideo ? 'true' : 'false',
has_photos: hasPhotos ? 'true' : 'false',
// Customer was told their video is too large to upload here and to send
// it via WhatsApp instead. Admin/n8n can use this to set expectations
// ("a WhatsApp video is coming for this enquiry").
has_oversized_video: hasOversizedVideo ? 'true' : 'false',
oversized_video_count: oversizedVideos.length
};
// Phase 1: send the lead to n8n. Tiny JSON, completes in <1s on any
// realistic connection. Retry up to 3x with exponential backoff in case of
// a transient blip — payload is small enough that retries are cheap.
let response = null;
let lastErr = null;
for (let attempt = 0; attempt < 3; attempt++) {
try {
response = await fetch(N8N_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (response.ok) { lastErr = null; break; }
lastErr = new Error('Server returned ' + response.status);
} catch (e) {
lastErr = e;
}
if (attempt < 2) {
await new Promise(function(r){ setTimeout(r, 1000 * Math.pow(2, attempt)); }); // 1s, 2s
}
}
if (lastErr) throw lastErr;
// From this point on the lead is safely in Airtable + Drive folder created.
// Per-file upload errors below DO NOT trigger the catch block — we never
// want to lose the lead just because one photo / video upload failed.
const overlayTextEl = uploadingOverlay.querySelector('.uploading-text');
const overlaySubEl = uploadingOverlay.querySelector('.uploading-subtext');
// Phase 2a: photos via TUS to tvmc-removals-photos. Compression keeps
// chunks small for faster uploads on poor connections (~10MB → ~500KB).
// Falls back to original file on any compression failure.
let compressedPhotos = photos;
if (hasPhotos) {
if (overlayTextEl) overlayTextEl.textContent = 'Preparing photos…';
if (overlaySubEl) overlaySubEl.textContent = 'Optimising image sizes for upload.';
try {
compressedPhotos = await compressPhotosForUpload(photos);
} catch (compressErr) {
console.warn('[Removals] compress helper threw, using originals', compressErr);
compressedPhotos = photos;
}
if (overlayTextEl) overlayTextEl.textContent = 'Uploading your photos…';
if (overlaySubEl) overlaySubEl.textContent = 'Please keep this tab open.';
for (let i = 0; i < compressedPhotos.length; i++) {
const file = compressedPhotos[i];
const objectName = submissionUuid + '/' + file.name;
try {
await uploadFileViaTUS(file, objectName, submissionUuid, SUPABASE_PHOTOS_BUCKET, N8N_PHOTO_WEBHOOK, 'photo', function(sent, total){
const pct = total > 0 ? Math.min(99, Math.round((sent / total) * 100)) : 0;
if (overlaySubEl) {
overlaySubEl.textContent = compressedPhotos.length === 1
? 'Uploading your photo — ' + pct + '%'
: 'Uploading photo ' + (i + 1) + ' of ' + compressedPhotos.length + ' — ' + pct + '%';
}
});
} catch (photoErr) {
// One photo failed — don't stop the rest, don't lose the lead.
console.warn('[Removals] Photo upload failed for', file.name, photoErr);
}
}
}
// Phase 2b: videos via TUS to tvmc-removals-videos.
if (hasVideo) {
if (overlayTextEl) overlayTextEl.textContent = 'Uploading your video…';
if (overlaySubEl) overlaySubEl.textContent = 'Please keep this tab open. Large videos can take a minute or two.';
for (let i = 0; i < videos.length; i++) {
const file = videos[i];
const objectName = submissionUuid + '/' + file.name;
try {
await uploadVideoViaTUS(file, objectName, submissionUuid, function(sent, total){
const pct = total > 0 ? Math.min(99, Math.round((sent / total) * 100)) : 0;
if (overlaySubEl) {
overlaySubEl.textContent = videos.length === 1
? 'Uploading your video — ' + pct + '%'
: 'Uploading video ' + (i + 1) + ' of ' + videos.length + ' — ' + pct + '%';
}
});
} catch (videoErr) {
// One video failed — don't stop the rest, don't lose the lead.
console.warn('[Removals] Video upload failed for', file.name, videoErr);
}
}
}
// Hide uploading overlay
uploadingOverlay.classList.remove('show');
thankYouMsg.innerHTML = `
✓ Your request has been sent.
We're preparing your quote and will get back to you shortly. If we need anything else to firm up the numbers, we'll reach out right away.`;
thankYouMsg.classList.add('show');
document.getElementById('rmvProgressBar').style.display = 'none';
document.getElementById('removalsForm').style.display = 'none';
// Scroll to thank you message
const rect = thankYouMsg.getBoundingClientRect();
const top = window.scrollY + rect.top - 60;
window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
} catch (error) {
// Hide uploading overlay on error
uploadingOverlay.classList.remove('show');
errorMsg.textContent = error.message || 'An error occurred. Please try again.';
errorMsg.classList.add('show');
// Scroll to error message
errorMsg.scrollIntoView({ behavior: 'smooth', block: 'start' });
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'SEND BOOKING REQUEST';
}
});
/**
* Upload a file (photo or video) to Supabase Storage using the TUS resumable protocol.
* Resolves on success (or quiet failure of the Phase 3 notify), rejects on TUS error.
* After the upload completes, notifies the supplied n8n webhook so it can
* mirror the file from Supabase into the customer's Drive folder.
*
* Both photos (tvmc-removals-photos bucket) and videos (tvmc-removals-videos bucket)
* use the same TUS endpoint — bucket selection is via the `bucketName` metadata.
*
* Reference:
* - TUS protocol: https://tus.io/
* - Supabase resumable uploads: https://supabase.com/docs/guides/storage/uploads/resumable-uploads
* - tus-js-client API: https://github.com/tus/tus-js-client/blob/main/docs/api.md
*/
function uploadFileViaTUS(file, objectName, submissionUuid, bucketName, notifyWebhookUrl, kind, onProgress) {
return new Promise(function(resolve, reject) {
if (!window.tus) {
return reject(new Error('tus-js-client not loaded'));
}
var fallbackContentType = (kind === 'photo') ? 'image/jpeg' : 'video/mp4';
var upload = new tus.Upload(file, {
endpoint: SUPABASE_TUS_ENDPOINT,
retryDelays: [0, 1000, 3000, 5000, 10000, 30000, 60000],
headers: {
authorization: 'Bearer ' + SUPABASE_ANON_KEY,
'x-upsert': 'true'
},
uploadDataDuringCreation: true,
removeFingerprintOnSuccess: true,
metadata: {
bucketName: bucketName,
objectName: objectName,
contentType: file.type || fallbackContentType,
cacheControl: '3600'
},
// Supabase requires exactly 6 MB chunks (per docs).
chunkSize: 6 * 1024 * 1024,
onError: function(err) {
reject(err);
},
onProgress: function(sent, total) {
try { if (typeof onProgress === 'function') onProgress(sent, total); } catch (e) {}
},
onSuccess: function() {
// Tell n8n the upload completed. Fire-and-forget — if the notify fails,
// n8n still has the Airtable record with submission_uuid; an admin can
// manually re-fire if needed.
try {
fetch(notifyWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
submission_uuid: submissionUuid,
supabase_path: objectName
})
}).catch(function() {
console.warn('[Removals] Phase 3 ' + kind + ' notify failed; file is in Supabase under ' + objectName);
});
} catch (e) {
console.warn('[Removals] Phase 3 ' + kind + ' notify threw:', e);
}
resolve();
}
});
upload.start();
});
}
// Backwards-compat wrapper — preserves the existing video call site signature.
function uploadVideoViaTUS(file, objectName, submissionUuid, onProgress) {
return uploadFileViaTUS(file, objectName, submissionUuid, SUPABASE_BUCKET, N8N_VIDEO_WEBHOOK, 'video', onProgress);
}
})();
Van Sizes & Rates in Manchester
Hourly rates include the driver's round-trip commute to and from your job
3 sizes
Medium Van
Ford Transit Custom or equivalent
from£40.00/hr
Internal dimensions2.5m L × 1.7m W × 1.4m H
Internal load space6.0m³
Large Van
Ford Transit Van or equivalent
from£45.00/hr
Internal dimensions3.4m L × 1.7m W × 1.8m H
Internal load space10.0m³
Extra Large Van
Ford Transit Luton or equivalent
from£60.00/hr
Internal dimensions4.0m L × 2.0m W × 2.2m H
Internal load space18.0m³
Our Moving Services in Manchester
Transparent hourly pricing, local drivers, fast booking.
Manchester is a major city with a mix of city centre apartments, Victorian terraces in the inner suburbs, and family housing across the wider Greater Manchester area. Whether you’re moving from a flat in the Northern Quarter or Ancoats, relocating from a house in Didsbury, Chorlton, or Sale, or downsizing to a smaller property in Levenshulme, we help you find local drivers who know the city and can assist with your move.
We cover all types of moves across Manchester and the surrounding area. If you need a full house move, help with a flat clearance, assistance shifting furniture between properties, or someone to deliver appliances, we connect you to drivers with vans suited to the job. The city’s varied housing means moves range from compact city centre apartments in Salford Quays and Castlefield to larger Victorian terraces in Fallowfield and family homes in the suburbs of South Manchester.
We also cover nearby areas including Liverpool, Bolton, Stockport, Oldham, and Salford. Manchester’s position on the M60 ring road and with excellent rail links including Piccadilly and Victoria stations makes it exceptionally well connected for moves across the North West. Local drivers regularly help with relocations around the city centre, the Manchester Royal Infirmary, and the retail areas near the Trafford Centre and Arndale. The busy streets around Oxford Road and Wilmslow Road see regular moving activity, while residential areas in Rusholme, Moss Side, and Hulme have steady demand. The university areas create student moving peaks in September and June, while the suburbs of Whalley Range, Withington, and Burnage attract families and professionals throughout the year.
The wider Greater Manchester and North West region is easily accessible from the city centre. Drivers based here regularly help with moves across the conurbation and into neighbouring areas like Cheshire and Lancashire, whether you’re heading to a nearby town or relocating further afield. We make it straightforward to compare available drivers, choose the right van size, and find someone who suits your specific requirements.
Here are some popular areas that we operate in around Manchester.
Had a great experience. Tomasz was our driver and he was on time, efficient and very helpful. Couldn't recommend him enough. Spoke to the office multiple times before making my booking and they were very helpful and answered all my questions. Highly recommended.