Skip to main content

web-embed Application

Embeddable Widgets for External Sites
Application Type: Embeddable Widget Platform
Port: 3003
Users: External website visitors
Status: Planned
Technology: Vanilla JS + React + @reptidex/core

Purpose & Responsibilities

web-embed provides lightweight, embeddable widgets that allow reptile breeders to integrate reptidex functionality directly into their own websites. It focuses on minimal bundle size, maximum compatibility, and seamless integration.

Core Features

Animal Profile Widgets

  • Compact animal profile cards
  • Photo galleries with lightbox
  • Basic animal information display
  • Contact breeder functionality
  • Responsive design for all devices

Pedigree Widgets

  • Interactive pedigree tree display
  • Multi-generation lineage visualization
  • Collapsible/expandable tree nodes
  • Print and export functionality
  • Mobile-optimized navigation

Breeder Showcase

  • Breeder profile summary cards
  • Featured animals carousel
  • Recent breeding announcements
  • Contact information and links
  • Social media integration

Marketplace Listings

  • Available animals grid display
  • Filtering and search functionality
  • Price display and availability
  • Quick inquiry forms
  • Real-time availability updates

Architecture & Technology

Lightweight Technology Stack

Core Technologies:
  • Vanilla JavaScript for core functionality
  • React 18 for widget components (optional)
  • TypeScript for type safety
  • Minimal CSS framework for styling
  • Web Components for framework agnostic widgets
Bundle Optimization:
  • @reptidex/core: Essential API clients only
  • Tree-shaken imports for minimal bundle size
  • Dynamic imports for advanced features
  • CSS-in-JS for scoped styling
  • Service worker for caching

Widget Architecture

src/
├── widgets/                  # Individual widget components
│   ├── animal-profile/      # Animal profile card widget
│   │   ├── AnimalProfile.tsx
│   │   ├── AnimalProfile.css
│   │   └── index.ts
│   ├── pedigree-tree/       # Pedigree visualization widget
│   │   ├── PedigreeTree.tsx
│   │   ├── PedigreeTree.css
│   │   └── index.ts
│   ├── breeder-showcase/    # Breeder showcase widget
│   │   ├── BreederShowcase.tsx
│   │   ├── BreederShowcase.css
│   │   └── index.ts
│   └── marketplace-grid/    # Marketplace listing widget
│       ├── MarketplaceGrid.tsx
│       ├── MarketplaceGrid.css
│       └── index.ts
├── core/                    # Core widget functionality
│   ├── widget-loader.ts    # Dynamic widget loading
│   ├── theme-system.ts     # Theming and customization
│   ├── api-client.ts       # Lightweight API client
│   └── error-handler.ts    # Error handling and fallbacks
├── embed/                   # Embed scripts and setup
│   ├── embed.ts            # Main embed script
│   ├── widget-config.ts    # Widget configuration
│   └── fallbacks.ts        # Fallback components
└── build/                   # Build outputs
    ├── widgets/            # Individual widget bundles
    ├── embed.min.js        # Main embed script
    └── themes/             # Pre-built themes

Widget Implementation

Animal Profile Widget

Animal Profile Widget:
interface AnimalProfileProps {
  animalId: string;
  theme?: 'light' | 'dark' | 'custom';
  showContact?: boolean;
  showPedigree?: boolean;
  maxWidth?: string;
}

const AnimalProfileWidget = ({ 
  animalId, 
  theme = 'light',
  showContact = true,
  showPedigree = false,
  maxWidth = '400px'
}: AnimalProfileProps) => {
  const { data: animal, isLoading, error } = useAnimal(animalId);

  if (isLoading) return <SkeletonCard />;
  if (error) return <ErrorFallback />;
  if (!animal) return <NotFoundFallback />;

  return (
    <WidgetContainer theme={theme} maxWidth={maxWidth}>
      <AnimalImage 
        src={animal.primaryImage}
        alt={animal.name}
        fallback="/placeholder-animal.jpg"
      />
      
      <AnimalInfo>
        <AnimalName>{animal.name}</AnimalName>
        <SpeciesInfo>{animal.species.commonName}</SpeciesInfo>
        <GeneticsDisplay genetics={animal.genetics} />
        
        {animal.price && (
          <PriceTag price={animal.price} currency="USD" />
        )}
      </AnimalInfo>

      {showContact && (
        <ContactSection>
          <ContactButton 
            breederId={animal.breederId}
            animalId={animal.id}
          />
        </ContactSection>
      )}

      {showPedigree && animal.hasLineage && (
        <PedigreeLink animalId={animal.id} />
      )}
      
      <PoweredBy />
    </WidgetContainer>
  );
};
Integration Example:
<!-- Simple integration -->
<div id="reptidex-animal-123"></div>
<script>
  reptidex.renderAnimalProfile('reptidex-animal-123', {
    animalId: '123',
    theme: 'light',
    showContact: true
  });
</script>

Pedigree Tree Widget

Pedigree Tree Component:
interface PedigreeTreeProps {
  animalId: string;
  generations?: number;
  theme?: ThemeConfig;
  interactive?: boolean;
  showPhotos?: boolean;
  compact?: boolean;
}

const PedigreeTreeWidget = ({
  animalId,
  generations = 3,
  theme,
  interactive = true,
  showPhotos = true,
  compact = false
}: PedigreeTreeProps) => {
  const { data: pedigree } = usePedigree(animalId, generations);
  const [selectedNode, setSelectedNode] = useState<string | null>(null);

  return (
    <PedigreeContainer theme={theme} compact={compact}>
      <PedigreeHeader>
        <GenerationControls 
          value={generations}
          onChange={handleGenerationChange}
        />
        <ExportControls onExport={handleExport} />
      </PedigreeHeader>

      <PedigreeTree
        data={pedigree}
        interactive={interactive}
        showPhotos={showPhotos}
        selectedNode={selectedNode}
        onNodeClick={setSelectedNode}
        onNodeHover={handleNodeHover}
      />

      {selectedNode && (
        <NodeDetails 
          animal={pedigree.getNode(selectedNode)}
          onClose={() => setSelectedNode(null)}
        />
      )}

      <PoweredBy />
    </PedigreeContainer>
  );
};
Mobile Optimization:
const ResponsivePedigreeTree = (props: PedigreeTreeProps) => {
  const isMobile = useMediaQuery('(max-width: 768px)');
  
  return isMobile ? (
    <MobilePedigreeView {...props} />
  ) : (
    <DesktopPedigreeView {...props} />
  );
};

Marketplace Grid Widget

Marketplace Grid Component:
interface MarketplaceGridProps {
  breederId?: string;
  species?: string[];
  maxItems?: number;
  showFilters?: boolean;
  gridColumns?: number;
  theme?: ThemeConfig;
}

const MarketplaceGridWidget = ({
  breederId,
  species,
  maxItems = 12,
  showFilters = true,
  gridColumns = 3,
  theme
}: MarketplaceGridProps) => {
  const [filters, setFilters] = useState({
    species: species || [],
    priceRange: null,
    availability: 'available'
  });

  const { data: listings } = useMarketplaceListings({
    breederId,
    filters,
    limit: maxItems
  });

  return (
    <MarketplaceContainer theme={theme}>
      {showFilters && (
        <FilterBar filters={filters} onChange={setFilters} />
      )}

      <AnimalGrid columns={gridColumns}>
        {listings?.map(listing => (
          <AnimalCard
            key={listing.id}
            animal={listing.animal}
            price={listing.price}
            availability={listing.availability}
            onInquiry={() => handleInquiry(listing.id)}
          />
        ))}
      </AnimalGrid>

      {listings?.length === 0 && (
        <EmptyState message="No animals available" />
      )}

      <PoweredBy />
    </MarketplaceContainer>
  );
};

Embed System & Integration

Widget Loading System

Widget Loader:
class WidgetLoader {
  private loadedWidgets = new Set<string>();
  private widgetRegistry = new Map<string, WidgetConfig>();

  async loadWidget(type: string, container: HTMLElement, props: any) {
    // Check if widget type is registered
    if (!this.widgetRegistry.has(type)) {
      throw new Error(`Unknown widget type: ${type}`);
    }

    // Load widget bundle if not already loaded
    if (!this.loadedWidgets.has(type)) {
      await this.loadWidgetBundle(type);
      this.loadedWidgets.add(type);
    }

    // Render widget
    const WidgetComponent = this.widgetRegistry.get(type)!.component;
    return this.renderWidget(WidgetComponent, container, props);
  }

  private async loadWidgetBundle(type: string) {
    // Dynamic import for code splitting
    const bundle = await import(`./widgets/${type}/index.js`);
    this.widgetRegistry.set(type, bundle.default);
  }

  private renderWidget(Component: React.ComponentType, container: HTMLElement, props: any) {
    const root = createRoot(container);
    root.render(React.createElement(Component, props));
    return root;
  }
}
Embed Script Integration:
// reptidex-embed.js - Main embed script
(function(window, document) {
  'use strict';

  // Global reptidex namespace
  window.reptidex = window.reptidex || {};

  const API_BASE = 'https://api.reptidex.com';
  const CDN_BASE = 'https://cdn.reptidex.com';

  // Widget rendering functions
  window.reptidex.renderAnimalProfile = function(containerId, options) {
    return loadWidget('animal-profile', containerId, options);
  };

  window.reptidex.renderPedigreeTree = function(containerId, options) {
    return loadWidget('pedigree-tree', containerId, options);
  };

  window.reptidex.renderBreederShowcase = function(containerId, options) {
    return loadWidget('breeder-showcase', containerId, options);
  };

  window.reptidex.renderMarketplaceGrid = function(containerId, options) {
    return loadWidget('marketplace-grid', containerId, options);
  };

  // Auto-initialize widgets based on data attributes
  function autoInitialize() {
    document.querySelectorAll('[data-reptidex-widget]').forEach(element => {
      const widgetType = element.getAttribute('data-reptidex-widget');
      const config = JSON.parse(element.getAttribute('data-reptidex-config') || '{}');
      
      if (window.reptidex[`render${capitalizeFirst(widgetType)}`]) {
        window.reptidex[`render${capitalizeFirst(widgetType)}`](element.id, config);
      }
    });
  }

  // Initialize when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', autoInitialize);
  } else {
    autoInitialize();
  }

})(window, document);

Theme System & Customization

Theme Configuration:
interface ThemeConfig {
  colors: {
    primary: string;
    secondary: string;
    background: string;
    text: string;
    border: string;
    accent: string;
  };
  typography: {
    fontFamily: string;
    fontSize: {
      small: string;
      medium: string;
      large: string;
    };
    fontWeight: {
      normal: number;
      medium: number;
      bold: number;
    };
  };
  spacing: {
    small: string;
    medium: string;
    large: string;
  };
  borderRadius: string;
  shadows: {
    small: string;
    medium: string;
    large: string;
  };
}

const createTheme = (customTheme: Partial<ThemeConfig>): ThemeConfig => {
  return deepMerge(defaultTheme, customTheme);
};

// CSS Custom Properties for theming
const applyTheme = (theme: ThemeConfig, container: HTMLElement) => {
  Object.entries(flattenTheme(theme)).forEach(([property, value]) => {
    container.style.setProperty(`--reptidex-${property}`, value);
  });
};
CSS Integration:
/* Widget base styles */
.reptidex-widget {
  --reptidex-primary: #2563eb;
  --reptidex-secondary: #64748b;
  --reptidex-background: #ffffff;
  --reptidex-text: #1e293b;
  --reptidex-border: #e2e8f0;
  
  font-family: var(--reptidex-font-family, system-ui, sans-serif);
  background: var(--reptidex-background);
  color: var(--reptidex-text);
  border: 1px solid var(--reptidex-border);
  border-radius: var(--reptidex-border-radius, 8px);
}

.reptidex-widget.dark {
  --reptidex-background: #1e293b;
  --reptidex-text: #f1f5f9;
  --reptidex-border: #475569;
}

Performance & Optimization

Bundle Size Optimization

Code Splitting & Tree Shaking:
// Webpack configuration for optimal bundling
const webpackConfig = {
  entry: {
    'embed': './src/embed/embed.ts',
    'animal-profile': './src/widgets/animal-profile/index.ts',
    'pedigree-tree': './src/widgets/pedigree-tree/index.ts',
    'breeder-showcase': './src/widgets/breeder-showcase/index.ts',
    'marketplace-grid': './src/widgets/marketplace-grid/index.ts'
  },
  output: {
    filename: '[name].min.js',
    chunkFilename: 'chunks/[name].[contenthash].js',
    library: 'reptidex',
    libraryTarget: 'umd'
  },
  optimization: {
    usedExports: true,
    sideEffects: false,
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all'
        }
      }
    }
  }
};
Bundle Size Targets:
  • Main embed script: < 5KB gzipped
  • Individual widgets: < 15KB gzipped each
  • Shared vendor chunks: < 30KB gzipped
  • Total page impact: < 50KB gzipped

Loading & Caching Strategy

Lazy Loading Implementation:
const LazyWidget = ({ type, ...props }: WidgetProps) => {
  const [isVisible, setIsVisible] = useState(false);
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.unobserve(entry.target);
        }
      },
      { threshold: 0.1 }
    );

    if (containerRef.current) {
      observer.observe(containerRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div ref={containerRef}>
      {isVisible ? (
        <Suspense fallback={<WidgetSkeleton />}>
          <DynamicWidget type={type} {...props} />
        </Suspense>
      ) : (
        <WidgetPlaceholder />
      )}
    </div>
  );
};
Caching Strategy:
  • Service worker for widget assets
  • CDN caching with long TTL
  • Local storage for widget preferences
  • API response caching with React Query
  • Image lazy loading and optimization

Integration Examples

WordPress Integration

WordPress Shortcode:
// reptidex WordPress Plugin
function reptidex_animal_profile_shortcode($atts) {
    $atts = shortcode_atts(array(
        'animal_id' => '',
        'theme' => 'light',
        'show_contact' => 'true',
        'show_pedigree' => 'false',
        'max_width' => '400px'
    ), $atts);

    $widget_id = 'reptidex-animal-' . $atts['animal_id'];
    
    ob_start();
    ?>
    <div id="<?php echo esc_attr($widget_id); ?>"></div>
    <script>
    document.addEventListener('DOMContentLoaded', function() {
        reptidex.renderAnimalProfile('<?php echo esc_js($widget_id); ?>', {
            animalId: '<?php echo esc_js($atts['animal_id']); ?>',
            theme: '<?php echo esc_js($atts['theme']); ?>',
            showContact: <?php echo $atts['show_contact'] === 'true' ? 'true' : 'false'; ?>,
            showPedigree: <?php echo $atts['show_pedigree'] === 'true' ? 'true' : 'false'; ?>,
            maxWidth: '<?php echo esc_js($atts['max_width']); ?>'
        });
    });
    </script>
    <?php
    return ob_get_clean();
}
add_shortcode('reptidex_animal', 'reptidex_animal_profile_shortcode');
Usage in WordPress:
[reptidex_animal animal_id="123" theme="dark" show_contact="true"]

Shopify Integration

Shopify App Integration:
<!-- Shopify theme integration -->
{% comment %} reptidex Widget Section {% endcomment %}
<div class="reptidex-widget-section">
  <div id="reptidex-marketplace-{{ breeder.id }}"></div>
</div>

<script>
  document.addEventListener('DOMContentLoaded', function() {
    reptidex.renderMarketplaceGrid('reptidex-marketplace-{{ breeder.id }}', {
      breederId: '{{ breeder.reptidex_id }}',
      maxItems: {{ section.settings.max_items | default: 12 }},
      showFilters: {{ section.settings.show_filters | default: true }},
      theme: {
        colors: {
          primary: '{{ section.settings.primary_color | default: "#2563eb" }}',
          background: '{{ section.settings.background_color | default: "#ffffff" }}'
        }
      }
    });
  });
</script>

{% schema %}
{
  "name": "reptidex Marketplace",
  "settings": [
    {
      "type": "text",
      "id": "breeder_id",
      "label": "reptidex Breeder ID"
    },
    {
      "type": "range",
      "id": "max_items",
      "min": 6,
      "max": 24,
      "step": 6,
      "label": "Maximum items to display",
      "default": 12
    }
  ]
}
{% endschema %}

Error Handling & Fallbacks

Graceful Degradation

Error Boundary Implementation:
class WidgetErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log error to reptidex analytics
    this.logErrorToService(error, errorInfo);
  }

  private logErrorToService(error: Error, errorInfo: React.ErrorInfo) {
    fetch(`${API_BASE}/widget-errors`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        error: error.message,
        stack: error.stack,
        componentStack: errorInfo.componentStack,
        timestamp: new Date().toISOString(),
        url: window.location.href,
        userAgent: navigator.userAgent
      })
    }).catch(() => {
      // Silently fail if error logging fails
    });
  }

  render() {
    if (this.state.hasError) {
      return (
        <ErrorFallback 
          error={this.state.error}
          onRetry={() => this.setState({ hasError: false, error: null })}
        />
      );
    }

    return this.props.children;
  }
}
Fallback Components:
const ErrorFallback = ({ error, onRetry }: ErrorFallbackProps) => (
  <div className="reptidex-error-fallback">
    <div className="error-icon">⚠️</div>
    <h3>Unable to load reptidex widget</h3>
    <p>There was an error loading this content.</p>
    <button onClick={onRetry} className="retry-button">
      Try Again
    </button>
    <div className="powered-by">
      Powered by <a href="https://reptidex.com">reptidex</a>
    </div>
  </div>
);

const OfflineFallback = () => (
  <div className="reptidex-offline-fallback">
    <div className="offline-icon">📡</div>
    <h3>Content temporarily unavailable</h3>
    <p>Please check your internet connection and try again.</p>
  </div>
);

Security & Privacy

Secure Widget Implementation

Content Security Policy:
// CSP-safe implementation
const SecureWidget = ({ config }: WidgetProps) => {
  // Sanitize all user inputs
  const sanitizedConfig = sanitizeConfig(config);
  
  // Use nonce-based inline scripts
  const scriptNonce = generateNonce();
  
  return (
    <WidgetContainer>
      <WidgetContent config={sanitizedConfig} />
      <script nonce={scriptNonce}>
        {generateSecureScript(sanitizedConfig)}
      </script>
    </WidgetContainer>
  );
};

const sanitizeConfig = (config: any): WidgetConfig => {
  return {
    animalId: validateUUID(config.animalId),
    theme: validateTheme(config.theme),
    maxWidth: validateCSSValue(config.maxWidth),
    // ... other validations
  };
};
Data Privacy:
  • No tracking cookies without consent
  • Minimal data collection
  • GDPR compliance for EU visitors
  • Configurable privacy settings
  • Secure API communications (HTTPS only)

Testing & Quality Assurance

Widget Testing Strategy

Testing Framework:
describe('Animal Profile Widget', () => {
  beforeEach(() => {
    // Mock API responses
    mockAPI.setup();
  });

  it('renders correctly with valid animal ID', async () => {
    const container = document.createElement('div');
    await reptidex.renderAnimalProfile(container.id, {
      animalId: 'valid-id',
      theme: 'light'
    });

    expect(container.querySelector('.animal-name')).toBeInTheDocument();
    expect(container.querySelector('.animal-image')).toBeInTheDocument();
  });

  it('shows error fallback for invalid animal ID', async () => {
    const container = document.createElement('div');
    await reptidex.renderAnimalProfile(container.id, {
      animalId: 'invalid-id'
    });

    expect(container.querySelector('.reptidex-error-fallback')).toBeInTheDocument();
  });

  it('handles network errors gracefully', async () => {
    mockAPI.mockNetworkError();
    
    const container = document.createElement('div');
    await reptidex.renderAnimalProfile(container.id, {
      animalId: 'valid-id'
    });

    expect(container.querySelector('.reptidex-offline-fallback')).toBeInTheDocument();
  });
});
Cross-Browser Compatibility:
  • Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
  • Mobile browsers (iOS Safari, Chrome Mobile)
  • Internet Explorer 11 (with polyfills)
  • Testing with BrowserStack automation

Deployment & Distribution

CDN Distribution

CDN Configuration:
// CDN deployment configuration
const CDN_ENDPOINTS = {
  primary: 'https://cdn.reptidex.com',
  fallback: 'https://widgets.reptidex.com',
  development: 'https://dev-cdn.reptidex.com'
};

const loadFromCDN = async (path: string) => {
  for (const endpoint of Object.values(CDN_ENDPOINTS)) {
    try {
      const response = await fetch(`${endpoint}${path}`);
      if (response.ok) return response;
    } catch (error) {
      console.warn(`Failed to load from ${endpoint}, trying next...`);
    }
  }
  throw new Error('All CDN endpoints failed');
};
Versioning Strategy:
  • Semantic versioning for widget releases
  • Backward compatibility for at least 2 major versions
  • Graceful deprecation warnings
  • Automatic fallbacks to stable versions

web-embed extends reptidex’s reach by enabling seamless integration into external websites while maintaining performance, security, and user experience standards.