Document Version: 1.0.0 Last Updated: 2026-01-21 Maintained By: Development Team Status: Active
- Overview
- Database API
- AI Services API
- Authentication API
- Third-Party Integrations
- Integration Guide
- Code Examples
- Error Handling
- Best Practices
This document provides comprehensive API documentation for Solus, covering database operations, AI services, authentication, and third-party integrations. It serves as a reference for developers working on the codebase or integrating with Solus systems.
Solus uses a service layer pattern where all business logic and external API calls are abstracted into service modules:
Component (UI)
↓
Service Layer (db/, lib/ai/, hooks/)
↓
External APIs (Firebase, Gemini, etc.)
All imports use the @/ alias for cleaner code:
import { createQuickDecision } from '@/db/Decision/Quick/quickDecisionDb';
import { generateRecommendation } from '@/lib/ai/quickDecisionService';
import { useAuth } from '@/hooks/useAuth';Location: src/db/User/userDb.ts
Creates a new user profile in Firestore.
function createUser(userData: Omit<User, 'id'>): Promise<User>Parameters:
userData: User object withoutid(auto-generated)
Returns: Promise<User> - Created user with generated ID
Example:
import { createUser } from '@/db/User/userDb';
const newUser = await createUser({
email: 'sarah@example.com',
displayName: 'Sarah',
photoURL: '',
subscriptionTier: 'free',
preferences: {
theme: 'light',
notifications: true,
},
createdAt: new Date(),
updatedAt: new Date(),
});
console.log(newUser.id); // Auto-generated Firebase IDThrows:
- Firebase errors (permission denied, network issues, etc.)
Retrieves a user profile by ID.
function getUser(userId: string): Promise<User | null>Parameters:
userId: Firebase UID
Returns: Promise<User | null> - User object or null if not found
Example:
const user = await getUser('abc123');
if (user) {
console.log(user.email);
} else {
console.log('User not found');
}Updates a user profile.
function updateUser(userId: string, updates: Partial<User>): Promise<void>Parameters:
userId: Firebase UIDupdates: Partial user object with fields to update
Returns: Promise<void>
Example:
await updateUser('abc123', {
displayName: 'Sarah Johnson',
subscriptionTier: 'premium',
updatedAt: new Date(),
});Note: updatedAt is automatically set by the database layer.
Deletes a user profile and all associated data.
function deleteUser(userId: string): Promise<void>Parameters:
userId: Firebase UID
Returns: Promise<void>
Example:
await deleteUser('abc123');
// User and all decisions/reflections are permanently deletedWarning: This operation is irreversible. Consider implementing a soft delete for production.
Location: src/db/Decision/Quick/quickDecisionDb.ts
Creates a new quick decision.
function createQuickDecision(
decision: Omit<QuickDecision, 'id' | 'createdAt' | 'updatedAt'>
): Promise<QuickDecision>Parameters:
decision: QuickDecision object without auto-generated fields
Returns: Promise<QuickDecision> - Created decision with generated ID and timestamps
Example:
import { createQuickDecision } from '@/db/Decision/Quick/quickDecisionDb';
import { DecisionCategory, DecisionStatus } from '@/db/types/BaseDecision';
const decision = await createQuickDecision({
type: 'quick',
userId: 'abc123',
title: 'What should I eat for lunch?',
category: DecisionCategory.FOOD,
status: DecisionStatus.COMPLETED,
options: [
{
id: '1',
text: 'Salad',
selected: false,
pros: ['Healthy', 'Light'],
cons: ['Not filling'],
},
{
id: '2',
text: 'Burrito',
selected: true,
pros: ['Filling', 'Tasty'],
cons: ['Heavy', 'Expensive'],
},
],
contextFactors: ['Trying to eat healthy', 'Very hungry'],
gutFeeling: 'I really want the burrito',
recommendation: {
optionId: '2',
reasoning: 'Given that you\'re very hungry, the burrito will provide more satisfaction.',
confidence: 0.85,
},
selectedOption: 'Burrito',
});Throws:
- Firebase errors
- Validation errors (if schema is invalid)
Retrieves a quick decision by ID.
function getQuickDecision(id: string): Promise<QuickDecision | null>Parameters:
id: Decision ID
Returns: Promise<QuickDecision | null> - Decision object or null if not found
Example:
const decision = await getQuickDecision('decision123');
if (decision && decision.type === 'quick') {
console.log(decision.title);
console.log(decision.options);
}Updates a quick decision.
function updateQuickDecision(
id: string,
updates: Partial<QuickDecision>
): Promise<void>Parameters:
id: Decision IDupdates: Partial decision object with fields to update
Returns: Promise<void>
Example:
await updateQuickDecision('decision123', {
selectedOption: 'Salad',
status: DecisionStatus.IMPLEMENTED,
});Deletes a quick decision.
function deleteQuickDecision(id: string): Promise<void>Parameters:
id: Decision ID
Returns: Promise<void>
Example:
await deleteQuickDecision('decision123');Lists quick decisions for a user with optional filtering.
function listQuickDecisions(
userId: string,
options?: {
category?: DecisionCategory;
status?: DecisionStatus;
limit?: number;
startAfter?: DocumentSnapshot;
}
): Promise<QuickDecision[]>Parameters:
userId: User IDoptions: Optional filterscategory: Filter by decision categorystatus: Filter by decision statuslimit: Max number of results (default: 20)startAfter: Document snapshot for pagination
Returns: Promise<QuickDecision[]> - Array of decisions
Example:
// Get all food decisions
const foodDecisions = await listQuickDecisions('abc123', {
category: DecisionCategory.FOOD,
limit: 10,
});
// Get completed decisions
const completed = await listQuickDecisions('abc123', {
status: DecisionStatus.COMPLETED,
});
// Pagination
const firstPage = await listQuickDecisions('abc123', { limit: 20 });
const lastDoc = firstPage[firstPage.length - 1];
const secondPage = await listQuickDecisions('abc123', {
limit: 20,
startAfter: lastDoc,
});Location: src/db/Decision/Deep/deepDecisionDb.ts
Deep decision operations follow the same pattern as quick decisions:
createDeepDecision(decision)getDeepDecision(id)updateDeepDecision(id, updates)deleteDeepDecision(id)listDeepDecisions(userId, options)
Key Difference: Deep decisions have additional fields:
interface DeepDecision extends BaseDecision {
type: 'deep';
values: string[]; // User's personal values
longTermGoals: string[]; // Long-term goals
stakeholders: string[]; // People affected by decision
prosConsAnalysis?: ProsConsAnalysis; // Weighted analysis
futureScenarios?: FutureScenario[]; // "What if" scenarios
cognitiveBiases?: string[]; // Identified biases
}Location: src/db/Decision/decisionDb.ts
Retrieves any decision (quick or deep) by ID.
function getDecision(
id: string,
type?: 'quick' | 'deep'
): Promise<Decision | null>Parameters:
id: Decision IDtype: Optional decision type hint (improves performance)
Returns: Promise<Decision | null> - Union type of QuickDecision or DeepDecision
Example:
import { getDecision } from '@/db/Decision/decisionDb';
const decision = await getDecision('decision123');
if (decision) {
// Type narrowing with discriminator
if (decision.type === 'quick') {
console.log(decision.options); // TypeScript knows this is QuickDecision
} else if (decision.type === 'deep') {
console.log(decision.values); // TypeScript knows this is DeepDecision
}
}Location: src/db/Reflection/reflectionDb.ts
Creates a post-decision reflection.
function createReflection(
reflection: Omit<Reflection, 'id' | 'createdAt' | 'updatedAt'>
): Promise<Reflection>Example:
import { createReflection } from '@/db/Reflection/reflectionDb';
const reflection = await createReflection({
decisionId: 'decision123',
userId: 'abc123',
decisionType: 'quick',
outcomeRating: 5,
notes: 'Great choice! The burrito was delicious and I felt satisfied.',
wouldChooseAgain: true,
lessonsLearned: 'When I\'m very hungry, go with the more filling option.',
});Retrieves all reflections for a specific decision.
function getReflectionsForDecision(decisionId: string): Promise<Reflection[]>Location: src/db/Dashboard/dashboardDb.ts
Retrieves user statistics for dashboard.
function getDashboardStats(userId: string): Promise<DashboardStats>Returns:
interface DashboardStats {
totalDecisions: number;
quickDecisions: number;
deepDecisions: number;
decisionsThisWeek: number;
decisionsThisMonth: number;
averageOutcomeRating: number;
topCategory: DecisionCategory;
completionRate: number;
}Example:
import { getDashboardStats } from '@/db/Dashboard/dashboardDb';
const stats = await getDashboardStats('abc123');
console.log(`Total decisions: ${stats.totalDecisions}`);
console.log(`Completion rate: ${stats.completionRate}%`);
console.log(`Top category: ${stats.topCategory}`);Location: src/lib/ai/quickDecisionService.ts
Extracts structured decision data from natural language input.
function extractDecisionOptions(userInput: string): Promise<ExtractedDecision>Parameters:
userInput: Natural language description of decision
Returns: Promise<ExtractedDecision>
interface ExtractedDecision {
title: string;
category: DecisionCategory;
options: Array<{
text: string;
pros: string[];
cons: string[];
}>;
contextFactors?: string[];
gutFeeling?: string;
}Example:
import { extractDecisionOptions } from '@/lib/ai/quickDecisionService';
const input = `
I'm trying to decide between getting a salad or a burrito bowl for lunch.
I'm trying to eat healthy but I'm really hungry and the burrito sounds good.
The salad is lighter but might not fill me up.
`;
const extracted = await extractDecisionOptions(input);
console.log(extracted);
// {
// title: "Lunch choice",
// category: DecisionCategory.FOOD,
// options: [
// {
// text: "Salad",
// pros: ["Healthy", "Light"],
// cons: ["Might not be filling"]
// },
// {
// text: "Burrito bowl",
// pros: ["Filling", "Tasty"],
// cons: ["Less healthy", "Heavier"]
// }
// ],
// contextFactors: ["Trying to eat healthy", "Very hungry"],
// gutFeeling: "The burrito sounds good"
// }Throws:
AIServiceErrorif API call failsValidationErrorif response doesn't match schema
Generates an AI recommendation for a decision.
function generateRecommendation(
title: string,
options: Option[],
contextFactors: string[],
gutFeeling?: string
): Promise<Recommendation>Parameters:
title: Decision titleoptions: Array of options with pros/conscontextFactors: Contextual informationgutFeeling: Optional user intuition
Returns: Promise<Recommendation>
interface Recommendation {
recommendedOption: string;
reasoning: string;
confidence: number; // 0-1
alternativeConsideration?: string;
}Example:
import { generateRecommendation } from '@/lib/ai/quickDecisionService';
const recommendation = await generateRecommendation(
'Lunch choice',
[
{
id: '1',
text: 'Salad',
pros: ['Healthy', 'Light'],
cons: ['Not filling'],
},
{
id: '2',
text: 'Burrito',
pros: ['Filling', 'Tasty'],
cons: ['Heavy'],
},
],
['Trying to eat healthy', 'Very hungry'],
'I really want the burrito'
);
console.log(recommendation);
// {
// recommendedOption: "Burrito",
// reasoning: "Given that you're very hungry, the burrito will provide...",
// confidence: 0.85,
// alternativeConsideration: "If health is a priority, consider the salad..."
// }Graceful Fallback: If structured output fails, falls back to text generation, then to simple heuristics.
Processes voice input into a decision.
function processSpeechInput(transcript: string): Promise<ProcessedDecision>Parameters:
transcript: Speech-to-text transcript
Returns: Promise<ProcessedDecision> - Extracted decision + recommendation
Example:
import { processSpeechInput } from '@/lib/ai/quickDecisionService';
const transcript = "Should I watch a movie or read a book tonight?";
const processed = await processSpeechInput(transcript);
console.log(processed.decision.title); // "Evening activity"
console.log(processed.recommendation); // AI recommendationLocation: src/lib/ai/reflectionService.ts
Generates follow-up questions for decision reflection.
function generateReflectionPrompts(
decision: Decision
): Promise<ReflectionPrompt[]>Parameters:
decision: QuickDecision or DeepDecision
Returns: Promise<ReflectionPrompt[]>
interface ReflectionPrompt {
question: string;
category: 'outcome' | 'satisfaction' | 'lessons' | 'alternative';
}Example:
import { generateReflectionPrompts } from '@/lib/ai/reflectionService';
const prompts = await generateReflectionPrompts(decision);
console.log(prompts);
// [
// {
// question: "How satisfied are you with your choice?",
// category: "satisfaction"
// },
// {
// question: "Would you make the same decision again?",
// category: "outcome"
// },
// ...
// ]Location: src/lib/ai/provider.ts
import { createGoogleGenerativeAI } from '@ai-sdk/google';
export const google = createGoogleGenerativeAI({
apiKey: import.meta.env.VITE_GOOGLE_GENERATIVE_AI_API_KEY,
});
// Fast model for quick decisions
export const geminiFlash = google('gemini-2.0-flash');
// Advanced model for deep reflections
export const geminiPro = google('gemini-1.5-pro-latest');
// Model with search grounding
export const geminiProWithSearch = google('gemini-1.5-pro-latest', {
useSearchGrounding: true,
});Usage Guidelines:
- Use
geminiFlashfor quick decisions (fast, cost-effective) - Use
geminiProfor deep reflections (more capable, slower) - Use
geminiProWithSearchfor real-time information needs
Location: src/hooks/useAuth.tsx
React hook for authentication state and operations.
function useAuth(): AuthContextTypeReturns: AuthContextType
interface AuthContextType {
user: User | null;
loading: boolean;
signIn: (email: string, password: string) => Promise<void>;
signInWithGoogle: () => Promise<void>;
signUp: (email: string, password: string, displayName: string) => Promise<void>;
signOut: () => Promise<void>;
resetPassword: (email: string) => Promise<void>;
updateProfile: (updates: Partial<User>) => Promise<void>;
}Example:
import { useAuth } from '@/hooks/useAuth';
function ProfilePage() {
const { user, loading, signOut } = useAuth();
if (loading) return <div>Loading...</div>;
if (!user) return <div>Not authenticated</div>;
return (
<div>
<h1>Welcome, {user.displayName}!</h1>
<p>Email: {user.email}</p>
<button onClick={signOut}>Sign Out</button>
</div>
);
}Signs in with email and password.
function signIn(email: string, password: string): Promise<void>Example:
const { signIn } = useAuth();
async function handleSignIn() {
try {
await signIn('sarah@example.com', 'password123');
// Redirect to dashboard
} catch (error) {
console.error('Sign in failed:', error);
}
}Throws:
auth/user-not-foundauth/wrong-passwordauth/invalid-emailauth/user-disabled
Signs in with Google OAuth.
function signInWithGoogle(): Promise<void>Example:
const { signInWithGoogle } = useAuth();
async function handleGoogleSignIn() {
try {
await signInWithGoogle();
// User is signed in, redirect to dashboard
} catch (error) {
if (error.code === 'auth/popup-closed-by-user') {
console.log('User closed the popup');
}
}
}Creates a new account.
function signUp(
email: string,
password: string,
displayName: string
): Promise<void>Example:
const { signUp } = useAuth();
async function handleSignUp() {
try {
await signUp('sarah@example.com', 'SecurePass123!', 'Sarah Johnson');
// User is created and signed in
} catch (error) {
console.error('Sign up failed:', error);
}
}Throws:
auth/email-already-in-useauth/weak-passwordauth/invalid-email
Signs out the current user.
function signOut(): Promise<void>Example:
const { signOut } = useAuth();
async function handleSignOut() {
await signOut();
// Redirect to landing page
}Sends a password reset email.
function resetPassword(email: string): Promise<void>Example:
const { resetPassword } = useAuth();
async function handleResetPassword() {
try {
await resetPassword('sarah@example.com');
alert('Password reset email sent!');
} catch (error) {
console.error('Failed to send reset email:', error);
}
}Setup: src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getAnalytics } from 'firebase/analytics';
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
export const analytics = getAnalytics(app);Environment Variables Required:
VITE_FIREBASE_API_KEY=
VITE_FIREBASE_AUTH_DOMAIN=
VITE_FIREBASE_PROJECT_ID=
VITE_FIREBASE_STORAGE_BUCKET=
VITE_FIREBASE_MESSAGING_SENDER_ID=
VITE_FIREBASE_APP_ID=
VITE_FIREBASE_MEASUREMENT_ID=Setup: src/lib/ai/provider.ts
import { createGoogleGenerativeAI } from '@ai-sdk/google';
export const google = createGoogleGenerativeAI({
apiKey: import.meta.env.VITE_GOOGLE_GENERATIVE_AI_API_KEY,
});Environment Variable Required:
VITE_GOOGLE_GENERATIVE_AI_API_KEY=API Key Setup:
- Go to Google AI Studio
- Create a new API key
- Add to
.env.local
Planned for Phase 2
import { loadStripe } from '@stripe/stripe-js';
const stripe = await loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);Create interface in src/db/types/
// src/db/types/Template.ts
export interface Template {
id: string;
userId: string;
name: string;
category: DecisionCategory;
isPublic: boolean;
template: {
title: string;
options: string[];
contextFactors: string[];
};
usageCount: number;
createdAt: Date;
updatedAt: Date;
}Create CRUD functions in src/db/Template/
// src/db/Template/templateDb.ts
import { collection, addDoc, getDoc, doc } from 'firebase/firestore';
import { db } from '@/lib/firebase';
import type { Template } from '@/db/types/Template';
export async function createTemplate(
template: Omit<Template, 'id' | 'createdAt' | 'updatedAt'>
): Promise<Template> {
const docRef = await addDoc(collection(db, 'templates'), {
...template,
createdAt: new Date(),
updatedAt: new Date(),
});
return {
...template,
id: docRef.id,
createdAt: new Date(),
updatedAt: new Date(),
};
}
export async function getTemplate(id: string): Promise<Template | null> {
const docRef = doc(db, 'templates', id);
const docSnap = await getDoc(docRef);
if (!docSnap.exists()) return null;
return {
id: docSnap.id,
...docSnap.data(),
} as Template;
}
// ... more CRUD operationsCreate vertical slice in src/Core/Templates/
// src/Core/Templates/TemplatesPage.tsx
export default function TemplatesPage() {
const [templates, setTemplates] = useState<Template[]>([]);
const { user } = useAuth();
useEffect(() => {
async function loadTemplates() {
if (!user) return;
const userTemplates = await listTemplates(user.uid);
setTemplates(userTemplates);
}
loadTemplates();
}, [user]);
return (
<div>
<h1>Decision Templates</h1>
{templates.map(template => (
<TemplateCard key={template.id} template={template} />
))}
</div>
);
}Update src/App.tsx
import TemplatesPage from '@/Core/Templates/TemplatesPage';
// In Routes
<Route path="/templates" element={<TemplatesPage />} />Update src/Core/Shared/Navbar.tsx
<Link to="/templates">Templates</Link>Create test file src/Core/Templates/__tests__/TemplatesPage.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import TemplatesPage from '../TemplatesPage';
describe('TemplatesPage', () => {
it('renders template list', () => {
render(<TemplatesPage />);
expect(screen.getByText('Decision Templates')).toBeInTheDocument();
});
});// src/lib/ai/templateService.ts
import { z } from 'zod';
const templateSchema = z.object({
name: z.string(),
category: z.nativeEnum(DecisionCategory),
suggestedOptions: z.array(z.string()).min(2),
contextPrompts: z.array(z.string()),
});import { generateObject } from 'ai';
import { geminiFlash } from './provider';
export async function generateTemplate(
description: string
): Promise<GeneratedTemplate> {
const result = await generateObject({
model: geminiFlash,
schema: templateSchema,
prompt: `Create a decision template for: ${description}`,
});
return result.object;
}const [template, setTemplate] = useState(null);
const [loading, setLoading] = useState(false);
async function handleGenerate() {
setLoading(true);
try {
const generated = await generateTemplate(userInput);
setTemplate(generated);
} catch (error) {
console.error('AI generation failed:', error);
} finally {
setLoading(false);
}
}import { useState } from 'react';
import { useAuth } from '@/hooks/useAuth';
import { extractDecisionOptions, generateRecommendation } from '@/lib/ai/quickDecisionService';
import { createQuickDecision } from '@/db/Decision/Quick/quickDecisionDb';
import { DecisionStatus } from '@/db/types/BaseDecision';
export default function QuickDecisionFlow() {
const [input, setInput] = useState('');
const [extracted, setExtracted] = useState(null);
const [recommendation, setRecommendation] = useState(null);
const [loading, setLoading] = useState(false);
const { user } = useAuth();
async function handleExtract() {
setLoading(true);
try {
const result = await extractDecisionOptions(input);
setExtracted(result);
} catch (error) {
console.error('Extraction failed:', error);
} finally {
setLoading(false);
}
}
async function handleRecommendation() {
if (!extracted) return;
setLoading(true);
try {
const rec = await generateRecommendation(
extracted.title,
extracted.options,
extracted.contextFactors || [],
extracted.gutFeeling
);
setRecommendation(rec);
} catch (error) {
console.error('Recommendation failed:', error);
} finally {
setLoading(false);
}
}
async function handleSave() {
if (!user || !extracted || !recommendation) return;
try {
await createQuickDecision({
type: 'quick',
userId: user.uid,
title: extracted.title,
category: extracted.category,
status: DecisionStatus.COMPLETED,
options: extracted.options.map((opt, i) => ({
id: String(i),
text: opt.text,
selected: opt.text === recommendation.recommendedOption,
pros: opt.pros,
cons: opt.cons,
})),
contextFactors: extracted.contextFactors,
gutFeeling: extracted.gutFeeling,
recommendation: {
optionId: '0', // Find matching option
reasoning: recommendation.reasoning,
confidence: recommendation.confidence,
},
selectedOption: recommendation.recommendedOption,
});
alert('Decision saved!');
} catch (error) {
console.error('Save failed:', error);
}
}
return (
<div>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Describe your decision..."
/>
<button onClick={handleExtract} disabled={loading}>
Extract Decision
</button>
{extracted && (
<div>
<h2>{extracted.title}</h2>
<ul>
{extracted.options.map((opt, i) => (
<li key={i}>{opt.text}</li>
))}
</ul>
<button onClick={handleRecommendation} disabled={loading}>
Get Recommendation
</button>
</div>
)}
{recommendation && (
<div>
<h3>Recommendation: {recommendation.recommendedOption}</h3>
<p>{recommendation.reasoning}</p>
<p>Confidence: {(recommendation.confidence * 100).toFixed(0)}%</p>
<button onClick={handleSave}>Save Decision</button>
</div>
)}
</div>
);
}// Custom error classes
export class AIServiceError extends Error {
constructor(message: string, public cause?: unknown) {
super(message);
this.name = 'AIServiceError';
}
}
export class DatabaseError extends Error {
constructor(message: string, public cause?: unknown) {
super(message);
this.name = 'DatabaseError';
}
}
export class ValidationError extends Error {
constructor(message: string, public cause?: unknown) {
super(message);
this.name = 'ValidationError';
}
}async function robustOperation() {
try {
// Attempt operation
const result = await riskyFunction();
return result;
} catch (error) {
// Log error with context
console.error('Operation failed:', {
operation: 'robustOperation',
error,
timestamp: new Date().toISOString(),
});
// Determine error type
if (error instanceof AIServiceError) {
// Handle AI-specific errors
return fallbackAIResponse();
} else if (error instanceof DatabaseError) {
// Handle database errors
throw new Error('Failed to save data. Please try again.');
} else {
// Unknown error
throw new Error('An unexpected error occurred.');
}
}
}// Good
if (decision && decision.type === 'quick') {
// TypeScript knows this is QuickDecision
console.log(decision.options);
}
// Bad
console.log(decision.options); // May error if decision is DeepDecisionconst [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
async function fetchData() {
setLoading(true);
setError(null);
try {
const result = await apiCall();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}async function deleteDecision(id: string) {
// Optimistic update
setDecisions(prev => prev.filter(d => d.id !== id));
try {
await deleteQuickDecision(id);
} catch (error) {
// Revert on error
const deleted = await getQuickDecision(id);
if (deleted) {
setDecisions(prev => [...prev, deleted]);
}
alert('Failed to delete decision');
}
}import { useDebounce } from '@/hooks/useDebounce';
function SearchDecisions() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
if (debouncedQuery) {
searchDecisions(debouncedQuery);
}
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}const [decisions, setDecisions] = useState<QuickDecision[]>([]);
const [lastDoc, setLastDoc] = useState<DocumentSnapshot | null>(null);
const [hasMore, setHasMore] = useState(true);
async function loadMore() {
const result = await listQuickDecisions(userId, {
limit: 20,
startAfter: lastDoc,
});
setDecisions(prev => [...prev, ...result.decisions]);
setLastDoc(result.lastDoc);
setHasMore(result.decisions.length === 20);
}Document End