- Suggested habits
+ Ideas for you
{suggestions.map((s) => (
{validCount === 0
- ? 'Add at least 1 habit to begin'
+ ? 'Add at least one habit to get started'
: `${validCount} habit${validCount > 1 ? 's' : ''} ready. Let's go.`}
diff --git a/src/screens/DailyScreen.js b/src/screens/DailyScreen.js
index b635f1f..97b894a 100644
--- a/src/screens/DailyScreen.js
+++ b/src/screens/DailyScreen.js
@@ -3,74 +3,116 @@ import {
View, Text, StyleSheet, Pressable,
TouchableOpacity, Animated, KeyboardAvoidingView, Platform,
} from 'react-native';
-import { useFocusEffect } from '@react-navigation/native';
-let ViewShot, Sharing, MediaLibrary, Print;
+import { useFocusEffect, CommonActions } from '@react-navigation/native';
+let ViewShot, Sharing;
try {
ViewShot = require('react-native-view-shot').default;
Sharing = require('expo-sharing');
- MediaLibrary = require('expo-media-library');
- Print = require('expo-print');
-} catch (_) {
- // Modules unavailable — share features disabled
-}
+} catch (_) {}
import { showAlert } from '../components/NovaAlert';
import ScreenWrapper from '../components/ScreenWrapper';
import Button from '../components/Button';
-import Input from '../components/Input';
+import FadeIn from '../components/FadeIn';
+import NovaAd from '../components/NovaAd';
import useIdentityStore from '../store/useIdentityStore';
import useHabitStore from '../store/useHabitStore';
import { generateDailyReflection, saveDailyJournal, getJournalEntry } from '../services/journalService';
+import { trackDayCompleted, trackJournalSaved } from '../services/analytics';
import { colors, fonts, spacing, borderRadius } from '../utils/theme';
import { getDayNarrative } from '../utils/helpers';
const MOODS = [
- { key: 'great', emoji: '😄', label: 'Great' },
- { key: 'good', emoji: '🙂', label: 'Good' },
- { key: 'okay', emoji: '😐', label: 'Okay' },
- { key: 'bad', emoji: '😔', label: 'Bad' },
- { key: 'terrible', emoji: '😞', label: 'Awful' },
+ { key: 'great', emoji: '🔥', label: 'Amazing' },
+ { key: 'good', emoji: '😊', label: 'Good' },
+ { key: 'okay', emoji: '😌', label: 'Okay' },
+ { key: 'bad', emoji: '😔', label: 'Tough' },
+ { key: 'terrible', emoji: '💀', label: 'Hard' },
];
+const MOOD_EMOJIS = { great: '🔥', good: '😊', okay: '😌', bad: '😔', terrible: '💀' };
-const MOOD_EMOJIS = { great: '😄', good: '🙂', okay: '😐', bad: '😔', terrible: '😞' };
+// Progress ring for day counter
+function DayRing({ current, total }) {
+ const pct = Math.min(current / total, 1);
+ return (
+
+
+
+
+ {current}
+ / {total}
+
+
+ );
+}
+const ringStyles = StyleSheet.create({
+ container: { alignSelf: 'center', width: 80, height: 80, borderRadius: 40, backgroundColor: colors.surface, borderWidth: 2, borderColor: colors.border, alignItems: 'center', justifyContent: 'center', marginBottom: spacing.md, overflow: 'hidden' },
+ track: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.surface },
+ fill: { position: 'absolute', left: 0, top: 0, bottom: 0, backgroundColor: 'rgba(108, 99, 255, 0.15)' },
+ center: { alignItems: 'center' },
+ day: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold },
+ of: { color: colors.textMuted, fontSize: fonts.sizes.xs },
+});
-function HabitItem({ habit, completed, onToggle, disabled }) {
+function formatMin(min) {
+ if (!min || min <= 0) return null;
+ if (min >= 60) return `${Math.floor(min / 60)}h${min % 60 > 0 ? `${min % 60}m` : ''}`;
+ return `${min}m`;
+}
+
+function HabitItem({ habit, completed, onToggle }) {
+ const dur = formatMin(habit.duration);
+ const hasTime = habit.best_time && habit.best_time.length >= 4;
+ const hasMeta = hasTime || dur;
return (
-
- {completed && ✓}
+
+ {completed && {'\u2713'}}
-
- {habit.title}
- {habit.description ? {habit.description} : null}
+
+ {habit.title}
+ {hasMeta && }
+ {hasMeta && (
+
+ {hasTime && (
+
+
+ {habit.best_time}
+
+ )}
+ {dur && (
+
+
+ {dur}
+
+ )}
+
+ )}
+ {completed && Done}
);
}
-export default function DailyScreen() {
- const identity = useIdentityStore((s) => s.identity);
- const currentDay = useIdentityStore((s) => s.currentDay);
- const habits = useHabitStore((s) => s.habits);
- const habitLogs = useHabitStore((s) => s.habitLogs);
- const toggleHabit = useHabitStore((s) => s.toggleHabit);
- const loadTodayLogs = useHabitStore((s) => s.loadTodayLogs);
+export default function DailyScreen({ navigation }) {
+ const identity = useIdentityStore((st) => st.identity);
+ const currentDay = useIdentityStore((st) => st.currentDay);
+ const fetchIdentity = useIdentityStore((st) => st.fetchIdentity);
+ const habits = useHabitStore((st) => st.habits);
+ const habitLogs = useHabitStore((st) => st.habitLogs);
+ const toggleHabit = useHabitStore((st) => st.toggleHabit);
+ const loadTodayLogs = useHabitStore((st) => st.loadTodayLogs);
+ const [initialLoading, setInitialLoading] = useState(true);
const [identityCheck, setIdentityCheck] = useState('');
const [mood, setMood] = useState('');
- const [win, setWin] = useState('');
- const [struggle, setStruggle] = useState('');
- const [highlight, setHighlight] = useState('');
- const [note, setNote] = useState('');
const [saving, setSaving] = useState(false);
-
- // Already completed today
const [completed, setCompleted] = useState(false);
const [savedEntry, setSavedEntry] = useState(null);
+ const [focusCount, setFocusCount] = useState(0);
const fadeAnim = useRef(new Animated.Value(0)).current;
const viewShotRef = useRef(null);
@@ -78,33 +120,24 @@ export default function DailyScreen() {
useFocusEffect(
useCallback(() => {
const init = async () => {
+ await fetchIdentity();
+ const id = useIdentityStore.getState().identity;
+ const day = useIdentityStore.getState().currentDay;
+ if (!id) { setInitialLoading(false); navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'IdentityStory' }] })); return; }
+ if (day > 40) { navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'Completion' }] })); return; }
+ setInitialLoading(false);
+ setFocusCount((c) => c + 1);
loadTodayLogs();
-
- // Check if today is already completed
- if (identity?.id) {
- const existing = await getJournalEntry(identity.id);
+ if (id?.id) {
+ const existing = await getJournalEntry(id.id);
if (existing && existing.ai_title) {
- setCompleted(true);
- setSavedEntry(existing);
- // Fill form with saved data
- setIdentityCheck(existing.identity_check || '');
- setMood(existing.mood || '');
- setWin(existing.win || '');
- setStruggle(existing.struggle || '');
- setHighlight(existing.highlight || '');
- setNote(existing.note || '');
+ setCompleted(true); setSavedEntry(existing);
+ setIdentityCheck(existing.identity_check || ''); setMood(existing.mood || '');
} else {
- setCompleted(false);
- setSavedEntry(null);
- setIdentityCheck('');
- setMood('');
- setWin('');
- setStruggle('');
- setHighlight('');
- setNote('');
+ setCompleted(false); setSavedEntry(null);
+ setIdentityCheck(''); setMood('');
}
}
-
Animated.timing(fadeAnim, { toValue: 1, duration: 600, useNativeDriver: true }).start();
};
init();
@@ -112,355 +145,276 @@ export default function DailyScreen() {
);
const habitsCompleted = Object.values(habitLogs).filter(Boolean).length;
+ const habitsTotal = habits.length;
+ const allHabitsDone = habitsTotal > 0 && habitsCompleted === habitsTotal;
+ const habitsPct = habitsTotal > 0 ? Math.round((habitsCompleted / habitsTotal) * 100) : 0;
const handleCompleteDay = async () => {
- if (!identityCheck) {
- showAlert('Identity Check', 'Did you embody your identity today?');
- return;
- }
- if (!mood) {
- showAlert('Mood', 'How are you feeling today?');
- return;
- }
-
+ if (!identityCheck) { showAlert('Hold on', 'How did you show up as your new self today?'); return; }
+ if (!mood) { showAlert('One more thing', 'How are you feeling right now?'); return; }
setSaving(true);
try {
- const ai = await generateDailyReflection({ mood, win, struggle, highlight, note });
-
+ const ai = await generateDailyReflection({ mood });
const saved = await saveDailyJournal(identity.id, currentDay, {
- identityCheck,
- habitsCompleted,
- mood, win, struggle, highlight, note,
- aiTitle: ai.title,
- aiSummary: ai.summary,
- aiQuote: ai.quote,
+ identityCheck, habitsCompleted, mood,
+ win: '', struggle: '', highlight: '', note: '',
+ aiTitle: ai.title, aiSummary: ai.summary, aiQuote: ai.quote,
});
-
setCompleted(true);
setSavedEntry({ ...saved, ai_title: ai.title, ai_summary: ai.summary, ai_quote: ai.quote });
+ trackDayCompleted(currentDay, identityCheck, mood);
+ trackJournalSaved(currentDay);
} catch (error) {
- console.warn('DailyScreen save error:', error);
- showAlert('Error', error?.message || 'Failed to save. Try again.');
- } finally {
- setSaving(false);
- }
+ showAlert('Oops', error?.message || 'Something went wrong. Give it another try.');
+ } finally { setSaving(false); }
};
- // --- Share/Save/PDF actions ---
- const handleShare = async () => {
- try {
- const uri = await viewShotRef.current.capture();
- await Sharing.shareAsync(uri, { mimeType: 'image/png' });
- } catch (_) { showAlert('Error', 'Could not share.'); }
- };
-
- const handleSaveImage = async () => {
- try {
- const { status } = await MediaLibrary.requestPermissionsAsync();
- if (status !== 'granted') { showAlert('Permission', 'Allow access to save images.'); return; }
- const uri = await viewShotRef.current.capture();
- await MediaLibrary.saveToLibraryAsync(uri);
- showAlert('Saved', 'Journal saved to your gallery.');
- } catch (_) { showAlert('Error', 'Could not save image.'); }
- };
-
- const handleExportPDF = async () => {
- try {
- const e = savedEntry;
- const dateStr = new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
- const html = `
- NOVA40 — DAILY JOURNAL
- ${e?.ai_title || ''}
- ${e?.ai_summary || ''}
-
- "${e?.ai_quote || ''}"
-
- Day ${currentDay} — ${dateStr}
- ${identity?.title || ''}
- `;
- const { uri } = await Print.printToFileAsync({ html });
- await Sharing.shareAsync(uri, { mimeType: 'application/pdf' });
- } catch (_) { showAlert('Error', 'Could not generate PDF.'); }
- };
+ const handleShare = async () => { try { const uri = await viewShotRef.current.capture(); await Sharing.shareAsync(uri, { mimeType: 'image/png' }); } catch (_) { showAlert('Error', 'Could not share.'); } };
const narrative = getDayNarrative(currentDay);
- if (!identity) {
+ if (!identity || initialLoading) {
+ return (Loading your journey...);
+ }
+
+ // ====== COMPLETED STATE ======
+ if (completed && savedEntry?.ai_title) {
+ const JournalCard = () => (
+
+ NOVA40
+ {MOOD_EMOJIS[savedEntry.mood] || '\u2728'}
+ {savedEntry.ai_title}
+ {savedEntry.ai_summary}
+
+ "{savedEntry.ai_quote}"
+
+ Day {currentDay}
+ {new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
+
+ {identity?.title && {identity.title}}
+
+ );
return (
-
- Loading...
-
+
+ {/* Success header */}
+
+ {'\u2728'}
+ Day {currentDay} Complete!
+ You showed up. That matters.
+
+
+ {ViewShot ? () : ()}
+
+
+
+
+
+ {/* Entries removed */}
+
);
}
+ // ====== FORM STATE ======
return (
-
-
- DAY {currentDay} OF 40
- {narrative}
+
+
- {/* === COMPLETED STATE: show journal card + actions === */}
- {completed && savedEntry?.ai_title ? (
- <>
- {/* Completion badge */}
-
- ✓ Day {currentDay} Complete
+ {/* Hero section */}
+
+
+
+ {currentDay <= 3 ? "Let's build momentum!" : currentDay <= 20 ? 'Keep pushing forward!' : currentDay <= 35 ? "You're in the zone!" : 'The finish line is close!'}
+
+ {narrative}
+
+
+ {/* ── STEP 1: Habits ── */}
+
+
+ {allHabitsDone ? '🔥' : '📋'}
+
+ Your habits for today
+ {habitsCompleted}/{habitsTotal} done
-
- {/* Shareable journal card */}
- {ViewShot ? (
-
-
- NOVA40
- {MOOD_EMOJIS[savedEntry.mood] || '✦'}
- {savedEntry.ai_title}
- {savedEntry.ai_summary}
-
- "{savedEntry.ai_quote}"
-
- Day {currentDay}
-
- {new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
-
-
- {identity?.title && {identity.title}}
-
-
- ) : (
-
- NOVA40
- {MOOD_EMOJIS[savedEntry.mood] || '✦'}
- {savedEntry.ai_title}
- {savedEntry.ai_summary}
-
- "{savedEntry.ai_quote}"
-
- Day {currentDay}
-
- {new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
-
-
- {identity?.title && {identity.title}}
-
- )}
-
- {/* Share actions */}
-
-
-
-
-
-
+ {/* Percentage badge */}
+
+ {habitsPct}%
+
- {/* Show what was logged */}
-
- What you logged
- {savedEntry.win ? : null}
- {savedEntry.struggle ? : null}
- {savedEntry.highlight ? : null}
- {savedEntry.note ? : null}
+ {/* Progress bar */}
+
+
+
+
+ {habits.map((habit, i) => (
+
+ toggleHabit(habit.id)} />
+
+ ))}
+
+ {allHabitsDone && (
+
+ 🎉
+ All habits done! You're crushing it today!
- >
- ) : (
- <>
- {/* === FORM STATE: habits + journal inputs === */}
+ )}
+
- {/* Habits */}
-
- Today's Habits
- {habits.map((habit) => (
- toggleHabit(habit.id)}
- disabled={false}
- />
- ))}
- {habitsCompleted}/{habits.length} completed
+
+
+ {/* ── STEP 2: Identity Check ── */}
+
+
+ 🪞
+
+ How did you show up?
+ Did you live as "{identity?.title}" today?
+
+
+ {[
+ { key: 'yes', emoji: '✅', label: 'Yes!', color: colors.success },
+ { key: 'almost', emoji: '🔶', label: 'Almost', color: colors.warning },
+ { key: 'no', emoji: '❌', label: 'Not yet', color: colors.error },
+ ].map((opt) => (
+ setIdentityCheck(opt.key)}
+ >
+ {opt.emoji}
+ {opt.label}
+
+ ))}
+
+
- {/* Identity Check */}
-
- Identity Check
- Did you embody "{identity?.title}" today?
-
- {[
- { key: 'yes', label: 'Yes', color: colors.success },
- { key: 'almost', label: 'Almost', color: colors.warning },
- { key: 'no', label: 'No', color: colors.error },
- ].map((opt) => (
- setIdentityCheck(opt.key)}
- >
- {opt.label}
-
- ))}
-
-
+ {/* ── STEP 3: Mood ── */}
+
+
+ 💭
+ How's your mood?
+
+
+ {MOODS.map((m) => (
+ setMood(m.key)}>
+ {m.emoji}
+ {m.label}
+
+ ))}
+
+
- {/* Mood */}
-
- How are you feeling?
-
- {MOODS.map((m) => (
- setMood(m.key)}>
- {m.emoji}
- {m.label}
-
- ))}
-
-
-
- {/* Journal */}
-
- Daily Journal
-
-
-
-
-
-
- {/* Complete Day */}
-
- >
+ {/* ── Submit ── */}
+
+
+ {(!identityCheck || !mood) && (
+
+ {!identityCheck ? 'Select your identity check above' : 'Select your mood above'}
+
)}
+
+
);
}
-function LoggedRow({ label, value }) {
- return (
-
- {label}
- {value}
-
- );
-}
-const styles = StyleSheet.create({
+const s = StyleSheet.create({
flex: { flex: 1 },
- loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center' },
+ loadingWrap: { flex: 1, alignItems: 'center', justifyContent: 'center' },
loadingText: { color: colors.textSecondary, fontSize: fonts.sizes.md },
- scrollContent: { padding: spacing.lg, paddingBottom: spacing.xxl * 2 },
- dayLabel: { color: colors.accent, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold, letterSpacing: 3, marginBottom: spacing.sm },
- narrative: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.semibold, lineHeight: 32, marginBottom: spacing.xl },
+ scrollContent: { padding: spacing.md, paddingBottom: spacing.xxl * 2 },
- section: { marginBottom: spacing.xl },
- sectionTitle: { color: colors.text, fontSize: fonts.sizes.lg, fontWeight: fonts.weights.semibold, marginBottom: spacing.sm },
- sectionSubtitle: { color: colors.textSecondary, fontSize: fonts.sizes.sm, marginBottom: spacing.md },
+ // Hero
+ hero: { alignItems: 'center', marginBottom: spacing.md, paddingTop: spacing.xs },
+ heroGreet: { color: colors.accent, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.semibold, letterSpacing: 1, marginBottom: spacing.xs },
+ heroNarrative: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.semibold, textAlign: 'center', lineHeight: 22 },
+
+ // Sections
+ section: { marginBottom: spacing.md },
+ sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: spacing.sm, gap: spacing.sm },
+ sectionIcon: { fontSize: 18 },
+ sectionTitle: { color: colors.text, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.bold },
+ sectionMeta: { color: colors.textMuted, fontSize: 10, marginTop: 1 },
+ sectionHeaderText: { flex: 1 },
+ pctBadge: { backgroundColor: colors.surface, borderRadius: borderRadius.full, paddingHorizontal: spacing.md, paddingVertical: spacing.xs, borderWidth: 1, borderColor: colors.border },
+ pctBadgeDone: { backgroundColor: 'rgba(0,230,118,0.1)', borderColor: 'rgba(0,230,118,0.3)' },
+ pctText: { color: colors.textSecondary, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.bold },
+ pctTextDone: { color: colors.success },
+
+ // Progress bar
+ progressBar: { height: 3, backgroundColor: colors.surface, borderRadius: 2, marginBottom: spacing.sm, overflow: 'hidden' },
+ progressFill: { height: '100%', backgroundColor: colors.primary, borderRadius: 2 },
// Habits
- habitItem: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.surface, borderRadius: borderRadius.md, padding: spacing.md, marginBottom: spacing.sm, borderWidth: 1, borderColor: colors.border },
- habitCompleted: { borderColor: colors.success, backgroundColor: 'rgba(0, 230, 118, 0.05)' },
- checkbox: { width: 28, height: 28, borderRadius: 14, borderWidth: 2, borderColor: colors.textMuted, alignItems: 'center', justifyContent: 'center', marginRight: spacing.md },
- checkboxChecked: { borderColor: colors.success, backgroundColor: colors.success },
- checkmark: { color: colors.background, fontSize: 14, fontWeight: fonts.weights.bold },
- habitContent: { flex: 1 },
- habitTitle: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.medium },
- habitTitleDone: { textDecorationLine: 'line-through', opacity: 0.6 },
- habitDesc: { color: colors.textSecondary, fontSize: fonts.sizes.sm, marginTop: 2 },
- habitCount: { color: colors.textMuted, fontSize: fonts.sizes.sm, textAlign: 'center', marginTop: spacing.sm },
+ habitItem: { flexDirection: 'row', alignItems: 'flex-start', backgroundColor: colors.surface, borderRadius: borderRadius.sm, paddingVertical: spacing.sm, paddingHorizontal: spacing.md, marginBottom: 6, borderWidth: 1, borderColor: colors.border },
+ habitDone: { borderColor: 'rgba(0,230,118,0.3)', backgroundColor: 'rgba(0,230,118,0.04)' },
+ habitCheck: { width: 22, height: 22, borderRadius: 11, borderWidth: 2, borderColor: colors.textMuted, alignItems: 'center', justifyContent: 'center', marginRight: spacing.sm, marginTop: 2 },
+ habitCheckDone: { borderColor: colors.success, backgroundColor: colors.success },
+ habitCheckIcon: { color: '#fff', fontSize: 11, fontWeight: fonts.weights.bold },
+ habitBody: { flex: 1 },
+ habitText: { color: colors.text, fontSize: fonts.sizes.sm },
+ habitTextDone: { color: colors.textSecondary, textDecorationLine: 'line-through' },
+ habitSep: { height: 1, backgroundColor: colors.border, marginVertical: 5 },
+ habitMeta: { flexDirection: 'row', gap: spacing.lg },
+ habitMetaItem: { flexDirection: 'row', alignItems: 'center' },
+ habitMetaDot: { width: 5, height: 5, borderRadius: 2.5, marginRight: 5 },
+ habitMetaLabel: { color: colors.textSecondary, fontSize: 11 },
+ habitDoneTag: { color: colors.success, fontSize: 10, fontWeight: fonts.weights.bold, marginLeft: spacing.sm, marginTop: 3 },
+ allDoneBanner: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,230,118,0.08)', borderRadius: borderRadius.sm, padding: spacing.sm, marginTop: 4, borderWidth: 1, borderColor: 'rgba(0,230,118,0.2)' },
+ allDoneEmoji: { fontSize: 14, marginRight: spacing.sm },
+ allDoneText: { color: colors.success, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.semibold },
- // Identity check
- identityRow: { flexDirection: 'row', gap: spacing.sm },
- identityBtn: { flex: 1, alignItems: 'center', paddingVertical: spacing.md, borderRadius: borderRadius.md, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.surface },
- identityLabel: { color: colors.textSecondary, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.medium },
+ // Identity
+ identityRow: { flexDirection: 'row', gap: 6 },
+ identityBtn: { flex: 1, alignItems: 'center', paddingVertical: spacing.sm, borderRadius: borderRadius.md, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.surface },
+ identityEmoji: { fontSize: 18, marginBottom: 2 },
+ identityLabel: { color: colors.textSecondary, fontSize: 10, fontWeight: fonts.weights.medium },
// Mood
- moodRow: { flexDirection: 'row', justifyContent: 'space-between' },
- moodBtn: { alignItems: 'center', paddingVertical: spacing.sm, paddingHorizontal: spacing.sm, borderRadius: borderRadius.md, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.surface, flex: 1, marginHorizontal: 3 },
- moodBtnSelected: { borderColor: colors.primary, backgroundColor: 'rgba(108, 99, 255, 0.1)' },
- moodEmoji: { fontSize: 20, marginBottom: 3 },
- moodLabel: { color: colors.textMuted, fontSize: 9, fontWeight: fonts.weights.medium },
- moodLabelSelected: { color: colors.primary },
+ moodRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 4 },
+ moodBtn: { alignItems: 'center', paddingVertical: spacing.sm, borderRadius: borderRadius.md, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.surface, flex: 1 },
+ moodBtnActive: { borderColor: colors.primary, backgroundColor: 'rgba(108,99,255,0.12)' },
+ moodEmoji: { fontSize: 20, marginBottom: 2 },
+ moodLabel: { color: colors.textMuted, fontSize: 8, fontWeight: fonts.weights.medium },
+ moodLabelActive: { color: colors.primary, fontWeight: fonts.weights.bold },
- completeBtn: { marginTop: spacing.md },
+ // Submit
+ submitBtn: { marginTop: spacing.sm },
+ submitHint: { color: colors.textMuted, fontSize: fonts.sizes.xs, textAlign: 'center', marginTop: spacing.sm },
- // === Completed state ===
- completedBadge: {
- alignSelf: 'center',
- backgroundColor: 'rgba(0, 230, 118, 0.1)',
- borderRadius: borderRadius.full,
- paddingVertical: spacing.sm,
- paddingHorizontal: spacing.lg,
- borderWidth: 1,
- borderColor: 'rgba(0, 230, 118, 0.3)',
- marginBottom: spacing.xl,
- },
- completedBadgeText: {
- color: colors.success,
- fontSize: fonts.sizes.sm,
- fontWeight: fonts.weights.semibold,
- },
+ // ====== Completed state ======
+ doneHeader: { alignItems: 'center', paddingTop: spacing.lg, marginBottom: spacing.xl },
+ doneEmoji: { fontSize: 48, marginBottom: spacing.md },
+ doneTitle: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold, marginBottom: spacing.xs },
+ doneSubtitle: { color: colors.textSecondary, fontSize: fonts.sizes.md },
- // Journal card
- journalCard: {
- backgroundColor: colors.background,
- borderRadius: borderRadius.xl,
- padding: spacing.xl,
- borderWidth: 1,
- borderColor: 'rgba(108, 99, 255, 0.25)',
- alignItems: 'center',
- },
- cardBrand: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold, letterSpacing: 5, marginBottom: spacing.lg },
- cardMoodEmoji: { fontSize: 36, marginBottom: spacing.md },
- cardTitle: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, textAlign: 'center', lineHeight: 30, marginBottom: spacing.md },
- cardSummary: { color: colors.textSecondary, fontSize: fonts.sizes.md, textAlign: 'center', lineHeight: 24, marginBottom: spacing.lg, paddingHorizontal: spacing.sm },
- cardDivider: { width: 40, height: 2, backgroundColor: colors.primary, borderRadius: 1, marginBottom: spacing.lg },
- cardQuote: { color: colors.accent, fontSize: fonts.sizes.md, fontStyle: 'italic', textAlign: 'center', lineHeight: 24, marginBottom: spacing.lg, paddingHorizontal: spacing.sm },
- cardFooter: { flexDirection: 'row', justifyContent: 'center', gap: spacing.md, marginBottom: spacing.sm },
- cardDay: { color: colors.textMuted, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold },
- cardDate: { color: colors.textMuted, fontSize: fonts.sizes.sm },
- cardIdentity: { color: colors.textMuted, fontSize: fonts.sizes.xs, fontStyle: 'italic' },
+ journalCard: { backgroundColor: colors.background, borderRadius: borderRadius.xl, padding: spacing.xl, borderWidth: 1, borderColor: 'rgba(108,99,255,0.25)', alignItems: 'center' },
+ jBrand: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold, letterSpacing: 5, marginBottom: spacing.lg },
+ jEmoji: { fontSize: 36, marginBottom: spacing.md },
+ jTitle: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, textAlign: 'center', lineHeight: 30, marginBottom: spacing.md },
+ jSummary: { color: colors.textSecondary, fontSize: fonts.sizes.md, textAlign: 'center', lineHeight: 24, marginBottom: spacing.lg, paddingHorizontal: spacing.sm },
+ jDivider: { width: 40, height: 2, backgroundColor: colors.primary, borderRadius: 1, marginBottom: spacing.lg },
+ jQuote: { color: colors.accent, fontSize: fonts.sizes.md, fontStyle: 'italic', textAlign: 'center', lineHeight: 24, marginBottom: spacing.lg },
+ jFooter: { flexDirection: 'row', justifyContent: 'center', gap: spacing.md, marginBottom: spacing.sm },
+ jDay: { color: colors.textMuted, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold },
+ jDate: { color: colors.textMuted, fontSize: fonts.sizes.sm },
+ jIdentity: { color: colors.textMuted, fontSize: fonts.sizes.xs, fontStyle: 'italic' },
- // Actions
actions: { marginTop: spacing.xl },
actionBtn: { marginBottom: spacing.sm },
- actionRow: { flexDirection: 'row', gap: spacing.sm, marginBottom: spacing.sm },
- halfBtn: { flex: 1 },
- // Logged data
- loggedSection: {
- marginTop: spacing.xl,
- backgroundColor: colors.surface,
- borderRadius: borderRadius.md,
- padding: spacing.lg,
- borderWidth: 1,
- borderColor: colors.border,
- },
- loggedTitle: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.semibold, marginBottom: spacing.md },
- loggedRow: { marginBottom: spacing.md },
- loggedLabel: { color: colors.textMuted, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.medium, marginBottom: spacing.xs },
- loggedValue: { color: colors.textSecondary, fontSize: fonts.sizes.sm, lineHeight: 20 },
});
diff --git a/src/screens/EditProfileScreen.js b/src/screens/EditProfileScreen.js
new file mode 100644
index 0000000..c44d75f
--- /dev/null
+++ b/src/screens/EditProfileScreen.js
@@ -0,0 +1,296 @@
+import React, { useState, useEffect, useRef } from 'react';
+import {
+ View, Text, StyleSheet, Image, Pressable, Animated,
+ KeyboardAvoidingView, Platform, ScrollView,
+} from 'react-native';
+import * as ImagePicker from 'expo-image-picker';
+import { showAlert } from '../components/NovaAlert';
+import ScreenWrapper from '../components/ScreenWrapper';
+import Input from '../components/Input';
+import Button from '../components/Button';
+import useAuthStore from '../store/useAuthStore';
+import { getProfile, saveProfile } from '../services/profileService';
+import { colors, fonts, spacing, borderRadius } from '../utils/theme';
+
+export default function EditProfileScreen({ navigation }) {
+ const user = useAuthStore((s) => s.user);
+
+ const [nickname, setNickname] = useState('');
+ const [fullName, setFullName] = useState('');
+ const [photoUri, setPhotoUri] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [initialLoading, setInitialLoading] = useState(true);
+
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ const load = async () => {
+ if (user?.id) {
+ const profile = await getProfile(user.id);
+ if (profile) {
+ setNickname(profile.nickname || '');
+ setFullName(profile.fullName || '');
+ setPhotoUri(profile.photoUri || null);
+ }
+ }
+ setInitialLoading(false);
+ Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }).start();
+ };
+ load();
+ }, []);
+
+ const pickFromGallery = async () => {
+ try {
+ const result = await ImagePicker.launchImageLibraryAsync({
+ allowsEditing: true,
+ aspect: [1, 1],
+ quality: 0.5,
+ exif: false,
+ });
+ if (!result.canceled && result.assets?.[0]?.uri) {
+ setPhotoUri(result.assets[0].uri);
+ }
+ } catch (_) {}
+ };
+
+ const takePhoto = async () => {
+ try {
+ const result = await ImagePicker.launchCameraAsync({
+ allowsEditing: true,
+ aspect: [1, 1],
+ quality: 0.5,
+ exif: false,
+ });
+ if (!result.canceled && result.assets?.[0]?.uri) {
+ setPhotoUri(result.assets[0].uri);
+ }
+ } catch (_) {}
+ };
+
+ const handlePickPhoto = () => {
+ pickFromGallery();
+ };
+
+ const handleSave = async () => {
+ if (!nickname.trim() && !fullName.trim()) {
+ showAlert('Almost there', 'Fill in at least your nickname or full name.');
+ return;
+ }
+
+ setLoading(true);
+ try {
+ await saveProfile(user.id, {
+ nickname: nickname.trim(),
+ fullName: fullName.trim(),
+ photoUri,
+ });
+ showAlert('Saved', 'Your profile has been updated.', [
+ { text: 'OK', onPress: () => navigation.goBack() },
+ ]);
+ } catch (e) {
+ showAlert('Oops', e.message || 'Could not save your profile.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const displayInitial = nickname?.[0] || fullName?.[0] || user?.email?.[0] || 'N';
+
+ if (initialLoading) return ;
+
+ return (
+
+
+
+ Edit Profile
+ Make it yours — this is how we'll greet you.
+
+ {/* Photo */}
+
+ {photoUri ? (
+
+ ) : (
+
+ {displayInitial.toUpperCase()}
+
+ )}
+
+
+ [styles.photoBtn, pressed && { opacity: 0.6 }]} onPress={pickFromGallery}>
+ Gallery
+
+ [styles.photoBtn, pressed && { opacity: 0.6 }]} onPress={takePhoto}>
+ Camera
+
+
+
+ {/* Fields */}
+
+
+
+
+ {/* Email (read-only) */}
+
+ Email
+ {user?.email || '—'}
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ flex: { flex: 1 },
+ scrollContent: {
+ paddingHorizontal: spacing.lg,
+ paddingTop: spacing.xxl,
+ paddingBottom: spacing.xxl * 2,
+ alignItems: 'center',
+ },
+ title: {
+ color: colors.text,
+ fontSize: fonts.sizes.xxl,
+ fontWeight: fonts.weights.bold,
+ marginBottom: spacing.xs,
+ alignSelf: 'flex-start',
+ },
+ subtitle: {
+ color: colors.textSecondary,
+ fontSize: fonts.sizes.md,
+ marginBottom: spacing.xl,
+ alignSelf: 'flex-start',
+ },
+
+ // Photo
+ photoSection: {
+ position: 'relative',
+ marginBottom: spacing.sm,
+ },
+ photo: {
+ width: 100,
+ height: 100,
+ borderRadius: 50,
+ borderWidth: 3,
+ borderColor: colors.primary,
+ },
+ photoPlaceholder: {
+ width: 100,
+ height: 100,
+ borderRadius: 50,
+ backgroundColor: colors.primary,
+ alignItems: 'center',
+ justifyContent: 'center',
+ shadowColor: colors.primary,
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.4,
+ shadowRadius: 12,
+ elevation: 6,
+ },
+ photoInitial: {
+ color: colors.text,
+ fontSize: fonts.sizes.hero,
+ fontWeight: fonts.weights.bold,
+ },
+ photoBadge: {
+ position: 'absolute',
+ bottom: 0,
+ right: 0,
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ backgroundColor: colors.surface,
+ borderWidth: 2,
+ borderColor: colors.border,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ photoBadgeText: {
+ fontSize: 14,
+ },
+ photoActions: {
+ flexDirection: 'row',
+ gap: spacing.md,
+ marginTop: spacing.sm,
+ marginBottom: spacing.xl,
+ },
+ photoBtn: {
+ paddingVertical: spacing.xs + 2,
+ paddingHorizontal: spacing.lg,
+ borderRadius: borderRadius.full,
+ borderWidth: 1,
+ borderColor: colors.border,
+ backgroundColor: colors.surface,
+ },
+ photoBtnText: {
+ color: colors.textSecondary,
+ fontSize: fonts.sizes.xs,
+ fontWeight: fonts.weights.medium,
+ },
+
+ // Form
+ form: {
+ width: '100%',
+ marginBottom: spacing.lg,
+ },
+ readonlyField: {
+ backgroundColor: colors.surface,
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
+ borderWidth: 1,
+ borderColor: colors.border,
+ marginBottom: spacing.md,
+ },
+ readonlyLabel: {
+ color: colors.textMuted,
+ fontSize: fonts.sizes.xs,
+ fontWeight: fonts.weights.medium,
+ marginBottom: spacing.xs,
+ },
+ readonlyValue: {
+ color: colors.textSecondary,
+ fontSize: fonts.sizes.md,
+ },
+
+ saveBtn: {
+ width: '100%',
+ marginBottom: spacing.sm,
+ },
+ cancelBtn: {
+ width: '100%',
+ },
+});
diff --git a/src/screens/ForgotPasswordScreen.js b/src/screens/ForgotPasswordScreen.js
new file mode 100644
index 0000000..dd018eb
--- /dev/null
+++ b/src/screens/ForgotPasswordScreen.js
@@ -0,0 +1,195 @@
+import React, { useState } from 'react';
+import { View, Text, StyleSheet, KeyboardAvoidingView, Platform } from 'react-native';
+import { showAlert } from '../components/NovaAlert';
+import ScreenWrapper from '../components/ScreenWrapper';
+import Input from '../components/Input';
+import Button from '../components/Button';
+import useAuthStore from '../store/useAuthStore';
+import { colors, fonts, spacing } from '../utils/theme';
+
+export default function ForgotPasswordScreen({ navigation }) {
+ const [step, setStep] = useState(1); // 1 = email, 2 = new password
+ const [email, setEmail] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const resetPassword = useAuthStore((s) => s.resetPassword);
+
+ const handleCheckEmail = () => {
+ if (!email.trim()) {
+ showAlert('Oops', 'Enter the email you signed up with.');
+ return;
+ }
+ setStep(2);
+ };
+
+ const handleReset = async () => {
+ if (!newPassword.trim()) {
+ showAlert('Oops', 'Enter your new password.');
+ return;
+ }
+ if (newPassword.length < 6) {
+ showAlert('Too short', 'Your password needs at least 6 characters.');
+ return;
+ }
+ if (newPassword !== confirmPassword) {
+ showAlert('Oops', "Those passwords don't match.");
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const result = await resetPassword(email.trim(), newPassword);
+
+ if (result.method === 'email') {
+ showAlert(
+ 'Check your email',
+ "We sent you a link to reset your password. Open it and you're good to go.",
+ [{ text: 'OK', onPress: () => navigation.goBack() }]
+ );
+ } else {
+ showAlert(
+ 'Password updated',
+ 'Your password has been changed. You can sign in now.',
+ [{ text: 'Sign In', onPress: () => navigation.goBack() }]
+ );
+ }
+ } catch (e) {
+ showAlert('Something went wrong', e.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+ {step === 1 ? 'Forgot your password?' : 'Create a new password'}
+
+
+ {step === 1
+ ? "No worries — it happens to everyone. Enter your email and we'll help you get back in."
+ : 'Pick something you\'ll remember this time.'}
+
+
+
+ {/* Step 1: Email */}
+ {step === 1 && (
+
+
+
+
+ )}
+
+ {/* Step 2: New password */}
+ {step === 2 && (
+
+
+ Resetting password for
+ {email}
+
+
+ setShowPassword((p) => !p)}
+ />
+
+
+
+ )}
+
+ {/* Back */}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ paddingHorizontal: spacing.lg,
+ },
+ header: {
+ marginBottom: spacing.xxl,
+ },
+ title: {
+ color: colors.text,
+ fontSize: fonts.sizes.xxl,
+ fontWeight: fonts.weights.bold,
+ marginBottom: spacing.sm,
+ },
+ subtitle: {
+ color: colors.textSecondary,
+ fontSize: fonts.sizes.md,
+ lineHeight: 22,
+ },
+ form: {
+ marginBottom: spacing.lg,
+ },
+ emailConfirm: {
+ backgroundColor: colors.surface,
+ borderRadius: 12,
+ padding: spacing.md,
+ borderWidth: 1,
+ borderColor: colors.border,
+ marginBottom: spacing.lg,
+ },
+ emailLabel: {
+ color: colors.textMuted,
+ fontSize: fonts.sizes.xs,
+ marginBottom: spacing.xs,
+ },
+ emailValue: {
+ color: colors.text,
+ fontSize: fonts.sizes.md,
+ fontWeight: fonts.weights.semibold,
+ },
+ btn: {
+ marginTop: spacing.sm,
+ },
+ backBtn: {
+ marginTop: spacing.sm,
+ },
+});
diff --git a/src/screens/HabitSelectionScreen.js b/src/screens/HabitSelectionScreen.js
index 78e7008..cf12071 100644
--- a/src/screens/HabitSelectionScreen.js
+++ b/src/screens/HabitSelectionScreen.js
@@ -9,19 +9,69 @@ import Button from '../components/Button';
import useIdentityStore from '../store/useIdentityStore';
import { colors, fonts, spacing, borderRadius } from '../utils/theme';
-const MAX_HABITS = 5;
+const CAT_ICONS = { Mind: '🧠', Body: '💪', Emotion: '❤️', Social: '👥', Skill: '⚡', Discipline: '🎯', Health: '🌿', Creative: '🎨' };
-function HabitChip({ text, selected, onToggle, disabled }) {
+function formatDuration(min) {
+ if (!min || min <= 0) return null;
+ if (min >= 60) return `${Math.floor(min / 60)}h${min % 60 > 0 ? ` ${min % 60}m` : ''}`;
+ return `${min} min`;
+}
+
+function HabitChip({ habit, selected, onToggle, isPriority }) {
+ const dur = formatDuration(habit.duration);
+ const hasTime = habit.best_time && habit.best_time.length >= 4 && habit.best_time !== '--';
+ const hasMeta = hasTime || dur;
return (
-
- {selected ? '✓' : '+'}
-
- {text}
+ {/* Checkbox */}
+
+
+ {selected ? '\u2713' : '+'}
+
+ {isPriority && selected && (
+
+ {'\u2605'}
+
+ )}
+
+
+ {/* Content */}
+
+ {/* Title */}
+ {habit.title}
+
+ {/* Description / Why */}
+ {habit.why ? {habit.why} : null}
+
+ {/* ---- Separator ---- */}
+ {hasMeta && }
+
+ {/* Time & Duration */}
+ {hasMeta && (
+
+ {hasTime && (
+
+ {'\u{1F554}'}
+ {habit.best_time}
+
+ )}
+ {dur && (
+
+ {'\u23F1'}
+ {dur}
+
+ )}
+
+ )}
+
+
+ {/* Category icon */}
+ {habit.category && (
+ {CAT_ICONS[habit.category] || ''}
+ )}
);
}
@@ -30,7 +80,13 @@ export default function HabitSelectionScreen({ navigation, route }) {
const { aiResult, story } = route.params;
const createIdentity = useIdentityStore((s) => s.createIdentity);
- const [selectedHabits, setSelectedHabits] = useState([]);
+ const priorityHabits = aiResult.priority_habits || [];
+ const additionalHabits = aiResult.suggested_habits || [];
+
+ // Pre-select the 5 priority habit titles
+ const [selectedTitles, setSelectedTitles] = useState(
+ () => priorityHabits.map((h) => h.title)
+ );
const [customHabit, setCustomHabit] = useState('');
const [customHabits, setCustomHabits] = useState([]);
const [editingTitle, setEditingTitle] = useState(false);
@@ -43,26 +99,23 @@ export default function HabitSelectionScreen({ navigation, route }) {
Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }).start();
}, []);
- const allSelected = [...selectedHabits, ...customHabits];
- const atMax = allSelected.length >= MAX_HABITS;
+ const allSelectedCount = selectedTitles.length + customHabits.length;
+ const allHabitsMap = [...priorityHabits, ...additionalHabits];
- const toggleHabit = (habit) => {
- setSelectedHabits((prev) =>
- prev.includes(habit)
- ? prev.filter((h) => h !== habit)
- : atMax ? prev : [...prev, habit]
+ const toggleHabit = (habitTitle) => {
+ setSelectedTitles((prev) =>
+ prev.includes(habitTitle)
+ ? prev.filter((t) => t !== habitTitle)
+ : [...prev, habitTitle]
);
};
const addCustomHabit = () => {
const trimmed = customHabit.trim();
if (!trimmed) return;
- if (allSelected.length >= MAX_HABITS) {
- showAlert('Limit', `Maximum ${MAX_HABITS} habits allowed.`);
- return;
- }
- if ([...selectedHabits, ...customHabits].some((h) => h.toLowerCase() === trimmed.toLowerCase())) {
- showAlert('Duplicate', 'This habit is already in your list.');
+ const allTitles = [...selectedTitles, ...customHabits];
+ if (allTitles.some((t) => t.toLowerCase() === trimmed.toLowerCase())) {
+ showAlert('Already added', "You've already got that one.");
return;
}
setCustomHabits((prev) => [...prev, trimmed]);
@@ -74,14 +127,21 @@ export default function HabitSelectionScreen({ navigation, route }) {
};
const handleStart = async () => {
- if (allSelected.length === 0) {
- showAlert('No Habits', 'Please select at least 1 habit.');
+ if (allSelectedCount === 0) {
+ showAlert('No habits yet', 'Choose at least one habit to begin your journey.');
return;
}
setLoading(true);
try {
- const habits = allSelected.map((h) => ({ title: h }));
+ // Build habits array — rich objects for AI habits, simple for custom
+ const selectedRich = selectedTitles.map((t) => {
+ const found = allHabitsMap.find((h) => h.title === t);
+ return found || { title: t };
+ });
+ const customRich = customHabits.map((t) => ({ title: t }));
+ const habits = [...selectedRich, ...customRich];
+
const identityTitle = title.trim() || aiResult.identity_title;
const description = aiResult.identity_summary || '';
await createIdentity(identityTitle, description, habits, story || '');
@@ -110,7 +170,7 @@ export default function HabitSelectionScreen({ navigation, route }) {
{/* Identity Title */}
- YOUR NEW IDENTITY
+ Your new identity
{editingTitle ? (
setEditingTitle(true)}>
{title}
- Tap to edit
+ Tap to change this
)}
@@ -131,76 +191,95 @@ export default function HabitSelectionScreen({ navigation, route }) {
{/* Summary */}
{aiResult.identity_summary}
- {/* Habit selection */}
-
-
- Select Your Habits
-
- {allSelected.length} / {MAX_HABITS}
-
-
-
- {/* AI suggested habits */}
- {aiResult.suggested_habits.map((habit) => (
- toggleHabit(habit)}
- disabled={atMax}
- />
- ))}
-
- {/* Custom habits */}
- {customHabits.map((habit) => (
-
-
- ✓
- {habit}
-
- removeCustom(habit)} style={styles.removeBtn}>
- ✕
-
+ {/* Priority Habits (pre-selected) */}
+ {priorityHabits.length > 0 && (
+
+
+ ★ Top Priority
+ {allSelectedCount} selected
- ))}
-
- {/* Add custom */}
- {!atMax && (
-
- (
+ toggleHabit(habit.title)}
+ isPriority
/>
-
- +
-
-
- )}
+ ))}
+
+ )}
+
+ {/* Additional Habits */}
+ {additionalHabits.length > 0 && (
+
+ More Recommendations
+ {additionalHabits.map((habit) => (
+ toggleHabit(habit.title)}
+ isPriority={false}
+ />
+ ))}
+
+ )}
+
+ {/* Custom habits */}
+ {customHabits.length > 0 && (
+
+ Your Custom Habits
+ {customHabits.map((habit) => (
+
+
+ ✓
+
+ {habit}
+
+
+ removeCustom(habit)} style={styles.removeBtn}>
+ ✕
+
+
+ ))}
+
+ )}
+
+ {/* Add custom */}
+
+
+
+ +
+
{/* Start button */}
- {allSelected.length === 0
- ? 'Select at least 1 habit to begin'
- : `${allSelected.length} habit${allSelected.length > 1 ? 's' : ''} selected. Ready to transform.`}
+ {allSelectedCount === 0
+ ? 'Pick at least one habit to get started'
+ : `${allSelectedCount} habit${allSelectedCount > 1 ? 's' : ''} selected. Ready to transform.`}
@@ -212,10 +291,9 @@ const styles = StyleSheet.create({
scrollContent: {
paddingHorizontal: spacing.lg,
paddingTop: spacing.xl,
- paddingBottom: spacing.xxl,
+ paddingBottom: spacing.xxl * 2,
},
- // Source badge
sourceBadge: {
alignSelf: 'flex-start',
backgroundColor: 'rgba(108, 99, 255, 0.08)',
@@ -232,7 +310,6 @@ const styles = StyleSheet.create({
fontWeight: fonts.weights.medium,
},
- // Identity card
identityCard: {
backgroundColor: colors.surface,
borderRadius: borderRadius.lg,
@@ -267,8 +344,6 @@ const styles = StyleSheet.create({
fontSize: fonts.sizes.xs,
marginTop: spacing.xs,
},
-
- // Summary
summary: {
color: colors.textSecondary,
fontSize: fonts.sizes.md,
@@ -278,7 +353,7 @@ const styles = StyleSheet.create({
// Habits
habitSection: {
- marginBottom: spacing.xl,
+ marginBottom: spacing.lg,
},
habitHeader: {
flexDirection: 'row',
@@ -288,22 +363,19 @@ const styles = StyleSheet.create({
},
sectionTitle: {
color: colors.text,
- fontSize: fonts.sizes.lg,
+ fontSize: fonts.sizes.md,
fontWeight: fonts.weights.semibold,
+ marginBottom: spacing.sm,
},
counter: {
- color: colors.textMuted,
+ color: colors.accent,
fontSize: fonts.sizes.sm,
fontWeight: fonts.weights.medium,
},
- counterFull: {
- color: colors.warning,
- },
- // Chips
chip: {
flexDirection: 'row',
- alignItems: 'center',
+ alignItems: 'flex-start',
backgroundColor: colors.surface,
borderRadius: borderRadius.md,
paddingVertical: spacing.md,
@@ -316,8 +388,10 @@ const styles = StyleSheet.create({
borderColor: colors.primary,
backgroundColor: 'rgba(108, 99, 255, 0.08)',
},
- chipDisabled: {
- opacity: 0.4,
+ chipLeft: {
+ alignItems: 'center',
+ marginRight: spacing.md,
+ paddingTop: 2,
},
chipCheck: {
width: 24,
@@ -329,7 +403,6 @@ const styles = StyleSheet.create({
lineHeight: 22,
fontSize: 12,
color: colors.textMuted,
- marginRight: spacing.md,
flexShrink: 0,
},
chipCheckSelected: {
@@ -337,18 +410,66 @@ const styles = StyleSheet.create({
backgroundColor: colors.primary,
color: colors.text,
},
+ chipBody: {
+ flex: 1,
+ },
chipText: {
color: colors.text,
fontSize: fonts.sizes.md,
- flex: 1,
flexWrap: 'wrap',
},
chipTextSelected: {
color: colors.text,
+ fontWeight: fonts.weights.semibold,
+ },
+ chipWhy: {
+ color: colors.textSecondary,
+ fontSize: fonts.sizes.xs,
+ marginTop: spacing.xs,
+ lineHeight: 17,
+ },
+ chipSeparator: {
+ height: 1,
+ backgroundColor: colors.border,
+ marginTop: spacing.sm,
+ marginBottom: spacing.sm,
+ },
+ chipTimeRow: {
+ flexDirection: 'row',
+ gap: spacing.lg,
+ },
+ chipTimeItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ chipTimeIcon: {
+ fontSize: 13,
+ marginRight: spacing.xs,
+ },
+ chipTimeLabel: {
+ color: colors.accent,
+ fontSize: fonts.sizes.xs,
fontWeight: fonts.weights.medium,
},
+ catBadge: {
+ fontSize: 16,
+ marginLeft: spacing.sm,
+ alignSelf: 'flex-start',
+ marginTop: 2,
+ },
+ priorityBadge: {
+ backgroundColor: 'rgba(0, 229, 255, 0.15)',
+ borderRadius: borderRadius.full,
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ marginLeft: spacing.sm,
+ },
+ priorityBadgeText: {
+ color: colors.accent,
+ fontSize: 10,
+ },
- // Custom chips
+ // Custom
customChipRow: {
flexDirection: 'row',
alignItems: 'center',
@@ -374,7 +495,7 @@ const styles = StyleSheet.create({
addRow: {
flexDirection: 'row',
alignItems: 'center',
- marginTop: spacing.xs,
+ marginBottom: spacing.xl,
},
addInput: {
flex: 1,
@@ -407,8 +528,9 @@ const styles = StyleSheet.create({
lineHeight: 24,
},
- // Start
- startBtn: {},
+ startBtn: {
+ marginTop: spacing.sm,
+ },
startHint: {
color: colors.textMuted,
fontSize: fonts.sizes.sm,
diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js
index fea37de..5668c46 100644
--- a/src/screens/HomeScreen.js
+++ b/src/screens/HomeScreen.js
@@ -62,11 +62,11 @@ export default function HomeScreen({ navigation }) {
useEffect(() => {
if (identity && currentDay > 0 && isCriticalDay(currentDay)) {
showAlert(
- 'Critical Day',
- `Day ${currentDay} is a pivotal moment in your journey. Complete a focus challenge to power through.`,
+ 'Important Day',
+ `Day ${currentDay} is one of the toughest. Many people quit here — but not you. Want to train your focus?`,
[
{ text: 'Later', style: 'cancel' },
- { text: 'Take Challenge', onPress: () => navigation.navigate('MiniGame') },
+ { text: 'Take Challenge', onPress: () => navigation.navigate('Game') },
]
);
}
@@ -83,7 +83,7 @@ export default function HomeScreen({ navigation }) {
return (
- Loading your universe...
+ Getting things ready...
);
@@ -149,13 +149,13 @@ export default function HomeScreen({ navigation }) {
{isCriticalDay(currentDay) && (
- CRITICAL DAY — Stay focused
+ This is a turning point. Stay with it.
)}
{/* Start Button */}
@@ -224,13 +230,23 @@ const styles = StyleSheet.create({
marginBottom: spacing.xl,
},
- // Remember Me
- rememberRow: {
+ // Options row
+ optionsRow: {
flexDirection: 'row',
+ justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.md,
marginTop: spacing.xs,
},
+ rememberRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ forgotText: {
+ color: colors.primary,
+ fontSize: fonts.sizes.sm,
+ fontWeight: fonts.weights.medium,
+ },
checkbox: {
width: 22,
height: 22,
diff --git a/src/screens/MiniGameScreen.js b/src/screens/MiniGameScreen.js
index 6eb0beb..4e61497 100644
--- a/src/screens/MiniGameScreen.js
+++ b/src/screens/MiniGameScreen.js
@@ -1,14 +1,18 @@
import React, { useState, useCallback } from 'react';
-import { View, Text, StyleSheet, Pressable, Animated, ScrollView } from 'react-native';
+import { View, Text, StyleSheet, Pressable, ScrollView } from 'react-native';
+import { useFocusEffect } from '@react-navigation/native';
import { showAlert } from '../components/NovaAlert';
import ScreenWrapper from '../components/ScreenWrapper';
+import FadeIn from '../components/FadeIn';
+import NovaAd from '../components/NovaAd';
import ReflexTap from '../components/games/ReflexTap';
import FocusHold from '../components/games/FocusHold';
import TemptationChoice from '../components/games/TemptationChoice';
import TimingTap from '../components/games/TimingTap';
import GameResult from '../components/games/GameResult';
import useIdentityStore from '../store/useIdentityStore';
-import { saveGameSession } from '../services/gameService';
+import { saveGameSession, getGameScores } from '../services/gameService';
+import { trackGamePlayed, logScreenView } from '../services/analytics';
import { colors, fonts, spacing, borderRadius } from '../utils/theme';
const GAMES = [
@@ -16,7 +20,7 @@ const GAMES = [
id: 'reflex_tap',
name: 'Reflex Tap',
icon: '⚡',
- desc: 'Train quick action',
+ desc: 'Sharpen your reflexes',
stat: 'Discipline',
color: colors.accent,
},
@@ -24,7 +28,7 @@ const GAMES = [
id: 'focus_hold',
name: 'Focus Hold',
icon: '🎯',
- desc: 'Train focus & stability',
+ desc: 'Practice being still',
stat: 'Focus',
color: colors.primary,
},
@@ -32,7 +36,7 @@ const GAMES = [
id: 'temptation_choice',
name: 'Temptation',
icon: '🧠',
- desc: 'Real-life decisions',
+ desc: 'Face real temptations',
stat: 'Consistency',
color: colors.success,
},
@@ -40,7 +44,7 @@ const GAMES = [
id: 'timing_tap',
name: 'Timing Tap',
icon: '⏱',
- desc: 'Precision & awareness',
+ desc: 'Find your rhythm',
stat: 'Focus',
color: colors.warning,
},
@@ -54,7 +58,17 @@ export default function MiniGameScreen({ navigation, route }) {
const [phase, setPhase] = useState(route?.params?.gameType ? 'playing' : 'select');
const [activeGame, setActiveGame] = useState(route?.params?.gameType || null);
const [result, setResult] = useState(null);
- const [saving, setSaving] = useState(false);
+ const [focusCount, setFocusCount] = useState(0);
+ const [gameScores, setGameScores] = useState({});
+
+ useFocusEffect(
+ useCallback(() => {
+ setFocusCount((c) => c + 1);
+ if (identity?.id) {
+ getGameScores(identity.id).then(setGameScores);
+ }
+ }, [identity?.id])
+ );
const selectGame = (gameId) => {
setActiveGame(gameId);
@@ -62,22 +76,25 @@ export default function MiniGameScreen({ navigation, route }) {
setResult(null);
};
- const handleGameComplete = useCallback((score, details) => {
+ const handleGameComplete = useCallback(async (score, details) => {
setResult({ score, details });
setPhase('result');
- }, []);
+ trackGamePlayed(activeGame, score);
- const handleSave = async () => {
- if (!identity || !result) return;
- setSaving(true);
- try {
- await saveGameSession(identity.id, activeGame, result.score);
- navigation.goBack();
- } catch (e) {
- showAlert('Error', 'Failed to save score.');
- } finally {
- setSaving(false);
+ // Auto-save score immediately
+ if (identity?.id && activeGame) {
+ try {
+ await saveGameSession(identity.id, activeGame, score);
+ const updated = await getGameScores(identity.id);
+ setGameScores(updated);
+ } catch (_) {}
}
+ }, [identity?.id, activeGame]);
+
+ const handleExit = () => {
+ setPhase('select');
+ setActiveGame(null);
+ setResult(null);
};
const handleRetry = () => {
@@ -90,25 +107,35 @@ export default function MiniGameScreen({ navigation, route }) {
return (
- Mind Training
- Choose your challenge
+
+ Train Your Mind
+ Pick a quick challenge
+
- {GAMES.map((game) => (
- [styles.gameCard, pressed && styles.gameCardPressed]}
- onPress={() => selectGame(game.id)}
- >
- {game.icon}
- {game.name}
- {game.desc}
-
- +{game.stat}
-
-
+ {GAMES.map((game, i) => (
+
+ [styles.gameCard, pressed && styles.gameCardPressed]}
+ onPress={() => selectGame(game.id)}
+ >
+ {game.icon}
+ {game.name}
+ {game.desc}
+ {gameScores[game.id] && (
+
+ Last: {gameScores[game.id].last}
+ Best: {gameScores[game.id].best}
+
+ )}
+
+ +{game.stat}
+
+
+
))}
+
);
@@ -122,9 +149,8 @@ export default function MiniGameScreen({ navigation, route }) {
gameType={activeGame}
score={result.score}
details={result.details}
- onSave={handleSave}
+ onExit={handleExit}
onRetry={handleRetry}
- saving={saving}
/>
);
@@ -143,9 +169,24 @@ export default function MiniGameScreen({ navigation, route }) {
return null;
}
+ const gameName = GAMES.find((g) => g.id === activeGame)?.name || 'Game';
+
return (
+ {/* Exit header */}
+
+ [styles.exitBtn, pressed && styles.exitBtnPressed]} onPress={handleExit}>
+
+ {'\u2715'}
+
+
+
+ {GAMES.find((g) => g.id === activeGame)?.icon || '🎮'}
+ {gameName}
+
+
+
- Loading your mirror...
+ Looking back...
);
}
@@ -66,37 +66,37 @@ export default function MirrorScreen() {
return (
- The Mirror
- See how far you've come
+ Then vs. Now
+ Look at how much you've grown
{identity && (
- YOUR IDENTITY
+ Your identity
{identity.title}
Started {formatDate(identity.start_date)}
)}
-
+
↓
- {currentDay - 1} days of growth
+ {currentDay - 1} days of becoming
-
+
- Journey Insights
+ The bigger picture
- Days Completed
+ Days you showed up
{logs.length} / {currentDay}
- Completion Rate
+ Consistency
{currentDay > 0 ? Math.round((logs.length / currentDay) * 100) : 0}%
- Identity Alignment
- {logs.filter((l) => l.identity_check === 'yes').length} days fully aligned
+ Days fully aligned
+ {logs.filter((l) => l.identity_check === 'yes').length} days as your best self
diff --git a/src/screens/OnboardingScreen.js b/src/screens/OnboardingScreen.js
index 1d079f9..d272bc9 100644
--- a/src/screens/OnboardingScreen.js
+++ b/src/screens/OnboardingScreen.js
@@ -1,59 +1,75 @@
import React, { useRef, useState, useEffect, useCallback } from 'react';
import {
- View, Text, Image, TextInput, StyleSheet, FlatList,
+ View, Text, Image, StyleSheet, FlatList,
Dimensions, TouchableOpacity, Animated,
- KeyboardAvoidingView, Platform, Keyboard,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import StarField from '../components/StarField';
import Button from '../components/Button';
-import useAppStore from '../store/useAppStore';
import useAuthStore from '../store/useAuthStore';
import { colors, fonts, spacing, borderRadius } from '../utils/theme';
-const { width } = Dimensions.get('window');
+const { width, height } = Dimensions.get('window');
const ONBOARDING_KEY = 'onboarding_done';
const pages = [
{
id: '1',
- title: 'Identity First',
- text: 'Every transformation begins with identity.\nNot goals. Not habits. Identity.',
iconSize: 90,
- glowIntensity: 0.3,
- showOrbit: false,
+ glowIntensity: 0.4,
+ headline: 'Become\nsomeone new',
+ body: 'Nova40 is a 40-day journey that helps you\ntransform your identity through daily habits,\njournaling, and real self-reflection.',
+ accent: colors.primary,
},
{
id: '2',
- title: '40 Days',
- text: "In 40 days, you won't just change habits\n— you'll change who you are.",
- iconSize: 110,
- glowIntensity: 0.65,
- showOrbit: false,
+ iconSize: 90,
+
+ glowIntensity: 0.6,
+ headline: 'Your story.\nYour plan.\nYour growth.',
+ body: null,
+ accent: colors.accent,
+ bullets: [
+ 'Write your story — AI builds your personal path',
+ 'Track habits and mood every single day',
+ 'Train your mind with focus challenges',
+ 'Watch yourself grow on a 40-day map',
+ ],
},
{
id: '3',
- title: 'Your Nova Awaits',
- text: 'Small actions. Massive transformation.',
- iconSize: 110,
+ iconSize: 90,
glowIntensity: 1,
showOrbit: true,
+ headline: '40 days is\nall it takes.',
+ body: "You don't need to be perfect.\nJust show up — one day at a time.\nThat's the whole secret.",
+ accent: colors.success,
isLast: true,
},
];
function PageContent({ item, isActive }) {
const fadeAnim = useRef(new Animated.Value(0)).current;
- const slideAnim = useRef(new Animated.Value(30)).current;
+ const slideAnim = useRef(new Animated.Value(40)).current;
+ const iconFade = useRef(new Animated.Value(0)).current;
+ const iconScale = useRef(new Animated.Value(0.8)).current;
const pulse = useRef(new Animated.Value(1)).current;
useEffect(() => {
if (isActive) {
+ iconFade.setValue(0);
+ iconScale.setValue(0.8);
fadeAnim.setValue(0);
- slideAnim.setValue(30);
- Animated.parallel([
- Animated.timing(fadeAnim, { toValue: 1, duration: 500, delay: 150, useNativeDriver: true }),
- Animated.timing(slideAnim, { toValue: 0, duration: 500, delay: 150, useNativeDriver: true }),
+ slideAnim.setValue(40);
+ Animated.sequence([
+ Animated.parallel([
+ Animated.timing(iconFade, { toValue: 1, duration: 600, useNativeDriver: true }),
+ Animated.spring(iconScale, { toValue: 1, useNativeDriver: true, speed: 12, bounciness: 6 }),
+ ]),
+ Animated.parallel([
+ Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }),
+ Animated.timing(slideAnim, { toValue: 0, duration: 500, useNativeDriver: true }),
+ ]),
]).start();
}
}, [isActive]);
@@ -61,8 +77,8 @@ function PageContent({ item, isActive }) {
useEffect(() => {
const anim = Animated.loop(
Animated.sequence([
- Animated.timing(pulse, { toValue: 1.12, duration: 2200, useNativeDriver: true }),
- Animated.timing(pulse, { toValue: 1, duration: 2200, useNativeDriver: true }),
+ Animated.timing(pulse, { toValue: 1.1, duration: 2500, useNativeDriver: true }),
+ Animated.timing(pulse, { toValue: 1, duration: 2500, useNativeDriver: true }),
])
);
anim.start();
@@ -71,43 +87,52 @@ function PageContent({ item, isActive }) {
return (
- {/* Icon area */}
-
+ {/* Logo */}
+
{item.showOrbit && (
-
+
)}
-
+