1.0.9 โ€ข Published 11 months ago

@embeddable/sdk v1.0.9

Weekly downloads
-
License
MIT
Repository
github
Last release
11 months ago

@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 CI/CD codecov

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

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

Usage

Import Everything

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

Import Specific Modules (Tree Shaking)

// 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:

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:

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

function WidgetComponent() {
  const config = useEmbeddableConfig();
  const api = useApi(); // Automatically uses global config

  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.

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.

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)} />
}

useApi(config: EmbeddableApiConfig)

A React hook for API calls with loading and error states.

import { useApi } from '@embeddable/sdk/hooks'

function DataComponent() {
  const api = useApi({ apiKey: 'your-api-key', baseUrl: 'https://api.example.com' })

  const fetchData = async () => {
    const response = await api.get('/users')
    if (response.success) {
      console.log(response.data)
    }
  }

  return (
    <div>
      {api.loading && <div>Loading...</div>}
      {api.error && <div>Error: {api.error}</div>}
      <button onClick={fetchData}>Fetch Data</button>
    </div>
  )
}

useEmbeddableConfig()

A React hook to access the global SDK configuration.

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.

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.

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.

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:

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.

Utilities

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

Creates a debounced function.

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

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

createApiClient(config: EmbeddableApiConfig)

Creates a type-safe API client.

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.

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:

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

Development

# 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

Changelog

See CHANGELOG.md for a list of changes.

1.0.9

11 months ago

1.0.8

11 months ago

1.0.7

11 months ago

1.0.6

11 months ago

1.0.5

12 months ago

1.0.4

12 months ago

1.0.3

12 months ago

1.0.2

12 months ago

1.0.1

12 months ago

1.0.0

12 months ago