diff --git a/AGENTS.md b/AGENTS.md index b6cab1e..8496a72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -280,3 +280,7 @@ Before every feature: - Don't change the design of the Nav, only add the routing functionality. - Keep 'leading-6' in all components, and fix if you find any un-matching ones. + +- If you see any svgs in the attached design look for it in the '/assets' folder. If we don't have it, try to create them yourself like you created the google icon in the login screens. + +- Keep the text sizes like : headings, subheadings, body text, etc consistent on each screen. diff --git a/UI/Onb v2/onbv2 scr2 card.png b/UI/Onb v2/onbv2 scr2 card.png new file mode 100644 index 0000000..307d3fc Binary files /dev/null and b/UI/Onb v2/onbv2 scr2 card.png differ diff --git a/UI/Onb v2/scr1.png b/UI/Onb v2/scr1.png new file mode 100644 index 0000000..061ed0f Binary files /dev/null and b/UI/Onb v2/scr1.png differ diff --git a/UI/Onb v2/scr2.png b/UI/Onb v2/scr2.png new file mode 100644 index 0000000..427cbf3 Binary files /dev/null and b/UI/Onb v2/scr2.png differ diff --git a/UI/Onb v2/scr3.png b/UI/Onb v2/scr3.png new file mode 100644 index 0000000..eaefe25 Binary files /dev/null and b/UI/Onb v2/scr3.png differ diff --git a/UI/Onb v2/scr4.png b/UI/Onb v2/scr4.png new file mode 100644 index 0000000..bff5b00 Binary files /dev/null and b/UI/Onb v2/scr4.png differ diff --git a/UI/Onb v2/scr5.png b/UI/Onb v2/scr5.png new file mode 100644 index 0000000..2eff784 Binary files /dev/null and b/UI/Onb v2/scr5.png differ diff --git a/app/(auth)/login.tsx b/app/(auth)/login.tsx index 4ef8aa5..e3fdd26 100644 --- a/app/(auth)/login.tsx +++ b/app/(auth)/login.tsx @@ -18,6 +18,7 @@ export default function LoginScreen() { footerText="Don't have an account?" heading="Welcome back" mode="login" + showTemporaryOnboardingBackButton subheading="Your thoughts are waiting for you." /> diff --git a/app/(onboarding)/onboarding-screen-1.tsx b/app/(onboarding)/onboarding-screen-1.tsx index 456364f..dbc60bf 100644 --- a/app/(onboarding)/onboarding-screen-1.tsx +++ b/app/(onboarding)/onboarding-screen-1.tsx @@ -3,62 +3,204 @@ import { LinearGradient } from "expo-linear-gradient"; import type { Href } from "expo-router"; import { Link, router, Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; -import { Pressable, ScrollView, Text, View } from "react-native"; +import { + Platform, + Pressable, + ScrollView, + Text, + useWindowDimensions, + View, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { OnboardingProgressHeader } from "@/components/onboarding/onboarding-progress-header"; import { images } from "@/constants/images"; +import { colors } from "@/constants/theme"; import { useOnboardingStore } from "@/store/onboarding-store"; const loginHref = "/login" as Href; +const nextHref = "/onboarding-screen-2" as Href; +const cream = "#FFF9F3"; +const deepPlum = "#251238"; +const mutedPlum = "#777188"; +const headingFontFamily = Platform.select({ + android: "serif", + default: "Georgia", + ios: "Georgia", +}); export default function OnboardingScreenOne() { + const { height, width } = useWindowDimensions(); + const insets = useSafeAreaInsets(); const completeOnboarding = useOnboardingStore( (state) => state.completeOnboarding, ); + const usableHeight = height - insets.top - insets.bottom; + const isTiny = usableHeight < 600; + const isVeryCompact = usableHeight < 740; + const isCompact = usableHeight < 860; + const isTextCompact = usableHeight < 720; + const isTextMedium = usableHeight < 850; + const isNarrow = width < 380; + + const horizontalPadding = isNarrow ? 28 : 34; + const heroWidth = Math.min( + width - horizontalPadding * 2, + isVeryCompact ? 244 : isCompact ? 272 : 306, + ); + const heroHeight = isVeryCompact ? 270 : isCompact ? 306 : 354; + const headingSize = isTiny ? 27 : isTextCompact ? 31 : isTextMedium ? 36 : 40; + const headingLineHeight = headingSize + 4; + const bodySize = isTiny ? 11.5 : isTextCompact ? 13 : isTextMedium ? 15 : 16; + const bodyLineHeight = isTiny ? 17 : isTextCompact ? 20 : isTextMedium ? 23 : 25; function handleExistingAccountPress() { completeOnboarding(); router.replace(loginHref); } + function handleSkipPress() { + completeOnboarding(); + router.replace(loginHref); + } + return ( ); } + +type FeatureItemProps = { + background: string; + color: string; + icon: "brain" | "heart" | "message-square"; + iconSize: number; + label: string; + labelLineHeight: number; + labelSize: number; +}; + +function FeatureItem({ + background, + color, + icon, + iconSize, + label, + labelLineHeight, + labelSize, +}: FeatureItemProps) { + const labelHeight = labelLineHeight * 2; + + return ( + + + {icon === "brain" ? ( + + ) : ( + + )} + + + {label} + + + ); +} diff --git a/app/(onboarding)/onboarding-screen-3.tsx b/app/(onboarding)/onboarding-screen-3.tsx index 1e2fc5e..00da5fb 100644 --- a/app/(onboarding)/onboarding-screen-3.tsx +++ b/app/(onboarding)/onboarding-screen-3.tsx @@ -1,289 +1,324 @@ import { LinearGradient } from "expo-linear-gradient"; -import { Link, Stack } from "expo-router"; +import type { Href } from "expo-router"; +import { Link, router, Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { + Platform, Pressable, - ScrollView, Text, useWindowDimensions, View, } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; -const moods = [ - { - emoji: "😊", - label: "Happy", - background: "#FFDDE8", - color: "#ff2056", - selected: true, - shadow: "0 8px 20px -8px rgba(255, 32, 86, 0.4)", - }, - { - emoji: "😌", - label: "Calm", - background: "#D8EEDB", - color: "#3f3f46", - shadow: "0 8px 20px -8px rgba(120, 170, 130, 0.4)", - }, - { - emoji: "😔", - label: "Sad", - background: "#DDEFFF", - color: "#3f3f46", - shadow: "0 8px 20px -8px rgba(120, 150, 200, 0.4)", - }, - { - emoji: "🔥", - label: "Motivated", - background: "#F4EFFA", - color: "#3f3f46", - shadow: "0 8px 20px -8px rgba(160, 140, 200, 0.4)", - }, - { - emoji: "🙏", - label: "Grateful", - background: "#D8EEDB", - color: "#3f3f46", - shadow: "0 8px 20px -8px rgba(120, 170, 130, 0.4)", - }, -]; +import { + InsightFeature, + OnboardingInsightsPreview, +} from "@/components/onboarding/onboarding-insights-preview"; +import { OnboardingProgressHeader } from "@/components/onboarding/onboarding-progress-header"; +import { colors } from "@/constants/theme"; +import { useOnboardingStore } from "@/store/onboarding-store"; + +const backHref = "/onboarding-screen-2" as Href; +const loginHref = "/login" as Href; +const nextHref = "/onboarding-screen-4" as Href; +const deepPlum = "#251238"; +const mutedPlum = "#777188"; +const headingFontFamily = Platform.select({ + android: "serif", + default: "Georgia", + ios: "Georgia", +}); export default function OnboardingScreenThree() { - const { height } = useWindowDimensions(); - const isCompact = height < 760; + const { height, width } = useWindowDimensions(); + const insets = useSafeAreaInsets(); + const completeOnboarding = useOnboardingStore( + (state) => state.completeOnboarding, + ); - const sceneHeight = isCompact ? 280 : 340; - const sceneMarginTop = isCompact ? 24 : 32; + const usableHeight = height - insets.top - insets.bottom; + const isVeryCompact = usableHeight < 600; + const isCompact = usableHeight < 720; + const isMedium = usableHeight < 850; + const isNarrow = width < 380; + const horizontalPadding = isNarrow ? 22 : 30; + const contentWidth = Math.min(width - horizontalPadding * 2, 390); + const featureWidth = Math.min(contentWidth, 340); + const headerHeight = isVeryCompact + ? 34 + : isCompact + ? 40 + : isMedium + ? 44 + : 48; + const previewHeight = isVeryCompact + ? 215 + : isCompact + ? Math.min(282, contentWidth) + : isMedium + ? Math.min(340, contentWidth * 1.02) + : Math.min(390, contentWidth * 1.12); + const headingSize = isVeryCompact ? 23 : isCompact ? 27 : isMedium ? 28 : 30; + const headingLineHeight = headingSize + 4; + const buttonHeight = + height < 680 ? 46 : height < 740 ? 50 : height < 860 ? 54 : 60; + const ctaTopPadding = + height < 680 ? 8 : height < 740 ? 10 : height < 860 ? 14 : 24; + const ctaGap = height < 680 ? 0 : height < 740 ? 2 : height < 860 ? 6 : 10; + + function handleSkipPress() { + completeOnboarding(); + router.replace(loginHref); + } return ( - - - ); } diff --git a/app/(onboarding)/onboarding-screen-4.tsx b/app/(onboarding)/onboarding-screen-4.tsx index c4b6b0c..981d5da 100644 --- a/app/(onboarding)/onboarding-screen-4.tsx +++ b/app/(onboarding)/onboarding-screen-4.tsx @@ -1,173 +1,271 @@ import { LinearGradient } from "expo-linear-gradient"; -import { Link, Stack } from "expo-router"; +import type { Href } from "expo-router"; +import { Link, router, Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { + Platform, Pressable, - ScrollView, Text, useWindowDimensions, View, } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; -const ritualCards = [ - { - icon: "☀️", - title: "Morning Intention ☀️", - body: "Start each day with clarity. Set one focus and carry it with you.", - prompt: "What will you focus on today?", - background: "#D8EEDB", - shadow: "0 12px 30px -12px rgba(120, 180, 140, 0.55)", - }, - { - icon: "🌙", - title: "Evening Reflection 🌙", - body: "End each day with gratitude. Reflect on what moved you.", - prompt: "How did today feel?", - background: "#F4EFFA", - shadow: "0 12px 30px -12px rgba(160, 140, 200, 0.55)", - }, -]; +import { + OnboardingRitualCard, + OnboardingRitualFeatures, +} from "@/components/onboarding/onboarding-ritual-content"; +import { OnboardingProgressHeader } from "@/components/onboarding/onboarding-progress-header"; +import { colors } from "@/constants/theme"; +import { useOnboardingStore } from "@/store/onboarding-store"; + +const backHref = "/onboarding-screen-3" as Href; +const loginHref = "/login" as Href; +const nextHref = "/onboarding-screen-5" as Href; +const deepPlum = "#251238"; +const mutedPlum = "#777188"; +const headingFontFamily = Platform.select({ + android: "serif", + default: "Georgia", + ios: "Georgia", +}); export default function OnboardingScreenFour() { - const { height } = useWindowDimensions(); - const isCompact = height < 760; + const { height, width } = useWindowDimensions(); + const insets = useSafeAreaInsets(); + const completeOnboarding = useOnboardingStore( + (state) => state.completeOnboarding, + ); + + const usableHeight = height - insets.top - insets.bottom; + const isTiny = usableHeight < 600; + const isCompact = usableHeight < 720; + const isMedium = usableHeight < 850; + const isNarrow = width < 380; + const size = isTiny + ? "tiny" + : isCompact + ? "compact" + : isMedium + ? "medium" + : "wide"; + const horizontalPadding = isNarrow ? 22 : 30; + const contentWidth = Math.min(width - horizontalPadding * 2, 390); + const ritualWidth = Math.min(contentWidth, 340); + const headerHeight = isTiny ? 34 : isCompact ? 40 : isMedium ? 44 : 48; + const headingSize = isTiny ? 23 : isCompact ? 27 : isMedium ? 28 : 30; + const headingLineHeight = headingSize + 4; + const cardHeight = isTiny ? 128 : isCompact ? 140 : isMedium ? 154 : 170; + const cardGap = isTiny ? 5 : isCompact ? 6 : isMedium ? 8 : 10; + const featureHeight = isTiny ? 72 : isCompact ? 84 : isMedium ? 96 : 110; + const buttonHeight = + height < 680 ? 46 : height < 740 ? 50 : height < 860 ? 54 : 60; + const ctaTopPadding = + height < 680 ? 8 : height < 740 ? 10 : height < 860 ? 14 : 24; + const ctaGap = height < 680 ? 0 : height < 740 ? 2 : height < 860 ? 6 : 10; + + function handleSkipPress() { + completeOnboarding(); + router.replace(loginHref); + } return ( - - - ); } diff --git a/app/(onboarding)/onboarding-screen-5.tsx b/app/(onboarding)/onboarding-screen-5.tsx index 513cfb3..8b69214 100644 --- a/app/(onboarding)/onboarding-screen-5.tsx +++ b/app/(onboarding)/onboarding-screen-5.tsx @@ -1,49 +1,58 @@ -import { Feather, Ionicons } from "@expo/vector-icons"; import { LinearGradient } from "expo-linear-gradient"; import { router, Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { + Platform, Pressable, - ScrollView, Text, useWindowDimensions, View, } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { OnboardingProgressHeader } from "@/components/onboarding/onboarding-progress-header"; +import { + OnboardingOverviewFeatureStrip, + OnboardingOverviewPreview, +} from "@/components/onboarding/onboarding-overview-content"; +import { colors } from "@/constants/theme"; import { useOnboardingStore } from "@/store/onboarding-store"; -const featureCards = [ - { - icon: "book-open", - label: "Daily\nPrompts", - background: "#FFDDE8", - color: "#ff2056", - }, - { - icon: "sparkles-outline", - label: "AI Insights", - background: "#D8EEDB", - color: "#059669", - }, - { - icon: "bar-chart-2", - label: "Mood Trends", - background: "#F4EFFA", - color: "#8b5cf6", - }, -] as const; +const deepPlum = "#251238"; +const mutedPlum = "#777188"; +const headingFontFamily = Platform.select({ + android: "serif", + default: "Georgia", + ios: "Georgia", +}); export default function OnboardingScreenFive() { - const { height } = useWindowDimensions(); + const { height, width } = useWindowDimensions(); + const insets = useSafeAreaInsets(); const completeOnboarding = useOnboardingStore( (state) => state.completeOnboarding, ); - const isCompact = height < 760; - const heroHeight = isCompact ? 260 : 332; - const outerSize = isCompact ? 156 : 176; - const innerSize = isCompact ? 112 : 128; - const featureIconSize = isCompact ? 50 : 56; + const usableHeight = height - insets.top - insets.bottom; + const isTiny = usableHeight < 600; + const isCompact = usableHeight < 720; + const isMedium = usableHeight < 850; + const isNarrow = width < 380; + const horizontalPadding = isNarrow ? 18 : 24; + const contentWidth = Math.min(width - horizontalPadding * 2, 390); + const headerHeight = isTiny ? 34 : isCompact ? 40 : isMedium ? 44 : 48; + const headingSize = isTiny ? 23 : isCompact ? 27 : isMedium ? 29 : 31; + const headingLineHeight = headingSize + 4; + const subtitleSize = isTiny ? 10 : isCompact ? 11.5 : isMedium ? 13 : 14; + const subtitleLineHeight = isTiny ? 14 : isCompact ? 17 : isMedium ? 19 : 21; + const previewHeight = isTiny ? 140 : isCompact ? 205 : isMedium ? 270 : 330; + const featureHeight = isTiny ? 88 : isCompact ? 98 : isMedium ? 112 : 126; + const featureIconSize = isTiny ? 38 : isCompact ? 42 : isMedium ? 46 : 52; + const buttonHeight = + height < 680 ? 46 : height < 740 ? 50 : height < 860 ? 54 : 60; + const ctaTopPadding = + height < 680 ? 6 : height < 740 ? 8 : height < 860 ? 12 : 18; + const ctaGap = height < 680 ? 0 : height < 740 ? 2 : height < 860 ? 6 : 10; function handleStartWritingPress() { completeOnboarding(); @@ -57,236 +66,191 @@ export default function OnboardingScreenFive() { return ( - - - ); } diff --git a/assets/images/AI-chat-mockup.png b/assets/images/AI-chat-mockup.png new file mode 100644 index 0000000..307d3fc Binary files /dev/null and b/assets/images/AI-chat-mockup.png differ diff --git a/assets/images/ai-card.png b/assets/images/ai-card.png new file mode 100644 index 0000000..18e6d62 Binary files /dev/null and b/assets/images/ai-card.png differ diff --git a/assets/images/evening-card.png b/assets/images/evening-card.png new file mode 100644 index 0000000..e88fba9 Binary files /dev/null and b/assets/images/evening-card.png differ diff --git a/assets/images/mood card heart.png b/assets/images/mood card heart.png new file mode 100644 index 0000000..143bedc Binary files /dev/null and b/assets/images/mood card heart.png differ diff --git a/assets/images/morning-card.png b/assets/images/morning-card.png new file mode 100644 index 0000000..701feb4 Binary files /dev/null and b/assets/images/morning-card.png differ diff --git a/assets/images/noon.png b/assets/images/noon.png new file mode 100644 index 0000000..1432a53 Binary files /dev/null and b/assets/images/noon.png differ diff --git a/components/auth/auth-screen.tsx b/components/auth/auth-screen.tsx index beb83c4..3d6636d 100644 --- a/components/auth/auth-screen.tsx +++ b/components/auth/auth-screen.tsx @@ -23,6 +23,7 @@ import { import { images } from "@/constants/images"; import { useAppDialog } from "@/hooks/useAppDialog"; +import { useOnboardingStore } from "@/store/onboarding-store"; import { AuthTextField } from "./auth-text-field"; import { @@ -45,11 +46,13 @@ type AuthScreenProps = { heading: string; mode?: "login" | "signup"; subheading: string; + showTemporaryOnboardingBackButton?: boolean; }; type VerificationFlow = "signup" | "login-email-code" | null; const ssoRedirectUrl = AuthSession.makeRedirectUri({ path: "sso" }); +const onboardingHref = "/onboarding-screen-1" as Href; export function AuthScreen({ buttonText, @@ -60,6 +63,7 @@ export function AuthScreen({ footerText, heading, mode = "signup", + showTemporaryOnboardingBackButton = false, subheading, }: AuthScreenProps) { const { height, width } = useWindowDimensions(); @@ -88,6 +92,7 @@ export function AuthScreen({ const { fetchStatus: signUpFetchStatus, signUp } = useSignUp(); const { startSSOFlow } = useSSO(); const { showDialog } = useAppDialog(); + const resetOnboarding = useOnboardingStore((state) => state.resetOnboarding); const isFetching = signInFetchStatus === "fetching" || signUpFetchStatus === "fetching"; const isClerkReady = isAuthLoaded && !isFetching; @@ -346,6 +351,11 @@ export function AuthScreen({ } } + function handleTemporaryOnboardingBackPress() { + resetOnboarding(); + router.replace(onboardingHref); + } + return ( Take a moment. Breathe. Begin again. + + {isLogin && showTemporaryOnboardingBackButton ? ( + + + Back to onboarding + + + ) : null} diff --git a/components/home/home-screen.tsx b/components/home/home-screen.tsx index 166b605..94e9e59 100644 --- a/components/home/home-screen.tsx +++ b/components/home/home-screen.tsx @@ -3,8 +3,14 @@ import { Image } from "expo-image"; import { LinearGradient } from "expo-linear-gradient"; import { useRouter, type Href } from "expo-router"; import { StatusBar } from "expo-status-bar"; -import { useMemo } from "react"; -import { Pressable, ScrollView, Text, View } from "react-native"; +import { useEffect, useMemo, useState } from "react"; +import { + Pressable, + ScrollView, + Text, + useWindowDimensions, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HomeMoodCheckInCard } from "@/components/home/mood/HomeMoodCheckInCard"; @@ -13,6 +19,7 @@ import { bottomTabBarBaseHeight, } from "@/components/navigation/bottom-tab-bar"; import { ScreenErrorState } from "@/components/states/ScreenErrorState"; +import { ScenicCardBackground } from "@/components/ui/scenic-card-background"; import { images } from "@/constants/images"; import { fallbackMoodMetadata, moodMetadata } from "@/constants/moods"; import { useDelayedVisibility } from "@/hooks/useDelayedVisibility"; @@ -21,7 +28,10 @@ import { useJournalHydrationStore, useJournalStore, } from "@/store/journal-store"; -import type { JournalEntry as StoredJournalEntry } from "@/types/journal"; +import type { + EntryType, + JournalEntry as StoredJournalEntry, +} from "@/types/journal"; type HomeScreenProps = { avatarUrl?: string; @@ -31,6 +41,51 @@ type HomeScreenProps = { const colors = { primary: "#FF2056", }; +const homeScenicCardAspectRatio = 1.6; + +type GreetingPeriod = "evening" | "morning" | "noon"; + +const greetingBackgrounds = { + evening: images.eveningCard, + morning: images.morningCard, + noon: images.noon, +} as const; + +const entryTypeMetadata: Record< + EntryType, + { color: string; icon: string; label: string } +> = { + ai_reflection: { + color: "#A3B31E", + icon: "✨", + label: "AI prompt", + }, + daily_prompt: { + color: "#7C9FD9", + icon: "💭", + label: "Daily prompt", + }, + evening_reflection: { + color: "#7C9FD9", + icon: "🌙", + label: "Evening reflection", + }, + free_write: { + color: "#71717B", + icon: "✍️", + label: "Journal entry", + }, + gratitude: { + color: "#FF8A3D", + icon: "🙏", + label: "Gratitude", + }, + morning_intention: { + color: "#FFB02E", + icon: "☀️", + label: "Morning intention", + }, +}; const aiReflectionPrompt = "What made you smile unexpectedly today?"; const journalEditorHref = { @@ -56,14 +111,21 @@ const morningIntentionHref = { type HomeRecentEntry = { backgroundColor: string; + borderColor: string; date: string; emoji: string; excerpt: string; id: string; + moodColor: string; + moodLabel: string; title: string; + typeColor: string; + typeIcon: string; + typeLabel: string; }; export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) { + const { width } = useWindowDimensions(); const insets = useSafeAreaInsets(); const router = useRouter(); const entries = useJournalStore((state) => state.entries); @@ -75,9 +137,13 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) { ); const showHydrationState = useDelayedVisibility(!hasHydrated); const bottomNavHeight = bottomTabBarBaseHeight + insets.bottom; - const displayName = firstName?.trim() || "Aryan"; - const greeting = useMemo(() => getGreeting(), []); - const todayLabel = useMemo(() => formatTodayDate(), []); + const topBackgroundHeight = Math.max(188, insets.top + 148); + const homeCardWidth = Math.max(width - 56, 0); + const currentTime = useCurrentMinute(); + const displayName = firstName?.trim() || ""; + const greeting = useMemo(() => getGreeting(currentTime), [currentTime]); + const greetingBackground = greetingBackgrounds[greeting.period]; + const todayLabel = useMemo(() => formatTodayDate(currentTime), [currentTime]); const reflectionStreak = useMemo(() => getReflectionStreak(entries), [entries]); const morningIntention = useMemo( () => getTodayMorningIntention(entries), @@ -120,13 +186,31 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) { return (