JavaScript is required for the Virtual Yard. You can still browse our stock list.

Best 4X4 Cars Under $20K in Australia

Friday, 15th December 2023

(function () { const MIN_PRICE = 2990, MAX_PRICE = 29990, STEP = 500; const cfg = (window.VY || {}); const ACCENT = cfg.ACCENT || "#dc1c27"; const FORM_URL = cfg.FORM_URL || "/contact"; // fallback const CSV_URL = cfg.INVENTORY_CSV || null; // Seed stock (used if no CSV) const SEED = [ {id:'c1', make:'Toyota', model:'Corolla Ascent Sport', year:2018, price:15990, kms:82500, fuel:'Petrol', body:'Hatch', location:'Lambton NSW 2299', images:[ 'https://images.unsplash.com/photo-1549924231-f129b911e442?q=80&w=1880&auto=format&fit=crop', 'https://images.unsplash.com/photo-1511919884226-fd3cad34687c?q=80&w=1880&auto=format&fit=crop' ]}, {id:'c2', make:'Mazda', model:'CX-5 Touring AWD', year:2017, price:22990, kms:101200, fuel:'Petrol', body:'SUV', location:'Lambton NSW 2299', images:[ 'https://images.unsplash.com/photo-1606661126327-98cf47c2b9be?q=80&w=1880&auto=format&fit=crop' ]}, {id:'c3', make:'Hyundai', model:'i30 Active', year:2016, price:12990, kms:118500, fuel:'Petrol', body:'Hatch', location:'Lambton NSW 2299', images:[ 'https://images.unsplash.com/photo-1503376780353-7e6692767b70?q=80&w=1880&auto=format&fit=crop' ]} ]; // Helpers const $ = (sel, root=document) => root.querySelector(sel); const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel)); const fmtAUD = n => new Intl.NumberFormat('en-AU',{style:'currency',currency:'AUD',maximumFractionDigits:0}).format(n); const fmtKM = n => new Intl.NumberFormat('en-AU').format(n) + ' km'; // Boot when the yard exists document.addEventListener('DOMContentLoaded', init); function init(){ const root = document.getElementById('vy-yard'); if (!root) return; // Accent color root.style.setProperty('--vy-acc', ACCENT); const state = { q:'', body:'all', sort:'price-asc', min:MIN_PRICE, max:MAX_PRICE, data:SEED.slice() }; // Controls const q = $('#vy-q'), body = $('#vy-body'), sort = $('#vy-sort'); const minPrice = $('#vy-minPrice'), maxPrice = $('#vy-maxPrice'); const minRange = $('#vy-minRange'), maxRange = $('#vy-maxRange'); const priceLabel = $('#vy-priceLabel'), minPriceText = $('#vy-minPriceText'), maxPriceText = $('#vy-maxPriceText'); const grid = $('#vy-grid'), resultCount = $('#vy-resultCount'); // Preview dialog refs const dlg = $('#vy-preview'), pvImg = $('#vy-pvImg'), pvThumbs = $('#vy-pvThumbs'); // Init controls [minPrice, maxPrice, minRange, maxRange].forEach(x => { x.min = MIN_PRICE; x.max = MAX_PRICE; x.step = STEP; }); minPrice.value = state.min; maxPrice.value = state.max; minRange.value = state.min; maxRange.value = state.max; minPriceText.textContent = fmtAUD(MIN_PRICE); maxPriceText.textContent = fmtAUD(MAX_PRICE); updatePriceLabel(); // Events q.addEventListener('input', ()=>{ state.q = q.value.trim().toLowerCase(); render(); }); body.addEventListener('change', ()=>{ state.body = body.value; render(); }); sort.addEventListener('change', ()=>{ state.sort = sort.value; render(); }); const clamp = ()=>{ if(state.min < MIN_PRICE) state.min=MIN_PRICE; if(state.max>MAX_PRICE) state.max=MAX_PRICE; if(state.min>state.max) [state.min,state.max]=[state.max,state.min]; setInputs(); updatePriceLabel(); }; const setInputs = ()=>{ minPrice.value=state.min; maxPrice.value=state.max; minRange.value=state.min; maxRange.value=state.max; }; minPrice.addEventListener('input', ()=>{ state.min = Number(minPrice.value||MIN_PRICE); clamp(); render(); }); maxPrice.addEventListener('input', ()=>{ state.max = Number(maxPrice.value||MAX_PRICE); clamp(); render(); }); minRange.addEventListener('input', ()=>{ state.min = Number(minRange.value); clamp(); render(); }); maxRange.addEventListener('input', ()=>{ state.max = Number(maxRange.value); clamp(); render(); }); // Load CSV if provided if (CSV_URL) { fetch(CSV_URL, { cache: 'no-store' }) .then(r => { if(!r.ok) throw new Error('CSV load failed'); return r.text(); }) .then(text => { state.data = mapCSV(text); render(); }) .catch(() => render()); } else { render(); } function updatePriceLabel(){ priceLabel.textContent = `${fmtAUD(state.min)} – ${fmtAUD(state.max)}`; } function mapCSV(src){ const rows = parseCSV(src); if (!rows.length) return SEED.slice(); const headers = rows.shift().map(h => h.trim().toLowerCase()); const H = k => headers.indexOf(k); return rows.map(r => ({ id: r[H('id')] || crypto.randomUUID().slice(0,8), make: r[H('make')] || '', model: r[H('model')] || '', year: +r[H('year')] || 0, price: +r[H('price')] || 0, kms: +r[H('kms')] || 0, fuel: r[H('fuel')] || 'Petrol', body: r[H('body')] || '', location: r[H('location')] || '', images: (r[H('images')]||'').split('|').filter(Boolean) })).filter(v => v.price>0); } function filterAndSort(){ let cars = state.data.filter(c => c.price >= state.min && c.price <= state.max); if (state.body !== 'all') cars = cars.filter(c => (c.body||'').toLowerCase() === state.body); if (state.q) cars = cars.filter(c => (`${c.make} ${c.model} ${c.year}`).toLowerCase().includes(state.q)); switch(state.sort){ case 'price-desc': cars.sort((a,b)=>b.price-a.price); break; case 'kms-asc': cars.sort((a,b)=>a.kms-b.kms); break; case 'kms-desc': cars.sort((a,b)=>b.kms-a.kms); break; default: cars.sort((a,b)=>a.price-b.price); } return cars; } function card(c){ const img = (c.images && c.images[0]) || ''; const el = document.createElement('div'); el.className = 'vy-card'; el.innerHTML = `
${escapeHtml(`${c.make} ${c.model}`)}
${c.year} ${c.make} ${c.model}${fmtAUD(c.price)}
⏱ ${fmtKM(c.kms)} ⛽ ${c.fuel} 📍 ${c.location}
`; $('.vy-preview', el).addEventListener('click', ()=> openPreview(c)); $('.vy-enquire', el).addEventListener('click', ()=> openEnquiry(c)); return el; } function render(){ const cars = filterAndSort(); grid.innerHTML = ''; cars.forEach(c => grid.appendChild(card(c))); resultCount.textContent = `${cars.length} result${cars.length===1?'':'s'}`; } // Preview dialog function openPreview(c){ $('#vy-pvTitle').textContent = `${c.year} ${c.make} ${c.model}`; $('#vy-pvPrice').textContent = fmtAUD(c.price); $('#vy-pvKms').textContent = fmtKM(c.kms); $('#vy-pvFuel').textContent = c.fuel; $('#vy-pvBody').textContent = c.body; $('#vy-pvLoc').textContent = c.location; $('#vy-pvId').textContent = c.id; $('#vy-pvYear').textContent = c.year; const imgs = c.images && c.images.length ? c.images : ['']; pvImg.src = imgs[0]; pvThumbs.innerHTML = ''; imgs.forEach(src => { const b = document.createElement('button'); b.className='vy-thumb'; b.innerHTML=``; b.addEventListener('click', ()=>{ pvImg.src = src; }); pvThumbs.appendChild(b); }); // Tabs $$('.vy-tab').forEach(t=>t.classList.remove('active')); $('[data-tab="details"]').classList.add('active'); $('#vy-tab-details').classList.remove('vy-hidden'); $('#vy-tab-delivery').classList.add('vy-hidden'); $('#vy-tab-finance').classList.add('vy-hidden'); $('#vy-pvTabs').onclick = e => { const btn = e.target.closest('.vy-tab'); if(!btn) return; const tab = btn.dataset.tab; $$('.vy-tab').forEach(x=>x.classList.toggle('active', x===btn)); $('#vy-tab-details').classList.toggle('vy-hidden', tab!=='details'); $('#vy-tab-delivery').classList.toggle('vy-hidden', tab!=='delivery'); $('#vy-tab-finance').classList.toggle('vy-hidden', tab!=='finance'); }; $('#vy-pvClose').onclick = ()=> dlg.close(); $('#vy-pvEnquire').onclick = ()=> { dlg.close(); openEnquiry(c); }; try { dlg.showModal(); } catch { /* older browsers */ } } function openEnquiry(c){ // Redirect to your existing Virtual Yard form with prefilled fields const qs = new URLSearchParams({ vehicle: `${c.year} ${c.make} ${c.model}`, stockId: c.id, price: c.price, kms: c.kms, location: c.location }); window.location.href = `${FORM_URL}?${qs.toString()}`; } // CSV parsing (quotes + commas) function parseCSV(src){ const lines=[], out=[]; let cur='', q=false; for (let i=0;i{ const arr=[]; let cell='', qq=false; for(let i=0;i"]/g, m => ({'&':'&','<':'<','>':'>','"':'"'}[m])); } } })(); function startMap() { if(typeof initMap === "function") { initMap(); } else { } }