# Profile Redesign Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Redesign the Profile section to be more user-friendly: editable name/last name, avatar upload, search history as addresses, and remove irrelevant sections.
**Architecture:** Add new fields to users table (first_name, last_name, avatar_url), create search_history table for location searches, update frontend/mobile profile views to be editable and modern.
**Tech Stack:** React (frontend), React Native/Expo (mobile), SQLite/PostgreSQL (backend), Node.js/Express
---
## File Structure
### Backend
- Modify: `apps/backend/server.js` - Add new fields, search history API
- Create: `apps/backend/migrations/` - Database migration scripts
### Frontend (Web)
- Modify: `apps/frontend/src/views/ProfileView.jsx` - Redesign profile view
- Modify: `apps/frontend/src/views/ProfileView.css` - Update styles
### Mobile
- Modify: `apps/frontend-mobile/app/(tabs)/profile.tsx` - Redesign mobile profile
---
## Task 1: Backend - Add new user fields and search history table
**Files:**
- Modify: `apps/backend/server.js`
- [ ] **Step 1: Add migration for new user fields**
Add to `initDatabase()` function, after existing user table creation:
```javascript
// Add new fields for profile redesign
if (pgPool) {
await pgPool.query(`
ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT;
`);
} else {
try { await dbRun('ALTER TABLE users ADD COLUMN first_name TEXT'); } catch {}
try { await dbRun('ALTER TABLE users ADD COLUMN last_name TEXT'); } catch {}
try { await dbRun('ALTER TABLE users ADD COLUMN avatar_url TEXT'); } catch {}
}
```
- [ ] **Step 2: Create search_history table**
Add after user alerts table creation:
```javascript
// Search history for "find pharmacies near me"
if (pgPool) {
await pgPool.query(`
CREATE TABLE IF NOT EXISTS search_history (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
address TEXT NOT NULL,
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
created_at TIMESTAMPTZ DEFAULT NOW()
)
`);
} else {
await dbRun(`
CREATE TABLE IF NOT EXISTS search_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
address TEXT NOT NULL,
latitude REAL,
longitude REAL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
}
```
- [ ] **Step 3: Update GET /api/users/me to include new fields**
Change the SELECT query in `app.get('/api/users/me', ...)`:
```javascript
const user = await userDbGet(
'SELECT id, username, first_name, last_name, avatar_url, is_admin, address, latitude, longitude FROM users WHERE id = ?',
[req.session.userId]
);
```
And update the response:
```javascript
res.json({
id: user.id,
username: user.username,
first_name: user.first_name || null,
last_name: user.last_name || null,
avatar_url: user.avatar_url || null,
is_admin: Boolean(user.is_admin),
address: user.address || null,
latitude: user.latitude,
longitude: user.longitude,
});
```
- [ ] **Step 4: Update PUT /api/users/me to handle new fields**
Change the PUT handler to accept and save new fields:
```javascript
app.put('/api/users/me', requireAuth, async (req, res) => {
try {
const { first_name, last_name, avatar_url, address, latitude, longitude } = req.body || {};
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);
// ... existing address/latitude/longitude handling ...
await userDbRun(
'UPDATE users SET first_name = ?, last_name = ?, avatar_url = ?, address = ?, latitude = ?, longitude = ? WHERE id = ?',
[fName, lName, avatar, addr, lat, lon, req.session.userId]
);
// ... rest of response ...
}
});
```
- [ ] **Step 5: Add search history API endpoints**
Add after user alerts API:
```javascript
// Get search history for current user
app.get('/api/search-history', requireAuth, async (req, res) => {
try {
const rows = await userDbAll(
'SELECT id, address, latitude, longitude, created_at FROM search_history WHERE user_id = ? ORDER BY created_at DESC LIMIT 20',
[req.session.userId]
);
res.json(rows);
} catch (error) {
console.error('Error fetching search history:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Add new search to history
app.post('/api/search-history', requireAuth, async (req, res) => {
try {
const { address, latitude, longitude } = req.body || {};
if (!address) {
return res.status(400).json({ error: 'Address is required' });
}
const result = await userDbRun(
'INSERT INTO search_history (user_id, address, latitude, longitude) VALUES (?, ?, ?, ?)',
[req.session.userId, address, latitude || null, longitude || null]
);
res.json({ id: result.lastID, address, latitude, longitude });
} catch (error) {
console.error('Error adding search history:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Delete search history item
app.delete('/api/search-history/:id', requireAuth, async (req, res) => {
try {
await userDbRun(
'DELETE FROM search_history WHERE id = ? AND user_id = ?',
[req.params.id, req.session.userId]
);
res.json({ ok: true });
} catch (error) {
console.error('Error deleting search history:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
```
- [ ] **Step 6: Commit backend changes**
```bash
git add apps/backend/server.js
git commit -m "feat: add profile fields and search history API"
```
---
## Task 2: Frontend - Redesign ProfileView
**Files:**
- Modify: `apps/frontend/src/views/ProfileView.jsx`
- Modify: `apps/frontend/src/views/ProfileView.css`
- [ ] **Step 1: Update ProfileView.jsx with editable fields**
Replace the entire ProfileView.jsx with:
```jsx
import React, { useEffect, useState, useRef } from 'react';
import './ProfileView.css';
function ProfileView({ currentUser, onProfileSaved, onShowSaved, onLogout, onAdminClick }) {
const [firstName, setFirstName] = useState(currentUser?.first_name || '');
const [lastName, setLastName] = useState(currentUser?.last_name || '');
const [avatarUrl, setAvatarUrl] = useState(currentUser?.avatar_url || '');
const [saving, setSaving] = useState(false);
const [feedback, setFeedback] = useState(null);
const [searchHistory, setSearchHistory] = useState([]);
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef(null);
useEffect(() => {
setFirstName(currentUser?.first_name || '');
setLastName(currentUser?.last_name || '');
setAvatarUrl(currentUser?.avatar_url || '');
setFeedback(null);
loadSearchHistory();
}, [currentUser?.id]);
async function loadSearchHistory() {
try {
const res = await fetch('/api/search-history', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
setSearchHistory(data);
}
} catch (err) {
console.error('Error loading search history:', err);
}
}
async function handleSave(e) {
e?.preventDefault();
setSaving(true);
setFeedback(null);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
first_name: firstName.trim() || null,
last_name: lastName.trim() || null,
avatar_url: avatarUrl || null,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `Error al guardar (HTTP ${res.status})`);
}
const updated = await res.json();
onProfileSaved?.(updated);
setFeedback({ type: 'ok', text: 'Perfil guardado.' });
} catch (err) {
setFeedback({ type: 'err', text: err.message || 'Error al guardar' });
} finally {
setSaving(false);
}
}
async function handleAvatarUpload(e) {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
setFeedback(null);
try {
// Convert to base64 for simplicity (in production, use a file upload service)
const reader = new FileReader();
reader.onload = async () => {
const base64 = reader.result;
setAvatarUrl(base64);
// Auto-save after upload
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ avatar_url: base64 }),
});
if (res.ok) {
const updated = await res.json();
onProfileSaved?.(updated);
setFeedback({ type: 'ok', text: 'Foto de perfil actualizada.' });
}
} catch (err) {
setFeedback({ type: 'err', text: 'Error al guardar la foto' });
}
setUploading(false);
};
reader.readAsDataURL(file);
} catch (err) {
setFeedback({ type: 'err', text: 'Error al procesar la imagen' });
setUploading(false);
}
}
async function handleDeleteSearch(id) {
try {
const res = await fetch(`/api/search-history/${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (res.ok) {
setSearchHistory(prev => prev.filter(item => item.id !== id));
}
} catch (err) {
console.error('Error deleting search:', err);
}
}
const displayName = [firstName, lastName].filter(Boolean).join(' ') || currentUser?.username || 'Usuario';
return (
{/* Avatar Section */}
fileInputRef.current?.click()}
>
{avatarUrl ? (

) : (
)}
{uploading &&
Subiendo foto...
}
{/* Editable Name Section */}
{/* Search History Section */}
{searchHistory.length > 0 && (
Tus búsquedas recientes:
{searchHistory.map((item) => (
{item.address}
))}
)}
{currentUser?.is_admin && (
)}
);
}
export default ProfileView;
```
- [ ] **Step 2: Update ProfileView.css with new styles**
Add these styles to the end of the CSS file:
```css
/* Profile Avatar Editable */
.profile-avatar-editable {
cursor: pointer;
position: relative;
}
.profile-avatar-editable:hover .profile-avatar-overlay {
opacity: 1;
}
.profile-avatar-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
color: white;
}
.profile-avatar-image {
width: 100%;
height: 100%;
border-radius: var(--radius-full);
object-fit: cover;
}
/* Editable Info Cards */
.info-card-input {
width: 100%;
padding: 0.75rem;
border: 2px solid var(--outline-variant);
border-radius: var(--radius);
background: var(--surface);
color: var(--on-surface);
font-size: 1rem;
font-family: inherit;
margin-top: 0.5rem;
}
.info-card-input:focus {
outline: none;
border-color: var(--primary);
}
.info-card-input:disabled {
opacity: 0.6;
}
/* Search History */
.profile-search-history {
background: var(--surface-container-lowest);
border: 1px solid var(--outline-variant);
border-radius: var(--radius-md);
padding: 1rem;
margin-top: 0.5rem;
}
.profile-search-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid var(--outline-variant);
}
.profile-search-item:last-child {
border-bottom: none;
}
.profile-search-address {
flex: 1;
font-size: 0.9rem;
color: var(--on-surface);
}
.profile-search-delete {
background: none;
border: none;
cursor: pointer;
color: var(--on-surface-variant);
padding: 0.25rem;
border-radius: var(--radius);
transition: background 0.15s;
}
.profile-search-delete:hover {
background: rgba(186, 26, 26, 0.1);
color: var(--error);
}
```
- [ ] **Step 3: Commit frontend changes**
```bash
git add apps/frontend/src/views/ProfileView.jsx apps/frontend/src/views/ProfileView.css
git commit -m "feat: redesign profile view with editable fields and search history"
```
---
## Task 3: Mobile - Redesign ProfileScreen
**Files:**
- Modify: `apps/frontend-mobile/app/(tabs)/profile.tsx`
- [ ] **Step 1: Update ProfileScreen.tsx with new functionality**
Replace the entire profile.tsx with:
```tsx
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { useAuth } from '../../hooks/useAuth';
import { colors, spacing, borderRadius } from '../../constants/theme';
interface SearchHistoryItem {
id: number;
address: string;
latitude: number | null;
longitude: number | null;
created_at: string;
}
export default function ProfileScreen() {
const router = useRouter();
const { user, isAuthenticated, isLoading, logout, isAdmin } = useAuth();
const [firstName, setFirstName] = useState(user?.first_name || '');
const [lastName, setLastName] = useState(user?.last_name || '');
const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url || '');
const [searchHistory, setSearchHistory] = useState([]);
const [saving, setSaving] = useState(false);
const [feedback, setFeedback] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
useEffect(() => {
if (isAuthenticated) {
loadSearchHistory();
}
}, [isAuthenticated]);
useEffect(() => {
setFirstName(user?.first_name || '');
setLastName(user?.last_name || '');
setAvatarUrl(user?.avatar_url || '');
}, [user]);
async function loadSearchHistory() {
try {
const res = await fetch('/api/search-history', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
setSearchHistory(data);
}
} catch (err) {
console.error('Error loading search history:', err);
}
}
async function handleSave() {
setSaving(true);
setFeedback(null);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
first_name: firstName.trim() || null,
last_name: lastName.trim() || null,
avatar_url: avatarUrl || null,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || 'Error al guardar');
}
setFeedback({ type: 'ok', text: 'Perfil guardado.' });
} catch (err: any) {
setFeedback({ type: 'err', text: err.message || 'Error al guardar' });
} finally {
setSaving(false);
}
}
async function handlePickImage() {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 0.8,
});
if (!result.canceled && result.assets[0]) {
setAvatarUrl(result.assets[0].uri);
// Auto-save
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ avatar_url: result.assets[0].uri }),
});
if (res.ok) {
setFeedback({ type: 'ok', text: 'Foto de perfil actualizada.' });
}
} catch (err) {
setFeedback({ type: 'err', text: 'Error al guardar la foto' });
}
}
}
async function handleDeleteSearch(id: number) {
try {
const res = await fetch(`/api/search-history/${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (res.ok) {
setSearchHistory(prev => prev.filter(item => item.id !== id));
}
} catch (err) {
console.error('Error deleting search:', err);
}
}
const handleLogout = async () => {
Alert.alert(
'Cerrar Sesión',
'¿Estás seguro que deseas cerrar sesión?',
[
{ text: 'Cancelar', style: 'cancel' },
{
text: 'Cerrar Sesión',
style: 'destructive',
onPress: async () => {
await logout();
router.replace('/auth/login');
}
},
]
);
};
if (isLoading) {
return (
Cargando...
);
}
if (!isAuthenticated) {
return (
Inicia Sesión
Inicia sesión para acceder a todas las funcionalidades
router.push('/auth/login')}
>
Iniciar Sesión
router.push('/auth/register')}
>
Crear cuenta nueva
);
}
return (
{/* Avatar Section */}
{avatarUrl ? (
) : (
)}
Toca para cambiar foto
{/* Editable Fields */}
Nombre
Apellidos
{feedback && (
{feedback.text}
)}
{saving ? (
) : (
Guardar Cambios
)}
{/* Menu Section */}
router.push('/saved')}>
Mis Direcciones
{searchHistory.length > 0 && (
Tus búsquedas recientes:
{searchHistory.map((item) => (
{item.address}
handleDeleteSearch(item.id)}
style={styles.searchDelete}
>
))}
)}
{isAdmin && (
Panel Admin
)}
{/* Logout Button */}
Cerrar Sesión
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
loadingText: {
textAlign: 'center',
marginTop: spacing.xxl,
color: colors.textSecondary,
},
authPrompt: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.xl,
},
authTitle: {
fontSize: 24,
fontWeight: 'bold',
color: colors.text,
marginTop: spacing.lg,
},
authSubtitle: {
fontSize: 16,
color: colors.textSecondary,
textAlign: 'center',
marginTop: spacing.sm,
marginBottom: spacing.xl,
},
avatarSection: {
alignItems: 'center',
padding: spacing.xl,
backgroundColor: colors.card,
},
avatarContainer: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: colors.background,
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
},
avatarImage: {
width: 100,
height: 100,
borderRadius: 50,
},
avatarOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
opacity: 0.8,
},
editAvatarText: {
marginTop: spacing.sm,
color: colors.textSecondary,
fontSize: 14,
},
formSection: {
padding: spacing.xl,
backgroundColor: colors.card,
marginTop: spacing.md,
},
inputGroup: {
marginBottom: spacing.md,
},
label: {
fontSize: 14,
fontWeight: '600',
color: colors.textSecondary,
marginBottom: spacing.xs,
},
input: {
borderWidth: 1,
borderColor: colors.border || '#E0E0E0',
borderRadius: borderRadius.md,
padding: spacing.md,
fontSize: 16,
color: colors.text,
},
feedback: {
padding: spacing.md,
borderRadius: borderRadius.md,
marginBottom: spacing.md,
},
feedbackOk: {
backgroundColor: 'rgba(0, 69, 13, 0.1)',
borderWidth: 1,
borderColor: 'rgba(0, 69, 13, 0.2)',
},
feedbackErr: {
backgroundColor: 'rgba(186, 26, 26, 0.1)',
borderWidth: 1,
borderColor: 'rgba(186, 26, 26, 0.2)',
},
feedbackText: {
fontSize: 14,
},
saveButton: {
backgroundColor: colors.primary,
borderRadius: borderRadius.md,
padding: spacing.md,
alignItems: 'center',
},
saveButtonDisabled: {
opacity: 0.6,
},
saveButtonText: {
color: colors.textInverse,
fontSize: 16,
fontWeight: '600',
},
menuSection: {
marginTop: spacing.md,
backgroundColor: colors.card,
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
padding: spacing.md,
borderBottomWidth: 1,
borderBottomColor: colors.separator,
},
menuText: {
flex: 1,
marginLeft: spacing.md,
fontSize: 16,
color: colors.text,
},
searchHistorySection: {
padding: spacing.md,
backgroundColor: colors.background,
borderBottomWidth: 1,
borderBottomColor: colors.separator,
},
searchHistoryTitle: {
fontSize: 14,
color: colors.textSecondary,
marginBottom: spacing.sm,
},
searchItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: spacing.sm,
borderBottomWidth: 1,
borderBottomColor: colors.separator,
},
searchAddress: {
flex: 1,
fontSize: 14,
color: colors.text,
},
searchDelete: {
padding: spacing.xs,
},
button: {
backgroundColor: colors.primary,
borderRadius: borderRadius.md,
paddingVertical: spacing.md,
paddingHorizontal: spacing.xl,
},
buttonText: {
color: colors.textInverse,
fontSize: 16,
fontWeight: '600',
},
linkButton: {
marginTop: spacing.md,
},
linkText: {
color: colors.primary,
fontSize: 14,
},
logoutButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
margin: spacing.xl,
padding: spacing.md,
backgroundColor: colors.card,
borderRadius: borderRadius.md,
borderWidth: 1,
borderColor: colors.danger,
},
logoutText: {
marginLeft: spacing.sm,
color: colors.danger,
fontSize: 16,
fontWeight: '500',
},
});
```
- [ ] **Step 2: Add expo-image-picker dependency**
```bash
cd apps/frontend-mobile && npx expo install expo-image-picker
```
- [ ] **Step 3: Commit mobile changes**
```bash
git add apps/frontend-mobile/app/\(tabs\)/profile.tsx
git commit -m "feat: redesign mobile profile with editable fields and avatar upload"
```
---
## Task 4: Integration - Save search history when user searches nearby
**Files:**
- Modify: `apps/frontend-mobile/app/(tabs)/index.tsx` or wherever the "find pharmacies near me" search happens
- [ ] **Step 1: Find the nearby search function**
Search for the component that handles "buscar farmacias cerca de mí" or similar nearby search.
- [ ] **Step 2: Add search history saving**
After a successful nearby search, add:
```javascript
// Save to search history
try {
await fetch('/api/search-history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
address: userAddress, // The address used in the search
latitude: userLatitude,
longitude: userLongitude,
}),
});
} catch (err) {
console.error('Error saving search history:', err);
}
```
- [ ] **Step 3: Commit integration changes**
```bash
git add
git commit -m "feat: save search history when searching nearby pharmacies"
```
---
## Task 5: Final verification and cleanup
- [ ] **Step 1: Run tests**
```bash
npm test
```
- [ ] **Step 2: Build and verify**
```bash
npm run build
```
- [ ] **Step 3: Final commit**
```bash
git add -A
git commit -m "chore: complete profile redesign implementation"
```
---
## Summary of Changes
1. **Backend**: Added `first_name`, `last_name`, `avatar_url` fields to users table; created `search_history` table; added API endpoints for search history CRUD
2. **Frontend (Web)**: Redesigned ProfileView with editable name fields, clickable avatar for photo upload, search history display, removed emergency contacts and text configuration sections
3. **Mobile**: Redesigned ProfileScreen with same features using React Native components and expo-image-picker
4. **Integration**: Added search history saving when user performs nearby pharmacy searches