Directory structure: └── vigourpt-mystic-balls/ ├── README.md ├── ROADMAP.md ├── eslint.config.js ├── index.html ├── netlify.toml ├── package.json ├── postcss.config.js ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts ├── .nvmrc ├── netlify/ │ └── functions/ │ ├── create-checkout-session.ts │ ├── getReading.ts │ └── utils/ │ └── rateLimiter.ts ├── public/ │ ├── sw.js │ └── optimized/ │ ├── MysticBalls-logo-1024.webp │ ├── MysticBalls-logo-1280.webp │ ├── MysticBalls-logo-1536.webp │ ├── MysticBalls-logo-320.webp │ ├── MysticBalls-logo-640.webp │ ├── MysticBalls-logo-768.webp │ ├── favicon-1024.webp │ ├── favicon-1280.webp │ ├── favicon-1536.webp │ ├── favicon-320.webp │ ├── favicon-640.webp │ └── favicon-768.webp ├── scripts/ │ ├── optimize-favicon.js │ └── optimize-images.ts ├── src/ │ ├── App.tsx │ ├── index.css │ ├── main.tsx │ ├── types.ts │ ├── vite-env.d.ts │ ├── components/ │ │ ├── Advertisement.tsx │ │ ├── ApiKeyModal.tsx │ │ ├── AsyncComponent.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── FAQ.tsx │ │ ├── Footer.tsx │ │ ├── FreeTrialPrompt.tsx │ │ ├── Header.tsx │ │ ├── LoadingSpinner.tsx │ │ ├── LoginModal.tsx │ │ ├── MainContent.tsx │ │ ├── OnboardingOverlay.tsx │ │ ├── OptimizedImage.tsx │ │ ├── PaymentModal.tsx │ │ ├── PaymentSuccess.tsx │ │ ├── PremiumBadge.tsx │ │ ├── PrivacyPolicy.tsx │ │ ├── ReadingCard.tsx │ │ ├── ReadingDisplay.tsx │ │ ├── ReadingForm.tsx │ │ ├── ReadingOutput.tsx │ │ ├── ReadingSelector.tsx │ │ ├── ReadingTypeCard.tsx │ │ ├── ReadingTypeInfo.tsx │ │ ├── TermsOfService.tsx │ │ ├── Tooltip.tsx │ │ ├── TourGuide.tsx │ │ ├── TutorialButton.tsx │ │ ├── UpgradeModal.tsx │ │ ├── YourComponent.tsx │ │ ├── UserStatus/ │ │ │ └── UserStatus.tsx │ │ ├── disabled/ │ │ │ ├── README.md │ │ │ └── TrialOfferModal.tsx │ │ ├── forms/ │ │ │ ├── AngelNumbersForm.tsx │ │ │ ├── AstrologyForm.tsx │ │ │ ├── AuraForm.tsx │ │ │ ├── DreamForm.tsx │ │ │ ├── HoroscopeForm.tsx │ │ │ ├── Magic8BallForm.tsx │ │ │ ├── NumerologyForm.tsx │ │ │ ├── PastLifeForm.tsx │ │ │ ├── QuestionForm.tsx │ │ │ └── types.ts │ │ └── icons/ │ │ ├── AngelIcon.tsx │ │ ├── AuraIcon.tsx │ │ ├── BallIcon.tsx │ │ ├── CardIcon.tsx │ │ ├── HexagramIcon.tsx │ │ ├── HistoryIcon.tsx │ │ ├── MoonIcon.tsx │ │ ├── NumberIcon.tsx │ │ ├── OracleIcon.tsx │ │ ├── RuneIcon.tsx │ │ ├── StarIcon.tsx │ │ └── index.ts │ ├── config/ │ │ ├── constants.ts │ │ ├── openai.ts │ │ ├── plans.ts │ │ ├── production.ts │ │ ├── stripe.ts │ │ └── tutorial.ts │ ├── data/ │ │ └── readingTypes.ts │ ├── hooks/ │ │ ├── index.ts │ │ ├── useAppState.ts │ │ ├── useAuth.ts │ │ ├── useAuthState.ts │ │ ├── useDataFetching.ts │ │ ├── useTutorial.ts │ │ ├── useUsageTracking.ts │ │ └── useUser.ts │ ├── lib/ │ │ └── supabaseClient.ts │ ├── middleware/ │ │ └── rateLimiter.ts │ ├── routes/ │ │ └── auth/ │ │ └── callback/ │ │ └── index.tsx │ ├── services/ │ │ ├── openai.ts │ │ ├── stripe.ts │ │ └── supabase.ts │ ├── types/ │ │ ├── env.d.ts │ │ ├── index.ts │ │ └── supabase.ts │ └── utils/ │ ├── cache.ts │ ├── confetti.ts │ ├── currency.ts │ ├── performance.ts │ └── retryFetch.ts ├── supabase/ │ ├── config.toml │ ├── functions/ │ │ └── create-checkout-session/ │ │ └── index.ts │ └── migrations/ │ ├── 20240207_create_user_profiles.sql │ ├── 20240218000000_add_free_readings_remaining.sql │ ├── 20250206104818_light_river.sql │ ├── 20250206104852_rustic_dew.sql │ ├── 20250207111717_crystal_ember.sql │ ├── 20250207111958_shy_coral.sql │ ├── 20250207112040_divine_bird.sql │ ├── 20250207112913_purple_palace.sql │ ├── 20250207172221_rough_mouse.sql │ ├── 20250207172429_pink_shape.sql │ ├── 20250207172657_sparkling_rain.sql │ ├── 20250207173056_patient_oasis.sql │ ├── 20250207173210_round_gate.sql │ ├── 20250207175328_aged_mode.sql │ └── 20250207190000_add_test_premium_user.sql └── .bolt/ ├── config.json └── prompt ================================================ File: README.md ================================================ # Mystic-Balls [Edit in StackBlitz next generation editor ⚡️](https://stackblitz.com/~/github.com/vigourpt/Mystic-Balls) ================================================ File: ROADMAP.md ================================================ # Mystic Insights Project Roadmap This roadmap outlines the path to MVP (Minimum Viable Product) and beyond for the Mystic Insights platform. ## Phase 1: Foundation ✅ - [x] Initial project setup with Vite and React - [x] Basic UI components and layout - [x] Dark/Light mode implementation - [x] Responsive design - [x] Basic routing and navigation ## Phase 2: Core Features ✅ - [x] Basic reading types implementation - [x] Tarot - [x] Numerology - [x] Astrology - [x] Oracle Cards - [x] Runes - [x] I Ching - [x] Angel Numbers - [x] Daily Horoscope - [x] Dream Analysis - [x] Magic 8 Ball - [x] AI integration with OpenAI - [x] Reading generation and display - [x] Form handling for different reading types ## Phase 3: User Management ✅ - [x] Supabase authentication - [x] User profile creation - [x] Usage tracking - [x] Reading limits for free users - [x] Premium user features ## Phase 4: Monetization ✅ - [x] Payment plans definition - [x] Stripe integration - [x] Free trial implementation - [x] Subscription management - [x] Payment modal - [x] Trial offer popup ## Phase 5: Enhanced Features ✅ - [x] Additional reading types - [x] Aura Reading - [x] Past Life Reading - [x] Detailed reading descriptions - [x] Reading type information - [x] FAQ section ## MVP Launch Requirements 🚧 - [x] Critical Security - [x] Content Security Policy configuration - [x] Secure API communication - [x] Rate limiting implementation - [x] Basic error logging - [x] Essential Production Setup - [x] Environment variables configuration - [x] Production database setup - [x] Performance Basics - [x] Initial load time optimization - [x] Image optimization - [x] Basic error handling - [ ] Launch Checklist - [x] SSL/HTTPS verification - [x] Payment flow testing - [ ] Cross-browser testing - [x] Mobile responsiveness verification - [x] Loading states and error messages - [x] User feedback on actions ## Current Focus 🎯 - [ ] Performance Optimization - [x] Image optimization implementation - [ ] Lazy loading optimization - [ ] Bundle size reduction - [ ] Caching strategy implementation ## Next Steps 🔄 1. Complete cross-browser testing 2. Implement advanced error tracking 3. Set up monitoring and analytics 4. Enhance user experience with additional features ## Future Phases (Prioritized) 🔮 1. User Experience Enhancements - Reading history - Favorite readings - Personalized recommendations 2. Platform Optimization - Advanced caching strategies - CDN implementation - Performance monitoring 3. Community Features - Share readings - User testimonials - Social integration 4. Mobile Enhancement - PWA implementation - Offline capabilities - Native app-like features 5. Marketing and Growth - SEO optimization - Newsletter integration - Referral program ## Post-MVP Features 🔄 ### Phase 6: Platform Enhancement - [ ] Advanced Security - [ ] Enhanced CORS configuration - [ ] Advanced XSS protection - [ ] CSRF protection - [ ] Security headers optimization - [ ] Performance Optimization - [ ] Code splitting - [ ] Advanced caching strategies - [ ] CDN configuration - [ ] Bundle size optimization - [ ] Monitoring and Analytics - [ ] Error tracking with Sentry - [ ] Performance monitoring - [ ] Usage analytics - [ ] Server monitoring ### Phase 7: User Experience - [ ] Enhanced Features - [ ] Reading history - [ ] Favorite readings - [ ] Custom reading preferences - [ ] Personalized recommendations - [ ] Social Features - [ ] Share readings - [ ] Community features - [ ] User testimonials - [ ] Content Expansion - [ ] Additional reading types - [ ] Guided meditations - [ ] Educational content ### Phase 8: Marketing and Growth - [ ] SEO Optimization - [ ] Meta tags optimization - [ ] Sitemap generation - [ ] Schema markup - [ ] Robots.txt configuration - [ ] Marketing Tools - [ ] Newsletter integration - [ ] Social media integration - [ ] Referral program - [ ] Analytics - [ ] Conversion tracking - [ ] User behavior analysis - [ ] A/B testing framework ### Phase 9: Mobile and Accessibility - [ ] Mobile Experience - [ ] Progressive Web App (PWA) - [ ] Native app-like features - [ ] Offline capabilities - [ ] Accessibility - [ ] WCAG compliance - [ ] Screen reader optimization - [ ] Keyboard navigation - [ ] Color contrast improvements ## Long-term Vision - [ ] AI Enhancements - [ ] Custom AI model training - [ ] Multi-modal readings (text, voice, image) - [ ] Real-time reading updates - [ ] Platform Expansion - [ ] API for third-party integrations - [ ] White-label solutions - [ ] Professional reader marketplace - [ ] Community Building - [ ] User forums - [ ] Expert consultations - [ ] Live reading events ================================================ File: eslint.config.js ================================================ import js from '@eslint/js'; import globals from 'globals'; import reactHooks from 'eslint-plugin-react-hooks'; import reactRefresh from 'eslint-plugin-react-refresh'; import tseslint from 'typescript-eslint'; export default tseslint.config( { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, } ); ================================================ File: index.html ================================================ Mystic Insights - Spiritual Readings & Guidance
================================================ File: netlify.toml ================================================ [build] command = "npm run build" publish = "dist" [build.processing] skip_processing = false minify = true [build.processing.css] bundle = true minify = true [build.processing.js] bundle = true minify = true [build.processing.html] pretty_urls = true [build.processing.images] compress = true [secrets] SECRETS_SCAN_OMIT_PATHS = ["dist/assets/*"] [[headers]] for = "/*" [headers.values] X-Frame-Options = "DENY" X-Content-Type-Options = "nosniff" X-XSS-Protection = "1; mode=block" Referrer-Policy = "strict-origin-when-cross-origin" Permissions-Policy = "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" Content-Security-Policy = "default-src 'self' https://*.supabase.co https://*.stripe.com https://api.openai.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.supabase.co https://*.stripe.com https://cdn.jsdelivr.net https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https://* blob:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' https://*.supabase.co https://*.stripe.com https://api.openai.com https://*.netlify.app wss://*.supabase.co; frame-src https://*.stripe.com; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content" ================================================ File: package.json ================================================ { "name": "mystic-insights", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "lint": "eslint .", "preview": "vite preview", "optimize-images": "tsx scripts/optimize-images.ts" }, "dependencies": { "@netlify/functions": "^3.0.0", "@stripe/stripe-js": "^2.4.0", "@supabase/supabase-js": "^2.48.1", "canvas-confetti": "^1.9.3", "express-rate-limit": "^7.5.0", "lucide-react": "^0.344.0", "openai": "^4.83.0", "pdf-parse": "^1.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.5.0", "react-markdown": "^9.0.1", "react-router-dom": "^7.2.0", "terser": "^5.38.1" }, "devDependencies": { "@babel/core": "^7.24.0", "@babel/plugin-transform-react-jsx": "^7.25.9", "@eslint/js": "^9.9.1", "@types/canvas-confetti": "^1.9.0", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@types/stripe": "^8.0.416", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.18", "eslint": "^9.9.1", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.11", "glob": "^11.0.1", "globals": "^15.9.0", "postcss": "^8.4.35", "sharp": "^0.33.5", "tailwindcss": "^3.4.1", "tsx": "^4.19.3", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", "vite": "^5.4.14" } } ================================================ File: postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ File: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ export default { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], theme: { extend: {}, }, plugins: [], }; ================================================ File: tsconfig.app.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "types": ["vite/client"] }, "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"] } ================================================ File: tsconfig.json ================================================ { "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ] } ================================================ File: tsconfig.node.json ================================================ { "compilerOptions": { "target": "ES2022", "lib": ["ES2023"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["vite.config.ts"] } ================================================ File: vite.config.ts ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { splitVendorChunkPlugin } from 'vite'; export default defineConfig({ plugins: [react(), splitVendorChunkPlugin()], build: { chunkSizeWarningLimit: 1000, sourcemap: false, minify: 'esbuild', cssMinify: true, cssCodeSplit: true, rollupOptions: { output: { inlineDynamicImports: false, } } }, server: { cors: true, strictPort: true, hmr: { timeout: 5000 } } }); ================================================ File: .nvmrc ================================================ 20.0.0 ================================================ File: netlify/functions/create-checkout-session.ts ================================================ import { Handler } from '@netlify/functions'; import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', { apiVersion: '2025-01-27.acacia' // Update to the required API version }); export const handler: Handler = async (event) => { if (event.httpMethod !== 'POST') { return { statusCode: 405, body: 'Method Not Allowed' }; } try { const { priceId } = JSON.parse(event.body || '{}'); // Prevent multiple session creation const session = await stripe.checkout.sessions.create({ mode: 'subscription', payment_method_types: ['card'], line_items: [ { price: priceId, quantity: 1, }, ], success_url: `${event.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${event.headers.origin}/`, }); return { statusCode: 200, body: JSON.stringify({ sessionId: session.id, url: session.url // Include the URL in the response }), }; } catch (error) { console.error('Stripe checkout error:', error); return { statusCode: 500, body: JSON.stringify({ error: 'Failed to create checkout session' }), }; } }; ================================================ File: netlify/functions/getReading.ts ================================================ import { Handler } from '@netlify/functions'; import OpenAI from 'openai'; import { rateLimiter } from './utils/rateLimiter'; import { createClient } from '@supabase/supabase-js'; import { supabaseClient } from '../../src/lib/supabaseClient'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, defaultHeaders: { 'OpenAI-Project-Id': process.env.OPENAI_PROJECT_ID } }); const MAX_FREE_READINGS = 3; const supabase = createClient( process.env.SUPABASE_URL || '', process.env.SUPABASE_SERVICE_KEY || '' ); const readingConfigs: Record = { 'tarot': { maxTokens: 1000, temperature: 0.7, systemPrompt: `You are an experienced tarot reader with deep knowledge of the 78-card deck. Provide: ### Cards Drawn [List the cards intuitively selected] ### Individual Interpretations [Analyze each card's meaning] ### Cards Interaction [Explain how the cards relate] ### Overall Message [Provide guidance and insights] Use clear, compassionate language and maintain proper markdown formatting.` }, 'numerology': { maxTokens: 800, temperature: 0.6, systemPrompt: `You are a skilled numerologist. Analyze the numerical patterns and provide: ### Life Path Number [Calculate and interpret] ### Destiny Number [Calculate and interpret] ### Soul Urge Number [Calculate and interpret] ### Personality Number [Calculate and interpret] ### Life Purpose [Synthesize overall meaning] Use clear, compassionate language and maintain proper markdown formatting.` }, 'astrology': { maxTokens: 1000, temperature: 0.7, systemPrompt: `You are an expert astrologer. Provide a detailed reading covering: ### Planetary Positions [Detail current celestial alignments] ### Personal Influences [Explain how these affect the individual] ### Key Life Areas [Career, relationships, personal growth] ### Future Opportunities [Upcoming favorable periods and potential challenges] Use clear, compassionate language and maintain proper markdown formatting.` }, 'oracle': { maxTokens: 800, temperature: 0.7, systemPrompt: `You are a mystic oracle reader. Based on the seeker's question, provide: ### Initial Insights [Share immediate impressions] ### Card Messages [Interpret the oracle cards drawn] ### Divine Guidance [Offer spiritual advice] ### Action Steps [Suggest practical next steps] Use clear, compassionate language and maintain proper markdown formatting.` }, 'dreamanalysis': { // Changed from 'dream' to match the reading type ID maxTokens: 1000, temperature: 0.7, systemPrompt: `You are a skilled dream interpreter. Analyze the dream and provide: ### Symbol Analysis [Interpret key dream symbols] ### Emotional Context [Explore feelings and meanings] ### Personal Significance [Connect to dreamer's life] ### Guidance & Messages [Offer insights and advice] Use clear, compassionate language and maintain proper markdown formatting.` }, 'aura': { maxTokens: 800, temperature: 0.7, systemPrompt: `You are an experienced aura reader. Provide insights into: ### Aura Colors [Identify and interpret dominant colors] ### Energy Patterns [Describe energy flow and blocks] ### Chakra Balance [Assess major energy centers] ### Recommendations [Suggest energy maintenance practices] Use clear, compassionate language and maintain proper markdown formatting.` }, 'runes': { maxTokens: 800, temperature: 0.7, systemPrompt: `You are a skilled rune reader versed in Norse wisdom. Provide: ### Runes Drawn [List the runes selected] ### Individual Meanings [Interpret each rune's significance] ### Combined Message [Explain how runes work together] ### Practical Guidance [Offer actionable wisdom] Use clear, compassionate language and maintain proper markdown formatting.` }, 'iching': { maxTokens: 1000, temperature: 0.6, systemPrompt: `You are a wise I-Ching interpreter. Provide: ### Hexagram Drawn [Show and name the hexagram] ### Core Meaning [Explain primary symbolism] ### Changing Lines [Detail any changing lines] ### Guidance [Share wisdom for the situation] Use clear, compassionate language and maintain proper markdown formatting.` }, 'horoscope': { maxTokens: 1000, temperature: 0.7, systemPrompt: `You are an expert astrologer. Provide: ### Daily Overview [General energy and influences] ### Love & Relationships [Romantic and social insights] ### Career & Goals [Professional guidance] ### Health & Wellness [Physical and emotional wellbeing] ### Lucky Elements [Favorable factors for today] Use clear, compassionate language and maintain proper markdown formatting.` }, 'pastlife': { maxTokens: 1000, temperature: 0.8, systemPrompt: `You are a past life reader. Create a narrative covering: ### Time Period & Location [Historical context] ### Past Identity [Key characteristics and role] ### Significant Events [Important life experiences] ### Present Connections [Links to current life] ### Soul Lessons [Wisdom carried forward] Use clear, compassionate language and maintain proper markdown formatting.` }, 'angelnumbers': { maxTokens: 800, temperature: 0.7, systemPrompt: `You are an angel number interpreter. Provide a detailed interpretation that includes: ### Number Significance [Explain the spiritual significance of the number sequence] ### Divine Message [Share the angels' message] ### Spiritual Meaning [Explain deeper spiritual implications] ### Practical Guidance [Offer actionable steps or advice] Use clear, compassionate language and maintain proper markdown formatting.` }, 'magic8ball': { maxTokens: 100, temperature: 0.7, systemPrompt: `You are a mystical Magic 8 Ball oracle. Provide a clear, concise response in this format: ### The Magic 8 Ball Says [Provide one of the classic Magic 8 Ball responses like "It is certain", "Ask again later", "Don't count on it", etc.] ### Mystical Insight [A brief 1-2 sentence elaboration on the answer] Use clear, compassionate language and maintain proper markdown formatting.` }, }; const handler: Handler = async (event, context) => { console.log('Received event:', JSON.stringify(event)); try { // Apply rate limiting try { // Check OpenAI rate limit first const clientIp = event.headers['client-ip'] || event.headers['x-nf-client-connection-ip'] || 'unknown'; if (rateLimiter.isRateLimited(clientIp)) { console.log('Rate limit exceeded for IP:', clientIp); return { statusCode: 429, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Retry-After': '60' }, body: JSON.stringify({ error: 'Too many requests. Please try again in 1 minute.', retryAfter: 60 }) }; } } catch (error) { console.error('Rate limit error:', error); return { statusCode: 429, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Retry-After': '60' }, body: JSON.stringify({ error: 'Rate limiting error occurred', retryAfter: 60 }) }; } if (event.httpMethod === 'OPTIONS') { return { statusCode: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Allow-Methods': 'POST, OPTIONS' } }; } if (event.httpMethod !== 'POST') { console.log('Method not allowed:', event.httpMethod); return { statusCode: 405, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ error: 'Method Not Allowed' }) }; } if (!process.env.OPENAI_API_KEY || !process.env.OPENAI_PROJECT_ID) { console.error('OpenAI API key or project ID missing'); return { statusCode: 500, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ error: 'OpenAI configuration missing' }) }; } try { const authHeader = event.headers.authorization; if (!authHeader) { console.error('Missing authorization header'); throw new Error('Missing authorization header'); } // Get user profile const { data: { user }, error: authError } = await supabaseClient.auth.getUser( authHeader.replace('Bearer ', '') ); if (authError || !user) { console.error('Supabase auth error:', authError); throw new Error('Unauthorized'); } // Get user profile with readings count const { data: profile, error: profileError } = await supabaseClient .from('user_profiles') .select('*') .eq('id', user.id) .single(); if (profileError) { console.error('Supabase Profile Error:', profileError); throw new Error('Failed to get user profile'); } // Fix the free readings check - ensure we start with correct initial values const currentReadingsCount = profile.readings_count || 0; const freeReadingsRemaining = profile.free_readings_remaining ?? MAX_FREE_READINGS; if (!profile.is_premium && freeReadingsRemaining <= 0) { console.log('Free trial ended for user:', user.id); return { statusCode: 402, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Retry-After': '60' }, body: JSON.stringify({ error: 'Free trial ended', message: 'You have used all your free readings. Please upgrade to continue.', requiresUpgrade: true }) }; } const { readingType, userInput } = JSON.parse(event.body || '{}'); if (!readingType || !userInput) { console.error('Missing readingType or userInput'); return { statusCode: 400, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ error: 'Missing required parameters' }) }; } // Input validation for specific reading types if (readingType === 'numerology' && (!userInput.fullname || !userInput.birthdate)) { console.error('Missing fullname or birthdate for numerology'); return { statusCode: 400, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ error: 'Name and birthdate required for numerology' }) }; } if (readingType === 'oracle' && !userInput.question) { console.error('Missing question for oracle reading'); return { statusCode: 400, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ error: 'Please provide a question for your oracle reading' }) }; } if (readingType === 'pastlife' && !userInput.concerns) { console.error('Missing concerns for pastlife'); return { statusCode: 400, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ error: 'Name and time period required for past life reading' }) }; } const config = readingConfigs[readingType]; if (!config) { console.error('Unsupported reading type:', readingType); return { statusCode: 400, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ error: `Unsupported reading type: ${readingType}` }) }; } // Update prompts to match configurations const prompts: Record = { 'tarot': `Provide a tarot reading for this question: ${userInput.question}`, 'numerology': `Analyze the numerological significance of ${userInput.fullname}, born on ${userInput.birthdate}`, 'pastlife': `Explore past life connections for ${userInput.name}. Recurring Experiences: ${userInput.recurringExperiences} Fears and Attractions: ${userInput.fearsAndAttractions} Natural Talents: ${userInput.naturalTalents}`, 'magic8ball': `Respond to this question: ${userInput.question}`, 'astrology': `Analyze the celestial influences for someone born on ${userInput.birthdate}${userInput.birthtime ? ` at ${userInput.birthtime}` : ''} in ${userInput.birthplace}`, 'oracle': `Interpret the oracle cards for: ${userInput.question}`, 'runes': `Cast the runes for: ${userInput.question}`, 'iching': `Consult the I Ching regarding: ${userInput.question}`, 'angelnumbers': `Interpret the significance of ${userInput.number} for ${userInput.name}`, 'horoscope': `Provide a detailed horoscope for ${userInput.zodiac}`, 'dreamanalysis': `Interpret this dream: ${userInput.dream}`, // Added dreamanalysis instead of 'dream' 'aura': `Read the aura and energy based on current feelings: ${userInput.feelings}`, // Remove duplicate 'aura' key since it was already defined above }; const prompt = prompts[readingType]; if (!prompt) { console.error('Missing prompt for reading type:', readingType); return { statusCode: 400, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ error: `Missing prompt for reading type: ${readingType}` }) }; } const completion = await openai.chat.completions.create({ model: process.env.NODE_ENV === 'production' ? "gpt-4" : "gpt-3.5-turbo", // Fix typo from "gpt-4o" messages: [ { role: "system", content: config.systemPrompt }, { role: "user", content: prompt } ], temperature: config.temperature, max_tokens: config.maxTokens }); // Update readings count for non-premium users if (!profile.is_premium) { const { error: updateError } = await supabaseClient .from('user_profiles') .update({ readings_count: currentReadingsCount + 1, free_readings_remaining: freeReadingsRemaining - 1, last_reading_date: new Date().toISOString() }) .eq('id', user.id); if (updateError) { console.error('Failed to update readings count:', updateError); } } const responseBody: { reading?: string; error?: string; readingsRemaining?: number | null } = { }; if (completion.choices && completion.choices[0] && completion.choices[0].message && completion.choices[0].message.content) { responseBody.reading = completion.choices[0].message.content.trim(); } else { console.error('No response received from OpenAI'); responseBody.error = 'No response received'; } if (!profile.is_premium) { responseBody.readingsRemaining = freeReadingsRemaining - 1; } else { responseBody.readingsRemaining = null; } const statusCode = 200; const headers = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }; return { statusCode, headers, body: JSON.stringify(responseBody) }; } catch (error: any) { console.error('Full OpenAI Error:', error); let errorMessage = 'Reading generation failed'; let statusCode = 500; let retryAfter: string | undefined = undefined; if (error instanceof Error) { console.error('OpenAI Error:', { message: error.message, ...(error as any).code ? {code: (error as any).code} : {}, ...(error as any).status ? {status: (error as any).status} : {}, stack: error.stack }); if (error.message.includes('API key')) { errorMessage = 'Invalid OpenAI API key'; } else if (typeof (error as any).status === 'number' && (error as any).status === 429) { statusCode = 429; errorMessage = 'Too many requests - please try again later'; retryAfter = '60'; } else if (error.message.includes('rate limit')) { statusCode = 429; errorMessage = 'Too many requests - please try again later'; retryAfter = '60'; } else { errorMessage = error.message; } } const headers: Record = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }; const responseBody: { error: string; retryAfter?: string } = { error: errorMessage }; if (retryAfter) { responseBody.retryAfter = retryAfter; headers['Retry-After'] = retryAfter; } return { statusCode, headers, body: JSON.stringify(responseBody) }; } } catch (error: unknown) { console.error('Outer error:', error); return { statusCode: 500, body: JSON.stringify({ error: 'An unexpected error occurred' }) }; } }; // Clean up expired rate limit entries periodically setInterval(() => { rateLimiter.cleanup(); }, 60000); export { handler }; ================================================ File: netlify/functions/utils/rateLimiter.ts ================================================ interface RateLimitEntry { count: number; timestamp: number; } class RateLimiter { private static instance: RateLimiter; private limits: Map; private readonly windowMs: number; private readonly maxRequests: number; private constructor(windowMs: number = 60_000, maxRequests: number = 5) { this.limits = new Map(); this.windowMs = windowMs; this.maxRequests = maxRequests; } public static getInstance(): RateLimiter { if (!RateLimiter.instance) { RateLimiter.instance = new RateLimiter(); } return RateLimiter.instance; } public isRateLimited(key: string): boolean { const now = Date.now(); const entry = this.limits.get(key); if (!entry) { this.limits.set(key, { count: 1, timestamp: now }); return false; } if (now - entry.timestamp > this.windowMs) { // Reset if window has passed this.limits.set(key, { count: 1, timestamp: now }); return false; } if (entry.count >= this.maxRequests) { return true; } entry.count++; return false; } public cleanup(): void { const now = Date.now(); for (const [key, entry] of this.limits.entries()) { if (now - entry.timestamp > this.windowMs) { this.limits.delete(key); } } } } export const rateLimiter = RateLimiter.getInstance(); ================================================ File: public/sw.js ================================================ const CACHE_NAME = 'mystic-balls-cache-v1'; const urlsToCache = [ '/', '/index.html', '/assets/index.css', // Add other static assets ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(urlsToCache)) ); }); ================================================ File: scripts/optimize-favicon.js ================================================ import sharp from 'sharp'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const inputPath = join(__dirname, '../public/MysticBalls-logo.png'); const outputPath = join(__dirname, '../public/favicon.png'); async function optimizeFavicon() { try { await sharp(inputPath) .resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }) .png() .toFile(outputPath); console.log('Favicon optimized successfully!'); } catch (error) { console.error('Error optimizing favicon:', error); } } optimizeFavicon(); ================================================ File: scripts/optimize-images.ts ================================================ import sharp from 'sharp'; import { glob } from 'glob'; import path from 'path'; import fs from 'fs/promises'; const SIZES = [320, 640, 768, 1024, 1280, 1536]; const FORMATS = ['webp', 'jpg'] as const; async function optimizeImages() { try { const images = await glob('public/**/*.{jpg,jpeg,png}'); for (const image of images) { const filename = path.parse(image).name; const directory = path.dirname(image); for (const format of FORMATS) { for (const width of SIZES) { const outputPath = path.join( directory, 'optimized', `${filename}-${width}.${format}` ); await fs.mkdir(path.dirname(outputPath), { recursive: true }); const pipeline = sharp(image).resize(width); if (format === 'webp') { await pipeline.webp({ quality: 80 }).toFile(outputPath); } else { await pipeline.jpeg({ quality: 80, progressive: true }).toFile(outputPath); } } } } console.log('Image optimization complete!'); } catch (error) { console.error('Error optimizing images:', error); } } optimizeImages(); ================================================ File: src/App.tsx ================================================ import { useState, useEffect, useCallback, useMemo, Suspense, lazy } from 'react'; import { useAuth } from './hooks/useAuth'; import { useAuthState } from './hooks/useAuthState'; import { READING_TYPES } from './data/readingTypes'; import Header from './components/Header'; import Footer from './components/Footer'; import ReadingSelector from './components/ReadingSelector'; import LoadingSpinner from './components/LoadingSpinner'; import { PricingPlan, ReadingType } from './types'; import { checkProject, supabaseClient } from './lib/supabaseClient'; import { UserProfile } from './services/supabase'; import PrivacyPolicy from './components/PrivacyPolicy'; import TermsOfService from './components/TermsOfService'; import TourGuide from './components/TourGuide'; import { ONBOARDING_STEPS } from './config/tutorial'; import { Step } from './types'; import { useUsageTracking } from './hooks/useUsageTracking'; import { fireConfetti } from './utils/confetti'; // Lazy load components const LoginModal = lazy(() => import('./components/LoginModal')); const UpgradeModal = lazy(() => import('./components/UpgradeModal')); const ReadingForm = lazy(() => import('./components/ReadingForm')); const ReadingOutput = lazy(() => import('./components/ReadingOutput')); const FAQ = lazy(() => import('./components/FAQ')); const App = (): JSX.Element => { const [isDarkMode, setIsDarkMode] = useState(() => { const savedMode = localStorage.getItem('darkMode'); return savedMode ? JSON.parse(savedMode) : true; }); const [selectedReadingType, setSelectedReadingType] = useState(null); const [showLoginModal, setShowLoginModal] = useState(false); const [showPaymentModal, setShowPaymentModal] = useState(false); const [profiles, setProfiles] = useState(null); const [currentPage, setCurrentPage] = useState(null); const [currentStep, setCurrentStep] = useState(() => ONBOARDING_STEPS.length > 0 ? ONBOARDING_STEPS[0] as Step : null ); const [readingOutput, setReadingOutput] = useState(null); const [isLoading, setIsLoading] = useState(false); const { user, loading: authLoading } = useAuthState(); const { signOut } = useAuth(); useUsageTracking(user?.id ?? null); const handleReadingSubmit = useCallback(async (formData: Record): Promise => { if (!user) { setShowLoginModal(true); return; } setIsLoading(true); setReadingOutput(null); try { const { data: { session } } = await supabaseClient.auth.getSession(); const response = await fetch('/.netlify/functions/getReading', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${session?.access_token}`, }, body: JSON.stringify({ readingType: selectedReadingType?.id, userInput: formData, }), }); if (!response.ok) { if (response.status === 402) { setShowPaymentModal(true); return; } throw new Error('Failed to get reading'); } const data = await response.json(); if (data.error) throw new Error(data.error); setReadingOutput(data.reading); fireConfetti(); if (!profiles?.[0]?.is_premium) { const { data: updatedProfile, error } = await supabaseClient .from('user_profiles') .select('*') .eq('id', user.id) .single(); if (!error && updatedProfile) setProfiles([updatedProfile]); } } catch (error) { console.error('Error getting reading:', error); setReadingOutput(error instanceof Error ? error.message : "There was an error getting your reading. Please try again."); } finally { setIsLoading(false); } }, [user, selectedReadingType, profiles, setShowLoginModal, setShowPaymentModal]); const handleSubscribe = useCallback(async (plan: PricingPlan) => { try { const response = await fetch('/.netlify/functions/create-checkout-session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ priceId: plan.stripePriceId, customerId: user?.id }) }); const result = await response.json(); if (result.error) throw new Error(result.error); if (result.url) { fireConfetti(); window.location.href = result.url; } else { throw new Error('No checkout URL returned'); } } catch (err) { console.error('Error creating checkout session:', err); throw err; } }, [user]); const nextStep = useCallback(() => { const currentIndex = ONBOARDING_STEPS.findIndex(step => step.id === currentStep?.id); if (currentIndex >= 0 && currentIndex < ONBOARDING_STEPS.length - 1) { setCurrentStep(ONBOARDING_STEPS[currentIndex + 1] as Step); } else { setCurrentStep(null); } }, [currentStep]); const handleReadingTypeSelect = useCallback((readingType: ReadingType) => { setSelectedReadingType(readingType); setReadingOutput(null); }, []); const handleDarkModeToggle = useCallback(() => { setIsDarkMode((prev: boolean) => !prev); // Add type annotation for prev }, []); useEffect(() => { localStorage.setItem('darkMode', JSON.stringify(isDarkMode)); }, [isDarkMode]); useEffect(() => { checkProject(); }, []); useEffect(() => { const fetchProfiles = async () => { try { const { data, error } = await supabaseClient .from('user_profiles') .select('*'); setProfiles(error ? null : data); } catch (err) { setProfiles(null); } }; fetchProfiles(); }, []); // Either use mainContent in the JSX or remove it if not needed const mainContent = useMemo(() => { if (currentPage === 'privacy') { return setCurrentPage(null)} />; } if (currentPage === 'terms') { return setCurrentPage(null)} />; } if (selectedReadingType) { return (
}> {readingOutput && (
)}
); } return (
); }, [currentPage, selectedReadingType, isDarkMode, handleReadingTypeSelect, readingOutput, isLoading, handleReadingSubmit]); // Add loading check back if (authLoading) { return (
); } return (

Welcome to Your Spiritual Journey

Explore ancient wisdom through our diverse collection of spiritual readings. Whether you seek guidance, clarity, or deeper understanding, our AI-powered insights combine traditional knowledge with modern technology to illuminate your path forward.

{mainContent} {/* Use mainContent here instead of the inline conditions */}
{!selectedReadingType && !currentPage && }
setCurrentPage('privacy')} onTermsClick={() => setCurrentPage('terms')} isDarkMode={isDarkMode} currentPage={currentPage} setCurrentPage={setCurrentPage} /> }> {showLoginModal && ( setShowLoginModal(false)} /> )} {showPaymentModal && ( setShowPaymentModal(false)} onSubscribe={handleSubscribe} /> )} {currentStep && ( setCurrentStep(null)} nextStep={nextStep} /> )}
); }; export default App; ================================================ File: src/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @keyframes glow { 0% { text-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff, 0 0 30px #ff00ff; } 50% { text-shadow: 0 0 20px #ff00ff, 0 0 30px #ff00ff, 0 0 40px #ff00ff, 0 0 50px #ff00ff; } 100% { text-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff, 0 0 30px #ff00ff; } } .glow-text { text-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff, 0 0 30px #ff00ff, 0 0 40px #ff00ff, 0 0 50px #ff00ff, 0 0 60px #ff00ff, 0 0 70px #ff00ff; animation: glow 2s ease-in-out infinite; } ================================================ File: src/main.tsx ================================================ /** @jsxImportSource react */ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; import './index.css'; const rootElement = document.getElementById('root'); if (!rootElement) { throw new Error('Failed to find the root element'); } const root = createRoot(rootElement); // Handle browser navigation without full page reload let isFirstLoad = true; window.addEventListener('popstate', (event) => { event.preventDefault(); if (!isFirstLoad) { // Update app state instead of reloading window.dispatchEvent(new CustomEvent('app:navigation')); } isFirstLoad = false; }); try { root.render( ); } catch (error) { console.error('Error rendering the app:', error); // Show error in the UI rootElement.innerHTML = `

Something went wrong

Please try refreshing the page. If the problem persists, contact support.

${error instanceof Error ? error.message : 'Unknown error'}
`; } ================================================ File: src/types.ts ================================================ import type { IconType } from 'react-icons'; import type { LucideIcon } from 'lucide-react'; export type ReadingTypeId = 'tarot' | 'numerology' | 'astrology' | 'oracle' | 'runes' | 'iching' | 'angelnumbers' | 'horoscope' | 'dreamanalysis' | 'magic8ball' | 'aura' | 'pastlife'; export interface ReadingType { id: ReadingTypeId; title: string; description: string; icon: LucideIcon | IconType; fields: ReadingField[]; isPremiumOnly?: boolean; } export interface ReadingField { name: string; type: 'text' | 'number' | 'date' | 'select' | 'textarea'; label: string; displayName: string; placeholder?: string; options?: string[]; required: boolean; } export interface UserUsage { readingsCount: number; readingsRemaining: number; isPremium: boolean; lastReadingDate?: Date | null; } export interface PaymentPlan { id: string; name: string; price: number; description: string; features: string[]; readingsPerMonth: number; } export interface PricingPlan extends PaymentPlan { stripePriceId: string; recommended?: boolean; } export interface Step { id: string; title: string; content: string; target: string; placement: 'top' | 'bottom' | 'left' | 'right'; } ================================================ File: src/vite-env.d.ts ================================================ /// ================================================ File: src/components/Advertisement.tsx ================================================ import React from 'react'; interface Props { isOpen: boolean; onClose: () => void; } const Advertisement: React.FC = () => { return null; // Component no longer needed }; export default Advertisement; ================================================ File: src/components/ApiKeyModal.tsx ================================================ import React, { useState } from 'react'; import { Key } from 'lucide-react'; import { setApiKey } from '../config/openai'; interface Props { isOpen: boolean; onClose: () => void; isDarkMode: boolean; } const ApiKeyModal: React.FC = ({ isOpen, onClose, isDarkMode }) => { const [apiKey, setApiKeyInput] = useState(''); if (!isOpen) return null; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setApiKey(apiKey); onClose(); }; return (

OpenAI API Key

setApiKeyInput(e.target.value)} className={`w-full p-3 rounded-lg ${ isDarkMode ? 'bg-indigo-800 border-indigo-700' : 'bg-white border-gray-300' } border focus:outline-none focus:ring-2 focus:ring-indigo-500`} placeholder="sk-..." required />
); }; export default ApiKeyModal; ================================================ File: src/components/AsyncComponent.tsx ================================================ import React, { Suspense } from 'react'; import LoadingSpinner from './LoadingSpinner'; import ErrorBoundary from './ErrorBoundary'; interface Props { children: React.ReactNode; fallback?: React.ReactNode; } const AsyncComponent: React.FC = ({ children, fallback }) => { return ( }> {children} ); }; export default AsyncComponent; ================================================ File: src/components/ErrorBoundary.tsx ================================================ import React, { Component, ErrorInfo, ReactNode } from 'react'; import { AlertTriangle } from 'lucide-react'; interface Props { children: ReactNode; fallback?: ReactNode; } interface State { hasError: boolean; error?: Error; errorInfo?: ErrorInfo; } class ErrorBoundary extends Component { public state: State = { hasError: false }; public static getDerivedStateFromError(error: Error): State { return { hasError: true, error }; } public componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('Uncaught error:', error); console.error('Component stack:', errorInfo.componentStack); this.setState({ error, errorInfo }); } private handleRetry = () => { this.setState({ hasError: false, error: undefined, errorInfo: undefined }); }; public render() { if (this.state.hasError) { return this.props.fallback || (

Oops! Something went wrong

{this.state.error?.message || 'An unexpected error occurred'}

); } try { return this.props.children; } catch (error) { console.error('Render error:', error); this.setState({ hasError: true, error: error as Error }); return null; } } } export default ErrorBoundary; ================================================ File: src/components/FAQ.tsx ================================================ import React from 'react'; interface Props { isDarkMode: boolean; } const FAQ: React.FC = ({ isDarkMode }) => { return (

How to Get the Best From Your Reading

Set Your Intention

Take a moment to center yourself and clearly focus on your question or area of concern. The more specific your intention, the more focused your reading will be.

Create Sacred Space

Find a quiet, comfortable place where you won't be disturbed. This helps create the right environment for receiving spiritual insights.

Stay Open

Approach your reading with an open mind and heart. Sometimes the guidance we receive isn't what we expect, but it's often what we need.

Frequently Asked Questions

How accurate are the readings?

Our readings combine traditional spiritual wisdom with advanced AI technology. While they provide valuable insights and guidance, remember that you have free will and the power to shape your path.

How often should I get a reading?

This varies by individual. Some find daily guidance helpful, while others prefer weekly or monthly readings. Listen to your intuition and seek guidance when you feel called to do so.

What if I don't understand my reading?

Take time to reflect on the messages received. Sometimes insights become clearer with time. You can also try journaling about your reading or discussing it with a trusted friend.

); }; export default FAQ; ================================================ File: src/components/Footer.tsx ================================================ import React from 'react'; import { Heart } from 'lucide-react'; interface Props { isDarkMode: boolean; onPrivacyClick: () => void; onTermsClick: () => void; currentPage: string | null; // Add currentPage prop setCurrentPage: React.Dispatch>; // Add setCurrentPage prop } const Footer: React.FC = ({ isDarkMode, onPrivacyClick, onTermsClick, currentPage, setCurrentPage }) => { return (
Disclaimer: Mystic Balls is an AI-powered divination tool that integrates traditional methods such as Tarot, Numerology, Astrology, Oracle Cards, Runes, I Ching, Aura Reading, Past Life analysis, Angel Numbers, Daily Horoscope, Dream Analysis, and Magic 8 Ball with modern artificial intelligence. The insights and guidance provided by Mystic Balls are intended for entertainment and personal reflection purposes only. They do not constitute professional advice, nor are they guaranteed to be accurate, complete, or up-to-date. Users are encouraged to exercise their own judgment and seek professional counsel when making any important personal, financial, or medical decisions. By using Mystic Balls, you acknowledge that the interpretations, predictions, and guidance generated by the tool are subjective in nature, and the company assumes no liability for any decisions made or actions taken based on the provided content.
Made with by Mystic Insights
); }; export default Footer; ================================================ File: src/components/FreeTrialPrompt.tsx ================================================ import React from 'react'; import { Sparkles, X } from 'lucide-react'; import { PAYMENT_PLANS } from '../config/plans'; interface Props { isOpen: boolean; onClose: () => void; onStartTrial: () => void; isDarkMode: boolean; } const FreeTrialPrompt: React.FC = ({ isOpen, onClose, onStartTrial, isDarkMode }) => { if (!isOpen) return null; const premiumPlan = PAYMENT_PLANS.find(plan => plan.id === 'premium'); return (
{/* Background decoration */}
{/* Close button */}
{/* Header */}

Unlock Unlimited Readings

You've used all your free readings. Continue your spiritual journey with our premium features!

{/* Features */}
    {premiumPlan?.features.map((feature, index) => (
  • {feature}
  • ))}
{/* Trial offer */}
24 Hours Free

Then ${premiumPlan?.price}/month. Cancel anytime.

{/* Action buttons */}
); }; export default FreeTrialPrompt; ================================================ File: src/components/Header.tsx ================================================ import React from 'react'; import { User } from '@supabase/supabase-js'; import { UserProfile } from '../services/supabase'; import { Moon, Sun } from 'lucide-react'; interface HeaderProps { user: User | null; isDarkMode: boolean; onDarkModeToggle: () => void; onSignOut: () => Promise<{ success: boolean }>; userProfile?: UserProfile; } const Header: React.FC = ({ user, isDarkMode, onDarkModeToggle, onSignOut, userProfile }) => { return (

Mystic Balls

{user && (
{user.email} {userProfile && ( {userProfile.is_premium ? 'Premium Member' : `${userProfile.free_readings_remaining ?? 5} free readings remaining`} )}
)}
); }; export default Header; ================================================ File: src/components/LoadingSpinner.tsx ================================================ /** @jsxImportSource react */ import { useState, useEffect } from 'react'; import type { FC } from 'react'; interface LoadingSpinnerProps { message?: string; size?: 'small' | 'medium' | 'large'; showSlowLoadingMessage?: boolean; className?: string; } const LoadingSpinner: FC = ({ message = 'Loading...', size = 'medium', showSlowLoadingMessage = true, className = '' }) => { const [showMessage, setShowMessage] = useState(false); useEffect(() => { if (!showSlowLoadingMessage) return; const timer = setTimeout(() => { setShowMessage(true); }, 5000); return () => clearTimeout(timer); }, [showSlowLoadingMessage]); const sizeClasses = { small: 'w-6 h-6 border-2', medium: 'w-12 h-12 border-4', large: 'w-16 h-16 border-4' }; return (
{message && (

{message}

{showMessage && showSlowLoadingMessage && (

This is taking longer than expected.
Please wait a moment...

)}
)}
); }; export default LoadingSpinner; ================================================ File: src/components/LoginModal.tsx ================================================ import React, { useState, useEffect } from 'react'; import type { FC } from 'react'; import { useAuth } from '../hooks/useAuth'; import { signInWithGoogle } from '../services/supabase'; import { supabaseClient } from '../lib/supabaseClient'; // Add this import interface Props { isOpen: boolean; onClose: () => void; } const LoginModal: FC = ({ isOpen, onClose }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [isSignUp, setIsSignUp] = useState(false); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const { signIn, signUp, loading: authLoading, confirmEmail } = useAuth(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (isLoading || authLoading) return; setError(null); setIsLoading(true); try { if (!email || !password) { throw new Error('Please enter both email and password'); } if (password.length < 6) { throw new Error('Password must be at least 6 characters long'); } if (isSignUp) { await signUp(email, password); // Don't close modal, wait for confirmation screen } else { await signIn(email, password); // Close modal on successful sign in onClose(); } } catch (err: unknown) { console.error('Auth error:', err); const authErrorMessage = 'An error occurred during authentication'; if (err instanceof Error) { if (err.message?.toLowerCase().includes('already registered')) { setError('This email is already registered. Please sign in instead.'); setIsSignUp(false); // Switch to sign in mode } else if (err.message?.toLowerCase().includes('invalid login credentials')) { setError('Invalid email or password. Please try again.'); } else if (err.message?.toLowerCase().includes('email link is invalid or has expired')) { setError('The confirmation link is invalid or has expired. Please try signing up again.'); } else { setError(err.message); } } else { console.error('Unknown error:', err); setError(authErrorMessage); } } finally { setIsLoading(false); } }; const handleGoogleSignIn = async () => { if (isLoading || authLoading) return; setError(null); setIsLoading(true); try { const { error } = await signInWithGoogle(); if (error) throw error; // Close modal on successful Google sign in onClose(); } catch (err: unknown) { console.error('Google sign in error:', err); const googleErrorMessage = 'Failed to sign in with Google'; if (err instanceof Error) { setError(err.message || googleErrorMessage); } else { console.error('Unknown error:', err); setError(googleErrorMessage); } setIsLoading(false); } }; // Close modal if user is authenticated // Update the checkUser function to use supabaseClient React.useEffect(() => { const checkUser = async () => { const { data: { user } } = await supabaseClient.auth.getUser(); // Only close if we have a successful auth AND no errors if (!isLoading && !error && !confirmEmail && user) { onClose(); } }; checkUser(); }, [isLoading, error, confirmEmail, onClose]); useEffect(() => { if (!isOpen) { // Reset state when modal closes setEmail(''); setPassword(''); setError(null); setIsLoading(false); setIsSignUp(false); } }, [isOpen]); if (!isOpen) return null; return (

{isSignUp ? 'Create Account' : 'Welcome Back'}

{isSignUp ? 'Sign up to start your mystical journey' : 'Sign in to continue your journey'}

{error && (
{error}
)}
setEmail(e.target.value)} className="w-full px-4 py-2 rounded-lg bg-indigo-900/50 border border-indigo-700 text-white placeholder-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Enter your email" required />
setPassword(e.target.value)} className="w-full px-4 py-2 rounded-lg bg-indigo-900/50 border border-indigo-700 text-white placeholder-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Enter your password" required />
Or continue with
); }; export default LoginModal; ================================================ File: src/components/MainContent.tsx ================================================ import React, { Suspense, lazy } from 'react'; import { ReadingType } from '../types'; import LoadingSpinner from './LoadingSpinner'; import ReadingSelector from './ReadingSelector'; import { READING_TYPES } from '../data/readingTypes'; // Lazy load components const ReadingForm = lazy(() => import('./ReadingForm')); const ReadingOutput = lazy(() => import('./ReadingOutput')); interface MainContentProps { selectedReadingType: ReadingType | null; isDarkMode: boolean; handleReadingSubmit: (formData: Record) => Promise; readingOutput: string | null; isLoading: boolean; setSelectedReadingType: (type: ReadingType | null) => void; } export const MainContent: React.FC = ({ selectedReadingType, isDarkMode, handleReadingSubmit, readingOutput, isLoading, setSelectedReadingType }) => { return (
{selectedReadingType ? (
}> {isLoading ? ( ) : ( <> {readingOutput && ( )} )}
) : ( )}
); }; ================================================ File: src/components/OnboardingOverlay.tsx ================================================ import React, { useState, useEffect } from 'react'; import { X } from 'lucide-react'; interface Step { target: string; title: string; content: string; position: 'top' | 'bottom' | 'left' | 'right'; } interface Props { steps: Step[]; isOpen: boolean; onComplete: () => void; isDarkMode: boolean; } const OnboardingOverlay: React.FC = ({ steps, isOpen, onComplete, isDarkMode }) => { const [currentStep, setCurrentStep] = useState(0); const [position, setPosition] = useState({ top: 0, left: 0 }); useEffect(() => { if (isOpen && steps[currentStep]) { const element = document.querySelector(steps[currentStep].target); if (element) { const rect = element.getBoundingClientRect(); const viewportHeight = window.innerHeight; const tooltipHeight = 160; // Approximate height of tooltip const tooltipWidth = 300; // Width of tooltip // Center in viewport if no element is found let top = Math.max(20, (viewportHeight - tooltipHeight) / 2); let left = (window.innerWidth - tooltipWidth) / 2; if (rect.height > 0) { // If element exists and is visible, position relative to it switch (steps[currentStep].position) { case 'top': top = rect.top - tooltipHeight - 10; left = rect.left + (rect.width - tooltipWidth) / 2; break; case 'bottom': top = rect.bottom + 10; left = rect.left + (rect.width - tooltipWidth) / 2; break; case 'left': top = rect.top + (rect.height - tooltipHeight) / 2; left = rect.left - tooltipWidth - 10; break; case 'right': top = rect.top + (rect.height - tooltipHeight) / 2; left = rect.right + 10; break; } } // Ensure tooltip stays within viewport top = Math.max(20, Math.min(top, viewportHeight - tooltipHeight - 20)); left = Math.max(20, Math.min(left, window.innerWidth - tooltipWidth - 20)); setPosition({ top: top + window.scrollY, left: left + window.scrollX }); } } }, [currentStep, isOpen, steps]); if (!isOpen) return null; const handleNext = () => { if (currentStep < steps.length - 1) { setCurrentStep(currentStep + 1); } else { onComplete(); } }; const step = steps[currentStep]; return ( <>

{step.title}

{step.content}

{steps.map((_, index) => ( ))}
); }; export default OnboardingOverlay; ================================================ File: src/components/OptimizedImage.tsx ================================================ import React, { useState, useEffect } from 'react'; interface OptimizedImageProps { src: string; alt: string; sizes?: string; className?: string; loading?: 'lazy' | 'eager'; } const OptimizedImage: React.FC = ({ src, alt, sizes = '100vw', className = '', loading = 'lazy' }) => { const [imageSrc, setImageSrc] = useState(src); const [isWebpSupported, setIsWebpSupported] = useState(true); useEffect(() => { // Check WebP support const checkWebP = async () => { const webP = new Image(); webP.src = 'data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=='; const supported = await new Promise((resolve) => { webP.onload = () => resolve(true); webP.onerror = () => resolve(false); }); setIsWebpSupported(!!supported); }; checkWebP(); }, []); // Generate srcset for different sizes const generateSrcSet = () => { const widths = [320, 640, 768, 1024, 1280, 1536]; const extension = isWebpSupported ? 'webp' : 'jpg'; return widths .map(width => { const imgPath = src.replace(/\.(jpg|jpeg|png)$/, `.${extension}`); return `${imgPath}?w=${width} ${width}w`; }) .join(', '); }; return ( {alt} { // Fallback to original format if WebP fails if (isWebpSupported) { setIsWebpSupported(false); setImageSrc(src); } }} /> ); }; export default OptimizedImage; ================================================ File: src/components/PaymentModal.tsx ================================================ import React, { useState, useEffect } from 'react'; import { User } from '@supabase/supabase-js'; import { PricingPlan } from '../types'; import { PAYMENT_PLANS } from '../config/plans'; import LoadingSpinner from './LoadingSpinner'; import { Check } from 'lucide-react'; interface PaymentModalProps { isOpen: boolean; isDarkMode: boolean; onClose: () => void; user: User | null; onSubscribe: (plan: PricingPlan) => Promise; } export const PaymentModal: React.FC = ({ isOpen, isDarkMode, onClose, user, onSubscribe }) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [selectedPlan, setSelectedPlan] = useState(null); const handleSubscribe = async (plan: PricingPlan) => { if (!user) { setError('Please sign in to subscribe'); return; } setSelectedPlan(plan); setIsLoading(true); setError(null); try { await onSubscribe(plan); } catch (err) { console.error('Subscription error:', err); setError( err instanceof Error ? err.message : 'Failed to process subscription. Please try again.' ); setSelectedPlan(null); } finally { setIsLoading(false); } }; // Reset state when modal closes useEffect(() => { if (!isOpen) { setError(null); setIsLoading(false); setSelectedPlan(null); } }, [isOpen]); if (!isOpen) return null; return (

Upgrade Your Spiritual Journey

Unlock unlimited readings and premium features!

{error && (
{error}
)}
{PAYMENT_PLANS.map((plan) => (

{plan.name}

${plan.price} /month

{plan.description}

    {plan.features.map((feature, index) => (
  • {feature}
  • ))}
))}
); }; export default PaymentModal; ================================================ File: src/components/PaymentSuccess.tsx ================================================ import React, { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import confetti from 'canvas-confetti'; const PaymentSuccess: React.FC = () => { const navigate = useNavigate(); useEffect(() => { // Celebrate with confetti! confetti({ particleCount: 100, spread: 70, origin: { y: 0.6 } }); // Redirect after 3 seconds const timer = setTimeout(() => { navigate('/dashboard'); }, 3000); return () => clearTimeout(timer); }, [navigate]); return (

Payment Successful!

Thank you for upgrading. You will be redirected shortly...

); }; export default PaymentSuccess; ================================================ File: src/components/PremiumBadge.tsx ================================================ import React from 'react'; import { Sparkles } from 'lucide-react'; interface Props { className?: string; } const PremiumBadge: React.FC = ({ className = '' }) => { return (
Premium
); }; export default PremiumBadge; ================================================ File: src/components/PrivacyPolicy.tsx ================================================ import React from 'react'; interface Props { isDarkMode: boolean; onBack: () => void; } const PrivacyPolicy: React.FC = ({ isDarkMode, onBack }) => { return (

Privacy Policy

1. Information We Collect

We collect information that you provide directly to us, including:

  • Email address and password when you create an account
  • Usage data related to your readings and interactions with our services
  • Payment information when you subscribe to our premium services
  • Communications you send to us

2. How We Use Your Information

We use the information we collect to:

  • Provide, maintain, and improve our services
  • Process your transactions and manage your account
  • Send you technical notices, updates, and support messages
  • Respond to your comments and questions
  • Protect against fraudulent or illegal activity

3. Data Security

We implement appropriate technical and organizational security measures to protect your personal information. However, no security system is impenetrable and we cannot guarantee the security of our systems 100%.

4. Your Rights

You have the right to:

  • Access your personal information
  • Correct inaccurate or incomplete information
  • Request deletion of your personal information
  • Object to our processing of your information
  • Receive your information in a structured, commonly used format

5. Contact Us

If you have any questions about this Privacy Policy, please contact us at privacy@mysticballs.com

6. Changes to This Policy

We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last Updated" date.

Last Updated: February 8, 2025

); }; export default PrivacyPolicy; ================================================ File: src/components/ReadingCard.tsx ================================================ import React from 'react'; import { ReadingType } from '../types'; import PremiumBadge from './PremiumBadge'; import { useAuthState } from '../hooks/useAuthState'; interface Props { reading: ReadingType; onClick: () => void; isDarkMode: boolean; isSelected?: boolean; } const ReadingCard: React.FC = ({ reading, onClick, isDarkMode, isSelected }) => { const { profiles } = useAuthState(); const Icon = reading.icon; const isAccessible = profiles?.[0]?.is_premium || (profiles?.[0]?.free_readings_remaining ?? 0) > 0; return ( ); }; export default ReadingCard; ================================================ File: src/components/ReadingDisplay.tsx ================================================ import React from 'react'; import { ReadingType } from '../types'; import { ArrowLeft } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; interface Props { reading: string; readingType: ReadingType; onBack: () => void; readingsRemaining: number | null; } const ReadingDisplay: React.FC = ({ reading, readingType, onBack, readingsRemaining }) => { const readingTitle = `Your ${readingType.title}`; return (

{readingTitle}

{reading}
{readingsRemaining !== null && (

Readings remaining: {readingsRemaining}

)}
); }; export default ReadingDisplay; ================================================ File: src/components/ReadingForm.tsx ================================================ import { useState } from 'react'; import type { ReadingType, ReadingField } from '../types'; interface Props { readingType: ReadingType; onSubmit: (formData: Record) => Promise; isDarkMode?: boolean; } export const ReadingForm = ({ readingType, onSubmit, isDarkMode = true }: Props) => { const [formData, setFormData] = useState>({}); const handleChange = (field: ReadingField, value: string) => { setFormData(prev => ({ ...prev, [field.name]: value })); }; const baseInputClasses = isDarkMode ? 'bg-indigo-800/50 border-indigo-700 text-white placeholder-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500'; const [isLoading, setIsLoading] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { setIsLoading(true); await onSubmit(formData); } catch (error) { console.error('Error submitting form:', error); } finally { setIsLoading(false); } }; return (

{readingType.title}

{readingType.fields?.map((field) => (
{field.type === 'textarea' ? (