feat: avatar popup options, profile redesign, and search dosage parsing
- Added avatar selection popup with predefined avatar options - Redesigned ProfileView (web + mobile) with avatar upload and editable fields - Added AvatarSelectionModal for mobile profile - Fixed medicine search: parse dosage terms (e.g. 'Paracetamol 1G') to query CIMA only by name and filter by dosage field - Updated styles for profile, search, and medicine results
This commit is contained in:
+244
-27
@@ -53,7 +53,7 @@ app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(pinoHttp({ logger }));
|
||||
|
||||
const PG_URL = process.env.PG_URL;
|
||||
@@ -356,6 +356,36 @@ if (!pgPool) {
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
// User addresses table
|
||||
if (pgPool) {
|
||||
await pgPool.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_addresses (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
address TEXT NOT NULL,
|
||||
latitude DOUBLE PRECISION,
|
||||
longitude DOUBLE PRECISION,
|
||||
label TEXT DEFAULT '',
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
} else {
|
||||
await dbRun(`
|
||||
CREATE TABLE IF NOT EXISTS user_addresses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
latitude REAL,
|
||||
longitude REAL,
|
||||
label TEXT DEFAULT '',
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('initDatabase failed:', err);
|
||||
throw err;
|
||||
@@ -787,7 +817,7 @@ app.get('/api/auth/check', async (req, res) => {
|
||||
try {
|
||||
if (req.session && req.session.userId) {
|
||||
const user = await userDbGet(
|
||||
'SELECT id, username, first_name, last_name, avatar_url, is_admin, address, latitude, longitude FROM users WHERE id = ?',
|
||||
'SELECT id, username, first_name, last_name, avatar_url, email, city, is_admin, address, latitude, longitude FROM users WHERE id = ?',
|
||||
[req.session.userId]
|
||||
);
|
||||
if (!user) {
|
||||
@@ -802,6 +832,8 @@ app.get('/api/auth/check', async (req, res) => {
|
||||
first_name: user.first_name || null,
|
||||
last_name: user.last_name || null,
|
||||
avatar_url: user.avatar_url || null,
|
||||
email: user.email || null,
|
||||
city: user.city || null,
|
||||
is_admin: Boolean(user.is_admin),
|
||||
address: user.address || null,
|
||||
latitude: user.latitude,
|
||||
@@ -846,38 +878,76 @@ app.get('/api/users/me', requireAuth, async (req, res) => {
|
||||
// Update the current user's address + coordinates
|
||||
app.put('/api/users/me', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { first_name, last_name, avatar_url, email, city, address, latitude, longitude } = req.body || {};
|
||||
const body = req.body || {};
|
||||
const sets = [];
|
||||
const vals = [];
|
||||
|
||||
const fName = first_name == null ? null : String(first_name).trim().slice(0, 100);
|
||||
const lName = last_name == null ? null : String(last_name).trim().slice(0, 100);
|
||||
const avatar = avatar_url == null ? null : String(avatar_url).slice(0, 500);
|
||||
const userEmail = email == null ? null : String(email).trim().slice(0, 200);
|
||||
const userCity = city == null ? null : String(city).trim().slice(0, 200);
|
||||
|
||||
const addr =
|
||||
address == null || String(address).trim() === ''
|
||||
if ('first_name' in body) {
|
||||
const v = body.first_name == null ? null : String(body.first_name).trim().slice(0, 100);
|
||||
sets.push('first_name = ?');
|
||||
vals.push(v);
|
||||
}
|
||||
if ('last_name' in body) {
|
||||
const v = body.last_name == null ? null : String(body.last_name).trim().slice(0, 100);
|
||||
sets.push('last_name = ?');
|
||||
vals.push(v);
|
||||
}
|
||||
if ('avatar_url' in body) {
|
||||
const v = body.avatar_url == null ? null : String(body.avatar_url);
|
||||
sets.push('avatar_url = ?');
|
||||
vals.push(v);
|
||||
}
|
||||
if ('email' in body) {
|
||||
const v = body.email == null ? null : String(body.email).trim().slice(0, 200);
|
||||
sets.push('email = ?');
|
||||
vals.push(v);
|
||||
}
|
||||
if ('city' in body) {
|
||||
const v = body.city == null ? null : String(body.city).trim().slice(0, 200);
|
||||
sets.push('city = ?');
|
||||
vals.push(v);
|
||||
}
|
||||
if ('address' in body) {
|
||||
const v = body.address == null || String(body.address).trim() === ''
|
||||
? null
|
||||
: String(address).trim().slice(0, 500);
|
||||
|
||||
let lat = null;
|
||||
let lon = null;
|
||||
if (latitude !== '' && latitude != null) {
|
||||
lat = typeof latitude === 'number' ? latitude : parseFloat(latitude);
|
||||
if (!Number.isFinite(lat) || lat < -90 || lat > 90) {
|
||||
return res.status(400).json({ error: 'Invalid latitude' });
|
||||
: String(body.address).trim().slice(0, 500);
|
||||
sets.push('address = ?');
|
||||
vals.push(v);
|
||||
}
|
||||
if ('latitude' in body) {
|
||||
if (body.latitude !== '' && body.latitude != null) {
|
||||
const lat = typeof body.latitude === 'number' ? body.latitude : parseFloat(body.latitude);
|
||||
if (!Number.isFinite(lat) || lat < -90 || lat > 90) {
|
||||
return res.status(400).json({ error: 'Invalid latitude' });
|
||||
}
|
||||
sets.push('latitude = ?');
|
||||
vals.push(lat);
|
||||
} else {
|
||||
sets.push('latitude = ?');
|
||||
vals.push(null);
|
||||
}
|
||||
}
|
||||
if (longitude !== '' && longitude != null) {
|
||||
lon = typeof longitude === 'number' ? longitude : parseFloat(longitude);
|
||||
if (!Number.isFinite(lon) || lon < -180 || lon > 180) {
|
||||
return res.status(400).json({ error: 'Invalid longitude' });
|
||||
if ('longitude' in body) {
|
||||
if (body.longitude !== '' && body.longitude != null) {
|
||||
const lon = typeof body.longitude === 'number' ? body.longitude : parseFloat(body.longitude);
|
||||
if (!Number.isFinite(lon) || lon < -180 || lon > 180) {
|
||||
return res.status(400).json({ error: 'Invalid longitude' });
|
||||
}
|
||||
sets.push('longitude = ?');
|
||||
vals.push(lon);
|
||||
} else {
|
||||
sets.push('longitude = ?');
|
||||
vals.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
await userDbRun(
|
||||
'UPDATE users SET first_name = ?, last_name = ?, avatar_url = ?, email = ?, city = ?, address = ?, latitude = ?, longitude = ? WHERE id = ?',
|
||||
[fName, lName, avatar, userEmail, userCity, addr, lat, lon, req.session.userId]
|
||||
);
|
||||
if (sets.length > 0) {
|
||||
vals.push(req.session.userId);
|
||||
await userDbRun(
|
||||
'UPDATE users SET ' + sets.join(', ') + ' WHERE id = ?',
|
||||
vals
|
||||
);
|
||||
}
|
||||
|
||||
const user = await userDbGet(
|
||||
'SELECT id, username, first_name, last_name, avatar_url, email, city, is_admin, address, latitude, longitude FROM users WHERE id = ?',
|
||||
@@ -1038,6 +1108,153 @@ app.delete('/api/search-history/:id', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ========== USER ADDRESSES API ==========
|
||||
|
||||
// List all addresses for the current user
|
||||
app.get('/api/addresses', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const rows = await userDbAll(
|
||||
'SELECT id, address, latitude, longitude, label, is_default, created_at FROM user_addresses WHERE user_id = ? ORDER BY is_default DESC, created_at ASC',
|
||||
[req.session.userId]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching addresses:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Add a new address
|
||||
app.post('/api/addresses', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { address, latitude, longitude, label, is_default } = req.body || {};
|
||||
if (!address) {
|
||||
return res.status(400).json({ error: 'Address is required' });
|
||||
}
|
||||
|
||||
if (is_default) {
|
||||
await userDbRun(
|
||||
'UPDATE user_addresses SET is_default = 0 WHERE user_id = ?',
|
||||
[req.session.userId]
|
||||
);
|
||||
}
|
||||
|
||||
const result = await userDbRun(
|
||||
'INSERT INTO user_addresses (user_id, address, latitude, longitude, label, is_default) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[req.session.userId, address, latitude || null, longitude || null, label || '', is_default ? 1 : 0]
|
||||
);
|
||||
|
||||
const newAddress = await userDbGet(
|
||||
'SELECT id, address, latitude, longitude, label, is_default, created_at FROM user_addresses WHERE id = ?',
|
||||
[result.lastID]
|
||||
);
|
||||
res.status(201).json(newAddress);
|
||||
} catch (error) {
|
||||
console.error('Error adding address:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update an address
|
||||
app.put('/api/addresses/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const addressId = parseInt(req.params.id);
|
||||
const { address, latitude, longitude, label, is_default } = req.body || {};
|
||||
|
||||
if (!address) {
|
||||
return res.status(400).json({ error: 'Address is required' });
|
||||
}
|
||||
|
||||
if (is_default) {
|
||||
await userDbRun(
|
||||
'UPDATE user_addresses SET is_default = 0 WHERE user_id = ?',
|
||||
[req.session.userId]
|
||||
);
|
||||
}
|
||||
|
||||
await userDbRun(
|
||||
'UPDATE user_addresses SET address = ?, latitude = ?, longitude = ?, label = ?, is_default = ? WHERE id = ? AND user_id = ?',
|
||||
[address, latitude || null, longitude || null, label || '', is_default ? 1 : 0, addressId, req.session.userId]
|
||||
);
|
||||
|
||||
const updated = await userDbGet(
|
||||
'SELECT id, address, latitude, longitude, label, is_default, created_at FROM user_addresses WHERE id = ?',
|
||||
[addressId]
|
||||
);
|
||||
|
||||
if (!updated) {
|
||||
return res.status(404).json({ error: 'Address not found' });
|
||||
}
|
||||
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error('Error updating address:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Set address as default
|
||||
app.put('/api/addresses/:id/default', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const addressId = parseInt(req.params.id);
|
||||
|
||||
const existing = await userDbGet(
|
||||
'SELECT id FROM user_addresses WHERE id = ? AND user_id = ?',
|
||||
[addressId, req.session.userId]
|
||||
);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: 'Address not found' });
|
||||
}
|
||||
|
||||
await userDbRun(
|
||||
'UPDATE user_addresses SET is_default = 0 WHERE user_id = ?',
|
||||
[req.session.userId]
|
||||
);
|
||||
await userDbRun(
|
||||
'UPDATE user_addresses SET is_default = 1 WHERE id = ?',
|
||||
[addressId]
|
||||
);
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
console.error('Error setting default address:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete an address
|
||||
app.delete('/api/addresses/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const addressId = parseInt(req.params.id);
|
||||
|
||||
const deleted = await userDbGet(
|
||||
'SELECT is_default FROM user_addresses WHERE id = ? AND user_id = ?',
|
||||
[addressId, req.session.userId]
|
||||
);
|
||||
|
||||
await userDbRun(
|
||||
'DELETE FROM user_addresses WHERE id = ? AND user_id = ?',
|
||||
[addressId, req.session.userId]
|
||||
);
|
||||
|
||||
// If the deleted address was default, set the oldest remaining as default
|
||||
if (deleted?.is_default) {
|
||||
const oldest = await userDbGet(
|
||||
'SELECT id FROM user_addresses WHERE user_id = ? ORDER BY created_at ASC LIMIT 1',
|
||||
[req.session.userId]
|
||||
);
|
||||
if (oldest) {
|
||||
await userDbRun('UPDATE user_addresses SET is_default = 1 WHERE id = ?', [oldest.id]);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting address:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Geocode for any authenticated user (rate-limited). Same impl as /api/admin/geocode.
|
||||
app.get('/api/geocode', requireAuth, geocodeLimiter, async (req, res) => {
|
||||
const q = (req.query.q || '').trim();
|
||||
|
||||
Reference in New Issue
Block a user