# @embeddable/sdk

A TypeScript/JavaScript SDK with React utilities and hooks for embeddable applications. Built with tree shaking support and modern development practices.

[![npm version](https://badge.fury.io/js/%40embeddable%2Fsdk.svg)](https://badge.fury.io/js/%40embeddable%2Fsdk)
[![CI/CD](https://github.com/CommonNinja/embeddable-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/CommonNinja/embeddable-sdk/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/CommonNinja/embeddable-sdk/branch/main/graph/badge.svg)](https://codecov.io/gh/CommonNinja/embeddable-sdk)

## Features

- 🌳 **Tree Shaking**: Import only what you need
- 📦 **TypeScript**: Full TypeScript support with type definitions
- ⚛️ **React Hooks**: Custom hooks for common patterns
- 🛠️ **Utilities**: Useful utility functions
- 📱 **Storage**: Enhanced localStorage utilities
- 🌐 **API Client**: Type-safe API client with error handling
- 🧪 **Well Tested**: Comprehensive test coverage

## Installation

```bash
npm install @embeddable/sdk
# or
yarn add @embeddable/sdk
# or
pnpm add @embeddable/sdk
```

## Usage

### Import Everything

```typescript
import { useLocalStorage, debounce } from '@embeddable/sdk';
```

### Import Specific Modules (Tree Shaking)

```typescript
// Import only hooks
import { useLocalStorage } from '@embeddable/sdk/hooks';

// Import only utilities
import { debounce } from '@embeddable/sdk/utils';
```

## Global Configuration

The SDK supports global configuration through the `EmbeddableProvider` context. This allows you to set a `widgetId` and `version` and `mode` once and access them throughout your application.

### Setup

Wrap your application with the `EmbeddableProvider`:

```typescript
import { EmbeddableProvider } from '@embeddable/sdk';
import type { EmbeddableConfig } from '@embeddable/sdk';

function App() {
  const config: EmbeddableConfig = {
    widgetId: 'my-widget-123',
    version: '1.0.0', // 'dev' | 'latest' | string
    mode: 'embeddable', // 'embeddable' | 'standalone' | 'preview'
    ignoreCache: false,
    lazyLoad: false,
    loader: true,
    _containerId: 'my-container-id',
    _shadowRoot: undefined,
  };

  return (
    <EmbeddableProvider config={config}>
      <YourAppComponents />
    </EmbeddableProvider>
  );
}
```

### Using Global Configuration

Access the global configuration in any component:

```typescript
import { useEmbeddableConfig, useApi } from '@embeddable/sdk';

function WidgetComponent() {
  const config = useEmbeddableConfig();

  return (
    <div>
      <h2>Widget: {config.widgetId}</h2>
      <p>Version: {config.version}</p>
      <p>Mode: {config.mode}</p>
      <p>Ignore Cache: {config.ignoreCache ? 'Yes' : 'No'}</p>
      <p>Lazy Load: {config.lazyLoad ? 'Yes' : 'No'}</p>
      <p>Show Loader: {config.loader ? 'Yes' : 'No'}</p>
      {/* Your widget content */}
    </div>
  );
}
```

## API Reference

### Hooks

#### `useLocalStorage<T>(key: string, initialValue: T, options?: LocalStorageOptions)`

A React hook for localStorage with state synchronization across tabs.

```typescript
import { useLocalStorage } from '@embeddable/sdk/hooks'

function MyComponent() {
  const [user, setUser, removeUser] = useLocalStorage('user', { name: '', email: '' })

  return (
    <div>
      <input
        value={user.name}
        onChange={(e) => setUser(prev => ({ ...prev, name: e.target.value }))}
      />
      <button onClick={removeUser}>Clear</button>
    </div>
  )
}
```

#### `useDebounce<T>(value: T, delay: number)`

A React hook that debounces a value.

```typescript
import { useDebounce } from '@embeddable/sdk/hooks'

function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('')
  const debouncedSearchTerm = useDebounce(searchTerm, 300)

  useEffect(() => {
    if (debouncedSearchTerm) {
      // Perform search
    }
  }, [debouncedSearchTerm])

  return <input onChange={(e) => setSearchTerm(e.target.value)} />
}
```

#### `useEmbeddableConfig()`

A React hook to access the global SDK configuration.

```typescript
import { useEmbeddableConfig } from '@embeddable/sdk/hooks';

function MyWidget() {
  const config = useEmbeddableConfig();

  return (
    <div>
      <h3>Widget ID: {config.widgetId}</h3>
      <p>Version: {config.version}</p>
      <p>Mode: {config.mode}</p>
    </div>
  );
}
```

**Note:** This hook must be used within an `EmbeddableProvider`.

#### `useFormSubmission<TPayload>(options?: FormSubmissionOptions)`

A React hook for handling form submissions with loading states, error handling, and payload validation. Automatically uses the widget ID from the global configuration.

```typescript
import { useFormSubmission } from '@embeddable/sdk/hooks';

// Define your form data type
interface ContactForm {
  name: string;
  email: string;
  message: string;
}

function ContactFormComponent() {
  const [formData, setFormData] = useState<ContactForm>({
    name: '',
    email: '',
    message: ''
  });

  const { submit, loading, error, success, reset } = useFormSubmission<ContactForm>({
    collectionName: 'contact-forms', // Optional, defaults to 'submissions'
    validatePayload: (payload) => {
      if (!payload.email) return 'Email is required';
      if (!payload.name) return 'Name is required';
      if (!payload.message) return 'Message is required';
      return null; // No validation errors
    }
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    const result = await submit(formData);
    if (result.success) {
      console.log('Form submitted successfully!', result.id);
      // Reset form or show success message
      setFormData({ name: '', email: '', message: '' });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Name"
        value={formData.name}
        onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
      />
      <input
        type="email"
        placeholder="Email"
        value={formData.email}
        onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
      />
      <textarea
        placeholder="Message"
        value={formData.message}
        onChange={(e) => setFormData(prev => ({ ...prev, message: e.target.value }))}
      />

      <button type="submit" disabled={loading}>
        {loading ? 'Submitting...' : 'Submit'}
      </button>

      {error && <p style={{ color: 'red' }}>{error}</p>}
      {success && <p style={{ color: 'green' }}>Message sent successfully!</p>}

      <button type="button" onClick={reset}>Reset Status</button>
    </form>
  );
}
```

**Options:**

- `collectionName?: string` - The collection name for submissions (defaults to 'submissions')
- `validatePayload?: (payload: any) => string | null` - Optional validation function

**Returns:**

- `submit: (payload: TPayload) => Promise<FormSubmissionResponse>` - Function to submit the form
- `loading: boolean` - Whether the submission is in progress
- `error: string | null` - Error message if submission failed
- `success: boolean` - Whether the submission was successful
- `reset: () => void` - Function to reset the hook state

#### `useVote(options?: VoteOptions)`

A React hook for handling votes with loading states and error handling. Automatically uses the widget ID from the global configuration.

```typescript
import { useVote } from '@embeddable/sdk/hooks';

function VotingComponent() {
  const { vote, loading, error, success, reset } = useVote({
    collectionName: 'product-poll' // Optional, defaults to 'votes'
  });

  const handleVote = async (optionId: string) => {
    const result = await vote(optionId);
    if (result.success) {
      console.log('Vote submitted!', result.id);
    }
  };

  return (
    <div>
      <h3>Which product do you prefer?</h3>

      <button
        onClick={() => handleVote('product-a')}
        disabled={loading}
      >
        Product A
      </button>

      <button
        onClick={() => handleVote('product-b')}
        disabled={loading}
      >
        Product B
      </button>

      <button
        onClick={() => handleVote('product-c')}
        disabled={loading}
      >
        Product C
      </button>

      {loading && <p>Submitting your vote...</p>}
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}
      {success && <p style={{ color: 'green' }}>Thank you for voting!</p>}

      <button onClick={reset}>Reset</button>
    </div>
  );
}

// Example with dynamic voting options
function DynamicVotingComponent() {
  const { vote, loading, error, success } = useVote({
    collectionName: 'feature-requests'
  });

  const features = [
    { id: 'dark-mode', name: 'Dark Mode' },
    { id: 'mobile-app', name: 'Mobile App' },
    { id: 'api-access', name: 'API Access' },
  ];

  const handleFeatureVote = async (featureId: string, featureName: string) => {
    const result = await vote(featureId);
    if (result.success) {
      alert(`Thank you for voting for ${featureName}!`);
    }
  };

  return (
    <div>
      <h3>Vote for the next feature:</h3>
      {features.map(feature => (
        <button
          key={feature.id}
          onClick={() => handleFeatureVote(feature.id, feature.name)}
          disabled={loading}
          style={{
            margin: '5px',
            padding: '10px',
            opacity: loading ? 0.6 : 1
          }}
        >
          {feature.name}
        </button>
      ))}

      {loading && <p>Processing vote...</p>}
      {error && <p style={{ color: 'red' }}>{error}</p>}
      {success && <p style={{ color: 'green' }}>Vote recorded!</p>}
    </div>
  );
}
```

**Options:**

- `collectionName?: string` - The collection name for votes (defaults to 'votes')

**Returns:**

- `vote: (voteFor: string) => Promise<VoteResponse>` - Function to submit a vote
- `loading: boolean` - Whether the vote submission is in progress
- `error: string | null` - Error message if vote submission failed
- `success: boolean` - Whether the vote was submitted successfully
- `reset: () => void` - Function to reset the hook state

**Note:** Both `useFormSubmission` and `useVote` hooks must be used within an `EmbeddableProvider` as they automatically retrieve the `widgetId` from the global configuration.

#### `useVoteAggregations(options?: UseVoteAggregationsOptions)`

A React hook for fetching vote aggregations with loading states, error handling, and optional auto-refresh. Automatically uses the widget ID from the global configuration.

```typescript
import { useVoteAggregations } from '@embeddable/sdk/hooks';

function VoteResultsComponent() {
  const { data, loading, error, refetch, reset } = useVoteAggregations({
    collectionName: 'product-poll', // Optional, defaults to 'votes'
    autoFetch: true, // Optional, defaults to true
    refetchInterval: 30000, // Optional, refetch every 30 seconds
  });

  if (loading) {
    return <div>Loading vote results...</div>;
  }

  if (error) {
    return (
      <div>
        <p style={{ color: 'red' }}>Error: {error}</p>
        <button onClick={refetch}>Retry</button>
      </div>
    );
  }

  if (!data) {
    return <div>No vote data available</div>;
  }

  return (
    <div>
      <h3>Vote Results</h3>

      <div>
        <p><strong>Total Votes:</strong> {data.summary.totalVotes}</p>
        <p><strong>Total Options:</strong> {data.summary.totalOptions}</p>
      </div>

      <div>
        {data.results.map((result) => (
          <div key={result._id} style={{ margin: '10px 0', padding: '10px', border: '1px solid #ccc' }}>
            <h4>{result._id}</h4>
            <p>Votes: {result.count}</p>
            {result.percentage && <p>Percentage: {result.percentage.toFixed(1)}%</p>}
            <div style={{ width: '100%', backgroundColor: '#e0e0e0', borderRadius: '4px' }}>
              <div
                style={{
                  width: `${result.percentage || 0}%`,
                  backgroundColor: '#4caf50',
                  height: '20px',
                  borderRadius: '4px',
                  transition: 'width 0.3s ease'
                }}
              />
            </div>
          </div>
        ))}
      </div>

      <button onClick={refetch}>Refresh Results</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

// Example with manual fetching
function ManualVoteResultsComponent() {
  const { data, loading, error, fetch } = useVoteAggregations({
    collectionName: 'feature-requests',
    autoFetch: false, // Don't fetch automatically
  });

  const handleLoadResults = async () => {
    const response = await fetch();
    if (response.success) {
      console.log('Results loaded:', response.data);
    }
  };

  return (
    <div>
      <button onClick={handleLoadResults} disabled={loading}>
        {loading ? 'Loading...' : 'Load Vote Results'}
      </button>

      {error && <p style={{ color: 'red' }}>{error}</p>}

      {data && (
        <div>
          <h3>Feature Request Votes</h3>
          <p>Total: {data.summary.totalVotes} votes</p>
          <ul>
            {data.results.map(result => (
              <li key={result._id}>
                {result._id}: {result.count} votes
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

// Example with real-time updates
function LiveVoteResultsComponent() {
  const { data, loading, error } = useVoteAggregations({
    collectionName: 'live-poll',
    refetchInterval: 5000, // Update every 5 seconds
  });

  return (
    <div>
      <h3>Live Poll Results {loading && '(Updating...)'}</h3>

      {error ? (
        <p style={{ color: 'red' }}>Failed to load results</p>
      ) : (
        <div>
          {data?.results.map(result => (
            <div key={result._id} style={{ display: 'flex', alignItems: 'center', margin: '5px 0' }}>
              <span style={{ minWidth: '100px' }}>{result._id}:</span>
              <div style={{ flex: 1, margin: '0 10px', backgroundColor: '#e0e0e0', borderRadius: '10px' }}>
                <div
                  style={{
                    width: `${result.percentage || 0}%`,
                    backgroundColor: '#2196f3',
                    height: '20px',
                    borderRadius: '10px',
                    transition: 'all 0.5s ease'
                  }}
                />
              </div>
              <span>{result.count} votes</span>
            </div>
          ))}
          <p style={{ marginTop: '10px', fontSize: '12px', color: '#666' }}>
            Last updated: {new Date().toLocaleTimeString()}
          </p>
        </div>
      )}
    </div>
  );
}
```

**Options:**

- `collectionName?: string` - The collection name for votes (defaults to 'votes')
- `autoFetch?: boolean` - Whether to automatically fetch data on mount (defaults to true)
- `refetchInterval?: number` - Interval in milliseconds for automatic refetching (optional)

**Returns:**

- `data: VoteAggregationData | null` - The aggregation data with results and summary
- `loading: boolean` - Whether a request is in progress
- `error: string | null` - Error message if request failed
- `fetch: () => Promise<VoteAggregationResponse>` - Function to manually fetch aggregations
- `refetch: () => Promise<VoteAggregationResponse>` - Alias for fetch function
- `reset: () => void` - Function to reset the hook state

**Data Structure:**

```typescript
interface VoteAggregationData {
  results: Array<{
    _id: string; // The option that was voted for
    count: number; // Number of votes for this option
    percentage?: number; // Percentage of total votes
  }>;
  summary: {
    totalVotes: number; // Total number of votes cast
    totalOptions: number; // Number of different options
    groupBy: string; // The field used for grouping
  };
}
```

**Note:** This hook must be used within an `EmbeddableProvider` as it automatically retrieves the `widgetId` from the global configuration.

#### `useAI(options: UseAIOptions)`

A React hook for handling AI API calls with loading states, error handling, and support for multiple AI platforms. Automatically uses the widget ID from the global configuration.

```typescript
import { useAI } from '@embeddable/sdk/hooks';

function ChatComponent() {
  const { callAI, loading, error, success, reset } = useAI({
    platform: 'openai' // 'openai' | 'anthropic' | 'gemini'
  });

  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');

  const handleSendMessage = async () => {
    if (!input.trim()) return;

    const userMessage = { role: 'user', content: input };
    const newMessages = [...messages, userMessage];
    setMessages(newMessages);
    setInput('');

    const result = await callAI(input, messages);
    if (result.success && result.data) {
      setMessages(prev => [...prev, {
        role: 'assistant',
        content: result.data.response
      }]);

      console.log('Tokens used:', result.data.tokensUsed);
      console.log('Model used:', result.metadata?.model);
    }
  };

  return (
    <div>
      <div style={{ height: '400px', overflowY: 'auto', border: '1px solid #ccc', padding: '10px' }}>
        {messages.map((msg, index) => (
          <div key={index} style={{ marginBottom: '10px' }}>
            <strong>{msg.role}:</strong> {msg.content}
          </div>
        ))}
        {loading && <div>AI is thinking...</div>}
      </div>

      <div style={{ marginTop: '10px', display: 'flex', gap: '10px' }}>
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
          placeholder="Type your message..."
          style={{ flex: 1, padding: '8px' }}
          disabled={loading}
        />
        <button onClick={handleSendMessage} disabled={loading || !input.trim()}>
          Send
        </button>
      </div>

      {error && <p style={{ color: 'red', marginTop: '10px' }}>Error: {error}</p>}
      {success && <p style={{ color: 'green', marginTop: '10px' }}>Message sent successfully!</p>}

      <button onClick={reset} style={{ marginTop: '10px' }}>Reset Status</button>
    </div>
  );
}
```

**Options:**

- `platform: 'openai' | 'anthropic' | 'gemini'` - The AI platform to use for API calls

**Returns:**

- `callAI: (prompt: string, history?: Array<ChatMessage>) => Promise<AIResponse>` - Function to make AI API calls
- `loading: boolean` - Whether an AI request is in progress
- `error: string | null` - Error message if the request failed
- `success: boolean` - Whether the last request was successful
- `reset: () => void` - Function to reset the hook state

**Data Structure:**

```typescript
interface ChatMessage {
  role: 'user' | 'assistant';
  content: string;
}

interface AIResponse {
  success: boolean;
  message?: string;
  error?: string;
  data?: {
    response: string;
    tokensUsed: number | null;
  };
  metadata?: {
    model: string | null;
    integrationKey: string;
    capabilityId: string;
    executedAt: string;
    [key: string]: any; // Additional metadata fields
  };
}
```

### Utilities

#### `debounce<T>(func: T, wait: number)`

Creates a debounced function.

```typescript
import { debounce } from '@embeddable/sdk/utils';

const debouncedSave = debounce(data => {
  // Save data
}, 500);
```

#### `createApiClient(config: EmbeddableApiConfig)`

Creates a type-safe API client.

```typescript
import { createApiClient } from '@embeddable/sdk/utils';

const api = createApiClient({
  apiKey: 'your-api-key',
  baseUrl: 'https://api.example.com',
  debug: true,
});

const response = await api.get('/users');
```

#### `storage`

Enhanced localStorage utility with serialization support.

```typescript
import { storage } from '@embeddable/sdk/utils';

// Basic usage
storage.set('user', { name: 'John', age: 30 });
const user = storage.get('user');

// With options
storage.set('data', complexObject, {
  prefix: 'myapp_',
  serialize: JSON.stringify,
  deserialize: JSON.parse,
});
```

## Types

The SDK exports TypeScript types for better development experience:

```typescript
import type {
  EmbeddableConfig,
  EmbeddableApiConfig,
  ApiResponse,
  LocalStorageOptions,
  FormSubmission,
  FormSubmissionResponse,
  FormSubmissionOptions,
  Vote,
  VoteResponse,
  VoteOptions,
  VoteAggregationResult,
  VoteAggregationSummary,
  VoteAggregationData,
  VoteAggregationResponse,
  ChatMessage,
  AIResponse,
  UseAIOptions,
} from '@embeddable/sdk';
```

## Development

```bash
# Install dependencies
npm install

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Build the package
npm run build

# Run linter
npm run lint

# Fix linting issues
npm run lint:fix

# Format code with Prettier
npm run format

# Check code formatting
npm run format:check

# Type check
npm run type-check
```

## Contributing

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes using conventional commits (`git commit -m 'feat: add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## License

MIT © [Embeddable Team](https://github.com/CommonNinja/embeddable-sdk)

## Changelog

See [CHANGELOG.md](./CHANGELOG.md) for a list of changes.
