// ============================================================================
// Sun Systems Catalogue — Main App
// ============================================================================
const { useState, useEffect, useMemo, useRef, useCallback } = React;
// Brand config — edit me
const BRAND = {
name: 'Sun Systems',
tag: 'Pre-owned business laptops',
phone: '7013608439',
phoneDisplay: '+91 70136 08439',
};
// Shown as a banner when a customer filters by a single brand
const BRAND_TAGLINES = {
Dell: 'Built to endure — Latitude & Precision, the workhorses of business computing.',
HP: 'Sleek and dependable — EliteBook & ProBook for professional everyday work.',
Lenovo: 'Legendary ThinkPad reliability — the keyboard typists swear by.',
Apple: 'Premium power, beautifully built — MacBook Pro for creative work.',
Microsoft: 'Surface — premium 2-in-1 versatility for work on the move.',
};
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"theme": "light",
"density": 4,
"cardVariant": "minimal"
}/*EDITMODE-END*/;
const SORTS = {
'featured': { label: 'Featured first', fn: (a,b) => (b.featured?1:0) - (a.featured?1:0) || (a.stock==='sold-out'?1:0) - (b.stock==='sold-out'?1:0) },
'price-asc': { label: 'Price: low → high', fn: (a,b) => a.priceMin - b.priceMin },
'price-desc': { label: 'Price: high → low', fn: (a,b) => b.priceMin - a.priceMin },
'newest': { label: 'Newest generation', fn: (a,b) => (b.generation||0) - (a.generation||0) },
'ram': { label: 'Most RAM', fn: (a,b) => b.ram - a.ram },
};
// ============================================================================
// DATA SOURCE — paste your published Google Sheet CSV link between the quotes.
// Empty string ('') = use the offline list in products.js.
// Setup steps are in GOOGLE-SHEET-SETUP.txt
// ============================================================================
const GOOGLE_SHEET_CSV_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQFQr_EWfGedSwGiLn7I6V4OvIPl2q0dKCX37r0T1iNmgQDN-0Plhvg2rJBBG93VH8leSKN5lptsy9l/pub?gid=1093569637&single=true&output=csv';
// ---------- Filter chip set helper ----------
function uniqSorted(arr, cmp) {
return [...new Set(arr)].filter(x => x !== null && x !== undefined && x !== '').sort(cmp);
}
// ---------- Tiny CSV parser (handles quotes, commas, newlines) ----------
function parseCsv(text) {
const rows = [];
let cur = [], val = '', inQ = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (inQ) {
if (c === '"' && text[i+1] === '"') { val += '"'; i++; }
else if (c === '"') { inQ = false; }
else { val += c; }
} else {
if (c === '"') inQ = true;
else if (c === ',') { cur.push(val); val = ''; }
else if (c === '\n') { cur.push(val); rows.push(cur); cur = []; val = ''; }
else if (c === '\r') { /* skip */ }
else { val += c; }
}
}
if (val.length || cur.length) { cur.push(val); rows.push(cur); }
if (!rows.length) return [];
const headers = rows[0].map(h => h.trim().toLowerCase().replace(/[^a-z0-9]/g, ''));
return rows.slice(1).filter(r => r.some(v => v.trim() !== '')).map(r => {
const o = {};
headers.forEach((h, i) => o[h] = r[i] !== undefined ? r[i].trim() : '');
return o;
});
}
// ---------- Infer CPU tier & generation from a processor string ----------
function inferCpuTier(p = '') {
const s = p.toLowerCase();
if (s.includes('i9')) return 'i9';
if (s.includes('i7')) return 'i7';
if (s.includes('i5')) return 'i5';
if (s.includes('i3')) return 'i3';
if (s.includes('xeon')) return 'Xeon';
if (s.includes('ryzen 7')) return 'Ryzen 7';
if (s.includes('ryzen 5')) return 'Ryzen 5';
if (s.includes('amd')) return 'AMD';
return 'Other';
}
function inferGen(p = '') { const m = String(p).match(/(\d+)\s*(st|nd|rd|th)?\s*gen/i); return m ? parseInt(m[1]) : null; }
// ---------- Normalize one raw row (from products.js OR the Google Sheet) ----------
function normalizeProduct(raw, i) {
// Accept several spellings. Sheet rows arrive lowercased+stripped (parseCsv);
// products.js rows use camelCase keys — so check BOTH forms for each key.
const g = (...keys) => {
for (const k of keys) {
const stripped = k.toLowerCase().replace(/[^a-z0-9]/g, '');
for (const kk of [k, stripped]) {
if (raw[kk] !== undefined && raw[kk] !== '' && raw[kk] !== null) return raw[kk];
}
}
return '';
};
const num = (x) => { const n = parseInt(String(x).replace(/[^0-9.]/g, '')); return isNaN(n) ? null : n; };
const truthy = (x) => /^(1|true|yes|y)$/i.test(String(x).trim());
const model = String(g('model', 'name') || `Item ${i+1}`).trim();
const processor = String(g('processor', 'cpu')).trim();
const baseRam = num(g('baseRam', 'ram')) || 8;
const baseStorage = num(g('baseStorage', 'storage', 'ssd')) || 256;
const basePrice = num(g('basePrice', 'price')) || 0;
const addRam = num(g('addRam', 'ramUpgrade', 'plusRam'));
const addSsd = num(g('addSsd', 'ssdUpgrade', 'plusSsd', 'addStorage'));
const display = parseFloat(String(g('display', 'screen', 'screensize')).replace(/[^0-9.]/g, '')) || 0;
const displayNote = String(g('displayNote', 'screennote')).trim();
const gfx = String(g('graphics', 'gpu')).trim();
const brand = String(g('brand') || 'Other').trim();
const series = String(g('series') || 'Other').trim();
const stockRaw = String(g('stock', 'availability')).toLowerCase();
const images = [g('image1', 'image', 'img1'), g('image2', 'img2'), g('image3', 'img3')]
.map(x => String(x).trim()).filter(Boolean);
const v = variantsOf({ baseRam, baseStorage, basePrice, addRam, addSsd });
return {
id: String(g('id') || ('p' + String(i+1).padStart(3, '0'))),
model, brand, series, processor,
cpuTier: String(g('cputier') || inferCpuTier(processor)),
generation: num(g('generation', 'gen')) ?? inferGen(processor),
baseRam, baseStorage, basePrice, addRam, addSsd,
ramOptions: v.ramOptions, storageOptions: v.storageOptions,
ram: baseRam, storage: baseStorage,
ramLabel: `${baseRam}GB`, storageLabel: `${baseStorage}GB SSD`,
display, displayNote,
displayLabel: display ? `${display} in${displayNote ? ` (${displayNote})` : ''}` : '—',
graphics: gfx && gfx !== '-' ? gfx : null,
hasDedicatedGpu: !!(gfx && gfx !== '-'),
priceMin: v.priceMin, priceMax: v.priceMax,
images, image: images[0] || null,
stock: stockRaw.includes('sold') || stockRaw.includes('out') ? 'sold-out' : 'in-stock',
featured: truthy(g('featured')),
notes: String(g('notes', 'note')).trim(),
};
}
function normalizeAll(rows) { return (rows || []).map(normalizeProduct); }
// ---------- Persistent state hook ----------
function useLocalState(key, initial) {
const [v, setV] = useState(() => {
try {
const raw = localStorage.getItem(key);
if (raw !== null) return JSON.parse(raw);
} catch (e) {}
return initial;
});
useEffect(() => {
try { localStorage.setItem(key, JSON.stringify(v)); } catch (e) {}
}, [key, v]);
return [v, setV];
}
// ---------- Persistent tweaks (stays across reloads) ----------
function usePersistentTweaks(defaults) {
const [vals, setVals] = useLocalState('sun-systems-tweaks', defaults);
useEffect(() => { setVals(v => ({ ...defaults, ...v })); /* eslint-disable-next-line */ }, []);
const setTweak = useCallback((keyOrEdits, val) => {
setVals(v => {
if (typeof keyOrEdits === 'object') return { ...v, ...keyOrEdits };
return { ...v, [keyOrEdits]: val };
});
}, [setVals]);
return [vals, setTweak];
}
function App() {
const [tweaks, setTweak] = usePersistentTweaks(TWEAK_DEFAULTS);
// Start with the bundled fallback list, then (if configured) swap in live sheet data
const [products, setProducts] = useState(() => normalizeAll(window.PRODUCTS || []));
const [search, setSearch] = useState('');
const [sort, setSort] = useState('featured');
const [showFilters, setShowFilters] = useState(false);
const [filters, setFilters] = useState({
brand: [],
cpuTier: [],
generation: [],
ram: [],
priceMin: '',
priceMax: '',
inStockOnly: false,
});
const [openProduct, setOpenProduct] = useState(null);
const [compareIds, setCompareIds] = useState([]);
const [showCompare, setShowCompare] = useState(false);
const [toast, setToast] = useState('');
// Track viewport width so we can hide desktop-only options on mobile (< 768px)
const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);
useEffect(() => {
const onResize = () => setIsMobile(window.innerWidth < 768);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
// Apply theme
useEffect(() => {
document.documentElement.dataset.theme = tweaks.theme;
}, [tweaks.theme]);
// Load live data from the Google Sheet (falls back to products.js on any error)
useEffect(() => {
if (!GOOGLE_SHEET_CSV_URL) return;
let cancelled = false;
fetch(GOOGLE_SHEET_CSV_URL)
.then(r => r.text())
.then(txt => {
const rows = normalizeAll(parseCsv(txt));
if (!cancelled && rows.length) setProducts(rows);
})
.catch(() => { /* keep the bundled fallback list */ });
return () => { cancelled = true; };
}, []);
// Toast helper
const showToast = useCallback((msg) => {
setToast(msg);
setTimeout(() => setToast(''), 2200);
}, []);
// ---------- Filter options ----------
const opts = useMemo(() => ({
brands: uniqSorted(products.map(p => p.brand)),
cpuTiers: ['i3','i5','i7','i9','Xeon','Ryzen 5','Ryzen 7','AMD','Other'].filter(t => products.some(p => p.cpuTier === t)),
generations: uniqSorted(products.map(p => p.generation), (a,b) => b - a),
rams: uniqSorted(products.flatMap(p => p.ramOptions || [p.ram]), (a,b) => a - b),
}), [products]);
// ---------- Filtered + sorted ----------
const visible = useMemo(() => {
const q = search.trim().toLowerCase();
let result = products.filter(p => {
if (q) {
const hay = [p.model, p.brand, p.series, p.processor, p.cpuTier, p.ramLabel, p.storageLabel].join(' ').toLowerCase();
if (!hay.includes(q)) return false;
}
if (filters.brand.length && !filters.brand.includes(p.brand)) return false;
if (filters.cpuTier.length && !filters.cpuTier.includes(p.cpuTier)) return false;
if (filters.generation.length && !filters.generation.includes(p.generation)) return false;
if (filters.ram.length && !filters.ram.some(r => (p.ramOptions || [p.ram]).includes(r))) return false;
if (filters.inStockOnly && p.stock !== 'in-stock') return false;
const pmin = parseInt(filters.priceMin) || 0;
const pmax = parseInt(filters.priceMax) || Infinity;
if (p.priceMax < pmin || p.priceMin > pmax) return false;
return true;
});
result.sort(SORTS[sort].fn);
return result;
}, [products, search, filters, sort]);
const activeFilterCount = (
filters.brand.length + filters.cpuTier.length + filters.generation.length +
filters.ram.length + (filters.priceMin?1:0) + (filters.priceMax?1:0) + (filters.inStockOnly?1:0)
);
const compareItems = compareIds.map(id => products.find(p => p.id === id)).filter(Boolean);
// ---------- Toggle helpers ----------
const toggleFilter = (key, value) => {
setFilters(f => ({
...f,
[key]: f[key].includes(value) ? f[key].filter(v => v !== value) : [...f[key], value]
}));
};
const clearFilters = () => setFilters({ brand:[], cpuTier:[], generation:[], ram:[], priceMin:'', priceMax:'', inStockOnly:false });
const toggleCompare = (product) => {
setCompareIds(ids => {
if (ids.includes(product.id)) {
return ids.filter(i => i !== product.id);
}
if (ids.length >= 3) {
showToast('You can compare up to 3 laptops at a time');
return ids;
}
showToast(`Added ${product.model} to compare`);
return [...ids, product.id];
});
};
return (
<>
Flypzo is the online store of Sun Systems — Hyderabad's trusted name in refurbished
business laptops. We stock pre-owned Dell, HP, Lenovo and Apple machines, fully tested and professionally
cleaned, each covered by a 1-month testing warranty and a 6-month service warranty. Browse, compare specs,
and message us on WhatsApp to confirm your model.
A curated catalogue of business-grade laptops, ready to ship.
Try clearing some filters or adjusting your search.
{activeFilterCount > 0 && }Drop your details and we'll get back within a few hours with availability, photos and the latest price. For instant replies, message us on WhatsApp.