diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..9683fd1 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,21 @@ +name: Lint and Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: '22.x' + - run: npm install + - run: npm install -g typescript + - run: tsc --noEmit diff --git a/Build_plan.md b/Build_plan.md index ae96331..01f7e3a 100644 --- a/Build_plan.md +++ b/Build_plan.md @@ -93,7 +93,7 @@ All Jules prompts are written by the Orchestrator. Copilot reviews every PR. Orc ### 🔲 Phase 6 — Profile Screen | # | Task | Status | Notes | |---|---|---|---| -| 6.1 | Profile screen | ✅ Done | Name, occupation, custom categories management, view onboarding answers, sign out | +| 6.1 | Profile screen | ✅ Done | Name, occupation, custom categories management, view onboarding answers, sign out, behaviour settings | --- @@ -203,8 +203,9 @@ Do not build any of the following until explicitly added to the build plan: | 09 | Home screen + LogModal | ✅ Merged | | 10 | History screen | ✅ Merged | | 11 | Coach screen | ✅ Merged | -| 12 | Profile screen | 🔲 Pending — prompt ready in handoff document | +| 12 | Profile screen | ✅ Merged | | 13 | ProfileContext — shared profile state | ✅ Merged | +| 17 | Profile screen rework — edit UX, visual refresh, behaviour settings | ✅ Merged | | 09 | Home screen + LogModal component | ✅ Merged | | 09b | Undo window + deleteLog | ✅ Merged | @@ -224,6 +225,7 @@ Do not build any of the following until explicitly added to the build plan: - ProfileContext silent sign-out removed (was incorrectly signing out new users) - `isCreatingAccount` flag added to ProfileContext - **HF-01**: Model string updates (Sonnet 4.6, Haiku dated) — ✅ Merged +- **HF-04**: Profile rework PR fixes — icon paths, LogContext type, LogModal dep array, error handling — ✅ Merged - **HF-02**: Dead code cleanup — getLogs filter + deleteLog rename — ✅ Merged --- diff --git a/src/components/LogModal.tsx b/src/components/LogModal.tsx index 373c637..6873c9c 100644 --- a/src/components/LogModal.tsx +++ b/src/components/LogModal.tsx @@ -13,6 +13,7 @@ import { import { Colors, Fonts, FontSizes, Radius, DEFAULT_CATEGORIES } from '../constants/theme'; import { PillButton } from './PillButton'; import type { LogContext } from '../lib/types'; +import { useProfile } from '../hooks/useProfile'; interface LogModalProps { visible: boolean; @@ -31,9 +32,13 @@ export const LogModal = ({ customCategories, onAddCategory }: LogModalProps) => { + const { profile } = useProfile(); + const [selectedCategory, setSelectedCategory] = useState(''); const [note, setNote] = useState(''); - const [selectedContext, setSelectedContext] = useState(); + const [selectedContext, setSelectedContext] = useState( + profile?.default_context ?? undefined + ); const [addingCategory, setAddingCategory] = useState(false); const [newCategoryInput, setNewCategoryInput] = useState(''); const [loading, setLoading] = useState(false); @@ -41,7 +46,7 @@ export const LogModal = ({ const resetState = () => { setSelectedCategory(''); setNote(''); - setSelectedContext(undefined); + setSelectedContext(profile?.default_context ?? undefined); setAddingCategory(false); setNewCategoryInput(''); }; @@ -57,7 +62,7 @@ export const LogModal = ({ // Reset category/context when switching between win and sin. useEffect(() => { resetState(); - }, [type]); + }, [type, profile?.default_context]); const isWin = type === 'win'; const activeColor = isWin ? Colors.primary : Colors.amber; @@ -78,7 +83,7 @@ export const LogModal = ({ // Reset state setSelectedCategory(''); setNote(''); - setSelectedContext(undefined); + setSelectedContext(profile?.default_context ?? undefined); setAddingCategory(false); setNewCategoryInput(''); } catch (err: unknown) { diff --git a/src/components/icons/GearIcon.tsx b/src/components/icons/GearIcon.tsx new file mode 100644 index 0000000..8eb2e56 --- /dev/null +++ b/src/components/icons/GearIcon.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; +import { Colors } from '../../constants/theme'; + +interface Props { + size?: number; + color?: string; +} + +export const GearIcon: React.FC = ({ size = 12, color = Colors.primary }) => { + return ( + + + + + ); +}; diff --git a/src/components/icons/TargetIcon.tsx b/src/components/icons/TargetIcon.tsx new file mode 100644 index 0000000..18a21f4 --- /dev/null +++ b/src/components/icons/TargetIcon.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; +import { Colors } from '../../constants/theme'; + +interface Props { + size?: number; + color?: string; +} + +export const TargetIcon: React.FC = ({ size = 12, color = Colors.primary }) => { + return ( + + + + + ); +}; diff --git a/src/lib/types.ts b/src/lib/types.ts index 4691fa9..70c90bb 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -25,6 +25,7 @@ export interface UserProfile { custom_categories: string[]; created_at: string; onboarded: boolean; + default_context?: LogContext | null; } export interface AppState { diff --git a/src/screens/ProfileScreen.tsx b/src/screens/ProfileScreen.tsx index 144db6c..dff54e4 100644 --- a/src/screens/ProfileScreen.tsx +++ b/src/screens/ProfileScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { View, Text, @@ -7,30 +7,42 @@ import { TextInput, StyleSheet, Alert, + KeyboardAvoidingView, + Platform, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Colors, Fonts, FontSizes, Spacing, Radius, BorderWidths, Sizes, DEFAULT_CATEGORIES } from '../constants/theme'; import { useProfile } from '../hooks/useProfile'; import { useAuth } from '../hooks/useAuth'; +import { LogContext } from '../lib/types'; import { Card } from '../components/Card'; import { PillButton } from '../components/PillButton'; import { PersonIcon } from '../components/icons/PersonIcon'; import { CloseIcon } from '../components/icons/CloseIcon'; +import { TargetIcon } from '../components/icons/TargetIcon'; +import { ChipIcon } from '../components/icons/ChipIcon'; +import { BrainIcon } from '../components/icons/BrainIcon'; +import { GearIcon } from '../components/icons/GearIcon'; export default function ProfileScreen() { const { profile, updateProfile } = useProfile(); const { signOut } = useAuth(); const insets = useSafeAreaInsets(); - const [editingName, setEditingName] = useState(false); - const [nameDraft, setNameDraft] = useState(''); + const scrollViewRef = useRef(null); - const [editingOccupation, setEditingOccupation] = useState(false); + const [nameDraft, setNameDraft] = useState(''); const [occupationDraft, setOccupationDraft] = useState(''); - - const [editingGoal, setEditingGoal] = useState(false); const [goalDraft, setGoalDraft] = useState(''); + useEffect(() => { + if (profile) { + setNameDraft(profile.name || ''); + setOccupationDraft(profile.occupation || ''); + setGoalDraft(profile.goal || ''); + } + }, [profile]); + const [newCategoryInput, setNewCategoryInput] = useState(''); const [showAddCategory, setShowAddCategory] = useState(false); @@ -53,107 +65,110 @@ export default function ProfileScreen() { custom_categories: [...(profile?.custom_categories ?? []), normalised] }); setNewCategoryInput(''); - setShowAddCategory(false); + }; + + const hasNameOrOccChanges = nameDraft.trim() !== (profile?.name || '') || occupationDraft.trim() !== (profile?.occupation || ''); + const hasGoalChanges = goalDraft.trim() !== (profile?.goal || ''); + + const scrollToInput = (yOffset: number) => { + scrollViewRef.current?.scrollTo({ y: yOffset, animated: true }); }; return ( - + {/* Header section */} - + - {editingName ? ( - { - updateProfile({ name: nameDraft.trim() }); - setEditingName(false); - }} - onBlur={() => { - if (nameDraft.trim() !== profile?.name) { - updateProfile({ name: nameDraft.trim() }); - } - setEditingName(false); - }} - /> - ) : ( - { - setEditingName(true); - setNameDraft(profile?.name ?? ''); - }}> - {profile?.name || 'Add Name'} - - )} + scrollToInput(0)} + /> - {editingOccupation ? ( - { - updateProfile({ occupation: occupationDraft.trim() }); - setEditingOccupation(false); - }} - onBlur={() => { - if (occupationDraft.trim() !== profile?.occupation) { - updateProfile({ occupation: occupationDraft.trim() }); + scrollToInput(50)} + /> + + + {hasNameOrOccChanges && ( + + { + try { + await updateProfile({ name: nameDraft.trim(), occupation: occupationDraft.trim() }); + } catch (err) { + Alert.alert('Error', 'Failed to save. Please try again.'); } - setEditingOccupation(false); }} /> - ) : ( - { - setEditingOccupation(true); - setOccupationDraft(profile?.occupation ?? ''); - }}> - {profile?.occupation || 'Add Occupation'} - - )} - + + )} {/* Your Goals */} - YOUR GOALS + + + YOUR GOALS + What you want to do without AI - {editingGoal ? ( - { - updateProfile({ goal: goalDraft.trim() }); - setEditingGoal(false); - }} - /> - ) : ( - { - setEditingGoal(true); - setGoalDraft(profile?.goal ?? ''); - }}> - {profile?.goal || 'Add Goal'} - + scrollToInput(150)} + /> + {hasGoalChanges && ( + + { + try { + await updateProfile({ goal: goalDraft.trim() }); + } catch (err) { + Alert.alert('Error', 'Failed to save. Please try again.'); + } + }} + /> + )} @@ -168,7 +183,10 @@ export default function ProfileScreen() { {/* AI Tools */} - AI TOOLS YOU USE + + + AI TOOLS YOU USE + {profile?.ai_tools_used?.map((tool: string) => ( - YOUR CATEGORIES + + + YOUR CATEGORIES + Defaults plus any you've added. @@ -221,10 +242,14 @@ export default function ProfileScreen() { { text: 'Archive', style: 'destructive', - onPress: () => { - updateProfile({ - custom_categories: (profile?.custom_categories ?? []).filter((c: string) => c !== cat) - }); + onPress: async () => { + try { + await updateProfile({ + custom_categories: (profile?.custom_categories ?? []).filter((c: string) => c !== cat) + }); + } catch (err) { + Alert.alert('Error', 'Failed to save. Please try again.'); + } } } ] @@ -245,6 +270,8 @@ export default function ProfileScreen() { autoFocus autoCapitalize="none" onSubmitEditing={handleAddCategory} + maxLength={30} + onFocus={() => scrollToInput(400)} /> Add @@ -266,11 +293,93 @@ export default function ProfileScreen() { )} + + {/* Behaviour */} + + + + BEHAVIOUR + + + + Default log context + Pre-selects work or personal on every new log. + + + {['None', 'Work', 'Personal'].map((option) => { + const val = option === 'None' ? null : option.toLowerCase(); + const isSelected = (profile?.default_context ?? null) === val; + + return ( + { + try { + await updateProfile({ default_context: val as LogContext | null }); + } catch (err) { + Alert.alert('Error', 'Failed to save. Please try again.'); + } + }} + > + + {option} + + + ); + })} + + + + {/* Account */} - ACCOUNT + + + ACCOUNT + + { + Alert.alert( + "Coming soon", + "Data export will be available in an upcoming update.", + [{ text: 'OK' }] + ); + }} + > + + Export my data + Download all your logs as a CSV. + + › + + + { + Alert.alert( + "Delete account?", + "This is permanent. All your logs and profile data will be deleted and cannot be recovered.", + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Delete account', style: 'destructive', onPress: () => { + Alert.alert("Coming soon", "Account deletion will be available in an upcoming update.", [{ text: 'OK' }]) + }} + ] + ); + }} + > + + Delete account + Permanently deletes your account and all logs. + + › + + + { Alert.alert( 'Sign out?', @@ -282,8 +391,9 @@ export default function ProfileScreen() { ); }} > - Sign out + Sign out + {/* Footer */} @@ -292,7 +402,8 @@ export default function ProfileScreen() { dev - + + ); } @@ -309,14 +420,17 @@ const styles = StyleSheet.create({ marginBottom: Spacing.xxl, }, avatarCircle: { - width: Sizes.avatar, - height: Sizes.avatar, + width: 80, + height: 80, backgroundColor: Colors.primaryLight, alignItems: 'center', justifyContent: 'center', alignSelf: 'center', marginBottom: Spacing.lg, borderRadius: Radius.full, + borderWidth: 2, + borderColor: Colors.primary, + padding: 3, }, nameDisplay: { alignItems: 'center', @@ -326,6 +440,17 @@ const styles = StyleSheet.create({ fontFamily: Fonts.serifSemiBold, textAlign: 'center', color: Colors.textPrimary, + borderBottomWidth: 1, + borderBottomColor: Colors.border, + paddingBottom: 4, + }, + saveHeaderButtonContainer: { + marginTop: 12, + alignSelf: 'center', + }, + saveGoalButtonContainer: { + marginTop: 12, + alignSelf: 'flex-start', }, nameText: { fontFamily: Fonts.serifSemiBold, @@ -342,6 +467,9 @@ const styles = StyleSheet.create({ fontSize: FontSizes.md, color: Colors.textMuted, textAlign: 'center', + borderBottomWidth: 1, + borderBottomColor: Colors.border, + paddingBottom: 4, }, occupationText: { fontFamily: Fonts.sans, @@ -352,13 +480,20 @@ const styles = StyleSheet.create({ sectionCard: { padding: Spacing.lg, marginBottom: Spacing.lg, + borderLeftWidth: 3, + borderLeftColor: Colors.primaryLight, + }, + sectionHeaderRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + marginBottom: Spacing.md, }, sectionLabel: { fontFamily: Fonts.sansMedium, - fontSize: FontSizes.sm, - color: Colors.textHint, - letterSpacing: 0.08, - marginBottom: Spacing.md, + fontSize: FontSizes.xs, + color: Colors.primary, + letterSpacing: 0.8, }, subsection: {}, subsectionLabel: { @@ -463,15 +598,75 @@ const styles = StyleSheet.create({ fontSize: FontSizes.sm, color: Colors.textMuted, }, - signOutButton: { - paddingVertical: Spacing.md, - paddingHorizontal: 0, + + settingLabel: { + fontFamily: Fonts.sansMedium, + fontSize: FontSizes.md, + color: Colors.textPrimary, }, - signOutText: { + settingSubtitle: { + fontFamily: Fonts.sans, + fontSize: FontSizes.sm, + color: Colors.textMuted, + marginTop: 2, + marginBottom: 12, + }, + contextToggleRow: { + flexDirection: 'row', + gap: 8, + }, + contextOptionBtn: { + paddingVertical: 8, + paddingHorizontal: 14, + borderRadius: Radius.pill, + backgroundColor: Colors.cardBg, + borderWidth: 1, + borderColor: Colors.border, + }, + contextOptionBtnSelected: { + backgroundColor: Colors.primary, + borderColor: Colors.primary, + }, + contextOptionText: { + fontFamily: Fonts.sans, + color: Colors.textSecondary, + fontSize: FontSizes.base, + }, + contextOptionTextSelected: { + fontFamily: Fonts.sansMedium, + color: Colors.white, + }, + accountRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 14, + }, + accountRowBorder: { + borderBottomWidth: 1, + borderBottomColor: Colors.border, + }, + accountRowText: { fontFamily: Fonts.sansMedium, fontSize: FontSizes.md, color: Colors.textPrimary, }, + accountRowDestructiveText: { + fontFamily: Fonts.sansMedium, + fontSize: FontSizes.md, + color: "#C0392B", + }, + accountRowSub: { + fontFamily: Fonts.sans, + fontSize: FontSizes.sm, + color: Colors.textMuted, + marginTop: 2, + }, + chevron: { + fontSize: 20, + color: Colors.textHint, + }, + footer: { alignItems: 'center', marginTop: Spacing.sm,