tekout register, add login by fb, add menu history
This commit is contained in:
@@ -21,7 +21,6 @@
|
|||||||
"backgroundColor": "#0A0E1A"
|
"backgroundColor": "#0A0E1A"
|
||||||
},
|
},
|
||||||
"package": "com.nova40.app",
|
"package": "com.nova40.app",
|
||||||
"googleServicesFile": "./google-services.json",
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"WRITE_EXTERNAL_STORAGE",
|
"WRITE_EXTERNAL_STORAGE",
|
||||||
"READ_EXTERNAL_STORAGE"
|
"READ_EXTERNAL_STORAGE"
|
||||||
@@ -36,10 +35,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"owner": "heyaciell",
|
"owner": "heyaciell",
|
||||||
"plugins": [
|
|
||||||
"@react-native-firebase/app",
|
|
||||||
"@react-native-firebase/crashlytics",
|
|
||||||
"expo-sharing"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1590
-2957
File diff suppressed because it is too large
Load Diff
+15
-20
@@ -10,38 +10,33 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
"@react-native-firebase/analytics": "^24.0.0",
|
|
||||||
"@react-native-firebase/app": "^24.0.0",
|
|
||||||
"@react-native-firebase/crashlytics": "^24.0.0",
|
|
||||||
"@react-navigation/bottom-tabs": "^7.15.9",
|
"@react-navigation/bottom-tabs": "^7.15.9",
|
||||||
"@react-navigation/native": "^7.2.2",
|
"@react-navigation/native": "^7.2.2",
|
||||||
"@react-navigation/native-stack": "^7.14.11",
|
"@react-navigation/native-stack": "^7.14.11",
|
||||||
"@supabase/ssr": "^0.10.2",
|
"@supabase/ssr": "^0.10.2",
|
||||||
"@supabase/supabase-js": "^2.103.0",
|
"@supabase/supabase-js": "^2.103.0",
|
||||||
"babel-preset-expo": "~55.0.8",
|
"babel-preset-expo": "~54.0.10",
|
||||||
"expo": "^55.0.19",
|
"expo": "~54.0.0",
|
||||||
"expo-dev-client": "~55.0.30",
|
"expo-dev-client": "~6.0.21",
|
||||||
"expo-image-picker": "~55.0.19",
|
"expo-image-picker": "~17.0.11",
|
||||||
"expo-linear-gradient": "~55.0.13",
|
"expo-linear-gradient": "~15.0.8",
|
||||||
"expo-media-library": "~55.0.15",
|
"expo-media-library": "~18.2.1",
|
||||||
"expo-print": "~55.0.13",
|
"expo-print": "~15.0.8",
|
||||||
"expo-sharing": "~55.0.18",
|
"expo-sharing": "~14.0.8",
|
||||||
"expo-status-bar": "~55.0.5",
|
"expo-status-bar": "~3.0.9",
|
||||||
"react": "19.2.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.83.6",
|
"react-native": "0.81.5",
|
||||||
"react-native-gesture-handler": "~2.30.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-google-mobile-ads": "^16.3.3",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-reanimated": "4.2.1",
|
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.23.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-url-polyfill": "^3.0.0",
|
"react-native-url-polyfill": "^3.0.0",
|
||||||
"react-native-view-shot": "4.0.3",
|
"react-native-view-shot": "4.0.3",
|
||||||
"react-native-worklets": "0.7.4",
|
|
||||||
"zustand": "^5.0.12"
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.2.10",
|
"@types/react": "~19.1.10",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import DailyScreen from '../screens/DailyScreen';
|
|||||||
import StatsScreen from '../screens/StatsScreen';
|
import StatsScreen from '../screens/StatsScreen';
|
||||||
import MirrorScreen from '../screens/MirrorScreen';
|
import MirrorScreen from '../screens/MirrorScreen';
|
||||||
import MiniGameScreen from '../screens/MiniGameScreen';
|
import MiniGameScreen from '../screens/MiniGameScreen';
|
||||||
|
import HistoryScreen from '../screens/HistoryScreen';
|
||||||
import CompletionScreen from '../screens/CompletionScreen';
|
import CompletionScreen from '../screens/CompletionScreen';
|
||||||
import ProfileScreen from '../screens/ProfileScreen';
|
import ProfileScreen from '../screens/ProfileScreen';
|
||||||
import DailyJournalScreen from '../screens/DailyJournalScreen';
|
import DailyJournalScreen from '../screens/DailyJournalScreen';
|
||||||
@@ -33,7 +34,7 @@ const Stack = createNativeStackNavigator();
|
|||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator();
|
||||||
|
|
||||||
function TabIcon({ label, focused }) {
|
function TabIcon({ label, focused }) {
|
||||||
const icons = { Daily: '◈', Game: '◎', Stats: '◉', Profile: '◆' };
|
const icons = { Daily: '◈', History: '◎', Stats: '◉', Profile: '◆' };
|
||||||
const activeColor = colors.primary;
|
const activeColor = colors.primary;
|
||||||
const inactiveColor = colors.textSecondary; // #8A8FB5 — brighter than textMuted
|
const inactiveColor = colors.textSecondary; // #8A8FB5 — brighter than textMuted
|
||||||
return (
|
return (
|
||||||
@@ -63,9 +64,9 @@ function MainTabs() {
|
|||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
borderTopColor: colors.border,
|
borderTopColor: colors.border,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
height: 70,
|
height: 90,
|
||||||
paddingBottom: 10,
|
paddingBottom: 28,
|
||||||
paddingTop: 6,
|
paddingTop: 8,
|
||||||
},
|
},
|
||||||
tabBarShowLabel: false,
|
tabBarShowLabel: false,
|
||||||
}}
|
}}
|
||||||
@@ -74,8 +75,8 @@ function MainTabs() {
|
|||||||
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Daily" focused={focused} /> }} />
|
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Daily" focused={focused} /> }} />
|
||||||
<Tab.Screen name="Stats" component={StatsScreen}
|
<Tab.Screen name="Stats" component={StatsScreen}
|
||||||
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Stats" focused={focused} /> }} />
|
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Stats" focused={focused} /> }} />
|
||||||
<Tab.Screen name="Game" component={MiniGameScreen}
|
<Tab.Screen name="History" component={HistoryScreen}
|
||||||
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Game" focused={focused} /> }} />
|
options={{ tabBarIcon: ({ focused }) => <TabIcon label="History" focused={focused} /> }} />
|
||||||
<Tab.Screen name="Profile" component={ProfileScreen}
|
<Tab.Screen name="Profile" component={ProfileScreen}
|
||||||
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Profile" focused={focused} /> }} />
|
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Profile" focused={focused} /> }} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
@@ -106,7 +107,7 @@ export default function AppNavigator() {
|
|||||||
<Stack.Screen name="Splash" component={SplashScreen} />
|
<Stack.Screen name="Splash" component={SplashScreen} />
|
||||||
<Stack.Screen name="Onboarding" component={OnboardingScreen} />
|
<Stack.Screen name="Onboarding" component={OnboardingScreen} />
|
||||||
<Stack.Screen name="Login" component={LoginScreen} />
|
<Stack.Screen name="Login" component={LoginScreen} />
|
||||||
<Stack.Screen name="Register" component={RegisterScreen} />
|
{/* Register removed — users sign in via email, Google, or Facebook */}
|
||||||
<Stack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
|
<Stack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
|
||||||
<Stack.Screen name="IdentityStory" component={IdentityStoryScreen} />
|
<Stack.Screen name="IdentityStory" component={IdentityStoryScreen} />
|
||||||
<Stack.Screen name="HabitSelection" component={HabitSelectionScreen} />
|
<Stack.Screen name="HabitSelection" component={HabitSelectionScreen} />
|
||||||
|
|||||||
+125
-17
@@ -179,38 +179,108 @@ export default function DailyScreen({ navigation }) {
|
|||||||
|
|
||||||
// ====== COMPLETED STATE ======
|
// ====== COMPLETED STATE ======
|
||||||
if (completed && savedEntry?.ai_title) {
|
if (completed && savedEntry?.ai_title) {
|
||||||
|
const progressPct = Math.round((currentDay / 40) * 100);
|
||||||
|
const daysLeft = Math.max(0, 40 - currentDay);
|
||||||
|
const streak = currentDay; // simplified for display
|
||||||
|
|
||||||
|
const celebrationMessages = [
|
||||||
|
'You showed up when it mattered most.',
|
||||||
|
'Another day closer to the person you want to be.',
|
||||||
|
'This is what transformation looks like.',
|
||||||
|
"Consistency isn't sexy, but it's powerful.",
|
||||||
|
'You chose growth today. That takes courage.',
|
||||||
|
"Most people quit by now. You're still here.",
|
||||||
|
'Small steps. Massive change. Keep going.',
|
||||||
|
];
|
||||||
|
const celebMsg = celebrationMessages[currentDay % celebrationMessages.length];
|
||||||
|
|
||||||
const JournalCard = () => (
|
const JournalCard = () => (
|
||||||
<View style={s.journalCard}>
|
<View style={s.journalCard}>
|
||||||
<Text style={s.jBrand}>NOVA40</Text>
|
<Text style={s.jBrand}>NOVA40</Text>
|
||||||
|
|
||||||
|
{/* Big day number */}
|
||||||
|
<View style={s.jDayCircle}>
|
||||||
|
<Text style={s.jDayNum}>{currentDay}</Text>
|
||||||
|
<Text style={s.jDayLabel}>DAY</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={s.jEmoji}>{MOOD_EMOJIS[savedEntry.mood] || '\u2728'}</Text>
|
<Text style={s.jEmoji}>{MOOD_EMOJIS[savedEntry.mood] || '\u2728'}</Text>
|
||||||
<Text style={s.jTitle}>{savedEntry.ai_title}</Text>
|
<Text style={s.jTitle}>{savedEntry.ai_title}</Text>
|
||||||
<Text style={s.jSummary}>{savedEntry.ai_summary}</Text>
|
<Text style={s.jSummary}>{savedEntry.ai_summary}</Text>
|
||||||
<View style={s.jDivider} />
|
<View style={s.jDivider} />
|
||||||
<Text style={s.jQuote}>"{savedEntry.ai_quote}"</Text>
|
<Text style={s.jQuote}>"{savedEntry.ai_quote}"</Text>
|
||||||
|
|
||||||
|
{/* Progress bar inside card */}
|
||||||
|
<View style={s.jProgressRow}>
|
||||||
|
<View style={s.jProgressBar}>
|
||||||
|
<View style={[s.jProgressFill, { width: `${progressPct}%` }]} />
|
||||||
|
</View>
|
||||||
|
<Text style={s.jProgressText}>{progressPct}%</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={s.jFooter}>
|
<View style={s.jFooter}>
|
||||||
<Text style={s.jDay}>Day {currentDay}</Text>
|
<Text style={s.jDay}>Day {currentDay}/40</Text>
|
||||||
<Text style={s.jDate}>{new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</Text>
|
<Text style={s.jDate}>{new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</Text>
|
||||||
</View>
|
</View>
|
||||||
{identity?.title && <Text style={s.jIdentity}>{identity.title}</Text>}
|
{identity?.title && <Text style={s.jIdentity}>{identity.title}</Text>}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenWrapper>
|
<ScreenWrapper>
|
||||||
<Animated.ScrollView style={[s.flex, { opacity: fadeAnim }]} contentContainerStyle={s.scrollContent} showsVerticalScrollIndicator={false}>
|
<Animated.ScrollView style={[s.flex, { opacity: fadeAnim }]} contentContainerStyle={s.scrollContent} showsVerticalScrollIndicator={false}>
|
||||||
{/* Success header */}
|
|
||||||
|
{/* Celebration header */}
|
||||||
<View style={s.doneHeader}>
|
<View style={s.doneHeader}>
|
||||||
<Text style={s.doneEmoji}>{'\u2728'}</Text>
|
<Text style={s.doneEmojiBig}>🔥</Text>
|
||||||
<Text style={s.doneTitle}>Day {currentDay} Complete!</Text>
|
<Text style={s.doneTitle}>Day {currentDay} Complete!</Text>
|
||||||
<Text style={s.doneSubtitle}>You showed up. That matters.</Text>
|
<Text style={s.doneSubtitle}>{celebMsg}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{ViewShot ? (<ViewShot ref={viewShotRef} options={{ format: 'png', quality: 1 }}><JournalCard /></ViewShot>) : (<JournalCard />)}
|
{/* Stats row */}
|
||||||
|
<View style={s.doneStatsRow}>
|
||||||
|
<View style={s.doneStat}>
|
||||||
|
<Text style={s.doneStatValue}>{progressPct}%</Text>
|
||||||
|
<Text style={s.doneStatLabel}>Progress</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[s.doneStat, s.doneStatCenter]}>
|
||||||
|
<Text style={[s.doneStatValue, { color: colors.accent }]}>{currentDay}</Text>
|
||||||
|
<Text style={s.doneStatLabel}>Day streak</Text>
|
||||||
|
</View>
|
||||||
|
<View style={s.doneStat}>
|
||||||
|
<Text style={s.doneStatValue}>{daysLeft}</Text>
|
||||||
|
<Text style={s.doneStatLabel}>Days left</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Shareable journal card */}
|
||||||
|
{ViewShot ? (
|
||||||
|
<ViewShot ref={viewShotRef} options={{ format: 'png', quality: 1 }}><JournalCard /></ViewShot>
|
||||||
|
) : (
|
||||||
|
<JournalCard />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Share CTA */}
|
||||||
|
<View style={s.shareCTA}>
|
||||||
|
<Text style={s.shareCTAText}>Show the world you're transforming</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
<View style={s.actions}>
|
<View style={s.actions}>
|
||||||
<Button title="Share This" onPress={handleShare} style={s.actionBtn} />
|
<Button title="Share to Social Media" onPress={handleShare} style={s.actionBtn} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<NovaAd />
|
||||||
|
|
||||||
|
{/* Motivational footer */}
|
||||||
|
<View style={s.doneFooter}>
|
||||||
|
<Text style={s.doneFooterText}>
|
||||||
|
{daysLeft > 0
|
||||||
|
? `${daysLeft} more days to become who you're meant to be.`
|
||||||
|
: 'You made it. 40 days. A new identity.'}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Entries removed */}
|
|
||||||
</Animated.ScrollView>
|
</Animated.ScrollView>
|
||||||
</ScreenWrapper>
|
</ScreenWrapper>
|
||||||
);
|
);
|
||||||
@@ -397,24 +467,62 @@ const s = StyleSheet.create({
|
|||||||
submitHint: { color: colors.textMuted, fontSize: fonts.sizes.xs, textAlign: 'center', marginTop: spacing.sm },
|
submitHint: { color: colors.textMuted, fontSize: fonts.sizes.xs, textAlign: 'center', marginTop: spacing.sm },
|
||||||
|
|
||||||
// ====== Completed state ======
|
// ====== Completed state ======
|
||||||
doneHeader: { alignItems: 'center', paddingTop: spacing.lg, marginBottom: spacing.xl },
|
doneHeader: { alignItems: 'center', paddingTop: spacing.xl, marginBottom: spacing.lg },
|
||||||
doneEmoji: { fontSize: 48, marginBottom: spacing.md },
|
doneEmojiBig: { fontSize: 56, marginBottom: spacing.md },
|
||||||
doneTitle: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold, marginBottom: spacing.xs },
|
doneTitle: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold, marginBottom: spacing.sm },
|
||||||
doneSubtitle: { color: colors.textSecondary, fontSize: fonts.sizes.md },
|
doneSubtitle: { color: colors.textSecondary, fontSize: fonts.sizes.md, textAlign: 'center', lineHeight: 22, paddingHorizontal: spacing.md },
|
||||||
|
|
||||||
journalCard: { backgroundColor: colors.background, borderRadius: borderRadius.xl, padding: spacing.xl, borderWidth: 1, borderColor: 'rgba(108,99,255,0.25)', alignItems: 'center' },
|
// Stats row
|
||||||
jBrand: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold, letterSpacing: 5, marginBottom: spacing.lg },
|
doneStatsRow: {
|
||||||
jEmoji: { fontSize: 36, marginBottom: spacing.md },
|
flexDirection: 'row', justifyContent: 'space-around',
|
||||||
jTitle: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, textAlign: 'center', lineHeight: 30, marginBottom: spacing.md },
|
backgroundColor: colors.surface, borderRadius: borderRadius.lg,
|
||||||
|
padding: spacing.lg, marginBottom: spacing.xl,
|
||||||
|
borderWidth: 1, borderColor: colors.border,
|
||||||
|
},
|
||||||
|
doneStat: { alignItems: 'center', flex: 1 },
|
||||||
|
doneStatCenter: { borderLeftWidth: 1, borderRightWidth: 1, borderColor: colors.border },
|
||||||
|
doneStatValue: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, marginBottom: 2 },
|
||||||
|
doneStatLabel: { color: colors.textMuted, fontSize: 10 },
|
||||||
|
|
||||||
|
// Journal card
|
||||||
|
journalCard: {
|
||||||
|
backgroundColor: colors.background, borderRadius: borderRadius.xl,
|
||||||
|
padding: spacing.xl, borderWidth: 1,
|
||||||
|
borderColor: 'rgba(108,99,255,0.3)', alignItems: 'center',
|
||||||
|
},
|
||||||
|
jBrand: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold, letterSpacing: 5, marginBottom: spacing.md },
|
||||||
|
jDayCircle: {
|
||||||
|
width: 70, height: 70, borderRadius: 35,
|
||||||
|
borderWidth: 2, borderColor: colors.accent,
|
||||||
|
alignItems: 'center', justifyContent: 'center',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
backgroundColor: 'rgba(0,229,255,0.06)',
|
||||||
|
},
|
||||||
|
jDayNum: { color: colors.accent, fontSize: 28, fontWeight: fonts.weights.bold, lineHeight: 32 },
|
||||||
|
jDayLabel: { color: colors.accent, fontSize: 9, fontWeight: fonts.weights.bold, letterSpacing: 2 },
|
||||||
|
jEmoji: { fontSize: 32, marginBottom: spacing.sm },
|
||||||
|
jTitle: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, textAlign: 'center', lineHeight: 30, marginBottom: spacing.sm },
|
||||||
jSummary: { color: colors.textSecondary, fontSize: fonts.sizes.md, textAlign: 'center', lineHeight: 24, marginBottom: spacing.lg, paddingHorizontal: spacing.sm },
|
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 },
|
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 },
|
jQuote: { color: colors.accent, fontSize: fonts.sizes.md, fontStyle: 'italic', textAlign: 'center', lineHeight: 24, marginBottom: spacing.lg },
|
||||||
|
jProgressRow: { flexDirection: 'row', alignItems: 'center', width: '100%', marginBottom: spacing.lg, gap: spacing.sm },
|
||||||
|
jProgressBar: { flex: 1, height: 4, backgroundColor: colors.surfaceLight, borderRadius: 2, overflow: 'hidden' },
|
||||||
|
jProgressFill: { height: '100%', backgroundColor: colors.primary, borderRadius: 2 },
|
||||||
|
jProgressText: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold },
|
||||||
jFooter: { flexDirection: 'row', justifyContent: 'center', gap: spacing.md, marginBottom: spacing.sm },
|
jFooter: { flexDirection: 'row', justifyContent: 'center', gap: spacing.md, marginBottom: spacing.sm },
|
||||||
jDay: { color: colors.textMuted, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold },
|
jDay: { color: colors.textMuted, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold },
|
||||||
jDate: { color: colors.textMuted, fontSize: fonts.sizes.sm },
|
jDate: { color: colors.textMuted, fontSize: fonts.sizes.sm },
|
||||||
jIdentity: { color: colors.textMuted, fontSize: fonts.sizes.xs, fontStyle: 'italic' },
|
jIdentity: { color: colors.textMuted, fontSize: fonts.sizes.xs, fontStyle: 'italic' },
|
||||||
|
|
||||||
actions: { marginTop: spacing.xl },
|
// Share CTA
|
||||||
|
shareCTA: { alignItems: 'center', marginTop: spacing.lg, marginBottom: spacing.sm },
|
||||||
|
shareCTAText: { color: colors.textSecondary, fontSize: fonts.sizes.sm, fontStyle: 'italic' },
|
||||||
|
|
||||||
|
actions: { marginTop: spacing.sm },
|
||||||
actionBtn: { marginBottom: spacing.sm },
|
actionBtn: { marginBottom: spacing.sm },
|
||||||
|
|
||||||
|
// Footer motivation
|
||||||
|
doneFooter: { alignItems: 'center', paddingTop: spacing.xl, paddingBottom: spacing.lg },
|
||||||
|
doneFooterText: { color: colors.textMuted, fontSize: fonts.sizes.sm, textAlign: 'center', lineHeight: 20 },
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
import React, { useState, useRef, useCallback } from 'react';
|
||||||
|
import { View, Text, StyleSheet, Animated, Pressable } from 'react-native';
|
||||||
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
|
import ScreenWrapper from '../components/ScreenWrapper';
|
||||||
|
import NovaAd from '../components/NovaAd';
|
||||||
|
import useAuthStore from '../store/useAuthStore';
|
||||||
|
import * as offline from '../services/offlineStorage';
|
||||||
|
import { colors, fonts, spacing, borderRadius } from '../utils/theme';
|
||||||
|
|
||||||
|
const MOOD_EMOJIS = { great: '😄', good: '🙂', okay: '😐', bad: '😔', terrible: '😞' };
|
||||||
|
const CHECK_COLORS = { yes: colors.success, almost: colors.warning, no: colors.error };
|
||||||
|
const CHECK_LABELS = { yes: 'Aligned', almost: 'Almost', no: 'Missed' };
|
||||||
|
|
||||||
|
function HistoryCard({ entry, isFirst }) {
|
||||||
|
const [expanded, setExpanded] = useState(isFirst);
|
||||||
|
const hasJournal = entry.win || entry.struggle || entry.highlight || entry.note;
|
||||||
|
const hasAI = entry.ai_title;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable style={styles.card} onPress={() => setExpanded((p) => !p)}>
|
||||||
|
{/* Timeline dot + line */}
|
||||||
|
<View style={styles.timeline}>
|
||||||
|
<View style={[
|
||||||
|
styles.timelineDot,
|
||||||
|
{ backgroundColor: CHECK_COLORS[entry.identity_check] || colors.primary },
|
||||||
|
]} />
|
||||||
|
<View style={styles.timelineLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.cardContent}>
|
||||||
|
{/* Header row */}
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<View style={styles.cardHeaderLeft}>
|
||||||
|
<Text style={styles.dayNum}>Day {entry.day_number}</Text>
|
||||||
|
{entry._identityTitle && (
|
||||||
|
<Text style={styles.identityTag}>{entry._identityTitle}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={styles.cardHeaderRight}>
|
||||||
|
<Text style={styles.moodEmoji}>{MOOD_EMOJIS[entry.mood] || '•'}</Text>
|
||||||
|
<Text style={styles.dateText}>{formatDate(entry.date)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Identity check badge */}
|
||||||
|
{entry.identity_check && (
|
||||||
|
<View style={[styles.checkBadge, { borderColor: CHECK_COLORS[entry.identity_check] }]}>
|
||||||
|
<View style={[styles.checkDot, { backgroundColor: CHECK_COLORS[entry.identity_check] }]} />
|
||||||
|
<Text style={[styles.checkText, { color: CHECK_COLORS[entry.identity_check] }]}>
|
||||||
|
{CHECK_LABELS[entry.identity_check]}
|
||||||
|
</Text>
|
||||||
|
{entry.habits_completed > 0 && (
|
||||||
|
<Text style={styles.habitsBadge}>{entry.habits_completed} habits done</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* AI reflection (always visible) */}
|
||||||
|
{hasAI && (
|
||||||
|
<View style={styles.aiSection}>
|
||||||
|
<Text style={styles.aiTitle}>{entry.ai_title}</Text>
|
||||||
|
{entry.ai_quote && (
|
||||||
|
<Text style={styles.aiQuote}>"{entry.ai_quote}"</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Expanded: full details */}
|
||||||
|
{expanded && (
|
||||||
|
<View style={styles.expandedSection}>
|
||||||
|
{entry.ai_summary && (
|
||||||
|
<Text style={styles.aiSummary}>{entry.ai_summary}</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasJournal && (
|
||||||
|
<View style={styles.journalSection}>
|
||||||
|
<View style={styles.journalDivider}>
|
||||||
|
<View style={styles.journalDividerLine} />
|
||||||
|
<Text style={styles.journalDividerText}>Your words</Text>
|
||||||
|
<View style={styles.journalDividerLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{entry.win && (
|
||||||
|
<JournalEntry icon="🏆" label="Win" value={entry.win} />
|
||||||
|
)}
|
||||||
|
{entry.struggle && (
|
||||||
|
<JournalEntry icon="💪" label="Struggle" value={entry.struggle} />
|
||||||
|
)}
|
||||||
|
{entry.highlight && (
|
||||||
|
<JournalEntry icon="✨" label="Highlight" value={entry.highlight} />
|
||||||
|
)}
|
||||||
|
{entry.note && (
|
||||||
|
<JournalEntry icon="💭" label="Reflection" value={entry.note} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Expand hint */}
|
||||||
|
{(hasAI || hasJournal) && (
|
||||||
|
<Text style={styles.expandHint}>{expanded ? 'Tap to collapse' : 'Tap to read more'}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function JournalEntry({ icon, label, value }) {
|
||||||
|
return (
|
||||||
|
<View style={styles.journalRow}>
|
||||||
|
<Text style={styles.journalIcon}>{icon}</Text>
|
||||||
|
<View style={styles.journalBody}>
|
||||||
|
<Text style={styles.journalLabel}>{label}</Text>
|
||||||
|
<Text style={styles.journalValue}>{value}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
try {
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||||
|
} catch (_) {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HistoryScreen() {
|
||||||
|
const user = useAuthStore((s) => s.user);
|
||||||
|
const [entries, setEntries] = useState([]);
|
||||||
|
const [journeyCount, setJourneyCount] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
let active = true;
|
||||||
|
const load = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
if (user?.id) {
|
||||||
|
const allIdentities = await offline.getAll('identities', { user_id: user.id });
|
||||||
|
if (active) setJourneyCount(allIdentities.length);
|
||||||
|
const allLogs = await offline.getAll('daily_logs');
|
||||||
|
const identityMap = {};
|
||||||
|
allIdentities.forEach((id) => { identityMap[id.id] = id.title; });
|
||||||
|
const identityIds = allIdentities.map((id) => id.id);
|
||||||
|
const userLogs = allLogs
|
||||||
|
.filter((l) => identityIds.includes(l.identity_id))
|
||||||
|
.filter((l) => l.identity_check || l.ai_title || l.mood)
|
||||||
|
.map((l) => ({ ...l, _identityTitle: identityMap[l.identity_id] || '' }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.date !== b.date) return a.date > b.date ? -1 : 1;
|
||||||
|
return (b.day_number || 0) - (a.day_number || 0);
|
||||||
|
});
|
||||||
|
if (active) setEntries(userLogs);
|
||||||
|
}
|
||||||
|
if (active) {
|
||||||
|
setLoading(false);
|
||||||
|
Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }).start();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
load();
|
||||||
|
return () => { active = false; };
|
||||||
|
}, [user?.id])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stats summary
|
||||||
|
const totalDays = entries.length;
|
||||||
|
const yesCount = entries.filter((e) => e.identity_check === 'yes').length;
|
||||||
|
const streakCount = (() => {
|
||||||
|
let streak = 0;
|
||||||
|
for (const e of entries) {
|
||||||
|
if (e.identity_check === 'yes' || e.identity_check === 'almost') streak++;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
return streak;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ScreenWrapper>
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<Text style={styles.loadingText}>Loading your journey...</Text>
|
||||||
|
</View>
|
||||||
|
</ScreenWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScreenWrapper>
|
||||||
|
<Animated.ScrollView
|
||||||
|
style={[styles.scroll, { opacity: fadeAnim }]}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<Text style={styles.title}>Your Journey</Text>
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
{totalDays > 0
|
||||||
|
? 'Every entry is proof that you showed up.'
|
||||||
|
: 'Your story begins when you complete your first day.'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{entries.length === 0 ? (
|
||||||
|
/* Empty state */
|
||||||
|
<View style={styles.emptyState}>
|
||||||
|
<Text style={styles.emptyEmoji}>🌱</Text>
|
||||||
|
<Text style={styles.emptyTitle}>Your story starts today</Text>
|
||||||
|
<Text style={styles.emptyText}>
|
||||||
|
Complete your first day on the Daily tab.{'\n'}
|
||||||
|
Every small step gets recorded here as part of your transformation.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Summary stats */}
|
||||||
|
<View style={styles.statsRow}>
|
||||||
|
<View style={styles.statCard}>
|
||||||
|
<Text style={styles.statEmoji}>🚀</Text>
|
||||||
|
<Text style={[styles.statValue, { color: colors.primary }]}>{journeyCount}</Text>
|
||||||
|
<Text style={styles.statLabel}>{journeyCount === 1 ? 'Journey' : 'Journeys'}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statCard}>
|
||||||
|
<Text style={styles.statEmoji}>📅</Text>
|
||||||
|
<Text style={styles.statValue}>{totalDays}</Text>
|
||||||
|
<Text style={styles.statLabel}>Days logged</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statCard}>
|
||||||
|
<Text style={styles.statEmoji}>🎯</Text>
|
||||||
|
<Text style={[styles.statValue, { color: colors.success }]}>{yesCount}</Text>
|
||||||
|
<Text style={styles.statLabel}>Aligned</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statCard}>
|
||||||
|
<Text style={styles.statEmoji}>🔥</Text>
|
||||||
|
<Text style={[styles.statValue, { color: colors.accent }]}>{streakCount}</Text>
|
||||||
|
<Text style={styles.statLabel}>Streak</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<NovaAd />
|
||||||
|
|
||||||
|
{/* Motivational banner */}
|
||||||
|
<View style={styles.motivBanner}>
|
||||||
|
<Text style={styles.motivText}>
|
||||||
|
{totalDays < 5
|
||||||
|
? "You're just getting started. The hardest part is showing up — and you did."
|
||||||
|
: totalDays < 15
|
||||||
|
? "You're building something real. Most people never make it this far."
|
||||||
|
: totalDays < 30
|
||||||
|
? "Look at this. Look at all these days. You are not the same person who started."
|
||||||
|
: "This is what commitment looks like. You didn't just try — you transformed."}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Timeline */}
|
||||||
|
<Text style={styles.timelineTitle}>Timeline</Text>
|
||||||
|
{entries.map((entry, i) => (
|
||||||
|
<HistoryCard key={entry.id || i} entry={entry} isFirst={i === 0} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<NovaAd />
|
||||||
|
|
||||||
|
{/* Journey end message */}
|
||||||
|
<View style={styles.endMessage}>
|
||||||
|
<Text style={styles.endEmoji}>🌟</Text>
|
||||||
|
<Text style={styles.endText}>
|
||||||
|
{totalDays} days of choosing who you want to be.{'\n'}That's not nothing. That's everything.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Animated.ScrollView>
|
||||||
|
</ScreenWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
scroll: { flex: 1 },
|
||||||
|
scrollContent: { padding: spacing.lg, paddingBottom: spacing.xxl * 2 },
|
||||||
|
loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
||||||
|
loadingText: { color: colors.textSecondary, fontSize: fonts.sizes.md },
|
||||||
|
|
||||||
|
// Header
|
||||||
|
title: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold, marginBottom: spacing.xs },
|
||||||
|
subtitle: { color: colors.textSecondary, fontSize: fonts.sizes.sm, lineHeight: 20, marginBottom: spacing.lg },
|
||||||
|
|
||||||
|
// Empty state
|
||||||
|
emptyState: { alignItems: 'center', paddingTop: spacing.xxl * 2 },
|
||||||
|
emptyEmoji: { fontSize: 56, marginBottom: spacing.lg },
|
||||||
|
emptyTitle: { color: colors.text, fontSize: fonts.sizes.lg, fontWeight: fonts.weights.bold, marginBottom: spacing.sm },
|
||||||
|
emptyText: { color: colors.textMuted, fontSize: fonts.sizes.sm, textAlign: 'center', lineHeight: 22 },
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
statsRow: { flexDirection: 'row', gap: spacing.sm, marginBottom: spacing.lg },
|
||||||
|
statCard: {
|
||||||
|
flex: 1, alignItems: 'center',
|
||||||
|
backgroundColor: colors.surface, borderRadius: borderRadius.lg,
|
||||||
|
paddingVertical: spacing.md, borderWidth: 1, borderColor: colors.border,
|
||||||
|
},
|
||||||
|
statEmoji: { fontSize: 20, marginBottom: spacing.xs },
|
||||||
|
statValue: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold },
|
||||||
|
statLabel: { color: colors.textMuted, fontSize: 10, marginTop: 2 },
|
||||||
|
|
||||||
|
// Motivational banner
|
||||||
|
motivBanner: {
|
||||||
|
backgroundColor: 'rgba(108, 99, 255, 0.06)',
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
padding: spacing.lg,
|
||||||
|
borderLeftWidth: 3,
|
||||||
|
borderLeftColor: colors.primary,
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
motivText: { color: colors.textSecondary, fontSize: fonts.sizes.sm, lineHeight: 22, fontStyle: 'italic' },
|
||||||
|
|
||||||
|
// Timeline
|
||||||
|
timelineTitle: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.bold, marginBottom: spacing.md },
|
||||||
|
|
||||||
|
// Card
|
||||||
|
card: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
width: 24,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: 6,
|
||||||
|
},
|
||||||
|
timelineDot: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: 6,
|
||||||
|
shadowColor: colors.primary,
|
||||||
|
shadowOffset: { width: 0, height: 0 },
|
||||||
|
shadowOpacity: 0.5,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
timelineLine: {
|
||||||
|
flex: 1,
|
||||||
|
width: 2,
|
||||||
|
backgroundColor: colors.border,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
cardContent: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
padding: spacing.md,
|
||||||
|
marginLeft: spacing.sm,
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Card header
|
||||||
|
cardHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
cardHeaderLeft: {},
|
||||||
|
cardHeaderRight: { alignItems: 'flex-end' },
|
||||||
|
dayNum: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.bold },
|
||||||
|
identityTag: { color: colors.primary, fontSize: 10, fontWeight: fonts.weights.medium, marginTop: 2 },
|
||||||
|
moodEmoji: { fontSize: 18, marginBottom: 2 },
|
||||||
|
dateText: { color: colors.textMuted, fontSize: 10 },
|
||||||
|
|
||||||
|
// Check badge
|
||||||
|
checkBadge: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
paddingVertical: spacing.xs,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
checkDot: { width: 6, height: 6, borderRadius: 3 },
|
||||||
|
checkText: { fontSize: 11, fontWeight: fonts.weights.semibold },
|
||||||
|
habitsBadge: { color: colors.textMuted, fontSize: 10, marginLeft: 4 },
|
||||||
|
|
||||||
|
// AI section
|
||||||
|
aiSection: { marginBottom: spacing.sm },
|
||||||
|
aiTitle: { color: colors.text, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold, lineHeight: 20, marginBottom: spacing.xs },
|
||||||
|
aiQuote: { color: colors.accent, fontSize: fonts.sizes.xs, fontStyle: 'italic', lineHeight: 18 },
|
||||||
|
|
||||||
|
// Expanded
|
||||||
|
expandedSection: { marginTop: spacing.sm },
|
||||||
|
aiSummary: { color: colors.textSecondary, fontSize: fonts.sizes.sm, lineHeight: 20, marginBottom: spacing.md },
|
||||||
|
|
||||||
|
// Journal section
|
||||||
|
journalSection: { marginTop: spacing.xs },
|
||||||
|
journalDivider: { flexDirection: 'row', alignItems: 'center', marginBottom: spacing.md },
|
||||||
|
journalDividerLine: { flex: 1, height: 1, backgroundColor: colors.border },
|
||||||
|
journalDividerText: { color: colors.textMuted, fontSize: 10, marginHorizontal: spacing.sm, fontWeight: fonts.weights.medium },
|
||||||
|
|
||||||
|
journalRow: { flexDirection: 'row', marginBottom: spacing.sm },
|
||||||
|
journalIcon: { fontSize: 14, marginRight: spacing.sm, marginTop: 1 },
|
||||||
|
journalBody: { flex: 1 },
|
||||||
|
journalLabel: { color: colors.textMuted, fontSize: 10, fontWeight: fonts.weights.medium, marginBottom: 1 },
|
||||||
|
journalValue: { color: colors.textSecondary, fontSize: fonts.sizes.sm, lineHeight: 20 },
|
||||||
|
|
||||||
|
// Expand hint
|
||||||
|
expandHint: { color: colors.textMuted, fontSize: 10, textAlign: 'center', marginTop: spacing.xs },
|
||||||
|
|
||||||
|
// End message
|
||||||
|
endMessage: { alignItems: 'center', paddingTop: spacing.xl, paddingBottom: spacing.lg },
|
||||||
|
endEmoji: { fontSize: 32, marginBottom: spacing.sm },
|
||||||
|
endText: { color: colors.textSecondary, fontSize: fonts.sizes.sm, textAlign: 'center', lineHeight: 22 },
|
||||||
|
});
|
||||||
+66
-42
@@ -23,10 +23,12 @@ export default function LoginScreen({ navigation }) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [demoLoading, setDemoLoading] = useState(false);
|
const [demoLoading, setDemoLoading] = useState(false);
|
||||||
const [googleLoading, setGoogleLoading] = useState(false);
|
const [googleLoading, setGoogleLoading] = useState(false);
|
||||||
|
const [facebookLoading, setFacebookLoading] = useState(false);
|
||||||
|
|
||||||
const login = useAuthStore((s) => s.login);
|
const login = useAuthStore((s) => s.login);
|
||||||
const loginAsDemo = useAuthStore((s) => s.loginAsDemo);
|
const loginAsDemo = useAuthStore((s) => s.loginAsDemo);
|
||||||
const loginWithGoogle = useAuthStore((s) => s.loginWithGoogle);
|
const loginWithGoogle = useAuthStore((s) => s.loginWithGoogle);
|
||||||
|
const loginWithFacebook = useAuthStore((s) => s.loginWithFacebook);
|
||||||
|
|
||||||
// Load remembered email on mount
|
// Load remembered email on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -96,10 +98,7 @@ export default function LoginScreen({ navigation }) {
|
|||||||
setGoogleLoading(true);
|
setGoogleLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await loginWithGoogle();
|
const data = await loginWithGoogle();
|
||||||
// OAuth opens a browser — session will be picked up by onAuthStateChange
|
if (data?.url) await Linking.openURL(data.url);
|
||||||
if (data?.url) {
|
|
||||||
await Linking.openURL(data.url);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showAlert('Google sign-in failed', error.message);
|
showAlert('Google sign-in failed', error.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -107,7 +106,19 @@ export default function LoginScreen({ navigation }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const anyLoading = loading || demoLoading || googleLoading;
|
const handleFacebookLogin = async () => {
|
||||||
|
setFacebookLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await loginWithFacebook();
|
||||||
|
if (data?.url) await Linking.openURL(data.url);
|
||||||
|
} catch (error) {
|
||||||
|
showAlert('Facebook sign-in failed', error.message);
|
||||||
|
} finally {
|
||||||
|
setFacebookLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const anyLoading = loading || demoLoading || googleLoading || facebookLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenWrapper>
|
<ScreenWrapper>
|
||||||
@@ -176,31 +187,30 @@ export default function LoginScreen({ navigation }) {
|
|||||||
|
|
||||||
{/* Google Login */}
|
{/* Google Login */}
|
||||||
<Pressable
|
<Pressable
|
||||||
style={({ pressed }) => [styles.googleBtn, pressed && styles.googleBtnPressed]}
|
style={({ pressed }) => [styles.socialBtn, styles.googleBtn, pressed && styles.socialBtnPressed]}
|
||||||
onPress={handleGoogleLogin}
|
onPress={handleGoogleLogin}
|
||||||
disabled={anyLoading}
|
disabled={anyLoading}
|
||||||
>
|
>
|
||||||
<Text style={styles.googleIcon}>G</Text>
|
<Text style={styles.googleIcon}>G</Text>
|
||||||
<Text style={styles.googleText}>Continue with Google</Text>
|
<Text style={styles.socialText}>Continue with Google</Text>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
{/* Facebook Login */}
|
||||||
|
<Pressable
|
||||||
|
style={({ pressed }) => [styles.socialBtn, styles.facebookBtn, pressed && styles.socialBtnPressed]}
|
||||||
|
onPress={handleFacebookLogin}
|
||||||
|
disabled={anyLoading}
|
||||||
|
>
|
||||||
|
<Text style={styles.facebookIcon}>f</Text>
|
||||||
|
<Text style={styles.socialText}>Continue with Facebook</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
{/* Demo Login */}
|
|
||||||
<Button
|
|
||||||
title="Try the Demo"
|
|
||||||
onPress={handleDemoLogin}
|
|
||||||
loading={demoLoading}
|
|
||||||
disabled={anyLoading && !demoLoading}
|
|
||||||
variant="secondary"
|
|
||||||
style={styles.demoBtn}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* App info */}
|
||||||
<View style={styles.footer}>
|
<View style={styles.appInfo}>
|
||||||
<Text style={styles.footerText}>New here? </Text>
|
<Text style={styles.appName}>NOVA40</Text>
|
||||||
<TouchableOpacity onPress={() => navigation.navigate('Register')}>
|
<Text style={styles.appVersion}>v1.0.0</Text>
|
||||||
<Text style={styles.footerLink}>Join Nova40</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</ScreenWrapper>
|
</ScreenWrapper>
|
||||||
@@ -293,49 +303,63 @@ const styles = StyleSheet.create({
|
|||||||
marginHorizontal: spacing.md,
|
marginHorizontal: spacing.md,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Google
|
// Social buttons
|
||||||
googleBtn: {
|
socialBtn: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
borderRadius: borderRadius.lg,
|
borderRadius: borderRadius.lg,
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
borderColor: colors.border,
|
|
||||||
backgroundColor: colors.surface,
|
|
||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
},
|
},
|
||||||
googleBtnPressed: {
|
socialBtnPressed: {
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
},
|
},
|
||||||
|
socialText: {
|
||||||
|
color: colors.text,
|
||||||
|
fontSize: fonts.sizes.md,
|
||||||
|
fontWeight: fonts.weights.medium,
|
||||||
|
},
|
||||||
|
googleBtn: {
|
||||||
|
borderColor: colors.border,
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
},
|
||||||
googleIcon: {
|
googleIcon: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: fonts.weights.bold,
|
fontWeight: fonts.weights.bold,
|
||||||
color: '#4285F4',
|
color: '#4285F4',
|
||||||
marginRight: spacing.sm,
|
marginRight: spacing.sm,
|
||||||
},
|
},
|
||||||
googleText: {
|
facebookBtn: {
|
||||||
color: colors.text,
|
borderColor: 'rgba(24, 119, 242, 0.3)',
|
||||||
fontSize: fonts.sizes.md,
|
backgroundColor: 'rgba(24, 119, 242, 0.08)',
|
||||||
fontWeight: fonts.weights.medium,
|
},
|
||||||
|
facebookIcon: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: fonts.weights.bold,
|
||||||
|
color: '#1877F2',
|
||||||
|
marginRight: spacing.sm,
|
||||||
},
|
},
|
||||||
|
|
||||||
demoBtn: {
|
demoBtn: {
|
||||||
marginTop: spacing.xs,
|
marginTop: spacing.xs,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Footer
|
// App info
|
||||||
footer: {
|
appInfo: {
|
||||||
flexDirection: 'row',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
paddingTop: spacing.xl,
|
||||||
},
|
},
|
||||||
footerText: {
|
appName: {
|
||||||
color: colors.textSecondary,
|
|
||||||
fontSize: fonts.sizes.sm,
|
|
||||||
},
|
|
||||||
footerLink: {
|
|
||||||
color: colors.primary,
|
color: colors.primary,
|
||||||
fontSize: fonts.sizes.sm,
|
fontSize: fonts.sizes.xs,
|
||||||
fontWeight: fonts.weights.semibold,
|
fontWeight: fonts.weights.bold,
|
||||||
|
letterSpacing: 4,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
appVersion: {
|
||||||
|
color: colors.textMuted,
|
||||||
|
fontSize: fonts.sizes.xs,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+183
-128
@@ -10,8 +10,9 @@ import useAuthStore from '../store/useAuthStore';
|
|||||||
import useIdentityStore from '../store/useIdentityStore';
|
import useIdentityStore from '../store/useIdentityStore';
|
||||||
import useHabitStore from '../store/useHabitStore';
|
import useHabitStore from '../store/useHabitStore';
|
||||||
import { getProfile } from '../services/profileService';
|
import { getProfile } from '../services/profileService';
|
||||||
|
import * as offline from '../services/offlineStorage';
|
||||||
import { colors, fonts, spacing, borderRadius } from '../utils/theme';
|
import { colors, fonts, spacing, borderRadius } from '../utils/theme';
|
||||||
import { formatDate } from '../utils/helpers';
|
import { formatDate, getDayPhase } from '../utils/helpers';
|
||||||
|
|
||||||
// --- Reusable guide components ---
|
// --- Reusable guide components ---
|
||||||
function Section({ icon, title, children }) {
|
function Section({ icon, title, children }) {
|
||||||
@@ -60,14 +61,23 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
const [resetting, setResetting] = useState(false);
|
const [resetting, setResetting] = useState(false);
|
||||||
const [focusCount, setFocusCount] = useState(0);
|
const [focusCount, setFocusCount] = useState(0);
|
||||||
const [profile, setProfile] = useState(null);
|
const [profile, setProfile] = useState(null);
|
||||||
|
const [journeyCount, setJourneyCount] = useState(0);
|
||||||
|
const [totalDaysLogged, setTotalDaysLogged] = useState(0);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
setFocusCount((c) => c + 1);
|
setFocusCount((c) => c + 1);
|
||||||
Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }).start();
|
Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }).start();
|
||||||
// Load profile
|
|
||||||
if (user?.id) {
|
if (user?.id) {
|
||||||
getProfile(user.id).then((p) => setProfile(p));
|
getProfile(user.id).then((p) => setProfile(p));
|
||||||
|
// Load journey stats
|
||||||
|
offline.getAll('identities', { user_id: user.id }).then((ids) => setJourneyCount(ids.length));
|
||||||
|
offline.getAll('daily_logs').then((logs) => {
|
||||||
|
offline.getAll('identities', { user_id: user.id }).then((ids) => {
|
||||||
|
const idSet = new Set(ids.map((i) => i.id));
|
||||||
|
setTotalDaysLogged(logs.filter((l) => idSet.has(l.identity_id) && (l.identity_check || l.ai_title)).length);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [user?.id])
|
}, [user?.id])
|
||||||
);
|
);
|
||||||
@@ -77,36 +87,30 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
showAlert('Tell us why', "We'd like to understand why. It helps us help you.");
|
showAlert('Tell us why', "We'd like to understand why. It helps us help you.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showAlert(
|
showAlert('Are you sure?', "Everything you've built will be gone. This can't be undone.", [
|
||||||
'Are you sure?',
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
"Everything you've built will be gone. This can't be undone.",
|
{
|
||||||
[
|
text: 'Yes, Reset', style: 'destructive',
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
onPress: async () => {
|
||||||
{
|
setResetting(true);
|
||||||
text: 'Yes, Reset',
|
try {
|
||||||
style: 'destructive',
|
await resetJourney(resetReason.trim());
|
||||||
onPress: async () => {
|
setShowReset(false);
|
||||||
setResetting(true);
|
setResetReason('');
|
||||||
try {
|
navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'IdentityStory' }] }));
|
||||||
await resetJourney(resetReason.trim());
|
} catch (e) {
|
||||||
setShowReset(false);
|
showAlert('Error', e?.message || 'Failed to reset.');
|
||||||
setResetReason('');
|
} finally { setResetting(false); }
|
||||||
navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'IdentityStory' }] }));
|
|
||||||
} catch (e) {
|
|
||||||
showAlert('Error', e?.message || 'Failed to reset.');
|
|
||||||
} finally { setResetting(false); }
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
},
|
||||||
);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSignOut = () => {
|
const handleSignOut = () => {
|
||||||
showAlert('Leaving already?', 'You can always come back.', [
|
showAlert('Leaving already?', 'You can always come back.', [
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
{
|
{
|
||||||
text: 'Sign Out',
|
text: 'Sign Out', style: 'destructive',
|
||||||
style: 'destructive',
|
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
resetIdentity(); resetHabits(); await logout();
|
resetIdentity(); resetHabits(); await logout();
|
||||||
navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'Login' }] }));
|
navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'Login' }] }));
|
||||||
@@ -115,6 +119,23 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const phase = getDayPhase(currentDay);
|
||||||
|
const progressPct = Math.round((currentDay / 40) * 100);
|
||||||
|
const displayName = profile?.nickname || profile?.fullName || user?.email?.split('@')[0] || 'Nova';
|
||||||
|
|
||||||
|
// Dynamic greeting
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
|
||||||
|
|
||||||
|
// Phase motivational message
|
||||||
|
const phaseMessages = {
|
||||||
|
awakening: "You're planting the seeds of change.",
|
||||||
|
building: "Momentum is building. Can you feel it?",
|
||||||
|
testing: "This is where champions are made.",
|
||||||
|
strengthening: "Your new identity is taking shape.",
|
||||||
|
transcending: "You're becoming unstoppable.",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenWrapper>
|
<ScreenWrapper>
|
||||||
<Animated.ScrollView
|
<Animated.ScrollView
|
||||||
@@ -123,62 +144,91 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
{/* ═══ Profile Header ═══ */}
|
{/* ═══ Profile Hero ═══ */}
|
||||||
<FadeIn delay={0} trigger={focusCount}><View style={st.profileHeader}>
|
<FadeIn delay={0} trigger={focusCount}>
|
||||||
<Pressable onPress={() => navigation.navigate('EditProfile')}>
|
<View style={st.heroSection}>
|
||||||
{profile?.photoUri ? (
|
<Pressable onPress={() => navigation.navigate('EditProfile')}>
|
||||||
<Image source={{ uri: profile.photoUri }} style={st.avatarPhoto} />
|
{profile?.photoUri ? (
|
||||||
) : (
|
<Image source={{ uri: profile.photoUri }} style={st.avatarPhoto} />
|
||||||
<View style={st.avatar}>
|
) : (
|
||||||
<Text style={st.avatarText}>
|
<View style={st.avatar}>
|
||||||
{(profile?.nickname?.[0] || profile?.fullName?.[0] || user?.email?.[0] || 'N').toUpperCase()}
|
<Text style={st.avatarText}>
|
||||||
</Text>
|
{displayName[0].toUpperCase()}
|
||||||
</View>
|
</Text>
|
||||||
)}
|
</View>
|
||||||
</Pressable>
|
)}
|
||||||
{profile?.nickname || profile?.fullName ? (
|
</Pressable>
|
||||||
<>
|
|
||||||
<Text style={st.displayName}>{profile.nickname || profile.fullName}</Text>
|
|
||||||
{profile?.fullName && profile?.nickname ? (
|
|
||||||
<Text style={st.fullName}>{profile.fullName}</Text>
|
|
||||||
) : null}
|
|
||||||
<Text style={st.email} numberOfLines={1}>{user?.email}</Text>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Text style={st.email} numberOfLines={1} adjustsFontSizeToFit>{user?.email}</Text>
|
|
||||||
)}
|
|
||||||
<Pressable
|
|
||||||
style={({ pressed }) => [st.editBtn, pressed && st.editBtnPressed]}
|
|
||||||
onPress={() => navigation.navigate('EditProfile')}
|
|
||||||
>
|
|
||||||
<Text style={st.editBtnText}>Edit Profile</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View></FadeIn>
|
|
||||||
|
|
||||||
{/* ═══ Active Identity ═══ */}
|
<Text style={st.greeting}>{greeting},</Text>
|
||||||
|
<Text style={st.displayName}>{displayName}</Text>
|
||||||
|
|
||||||
|
<Pressable
|
||||||
|
style={({ pressed }) => [st.editBtn, pressed && st.editBtnPressed]}
|
||||||
|
onPress={() => navigation.navigate('EditProfile')}
|
||||||
|
>
|
||||||
|
<Text style={st.editBtnText}>Edit Profile</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
{/* ═══ Journey Progress Card ═══ */}
|
||||||
{identity && (
|
{identity && (
|
||||||
<FadeIn delay={100} trigger={focusCount}><View style={st.identityCard}>
|
<FadeIn delay={100} trigger={focusCount}>
|
||||||
<Text style={st.identityLabel}>Your identity</Text>
|
<View style={st.journeyCard}>
|
||||||
<Text style={st.identityTitle}>{identity.title}</Text>
|
<View style={st.journeyHeader}>
|
||||||
<View style={st.identitySep} />
|
<View>
|
||||||
<View style={st.identityRow}>
|
<Text style={st.journeyLabel}>{phase.toUpperCase()} PHASE</Text>
|
||||||
<Text style={st.identityMeta}>Day {currentDay}/40</Text>
|
<Text style={st.journeyTitle}>{identity.title}</Text>
|
||||||
<Text style={st.identityMeta}>Started {formatDate(identity.start_date)}</Text>
|
</View>
|
||||||
|
<View style={st.journeyDayCircle}>
|
||||||
|
<Text style={st.journeyDayNum}>{currentDay}</Text>
|
||||||
|
<Text style={st.journeyDayLabel}>DAY</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<View style={st.progressRow}>
|
||||||
|
<View style={st.progressBar}>
|
||||||
|
<View style={[st.progressFill, { width: `${progressPct}%` }]} />
|
||||||
|
</View>
|
||||||
|
<Text style={st.progressPct}>{progressPct}%</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Phase message */}
|
||||||
|
<Text style={st.phaseMessage}>{phaseMessages[phase] || 'Keep going.'}</Text>
|
||||||
|
|
||||||
|
{/* Journey meta */}
|
||||||
|
<View style={st.journeyMeta}>
|
||||||
|
<View style={st.journeyMetaItem}>
|
||||||
|
<Text style={st.journeyMetaValue}>{40 - currentDay}</Text>
|
||||||
|
<Text style={st.journeyMetaLabel}>days left</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[st.journeyMetaItem, st.journeyMetaCenter]}>
|
||||||
|
<Text style={st.journeyMetaValue}>{formatDate(identity.start_date)}</Text>
|
||||||
|
<Text style={st.journeyMetaLabel}>started</Text>
|
||||||
|
</View>
|
||||||
|
<View style={st.journeyMetaItem}>
|
||||||
|
<Text style={st.journeyMetaValue}>{formatDate(identity.end_date)}</Text>
|
||||||
|
<Text style={st.journeyMetaLabel}>finish</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View></FadeIn>
|
</FadeIn>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<NovaAd />
|
<NovaAd />
|
||||||
|
|
||||||
{/* ═══ Scoring Guide ═══ */}
|
{/* ═══ Scoring Guide ═══ */}
|
||||||
<FadeIn delay={200} trigger={focusCount}><Pressable
|
<FadeIn delay={200} trigger={focusCount}>
|
||||||
style={({ pressed }) => [st.accordion, st.accordionGuide, pressed && st.accordionPressed]}
|
<Pressable
|
||||||
onPress={() => setShowGuide((p) => !p)}
|
style={({ pressed }) => [st.accordion, st.accordionGuide, pressed && st.accordionPressed]}
|
||||||
>
|
onPress={() => setShowGuide((p) => !p)}
|
||||||
<Text style={st.accordionIcon}>📘</Text>
|
>
|
||||||
<Text style={st.accordionText}>How your score works</Text>
|
<Text style={st.accordionIcon}>📘</Text>
|
||||||
<Text style={[st.accordionArrow, { color: colors.primary }]}>{showGuide ? '▲' : '▼'}</Text>
|
<Text style={st.accordionText}>How your score works</Text>
|
||||||
</Pressable></FadeIn>
|
<Text style={[st.accordionArrow, { color: colors.primary }]}>{showGuide ? '▲' : '▼'}</Text>
|
||||||
|
</Pressable>
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
{showGuide && (
|
{showGuide && (
|
||||||
<View style={st.accordionBody}>
|
<View style={st.accordionBody}>
|
||||||
@@ -195,20 +245,11 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
<Section icon="🔥" title="Streak">
|
<Section icon="🔥" title="Streak">
|
||||||
<Text style={st.desc}>Consecutive "Yes" or "Almost" days. Resets on "No" or missed day.</Text>
|
<Text style={st.desc}>Consecutive "Yes" or "Almost" days. Resets on "No" or missed day.</Text>
|
||||||
</Section>
|
</Section>
|
||||||
<Section icon="🗺️" title="40-Day Map Colors">
|
<Section icon="📖" title="History">
|
||||||
<Rule label="Green" value="Yes" color={colors.success} />
|
<Text style={st.desc}>All completed days are saved in the History tab — even after you reset and start a new journey.</Text>
|
||||||
<Rule label="Yellow" value="Almost" color={colors.warning} />
|
|
||||||
<Rule label="Red" value="No" color={colors.error} />
|
|
||||||
<Rule label="Cyan border" value="Today" color={colors.accent} />
|
|
||||||
</Section>
|
|
||||||
<Section icon="🎮" title="Game Bonus">
|
|
||||||
<Rule label="Reflex Tap" value="Disc ×1.0" color={colors.primary} />
|
|
||||||
<Rule label="Focus Hold" value="Focus ×1.0" color={colors.accent} />
|
|
||||||
<Rule label="Temptation" value="Cons ×1.0" color={colors.success} />
|
|
||||||
<Rule label="Timing Tap" value="Focus ×0.8" color={colors.warning} />
|
|
||||||
</Section>
|
</Section>
|
||||||
<Section icon="🏆" title="Max Score">
|
<Section icon="🏆" title="Max Score">
|
||||||
<Text style={st.desc}>8 pts/day × 40 days = 320 max (before game bonus)</Text>
|
<Text style={st.desc}>8 pts/day × 40 days = 320 max score possible.</Text>
|
||||||
</Section>
|
</Section>
|
||||||
<Section icon="⚡" title="Critical Days">
|
<Section icon="⚡" title="Critical Days">
|
||||||
<Text style={st.desc}>Day 3, 10, 21, 30 — turning points where most give up.</Text>
|
<Text style={st.desc}>Day 3, 10, 21, 30 — turning points where most give up.</Text>
|
||||||
@@ -227,18 +268,12 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
<FadeIn delay={300} trigger={focusCount}>
|
<FadeIn delay={300} trigger={focusCount}>
|
||||||
<View style={st.divider} />
|
<View style={st.divider} />
|
||||||
<View style={st.legalRow}>
|
<View style={st.legalRow}>
|
||||||
<Pressable
|
<Pressable style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]} onPress={() => navigation.navigate('PrivacyPolicy')}>
|
||||||
style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]}
|
|
||||||
onPress={() => navigation.navigate('PrivacyPolicy')}
|
|
||||||
>
|
|
||||||
<Text style={st.legalIcon}>🔒</Text>
|
<Text style={st.legalIcon}>🔒</Text>
|
||||||
<Text style={st.legalText}>Privacy Policy</Text>
|
<Text style={st.legalText}>Privacy Policy</Text>
|
||||||
<Text style={st.legalArrow}>›</Text>
|
<Text style={st.legalArrow}>›</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]} onPress={() => navigation.navigate('Terms')}>
|
||||||
style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]}
|
|
||||||
onPress={() => navigation.navigate('Terms')}
|
|
||||||
>
|
|
||||||
<Text style={st.legalIcon}>📄</Text>
|
<Text style={st.legalIcon}>📄</Text>
|
||||||
<Text style={st.legalText}>Terms & Conditions</Text>
|
<Text style={st.legalText}>Terms & Conditions</Text>
|
||||||
<Text style={st.legalArrow}>›</Text>
|
<Text style={st.legalArrow}>›</Text>
|
||||||
@@ -250,7 +285,6 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
{identity && (
|
{identity && (
|
||||||
<FadeIn delay={350} trigger={focusCount}>
|
<FadeIn delay={350} trigger={focusCount}>
|
||||||
<View style={st.divider} />
|
<View style={st.divider} />
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={({ pressed }) => [st.accordion, st.accordionReset, pressed && st.accordionPressed]}
|
style={({ pressed }) => [st.accordion, st.accordionReset, pressed && st.accordionPressed]}
|
||||||
onPress={() => setShowReset((p) => !p)}
|
onPress={() => setShowReset((p) => !p)}
|
||||||
@@ -264,9 +298,8 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
<View style={st.resetBody}>
|
<View style={st.resetBody}>
|
||||||
<View style={st.resetWarningBox}>
|
<View style={st.resetWarningBox}>
|
||||||
<Text style={st.resetWarningIcon}>⚠️</Text>
|
<Text style={st.resetWarningIcon}>⚠️</Text>
|
||||||
<Text style={st.resetWarningText}>This will erase everything — your identity, habits, journal entries, stats, and game scores. There's no going back.</Text>
|
<Text style={st.resetWarningText}>This will erase your current identity, habits, and stats. Your history will be kept.</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={st.resetLabel}>Before you go — why the reset?</Text>
|
<Text style={st.resetLabel}>Before you go — why the reset?</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={st.resetInput}
|
style={st.resetInput}
|
||||||
@@ -277,7 +310,6 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
multiline
|
multiline
|
||||||
selectionColor={colors.error}
|
selectionColor={colors.error}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
title={resetting ? 'Resetting...' : 'Reset Journey'}
|
title={resetting ? 'Resetting...' : 'Reset Journey'}
|
||||||
onPress={handleReset}
|
onPress={handleReset}
|
||||||
@@ -299,6 +331,7 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
{/* ═══ Footer ═══ */}
|
{/* ═══ Footer ═══ */}
|
||||||
<FadeIn delay={500} trigger={focusCount}>
|
<FadeIn delay={500} trigger={focusCount}>
|
||||||
<Text style={st.version}>Nova40 v1.0.0</Text>
|
<Text style={st.version}>Nova40 v1.0.0</Text>
|
||||||
|
<Text style={st.footerQuote}>Your identity is what you do repeatedly.</Text>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
|
|
||||||
</Animated.ScrollView>
|
</Animated.ScrollView>
|
||||||
@@ -310,49 +343,73 @@ const st = StyleSheet.create({
|
|||||||
flex: { flex: 1 },
|
flex: { flex: 1 },
|
||||||
scroll: { paddingHorizontal: spacing.lg, paddingTop: spacing.xl, paddingBottom: spacing.xxl * 2 },
|
scroll: { paddingHorizontal: spacing.lg, paddingTop: spacing.xl, paddingBottom: spacing.xxl * 2 },
|
||||||
|
|
||||||
// Profile header
|
// Hero
|
||||||
profileHeader: { alignItems: 'center', marginBottom: spacing.lg },
|
heroSection: { alignItems: 'center', marginBottom: spacing.xl },
|
||||||
avatar: {
|
avatar: {
|
||||||
width: 80, height: 80, borderRadius: 40, backgroundColor: colors.primary,
|
width: 90, height: 90, borderRadius: 45, backgroundColor: colors.primary,
|
||||||
alignItems: 'center', justifyContent: 'center', marginBottom: spacing.sm,
|
alignItems: 'center', justifyContent: 'center', marginBottom: spacing.md,
|
||||||
shadowColor: colors.primary, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.4, shadowRadius: 12, elevation: 6,
|
shadowColor: colors.primary, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.5, shadowRadius: 16, elevation: 8,
|
||||||
},
|
},
|
||||||
avatarPhoto: {
|
avatarPhoto: {
|
||||||
width: 80, height: 80, borderRadius: 40, marginBottom: spacing.sm,
|
width: 90, height: 90, borderRadius: 45, marginBottom: spacing.md,
|
||||||
borderWidth: 3, borderColor: colors.primary,
|
borderWidth: 3, borderColor: colors.primary,
|
||||||
},
|
},
|
||||||
avatarText: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold },
|
avatarText: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold },
|
||||||
displayName: { color: colors.text, fontSize: fonts.sizes.lg, fontWeight: fonts.weights.bold, marginBottom: 2 },
|
greeting: { color: colors.accent, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.medium, letterSpacing: 1 },
|
||||||
fullName: { color: colors.textSecondary, fontSize: fonts.sizes.sm, marginBottom: 2 },
|
displayName: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, marginTop: 2 },
|
||||||
email: { color: colors.textMuted, fontSize: fonts.sizes.xs },
|
|
||||||
editBtn: {
|
editBtn: {
|
||||||
marginTop: spacing.sm,
|
marginTop: spacing.md, paddingVertical: spacing.sm, paddingHorizontal: spacing.xl,
|
||||||
paddingVertical: spacing.xs + 2,
|
borderRadius: borderRadius.full, borderWidth: 1, borderColor: colors.primary,
|
||||||
paddingHorizontal: spacing.lg,
|
|
||||||
borderRadius: borderRadius.full,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.primary,
|
|
||||||
},
|
},
|
||||||
editBtnPressed: { opacity: 0.6 },
|
editBtnPressed: { opacity: 0.6 },
|
||||||
editBtnText: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.semibold },
|
editBtnText: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.semibold },
|
||||||
|
|
||||||
// Identity card
|
// Journey card
|
||||||
identityCard: {
|
journeyCard: {
|
||||||
backgroundColor: colors.surface, borderRadius: borderRadius.md,
|
backgroundColor: colors.surface, borderRadius: borderRadius.xl,
|
||||||
padding: spacing.md, marginBottom: spacing.lg,
|
padding: spacing.lg, marginBottom: spacing.lg,
|
||||||
borderWidth: 1, borderColor: 'rgba(108,99,255,0.2)',
|
borderWidth: 1, borderColor: 'rgba(108,99,255,0.25)',
|
||||||
},
|
},
|
||||||
identityLabel: { color: colors.primary, fontSize: 9, fontWeight: fonts.weights.bold, letterSpacing: 2, marginBottom: 6 },
|
journeyHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: spacing.md },
|
||||||
identityTitle: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.semibold },
|
journeyLabel: { color: colors.accent, fontSize: 10, fontWeight: fonts.weights.bold, letterSpacing: 2, marginBottom: spacing.xs },
|
||||||
identitySep: { height: 1, backgroundColor: colors.border, marginVertical: spacing.sm },
|
journeyTitle: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.bold, maxWidth: 200 },
|
||||||
identityRow: { flexDirection: 'row', justifyContent: 'space-between' },
|
journeyDayCircle: {
|
||||||
identityMeta: { color: colors.textMuted, fontSize: 11 },
|
width: 56, height: 56, borderRadius: 28,
|
||||||
|
borderWidth: 2, borderColor: colors.accent,
|
||||||
|
alignItems: 'center', justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(0,229,255,0.06)',
|
||||||
|
},
|
||||||
|
journeyDayNum: { color: colors.accent, fontSize: 22, fontWeight: fonts.weights.bold, lineHeight: 26 },
|
||||||
|
journeyDayLabel: { color: colors.accent, fontSize: 8, fontWeight: fonts.weights.bold, letterSpacing: 1 },
|
||||||
|
|
||||||
|
progressRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, marginBottom: spacing.sm },
|
||||||
|
progressBar: { flex: 1, height: 5, backgroundColor: colors.surfaceLight, borderRadius: 3, overflow: 'hidden' },
|
||||||
|
progressFill: { height: '100%', backgroundColor: colors.primary, borderRadius: 3 },
|
||||||
|
progressPct: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold, width: 36, textAlign: 'right' },
|
||||||
|
|
||||||
|
phaseMessage: { color: colors.textSecondary, fontSize: fonts.sizes.sm, fontStyle: 'italic', marginBottom: spacing.md },
|
||||||
|
|
||||||
|
journeyMeta: { flexDirection: 'row', borderTopWidth: 1, borderTopColor: colors.border, paddingTop: spacing.md },
|
||||||
|
journeyMetaItem: { flex: 1, alignItems: 'center' },
|
||||||
|
journeyMetaCenter: { borderLeftWidth: 1, borderRightWidth: 1, borderColor: colors.border },
|
||||||
|
journeyMetaValue: { color: colors.text, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold },
|
||||||
|
journeyMetaLabel: { color: colors.textMuted, fontSize: 9, marginTop: 2 },
|
||||||
|
|
||||||
|
// Lifetime stats
|
||||||
|
lifetimeRow: { flexDirection: 'row', gap: spacing.sm, marginBottom: spacing.lg },
|
||||||
|
lifetimeCard: {
|
||||||
|
flex: 1, alignItems: 'center',
|
||||||
|
backgroundColor: colors.surface, borderRadius: borderRadius.lg,
|
||||||
|
paddingVertical: spacing.lg, borderWidth: 1, borderColor: colors.border,
|
||||||
|
},
|
||||||
|
lifetimeEmoji: { fontSize: 24, marginBottom: spacing.sm },
|
||||||
|
lifetimeValue: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold },
|
||||||
|
lifetimeLabel: { color: colors.textMuted, fontSize: 10, marginTop: 2 },
|
||||||
|
|
||||||
// Accordion shared
|
// Accordion shared
|
||||||
accordion: {
|
accordion: {
|
||||||
flexDirection: 'row', alignItems: 'center',
|
flexDirection: 'row', alignItems: 'center',
|
||||||
borderRadius: borderRadius.md, padding: spacing.md,
|
borderRadius: borderRadius.md, padding: spacing.md, marginBottom: 6,
|
||||||
marginBottom: 6,
|
|
||||||
},
|
},
|
||||||
accordionGuide: { backgroundColor: 'rgba(108,99,255,0.06)', borderWidth: 1, borderColor: 'rgba(108,99,255,0.15)' },
|
accordionGuide: { backgroundColor: 'rgba(108,99,255,0.06)', borderWidth: 1, borderColor: 'rgba(108,99,255,0.15)' },
|
||||||
accordionReset: { backgroundColor: 'rgba(255,82,82,0.05)', borderWidth: 1, borderColor: 'rgba(255,82,82,0.12)' },
|
accordionReset: { backgroundColor: 'rgba(255,82,82,0.05)', borderWidth: 1, borderColor: 'rgba(255,82,82,0.12)' },
|
||||||
@@ -360,12 +417,9 @@ const st = StyleSheet.create({
|
|||||||
accordionIcon: { fontSize: 16, marginRight: spacing.sm },
|
accordionIcon: { fontSize: 16, marginRight: spacing.sm },
|
||||||
accordionText: { color: colors.text, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold, flex: 1 },
|
accordionText: { color: colors.text, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold, flex: 1 },
|
||||||
accordionArrow: { fontSize: 10 },
|
accordionArrow: { fontSize: 10 },
|
||||||
|
|
||||||
// Accordion body
|
|
||||||
accordionBody: { marginBottom: spacing.md },
|
accordionBody: { marginBottom: spacing.md },
|
||||||
desc: { color: colors.textSecondary, fontSize: 11, lineHeight: 16 },
|
desc: { color: colors.textSecondary, fontSize: 11, lineHeight: 16 },
|
||||||
|
|
||||||
// Divider
|
|
||||||
divider: { height: 1, backgroundColor: colors.border, marginVertical: spacing.md },
|
divider: { height: 1, backgroundColor: colors.border, marginVertical: spacing.md },
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
@@ -389,7 +443,7 @@ const st = StyleSheet.create({
|
|||||||
borderWidth: 1, borderColor: colors.border,
|
borderWidth: 1, borderColor: colors.border,
|
||||||
minHeight: 90, textAlignVertical: 'top', marginBottom: spacing.lg,
|
minHeight: 90, textAlignVertical: 'top', marginBottom: spacing.lg,
|
||||||
},
|
},
|
||||||
// Sign out
|
|
||||||
// Legal
|
// Legal
|
||||||
legalRow: { gap: spacing.sm },
|
legalRow: { gap: spacing.sm },
|
||||||
legalBtn: {
|
legalBtn: {
|
||||||
@@ -406,4 +460,5 @@ const st = StyleSheet.create({
|
|||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
version: { color: colors.textMuted, fontSize: 9, textAlign: 'center', marginTop: spacing.xl },
|
version: { color: colors.textMuted, fontSize: 9, textAlign: 'center', marginTop: spacing.xl },
|
||||||
|
footerQuote: { color: colors.textMuted, fontSize: 10, textAlign: 'center', fontStyle: 'italic', marginTop: spacing.sm },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { View, Text, Image, StyleSheet, Animated, Easing } from 'react-native';
|
import { View, Text, Image, StyleSheet, Animated, Easing } from 'react-native';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import StarField from '../components/StarField';
|
import StarField from '../components/StarField';
|
||||||
import useAuthStore from '../store/useAuthStore';
|
import useAuthStore from '../store/useAuthStore';
|
||||||
import useAppStore from '../store/useAppStore';
|
import useAppStore from '../store/useAppStore';
|
||||||
@@ -32,6 +33,18 @@ export default function SplashScreen({ navigation }) {
|
|||||||
try {
|
try {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
|
// === DEBUG: List registered users ===
|
||||||
|
try {
|
||||||
|
const raw = await AsyncStorage.getItem('nova40_users');
|
||||||
|
const users = raw ? JSON.parse(raw) : {};
|
||||||
|
const emails = Object.keys(users);
|
||||||
|
console.log('=== REGISTERED USERS ===');
|
||||||
|
console.log('Total:', emails.length);
|
||||||
|
emails.forEach((e) => console.log(' -', e));
|
||||||
|
console.log('========================');
|
||||||
|
} catch (_) {}
|
||||||
|
// === END DEBUG ===
|
||||||
|
|
||||||
// Check onboarding + session in parallel (with timeout to prevent hang)
|
// Check onboarding + session in parallel (with timeout to prevent hang)
|
||||||
const timeout = new Promise((r) => setTimeout(r, 5000));
|
const timeout = new Promise((r) => setTimeout(r, 5000));
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
|
|||||||
@@ -215,6 +215,18 @@ export async function signInWithGoogle() {
|
|||||||
throw new Error('Google login is not available in offline mode.');
|
throw new Error('Google login is not available in offline mode.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function signInWithFacebook() {
|
||||||
|
if (!USE_OFFLINE) {
|
||||||
|
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||||
|
provider: 'facebook',
|
||||||
|
options: { redirectTo: 'nova40://auth/callback' },
|
||||||
|
});
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
throw new Error('Facebook login is not available in offline mode.');
|
||||||
|
}
|
||||||
|
|
||||||
export function onAuthStateChange(callback) {
|
export function onAuthStateChange(callback) {
|
||||||
if (!USE_OFFLINE) {
|
if (!USE_OFFLINE) {
|
||||||
return supabase.auth.onAuthStateChange(callback);
|
return supabase.auth.onAuthStateChange(callback);
|
||||||
|
|||||||
@@ -56,6 +56,18 @@ const useAuthStore = create((set) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loginWithFacebook: async () => {
|
||||||
|
set({ loading: true, error: null });
|
||||||
|
try {
|
||||||
|
const data = await authService.signInWithFacebook();
|
||||||
|
set({ loading: false });
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
set({ loading: false, error: e.message });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
register: async (email, password) => {
|
register: async (email, password) => {
|
||||||
set({ loading: true, error: null });
|
set({ loading: true, error: null });
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user