DEV Community

Nebula
Nebula

Posted on

mvt-redesigned

import React, { useState, useEffect, useCallback } from 'react';
import { useUnleashContext, useUnleashClient } from '@unleash/proxy-client-react';
import styled, { keyframes } from 'styled-components';

// --- Simple Animations ---
const fadeIn = keyframes`
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
`;

const spin = keyframes`
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
`;

// --- Layout Components ---
const Container = styled.div`
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  padding: 2rem 1rem;
`;

const MainContent = styled.div`
  max-width: 1200px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 2rem;
`;

const Card = styled.div`
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(10px);
  border-radius: 20px;
  padding: 2rem;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.2);
  animation: ${fadeIn} 0.6s ease-out;
`;

// --- Header Components ---
const Header = styled.div`
  text-align: center;
  margin-bottom: 1rem;
`;

const Title = styled.h1`
  font-size: 2.5rem;
  font-weight: 700;
  background: linear-gradient(135deg, #667eea, #764ba2);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  margin-bottom: 0.5rem;
`;

const Subtitle = styled.p`
  font-size: 1.1rem;
  color: #64748b;
  font-weight: 400;
`;

// --- Form Components ---
const ConfigSection = styled.div`
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 1.5rem;
  align-items: end;

  @media (max-width: 768px) {
    grid-template-columns: 1fr;
  }
`;

const InputGroup = styled.div`
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
`;

const Label = styled.label`
  font-size: 0.9rem;
  font-weight: 600;
  color: #374151;
`;

const Input = styled.input`
  padding: 0.875rem 1rem;
  border: 2px solid #e5e7eb;
  border-radius: 12px;
  font-size: 1rem;
  transition: all 0.2s ease;
  background: white;

  &:focus {
    outline: none;
    border-color: #667eea;
    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
  }
`;

const Button = styled.button`
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.875rem 1.5rem;
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  border: none;
  border-radius: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);

  &:hover:not(:disabled) {
    transform: translateY(-2px);
    box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
  }

  &:disabled {
    opacity: 0.7;
    cursor: not-allowed;
    transform: none;
  }
`;

const Spinner = styled.div`
  width: 18px;
  height: 18px;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-top: 2px solid white;
  border-radius: 50%;
  animation: ${spin} 1s linear infinite;
`;

// --- Status Components ---
const StatusBanner = styled.div`
  background: linear-gradient(135deg, #3b82f6, #1d4ed8);
  color: white;
  padding: 1rem 1.5rem;
  border-radius: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  animation: ${fadeIn} 0.3s ease-out;
`;

const StatusSpinner = styled(Spinner)`
  border-color: rgba(255, 255, 255, 0.3);
  border-top-color: white;
`;

// --- Stats Components ---
const StatsGrid = styled.div`
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1.5rem;
`;

const StatCard = styled(Card)`
  text-align: center;
  padding: 1.5rem;
  background: linear-gradient(135deg, #f8fafc, #e2e8f0);
`;

const StatValue = styled.div`
  font-size: 2.5rem;
  font-weight: 800;
  color: ${props => props.color || '#1e293b'};
  margin-bottom: 0.5rem;
`;

const StatLabel = styled.div`
  font-size: 0.9rem;
  color: #64748b;
  font-weight: 500;
`;

// --- Variant Display Components ---
const VariantGrid = styled.div`
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1.5rem;
  margin-top: 1.5rem;
`;

const VariantCard = styled.div`
  background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
  border-radius: 16px;
  padding: 1.5rem;
  text-align: center;
  border: 2px solid rgba(255, 255, 255, 0.5);
`;

const VariantName = styled.h4`
  font-size: 1.1rem;
  font-weight: 600;
  color: #334155;
  margin-bottom: 1rem;
`;

const SimulatedButton = styled.button`
  padding: 0.75rem 1.5rem;
  border-radius: 10px;
  font-weight: 600;
  color: white;
  background: ${props => props.color || '#3b82f6'};
  border: none;
  cursor: pointer;
  transition: transform 0.2s ease;

  &:hover {
    transform: scale(1.05);
  }
`;

const VariantDescription = styled.p`
  font-size: 0.85rem;
  color: #64748b;
  margin-top: 0.75rem;
`;

// --- Progress Components ---
const ProgressContainer = styled.div`
  margin: 1.5rem 0;
`;

const ProgressBar = styled.div`
  width: 100%;
  height: 8px;
  background: #e2e8f0;
  border-radius: 4px;
  overflow: hidden;
  display: flex;
`;

const ProgressSegment = styled.div`
  height: 100%;
  background: ${props => props.color || '#3b82f6'};
  width: ${props => props.percentage}%;
  transition: width 0.5s ease;
`;

const ProgressLabels = styled.div`
  display: flex;
  justify-content: space-between;
  margin-top: 0.75rem;
  font-size: 0.85rem;
  color: #64748b;
`;

// --- Table Components ---
const TableContainer = styled.div`
  overflow-x: auto;
  border-radius: 12px;
  border: 1px solid #e5e7eb;
`;

const Table = styled.table`
  width: 100%;
  border-collapse: collapse;
  background: white;
`;

const TableHeader = styled.th`
  padding: 1rem 1.5rem;
  text-align: left;
  background: #f8fafc;
  font-weight: 600;
  color: #374151;
  font-size: 0.875rem;
  border-bottom: 1px solid #e5e7eb;
`;

const TableRow = styled.tr`
  &:nth-child(even) {
    background: #f9fafb;
  }

  &:hover {
    background: #f3f4f6;
  }
`;

const TableCell = styled.td`
  padding: 1rem 1.5rem;
  border-bottom: 1px solid #e5e7eb;
  font-size: 0.9rem;
  color: #374151;
`;

// --- User Display Components ---
const UserInfo = styled.div`
  display: flex;
  align-items: center;
  gap: 0.75rem;
`;

const UserAvatar = styled.div`
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: linear-gradient(135deg, #667eea, #764ba2);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 600;
  font-size: 0.85rem;
`;

const UserDetails = styled.div``;

const UserName = styled.div`
  font-weight: 500;
  color: #1f2937;
`;

const UserEmail = styled.div`
  font-size: 0.8rem;
  color: #6b7280;
`;

// --- Badge Components ---
const Badge = styled.span`
  padding: 0.25rem 0.75rem;
  border-radius: 20px;
  font-size: 0.75rem;
  font-weight: 600;
  background: ${props => {
    switch (props.type) {
      case 'Premium': return 'linear-gradient(135deg, #fbbf24, #f59e0b)';
      case 'Gold': return 'linear-gradient(135deg, #3b82f6, #1d4ed8)';
      case 'Standard': return 'linear-gradient(135deg, #8b5cf6, #7c3aed)';
      default: return '#e5e7eb';
    }
  }};
  color: ${props => props.type ? 'white' : '#374151'};
`;

const StatusBadge = styled.span`
  padding: 0.25rem 0.75rem;
  border-radius: 20px;
  font-size: 0.75rem;
  font-weight: 600;
  background: ${props => {
    switch (props.type) {
      case 'enabled': return 'linear-gradient(135deg, #10b981, #059669)';
      case 'disabled': return 'linear-gradient(135deg, #ef4444, #dc2626)';
      case 'error': return 'linear-gradient(135deg, #ef4444, #dc2626)';
      default: return '#e5e7eb';
    }
  }};
  color: ${props => props.type ? 'white' : '#374151'};
`;

// --- Empty State Components ---
const EmptyState = styled(Card)`
  text-align: center;
  padding: 3rem 2rem;
`;

const EmptyIcon = styled.div`
  font-size: 4rem;
  margin-bottom: 1rem;
`;

const EmptyTitle = styled.h3`
  font-size: 1.5rem;
  font-weight: 600;
  color: #1f2937;
  margin-bottom: 0.5rem;
`;

const EmptyText = styled.p`
  color: #6b7280;
  margin-bottom: 1rem;
`;

const EmptyNote = styled.p`
  font-size: 0.85rem;
  color: #9ca3af;
  font-style: italic;
`;

const SectionTitle = styled.h2`
  font-size: 1.5rem;
  font-weight: 600;
  color: #1f2937;
  margin-bottom: 1rem;
`;

// --- Helper Functions ---
const generateRandomId = () => {
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
};

const generateDummyUsers = (count = 100) => {
  return Array.from({ length: count }, (_, i) => ({
    id: generateRandomId(),
    name: `Test User ${i + 1}`,
    email: `user${i + 1}@example.com`,
    accountType: i % 3 === 0 ? 'Premium' : i % 2 === 0 ? 'Gold' : 'Standard',
  }));
};

// --- Main Component ---
const ABTestSimulator = () => {
  const [testResults, setTestResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [currentTestingUser, setCurrentTestingUser] = useState('');
  const [featureFlagName, setFeatureFlagName] = useState('ab-test-variant-feature');
  const updateContext = useUnleashContext();
  const unleashClient = useUnleashClient();

  const expectedVariants = [
    { name: 'control-cta', buttonText: 'Learn More', buttonColor: '#3b82f6', description: 'Standard blue button' },
    { name: 'variant-a-cta', buttonText: 'Discover Now', buttonColor: '#10b981', description: 'Green button for discovery' },
    { name: 'variant-b-cta', buttonText: 'Explore Features', buttonColor: '#ef4444', description: 'Red button to explore' },
  ];

  const dummyUsers = generateDummyUsers(100);

  const runABCTest = useCallback(async () => {
    setIsLoading(true);
    setTestResults([]);
    const results = [];

    for (let i = 0; i < dummyUsers.length; i++) {
      const user = dummyUsers[i];
      setCurrentTestingUser(user.name);

      try {
        await updateContext({
          userId: user.id,
        });

        await new Promise((resolve) => setTimeout(resolve, 100));

        const variant = unleashClient.getVariant(featureFlagName);

        results.push({
          ...user,
          variantName: variant.name,
          variantPayload: variant.payload ? JSON.parse(variant.payload.value) : null,
          testTimestamp: new Date().toLocaleTimeString(),
          flagEnabled: variant.enabled,
        });
      } catch (error) {
        console.error(`Error testing user ${user.id}:`, error);
        results.push({
          ...user,
          variantName: 'error',
          variantPayload: null,
          testTimestamp: new Date().toLocaleTimeString(),
          flagEnabled: false,
          error: true,
        });
      }
    }

    setTestResults(results);
    setCurrentTestingUser('');
    setIsLoading(false);
  }, [dummyUsers, featureFlagName, updateContext, unleashClient]);

  const calculateStats = useCallback(() => {
    if (testResults.length === 0) {
      return {
        total: 0,
        variants: expectedVariants.map(v => ({
          name: v.name,
          count: 0,
          percentage: 0,
          color: v.buttonColor,
          buttonText: v.buttonText,
        })),
        totalEnabled: 0,
        totalEnabledPercentage: 0,
      };
    }

    const variantCounts = {};
    let totalEnabled = 0;

    testResults.forEach((result) => {
      if (result.flagEnabled) {
        totalEnabled++;
        variantCounts[result.variantName] = (variantCounts[result.variantName] || 0) + 1;
      }
    });

    const stats = expectedVariants.map(v => {
      const count = variantCounts[v.name] || 0;
      const percentage = totalEnabled > 0 ? Math.round((count / totalEnabled) * 100) : 0;
      return {
        name: v.name,
        count,
        percentage,
        color: v.buttonColor,
        buttonText: v.buttonText,
      };
    });

    return {
      total: testResults.length,
      variants: stats,
      totalEnabled: totalEnabled,
      totalEnabledPercentage: testResults.length > 0 ? Math.round((totalEnabled / testResults.length) * 100) : 0,
    };
  }, [testResults, expectedVariants]);

  const stats = calculateStats();

  return (
    <Container>
      <MainContent>
        {/* Header */}
        <Card>
          <Header>
            <Title>✨ Unleash A/B/C Test Simulator</Title>
            <Subtitle>
              Simulate multivariate testing with real Unleash flag variants and observe distribution.
            </Subtitle>
          </Header>
        </Card>

        {/* Configuration */}
        <Card>
          <SectionTitle>Configuration</SectionTitle>
          <ConfigSection>
            <InputGroup>
              <Label htmlFor="flagName">Feature Flag Name</Label>
              <Input
                id="flagName"
                type="text"
                value={featureFlagName}
                onChange={(e) => setFeatureFlagName(e.target.value)}
                placeholder="e.g., ab-test-variant-feature"
              />
            </InputGroup>
            <Button onClick={runABCTest} disabled={isLoading}>
              {isLoading ? (
                <>
                  <Spinner />
                  Testing...
                </>
              ) : (
                <>🚀 Start A/B/C Test ({dummyUsers.length} Users)</>
              )}
            </Button>
          </ConfigSection>
        </Card>

        {/* Status */}
        {isLoading && (
          <StatusBanner>
            <StatusSpinner />
            <span>Currently testing: {currentTestingUser}</span>
          </StatusBanner>
        )}

        {/* Variant Preview */}
        <Card>
          <SectionTitle>Simulated Variant Preview</SectionTitle>
          <p style={{ color: '#64748b', fontSize: '0.9rem', marginBottom: '1rem' }}>
            These are the expected UI variations based on the payload configured in Unleash.
          </p>
          <VariantGrid>
            {expectedVariants.map((variant) => (
              <VariantCard key={variant.name}>
                <VariantName>{variant.name}</VariantName>
                <SimulatedButton color={variant.buttonColor}>
                  {variant.buttonText}
                </SimulatedButton>
                <VariantDescription>{variant.description}</VariantDescription>
              </VariantCard>
            ))}
          </VariantGrid>
        </Card>

        {/* Statistics */}
        {testResults.length > 0 && (
          <>
            <StatsGrid>
              <StatCard>
                <StatValue color="#3b82f6">{stats.total}</StatValue>
                <StatLabel>Total Users Tested</StatLabel>
              </StatCard>
              <StatCard>
                <StatValue color="#10b981">{stats.totalEnabled}</StatValue>
                <StatLabel>Flag Enabled Users</StatLabel>
              </StatCard>
              <StatCard>
                <StatValue color="#8b5cf6">{stats.totalEnabledPercentage}%</StatValue>
                <StatLabel>Total Rollout Percentage</StatLabel>
              </StatCard>
            </StatsGrid>

            <Card>
              <SectionTitle>Variant Distribution</SectionTitle>
              <ProgressContainer>
                <ProgressBar>
                  {stats.variants.map((variantStat) => (
                    <ProgressSegment
                      key={variantStat.name}
                      percentage={variantStat.percentage}
                      color={variantStat.color}
                    />
                  ))}
                </ProgressBar>
                <ProgressLabels>
                  {stats.variants.map((variantStat) => (
                    <span key={`label-${variantStat.name}`} style={{ color: variantStat.color }}>
                      {variantStat.name} ({variantStat.count})
                    </span>
                  ))}
                </ProgressLabels>
              </ProgressContainer>
            </Card>
          </>
        )}

        {/* Results Table */}
        {testResults.length > 0 && (
          <Card style={{ padding: 0 }}>
            <div style={{ padding: '2rem 2rem 1rem' }}>
              <SectionTitle style={{ margin: 0 }}>Detailed Test Results</SectionTitle>
            </div>
            <TableContainer>
              <Table>
                <thead>
                  <tr>
                    <TableHeader>User</TableHeader>
                    <TableHeader>Account Type</TableHeader>
                    <TableHeader>Assigned Variant</TableHeader>
                    <TableHeader>Flag Enabled</TableHeader>
                    <TableHeader>Test Time</TableHeader>
                  </tr>
                </thead>
                <tbody>
                  {testResults.map((result) => (
                    <TableRow key={result.id}>
                      <TableCell>
                        <UserInfo>
                          <UserAvatar>
                            {result.name
                              .split(' ')
                              .map((n) => n[0])
                              .join('')
                              .slice(0, 2)}
                          </UserAvatar>
                          <UserDetails>
                            <UserName>{result.name}</UserName>
                            <UserEmail>{result.email}</UserEmail>
                          </UserDetails>
                        </UserInfo>
                      </TableCell>
                      <TableCell>
                        <Badge type={result.accountType}>
                          {result.accountType}
                        </Badge>
                      </TableCell>
                      <TableCell>
                        {result.error ? (
                          <StatusBadge type="error">❌ Error</StatusBadge>
                        ) : result.flagEnabled ? (
                          <StatusBadge type="enabled">{result.variantName}</StatusBadge>
                        ) : (
                          <StatusBadge type="disabled">N/A (Disabled)</StatusBadge>
                        )}
                      </TableCell>
                      <TableCell>
                        {result.error ? (
                          <StatusBadge type="error">❌ No</StatusBadge>
                        ) : result.flagEnabled ? (
                          <StatusBadge type="enabled">✅ Yes</StatusBadge>
                        ) : (
                          <StatusBadge type="disabled">❌ No</StatusBadge>
                        )}
                      </TableCell>
                      <TableCell>{result.testTimestamp}</TableCell>
                    </TableRow>
                  ))}
                </tbody>
              </Table>
            </TableContainer>
          </Card>
        )}

        {/* Empty State */}
        {testResults.length === 0 && !isLoading && (
          <EmptyState>
            <EmptyIcon>📊</EmptyIcon>
            <EmptyTitle>Ready to Simulate A/B/C Tests</EmptyTitle>
            <EmptyText>
              Enter your feature flag name and click "Start Test" to see how Unleash distributes users across variants.
            </EmptyText>
            <EmptyNote>
              Ensure your flag "{featureFlagName}" is configured in Unleash with at least three variants and a gradual rollout strategy.
            </EmptyNote>
          </EmptyState>
        )}
      </MainContent>
    </Container>
  );
};

export default ABTestSimulator;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)