React Integration
Build astrology components with React
Setup
# Create React app with TypeScript
npx create-react-app my-astro-app --template typescript
# Install SDK
npm install @vedika/sdk
Security Note: Never expose your API key in client-side React code. Always use a backend proxy or Next.js API routes.
Backend Proxy Pattern
// server/api.js (Express backend)
import express from 'express';
import { VedikaClient } from '@vedika/sdk';
const app = express();
const vedika = new VedikaClient(); // Reads from env
app.post('/api/chart', async (req, res) => {
try {
const chart = await vedika.birthChart(req.body);
res.json(chart);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// React fetches from your backend, not Vedika directly
Custom Hook: useVedika
// hooks/useVedika.ts
import { useState, useCallback } from 'react';
interface BirthDetails {
datetime: string;
latitude: number;
longitude: number;
}
interface BirthChart {
sunSign: string;
moonSign: string;
ascendant: string;
planets: any[];
}
export function useVedika() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchChart = useCallback(async (birthDetails: BirthDetails): Promise<BirthChart | null> => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/chart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(birthDetails)
});
if (!response.ok) {
throw new Error('Failed to fetch chart');
}
return await response.json();
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
return null;
} finally {
setLoading(false);
}
}, []);
return { fetchChart, loading, error };
}
Birth Chart Component
// components/BirthChartForm.tsx
import { useState } from 'react';
import { useVedika } from '../hooks/useVedika';
interface BirthChartFormProps {
onChartGenerated: (chart: any) => void;
}
export function BirthChartForm({ onChartGenerated }: BirthChartFormProps) {
const [date, setDate] = useState('');
const [time, setTime] = useState('');
const [lat, setLat] = useState('');
const [lng, setLng] = useState('');
const { fetchChart, loading, error } = useVedika();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const chart = await fetchChart({
datetime: `${date}T${time}:00+05:30`,
latitude: parseFloat(lat),
longitude: parseFloat(lng)
});
if (chart) {
onChartGenerated(chart);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium">Date of Birth</label>
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300"
required
/>
</div>
<div>
<label className="block text-sm font-medium">Time of Birth</label>
<input
type="time"
value={time}
onChange={(e) => setTime(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300"
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium">Latitude</label>
<input
type="number"
step="0.0001"
value={lat}
onChange={(e) => setLat(e.target.value)}
placeholder="28.6139"
className="mt-1 block w-full rounded-md border-gray-300"
required
/>
</div>
<div>
<label className="block text-sm font-medium">Longitude</label>
<input
type="number"
step="0.0001"
value={lng}
onChange={(e) => setLng(e.target.value)}
placeholder="77.2090"
className="mt-1 block w-full rounded-md border-gray-300"
required
/>
</div>
</div>
{error && (
<div className="text-red-600 text-sm">{error}</div>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 disabled:opacity-50"
>
{loading ? 'Generating...' : 'Generate Chart'}
</button>
</form>
);
}
Chart Display Component
// components/ChartDisplay.tsx
interface ChartDisplayProps {
chart: {
sunSign: string;
moonSign: string;
ascendant: string;
planets: Array<{
name: string;
sign: string;
degree: number;
house: number;
}>;
};
}
export function ChartDisplay({ chart }: ChartDisplayProps) {
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold mb-4">Your Birth Chart</h2>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="text-center p-4 bg-yellow-50 rounded-lg">
<div className="text-sm text-gray-500">Sun Sign</div>
<div className="text-xl font-bold">{chart.sunSign}</div>
</div>
<div className="text-center p-4 bg-blue-50 rounded-lg">
<div className="text-sm text-gray-500">Moon Sign</div>
<div className="text-xl font-bold">{chart.moonSign}</div>
</div>
<div className="text-center p-4 bg-purple-50 rounded-lg">
<div className="text-sm text-gray-500">Ascendant</div>
<div className="text-xl font-bold">{chart.ascendant}</div>
</div>
</div>
<h3 className="text-lg font-semibold mb-3">Planetary Positions</h3>
<table className="w-full">
<thead>
<tr className="border-b">
<th className="text-left py-2">Planet</th>
<th className="text-left py-2">Sign</th>
<th className="text-left py-2">Degree</th>
<th className="text-left py-2">House</th>
</tr>
</thead>
<tbody>
{chart.planets.map((planet) => (
<tr key={planet.name} className="border-b">
<td className="py-2">{planet.name}</td>
<td className="py-2">{planet.sign}</td>
<td className="py-2">{planet.degree.toFixed(2)}</td>
<td className="py-2">{planet.house}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Streaming Chat Component
// components/AstrologyChat.tsx
import { useState, useRef, useEffect } from 'react';
interface Message {
role: 'user' | 'assistant';
content: string;
}
interface AstrologyChatProps {
birthDetails: {
datetime: string;
latitude: number;
longitude: number;
};
}
export function AstrologyChat({ birthDetails }: AstrologyChatProps) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [streaming, setStreaming] = useState(false);
const [currentResponse, setCurrentResponse] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(scrollToBottom, [messages, currentResponse]);
const sendMessage = async () => {
if (!input.trim() || streaming) return;
const userMessage = input.trim();
setInput('');
setMessages(prev => [...prev, { role: 'user', content: userMessage }]);
setStreaming(true);
setCurrentResponse('');
try {
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question: userMessage,
birthDetails
})
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'text') {
fullResponse += data.text;
setCurrentResponse(fullResponse);
}
} catch {}
}
}
}
setMessages(prev => [...prev, { role: 'assistant', content: fullResponse }]);
} catch (error) {
setMessages(prev => [...prev, {
role: 'assistant',
content: 'Sorry, I encountered an error. Please try again.'
}]);
} finally {
setStreaming(false);
setCurrentResponse('');
}
};
return (
<div className="flex flex-col h-96 border rounded-lg">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((msg, i) => (
<div
key={i}
className={`p-3 rounded-lg ${
msg.role === 'user'
? 'bg-indigo-100 ml-auto max-w-[80%]'
: 'bg-gray-100 mr-auto max-w-[80%]'
}`}
>
{msg.content}
</div>
))}
{streaming && currentResponse && (
<div className="bg-gray-100 mr-auto max-w-[80%] p-3 rounded-lg">
{currentResponse}
<span className="animate-pulse">|</span>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div className="border-t p-4">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Ask about your chart..."
className="flex-1 border rounded-lg px-4 py-2"
disabled={streaming}
/>
<button
onClick={sendMessage}
disabled={streaming || !input.trim()}
className="bg-indigo-600 text-white px-4 py-2 rounded-lg disabled:opacity-50"
>
Send
</button>
</div>
</div>
</div>
);
}