DEV Community

Kishan Srivastava
Kishan Srivastava

Posted on

iptester-redesigned

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

// --- Keyframe Animations ---

const fadeIn = keyframes`
  from { 
    opacity: 0; 
    transform: translateY(20px) scale(0.95); 
  }
  to { 
    opacity: 1; 
    transform: translateY(0) scale(1); 
  }
`;

const slideIn = keyframes`
  from { 
    opacity: 0; 
    transform: translateX(-20px); 
  }
  to { 
    opacity: 1; 
    transform: translateX(0); 
  }
`;

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

const pulse = keyframes`
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.05); }
`;

const gradientShift = keyframes`
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
`;

const float = keyframes`
  0%, 100% { transform: translateY(0px); }
  50% { transform: translateY(-10px); }
`;

// --- Modern Styled Components ---

const Container = styled.div`
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  padding: 2rem 1rem;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  position: relative;

  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: 
      radial-gradient(circle at 20% 50%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
      radial-gradient(circle at 80% 20%, rgba(255, 118, 117, 0.3) 0%, transparent 50%),
      radial-gradient(circle at 40% 80%, rgba(255, 177, 153, 0.3) 0%, transparent 50%);
    pointer-events: none;
  }
`;

const MaxWidthWrapper = styled.div`
  max-width: 1400px;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 2rem;
  position: relative;
  z-index: 1;
`;

const Card = styled.div`
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(20px);
  border-radius: 24px;
  box-shadow: 
    0 32px 64px -12px rgba(0, 0, 0, 0.25),
    0 0 0 1px rgba(255, 255, 255, 0.05);
  padding: 2rem;
  border: 1px solid rgba(255, 255, 255, 0.2);
  animation: ${fadeIn} 0.6s ease-out;
  transition: all 0.3s ease;

  &:hover {
    transform: translateY(-2px);
    box-shadow: 
      0 40px 80px -12px rgba(0, 0, 0, 0.3),
      0 0 0 1px rgba(255, 255, 255, 0.1);
  }
`;

const Header = styled.div`
  text-align: center;
  margin-bottom: 1.5rem;
  animation: ${slideIn} 0.8s ease-out;
`;

const Title = styled.h1`
  font-size: 3rem;
  font-weight: 800;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
  background-size: 200% 200%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  margin-bottom: 0.75rem;
  animation: ${gradientShift} 4s ease infinite;
  line-height: 1.2;
`;

const Subtitle = styled.p`
  font-size: 1.25rem;
  color: #64748b;
  font-weight: 500;
  max-width: 600px;
  margin: 0 auto;
  line-height: 1.6;
`;

const SectionTitle = styled.h2`
  font-size: 1.75rem;
  font-weight: 700;
  background: linear-gradient(135deg, #1e293b, #475569);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  margin-bottom: 1.5rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;

  &::before {
    content: '';
    width: 4px;
    height: 24px;
    background: linear-gradient(135deg, #667eea, #764ba2);
    border-radius: 2px;
  }
`;

const ConfigGrid = styled.div`
  display: grid;
  gap: 2rem;
  margin-bottom: 2rem;
`;

const InputGroup = styled.div`
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  gap: 1rem;
  align-items: end;

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

const DiscreteInputGroup = styled.div`
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 1rem;
  align-items: end;

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

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

const Label = styled.label`
  font-size: 0.875rem;
  font-weight: 600;
  color: #374151;
  letter-spacing: 0.025em;
`;

const Input = styled.input`
  width: 100%;
  padding: 1rem 1.25rem;
  border: 2px solid #e2e8f0;
  border-radius: 16px;
  font-size: 1rem;
  color: #1f2937;
  background: rgba(255, 255, 255, 0.8);
  backdrop-filter: blur(10px);
  transition: all 0.3s ease;
  font-weight: 500;

  &::placeholder {
    color: #9ca3af;
    font-weight: 400;
  }

  &:focus {
    outline: none;
    border-color: #667eea;
    background: rgba(255, 255, 255, 0.95);
    box-shadow: 
      0 0 0 4px rgba(102, 126, 234, 0.1),
      0 8px 25px -5px rgba(102, 126, 234, 0.2);
    transform: translateY(-1px);
  }
`;

const Button = styled.button`
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 1rem 2rem;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  font-weight: 600;
  border-radius: 16px;
  border: none;
  cursor: pointer;
  transition: all 0.3s ease;
  white-space: nowrap;
  font-size: 0.975rem;
  letter-spacing: 0.025em;
  position: relative;
  overflow: hidden;

  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: -100%;
    width: 100%;
    height: 100%;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
    transition: left 0.5s;
  }

  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 15px 35px -5px rgba(102, 126, 234, 0.4);

    &::before {
      left: 100%;
    }
  }

  &:active {
    transform: translateY(0);
  }

  &:disabled {
    background: linear-gradient(135deg, #9ca3af, #6b7280);
    cursor: not-allowed;
    transform: none;
    box-shadow: none;

    &::before {
      display: none;
    }
  }
`;

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

const ErrorMessage = styled.div`
  background: linear-gradient(135deg, #fee2e2, #fecaca);
  color: #dc2626;
  font-size: 0.875rem;
  padding: 1rem 1.5rem;
  border-radius: 12px;
  text-align: center;
  border: 1px solid #fca5a5;
  font-weight: 500;
  animation: ${fadeIn} 0.3s ease-out;
`;

const IpCardsGrid = styled.div`
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 1.5rem;
  margin-top: 2rem;
`;

const IpCardContainer = styled.div`
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(20px);
  border-radius: 20px;
  box-shadow: 
    0 20px 40px -12px rgba(0, 0, 0, 0.15),
    0 0 0 1px rgba(255, 255, 255, 0.1);
  padding: 1.5rem;
  border: 1px solid rgba(255, 255, 255, 0.2);
  display: flex;
  flex-direction: column;
  gap: 1rem;
  animation: ${fadeIn} 0.5s ease-out;
  transition: all 0.3s ease;
  position: relative;
  overflow: hidden;

  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 4px;
    background: linear-gradient(135deg, #667eea, #764ba2);
    border-radius: 20px 20px 0 0;
  }

  &:hover {
    transform: translateY(-4px) scale(1.02);
    box-shadow: 
      0 32px 64px -12px rgba(0, 0, 0, 0.25),
      0 0 0 1px rgba(255, 255, 255, 0.2);
  }
`;

const IpCardHeader = styled.h3`
  font-size: 1.5rem;
  font-weight: 700;
  color: #1f2937;
  margin: 0;
  font-family: 'Monaco', 'Menlo', monospace;
  background: linear-gradient(135deg, #1f2937, #374151);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
`;

const IpCardDetail = styled.div`
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.925rem;
  padding: 0.5rem 0;
`;

const IpCardLabel = styled.span`
  font-weight: 600;
  color: #6b7280;
  font-size: 0.875rem;
`;

const IpCardValue = styled.span`
  font-weight: 700;
  color: #1f2937;
  font-size: 0.875rem;
`;

const StatusBadge = styled.span`
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  border-radius: 50px;
  font-size: 0.875rem;
  font-weight: 600;
  transition: all 0.3s ease;

  ${(props) => {
    switch (props.type) {
      case 'enabled':
        return css`
          background: linear-gradient(135deg, #d1fae5, #a7f3d0);
          color: #059669;
          border: 1px solid #6ee7b7;
          animation: ${pulse} 2s ease-in-out infinite;
        `;
      case 'disabled':
        return css`
          background: linear-gradient(135deg, #fee2e2, #fecaca);
          color: #dc2626;
          border: 1px solid #fca5a5;
        `;
      case 'loading':
        return css`
          background: linear-gradient(135deg, #eff6ff, #dbeafe);
          color: #2563eb;
          border: 1px solid #93c5fd;
        `;
      case 'error':
        return css`
          background: linear-gradient(135deg, #fee2e2, #fecaca);
          color: #dc2626;
          border: 1px solid #fca5a5;
        `;
      default:
        return css`
          background: linear-gradient(135deg, #f3f4f6, #e5e7eb);
          color: #6b7280;
          border: 1px solid #d1d5db;
        `;
    }
  }}
`;

const TestButton = styled(Button)`
  width: 100%;
  margin-top: 0.5rem;
  background: linear-gradient(135deg, #4f46e5, #7c3aed);
  font-size: 0.875rem;
  padding: 0.875rem 1.5rem;

  &:hover {
    box-shadow: 0 15px 35px -5px rgba(79, 70, 229, 0.4);
  }

  &:disabled {
    background: linear-gradient(135deg, #9ca3af, #6b7280);
  }
`;

const EmptyState = styled.div`
  text-align: center;
  padding: 4rem 2rem;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(20px);
  border-radius: 24px;
  box-shadow: 
    0 32px 64px -12px rgba(0, 0, 0, 0.15),
    0 0 0 1px rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.2);
  animation: ${fadeIn} 0.6s ease-out;
  position: relative;
  overflow: hidden;

  &::before {
    content: '';
    position: absolute;
    top: -50%;
    left: -50%;
    width: 200%;
    height: 200%;
    background: 
      radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 50%);
    animation: ${float} 6s ease-in-out infinite;
  }
`;

const EmptyStateIcon = styled.div`
  font-size: 4rem;
  margin-bottom: 1.5rem;
  display: block;
  background: linear-gradient(135deg, #667eea, #764ba2);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: ${float} 3s ease-in-out infinite;
`;

const EmptyStateTitle = styled.h3`
  font-size: 1.75rem;
  font-weight: 700;
  background: linear-gradient(135deg, #1f2937, #374151);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  margin-bottom: 1rem;
`;

const EmptyStateText = styled.p`
  font-size: 1.125rem;
  color: #6b7280;
  margin-bottom: 1.5rem;
  line-height: 1.6;
  max-width: 400px;
  margin-left: auto;
  margin-right: auto;
`;

const EmptyStateNote = styled.p`
  font-size: 0.925rem;
  color: #9ca3af;
  font-style: italic;
  background: rgba(102, 126, 234, 0.05);
  padding: 1rem 1.5rem;
  border-radius: 12px;
  border: 1px solid rgba(102, 126, 234, 0.1);
  max-width: 500px;
  margin: 0 auto;
`;

const StatsBar = styled.div`
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 1rem;
  margin-top: 2rem;
  padding: 1.5rem;
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(10px);
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.3);
`;

const StatItem = styled.div`
  text-align: center;
  padding: 0.5rem;
`;

const StatValue = styled.div`
  font-size: 1.5rem;
  font-weight: 800;
  background: linear-gradient(135deg, #667eea, #764ba2);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
`;

const StatLabel = styled.div`
  font-size: 0.75rem;
  color: #6b7280;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-top: 0.25rem;
`;

// --- Helper Functions for IP Conversion ---
const ipToNumber = (ip) => {
  return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
};

const numberToIp = (num) => {
  return [
    (num >>> 24) & 0xFF,
    (num >>> 16) & 0xFF,
    (num >>> 8) & 0xFF,
    num & 0xFF
  ].join('.');
};

// --- IP Card Component ---
const IpCard = ({ ipData, featureFlagName, onTestFlag }) => {
  const { id, ipAddress, testResult } = ipData;

  const handleTestClick = () => {
    onTestFlag(id, ipAddress);
  };

  return (
    <IpCardContainer>
      <IpCardHeader>🌐 {ipAddress}</IpCardHeader>
      <IpCardDetail>
        <IpCardLabel>Status:</IpCardLabel>
        {testResult ? (
          testResult.isLoading ? (
            <StatusBadge type="loading">
              <Spinner style={{ width: '1rem', height: '1rem', borderTopColor: '#2563eb' }} />
              Testing...
            </StatusBadge>
          ) : testResult.error ? (
            <StatusBadge type="error">❌ Error</StatusBadge>
          ) : testResult.enabled ? (
            <StatusBadge type="enabled">βœ… Enabled</StatusBadge>
          ) : (
            <StatusBadge type="disabled">❌ Disabled</StatusBadge>
          )
        ) : (
          <StatusBadge>⏳ Not Tested</StatusBadge>
        )}
      </IpCardDetail>
      {testResult && testResult.timestamp && (
        <IpCardDetail>
          <IpCardLabel>Last Test:</IpCardLabel>
          <IpCardValue>{testResult.timestamp}</IpCardValue>
        </IpCardDetail>
      )}
      <TestButton onClick={handleTestClick} disabled={testResult?.isLoading}>
        {testResult?.isLoading ? (
          <>
            <Spinner style={{ width: '1rem', height: '1rem' }} />
            Testing...
          </>
        ) : (
          <>
            πŸ§ͺ Test Flag
          </>
        )}
      </TestButton>
    </IpCardContainer>
  );
};

// --- Main IP Range Simulator Component ---
const IPRangeSimulator = () => {
  const [featureFlagName, setFeatureFlagName] = useState('geo-targeted-feature');
  const [startIp, setStartIp] = useState('192.168.1.1');
  const [endIp, setEndIp] = useState('192.168.1.10');
  const [discreteIp, setDiscreteIp] = useState('');
  const [ipCards, setIpCards] = useState([]);
  const [errorMessage, setErrorMessage] = useState('');

  const unleashClient = useUnleashClient();
  const updateContext = useUnleashContext();

  // Calculate stats
  const stats = {
    total: ipCards.length,
    tested: ipCards.filter(card => card.testResult && !card.testResult.isLoading).length,
    enabled: ipCards.filter(card => card.testResult && card.testResult.enabled && !card.testResult.error).length,
    disabled: ipCards.filter(card => card.testResult && !card.testResult.enabled && !card.testResult.error).length,
  };

  const addRangeCards = useCallback(() => {
    setErrorMessage('');
    try {
      const startNum = ipToNumber(startIp);
      const endNum = ipToNumber(endIp);

      if (isNaN(startNum) || isNaN(endNum) || startNum > endNum) {
        setErrorMessage('Invalid IP range. Please enter valid start and end IPs.');
        return;
      }

      const newCards = [];
      for (let i = startNum; i <= endNum; i++) {
        newCards.push({
          id: crypto.randomUUID(),
          ipAddress: numberToIp(i),
          testResult: null,
        });
        if (newCards.length > 100) {
          setErrorMessage('Too many IPs in range. Max 100 cards allowed per range generation.');
          break;
        }
      }
      setIpCards(prevCards => [...prevCards, ...newCards]);
      setStartIp('');
      setEndIp('');
    } catch (e) {
      setErrorMessage('Error parsing IP range. Please check format.');
      console.error(e);
    }
  }, [startIp, endIp]);

  const addDiscreteCard = useCallback(() => {
    setErrorMessage('');
    if (!discreteIp || !/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(discreteIp)) {
      setErrorMessage('Please enter a valid discrete IP address.');
      return;
    }
    setIpCards(prevCards => [
      ...prevCards,
      {
        id: crypto.randomUUID(),
        ipAddress: discreteIp,
        testResult: null,
      },
    ]);
    setDiscreteIp('');
  }, [discreteIp]);

  const handleTestFlag = useCallback(async (cardId, ipAddress) => {
    setIpCards(prevCards =>
      prevCards.map(card =>
        card.id === cardId ? { ...card, testResult: { ...card.testResult, isLoading: true, error: false } } : card
      )
    );
    setErrorMessage('');

    try {
      await updateContext({
        remoteAddress: ipAddress,
        userId: 'simulator-user-' + ipAddress.replace(/\./g, '-'),
      });
      await new Promise((resolve) => setTimeout(resolve, 100));

      const isEnabled = unleashClient.isEnabled(featureFlagName);

      setIpCards(prevCards =>
        prevCards.map(card =>
          card.id === cardId
            ? {
                ...card,
                testResult: {
                  ip: ipAddress,
                  enabled: isEnabled,
                  timestamp: new Date().toLocaleTimeString(),
                  isLoading: false,
                  error: false,
                },
              }
            : card
        )
      );
    } catch (error) {
      console.error(`Error testing flag for IP ${ipAddress}:`, error);
      setIpCards(prevCards =>
        prevCards.map(card =>
          card.id === cardId
            ? {
                ...card,
                testResult: {
                  ip: ipAddress,
                  enabled: false,
                  timestamp: new Date().toLocaleTimeString(),
                  isLoading: false,
                  error: true,
                },
              }
            : card
        )
      );
      setErrorMessage(`Failed to test flag for ${ipAddress}. Check console.`);
    }
  }, [featureFlagName, updateContext, unleashClient]);

  return (
    <Container>
      <MaxWidthWrapper>
        {/* Header */}
        <Card>
          <Header>
            <Title>πŸ—ΊοΈ Unleash IP Strategy Simulator</Title>
            <Subtitle>
              Generate and test feature flag status for multiple IP addresses with modern, intuitive interface.
            </Subtitle>
          </Header>
        </Card>

        {/* Configuration Panel */}
        <Card>
          <SectionTitle>βš™οΈ Configuration & IP Generation</SectionTitle>

          <ConfigGrid>
            <InputField>
              <Label htmlFor="flagName">🚩 Feature Flag Name</Label>
              <Input
                id="flagName"
                type="text"
                value={featureFlagName}
                onChange={(e) => setFeatureFlagName(e.target.value)}
                placeholder="e.g., geo-targeted-feature"
              />
            </InputField>

            <InputGroup>
              <InputField>
                <Label htmlFor="startIp">πŸš€ Start IP (Range)</Label>
                <Input
                  id="startIp"
                  type="text"
                  value={startIp}
                  onChange={(e) => setStartIp(e.target.value)}
                  placeholder="e.g., 192.168.1.1"
                />
              </InputField>
              <InputField>
                <Label htmlFor="endIp">🏁 End IP (Range)</Label>
                <Input
                  id="endIp"
                  type="text"
                  value={endIp}
                  onChange={(e) => setEndIp(e.target.value)}
                  placeholder="e.g., 192.168.1.10"
                />
              </InputField>
              <Button onClick={addRangeCards}>
                πŸ“Š Generate Range
              </Button>
            </InputGroup>

            <DiscreteInputGroup>
              <InputField>
                <Label htmlFor="discreteIp">🎯 Discrete IP Address</Label>
                <Input

Enter fullscreen mode Exit fullscreen mode
<DiscreteInputGroup>
              <InputField>
                <Label htmlFor="discreteIp">🎯 Discrete IP Address</Label>
                <Input
                  id="discreteIp"
                  type="text"
                  value={discreteIp}
                  onChange={(e) => setDiscreteIp(e.target.value)}
                  placeholder="e.g., 203.0.113.45"
                />
              </InputField>
              <Button onClick={addDiscreteCard}>
                βž• Add IP
              </Button>
            </DiscreteInputGroup>
          </ConfigGrid>

          {errorMessage && (
            <ErrorMessage>{errorMessage}</ErrorMessage>
          )}
        </Card>

        {/* Stats Bar */}
        {ipCards.length > 0 && (
          <StatsBar>
            <StatItem>
              <StatValue>{stats.total}</StatValue>
              <StatLabel>Total IPs</StatLabel>
            </StatItem>
            <StatItem>
              <StatValue>{stats.tested}</StatValue>
              <StatLabel>Tested</StatLabel>
            </StatItem>
            <StatItem>
              <StatValue>{stats.enabled}</StatValue>
              <StatLabel>Enabled</StatLabel>
            </StatItem>
            <StatItem>
              <StatValue>{stats.disabled}</StatValue>
              <StatLabel>Disabled</StatLabel>
            </StatItem>
          </StatsBar>
        )}

        {/* IP Cards Display */}
        {ipCards.length > 0 ? (
          <IpCardsGrid>
            {ipCards.map((ipData) => (
              <IpCard
                key={ipData.id}
                ipData={ipData}
                featureFlagName={featureFlagName}
                onTestFlag={handleTestFlag}
              />
            ))}
          </IpCardsGrid>
        ) : (
          <EmptyState>
            <EmptyStateIcon>πŸš€</EmptyStateIcon>
            <EmptyStateTitle>Ready to Start Testing</EmptyStateTitle>
            <EmptyStateText>
              Enter an IP range or discrete IP address above to generate cards and begin testing your feature flags.
            </EmptyStateText>
            <EmptyStateNote>
              πŸ’‘ Ensure your feature flag "{featureFlagName}" has an "IP Address" strategy configured in Unleash for accurate testing results.
            </EmptyStateNote>
          </EmptyState>
        )}
      </MaxWidthWrapper>
    </Container>
  );
};

export default IPRangeSimulator;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)