# 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.