From b5e5edda30da3d0267a3ff2f6f457d15410fc929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Tue, 7 Jul 2026 01:49:55 +0200 Subject: [PATCH] ci/cd fixes --- .gitea/workflows/docker.yaml | 4 +- .gitea/workflows/test-branches.yaml | 46 +- .../plans/2026-07-06-profile-redesign.md | 1135 +++++++++++++++++ package-lock.json | 25 +- 4 files changed, 1184 insertions(+), 26 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-06-profile-redesign.md diff --git a/.gitea/workflows/docker.yaml b/.gitea/workflows/docker.yaml index 32acfe0..832a48d 100644 --- a/.gitea/workflows/docker.yaml +++ b/.gitea/workflows/docker.yaml @@ -14,7 +14,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '24' cache: 'npm' - name: Install dependencies run: npm ci @@ -28,7 +28,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '24' cache: 'npm' - name: Install dependencies run: npm ci diff --git a/.gitea/workflows/test-branches.yaml b/.gitea/workflows/test-branches.yaml index 2abc517..a82d007 100644 --- a/.gitea/workflows/test-branches.yaml +++ b/.gitea/workflows/test-branches.yaml @@ -3,43 +3,43 @@ name: Run Tests on Branches on: push: branches-ignore: - - 'main' + - 'main' jobs: test-backend: name: Backend Tests runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@v4 + - name: Checkout Code + uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'npm' - - name: Install Dependencies - run: npm ci + - name: Install Dependencies + run: npm ci - - name: Run Backend Tests - run: npm test --workspace=farma-clic-backend -- --ci + - name: Run Backend Tests + run: npm test --workspace=farma-clic-backend -- --ci test-frontend: name: Frontend Tests runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@v4 + - name: Checkout Code + uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'npm' - - name: Install Dependencies - run: npm ci + - name: Install Dependencies + run: npm ci - - name: Run Frontend Tests - run: npm test --workspace=farma-clic-frontend -- --run --reporter=basic + - name: Run Frontend Tests + run: npm test --workspace=farma-clic-frontend -- --run --reporter=basic diff --git a/docs/superpowers/plans/2026-07-06-profile-redesign.md b/docs/superpowers/plans/2026-07-06-profile-redesign.md new file mode 100644 index 0000000..064bba8 --- /dev/null +++ b/docs/superpowers/plans/2026-07-06-profile-redesign.md @@ -0,0 +1,1135 @@ +# 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 diff --git a/package-lock.json b/package-lock.json index 6bc3d29..c8e181c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,9 @@ "turbo": "^2.5.0" } }, - "apps/API": {}, + "apps/API": { + "name": "farma-clic-api-sources" + }, "apps/backend": { "name": "farma-clic-backend", "version": "1.0.0", @@ -40,6 +42,7 @@ "@opentelemetry/sdk-trace-base": "^1.28.0", "@opentelemetry/semantic-conventions": "^1.28.0", "axios": "^1.6.0", + "barcode-detector": "^3.2.0", "bcrypt": "^5.1.1", "connect-pg-simple": "^10.0.0", "connect-sqlite3": "^0.9.16", @@ -103,6 +106,7 @@ "expo-constants": "~57.0.3", "expo-dev-client": "~57.0.5", "expo-device": "~7.0.2", + "expo-image-picker": "~57.0.2", "expo-linking": "~57.0.1", "expo-local-authentication": "~57.0.0", "expo-notifications": "~57.0.3", @@ -724,6 +728,25 @@ "react-native": "*" } }, + "apps/frontend-mobile/node_modules/expo-image-loader": { + "version": "57.0.0", + "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-57.0.0.tgz", + "integrity": "sha512-EhwnoPC4T/EMdB7Nsg7qITzhO/qB0hUnmVghmtAKQwwDVaLWWYCGE4NLkes3zk9Ub451SmS/swgPp4PCPzOlnw==", + "peerDependencies": { + "expo": "*" + } + }, + "apps/frontend-mobile/node_modules/expo-image-picker": { + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-57.0.2.tgz", + "integrity": "sha512-XW+C5weIthkOLCJZzExeRJf9pcWfaXNHSDm76gmZVhDUVIHdi9IS3eihAIF0WA0fBo9ogIfQs/iep/c6CzIMKg==", + "dependencies": { + "expo-image-loader": "~57.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "apps/frontend-mobile/node_modules/expo-keep-awake": { "version": "57.0.0", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.0.tgz",