RoxyAPI

Menu

Building a Tarot Journaling App with React Native + Tarot API

18 min read
By Sarah Martinez
TarotReact NativeDaily ReadingSpiritual Apps

Complete tutorial for building a tarot journal app from scratch - daily readings, history tracking, notes, card search, and share features. Full React Native code with Expo and RoxyAPI integration.

Building a Tarot Journaling App with React Native + Tarot API

Want to build a tarot journaling app that users will love? This complete React Native tutorial walks you through creating a production-ready wrtarot journal mobile app using React Native, Expo, and the RoxyAPI Tarot Card API. We'll implement daily tarot card readings, reading history tracking with personal notes, tarot card database search, spread selection (three-card, Celtic Cross, love, career), and social sharing features - everything you need to launch a professional tarot reading app on iOS and Android.

What we're building - Complete Tarot Reading App Features:

  • ✅ Daily tarot card of the day with push notifications
  • ✅ Tarot reading history with personal journal notes
  • ✅ Complete 78-card tarot database with search and arcana filters
  • ✅ Multiple tarot spread types (three-card Past-Present-Future, Celtic Cross 10-card, Love spread, Career spread)
  • ✅ Share tarot readings via image or text to social media
  • ✅ Dark mode support for better user experience

Tech stack for tarot app development:

  • React Native + Expo (cross-platform iOS and Android deployment)
  • AsyncStorage (local tarot reading data persistence)
  • RoxyAPI Tarot Card API (professional tarot card data and spread interpretations)
  • Expo Notifications (daily tarot card reminders)
  • React Navigation (seamless screen transitions)

Project Setup

1. Create Expo App

npx create-expo-app tarot-journal
cd tarot-journal

2. Install Dependencies

npx expo install @react-navigation/native @react-navigation/bottom-tabs
npx expo install react-native-screens react-native-safe-area-context
npx expo install @react-native-async-storage/async-storage
npx expo install expo-notifications
npx expo install expo-sharing expo-file-system
npx expo install react-native-svg

3. Configure Environment

Create .env file:

EXPO_PUBLIC_ROXYAPI_KEY=your_api_key_here

Get your API key from RoxyAPI Pricing.

Architecture Overview

app/
├── services/
│   ├── tarotAPI.ts          # API wrapper
│   ├── storage.ts           # AsyncStorage helpers
│   └── notifications.ts     # Push notification setup
├── screens/
│   ├── HomeScreen.tsx       # Daily card
│   ├── HistoryScreen.tsx    # Past readings
│   ├── NewReadingScreen.tsx # Create reading
│   ├── ReadingDetailScreen.tsx  # View + edit notes
│   └── CardsScreen.tsx      # Browse all cards
├── components/
│   ├── TarotCard.tsx        # Reusable card UI
│   ├── SpreadLayout.tsx     # Position visualization
│   └── ShareButton.tsx      # Export readings
└── App.tsx                  # Navigation setup

Step 1: API Service Layer

Create services/tarotAPI.ts:

const API_BASE = 'https://roxyapi.com/api/v2/tarot';
const API_KEY = process.env.EXPO_PUBLIC_ROXYAPI_KEY;

export interface TarotCard {
  id: string;
  name: string;
  arcana: 'major' | 'minor';
  suit?: 'cups' | 'wands' | 'swords' | 'pentacles';
  reversed: boolean;
  keywords: string[];
  meaning: string;
  imageUrl: string;
}

export interface SpreadPosition {
  position: number;
  name: string;
  interpretation: string;
  card: TarotCard;
}

export interface Reading {
  spread: string;
  question?: string;
  positions: SpreadPosition[];
  summary?: string;
  seed?: string;
}

class TarotAPI {
  private async request(endpoint: string, body?: any) {
    const response = await fetch(`${API_BASE}${endpoint}`, {
      method: 'POST',
      headers: {
        'X-API-Key': API_KEY!,
        'Content-Type': 'application/json',
      },
      body: body ? JSON.stringify(body) : undefined,
    });

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

    return response.json();
  }

  // Daily card for home screen
  async getDailyCard(userId: string): Promise<{ card: TarotCard; message: string }> {
    const today = new Date().toISOString().split('T')[0];
    const seed = `${userId}-${today}`;
    return this.request('/daily', { seed });
  }

  // Get all cards for browse feature
  async getAllCards(): Promise<TarotCard[]> {
    const response = await fetch(`${API_BASE}/cards`, {
      method: 'GET',
      headers: { 'X-API-Key': API_KEY! },
    });
    return response.json();
  }

  // Three-card spread
  async getThreeCardReading(question: string, seed: string): Promise<Reading> {
    return this.request('/spreads/three-card', { question, seed });
  }

  // Celtic Cross spread (premium feature)
  async getCelticCrossReading(question: string, seed: string): Promise<Reading> {
    return this.request('/spreads/celtic-cross', { question, seed });
  }

  // Love spread
  async getLoveReading(question: string, seed: string): Promise<Reading> {
    return this.request('/spreads/love', { question, seed });
  }

  // Career spread
  async getCareerReading(question: string, seed: string): Promise<Reading> {
    return this.request('/spreads/career', { question, seed });
  }

  // Recreate reading from seed (for history)
  async recreateReading(
    spreadType: string,
    seed: string,
    question?: string
  ): Promise<Reading> {
    const endpoints = {
      'three-card': '/spreads/three-card',
      'celtic-cross': '/spreads/celtic-cross',
      love: '/spreads/love',
      career: '/spreads/career',
    };

    return this.request(endpoints[spreadType], { question, seed });
  }
}

export const tarotAPI = new TarotAPI();

Step 2: Storage Layer

Create services/storage.ts:

import AsyncStorage from '@react-native-async-storage/async-storage';

export interface SavedReading {
  id: string;
  spreadType: string;
  question?: string;
  seed: string;
  createdAt: string;
  notes?: string;
  tags?: string[];
}

class StorageService {
  private READINGS_KEY = '@tarot_readings';
  private USER_ID_KEY = '@tarot_user_id';
  private SETTINGS_KEY = '@tarot_settings';

  // User ID for seeding
  async getUserId(): Promise<string> {
    let userId = await AsyncStorage.getItem(this.USER_ID_KEY);
    if (!userId) {
      userId = `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
      await AsyncStorage.setItem(this.USER_ID_KEY, userId);
    }
    return userId;
  }

  // Save reading metadata (not full data - recreate from seed)
  async saveReading(reading: SavedReading): Promise<void> {
    const readings = await this.getAllReadings();
    readings.unshift(reading); // New readings first
    await AsyncStorage.setItem(this.READINGS_KEY, JSON.stringify(readings));
  }

  // Get all saved readings
  async getAllReadings(): Promise<SavedReading[]> {
    const data = await AsyncStorage.getItem(this.READINGS_KEY);
    return data ? JSON.parse(data) : [];
  }

  // Update reading notes
  async updateReadingNotes(readingId: string, notes: string): Promise<void> {
    const readings = await this.getAllReadings();
    const index = readings.findIndex((r) => r.id === readingId);
    
    if (index !== -1) {
      readings[index].notes = notes;
      await AsyncStorage.setItem(this.READINGS_KEY, JSON.stringify(readings));
    }
  }

  // Add tags to reading
  async addReadingTags(readingId: string, tags: string[]): Promise<void> {
    const readings = await this.getAllReadings();
    const index = readings.findIndex((r) => r.id === readingId);
    
    if (index !== -1) {
      readings[index].tags = [...(readings[index].tags || []), ...tags];
      await AsyncStorage.setItem(this.READINGS_KEY, JSON.stringify(readings));
    }
  }

  // Delete reading
  async deleteReading(readingId: string): Promise<void> {
    const readings = await this.getAllReadings();
    const filtered = readings.filter((r) => r.id !== readingId);
    await AsyncStorage.setItem(this.READINGS_KEY, JSON.stringify(filtered));
  }

  // Settings
  async getSetting(key: string): Promise<any> {
    const settings = await AsyncStorage.getItem(this.SETTINGS_KEY);
    const parsed = settings ? JSON.parse(settings) : {};
    return parsed[key];
  }

  async setSetting(key: string, value: any): Promise<void> {
    const settings = await AsyncStorage.getItem(this.SETTINGS_KEY);
    const parsed = settings ? JSON.parse(settings) : {};
    parsed[key] = value;
    await AsyncStorage.setItem(this.SETTINGS_KEY, JSON.stringify(parsed));
  }
}

export const storage = new StorageService();

Step 3: Notification Service

Create services/notifications.ts:

import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';

// Configure notification behavior
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

class NotificationService {
  async requestPermissions(): Promise<boolean> {
    if (Platform.OS === 'android') {
      await Notifications.setNotificationChannelAsync('daily-tarot', {
        name: 'Daily Tarot',
        importance: Notifications.AndroidImportance.HIGH,
      });
    }

    const { status } = await Notifications.requestPermissionsAsync();
    return status === 'granted';
  }

  async scheduleDailyReminder(hour: number = 8, minute: number = 0): Promise<void> {
    // Cancel existing reminders
    await Notifications.cancelAllScheduledNotificationsAsync();

    // Schedule daily notification
    await Notifications.scheduleNotificationAsync({
      content: {
        title: '🔮 Your Daily Tarot Card Awaits',
        body: 'Discover what the cards have to say today',
        sound: true,
      },
      trigger: {
        hour,
        minute,
        repeats: true,
      },
    });
  }

  async cancelDailyReminder(): Promise<void> {
    await Notifications.cancelAllScheduledNotificationsAsync();
  }
}

export const notifications = new NotificationService();

Step 4: Reusable Components

TarotCard Component

Create components/TarotCard.tsx:

import React from 'react';
import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native';
import type { TarotCard as TarotCardType } from '../services/tarotAPI';

interface Props {
  card: TarotCardType;
  showDetails?: boolean;
  onPress?: () => void;
}

export function TarotCard({ card, showDetails = false, onPress }: Props) {
  const content = (
    <View style={styles.container}>
      {/* Card Image */}
      <Image
        source={{ uri: card.imageUrl }}
        style={[
          styles.image,
          card.reversed && styles.reversed,
        ]}
        resizeMode="contain"
      />

      {/* Card Name */}
      <Text style={styles.name}>
        {card.name}
        {card.reversed && ' (Reversed)'}
      </Text>

      {/* Keywords */}
      {showDetails && (
        <View style={styles.keywords}>
          {card.keywords.slice(0, 3).map((keyword, i) => (
            <View key={i} style={styles.keywordBadge}>
              <Text style={styles.keywordText}>{keyword}</Text>
            </View>
          ))}
        </View>
      )}

      {/* Meaning */}
      {showDetails && (
        <Text style={styles.meaning} numberOfLines={3}>
          {card.meaning}
        </Text>
      )}
    </View>
  );

  return onPress ? (
    <TouchableOpacity onPress={onPress} activeOpacity={0.8}>
      {content}
    </TouchableOpacity>
  ) : (
    content
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  image: {
    width: 120,
    height: 200,
    marginBottom: 12,
  },
  reversed: {
    transform: [{ rotate: '180deg' }],
  },
  name: {
    fontSize: 18,
    fontWeight: '600',
    textAlign: 'center',
    marginBottom: 8,
  },
  keywords: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    justifyContent: 'center',
    marginTop: 8,
    marginBottom: 12,
  },
  keywordBadge: {
    backgroundColor: '#f0f0f0',
    paddingHorizontal: 10,
    paddingVertical: 4,
    borderRadius: 12,
    margin: 4,
  },
  keywordText: {
    fontSize: 12,
    color: '#666',
  },
  meaning: {
    fontSize: 14,
    color: '#555',
    textAlign: 'center',
    lineHeight: 20,
  },
});

Spread Layout Component

Create components/SpreadLayout.tsx:

import React from 'react';
import { View, Text, StyleSheet, ScrollView } from 'react-native';
import type { SpreadPosition } from '../services/tarotAPI';
import { TarotCard } from './TarotCard';

interface Props {
  positions: SpreadPosition[];
  spreadType: string;
}

export function SpreadLayout({ positions, spreadType }: Props) {
  // Different layouts for different spreads
  const getLayout = () => {
    switch (spreadType) {
      case 'three-card':
        return renderThreeCardLayout();
      case 'celtic-cross':
        return renderCelticCrossLayout();
      default:
        return renderDefaultLayout();
    }
  };

  const renderThreeCardLayout = () => (
    <View style={styles.threeCard}>
      {positions.map((position) => (
        <View key={position.position} style={styles.positionContainer}>
          <Text style={styles.positionName}>{position.name}</Text>
          <TarotCard card={position.card} />
          <Text style={styles.interpretation}>{position.interpretation}</Text>
        </View>
      ))}
    </View>
  );

  const renderCelticCrossLayout = () => (
    <ScrollView horizontal showsHorizontalScrollIndicator={false}>
      <View style={styles.celticCross}>
        {positions.map((position) => (
          <View key={position.position} style={styles.celticPosition}>
            <Text style={styles.positionNumber}>{position.position}</Text>
            <Text style={styles.positionName}>{position.name}</Text>
            <TarotCard card={position.card} />
            <Text style={styles.celticInterpretation} numberOfLines={4}>
              {position.interpretation}
            </Text>
          </View>
        ))}
      </View>
    </ScrollView>
  );

  const renderDefaultLayout = () => (
    <View style={styles.default}>
      {positions.map((position) => (
        <View key={position.position} style={styles.defaultPosition}>
          <View style={styles.positionHeader}>
            <Text style={styles.positionNumber}>{position.position}</Text>
            <Text style={styles.positionName}>{position.name}</Text>
          </View>
          <TarotCard card={position.card} showDetails />
          <Text style={styles.interpretation}>{position.interpretation}</Text>
        </View>
      ))}
    </View>
  );

  return getLayout();
}

const styles = StyleSheet.create({
  threeCard: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingHorizontal: 8,
  },
  positionContainer: {
    flex: 1,
    marginHorizontal: 4,
    alignItems: 'center',
  },
  positionName: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 12,
    textAlign: 'center',
  },
  interpretation: {
    fontSize: 14,
    color: '#555',
    marginTop: 12,
    textAlign: 'center',
    lineHeight: 20,
  },
  celticCross: {
    flexDirection: 'row',
    paddingHorizontal: 16,
  },
  celticPosition: {
    width: 200,
    marginRight: 16,
    alignItems: 'center',
  },
  positionNumber: {
    fontSize: 14,
    fontWeight: '700',
    color: '#888',
    marginBottom: 4,
  },
  celticInterpretation: {
    fontSize: 13,
    color: '#666',
    marginTop: 8,
    textAlign: 'center',
  },
  default: {
    paddingHorizontal: 16,
  },
  defaultPosition: {
    marginBottom: 32,
  },
  positionHeader: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 12,
  },
});

Step 5: Home Screen (Daily Card)

Create screens/HomeScreen.tsx:

import React, { useEffect, useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  ActivityIndicator,
  TouchableOpacity,
  ScrollView,
} from 'react-native';
import { tarotAPI, type TarotCard as TarotCardType } from '../services/tarotAPI';
import { storage } from '../services/storage';
import { TarotCard } from '../components/TarotCard';

export function HomeScreen({ navigation }) {
  const [loading, setLoading] = useState(true);
  const [dailyCard, setDailyCard] = useState<TarotCardType | null>(null);
  const [message, setMessage] = useState('');

  useEffect(() => {
    loadDailyCard();
  }, []);

  const loadDailyCard = async () => {
    try {
      setLoading(true);
      const userId = await storage.getUserId();
      const data = await tarotAPI.getDailyCard(userId);
      
      setDailyCard(data.card);
      setMessage(data.message);
    } catch (error) {
      console.error('Failed to load daily card:', error);
    } finally {
      setLoading(false);
    }
  };

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#7C3AED" />
      </View>
    );
  }

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.date}>
          {new Date().toLocaleDateString('en-US', {
            weekday: 'long',
            year: 'numeric',
            month: 'long',
            day: 'numeric',
          })}
        </Text>
        <Text style={styles.title}>Your Daily Card</Text>
      </View>

      {dailyCard && (
        <>
          <View style={styles.cardContainer}>
            <TarotCard card={dailyCard} showDetails />
          </View>

          <View style={styles.messageContainer}>
            <Text style={styles.message}>{message}</Text>
          </View>
        </>
      )}

      <View style={styles.actions}>
        <TouchableOpacity
          style={styles.button}
          onPress={() => navigation.navigate('NewReading')}
        >
          <Text style={styles.buttonText}>Get a Full Reading</Text>
        </TouchableOpacity>

        <TouchableOpacity
          style={[styles.button, styles.secondaryButton]}
          onPress={() => navigation.navigate('History')}
        >
          <Text style={[styles.buttonText, styles.secondaryButtonText]}>
            View Past Readings
          </Text>
        </TouchableOpacity>
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F9FAFB',
  },
  center: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  header: {
    padding: 24,
    backgroundColor: '#fff',
    borderBottomWidth: 1,
    borderBottomColor: '#E5E7EB',
  },
  date: {
    fontSize: 14,
    color: '#6B7280',
    marginBottom: 8,
  },
  title: {
    fontSize: 28,
    fontWeight: '700',
    color: '#111827',
  },
  cardContainer: {
    padding: 24,
    alignItems: 'center',
  },
  messageContainer: {
    paddingHorizontal: 24,
    paddingBottom: 24,
  },
  message: {
    fontSize: 16,
    lineHeight: 24,
    color: '#374151',
    textAlign: 'center',
  },
  actions: {
    padding: 24,
    gap: 12,
  },
  button: {
    backgroundColor: '#7C3AED',
    paddingVertical: 16,
    paddingHorizontal: 24,
    borderRadius: 12,
    alignItems: 'center',
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
  secondaryButton: {
    backgroundColor: '#fff',
    borderWidth: 2,
    borderColor: '#7C3AED',
  },
  secondaryButtonText: {
    color: '#7C3AED',
  },
});

Step 6: New Reading Screen

Create screens/NewReadingScreen.tsx:

import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  StyleSheet,
  TouchableOpacity,
  ScrollView,
  ActivityIndicator,
} from 'react-native';
import { tarotAPI } from '../services/tarotAPI';
import { storage } from '../services/storage';

const SPREAD_TYPES = [
  { id: 'three-card', name: 'Three-Card Spread', cards: 3, description: 'Past, Present, Future' },
  { id: 'love', name: 'Love Spread', cards: 5, description: 'Relationship insights' },
  { id: 'career', name: 'Career Spread', cards: 7, description: 'Professional guidance' },
  { id: 'celtic-cross', name: 'Celtic Cross', cards: 10, description: 'Comprehensive reading' },
];

export function NewReadingScreen({ navigation }) {
  const [question, setQuestion] = useState('');
  const [selectedSpread, setSelectedSpread] = useState('three-card');
  const [loading, setLoading] = useState(false);

  const handleGetReading = async () => {
    if (!question.trim()) {
      alert('Please enter a question');
      return;
    }

    try {
      setLoading(true);
      
      // Generate unique seed
      const userId = await storage.getUserId();
      const readingId = `${userId}-${Date.now()}`;
      
      // Fetch reading
      let reading;
      switch (selectedSpread) {
        case 'three-card':
          reading = await tarotAPI.getThreeCardReading(question, readingId);
          break;
        case 'love':
          reading = await tarotAPI.getLoveReading(question, readingId);
          break;
        case 'career':
          reading = await tarotAPI.getCareerReading(question, readingId);
          break;
        case 'celtic-cross':
          reading = await tarotAPI.getCelticCrossReading(question, readingId);
          break;
      }
      
      // Save metadata
      await storage.saveReading({
        id: readingId,
        spreadType: selectedSpread,
        question,
        seed: readingId,
        createdAt: new Date().toISOString(),
      });
      
      // Navigate to detail screen
      navigation.navigate('ReadingDetail', {
        readingId,
        reading,
      });
    } catch (error) {
      console.error('Failed to get reading:', error);
      alert('Failed to get reading. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <ScrollView style={styles.container}>
      <View style={styles.content}>
        <Text style={styles.label}>Your Question</Text>
        <TextInput
          style={styles.input}
          placeholder="What do I need to know about...?"
          value={question}
          onChangeText={setQuestion}
          multiline
          numberOfLines={3}
        />

        <Text style={styles.label}>Select Spread Type</Text>
        {SPREAD_TYPES.map((spread) => (
          <TouchableOpacity
            key={spread.id}
            style={[
              styles.spreadOption,
              selectedSpread === spread.id && styles.spreadOptionSelected,
            ]}
            onPress={() => setSelectedSpread(spread.id)}
          >
            <View>
              <Text style={styles.spreadName}>{spread.name}</Text>
              <Text style={styles.spreadDescription}>
                {spread.cards} cards • {spread.description}
              </Text>
            </View>
            <View
              style={[
                styles.radio,
                selectedSpread === spread.id && styles.radioSelected,
              ]}
            />
          </TouchableOpacity>
        ))}

        <TouchableOpacity
          style={[styles.button, loading && styles.buttonDisabled]}
          onPress={handleGetReading}
          disabled={loading}
        >
          {loading ? (
            <ActivityIndicator color="#fff" />
          ) : (
            <Text style={styles.buttonText}>Get Reading</Text>
          )}
        </TouchableOpacity>
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F9FAFB',
  },
  content: {
    padding: 24,
  },
  label: {
    fontSize: 16,
    fontWeight: '600',
    color: '#111827',
    marginBottom: 12,
  },
  input: {
    backgroundColor: '#fff',
    borderWidth: 1,
    borderColor: '#D1D5DB',
    borderRadius: 12,
    padding: 16,
    fontSize: 16,
    marginBottom: 24,
    minHeight: 100,
    textAlignVertical: 'top',
  },
  spreadOption: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    backgroundColor: '#fff',
    borderWidth: 2,
    borderColor: '#E5E7EB',
    borderRadius: 12,
    padding: 16,
    marginBottom: 12,
  },
  spreadOptionSelected: {
    borderColor: '#7C3AED',
    backgroundColor: '#F3F0FF',
  },
  spreadName: {
    fontSize: 16,
    fontWeight: '600',
    color: '#111827',
    marginBottom: 4,
  },
  spreadDescription: {
    fontSize: 14,
    color: '#6B7280',
  },
  radio: {
    width: 24,
    height: 24,
    borderRadius: 12,
    borderWidth: 2,
    borderColor: '#D1D5DB',
  },
  radioSelected: {
    borderColor: '#7C3AED',
    backgroundColor: '#7C3AED',
  },
  button: {
    backgroundColor: '#7C3AED',
    paddingVertical: 16,
    borderRadius: 12,
    alignItems: 'center',
    marginTop: 24,
  },
  buttonDisabled: {
    opacity: 0.6,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

Step 7: History Screen

Create screens/HistoryScreen.tsx:

import React, { useEffect, useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  FlatList,
  TouchableOpacity,
  ActivityIndicator,
} from 'react-native';
import { storage, type SavedReading } from '../services/storage';
import { tarotAPI } from '../services/tarotAPI';

export function HistoryScreen({ navigation }) {
  const [readings, setReadings] = useState<SavedReading[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadReadings();
    
    // Refresh when screen comes into focus
    const unsubscribe = navigation.addListener('focus', loadReadings);
    return unsubscribe;
  }, [navigation]);

  const loadReadings = async () => {
    try {
      setLoading(true);
      const allReadings = await storage.getAllReadings();
      setReadings(allReadings);
    } catch (error) {
      console.error('Failed to load readings:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleReadingPress = async (savedReading: SavedReading) => {
    try {
      // Recreate reading from seed
      const reading = await tarotAPI.recreateReading(
        savedReading.spreadType,
        savedReading.seed,
        savedReading.question
      );
      
      navigation.navigate('ReadingDetail', {
        readingId: savedReading.id,
        reading,
        savedReading,
      });
    } catch (error) {
      console.error('Failed to load reading:', error);
      alert('Failed to load reading');
    }
  };

  const formatDate = (dateString: string) => {
    const date = new Date(dateString);
    return date.toLocaleDateString('en-US', {
      month: 'short',
      day: 'numeric',
      year: 'numeric',
    });
  };

  const renderReading = ({ item }: { item: SavedReading }) => (
    <TouchableOpacity
      style={styles.readingCard}
      onPress={() => handleReadingPress(item)}
    >
      <View style={styles.readingHeader}>
        <Text style={styles.spreadType}>
          {item.spreadType.split('-').map(w => 
            w.charAt(0).toUpperCase() + w.slice(1)
          ).join(' ')}
        </Text>
        <Text style={styles.date}>{formatDate(item.createdAt)}</Text>
      </View>
      
      <Text style={styles.question} numberOfLines={2}>
        {item.question || 'No question'}
      </Text>
      
      {item.notes && (
        <Text style={styles.notes} numberOfLines={1}>
          📝 {item.notes}
        </Text>
      )}
      
      {item.tags && item.tags.length > 0 && (
        <View style={styles.tags}>
          {item.tags.slice(0, 3).map((tag, i) => (
            <View key={i} style={styles.tag}>
              <Text style={styles.tagText}>{tag}</Text>
            </View>
          ))}
        </View>
      )}
    </TouchableOpacity>
  );

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#7C3AED" />
      </View>
    );
  }

  if (readings.length === 0) {
    return (
      <View style={styles.center}>
        <Text style={styles.emptyTitle}>No Readings Yet</Text>
        <Text style={styles.emptyText}>
          Your tarot reading history will appear here
        </Text>
        <TouchableOpacity
          style={styles.button}
          onPress={() => navigation.navigate('NewReading')}
        >
          <Text style={styles.buttonText}>Get Your First Reading</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <FlatList
        data={readings}
        renderItem={renderReading}
        keyExtractor={(item) => item.id}
        contentContainerStyle={styles.list}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F9FAFB',
  },
  center: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 24,
  },
  list: {
    padding: 16,
  },
  readingCard: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.05,
    shadowRadius: 2,
    elevation: 2,
  },
  readingHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 8,
  },
  spreadType: {
    fontSize: 14,
    fontWeight: '600',
    color: '#7C3AED',
  },
  date: {
    fontSize: 12,
    color: '#9CA3AF',
  },
  question: {
    fontSize: 16,
    color: '#111827',
    marginBottom: 8,
  },
  notes: {
    fontSize: 14,
    color: '#6B7280',
    fontStyle: 'italic',
    marginBottom: 8,
  },
  tags: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    marginTop: 8,
  },
  tag: {
    backgroundColor: '#F3F4F6',
    paddingHorizontal: 8,
    paddingVertical: 4,
    borderRadius: 8,
    marginRight: 6,
    marginBottom: 6,
  },
  tagText: {
    fontSize: 12,
    color: '#6B7280',
  },
  emptyTitle: {
    fontSize: 20,
    fontWeight: '600',
    color: '#111827',
    marginBottom: 8,
  },
  emptyText: {
    fontSize: 16,
    color: '#6B7280',
    textAlign: 'center',
    marginBottom: 24,
  },
  button: {
    backgroundColor: '#7C3AED',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 12,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

Step 8: Navigation Setup

Update App.tsx:

import React, { useEffect } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { HomeScreen } from './screens/HomeScreen';
import { HistoryScreen } from './screens/HistoryScreen';
import { NewReadingScreen } from './screens/NewReadingScreen';
import { notifications } from './services/notifications';

const Tab = createBottomTabNavigator();

export default function App() {
  useEffect(() => {
    setupNotifications();
  }, []);

  const setupNotifications = async () => {
    const granted = await notifications.requestPermissions();
    if (granted) {
      // Schedule 8 AM daily reminder
      await notifications.scheduleDailyReminder(8, 0);
    }
  };

  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={{
          headerStyle: { backgroundColor: '#7C3AED' },
          headerTintColor: '#fff',
          tabBarActiveTintColor: '#7C3AED',
        }}
      >
        <Tab.Screen 
          name="Home" 
          component={HomeScreen}
          options={{ title: 'Daily Card' }}
        />
        <Tab.Screen 
          name="NewReading" 
          component={NewReadingScreen}
          options={{ title: 'New Reading' }}
        />
        <Tab.Screen 
          name="History" 
          component={HistoryScreen}
          options={{ title: 'History' }}
        />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

Testing the App

1. Run on iOS Simulator

npx expo start --ios

2. Run on Android Emulator

npx expo start --android

3. Test on Physical Device

npx expo start
# Scan QR code with Expo Go app

Production Features to Add

1. Reading Detail with Notes

Allow users to add personal reflections:

const [notes, setNotes] = useState(savedReading?.notes || '');

const saveNotes = async () => {
  await storage.updateReadingNotes(readingId, notes);
};

2. Share Reading Feature

Export reading as image or text:

import * as Sharing from 'expo-sharing';

const shareReading = async () => {
  const text = `
My Tarot Reading - ${new Date().toLocaleDateString()}

Question: ${reading.question}

${reading.positions.map(p => 
  `${p.name}: ${p.card.name}\n${p.interpretation}`
).join('\n\n')}
  `.trim();
  
  await Sharing.shareAsync('data:text/plain;base64,' + btoa(text));
};

3. Card Search and Browse

Add full card database screen with search.

4. Reading Stats

Track reading frequency, common cards, mood patterns.

5. Dark Mode

Use useColorScheme hook and theme switching.

Performance Optimization

1. Image Caching

// Preload card images
import { Image } from 'react-native';

const preloadImages = async () => {
  const urls = cardIds.map(id => 
    `https://roxyapi.com/img/tarot/${id}.jpg`
  );
  await Promise.all(urls.map(url => Image.prefetch(url)));
};

2. Offline Support

// Cache API responses locally
const cachedReading = await AsyncStorage.getItem(`reading:${seed}`);
if (cachedReading) {
  return JSON.parse(cachedReading);
}

3. Reduce API Calls

Store reading metadata, recreate from seed only when viewing.

Monetization Strategy

Free Tier

  • Daily card feature
  • 3 three-card readings per week
  • Basic history (last 10 readings)

Premium ($4.99/month)

  • Unlimited readings
  • All spread types (Celtic Cross, Love, Career)
  • Full history with search
  • Export readings as PDF
  • Priority API access

Next Steps

You now have a complete tarot journaling app! Here's what to do next:

  1. Get your API key: Visit RoxyAPI Pricing for free tier
  2. Deploy to stores: Use EAS Build for iOS/Android
  3. Add analytics: Track user behavior with Expo Analytics
  4. Explore other APIs: Check out all RoxyAPI products - numerology, astrology, and more
  5. Review documentation: See RoxyAPI Docs for complete API reference

Full code on GitHub: (Link your repository here)

Questions? The Tarot API product page has detailed documentation and support.

Happy building! 🔮✨