Building a Personalized Numerology Report Generator Using REST API
Create a complete numerology report generator with React Native and RoxyAPI. Learn to build comprehensive birth chart profiles with PDF export, sharing features, and offline storage for personalized user experiences.
Building a Personalized Numerology Report Generator Using REST API
A comprehensive numerology report is one of the most engaging features you can add to a wellness, spiritual, or personal development app. Users love receiving detailed insights about their personality, life purpose, relationships, and future predictions—all based on their birth date and name. In this tutorial, you will build a production-ready numerology report generator using React Native and RoxyAPI that creates beautiful, shareable PDF reports.
What You Will Build
By the end of this tutorial, you will have a complete numerology report generator featuring:
- Data collection form for name and birth date with validation
- Complete numerology chart with all core numbers (Life Path, Expression, Soul Urge, Personality, Birth Day, Maturity)
- Karmic analysis detecting debt numbers and missing lessons
- Personal year forecast for current year guidance
- Beautiful report display with organized sections and visual hierarchy
- PDF export for printing and sharing
- Local storage for accessing past reports offline
- Share functionality for social media and messaging apps
Understanding Complete Numerology Charts
A complete numerology chart provides a 360-degree view of someone's personality and life path. Here is what each number reveals:
Life Path Number: Your life purpose and destiny, calculated from birth date. This is the most important number and never changes throughout your life.
Expression Number: Natural talents and abilities, calculated from full birth name. Shows what you are naturally good at and your life goals.
Soul Urge Number: Inner desires and motivations, calculated from vowels in your name. Reveals what truly drives you at a soul level.
Personality Number: How others perceive you, calculated from consonants in your name. Your outer persona and first impressions.
Birth Day Number: Special talents from your birth day (1-31). Quick insights into unique abilities.
Maturity Number: Who you become later in life, combination of Life Path and Expression. Emerges around age 35-40.
Karmic Debt: Challenges from past lives (numbers 13, 14, 16, 19). Lessons you need to learn in this lifetime.
Karmic Lessons: Missing numbers in your name, indicating underdeveloped traits and growth opportunities.
Personal Year: Current year energy and themes (1-9 cycle). Changes annually and guides yearly planning.
Setting Up the Project
Create a new React Native project with TypeScript:
npx react-native init NumerologyReports --template react-native-template-typescript
cd NumerologyReports
Install required dependencies:
npm install axios @react-native-async-storage/async-storage
npm install react-native-pdf react-native-fs react-native-share
npm install react-native-date-picker
For iOS, install pods:
cd ios && pod install && cd ..
Building the Input Form
Create a comprehensive form that collects all necessary information:
// components/NumerologyForm.tsx
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
StyleSheet,
Platform,
} from 'react-native';
import DatePicker from 'react-native-date-picker';
interface FormData {
firstName: string;
middleName: string;
lastName: string;
birthDate: Date;
}
interface NumerologyFormProps {
onSubmit: (data: FormData) => void;
loading: boolean;
}
const NumerologyForm: React.FC<NumerologyFormProps> = ({ onSubmit, loading }) => {
const [firstName, setFirstName] = useState('');
const [middleName, setMiddleName] = useState('');
const [lastName, setLastName] = useState('');
const [birthDate, setBirthDate] = useState(new Date());
const [showDatePicker, setShowDatePicker] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const validate = (): boolean => {
const newErrors: Record<string, string> = {};
if (!firstName.trim()) {
newErrors.firstName = 'First name is required';
} else if (!/^[a-zA-Z\s]+$/.test(firstName)) {
newErrors.firstName = 'Name can only contain letters and spaces';
}
if (!lastName.trim()) {
newErrors.lastName = 'Last name is required';
} else if (!/^[a-zA-Z\s]+$/.test(lastName)) {
newErrors.lastName = 'Name can only contain letters and spaces';
}
if (middleName && !/^[a-zA-Z\s]+$/.test(middleName)) {
newErrors.middleName = 'Name can only contain letters and spaces';
}
const today = new Date();
const age = today.getFullYear() - birthDate.getFullYear();
if (age < 0 || age > 120) {
newErrors.birthDate = 'Please enter a valid birth date';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = () => {
if (validate()) {
const fullName = [firstName, middleName, lastName]
.filter(Boolean)
.join(' ');
onSubmit({
firstName: firstName.trim(),
middleName: middleName.trim(),
lastName: lastName.trim(),
birthDate,
});
}
};
const formatDate = (date: Date): string => {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
return (
<ScrollView style={styles.container} keyboardShouldPersistTaps="handled">
<View style={styles.header}>
<Text style={styles.title}>Generate Your Numerology Report</Text>
<Text style={styles.subtitle}>
Enter your birth name and date to receive a comprehensive numerology analysis
</Text>
</View>
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>
First Name <Text style={styles.required}>*</Text>
</Text>
<TextInput
style={[styles.input, errors.firstName && styles.inputError]}
value={firstName}
onChangeText={setFirstName}
placeholder="John"
autoCapitalize="words"
autoCorrect={false}
/>
{errors.firstName && (
<Text style={styles.errorText}>{errors.firstName}</Text>
)}
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Middle Name</Text>
<TextInput
style={[styles.input, errors.middleName && styles.inputError]}
value={middleName}
onChangeText={setMiddleName}
placeholder="William (optional)"
autoCapitalize="words"
autoCorrect={false}
/>
{errors.middleName && (
<Text style={styles.errorText}>{errors.middleName}</Text>
)}
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>
Last Name <Text style={styles.required}>*</Text>
</Text>
<TextInput
style={[styles.input, errors.lastName && styles.inputError]}
value={lastName}
onChangeText={setLastName}
placeholder="Smith"
autoCapitalize="words"
autoCorrect={false}
/>
{errors.lastName && (
<Text style={styles.errorText}>{errors.lastName}</Text>
)}
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>
Birth Date <Text style={styles.required}>*</Text>
</Text>
<TouchableOpacity
style={[styles.dateButton, errors.birthDate && styles.inputError]}
onPress={() => setShowDatePicker(true)}
>
<Text style={styles.dateButtonText}>{formatDate(birthDate)}</Text>
</TouchableOpacity>
{errors.birthDate && (
<Text style={styles.errorText}>{errors.birthDate}</Text>
)}
</View>
<DatePicker
modal
open={showDatePicker}
date={birthDate}
mode="date"
maximumDate={new Date()}
minimumDate={new Date(1900, 0, 1)}
onConfirm={(date) => {
setShowDatePicker(false);
setBirthDate(date);
}}
onCancel={() => setShowDatePicker(false)}
/>
<View style={styles.infoBox}>
<Text style={styles.infoText}>
💡 Use your full birth name as it appears on your birth certificate for most
accurate results. Middle names are optional but recommended.
</Text>
</View>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleSubmit}
disabled={loading}
>
<Text style={styles.buttonText}>
{loading ? 'Generating Report...' : 'Generate My Report'}
</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
};
export default NumerologyForm;
Integrating the Full Chart API
Create a service that fetches the complete numerology chart:
// services/numerology.service.ts
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
const API_BASE_URL = 'https://roxyapi.com/api/v2/numerology';
const API_KEY = 'your_api_key_here'; // Replace with your actual API key
const CACHE_KEY = 'numerology_reports';
interface FullChartParams {
fullName: string;
year: number;
month: number;
day: number;
currentYear?: number;
}
export class NumerologyService {
private static client = axios.create({
baseURL: API_BASE_URL,
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
timeout: 15000,
});
static async generateFullChart(params: FullChartParams) {
try {
const response = await this.client.post('/chart', {
fullName: params.fullName,
year: params.year,
month: params.month,
day: params.day,
currentYear: params.currentYear || new Date().getFullYear(),
});
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const message = error.response?.data?.error || 'Failed to generate report';
throw new Error(message);
}
throw error;
}
}
static async saveReport(report: any): Promise<void> {
try {
const existing = await this.getAllReports();
const reports = [
{
...report,
id: Date.now().toString(),
createdAt: new Date().toISOString(),
},
...existing,
];
await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(reports));
} catch (error) {
console.error('Failed to save report:', error);
}
}
static async getAllReports(): Promise<any[]> {
try {
const data = await AsyncStorage.getItem(CACHE_KEY);
return data ? JSON.parse(data) : [];
} catch (error) {
console.error('Failed to load reports:', error);
return [];
}
}
static async deleteReport(id: string): Promise<void> {
try {
const reports = await this.getAllReports();
const filtered = reports.filter((r) => r.id !== id);
await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(filtered));
} catch (error) {
console.error('Failed to delete report:', error);
}
}
}
Displaying the Complete Report
Create a beautiful report display component:
// components/NumerologyReport.tsx
import React from 'react';
import { View, Text, ScrollView, StyleSheet } from 'react-native';
interface ReportProps {
data: any; // Full chart response from API
}
const NumerologyReport: React.FC<ReportProps> = ({ data }) => {
const { profile, coreNumbers, additionalInsights } = data;
return (
<ScrollView style={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.name}>{profile.name}</Text>
<Text style={styles.birthdate}>{profile.birthdate}</Text>
</View>
{/* Summary */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Your Numerology Summary</Text>
<Text style={styles.summary}>{data.summary}</Text>
</View>
{/* Core Numbers */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Core Numbers</Text>
<NumberCard
label="Life Path"
number={coreNumbers.lifePath.number}
type={coreNumbers.lifePath.type}
title={coreNumbers.lifePath.meaning.title}
description={coreNumbers.lifePath.meaning.description}
keywords={coreNumbers.lifePath.meaning.keywords}
/>
<NumberCard
label="Expression"
number={coreNumbers.expression.number}
type={coreNumbers.expression.type}
title={coreNumbers.expression.meaning.title}
description={coreNumbers.expression.meaning.description}
keywords={coreNumbers.expression.meaning.keywords}
/>
<NumberCard
label="Soul Urge"
number={coreNumbers.soulUrge.number}
type={coreNumbers.soulUrge.type}
title={coreNumbers.soulUrge.meaning.title}
description={coreNumbers.soulUrge.meaning.description}
keywords={coreNumbers.soulUrge.meaning.keywords}
/>
<NumberCard
label="Personality"
number={coreNumbers.personality.number}
type={coreNumbers.personality.type}
title={coreNumbers.personality.meaning.title}
description={coreNumbers.personality.meaning.description}
keywords={coreNumbers.personality.meaning.keywords}
/>
<NumberCard
label="Birth Day"
number={coreNumbers.birthDay.number}
type="single"
title={coreNumbers.birthDay.meaning.title}
description={coreNumbers.birthDay.meaning.description}
keywords={coreNumbers.birthDay.meaning.keywords}
/>
<NumberCard
label="Maturity"
number={coreNumbers.maturity.number}
type={coreNumbers.maturity.type}
title={coreNumbers.maturity.meaning.title}
description={coreNumbers.maturity.meaning.description}
keywords={coreNumbers.maturity.meaning.keywords}
/>
</View>
{/* Strengths and Challenges */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Your Strengths</Text>
{coreNumbers.lifePath.meaning.strengths.map((strength: string, index: number) => (
<View key={index} style={styles.listItem}>
<Text style={styles.bullet}>✓</Text>
<Text style={styles.listText}>{strength}</Text>
</View>
))}
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Growth Opportunities</Text>
{coreNumbers.lifePath.meaning.challenges.map((challenge: string, index: number) => (
<View key={index} style={styles.listItem}>
<Text style={styles.bullet}>•</Text>
<Text style={styles.listText}>{challenge}</Text>
</View>
))}
</View>
{/* Career Guidance */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Career Guidance</Text>
<Text style={styles.text}>{coreNumbers.lifePath.meaning.career}</Text>
</View>
{/* Relationships */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Relationship Insights</Text>
<Text style={styles.text}>{coreNumbers.lifePath.meaning.relationships}</Text>
</View>
{/* Spiritual Path */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Spiritual Journey</Text>
<Text style={styles.text}>{coreNumbers.lifePath.meaning.spirituality}</Text>
</View>
{/* Personal Year */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>
{new Date().getFullYear()} Personal Year Forecast
</Text>
<View style={styles.yearCard}>
<Text style={styles.yearNumber}>
Personal Year {additionalInsights.personalYear.personalYear}
</Text>
<Text style={styles.yearTheme}>
{additionalInsights.personalYear.theme}
</Text>
<Text style={styles.yearForecast}>
{additionalInsights.personalYear.forecast}
</Text>
<Text style={styles.yearSubtitle}>Opportunities This Year</Text>
{additionalInsights.personalYear.opportunities.map((opp: string, index: number) => (
<Text key={index} style={styles.yearListItem}>
• {opp}
</Text>
))}
<Text style={styles.yearSubtitle}>Challenges to Navigate</Text>
{additionalInsights.personalYear.challenges.map((challenge: string, index: number) => (
<Text key={index} style={styles.yearListItem}>
• {challenge}
</Text>
))}
<Text style={styles.yearAdvice}>{additionalInsights.personalYear.advice}</Text>
</View>
</View>
{/* Karmic Lessons */}
{additionalInsights.karmicLessons.missingNumbers.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Karmic Lessons</Text>
<Text style={styles.text}>
Your name is missing the following numbers, indicating areas for growth:
</Text>
{additionalInsights.karmicLessons.lessons.map((lesson: any, index: number) => (
<View key={index} style={styles.karmicCard}>
<Text style={styles.karmicNumber}>Missing Number {lesson.number}</Text>
<Text style={styles.karmicLesson}>{lesson.lesson}</Text>
<Text style={styles.karmicDescription}>{lesson.description}</Text>
<Text style={styles.karmicOvercome}>
How to Overcome: {lesson.howToOvercome}
</Text>
</View>
))}
</View>
)}
{/* Karmic Debt */}
{additionalInsights.karmicDebt.hasKarmicDebt && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Karmic Debt Numbers</Text>
{additionalInsights.karmicDebt.meanings.map((debt: any, index: number) => (
<View key={index} style={styles.debtCard}>
<Text style={styles.debtNumber}>Karmic Debt {debt.number}</Text>
<Text style={styles.debtDescription}>{debt.description}</Text>
<Text style={styles.debtOvercome}>{debt.howToOvercome}</Text>
</View>
))}
</View>
)}
</ScrollView>
);
};
const NumberCard: React.FC<{
label: string;
number: number;
type: string;
title: string;
description: string;
keywords: string[];
}> = ({ label, number, type, title, description, keywords }) => (
<View style={styles.numberCard}>
<View style={styles.numberHeader}>
<View style={styles.numberBadge}>
<Text style={styles.numberText}>{number}</Text>
</View>
<View style={styles.numberInfo}>
<Text style={styles.numberLabel}>{label} Number</Text>
<Text style={styles.numberTitle}>{title}</Text>
{type === 'master' && (
<View style={styles.masterBadge}>
<Text style={styles.masterText}>Master Number</Text>
</View>
)}
</View>
</View>
<View style={styles.keywordContainer}>
{keywords.slice(0, 5).map((keyword, index) => (
<View key={index} style={styles.keywordPill}>
<Text style={styles.keywordText}>{keyword}</Text>
</View>
))}
</View>
<Text style={styles.numberDescription} numberOfLines={4}>
{description}
</Text>
</View>
);
export default NumerologyReport;
Implementing PDF Export
Add PDF generation functionality using react-native-html-to-pdf:
npm install react-native-html-to-pdf
Create a PDF generator service:
// services/pdf.service.ts
import RNHTMLtoPDF from 'react-native-html-to-pdf';
import Share from 'react-native-share';
export class PDFService {
static async generateReport(data: any): Promise<string> {
const html = this.generateHTML(data);
const options = {
html,
fileName: `Numerology_Report_${data.profile.name.replace(/\s+/g, '_')}_${Date.now()}`,
directory: 'Documents',
};
try {
const file = await RNHTMLtoPDF.convert(options);
return file.filePath!;
} catch (error) {
console.error('PDF generation failed:', error);
throw new Error('Failed to generate PDF report');
}
}
static async shareReport(filePath: string): Promise<void> {
try {
await Share.open({
url: `file://${filePath}`,
type: 'application/pdf',
title: 'Share Numerology Report',
});
} catch (error) {
console.log('Share cancelled or failed:', error);
}
}
private static generateHTML(data: any): string {
const { profile, coreNumbers, additionalInsights } = data;
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
line-height: 1.6;
color: #333;
padding: 40px;
background: white;
}
.header {
text-align: center;
margin-bottom: 40px;
border-bottom: 3px solid #007AFF;
padding-bottom: 20px;
}
.header h1 {
font-size: 32px;
color: #007AFF;
margin-bottom: 8px;
}
.header p {
font-size: 16px;
color: #666;
}
.section {
margin-bottom: 32px;
page-break-inside: avoid;
}
.section-title {
font-size: 24px;
font-weight: bold;
color: #007AFF;
margin-bottom: 16px;
}
.number-card {
background: #f8f9fa;
border-left: 4px solid #007AFF;
padding: 20px;
margin-bottom: 20px;
page-break-inside: avoid;
}
.number-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.number-badge {
width: 60px;
height: 60px;
background: #007AFF;
border-radius: 30px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
}
.number-badge span {
color: white;
font-size: 28px;
font-weight: bold;
}
.number-label {
font-size: 14px;
color: #666;
text-transform: uppercase;
letter-spacing: 1px;
}
.number-title {
font-size: 20px;
font-weight: bold;
color: #1a1a1a;
}
.master-badge {
display: inline-block;
background: #FFD700;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
margin-top: 4px;
}
.keywords {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 12px 0;
}
.keyword {
background: white;
padding: 6px 12px;
border-radius: 16px;
font-size: 13px;
color: #007AFF;
border: 1px solid #007AFF;
}
.description {
font-size: 14px;
color: #333;
line-height: 1.8;
}
.list-item {
display: flex;
margin-bottom: 8px;
}
.list-item span:first-child {
color: #007AFF;
margin-right: 8px;
font-weight: bold;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 2px solid #e0e0e0;
text-align: center;
color: #666;
font-size: 12px;
}
</style>
</head>
<body>
<div class="header">
<h1>Numerology Report</h1>
<p><strong>${profile.name}</strong> • Born ${profile.birthdate}</p>
<p>Generated on ${new Date().toLocaleDateString()}</p>
</div>
<div class="section">
<h2 class="section-title">Summary</h2>
<p class="description">${data.summary}</p>
</div>
<div class="section">
<h2 class="section-title">Core Numbers</h2>
${this.renderNumberCard('Life Path', coreNumbers.lifePath)}
${this.renderNumberCard('Expression', coreNumbers.expression)}
${this.renderNumberCard('Soul Urge', coreNumbers.soulUrge)}
${this.renderNumberCard('Personality', coreNumbers.personality)}
${this.renderNumberCard('Birth Day', coreNumbers.birthDay)}
${this.renderNumberCard('Maturity', coreNumbers.maturity)}
</div>
<div class="section">
<h2 class="section-title">${new Date().getFullYear()} Personal Year Forecast</h2>
<div class="number-card">
<h3>Personal Year ${additionalInsights.personalYear.personalYear}: ${additionalInsights.personalYear.theme}</h3>
<p class="description">${additionalInsights.personalYear.forecast}</p>
<h4 style="margin-top: 16px;">Opportunities:</h4>
${additionalInsights.personalYear.opportunities.map((opp: string) => `
<div class="list-item"><span>•</span><span>${opp}</span></div>
`).join('')}
<h4 style="margin-top: 16px;">Challenges:</h4>
${additionalInsights.personalYear.challenges.map((challenge: string) => `
<div class="list-item"><span>•</span><span>${challenge}</span></div>
`).join('')}
</div>
</div>
${additionalInsights.karmicLessons.missingNumbers.length > 0 ? `
<div class="section">
<h2 class="section-title">Karmic Lessons</h2>
${additionalInsights.karmicLessons.lessons.map((lesson: any) => `
<div class="number-card">
<h3>Missing Number ${lesson.number}: ${lesson.lesson}</h3>
<p class="description">${lesson.description}</p>
<p class="description" style="margin-top: 12px;"><strong>How to Overcome:</strong> ${lesson.howToOvercome}</p>
</div>
`).join('')}
</div>
` : ''}
${additionalInsights.karmicDebt.hasKarmicDebt ? `
<div class="section">
<h2 class="section-title">Karmic Debt</h2>
${additionalInsights.karmicDebt.meanings.map((debt: any) => `
<div class="number-card">
<h3>Karmic Debt ${debt.number}</h3>
<p class="description">${debt.description}</p>
<p class="description" style="margin-top: 12px;">${debt.howToOvercome}</p>
</div>
`).join('')}
</div>
` : ''}
<div class="footer">
<p>Generated with RoxyAPI Numerology</p>
<p>https://roxyapi.com/products/numerology-api</p>
</div>
</body>
</html>
`;
}
private static renderNumberCard(label: string, data: any): string {
const isMaster = data.type === 'master';
return `
<div class="number-card">
<div class="number-header">
<div class="number-badge"><span>${data.number}</span></div>
<div>
<div class="number-label">${label} Number</div>
<div class="number-title">${data.meaning.title}</div>
${isMaster ? '<div class="master-badge">Master Number</div>' : ''}
</div>
</div>
<div class="keywords">
${data.meaning.keywords.slice(0, 5).map((kw: string) => `<span class="keyword">${kw}</span>`).join('')}
</div>
<p class="description">${data.meaning.description.substring(0, 300)}...</p>
</div>
`;
}
}
Wiring Everything Together
Create the main screen that orchestrates the flow:
// screens/ReportGenerator.tsx
import React, { useState } from 'react';
import { View, Alert, ActivityIndicator, StyleSheet } from 'react-native';
import NumerologyForm from '../components/NumerologyForm';
import NumerologyReport from '../components/NumerologyReport';
import { NumerologyService } from '../services/numerology.service';
import { PDFService } from '../services/pdf.service';
const ReportGenerator: React.FC = () => {
const [loading, setLoading] = useState(false);
const [report, setReport] = useState<any>(null);
const handleGenerateReport = async (formData: any) => {
setLoading(true);
try {
const fullName = [formData.firstName, formData.middleName, formData.lastName]
.filter(Boolean)
.join(' ');
const year = formData.birthDate.getFullYear();
const month = formData.birthDate.getMonth() + 1;
const day = formData.birthDate.getDate();
const data = await NumerologyService.generateFullChart({
fullName,
year,
month,
day,
});
setReport(data);
await NumerologyService.saveReport(data);
} catch (error) {
Alert.alert('Error', error.message || 'Failed to generate report');
} finally {
setLoading(false);
}
};
const handleExportPDF = async () => {
if (!report) return;
try {
const filePath = await PDFService.generateReport(report);
Alert.alert(
'PDF Generated',
'Would you like to share your numerology report?',
[
{ text: 'Not Now', style: 'cancel' },
{
text: 'Share',
onPress: () => PDFService.shareReport(filePath),
},
]
);
} catch (error) {
Alert.alert('Error', 'Failed to generate PDF');
}
};
const handleReset = () => {
setReport(null);
};
if (loading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
</View>
);
}
if (report) {
return (
<View style={styles.container}>
<NumerologyReport data={report} />
<View style={styles.actions}>
<TouchableOpacity style={styles.actionButton} onPress={handleExportPDF}>
<Text style={styles.actionButtonText}>Export PDF</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.secondaryButton]}
onPress={handleReset}
>
<Text style={[styles.actionButtonText, styles.secondaryButtonText]}>
New Report
</Text>
</TouchableOpacity>
</View>
</View>
);
}
return <NumerologyForm onSubmit={handleGenerateReport} loading={loading} />;
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
actions: {
flexDirection: 'row',
padding: 16,
gap: 12,
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
backgroundColor: '#fff',
},
actionButton: {
flex: 1,
backgroundColor: '#007AFF',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
actionButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
secondaryButton: {
backgroundColor: '#f0f0f0',
},
secondaryButtonText: {
color: '#007AFF',
},
});
export default ReportGenerator;
Adding Report History
Create a screen to browse past reports:
// screens/ReportHistory.tsx
import React, { useState, useEffect } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
Alert,
} from 'react-native';
import { NumerologyService } from '../services/numerology.service';
const ReportHistory: React.FC<{ navigation: any }> = ({ navigation }) => {
const [reports, setReports] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadReports();
}, []);
const loadReports = async () => {
setLoading(true);
try {
const data = await NumerologyService.getAllReports();
setReports(data);
} catch (error) {
Alert.alert('Error', 'Failed to load reports');
} finally {
setLoading(false);
}
};
const handleDelete = (id: string) => {
Alert.alert(
'Delete Report',
'Are you sure you want to delete this report?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: async () => {
await NumerologyService.deleteReport(id);
loadReports();
},
},
]
);
};
const renderReport = ({ item }: { item: any }) => (
<TouchableOpacity
style={styles.reportCard}
onPress={() => navigation.navigate('ReportDetail', { report: item })}
>
<View style={styles.reportHeader}>
<Text style={styles.reportName}>{item.profile.name}</Text>
<TouchableOpacity onPress={() => handleDelete(item.id)}>
<Text style={styles.deleteButton}>Delete</Text>
</TouchableOpacity>
</View>
<Text style={styles.reportDate}>{item.profile.birthdate}</Text>
<Text style={styles.reportCreated}>
Generated {new Date(item.createdAt).toLocaleDateString()}
</Text>
<View style={styles.reportNumbers}>
<Text style={styles.reportNumber}>
Life Path: {item.coreNumbers.lifePath.number}
</Text>
<Text style={styles.reportNumber}>
Expression: {item.coreNumbers.expression.number}
</Text>
</View>
</TouchableOpacity>
);
return (
<View style={styles.container}>
<FlatList
data={reports}
renderItem={renderReport}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
ListEmptyComponent={
<View style={styles.empty}>
<Text style={styles.emptyText}>No saved reports yet</Text>
</View>
}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
list: {
padding: 16,
},
reportCard: {
backgroundColor: '#fff',
padding: 16,
borderRadius: 12,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
reportHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
reportName: {
fontSize: 18,
fontWeight: 'bold',
color: '#1a1a1a',
},
deleteButton: {
color: '#ff4444',
fontSize: 14,
fontWeight: '600',
},
reportDate: {
fontSize: 14,
color: '#666',
marginBottom: 4,
},
reportCreated: {
fontSize: 12,
color: '#999',
marginBottom: 12,
},
reportNumbers: {
flexDirection: 'row',
gap: 16,
},
reportNumber: {
fontSize: 13,
color: '#007AFF',
fontWeight: '500',
},
empty: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
emptyText: {
fontSize: 16,
color: '#999',
},
});
export default ReportHistory;
Styling the Complete App
Add comprehensive styles for a professional appearance:
// styles/report.styles.ts
import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({
// Form styles
container: {
flex: 1,
backgroundColor: '#fff',
},
header: {
padding: 24,
backgroundColor: '#007AFF',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#fff',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#fff',
opacity: 0.9,
lineHeight: 24,
},
form: {
padding: 20,
},
inputGroup: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#333',
marginBottom: 8,
},
required: {
color: '#ff4444',
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#f9f9f9',
},
inputError: {
borderColor: '#ff4444',
},
errorText: {
color: '#ff4444',
fontSize: 12,
marginTop: 4,
},
dateButton: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
backgroundColor: '#f9f9f9',
},
dateButtonText: {
fontSize: 16,
color: '#333',
},
infoBox: {
backgroundColor: '#e3f2fd',
padding: 12,
borderRadius: 8,
marginBottom: 20,
},
infoText: {
fontSize: 13,
color: '#1976d2',
lineHeight: 20,
},
button: {
backgroundColor: '#007AFF',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
// Report styles
name: {
fontSize: 28,
fontWeight: 'bold',
color: '#1a1a1a',
textAlign: 'center',
},
birthdate: {
fontSize: 16,
color: '#666',
textAlign: 'center',
marginTop: 4,
},
section: {
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
sectionTitle: {
fontSize: 22,
fontWeight: 'bold',
color: '#007AFF',
marginBottom: 12,
},
summary: {
fontSize: 16,
color: '#333',
lineHeight: 24,
},
numberCard: {
backgroundColor: '#f8f9fa',
padding: 16,
borderRadius: 12,
marginBottom: 16,
},
numberHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
numberBadge: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#007AFF',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
numberText: {
color: '#fff',
fontSize: 28,
fontWeight: 'bold',
},
numberInfo: {
flex: 1,
},
numberLabel: {
fontSize: 12,
color: '#666',
textTransform: 'uppercase',
letterSpacing: 1,
},
numberTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1a1a1a',
marginTop: 2,
},
masterBadge: {
backgroundColor: '#FFD700',
alignSelf: 'flex-start',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 8,
marginTop: 4,
},
masterText: {
fontSize: 11,
fontWeight: 'bold',
color: '#000',
},
keywordContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 12,
},
keywordPill: {
backgroundColor: '#fff',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
borderWidth: 1,
borderColor: '#007AFF',
},
keywordText: {
fontSize: 12,
color: '#007AFF',
},
numberDescription: {
fontSize: 14,
color: '#333',
lineHeight: 22,
},
listItem: {
flexDirection: 'row',
marginBottom: 8,
},
bullet: {
color: '#007AFF',
marginRight: 8,
fontWeight: 'bold',
},
listText: {
flex: 1,
fontSize: 14,
color: '#333',
lineHeight: 22,
},
text: {
fontSize: 14,
color: '#333',
lineHeight: 22,
},
yearCard: {
backgroundColor: '#f8f9fa',
padding: 16,
borderRadius: 12,
},
yearNumber: {
fontSize: 20,
fontWeight: 'bold',
color: '#007AFF',
marginBottom: 4,
},
yearTheme: {
fontSize: 18,
fontWeight: '600',
color: '#1a1a1a',
marginBottom: 12,
},
yearForecast: {
fontSize: 14,
color: '#333',
lineHeight: 22,
marginBottom: 16,
},
yearSubtitle: {
fontSize: 16,
fontWeight: '600',
color: '#007AFF',
marginTop: 16,
marginBottom: 8,
},
yearListItem: {
fontSize: 14,
color: '#333',
lineHeight: 22,
marginBottom: 4,
},
yearAdvice: {
fontSize: 14,
color: '#333',
lineHeight: 22,
marginTop: 16,
fontStyle: 'italic',
},
karmicCard: {
backgroundColor: '#fff3cd',
padding: 16,
borderRadius: 12,
marginBottom: 12,
},
karmicNumber: {
fontSize: 18,
fontWeight: 'bold',
color: '#856404',
marginBottom: 8,
},
karmicLesson: {
fontSize: 16,
fontWeight: '600',
color: '#1a1a1a',
marginBottom: 8,
},
karmicDescription: {
fontSize: 14,
color: '#333',
lineHeight: 22,
marginBottom: 8,
},
karmicOvercome: {
fontSize: 14,
color: '#333',
lineHeight: 22,
fontStyle: 'italic',
},
debtCard: {
backgroundColor: '#f8d7da',
padding: 16,
borderRadius: 12,
marginBottom: 12,
},
debtNumber: {
fontSize: 18,
fontWeight: 'bold',
color: '#721c24',
marginBottom: 8,
},
debtDescription: {
fontSize: 14,
color: '#333',
lineHeight: 22,
marginBottom: 8,
},
debtOvercome: {
fontSize: 14,
color: '#333',
lineHeight: 22,
fontStyle: 'italic',
},
});
Testing the Complete Flow
Test your numerology report generator:
- Form validation: Try submitting with missing fields, invalid names, invalid dates
- API integration: Generate reports for different people, verify all data appears correctly
- Master numbers: Test with birth dates that produce 11, 22, or 33
- Karmic detection: Test names with missing numbers and dates with karmic debt
- PDF export: Generate and share PDFs, verify formatting
- Offline access: Turn off network, load saved reports from storage
- Performance: Test with slow network, verify loading states
Next Steps
You now have a fully functional numerology report generator. Consider enhancing it with:
- Email delivery: Send reports via email using SendGrid or Mailgun
- Payment integration: Charge for premium reports with in-app purchases
- Social sharing: Add formatted share cards for Instagram stories
- Notifications: Remind users about their personal year themes
- Compatibility reports: Generate relationship reports for two people
- Premium interpretations: Offer extended meanings and guidance
The RoxyAPI Numerology API handles all the complex calculations and provides professional interpretations. Focus on building great user experiences that make these insights accessible and meaningful.
For complete API documentation, explore the RoxyAPI Numerology documentation.
Building a numerology app? RoxyAPI provides complete numerology calculations with comprehensive interpretations. Life path, expression, karmic analysis, compatibility, personal year forecasts—everything you need for professional numerology reports. Start generating meaningful insights today.