# 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 ? ( Avatar ) : ( )}
{uploading &&

Subiendo foto...

}
{/* Editable Name Section */}
setFirstName(e.target.value)} placeholder="Tu nombre" disabled={saving} />
setLastName(e.target.value)} placeholder="Tus apellidos" disabled={saving} />
{feedback && (

{feedback.text}

)}
{/* 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