RoxyAPI

Menu

Building a Numerology Dashboard App with React Native + Numerology API

20 min read
By Emma Chen
NumerologyReact NativeDashboard AppComplete TutorialApp Development

Complete tutorial for building a production-ready numerology app. User profiles, complete charts, karmic lessons, forecasts, notes, and history tracking.

Building a Numerology Dashboard App with React Native + Numerology API

Building a complete numerology dashboard requires orchestrating multiple data sources, creating intuitive navigation, and presenting complex spiritual insights in digestible formats. This comprehensive tutorial walks you through building a production-ready numerology app from scratch.

By the end, you will have a fully functional app with user profiles, complete numerology charts, karmic analysis, personal year forecasts, note-taking, and history tracking. Every feature uses real API responses from the RoxyAPI Numerology API.

What We Will Build

Our numerology dashboard app includes:

  1. User Onboarding - Collect name and birth date with validation
  2. Dashboard Home - Overview of all core numbers
  3. Complete Numerology Chart - Detailed view with all calculations
  4. Karmic Analysis - Debt detection and lessons to learn
  5. Personal Year Forecast - Current year guidance and insights
  6. Notes System - Journal entries tied to numerology insights
  7. History Tracking - View past calculations and growth over time
  8. Settings & Profile - Manage user data and preferences

Tech Stack

  • Frontend: React Native (Expo)
  • State Management: React Context + AsyncStorage
  • API: RoxyAPI Numerology endpoints
  • Navigation: React Navigation v6
  • Storage: AsyncStorage for offline data
  • UI: Custom components with React Native Paper

Prerequisites

  • Node.js 18+ or Bun
  • Expo CLI installed (npm install -g expo-cli)
  • RoxyAPI account (sign up here)
  • Basic React Native knowledge

Project Setup

Initialize the Expo project:

npx create-expo-app numerology-dashboard
cd numerology-dashboard
npm install @react-navigation/native @react-navigation/bottom-tabs @react-navigation/stack
npm install @react-native-async-storage/async-storage react-native-paper
npm install expo-linear-gradient
npx expo install react-native-screens react-native-safe-area-context

Step 1: API Client and Types

Create comprehensive types and API client:

// lib/types.ts
export interface UserProfile {
  id: string;
  fullName: string;
  birthDate: Date;
  createdAt: Date;
}

export interface NumberMeaning {
  title: string;
  keywords: string[];
  description: string;
  strengths: string[];
  challenges: string[];
  career: string;
  relationships: string;
  spirituality: string;
}

export interface CoreNumber {
  number: number;
  calculation: string;
  type: 'single' | 'master';
  hasKarmicDebt: boolean;
  karmicDebtNumber?: number;
  meaning: NumberMeaning;
}

export interface KarmicLesson {
  number: number;
  lesson: string;
  description: string;
  howToOvercome: string;
}

export interface PersonalYearForecast {
  personalYear: number;
  cycle: string;
  theme: string;
  forecast: string;
  opportunities: string[];
  challenges: string[];
  advice: string;
}

export interface NumerologyChart {
  profile: {
    name: string;
    birthdate: string;
  };
  coreNumbers: {
    lifePath: CoreNumber;
    expression: CoreNumber;
    soulUrge: CoreNumber;
    personality: CoreNumber;
    birthDay: CoreNumber;
    maturity: CoreNumber;
  };
  additionalInsights: {
    karmicLessons: {
      missingNumbers: number[];
      lessons: KarmicLesson[];
      presentNumbers: Record<string, number>;
    };
    karmicDebt: {
      hasKarmicDebt: boolean;
      debtNumbers: number[];
      meanings: Array<{
        number: number;
        lesson: string;
        description: string;
      }>;
    };
    personalYear: PersonalYearForecast;
  };
  summary: string;
}

export interface Note {
  id: string;
  userId: string;
  content: string;
  tags: string[];
  createdAt: Date;
  relatedNumber?: number;
}

API client:

// lib/api.ts
const API_BASE = 'http://localhost:3001/api/v2/numerology';
const API_KEY = 'b91dd626-4d6a-4951-bf87-e1c7c90c45ba.bbdc9613db164edb.ggQtNcDzEx-YqTK6l2ssX4Y_GErDRONUps9vCOlcMrM';

export async function fetchCompleteChart(
  fullName: string,
  birthDate: Date,
  currentYear: number = new Date().getFullYear()
): Promise<NumerologyChart> {
  const response = await fetch(`${API_BASE}/chart`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY,
    },
    body: JSON.stringify({
      fullName,
      year: birthDate.getFullYear(),
      month: birthDate.getMonth() + 1,
      day: birthDate.getDate(),
      currentYear,
    }),
  });

  if (!response.ok) {
    throw new Error(`Chart API error: ${response.statusText}`);
  }

  return response.json();
}

export async function fetchPersonalYearUpdate(
  birthDate: Date,
  year: number
): Promise<PersonalYearForecast> {
  const response = await fetch(`${API_BASE}/personal-year`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY,
    },
    body: JSON.stringify({
      month: birthDate.getMonth() + 1,
      day: birthDate.getDate(),
      year,
    }),
  });

  if (!response.ok) {
    throw new Error(`Personal Year API error: ${response.statusText}`);
  }

  return response.json();
}

Step 2: Context for Global State

Create context to manage user data and chart:

// context/NumerologyContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { fetchCompleteChart, fetchPersonalYearUpdate } from '../lib/api';
import type { UserProfile, NumerologyChart, Note } from '../lib/types';

interface NumerologyContextValue {
  user: UserProfile | null;
  chart: NumerologyChart | null;
  notes: Note[];
  loading: boolean;
  setUser: (user: UserProfile) => Promise<void>;
  updateChart: () => Promise<void>;
  addNote: (note: Omit<Note, 'id' | 'userId' | 'createdAt'>) => Promise<void>;
  deleteNote: (noteId: string) => Promise<void>;
  logout: () => Promise<void>;
}

const NumerologyContext = createContext<NumerologyContextValue | null>(null);

export function NumerologyProvider({ children }: { children: React.ReactNode }) {
  const [user, setUserState] = useState<UserProfile | null>(null);
  const [chart, setChart] = useState<NumerologyChart | null>(null);
  const [notes, setNotes] = useState<Note[]>([]);
  const [loading, setLoading] = useState(true);

  // Load user and chart from storage on mount
  useEffect(() => {
    async function loadData() {
      try {
        const userJson = await AsyncStorage.getItem('@user');
        const chartJson = await AsyncStorage.getItem('@chart');
        const notesJson = await AsyncStorage.getItem('@notes');

        if (userJson) setUserState(JSON.parse(userJson));
        if (chartJson) setChart(JSON.parse(chartJson));
        if (notesJson) setNotes(JSON.parse(notesJson));
      } catch (error) {
        console.error('Failed to load data:', error);
      } finally {
        setLoading(false);
      }
    }

    loadData();
  }, []);

  const setUser = async (newUser: UserProfile) => {
    setUserState(newUser);
    await AsyncStorage.setItem('@user', JSON.stringify(newUser));

    // Fetch chart automatically
    const chartData = await fetchCompleteChart(
      newUser.fullName,
      newUser.birthDate
    );
    setChart(chartData);
    await AsyncStorage.setItem('@chart', JSON.stringify(chartData));
  };

  const updateChart = async () => {
    if (!user) return;

    const chartData = await fetchCompleteChart(
      user.fullName,
      user.birthDate
    );
    setChart(chartData);
    await AsyncStorage.setItem('@chart', JSON.stringify(chartData));
  };

  const addNote = async (noteData: Omit<Note, 'id' | 'userId' | 'createdAt'>) => {
    if (!user) return;

    const newNote: Note = {
      ...noteData,
      id: Date.now().toString(),
      userId: user.id,
      createdAt: new Date(),
    };

    const updatedNotes = [newNote, ...notes];
    setNotes(updatedNotes);
    await AsyncStorage.setItem('@notes', JSON.stringify(updatedNotes));
  };

  const deleteNote = async (noteId: string) => {
    const updatedNotes = notes.filter((n) => n.id !== noteId);
    setNotes(updatedNotes);
    await AsyncStorage.setItem('@notes', JSON.stringify(updatedNotes));
  };

  const logout = async () => {
    setUserState(null);
    setChart(null);
    setNotes([]);
    await AsyncStorage.multiRemove(['@user', '@chart', '@notes']);
  };

  return (
    <NumerologyContext.Provider
      value={{
        user,
        chart,
        notes,
        loading,
        setUser,
        updateChart,
        addNote,
        deleteNote,
        logout,
      }}
    >
      {children}
    </NumerologyContext.Provider>
  );
}

export function useNumerology() {
  const context = useContext(NumerologyContext);
  if (!context) {
    throw new Error('useNumerology must be used within NumerologyProvider');
  }
  return context;
}

Step 3: Onboarding Screen

Collect user information:

// screens/OnboardingScreen.tsx
import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  ScrollView,
  ActivityIndicator,
} from 'react-native';
import { useNumerology } from '../context/NumerologyContext';

export function OnboardingScreen({ navigation }: any) {
  const { setUser } = useNumerology();
  const [fullName, setFullName] = useState('');
  const [birthDate, setBirthDate] = useState({ year: '', month: '', day: '' });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const validate = (): string | null => {
    if (fullName.trim().length < 3) {
      return 'Please enter your full name';
    }

    const year = parseInt(birthDate.year);
    const month = parseInt(birthDate.month);
    const day = parseInt(birthDate.day);

    if (isNaN(year) || year < 1900 || year > new Date().getFullYear()) {
      return 'Please enter valid birth year';
    }

    if (isNaN(month) || month < 1 || month > 12) {
      return 'Please enter valid month (1-12)';
    }

    if (isNaN(day) || day < 1 || day > 31) {
      return 'Please enter valid day (1-31)';
    }

    return null;
  };

  const handleSubmit = async () => {
    const validationError = validate();
    if (validationError) {
      setError(validationError);
      return;
    }

    setError(null);
    setLoading(true);

    try {
      const user = {
        id: Date.now().toString(),
        fullName: fullName.trim(),
        birthDate: new Date(
          parseInt(birthDate.year),
          parseInt(birthDate.month) - 1,
          parseInt(birthDate.day)
        ),
        createdAt: new Date(),
      };

      await setUser(user);
      navigation.replace('Main');
    } catch (err) {
      setError('Failed to create profile. Please try again.');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

  return (
    <ScrollView style={styles.container}>
      <View style={styles.content}>
        <Text style={styles.title}>Welcome to Your{'\n'}Numerology Journey</Text>
        <Text style={styles.subtitle}>
          Discover your life purpose, talents, and spiritual path through the ancient wisdom
          of numerology
        </Text>

        <View style={styles.form}>
          <Text style={styles.label}>Full Birth Name</Text>
          <Text style={styles.hint}>Exact name on your birth certificate</Text>
          <TextInput
            style={styles.input}
            placeholder="John William Smith"
            value={fullName}
            onChangeText={(text) => {
              setFullName(text);
              setError(null);
            }}
            autoCapitalize="words"
          />

          <Text style={[styles.label, { marginTop: 24 }]}>Birth Date</Text>
          <View style={styles.dateRow}>
            <View style={styles.dateField}>
              <Text style={styles.dateLabel}>Year</Text>
              <TextInput
                style={styles.dateInput}
                placeholder="1988"
                keyboardType="number-pad"
                maxLength={4}
                value={birthDate.year}
                onChangeText={(text) => {
                  setBirthDate({ ...birthDate, year: text });
                  setError(null);
                }}
              />
            </View>

            <View style={styles.dateField}>
              <Text style={styles.dateLabel}>Month</Text>
              <TextInput
                style={styles.dateInput}
                placeholder="07"
                keyboardType="number-pad"
                maxLength={2}
                value={birthDate.month}
                onChangeText={(text) => {
                  setBirthDate({ ...birthDate, month: text });
                  setError(null);
                }}
              />
            </View>

            <View style={styles.dateField}>
              <Text style={styles.dateLabel}>Day</Text>
              <TextInput
                style={styles.dateInput}
                placeholder="15"
                keyboardType="number-pad"
                maxLength={2}
                value={birthDate.day}
                onChangeText={(text) => {
                  setBirthDate({ ...birthDate, day: text });
                  setError(null);
                }}
              />
            </View>
          </View>

          {error && <Text style={styles.error}>{error}</Text>}

          <TouchableOpacity
            style={[styles.button, loading && styles.buttonDisabled]}
            onPress={handleSubmit}
            disabled={loading}
          >
            {loading ? (
              <ActivityIndicator color="#FFFFFF" />
            ) : (
              <Text style={styles.buttonText}>Create My Chart</Text>
            )}
          </TouchableOpacity>
        </View>

        <Text style={styles.privacy}>
          Your data is stored locally on your device and never shared
        </Text>
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F9FAFB',
  },
  content: {
    padding: 24,
    paddingTop: 60,
  },
  title: {
    fontSize: 32,
    fontWeight: 'bold',
    color: '#111827',
    marginBottom: 16,
    textAlign: 'center',
  },
  subtitle: {
    fontSize: 16,
    color: '#6B7280',
    textAlign: 'center',
    lineHeight: 24,
    marginBottom: 40,
  },
  form: {
    backgroundColor: '#FFFFFF',
    borderRadius: 20,
    padding: 24,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.1,
    shadowRadius: 12,
    elevation: 6,
  },
  label: {
    fontSize: 16,
    fontWeight: '600',
    color: '#111827',
    marginBottom: 8,
  },
  hint: {
    fontSize: 13,
    color: '#9CA3AF',
    marginBottom: 8,
  },
  input: {
    borderWidth: 2,
    borderColor: '#E5E7EB',
    borderRadius: 12,
    padding: 16,
    fontSize: 16,
    color: '#111827',
    backgroundColor: '#F9FAFB',
  },
  dateRow: {
    flexDirection: 'row',
    gap: 12,
  },
  dateField: {
    flex: 1,
  },
  dateLabel: {
    fontSize: 13,
    color: '#6B7280',
    marginBottom: 6,
  },
  dateInput: {
    borderWidth: 2,
    borderColor: '#E5E7EB',
    borderRadius: 12,
    padding: 16,
    fontSize: 16,
    color: '#111827',
    backgroundColor: '#F9FAFB',
    textAlign: 'center',
  },
  error: {
    color: '#EF4444',
    fontSize: 14,
    marginTop: 12,
  },
  button: {
    backgroundColor: '#6B46C1',
    borderRadius: 12,
    padding: 18,
    alignItems: 'center',
    marginTop: 24,
  },
  buttonDisabled: {
    backgroundColor: '#9CA3AF',
  },
  buttonText: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: '600',
  },
  privacy: {
    fontSize: 12,
    color: '#9CA3AF',
    textAlign: 'center',
    marginTop: 24,
  },
});

Step 4: Dashboard Home Screen

Overview of all core numbers:

// screens/DashboardScreen.tsx
import React from 'react';
import { ScrollView, View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useNumerology } from '../context/NumerologyContext';

export function DashboardScreen({ navigation }: any) {
  const { user, chart } = useNumerology();

  if (!chart) {
    return (
      <View style={styles.loadingContainer}>
        <Text>Loading your numerology chart...</Text>
      </View>
    );
  }

  const coreNumbers = [
    {
      key: 'lifePath',
      label: 'Life Path',
      icon: '🎯',
      gradient: ['#6B46C1', '#8B5CF6'],
    },
    {
      key: 'expression',
      label: 'Expression',
      icon: '✨',
      gradient: ['#EC4899', '#F472B6'],
    },
    {
      key: 'soulUrge',
      label: 'Soul Urge',
      icon: '💖',
      gradient: ['#3B82F6', '#60A5FA'],
    },
    {
      key: 'personality',
      label: 'Personality',
      icon: '👤',
      gradient: ['#10B981', '#34D399'],
    },
  ];

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.greeting}>Hello, {user?.fullName.split(' ')[0]}</Text>
        <Text style={styles.subtitle}>Your numerology dashboard</Text>
      </View>

      {/* Personal Year Card */}
      <TouchableOpacity
        style={styles.personalYearCard}
        onPress={() => navigation.navigate('PersonalYear')}
      >
        <LinearGradient
          colors={['#F59E0B', '#FBBF24']}
          start={{ x: 0, y: 0 }}
          end={{ x: 1, y: 1 }}
          style={styles.personalYearGradient}
        >
          <View style={styles.personalYearContent}>
            <Text style={styles.personalYearLabel}>Personal Year 2026</Text>
            <Text style={styles.personalYearNumber}>
              {chart.additionalInsights.personalYear.personalYear}
            </Text>
            <Text style={styles.personalYearTheme}>
              {chart.additionalInsights.personalYear.theme}
            </Text>
          </View>
        </LinearGradient>
      </TouchableOpacity>

      {/* Core Numbers Grid */}
      <View style={styles.grid}>
        {coreNumbers.map((item) => {
          const numberData = chart.coreNumbers[item.key as keyof typeof chart.coreNumbers];
          return (
            <TouchableOpacity
              key={item.key}
              style={styles.numberCard}
              onPress={() => navigation.navigate('NumberDetail', { numberType: item.key })}
            >
              <LinearGradient
                colors={item.gradient}
                start={{ x: 0, y: 0 }}
                end={{ x: 1, y: 1 }}
                style={styles.numberGradient}
              >
                <Text style={styles.numberIcon}>{item.icon}</Text>
                <Text style={styles.numberValue}>{numberData.number}</Text>
                <Text style={styles.numberLabel}>{item.label}</Text>
                {numberData.type === 'master' && (
                  <View style={styles.masterBadge}>
                    <Text style={styles.masterText}>Master</Text>
                  </View>
                )}
              </LinearGradient>
            </TouchableOpacity>
          );
        })}
      </View>

      {/* Quick Links */}
      <View style={styles.quickLinks}>
        <TouchableOpacity
          style={styles.linkCard}
          onPress={() => navigation.navigate('CompleteChart')}
        >
          <Text style={styles.linkIcon}>📊</Text>
          <Text style={styles.linkText}>Complete Chart</Text>
        </TouchableOpacity>

        <TouchableOpacity
          style={styles.linkCard}
          onPress={() => navigation.navigate('KarmicAnalysis')}
        >
          <Text style={styles.linkIcon}>🔮</Text>
          <Text style={styles.linkText}>Karmic Analysis</Text>
        </TouchableOpacity>

        <TouchableOpacity
          style={styles.linkCard}
          onPress={() => navigation.navigate('Notes')}
        >
          <Text style={styles.linkIcon}>📝</Text>
          <Text style={styles.linkText}>My Notes</Text>
        </TouchableOpacity>
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F9FAFB',
  },
  loadingContainer: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  header: {
    padding: 24,
    paddingTop: 60,
  },
  greeting: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#111827',
  },
  subtitle: {
    fontSize: 16,
    color: '#6B7280',
    marginTop: 4,
  },
  personalYearCard: {
    marginHorizontal: 16,
    marginBottom: 24,
    borderRadius: 20,
    overflow: 'hidden',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 12,
    elevation: 8,
  },
  personalYearGradient: {
    padding: 24,
  },
  personalYearContent: {
    alignItems: 'center',
  },
  personalYearLabel: {
    fontSize: 14,
    color: '#FFFFFF',
    opacity: 0.9,
    fontWeight: '600',
    textTransform: 'uppercase',
    letterSpacing: 1,
  },
  personalYearNumber: {
    fontSize: 64,
    fontWeight: 'bold',
    color: '#FFFFFF',
    marginVertical: 8,
  },
  personalYearTheme: {
    fontSize: 20,
    color: '#FFFFFF',
    fontWeight: '600',
  },
  grid: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    paddingHorizontal: 8,
  },
  numberCard: {
    width: '50%',
    padding: 8,
  },
  numberGradient: {
    borderRadius: 20,
    padding: 24,
    alignItems: 'center',
    minHeight: 160,
    justifyContent: 'center',
  },
  numberIcon: {
    fontSize: 32,
    marginBottom: 8,
  },
  numberValue: {
    fontSize: 48,
    fontWeight: 'bold',
    color: '#FFFFFF',
    marginBottom: 4,
  },
  numberLabel: {
    fontSize: 14,
    color: '#FFFFFF',
    fontWeight: '600',
    textTransform: 'uppercase',
    letterSpacing: 1,
  },
  masterBadge: {
    backgroundColor: 'rgba(255, 255, 255, 0.3)',
    borderRadius: 8,
    paddingHorizontal: 8,
    paddingVertical: 4,
    marginTop: 8,
  },
  masterText: {
    fontSize: 10,
    color: '#FFFFFF',
    fontWeight: '600',
  },
  quickLinks: {
    flexDirection: 'row',
    paddingHorizontal: 16,
    paddingVertical: 8,
    gap: 12,
  },
  linkCard: {
    flex: 1,
    backgroundColor: '#FFFFFF',
    borderRadius: 16,
    padding: 20,
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 4,
  },
  linkIcon: {
    fontSize: 32,
    marginBottom: 8,
  },
  linkText: {
    fontSize: 13,
    fontWeight: '600',
    color: '#111827',
    textAlign: 'center',
  },
});

Step 5: Complete Chart Screen

Display all numbers with calculations:

// screens/CompleteChartScreen.tsx
import React from 'react';
import { ScrollView, View, Text, StyleSheet } from 'react-native';
import { useNumerology } from '../context/NumerologyContext';

export function CompleteChartScreen() {
  const { chart } = useNumerology();

  if (!chart) return null;

  const renderNumberSection = (
    title: string,
    emoji: string,
    numberData: any
  ) => (
    <View style={styles.section}>
      <View style={styles.sectionHeader}>
        <Text style={styles.emoji}>{emoji}</Text>
        <Text style={styles.sectionTitle}>{title}</Text>
      </View>

      <View style={styles.numberRow}>
        <View style={styles.numberBadge}>
          <Text style={styles.numberText}>{numberData.number}</Text>
        </View>
        <View style={styles.typeInfo}>
          <Text style={styles.numberTitle}>{numberData.meaning.title}</Text>
          {numberData.type === 'master' && (
            <View style={styles.masterTag}>
              <Text style={styles.masterTagText}>✨ Master Number</Text>
            </View>
          )}
        </View>
      </View>

      <View style={styles.calculationBox}>
        <Text style={styles.calculationLabel}>Calculation</Text>
        <Text style={styles.calculationText}>{numberData.calculation}</Text>
      </View>

      <View style={styles.keywordsContainer}>
        {numberData.meaning.keywords.slice(0, 5).map((keyword: string, idx: number) => (
          <View key={idx} style={styles.keywordBadge}>
            <Text style={styles.keywordText}>{keyword}</Text>
          </View>
        ))}
      </View>
    </View>
  );

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>Complete Numerology Chart</Text>
        <Text style={styles.subtitle}>{chart.profile.name}</Text>
        <Text style={styles.birthdate}>Born {chart.profile.birthdate}</Text>
      </View>

      {renderNumberSection('Life Path', '🎯', chart.coreNumbers.lifePath)}
      {renderNumberSection('Expression', '✨', chart.coreNumbers.expression)}
      {renderNumberSection('Soul Urge', '💖', chart.coreNumbers.soulUrge)}
      {renderNumberSection('Personality', '👤', chart.coreNumbers.personality)}
      {renderNumberSection('Birth Day', '🎂', chart.coreNumbers.birthDay)}
      {renderNumberSection('Maturity', '🌟', chart.coreNumbers.maturity)}

      <View style={styles.summaryBox}>
        <Text style={styles.summaryTitle}>Chart Summary</Text>
        <Text style={styles.summaryText}>{chart.summary}</Text>
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F9FAFB',
  },
  header: {
    padding: 24,
    paddingTop: 60,
    backgroundColor: '#FFFFFF',
    borderBottomWidth: 1,
    borderBottomColor: '#E5E7EB',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#111827',
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 18,
    color: '#6B7280',
  },
  birthdate: {
    fontSize: 14,
    color: '#9CA3AF',
    marginTop: 4,
  },
  section: {
    backgroundColor: '#FFFFFF',
    marginHorizontal: 16,
    marginTop: 16,
    borderRadius: 16,
    padding: 20,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 4,
  },
  sectionHeader: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 16,
  },
  emoji: {
    fontSize: 28,
    marginRight: 12,
  },
  sectionTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#111827',
  },
  numberRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 16,
  },
  numberBadge: {
    width: 64,
    height: 64,
    borderRadius: 32,
    backgroundColor: '#6B46C1',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 16,
  },
  numberText: {
    fontSize: 32,
    fontWeight: 'bold',
    color: '#FFFFFF',
  },
  typeInfo: {
    flex: 1,
  },
  numberTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#111827',
    marginBottom: 4,
  },
  masterTag: {
    backgroundColor: '#FEF3C7',
    borderRadius: 6,
    paddingHorizontal: 8,
    paddingVertical: 4,
    alignSelf: 'flex-start',
  },
  masterTagText: {
    fontSize: 12,
    fontWeight: '600',
    color: '#92400E',
  },
  calculationBox: {
    backgroundColor: '#F9FAFB',
    borderRadius: 8,
    padding: 12,
    marginBottom: 16,
  },
  calculationLabel: {
    fontSize: 12,
    fontWeight: '600',
    color: '#6B7280',
    marginBottom: 4,
    textTransform: 'uppercase',
    letterSpacing: 1,
  },
  calculationText: {
    fontSize: 13,
    color: '#4B5563',
    fontFamily: 'monospace',
  },
  keywordsContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 8,
  },
  keywordBadge: {
    backgroundColor: '#EEF2FF',
    borderRadius: 8,
    paddingHorizontal: 12,
    paddingVertical: 6,
  },
  keywordText: {
    fontSize: 13,
    fontWeight: '600',
    color: '#4338CA',
  },
  summaryBox: {
    backgroundColor: '#FFFFFF',
    marginHorizontal: 16,
    marginTop: 16,
    marginBottom: 24,
    borderRadius: 16,
    padding: 20,
  },
  summaryTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#111827',
    marginBottom: 12,
  },
  summaryText: {
    fontSize: 14,
    color: '#4B5563',
    lineHeight: 22,
  },
});

Step 6: Karmic Analysis Screen

Show karmic debt and lessons:

// screens/KarmicAnalysisScreen.tsx
import React from 'react';
import { ScrollView, View, Text, StyleSheet } from 'react-native';
import { useNumerology } from '../context/NumerologyContext';

export function KarmicAnalysisScreen() {
  const { chart } = useNumerology();

  if (!chart) return null;

  const { karmicDebt, karmicLessons } = chart.additionalInsights;

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>Karmic Analysis</Text>
        <Text style={styles.subtitle}>
          Understanding your past life lessons and growth areas
        </Text>
      </View>

      {/* Karmic Debt */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>🔮 Karmic Debt</Text>
        {karmicDebt.hasKarmicDebt ? (
          <>
            <Text style={styles.description}>
              You carry karmic debt from past lives. These challenges are opportunities for
              spiritual growth.
            </Text>
            {karmicDebt.meanings.map((debt) => (
              <View key={debt.number} style={styles.debtCard}>
                <View style={styles.debtHeader}>
                  <View style={styles.debtBadge}>
                    <Text style={styles.debtNumber}>{debt.number}</Text>
                  </View>
                  <Text style={styles.debtTitle}>{debt.lesson}</Text>
                </View>
                <Text style={styles.debtDescription}>{debt.description}</Text>
              </View>
            ))}
          </>
        ) : (
          <View style={styles.noDebtCard}>
            <Text style={styles.noDebtText}>
              ✨ You have no karmic debt numbers. Your slate is clean for this lifetime.
            </Text>
          </View>
        )}
      </View>

      {/* Karmic Lessons */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>📚 Karmic Lessons</Text>
        <Text style={styles.description}>
          Numbers missing from your name reveal areas where you need to develop skills and
          awareness in this lifetime.
        </Text>

        {karmicLessons.missingNumbers.length > 0 ? (
          <>
            <View style={styles.missingNumbersRow}>
              {karmicLessons.missingNumbers.map((num) => (
                <View key={num} style={styles.missingNumberBadge}>
                  <Text style={styles.missingNumberText}>{num}</Text>
                </View>
              ))}
            </View>

            {karmicLessons.lessons.map((lesson) => (
              <View key={lesson.number} style={styles.lessonCard}>
                <View style={styles.lessonHeader}>
                  <View style={styles.lessonBadge}>
                    <Text style={styles.lessonNumber}>{lesson.number}</Text>
                  </View>
                  <View style={styles.lessonInfo}>
                    <Text style={styles.lessonTitle}>{lesson.lesson}</Text>
                  </View>
                </View>
                <Text style={styles.lessonDescription}>{lesson.description}</Text>
                <View style={styles.overcomeBox}>
                  <Text style={styles.overcomeLabel}>How to Overcome:</Text>
                  <Text style={styles.overcomeText}>{lesson.howToOvercome}</Text>
                </View>
              </View>
            ))}
          </>
        ) : (
          <View style={styles.noLessonsCard}>
            <Text style={styles.noLessonsText}>
              🌟 All numbers are present in your name. You have balanced energy across all
              areas.
            </Text>
          </View>
        )}
      </View>

      {/* Present Numbers */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>📊 Number Distribution</Text>
        <Text style={styles.description}>
          How many times each number appears in your name:
        </Text>
        <View style={styles.distributionGrid}>
          {Object.entries(karmicLessons.presentNumbers).map(([num, count]) => (
            <View key={num} style={styles.distributionItem}>
              <Text style={styles.distributionNumber}>{num}</Text>
              <Text style={styles.distributionCount}>×{count}</Text>
            </View>
          ))}
        </View>
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F9FAFB',
  },
  header: {
    padding: 24,
    paddingTop: 60,
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#111827',
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 16,
    color: '#6B7280',
    lineHeight: 24,
  },
  section: {
    backgroundColor: '#FFFFFF',
    marginHorizontal: 16,
    marginBottom: 16,
    borderRadius: 16,
    padding: 20,
  },
  sectionTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#111827',
    marginBottom: 12,
  },
  description: {
    fontSize: 14,
    color: '#6B7280',
    lineHeight: 22,
    marginBottom: 16,
  },
  debtCard: {
    backgroundColor: '#FEE2E2',
    borderRadius: 12,
    padding: 16,
    marginBottom: 12,
  },
  debtHeader: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 12,
  },
  debtBadge: {
    width: 40,
    height: 40,
    borderRadius: 20,
    backgroundColor: '#DC2626',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 12,
  },
  debtNumber: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#FFFFFF',
  },
  debtTitle: {
    flex: 1,
    fontSize: 16,
    fontWeight: '600',
    color: '#991B1B',
  },
  debtDescription: {
    fontSize: 14,
    color: '#7F1D1D',
    lineHeight: 20,
  },
  noDebtCard: {
    backgroundColor: '#D1FAE5',
    borderRadius: 12,
    padding: 16,
  },
  noDebtText: {
    fontSize: 14,
    color: '#065F46',
    lineHeight: 20,
  },
  missingNumbersRow: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 8,
    marginBottom: 16,
  },
  missingNumberBadge: {
    width: 48,
    height: 48,
    borderRadius: 24,
    backgroundColor: '#FEF3C7',
    alignItems: 'center',
    justifyContent: 'center',
  },
  missingNumberText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#92400E',
  },
  lessonCard: {
    backgroundColor: '#F9FAFB',
    borderRadius: 12,
    padding: 16,
    marginBottom: 12,
  },
  lessonHeader: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 12,
  },
  lessonBadge: {
    width: 40,
    height: 40,
    borderRadius: 20,
    backgroundColor: '#6B46C1',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 12,
  },
  lessonNumber: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#FFFFFF',
  },
  lessonInfo: {
    flex: 1,
  },
  lessonTitle: {
    fontSize: 15,
    fontWeight: '600',
    color: '#111827',
  },
  lessonDescription: {
    fontSize: 14,
    color: '#4B5563',
    lineHeight: 20,
    marginBottom: 12,
  },
  overcomeBox: {
    backgroundColor: '#FFFFFF',
    borderRadius: 8,
    padding: 12,
  },
  overcomeLabel: {
    fontSize: 13,
    fontWeight: '600',
    color: '#6B7280',
    marginBottom: 6,
  },
  overcomeText: {
    fontSize: 13,
    color: '#4B5563',
    lineHeight: 20,
  },
  noLessonsCard: {
    backgroundColor: '#D1FAE5',
    borderRadius: 12,
    padding: 16,
  },
  noLessonsText: {
    fontSize: 14,
    color: '#065F46',
    lineHeight: 20,
  },
  distributionGrid: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 12,
  },
  distributionItem: {
    width: 64,
    alignItems: 'center',
    backgroundColor: '#F3F4F6',
    borderRadius: 8,
    padding: 12,
  },
  distributionNumber: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#111827',
  },
  distributionCount: {
    fontSize: 13,
    color: '#6B7280',
    marginTop: 4,
  },
});

Step 7: Notes Feature

Add journaling capability:

// screens/NotesScreen.tsx
import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  FlatList,
  StyleSheet,
  Modal,
} from 'react-native';
import { useNumerology } from '../context/NumerologyContext';

export function NotesScreen() {
  const { notes, addNote, deleteNote } = useNumerology();
  const [modalVisible, setModalVisible] = useState(false);
  const [newNote, setNewNote] = useState('');
  const [selectedTags, setSelectedTags] = useState<string[]>([]);

  const availableTags = [
    'Reflection',
    'Insight',
    'Goal',
    'Challenge',
    'Growth',
    'Gratitude',
  ];

  const handleAddNote = async () => {
    if (newNote.trim().length === 0) return;

    await addNote({
      content: newNote.trim(),
      tags: selectedTags,
    });

    setNewNote('');
    setSelectedTags([]);
    setModalVisible(false);
  };

  const renderNote = ({ item }: { item: typeof notes[0] }) => (
    <View style={styles.noteCard}>
      <Text style={styles.noteContent}>{item.content}</Text>
      <View style={styles.noteFooter}>
        <View style={styles.tagsContainer}>
          {item.tags.map((tag) => (
            <View key={tag} style={styles.tag}>
              <Text style={styles.tagText}>{tag}</Text>
            </View>
          ))}
        </View>
        <Text style={styles.noteDate}>
          {new Date(item.createdAt).toLocaleDateString()}
        </Text>
      </View>
      <TouchableOpacity
        style={styles.deleteButton}
        onPress={() => deleteNote(item.id)}
      >
        <Text style={styles.deleteText}>Delete</Text>
      </TouchableOpacity>
    </View>
  );

  return (
    <View style={styles.container}>
      <FlatList
        data={notes}
        renderItem={renderNote}
        keyExtractor={(item) => item.id}
        contentContainerStyle={styles.list}
        ListEmptyComponent={
          <View style={styles.emptyContainer}>
            <Text style={styles.emptyText}>No notes yet</Text>
            <Text style={styles.emptySubtext}>
              Record your numerology insights and reflections
            </Text>
          </View>
        }
      />

      <TouchableOpacity
        style={styles.fab}
        onPress={() => setModalVisible(true)}
      >
        <Text style={styles.fabIcon}>+</Text>
      </TouchableOpacity>

      <Modal
        visible={modalVisible}
        animationType="slide"
        onRequestClose={() => setModalVisible(false)}
      >
        <View style={styles.modalContainer}>
          <View style={styles.modalHeader}>
            <Text style={styles.modalTitle}>New Note</Text>
            <TouchableOpacity onPress={() => setModalVisible(false)}>
              <Text style={styles.closeButton}>Cancel</Text>
            </TouchableOpacity>
          </View>

          <TextInput
            style={styles.noteInput}
            placeholder="Write your reflection..."
            multiline
            value={newNote}
            onChangeText={setNewNote}
            autoFocus
          />

          <Text style={styles.tagsLabel}>Tags (optional)</Text>
          <View style={styles.tagsSelector}>
            {availableTags.map((tag) => (
              <TouchableOpacity
                key={tag}
                style={[
                  styles.tagSelector,
                  selectedTags.includes(tag) && styles.tagSelectorActive,
                ]}
                onPress={() => {
                  setSelectedTags((prev) =>
                    prev.includes(tag)
                      ? prev.filter((t) => t !== tag)
                      : [...prev, tag]
                  );
                }}
              >
                <Text
                  style={[
                    styles.tagSelectorText,
                    selectedTags.includes(tag) && styles.tagSelectorTextActive,
                  ]}
                >
                  {tag}
                </Text>
              </TouchableOpacity>
            ))}
          </View>

          <TouchableOpacity style={styles.saveButton} onPress={handleAddNote}>
            <Text style={styles.saveButtonText}>Save Note</Text>
          </TouchableOpacity>
        </View>
      </Modal>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F9FAFB',
  },
  list: {
    padding: 16,
  },
  noteCard: {
    backgroundColor: '#FFFFFF',
    borderRadius: 16,
    padding: 20,
    marginBottom: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 4,
  },
  noteContent: {
    fontSize: 15,
    color: '#111827',
    lineHeight: 24,
    marginBottom: 12,
  },
  noteFooter: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  tagsContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 6,
  },
  tag: {
    backgroundColor: '#EEF2FF',
    borderRadius: 6,
    paddingHorizontal: 8,
    paddingVertical: 4,
  },
  tagText: {
    fontSize: 11,
    fontWeight: '600',
    color: '#4338CA',
  },
  noteDate: {
    fontSize: 12,
    color: '#9CA3AF',
  },
  deleteButton: {
    marginTop: 12,
    alignSelf: 'flex-end',
  },
  deleteText: {
    fontSize: 13,
    color: '#EF4444',
    fontWeight: '600',
  },
  emptyContainer: {
    padding: 40,
    alignItems: 'center',
  },
  emptyText: {
    fontSize: 18,
    fontWeight: '600',
    color: '#6B7280',
    marginBottom: 8,
  },
  emptySubtext: {
    fontSize: 14,
    color: '#9CA3AF',
    textAlign: 'center',
  },
  fab: {
    position: 'absolute',
    right: 20,
    bottom: 20,
    width: 60,
    height: 60,
    borderRadius: 30,
    backgroundColor: '#6B46C1',
    alignItems: 'center',
    justifyContent: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 8,
    elevation: 8,
  },
  fabIcon: {
    fontSize: 32,
    color: '#FFFFFF',
    fontWeight: 'bold',
  },
  modalContainer: {
    flex: 1,
    backgroundColor: '#FFFFFF',
    padding: 20,
  },
  modalHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 40,
    marginBottom: 24,
  },
  modalTitle: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#111827',
  },
  closeButton: {
    fontSize: 16,
    color: '#6B46C1',
    fontWeight: '600',
  },
  noteInput: {
    borderWidth: 1,
    borderColor: '#E5E7EB',
    borderRadius: 12,
    padding: 16,
    fontSize: 16,
    color: '#111827',
    minHeight: 200,
    textAlignVertical: 'top',
    marginBottom: 24,
  },
  tagsLabel: {
    fontSize: 16,
    fontWeight: '600',
    color: '#111827',
    marginBottom: 12,
  },
  tagsSelector: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 10,
    marginBottom: 24,
  },
  tagSelector: {
    borderWidth: 2,
    borderColor: '#E5E7EB',
    borderRadius: 8,
    paddingHorizontal: 16,
    paddingVertical: 8,
  },
  tagSelectorActive: {
    borderColor: '#6B46C1',
    backgroundColor: '#EEF2FF',
  },
  tagSelectorText: {
    fontSize: 14,
    color: '#6B7280',
    fontWeight: '600',
  },
  tagSelectorTextActive: {
    color: '#6B46C1',
  },
  saveButton: {
    backgroundColor: '#6B46C1',
    borderRadius: 12,
    padding: 18,
    alignItems: 'center',
  },
  saveButtonText: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: '600',
  },
});

Production Checklist

  • Implement proper error boundaries
  • Add offline mode with queued API calls
  • Set up push notifications for Personal Year changes
  • Implement analytics tracking
  • Add export to PDF feature
  • Create onboarding tutorial
  • Add accessibility labels
  • Implement dark mode
  • Add share functionality
  • Create settings screen with data deletion
  • Test on iOS and Android
  • Optimize image assets
  • Add crash reporting (Sentry)
  • Implement deep linking

Conclusion

You now have a complete numerology dashboard app with user profiles, comprehensive charts, karmic analysis, forecasts, and note-taking. This production-ready foundation can be extended with premium features like PDF reports, compatibility matching, and personalized daily insights.

The key to success is balancing comprehensive data with intuitive UI, caching aggressively to minimize API costs, and creating features that encourage daily engagement.

Start building: Sign up for RoxyAPI and get 50 free requests to test your app. Full documentation at roxyapi.com/docs.