# RSMotors.pk — Fullstack Responsive Website This project contains a lightweight fullstack web app (frontend + backend) to manage used car listings: add / update / delete / list. Backend uses Node.js + Express + SQLite. Frontend is a responsive single-page HTML + CSS + vanilla JS app. --- ## File structure ``` rsmotors-pk/ ├─ package.json ├─ server.js ├─ db-init.sql ├─ cars.db (created by server) ├─ public/ │ ├─ index.html │ ├─ styles.css │ └─ main.js └─ README.md ``` --- ## package.json ```json { "name": "rsmotors-pk", "version": "1.0.0", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "better-sqlite3": "^8.0.0", "express": "^4.18.2", "body-parser": "^1.20.2" } } ``` --- ## db-init.sql ```sql CREATE TABLE IF NOT EXISTS cars ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, make TEXT, model TEXT, year INTEGER, mileage INTEGER, color TEXT, price TEXT, location TEXT, image_url TEXT, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); ``` --- ## server.js ```js const express = require('express'); const bodyParser = require('body-parser'); const Database = require('better-sqlite3'); const path = require('path'); const app = express(); const db = new Database(path.join(__dirname, 'cars.db')); // init table if needed const initSql = require('fs').readFileSync(path.join(__dirname, 'db-init.sql'), 'utf8'); db.exec(initSql); app.use(bodyParser.json()); app.use(express.static(path.join(__dirname, 'public'))); // REST API app.get('/api/cars', (req, res) => { const rows = db.prepare('SELECT * FROM cars ORDER BY created_at DESC').all(); res.json(rows); }); app.get('/api/cars/:id', (req, res) => { const car = db.prepare('SELECT * FROM cars WHERE id = ?').get(req.params.id); if (!car) return res.status(404).json({ error: 'Not found' }); res.json(car); }); app.post('/api/cars', (req, res) => { const { title, make, model, year, mileage, color, price, location, image_url, description } = req.body; const stmt = db.prepare(`INSERT INTO cars (title, make, model, year, mileage, color, price, location, image_url, description) VALUES (?,?,?,?,?,?,?,?,?,?)`); const info = stmt.run(title, make, model, year || null, mileage || null, color, price, location, image_url, description); const car = db.prepare('SELECT * FROM cars WHERE id = ?').get(info.lastInsertRowid); res.json(car); }); app.put('/api/cars/:id', (req, res) => { const { title, make, model, year, mileage, color, price, location, image_url, description } = req.body; const stmt = db.prepare(`UPDATE cars SET title=?, make=?, model=?, year=?, mileage=?, color=?, price=?, location=?, image_url=?, description=? WHERE id=?`); stmt.run(title, make, model, year || null, mileage || null, color, price, location, image_url, description, req.params.id); const car = db.prepare('SELECT * FROM cars WHERE id = ?').get(req.params.id); res.json(car); }); app.delete('/api/cars/:id', (req, res) => { db.prepare('DELETE FROM cars WHERE id = ?').run(req.params.id); res.json({ ok: true }); }); // fallback app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html'))); const PORT = process.env.PORT || 3000; app.listen(PORT, () => console.log(`RSMotors.pk server running on http://localhost:${PORT}`)); ``` --- ## public/index.html ```html RSMotors.pk — Used Cars
``` --- ## public/styles.css ```css :root{--accent:#e53935;--muted:#666} *{box-sizing:border-box} body{font-family:Inter, system-ui, Arial; margin:0; color:#222} .container{max-width:1100px;margin:0 auto;padding:16px} .site-header{background:#fff;border-bottom:1px solid #eee} .site-header h1{margin:12px 0} .tag{color:var(--muted);margin:0} .actions{display:flex;gap:12px;align-items:center;margin:16px 0} .actions input{flex:1;padding:8px;border:1px solid #ddd;border-radius:6px} button{background:var(--accent);color:#fff;border:none;padding:8px 12px;border-radius:6px;cursor:pointer} .card{background:#fff;padding:16px;border-radius:8px;box-shadow:0 6px 18px rgba(0,0,0,0.05);margin-bottom:16px} .hidden{display:none} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px} .card-listing{background:#fff;border:1px solid #eee;border-radius:8px;overflow:hidden;display:flex;flex-direction:column} .card-listing img{width:100%;height:160px;object-fit:cover} .card-listing .meta{padding:10px} .card-listing .meta h3{margin:0 0 6px} .meta .row{display:flex;justify-content:space-between;color:var(--muted);font-size:14px} .list-actions{display:flex;gap:8px;padding:10px;border-top:1px solid #f0f0f0} .list-actions button{flex:1;padding:8px;font-size:14px} .form-actions{display:flex;gap:8px;margin-top:8px} label{display:block;margin:8px 0} input, textarea{width:100%;padding:8px;border:1px solid #ddd;border-radius:6px} textarea{min-height:80px} .site-footer{border-top:1px solid #eee;padding:12px;margin-top:24px;text-align:center;color:var(--muted)} /* Mobile tweaks */ @media (max-width:600px){.actions{flex-direction:column;align-items:stretch} .card-listing img{height:140px}} ``` --- ## public/main.js ```js const api = path => `/api${path}`; async function fetchCars(){ const res = await fetch(api('/cars')); return res.json(); } function el(tag, cls){ const e = document.createElement(tag); if(cls) e.className = cls; return e } function renderListing(car){ const card = el('div','card-listing'); const img = el('img'); img.src = car.image_url || 'https://via.placeholder.com/600x400?text=No+Image'; const meta = el('div','meta'); const h = el('h3'); h.textContent = car.title || `${car.make || ''} ${car.model || ''}`; const row = el('div','row'); row.innerHTML = `${car.year||''} • ${car.mileage?car.mileage+' km':''}${car.price||''}`; meta.appendChild(h); meta.appendChild(row); const desc = el('div'); desc.style.padding='0 10px 10px'; desc.textContent = car.location ? car.location + ' • ' + (car.color||'') : (car.description || ''); const actions = el('div','list-actions'); const btnEdit = document.createElement('button'); btnEdit.textContent='Edit'; btnEdit.onclick = ()=> openEditForm(car); const btnDel = document.createElement('button'); btnDel.textContent='Delete'; btnDel.onclick = ()=> removeCar(car.id); actions.appendChild(btnEdit); actions.appendChild(btnDel); card.appendChild(img); card.appendChild(meta); card.appendChild(desc); card.appendChild(actions); return card; } async function loadAndRender(){ const container = document.getElementById('listings'); container.innerHTML = 'Loading...'; const cars = await fetchCars(); container.innerHTML = ''; if(cars.length===0) container.innerHTML = '
No listings yet.
'; cars.forEach(c=> container.appendChild(renderListing(c))); } async function removeCar(id){ if(!confirm('Delete this listing?')) return; await fetch(api('/cars/'+id), { method:'DELETE' }); loadAndRender(); } function openAddForm(){ document.getElementById('form-title').textContent='Add New Car'; const form = document.getElementById('car-form'); form.reset(); form.id.value=''; document.getElementById('form-area').classList.remove('hidden'); } function openEditForm(car){ document.getElementById('form-title').textContent='Edit Car'; const f = document.getElementById('car-form'); f.id.value = car.id; f.title.value = car.title || ''; f.make.value = car.make || ''; f.model.value = car.model || ''; f.year.value = car.year||''; f.mileage.value = car.mileage||''; f.color.value = car.color||''; f.price.value = car.price||''; f.location.value = car.location||''; f.image_url.value = car.image_url||''; f.description.value = car.description||''; document.getElementById('form-area').classList.remove('hidden'); } async function submitForm(ev){ ev.preventDefault(); const f = ev.target; const data = { title: f.title.value, make: f.make.value, model: f.model.value, year: f.year.value||null, mileage: f.mileage.value||null, color: f.color.value, price: f.price.value, location: f.location.value, image_url: f.image_url.value, description: f.description.value }; if(f.id.value){ await fetch(api('/cars/'+f.id.value), { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) }); } else { await fetch(api('/cars'), { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) }); } document.getElementById('form-area').classList.add('hidden'); loadAndRender(); } async function init(){ document.getElementById('btn-show-add').onclick = openAddForm; document.getElementById('btn-cancel').onclick = ()=> document.getElementById('form-area').classList.add('hidden'); document.getElementById('car-form').onsubmit = submitForm; document.getElementById('search').oninput = async function(){ const q = this.value.toLowerCase(); const cars = await fetchCars(); const filtered = cars.filter(c => (c.title||'').toLowerCase().includes(q) || (c.make||'').toLowerCase().includes(q) || (c.model||'').toLowerCase().includes(q) || (c.location||'').toLowerCase().includes(q)); const container = document.getElementById('listings'); container.innerHTML = ''; if(filtered.length===0) container.innerHTML='
No results
'; filtered.forEach(c=> container.appendChild(renderListing(c))); } await loadAndRender(); } window.addEventListener('DOMContentLoaded', init); ``` --- ## README.md (run instructions) ``` 1. Install Node.js (v16+ recommended) 2. Save this project to a folder. 3. In project root run: npm install 4. Start server: npm start 5. Open in browser: http://localhost:3000 Notes: - The app uses an SQLite file `cars.db` created automatically. - Image upload is via URL. If you want file uploads, you can add multer and a public/uploads folder. - For production, set up reverse proxy (nginx), HTTPS, and a proper admin authentication. ``` --- If you'd like, I can: - Add image uploads (multer) and store files in `/public/uploads`. - Add a simple admin login (username/password) to protect add/edit/delete. - Convert frontend to React or Tailwind for a polished UI. Open this document's code editor to copy files or tell me which enhancement you'd like next.