main 0dc2160ac970 cached
46 files
51.5 KB
15.2k tokens
30 symbols
1 requests
Download .txt
Repository: Omerisra6/internet-speed-test-react
Branch: main
Commit: 0dc2160ac970
Files: 46
Total size: 51.5 KB

Directory structure:
gitextract_krgre3tb/

├── .gitignore
├── README.md
├── client/
│   ├── .gitignore
│   ├── package.json
│   ├── public/
│   │   └── index.html
│   └── src/
│       ├── App.js
│       ├── components/
│       │   ├── Header.js
│       │   ├── Navigation.js
│       │   ├── UserDetails.js
│       │   └── view-components/
│       │       ├── Button.js
│       │       ├── Circle.js
│       │       ├── LinesSpeedometer/
│       │       │   ├── LinesSpeedometer.js
│       │       │   ├── LongLine.js
│       │       │   ├── ShortLine.js
│       │       │   └── StyledLinesSpeedometer.js
│       │       ├── Link.js
│       │       └── Logo.js
│       ├── contexts/
│       │   ├── appAttributesContext.js
│       │   └── testResultContext.js
│       ├── index.css
│       ├── index.js
│       ├── pages/
│       │   └── speed-test/
│       │       ├── SpeedTestPage.js
│       │       ├── SpeedTestPage.test.js
│       │       └── components/
│       │           ├── ClientDetail.js
│       │           ├── ClientDetails.js
│       │           ├── LiveChat.js
│       │           ├── ResultCard.js
│       │           ├── ResultsCardsContainer.js
│       │           ├── SpeedDetails/
│       │           │   ├── SpeedDetails.js
│       │           │   └── SpeedDetails.test.js
│       │           ├── SpeedTestChart.js
│       │           ├── Speedometer/
│       │           │   ├── Speedometer.js
│       │           │   └── StyledSpeedometer.js
│       │           ├── TestButton.js
│       │           ├── TestExtraDetail.js
│       │           ├── TestExtraDetails.js
│       │           └── TestResults/
│       │               ├── TestResults.js
│       │               └── TestResults.test.js
│       ├── setupTests.js
│       └── utils/
│           └── api.js
├── package.json
└── server/
    ├── .gitignore
    ├── api-handlers-helpers.js
    ├── api-handlers.js
    ├── index.js
    └── package.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.env 

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*


================================================
FILE: README.md
================================================

<h1 align="center">
  <br>
  <img src="https://cdn-icons-png.flaticon.com/512/10321/10321953.png" alt="Speed Test App" width="200">
  <br>
  <h2 align="center">Speed Test App</h2>
  <br>
</h1>

<h4 align="center">A single page internet speed test app built with<a href="https://react.dev/" target="_blank"> React</a> and <a href="https://nodejs.org/" target="_blank">Node.js</a>.</h4>
<p align="center"></p>
<p align="center">
  <a href="#key-features">Key Features</a> •
  <a href="#how-to-use">How To Use</a> •
  <a href="#credits">Credits</a> •
  <a href="#license">License</a>
</p>
<h2 align="center">
  Screenshots
  <br>
  <br>
  <img src="https://i.ibb.co/BsVkXMK/localhost-3000.png" alt="localhost-3000" border="0">
  <br>
  <img src="https://i.postimg.cc/x1mTJ7ww/localhost-3000-1.png" alt="localhost-3000-1" border="0" height="400">
</h2>


## Key Features

* Responsive design 
  - The app is optimized for desktop and mobile devices, ensuring a smooth experience for all users.
* User Details
  - Displays the user's IP address, location, server, and operating system.
* Internet Speed Test
  - Measures the user's upload and download speeds, as well as latency, for accurate results.
* Testing and Debugging
  - Includes tests to ensure the app works as expected.
* Error handling
  - The app includes error handling features to ensure that users are informed of any issues that arise.
* User-friendly interface

## How To Use

To clone and run this application, you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. From your command line:

```bash
# Clone this repository
$ git clone https://github.com/Omerisra6/internet-speed-test-react

# Install Dependencies
$ npm run install:all
```

Create an .env file in the root of your project.
<br>
To add a new environment variable for your API url, set the variable name as REACT_APP_API_URL and assign it to the server API url ( the deafult port is 8000).
<br>
```bash
REACT_APP_API_URL=SERVER_URL
```
Run the following command:
```bash
# Run the app
$ npm start
```

## Credits

This software uses the following open source packages:

- [React](https://react.dev/)
- [Node.js](https://nodejs.org/)
- [Network Speed](https://www.npmjs.com/package/network-speed)


## License

MIT

---

> GitHub [@omerisra6](https://github.com/Omerisra6) &nbsp;&middot;&nbsp;
> Linkedin [@omerisraeli](https://www.linkedin.com/in/omer-israeli6/)



================================================
FILE: client/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.env

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*


================================================
FILE: client/package.json
================================================
{
  "name": "internet-speed-test-react",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^0.27.2",
    "caniuse-lite": "^1.0.30001481",
    "material-icons": "^1.11.10",
    "prop-types": "^15.8.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "styled-components": "^5.3.5",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@testing-library/react": "^13.4.0",
    "dotenv": "^16.0.2",
    "jest": "^29.0.3",
    "jest-fetch-mock": "^3.0.3",
    "jest-watch-typeahead": "^0.6.5",
    "react-test-renderer": "^18.2.0"
  }
}


================================================
FILE: client/public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />  
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Speed Test</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>


================================================
FILE: client/src/App.js
================================================
import Header from './components/Header';
import SpeedTestPage from './pages/speed-test/SpeedTestPage';

function App() {
  return (
    <div className="App">

      <Header/>

      <SpeedTestPage/>

    </div>
  );
}

export default App;


================================================
FILE: client/src/components/Header.js
================================================
import React from 'react'
import Logo from './view-components/Logo'
import Navigation from './Navigation'
import styled from 'styled-components'
import UserDetails from './UserDetails'

const StyledHeader = styled.div`

  z-index: 1;
  height: 6vh;
  width: 94vw;
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 2vh 3vw;

  @media ( max-width: 640px ){

    & > .app-logo{
      
      font-size: 0.7rem;
    }
  }

`
export default function Header() {

  return (
    <StyledHeader className='header-container'>

      <Logo size="md"></Logo>

      <Navigation/>

      <UserDetails/>

    </StyledHeader>
  )
    
}


================================================
FILE: client/src/components/Navigation.js
================================================
import styled from 'styled-components'
import Link from './view-components/Link';

const StyledNavigation = styled.div`

    display: flex;
    justify-content: center;
    align-items: center;
    position: absolute;
    left: 50%;
    transform: translate( -50%);

    & > *{
        padding: 0 3vw;
        display: flex;
        align-items: center;
    }
`

const navButtons = 
[
    { 'icon': 'speed', 'url':'/', 'checked': true }, 
    { 'icon': 'pie_chart', 'url':'/stats', 'checked': false },
    { 'icon': 'language', 'url':'/language', 'checked': false }, 
    { 'icon': 'dns', 'url':'/dns' },
    { 'icon': 'tune', 'url':'/tune', 'checked': false }
];

export default function Navigation() {

    
    return (
        <StyledNavigation>

            { navButtons.map( ( navButton, index ) => {

                return <Link key={ index } index={ index } icon={ navButton.icon } url={ navButton.url } padding='md' size='sm' color={ navButton.checked ? 'orange': 'theme' } />

            })}
            
        </StyledNavigation>
    )
}


================================================
FILE: client/src/components/UserDetails.js
================================================
import React from 'react'
import styled from 'styled-components'
import { Circle } from './view-components/Circle'
import Link from './view-components/Link'
import logo from '../images/N-1.png'

const StyledUserDetails = styled.div`
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 2vw;

    & > *{
        height: 6vh;
        display: flex;
        align-items: center;
        padding: 0;
    }

    & > .user-logo-container > img{
        border-radius: 50%;
        width: 2vw;
        height: 2vw;
    }


    & > .chat-container > .chat-circle{
        position: absolute;
        top: 1vh;
        right: 1vw;
    }

    & > .chat-container > a{
        height: 6vh;
        display: flex;
        align-items: center;
    }

    & > .chat-container{
        position: relative;
    }

`
export default function UserDetails() {

    return (
        <StyledUserDetails>
            
            <div className='chat-container' key='chat-container'>
                <Circle color='red' size='xxs' className='chat-count'>1</Circle>
                <Link icon={'chat_bubble_outline'} url='/chat' size='xs' color='light'/>
            </div>
            
            <Link className='user-notifications' key='user-notifications' icon={'notifications_none'} url='/chat' size='xs' color='theme'/>
            <div className='user-logo-container'>
                <img className='user-logo' key='user-logo' src={ logo }/>
            </div>
            

        </StyledUserDetails>
    )
}    


================================================
FILE: client/src/components/view-components/Button.js
================================================
import styled from 'styled-components'
import PropTypes from 'prop-types'


const sizesMap ={
    xxs: {
        fontSize: '--font-size-xxxs',
        width: '--width-xxs',
        height: '--height-xxs'
    },
    xs: {
        fontSize: '--font-size-xs',
        width: '--width-xs',
        height: '--width-xs'
    },
    sm: {
        fontSize: '--font-size-sm',
        width: '--width-sm',
        height: '--height-sm'
    },
    md: {
        fontSize: '--font-size-sm',
        width: '--width-md',
        height: '--height-md'
    },
    lg: {
        fontSize: '--font-size-lg',
        width: '--width-lg',
        height: '--height-lg'
    },
}

const colorsMap ={

    dark: {
        color: '--black',
        background: '--white',
    },
    light : {
        color: '--black',
        background: '--light-grey',
    },
    white: {
        color: '--white',
        background: '--black',
    },
    theme: {
        color: '--orange-theme',
        background: '--dark-blue',
    },
    red:{
        color: '--white',
        background: '--red',
    }
    
}


export const Button = styled.button`

    display: flex;
    justify-content: center;
    align-items: center;
    width: var( ${ ( { size } ) => sizesMap[ size ].width } );
    height: var( ${ ( { size, circle  } ) => circle ? sizesMap[ size ].width : sizesMap[ size ].height } );
    color: var( ${ ( { color } ) => colorsMap[ color ].color } );
    font-size: var( ${ ( { size } ) => sizesMap[ size ].fontSize } );
    background-color: var( ${ ( { color } ) => colorsMap[ color ].background } );;
    border-radius: ${ ( { circle } ) => ! circle ? '0%' : '50%' };
    border: none;
`

Button.propTypes ={

    size: PropTypes.oneOf([
        'xxs',
        'xs',
        'sm',
        'md',
        'lg',
    ]),

    color: PropTypes.oneOf([
        'dark',
        'light',
        'white',
        'theme',
        'red',
    ]),

    circle: PropTypes.oneOf([
        true,
        false,
    ])
}



================================================
FILE: client/src/components/view-components/Circle.js
================================================
import styled from 'styled-components'
import PropTypes from 'prop-types'


const sizesMap ={
    xxs: {
        fontSize: '--font-size-xxxs',
        width: '--width-xxs',
        height: '--width-xxs'
    },
    xs: {
        fontSize: '--font-size-xs',
        width: '--width-xs',
        height: '--width-xs'
    },
    sm: {
        fontSize: '--font-size-sm',
        width: '--width-sm',
        height: '--width-sm'
    },
    md: {
        fontSize: '--font-size-sm',
        width: '--width-md',
        height: '--width-md'
    },
    lg: {
        fontSize: '--font-size-lg',
        width: '--width-lg',
        height: '--width-lg'
    },
}

const colorsMap ={

    dark: {
        color: '--black',
        background: '--white',
    },
    light : {
        color: '--black',
        background: '--light-grey',
    },
    white: {
        color: '--white',
        background: '--black',
    },
    theme: {
        color: '--orange-theme',
        background: '--dark-blue',
    },
    red:{
        color: '--white',
        background: '--red',
    }
    
}


export const Circle = styled.div`

    display: flex;
    justify-content: center;
    align-items: center;
    width: var( ${ ( { size } ) => sizesMap[ size ].width } );
    height: var( ${ ( { size } ) => sizesMap[ size ].height } );
    color: var( ${ ( { color } ) => colorsMap[ color ].color } );
    font-size: var( ${ ( { size } ) => sizesMap[ size ].fontSize } );
    background-color: var( ${ ( { color } ) => colorsMap[ color ].background } );;
    border-radius: 50%;
`

Circle.propTypes ={

    size: PropTypes.oneOf([
        'xxs',
        'xs',
        'sm',
        'md',
        'lg',
    ]),

    color: PropTypes.oneOf([
        'dark',
        'light',
        'white',
        'theme',
        'red'
    ]),
}



================================================
FILE: client/src/components/view-components/LinesSpeedometer/LinesSpeedometer.js
================================================
import React from 'react'
import { useAppAttributes } from '../../../contexts/appAttributesContext'
import LongLine from "./LongLine"
import ShortLine from "./ShortLine"
import { StyledLinesSpeedometer } from './StyledLinesSpeedometer'

function getSpeedometerLines( markedCount, linesCount, loading ) 
{
    const rot   = `${ 184/linesCount }deg`
    const lines = []

    for ( var i = 0; i < linesCount; i++ ) 
    {
        i % 6 === 0 
        ?
        lines.push( 
            <LongLine key={ i } i={ i } rot={ rot } linesCount={ linesCount } markedCount={ markedCount } loading={ loading }/>
        )
        :
        lines.push(  
            <ShortLine key={ i } i={ i } rot={ rot } linesCount={ linesCount } markedCount={ markedCount } loading={ loading }/>
        )
    }

    return lines
}

export default function LinesSpeedometer( { linesCount, markedCount }) {

    const { loading } = useAppAttributes()
    const lines       = getSpeedometerLines( markedCount, linesCount, loading )

    return (

        <StyledLinesSpeedometer className='outside-speedometer-bar'>

            { lines }

        </StyledLinesSpeedometer>
    )
}


================================================
FILE: client/src/components/view-components/LinesSpeedometer/LongLine.js
================================================
import React from 'react'

export default function LongLine( { i, rot, linesCount, loading, markedCount } ) {
    return (
        <div className='long-speed-line-container'  key={ i } style={ { '--rot': rot, '--i': i, '--lines-count': linesCount } }>
            <span className={ `long-speed-line ${ loading  ? 'loading' :  i <= markedCount && 'marked' }` }></span>
            <span className={ `speed-number ${ loading  ? 'loading-text' :  i <= markedCount && 'marked-text' }` } >{ ( Math.floor( ( i ) * 3.3334 )) }</span>
        </div>
    )
}

================================================
FILE: client/src/components/view-components/LinesSpeedometer/ShortLine.js
================================================
import React from 'react'

export default function ShortLine( { i, rot, linesCount, loading, markedCount } ) {
    return (
        <span className={ `short-speed-line ${ loading  ? 'loading' :  i <= markedCount && 'marked' }` } key={ i } style={ { '--rot': rot, '--i': i, '--lines-count': linesCount } }></span>
    )
}


================================================
FILE: client/src/components/view-components/LinesSpeedometer/StyledLinesSpeedometer.js
================================================
import styled from "styled-components";

export const StyledLinesSpeedometer = styled.div`
    
    position: absolute;
    top: 100%;
    left: 50%;
    display: flex;
    align-items: center;
    rotate: -90deg;
    

    .short-speed-line, .long-speed-line{
        background-color: var( --lighter-theme );
    }

    .long-speed-line-container{
        display: flex;
        flex-direction: column;
        align-items: center;
    }

    .short-speed-line, .long-speed-line-container {
        position: absolute;
        transform: rotate( calc( var( --i ) * var( --rot ) ) ) translateY( -18vw );
    }

    .long-speed-line-container > .speed-number{  

        font-size: var( --font-size-xs );
        color: var( --lighter-theme );
        position: absolute;
        rotate: calc( 90deg - ( var( --i ) * var( --rot ) ) ) ;
        top: calc( ( var( --i ) * 0.025vh ) + 4vh ) ;
        font-weight: bold;
    }

    .marked{
        animation: glow-orange 0.03s linear forwards;
        animation-delay: calc( var( --i ) * 0.05s );
    } 

    .loading{   
        --first-delay: 0.02s;
        --all-delay: calc( var( --first-delay ) * var( --lines-count ) );

        animation: load-orange var( --all-delay ) var( --first-delay )  linear infinite;
    }


    .marked-text{
        animation: glow-white 0.03s linear forwards;
        animation-delay: calc( var( --i ) * 0.05s );
    }

    .short-speed-line{
        width: 0.2vw;
        height: 1vw;
    }

    .long-speed-line{
        width: 0.4vw;
        height: 1.5vw;
    }

    @media ( max-width: 1100px ) {

        .short-speed-line, .long-speed-line-container {

            position: absolute;
            transform: rotate( calc( var( --i ) * var( --rot ) ) ) translateY( -200px );
        }
    }


    @media ( min-height: 1100px ) {

        .long-speed-line-container > .speed-number{  

            top: 10px;
            
        }
    }

    @media ( max-width: 640px ){

        .short-speed-line{
            width: 0.2vw;
            height: 1vw;
        }

        .long-speed-line{
            width: 0.4vw;
            height: 1.5vw;
        }   

        .short-speed-line, .long-speed-line-container {
            position: absolute;
            transform: rotate( calc( var( --i ) * var( --rot ) ) ) translateY( -100px );
        }

        .outside-speedometer-bar {
            position: absolute;
            top: 50%;
            left: 50%;
            display: flex;
            align-items: center;
            rotate: -90deg;
        }

        .long-speed-line-container > .speed-number{  
            top: calc( ( var( --i ) * 0.025vh ) + 1vh ) ;
        }

    }

    @media ( max-width: 400px ){
        .short-speed-line, .long-speed-line-container {
            transform: rotate( calc( var( --i ) * var( --rot ) ) ) translateY( -80px );
        }
    }

    @keyframes glow-orange {
        0%{
            background: var( --lighter-theme );
            box-shadow: none;
        }
        100%{
            background: var( --orange-theme );
            box-shadow: 0 0 10px var( --orange-theme );
        }
    }

    @keyframes load-orange {
        0%{
            background: var( --orange-theme );
            box-shadow: 0 0 10px var( --orange-theme );
        }

        5%{
            background: var( --orange-theme );
            box-shadow: 0 0 10px var( --orange-theme );
        }

        95%{
            background: var( --lighter-theme );
            box-shadow: none;
        }

        100%{
            background: var( --orange-theme );
            box-shadow: 0 0 10px var( --orange-theme );
        }
    }

    @keyframes glow-white {
        0%{
            color: var( --lighter-theme );
        }
        100%{
            color: var( --white );
        }
    }

`

================================================
FILE: client/src/components/view-components/Link.js
================================================
import React from 'react'
import styled from 'styled-components'
import PropTypes from 'prop-types'

const sizesMap ={ 
    xs: {
        fontSize: '--font-size-sm',
    },
    sm: {
        fontSize: '--font-size-sm',
    },
    md: {
        fontSize: '--font-size-md',
    },
    lg: {
        fontSize: '--font-size-lg',
    },
}

const colorsMap ={

    dark: {
        color: '--theme',
        backgroundHover: '--lighter-theme',
    },
    light : {
        color: '--light-grey',
        backgroundHover: '--orange-theme',
    },
    white: {
        color: '--white',
        backgroundHover: '--theme',
    },
    theme: {
        color: '--lighter-theme',
        backgroundHover: '--orange-theme',
    },
    orange: {
        color: '--orange-theme',
        backgroundHover: '--orange-theme',
    },
    
}


const StyledLink = styled.a`
    
    background-color: inherit;

    & > .link-icon{

        font-size: var( ${ ( { size } ) => sizesMap[ size ].fontSize } );
        cursor: pointer;
        color: var( ${ ( { color } ) => colorsMap[ color ].color } );
    }

    &:hover > .link-icon{

        color: var( ${ ( { color } ) => colorsMap[ color ].backgroundHover } );
    }
`

StyledLink.propTypes ={

    size: PropTypes.oneOf([
        'xs',
        'sm',
        'md',
        'lg',
    ]),

    color: PropTypes.oneOf([
        'dark',
        'light',
        'white',
        'theme',
        'orange',
    ]),
}

export default function Link( { icon, size, color, url } ) {


    return (
        <StyledLink href={ url } size={ size } color={ color } >

            <span className='material-icons link-icon'>{ icon }</span>

        </StyledLink>
    )
}


================================================
FILE: client/src/components/view-components/Logo.js
================================================
import React from 'react'
import styled from 'styled-components';
import PropTypes from 'prop-types'

const sizeMap = {
    'sm':{
        'size': '--size-sm'
    },
    'md':{
        'size': '--size-md'
    },
    'lg':{
        'size': '--size-lg'
    }
}

const StyledLogo = styled.div`

    --size-sm: 0.8vw;
    --size-md: 1vw;
    --size-lg: 1.2vw;

    display: flex;
    flex-direction: column;
    background-color: var( --theme );
    font-size: var( ${ ( { size } ) => sizeMap[ size ].size  } );
    font-weight: bold;
    
    & > .logo-pro-text{

        color: var( --white );
    }

    & > .logo-earth-text{

        color: var( --orange-theme );
    }
`

StyledLogo.propTypes ={
    size: PropTypes.oneOf([
        'sm',
        'md',
        'lg',
    ]),
}

export default function Logo( { size } ) {


    return(
        <StyledLogo className='app-logo' size={ size }>

            <span className='logo-pro-text'>PRO</span>
            <span className='logo-earth-text'>EARTH</span>

        </StyledLogo>
    )
}


================================================
FILE: client/src/contexts/appAttributesContext.js
================================================
import React, { useContext, useState } from "react";

const AppAttributesContext = React.createContext( null );

export const useAppAttributes = () => {
    
    return useContext( AppAttributesContext )  
}

export const AppAttributesProvider = ( { children } ) => {

    const [ loading, setLoading ] = useState( false )
    const [ error, setError ]     = useState( false )


    const value = 
    {
        loading,
        setLoading,
        error,
        setError
    };
   
    return <AppAttributesContext.Provider children={ children } value={ value }  />;
};


================================================
FILE: client/src/contexts/testResultContext.js
================================================
import React, { useContext, useState } from "react";

const TestResultContext = React.createContext( null );

export const useTestResult = () => {
    
    return useContext( TestResultContext )  
}

export const TestResultProvider = ( { children } ) => {
    

    const [ testResult, setTestResult ] = useState( {
        uploadSpeed: 0.0, downloadSpeed: 0.0,
        latency: 0, userIp: '',
        userLocation: '', os: '',
        server: ''
    } )


    const value = 
    {
        testResult,
        setTestResult
    };

    return <TestResultContext.Provider children={ children } value={ value } />;
};


================================================
FILE: client/src/index.css
================================================
@import url('https://fonts.googleapis.com/css?family=Georama:wght@500&display=swap');

body {
  background: rgb(10,11,59);
  background: radial-gradient(circle, rgba(10,11,59,1) 35%, rgba(1,2,51,1) 100%);
  margin: 0;
  font-family: georama;
  overflow: hidden;
}

*{
  margin: 0;
  padding: 0;
  text-decoration: none;
}

:root{

  --theme: #010233;
  --orange-theme: #FF4848;
  --lighter-theme: #4A5380;
  --white: #ffffff;
  --light-text-color: #DCDBDE;
  --lighter-text-color: #EFF0F3;
  --blue: #131746;
  --black: #000;
  --light-grey: #8A8AA4;
  --dark-grey: #706C82; 
  --darker-grey: #34383c;
  --grey: #9A9A9A;
  --light-blue: #A8B9BD;
  --lighter-blue: #F2F8FF;
  --purple: #653FB4;
  --dark-purple: #01103B;
  --dark-blue: #01062E;
  --yellow: #F9CD45;
  --light-pink: #F9248E;
  --turquoise: #17BDC0;
  --light-theme: #5dfdfe;
  --dark-turquoise: #0F3F62;
  --green: #2FC201;
  --bordeaux: #994446;
  --red: #FF4747;
  --dark-blue: #1C1F4C;
  --primary-shadow: rgba(68,96,170,.2);
  --primary-weak-shadow: rgba(68,96,170,.07);

  --padding-xs: 1vh 0.5vw;
  --padding-sm: 1vh 2vw;
  --padding-md: 2vh 3vw;
  --padding-lg: 2vh 6vw;
  
  --font-size-xxxs: 0.5vw;
  --font-size-xxs: 0.8vw;
  --font-size-xs: 1.2vw;
  --font-size-sm: 1.5vw;
  --font-size-md: 2vw;
  --font-size-lg: 2.5vw;

  --width-xxs: 0.8vw;
  --width-xs: 1vw;
  --width-sm: 3vw;
  --width-md: 5vw;
  --width-lg: 8vw;

  --height-xxs: 0.9vh;
  --height-xs: 1vh;
  --height-sm: 3vh;
  --height-md: 5vh;
  --height-lg: 8vh;
}


================================================
FILE: client/src/index.js
================================================
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import 'material-icons/iconfont/material-icons.css';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

================================================
FILE: client/src/pages/speed-test/SpeedTestPage.js
================================================
import React from 'react'
import styled from 'styled-components'
import { TestResultProvider } from '../../contexts/testResultContext'
import ResultsCardsContainer from './components/ResultsCardsContainer'
import ClientDetails from './components/ClientDetails'
import SpeedDetails from './components/SpeedDetails/SpeedDetails'

const StyledSpeedTestPage = styled.div`

    width: 100vw;
    height: 90vh;
    min-height: 390px;
    min-width: 910px;

    @media ( max-width: 640px )
    {
        min-width: 0;
        min-height: 0;
    }
`

const StyledTestDetails = styled.div`
    
    position: relative;
    display: flex;
    justify-content: space-between;
    padding: 7vh 5vw 0 5vw;
    min-height: 410px;
    min-width: 750px;
    
    @media ( max-width: 640px )
    {
        min-width: 0;
        min-height: 0;
    }
`

export default function SpeedTestPage() {

    return (
        <StyledSpeedTestPage>

            <StyledTestDetails>

                <TestResultProvider>

                    <ClientDetails/>

                    <SpeedDetails/>

                    <ResultsCardsContainer/>
                    
                </TestResultProvider>

            </StyledTestDetails>`

        </StyledSpeedTestPage>
    )
}


================================================
FILE: client/src/pages/speed-test/SpeedTestPage.test.js
================================================
import axios from "axios";
import { fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import SpeedTestPage from "./SpeedTestPage";
import { act } from "react-dom/test-utils";



jest.mock( 'axios' )

const MOCK_TEST_DETAILS = 
{ 
    "downloadSpeed":50,"uploadSpeed":7.9,
    "downloaded":110,"uploaded":20,
    "latency":21,"bufferBloat":192,
    "userLocation":"Jerusalem, IL","userIp":"2a00:a040:197:22aa:804e:c312:1c2e:3b9e",
    "server":"LAPTOP-MJG5S0F3","os":"win32"
}

it( 'Shows test details on a succesfull result', async () => {

    //Arrange
    axios.get.mockResolvedValueOnce( { data: MOCK_TEST_DETAILS } )
    render( 

        <SpeedTestPage/>
    )

    //Act
    const testButton = screen.getByTestId( 'test-button' )
    await act( () => fireEvent.click( testButton ) )
    
    
    //Assert
    const testDataElementsLists =
    { 
        userLocation: screen.getAllByTestId( 'userLocation' ),
        userIp: screen.getAllByTestId( 'userIp' ),
        server: screen.getAllByTestId( 'server' ),
        os: screen.getAllByTestId( 'os' ),
        downloadSpeed: screen.getAllByTestId( 'downloadSpeed' ),
        uploadSpeed: screen.getAllByTestId( 'uploadSpeed' ),
        latency: screen.getAllByTestId( 'latency' ),
    }

    Object.keys( testDataElementsLists ).forEach( dataKey => {

        testDataElementsLists[ dataKey ].forEach( dataElement => {
            
            expect( dataElement ).toHaveTextContent( MOCK_TEST_DETAILS[ dataKey ] )
        })
    })

})

================================================
FILE: client/src/pages/speed-test/components/ClientDetail.js
================================================
import React from 'react'
import styled from 'styled-components'

const StyledClientDetail = styled.div`

    padding: 3vh 2vw;
    display: flex;
    gap: 1vw;
`

const StyledLeftClientDetail = styled.div`

    width: 20%;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;

    & > .client-detail-icon{
        
        font-size: var( --font-size-sm );
        color: var( --orange-theme );
    }
`

const StyledRightClientDetail = styled.div`

    width: 80%;
    height: 5vh;
    display: flex;
    flex-direction: column;
    gap: 0.5vh;
    font-size: var( --font-size-xxs );

    & > .client-detail-data{
        
        color: var( --light-text-color );
    }

    & > .client-detail-title{
        
        color: var( --lighter-theme );
    }

`

export default function ClientDetail( { detailKey, icon, data, title } ) {

    return (
        <StyledClientDetail>

            <StyledLeftClientDetail className='left-client-detail'>
                
                <span className='material-icons client-detail-icon'>{ icon }</span>

            </StyledLeftClientDetail>

            <StyledRightClientDetail className='right-client-detail'>

                <span className='client-detail-data' data-testid={ detailKey }>{ ! data ? '?' : data }</span>
                <span className='client-detail-title'>{ title }</span>

            </StyledRightClientDetail>

        </StyledClientDetail>                               
    )
}


================================================
FILE: client/src/pages/speed-test/components/ClientDetails.js
================================================
import styled from 'styled-components'
import { useTestResult } from '../../../contexts/testResultContext'
import ClientDetail from './ClientDetail'
import LiveChat from './LiveChat'

const StyledClientDetails = styled.div`
    z-index: 1;
    width: 18%;
    display: flex;  
    flex-direction: column;
    padding-top: 5vh;
`

const clientDetailsObject = {
    userIp: { icon: 'wifi', title: 'IP Address' },
    userLocation: { icon: 'pin_drop', title: 'Location' },
    server: { icon: 'language', title: 'Server' },
    os: { icon: 'album',title: 'OS' },
};

export default function ClientDetails() {

    const { testResult } = useTestResult()
    return (
        <StyledClientDetails>

            { Object.keys( clientDetailsObject ).map( ( detailKey  ) => {
                
                return <ClientDetail key={ detailKey } detailKey={ detailKey } icon={ clientDetailsObject[ detailKey ].icon } data={ testResult[ detailKey ] } title={ clientDetailsObject[ detailKey ].title }/>

            })}

            <LiveChat/>

        </StyledClientDetails>
    )
}


================================================
FILE: client/src/pages/speed-test/components/LiveChat.js
================================================
import React from 'react'
import styled from 'styled-components'

const StyledLiveChat = styled.div`

    display: flex;
    height: 12vh;
    min-height: 65px;
    background-color: var( --black );
    justify-content: flex-start;
    align-items: center;
    color: var( --light-text-color );
    padding: 3vh 1vw 3vh 1vw;
    margin-top: 5vh;
    font-weight: bold;
    border-radius:  8px;
    font-size: var( --font-size-xs );

    & > .live-chat-text-container{

        display: flex;
        justify-self: center;
        flex-direction: column;
        height: 70%;
    }

    & > div > .help-you-text{
        
        margin-bottom: auto;
    }

    & > div > .live-chat-text{

        cursor: pointer;
        color: var( --orange-theme );
    }


    & > div > .live-chat-text:hover{

        font-size: var( --font-size-sm );
    }

    & > .headset-icon{

        font-size: var( --font-size-lg );
        margin: auto;
    }

    @media ( max-width: 640px ){

        height: 8vh;
        min-height: 0;    
        width: 20vw;
        
        padding: 2vw 4vw;

        & > div > *{
        
            font-size: var( --font-size-sm );
        }

        & > div > .live-chat-text:hover{

            font-size: var( --font-size-md );
        }   
    }
`
export default function LiveChat() {

    return (
        <StyledLiveChat>
            
            <div className='live-chat-text-container'>

                <div className='how-can-text'>How can we</div>
                <div className='help-you-text'>help you?</div>
                <div className='live-chat-text'>Live chat</div>

            </div>

            <span className='material-icons headset-icon'>headset_mic</span>
            

        </StyledLiveChat>
        
    )
}



================================================
FILE: client/src/pages/speed-test/components/ResultCard.js
================================================
import React from 'react'
import styled from 'styled-components'

const StyledResultCard = styled.div`

    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2vh;
    padding: 2vh 5vw;
    background-color: var( --blue );
    -webkit-box-shadow: -2px -4px 39px 13px rgba(0,0,0,0.09); 
    box-shadow: -2px -4px 39px 13px rgba(0,0,0,0.09);
    border-radius: 8px;

    & > .result-card-top{

        display: flex;
        gap: 0.5vw;
        align-items: center;
        color: var( --orange-theme );
    }

    & > .result-card-top > .result-card-icon{

        font-size: var( --font-size-sm );
    }

    & > .result-card-top > .result-card-text{

        display: inline-block;
        font-size: var( --font-size-xs );
    }

    & > .result-card-data{

        font-size: var( --font-size-md );
        font-weight: bold;
        color: var( --light-text-color );
    }

    & > .result-card-unit{

        font-size: var( --font-size-xxs );
        color: var( --lighter-theme );
    }

`
export default function ResultCard( { dataKey, icon, text, unit, data }) {

    return (
        <StyledResultCard>

            <div className='result-card-top'>

                <span className='material-icons result-card-icon'>{ icon }</span>
                <span className='result-card-text'>{ text }</span>

            </div>

            <div className='result-card-data' data-testid={ dataKey }>{ data }</div>

            <div className='result-card-unit'>{ unit }</div>

        </StyledResultCard>
    )
}


================================================
FILE: client/src/pages/speed-test/components/ResultsCardsContainer.js
================================================
import React from 'react'
import styled from 'styled-components'
import { useTestResult } from '../../../contexts/testResultContext'
import ResultCard from './ResultCard'

const StyledResultsCardsContainer = styled.div`

    display: flex;
    flex-direction: column;
    gap: 3vh;
    justify-content: center;
`

const resultsCardsDetails =
{
    downloadSpeed: { icon: 'cloud_download', text: 'Download', unit: 'mbps' },
    uploadSpeed: { icon: 'cloud_upload', text: 'Upload', unit: 'mbps' },
    latency: { icon: 'monitor_heart', text: 'Latency', unit: 's' },
}

export default function ResultsCardsContainer() 
{
    const { testResult } = useTestResult()

    return (
        <StyledResultsCardsContainer>
            { Object.keys( resultsCardsDetails ).map( ( cardKey ) =>{

                const currentCard = resultsCardsDetails[ cardKey ]
                return <ResultCard key={ cardKey } dataKey={ cardKey } icon={ currentCard.icon } text={ currentCard.text } unit={ currentCard.unit }  data={ testResult[ cardKey ] }/>
            }) }
        </StyledResultsCardsContainer>
    )
}


================================================
FILE: client/src/pages/speed-test/components/SpeedDetails/SpeedDetails.js
================================================
import React from 'react'
import styled from 'styled-components'
import { AppAttributesProvider } from '../../../../contexts/appAttributesContext'
import SpeedTestChart from '../SpeedTestChart'
import TestButton from '../TestButton'
import TestExtraDetails from '../TestExtraDetails'

const StyledSpeedDetails = styled.div`
    width: 60%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 7vh;
    position: absolute;
    left: 50%;
    top: 53%;
    transform: translate( -50%, -50%);
`
export default function SpeedDetails() { 

    return (
        <StyledSpeedDetails>

            <AppAttributesProvider>

                <SpeedTestChart/>

                <TestExtraDetails/> 

                <TestButton/>

            </AppAttributesProvider>

        </StyledSpeedDetails>
    )
}


================================================
FILE: client/src/pages/speed-test/components/SpeedDetails/SpeedDetails.test.js
================================================
import axios from "axios";
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom'
import { TestDetailsProvider } from '../../../../contexts/testDetails'
import SpeedDetails from './SpeedDetails';



jest.mock( 'axios' )

const MOCK_TEST_DETAILS = 
{ 
    "downloadSpeed":50,"uploadSpeed":7.9,
    "downloaded":110,"uploaded":20,
    "latency":21,"bufferBloat":192,
    "userLocation":"Jerusalem, IL","userIp":"2a00:a040:197:22aa:804e:c312:1c2e:3b9e",
    "server":"LAPTOP-MJG5S0F3","os":"win32"
}

it( 'Disables the test button until the promise resolved', async () => {

    //Arrange
    axios.get.mockResolvedValueOnce( { data: MOCK_TEST_DETAILS } )
    render( 

        <TestDetailsProvider>
            
            <SpeedDetails/>

        </TestDetailsProvider>
    )

    //Act
    const testButton = screen.getByTestId( 'test-button' )
    fireEvent.click( testButton ) 
    
    //Assert
    expect( testButton.disabled ).toBe( true )    
    await waitFor( () => expect( testButton.disabled ).toEqual( false ) );

})

================================================
FILE: client/src/pages/speed-test/components/SpeedTestChart.js
================================================
import React from 'react'
import styled from 'styled-components'
import Speedometer from './Speedometer/Speedometer'

const StyledSpeedTestChart = styled.div`

    width: 70%;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: 0 8vw;
    color: var( --lighter-text-color );
    
    & > .speed-test-text{

        position: relative;
        bottom: 130px;
        z-index: 1;
        font-size: var( --font-size-lg );
        font-weight: bold;
    }
`
export default function SpeedTestChart() {

    return (
        <StyledSpeedTestChart>

            <div className='speed-test-text'>

                <div className='speed-text'>SPEED</div>
                <div className='test-text'>TEST</div>                

            </div>

            <Speedometer/>

        </StyledSpeedTestChart>
    )
}


================================================
FILE: client/src/pages/speed-test/components/Speedometer/Speedometer.js
================================================
import React from 'react'
import LinesSpeedometer from '../../../../components/view-components/LinesSpeedometer/LinesSpeedometer'
import { useAppAttributes } from '../../../../contexts/appAttributesContext'
import { useTestResult} from '../../../../contexts/testResultContext'
import TestResults from '../TestResults/TestResults'
import { StyledSpeedometer } from './StyledSpeedometer'

export default function Speedometer( ) {

    const { testResult }     = useTestResult()
    const { downloadSpeed }  = testResult
    const { loading, error } = useAppAttributes()
    const linesCount         = 49
    const maxSpeed           = 160
    const speedRatio         = ! error ? downloadSpeed / maxSpeed : 0
    const speedPercentage    = speedRatio * 100 
    const markedCount        = speedRatio * linesCount 

    return (
        <StyledSpeedometer>

            <div className={ `inside-speedometer-bar ${ ! loading ? 'forwards-animation' : 'loading-animation' }` } data-testid='inside-speedometer-bar' style={ { '--value' : speedPercentage, '--lines-count': linesCount, '--line-delay': 20 } }>
                
                <LinesSpeedometer key='outside-speedometer-bar' linesCount={ linesCount } markedCount={ markedCount }/>
                <TestResults key='test-results'/>

            </div>

            <div className='speedometer-background'></div>

        </StyledSpeedometer>
    )
}


================================================
FILE: client/src/pages/speed-test/components/Speedometer/StyledSpeedometer.js
================================================
import styled from "styled-components";

export const StyledSpeedometer = styled.div`
    
    @property --percentage {
        syntax: '<number>';
        inherits: true;
        initial-value: 0;
    }

    @keyframes progress {
        0% {
            --percentage: 0;
        }

        100% {
            --percentage: var( --value );
        }
    }

    @keyframes progress-back-and-forwards{

        0% {
            --percentage: 0;
        }

        50%{
            --percentage: 100;
        }

        100% {
            --percentage: 0;
        }
    }

    

    position: absolute;
    top: 65%;
    left: 50%;
    transform: translate( -50%, -50% );
    

    .inside-speedometer-bar {

        width: 28vw;
        min-width: 300px;
        aspect-ratio: 2 / 1;
        position: relative;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }

    .inside-speedometer-bar::before {

        content: "";
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: conic-gradient( from 0.75turn at 50% 100%, var( --orange-theme ) calc( var( --percentage ) * 1% / 2 ), var( --dark-blue ) 0 );
        mask: radial-gradient(at 50% 100%, white 68%, transparent 0);
        mask-mode: alpha;
        -webkit-mask: radial-gradient(at 50% 100%, #0000 68%, #000 0);
        -webkit-mask-mode: alpha;
        border-radius: 50% / 100% 100% 0 0;
    }

    .loading-animation{

        --line-delay-ms: calc( var( --line-delay ) * 0.001s );
        --circle-duration: calc( var( --line-delay-ms ) * var( --lines-count ) * 2 );
        animation: progress-back-and-forwards var( --circle-duration ) linear infinite;
    }

    .forwards-animation{
        
        animation: progress 2s 0.05s forwards;
    }

    .speedometer-background{

        z-index: -110;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate( -50%, -50%);
        width: 550px;
        height: 550px;
        background: rgb(10,11,59);
        background: radial-gradient(circle, rgb(12, 13, 61) 39%, rgba(7,8,56,1) 100%);
        border-radius: 50%;
        border: 2px solid #0c0c41;
    }

    .speedometer-background::after{

        content: "";
        z-index: -100;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate( -50%, -50%);
        width: 280px;
        height: 280px;
        background: rgb(10,11,59);
        background: radial-gradient(circle, rgba(10,11,59,1) 39%, rgba(7,8,56,1) 100%);
        border-radius: 50%;
        border: 2px solid #0d0d46;
    }

    .speedometer-background::before{

        content: "";
        z-index: -120;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate( -50%, -50%);
        width: 420px;
        height: 420px;
        background: rgb(10,11,59);
        background: radial-gradient(circle, rgb(12, 13, 61) 39%, rgba(7,8,56,1) 100%);
        border-radius: 50%;
        border: 2px solid #0c0c41;
    }

    @media ( max-width: 640px ){

        .speedometer-background{

            width: 275px;
            height: 275px;
        }
        
        .speedometer-background::after{
        
            width: 140px;
            height: 140px;
        }
        
        .speedometer-background::before{

            width: 210px;
            height: 210px;
        }

        .inside-speedometer-bar{
            width: 0;
            min-width: 0;
        }

        
    }


`

================================================
FILE: client/src/pages/speed-test/components/TestButton.js
================================================
import React from 'react'
import styled from 'styled-components'
import { Button } from '../../../components/view-components/Button'
import { getData } from '../../../utils/api.js'
import { useTestResult } from '../../../contexts/testResultContext'
import { useAppAttributes } from '../../../contexts/appAttributesContext'

const StyledTestButton = styled( Button )`

    z-index: 1;
    cursor: pointer;
    padding: 0.5vw;
    position: relative;
    top: 18vh;

    & > .test-button-icon{
        
        color: var( --orange-theme );
        font-size: var( --font-size-sm );
    }

    &:hover{

        background-color: var( --black );
    }

    &:hover > .test-button-icon{
        
        color: var( --lighter-theme );
    }

    @media ( max-width: 640px ) {
        top: 7vh;
        padding: 2vh;
    }

   

`

export default function TestButton(  ) {
    
    const { setTestResult }                 = useTestResult()
    const { loading, setLoading, setError } = useAppAttributes()
    return (

        <StyledTestButton size='sm' color='theme' circle={ true } data-testid='test-button' disabled={ loading } onClick={ () => { testSpeed( setTestResult, setLoading, setError ) } }>

            <span className='test-button-icon material-icons'>replay</span>

        </StyledTestButton>
    )

    async function testSpeed( setTestResult, setLoading, setError )
    {   
        setError( false )
        setLoading( true )
        
        await getData( '' ).then( res => {

            setTestResult( res.data )        
        }).catch( function( error ){

            setError( true )
        })  

        setLoading( false )
    }
}



================================================
FILE: client/src/pages/speed-test/components/TestExtraDetail.js
================================================
import React from 'react'
import styled from 'styled-components'

const StyledExtraDetail = styled.div`

    display: flex;
    gap: 5px;

    & > .extra-detail-name{

        font-size: var( --font-size-xxs );
        color: var( --lighter-theme );
    }

    & > .extra-detail-value{

        font-size: var( --font-size-xxs );
        color: var( --light-text-color );
    }
`
export default function TestExtraDetail( { detailKey, name, value } ) {
    
    return (
        <StyledExtraDetail>

            <span className='extra-detail-name'>{ name }</span>
            <span className='extra-detail-value' data-testid={ detailKey }>{ ! value ? '?' : value }</span>

        </StyledExtraDetail>
    )
}


================================================
FILE: client/src/pages/speed-test/components/TestExtraDetails.js
================================================
import styled from 'styled-components'
import { useTestResult } from '../../../contexts/testResultContext'
import TestExtraDetail from './TestExtraDetail'

const StyledTestExtraDetails = styled.div`
    position: relative;
    top: 21vh;
    z-index: 1;
    display: flex;
    gap: 10px;

    @media ( max-width: 640px ) {
        top: 11vh;
    }
`

const extraDetails = {
    server: { 'name': 'Server' },
    userIp: { 'name': 'IP Address' },
}

export default function TestExtraDetails() {

    const { testResult } = useTestResult()

    return (

        <StyledTestExtraDetails>

            { Object.keys( extraDetails ).map( ( detailKey ) => {

                const currentDetail = extraDetails[ detailKey ]
                return <TestExtraDetail key={ detailKey } detailKey={ detailKey } name={ currentDetail.name } value={ testResult[ detailKey ] }/>
            })}

        </StyledTestExtraDetails>
    )
}


================================================
FILE: client/src/pages/speed-test/components/TestResults/TestResults.js
================================================
import React from 'react'
import styled from 'styled-components'
import { useAppAttributes } from '../../../../contexts/appAttributesContext'
import { useTestResult } from '../../../../contexts/testResultContext'

const StyledTestResults = styled.div`

    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100px;
    margin-top: 20px;

    & > .speed-result > .speed-type-icon{

        font-size: var( --font-size-sm );
        color: var( --orange-theme );
    }

    & > .speed-result > .speed-result-text{

        font-weight: bold;
        font-size: var( --font-size-lg );
        color: var( --white );
    }

    & > .speed-result > .mbps-text{

        font-size: var( --font-size-xs );
        color: var( --lighter-theme );
    }

    & > .ping-result-container{

        display: flex;
        padding-top: 10px;
        gap: 0.5vw;
    }

    & > .ping-result-container > .ping-text{

        font-size: var( --font-size-xxs );
        color: var( --orange-theme );
    }


    & > .ping-result-container > .ping-result-text{

        font-size: var( --font-size-xxs );
        color: var( --white );
    }


`
export default function TestResults() {

    const { loading, error } = useAppAttributes()
    const { testResult }     = useTestResult()
    const { downloadSpeed, latency } = testResult
    
    return (
        <StyledTestResults className='test-results'>

            <div className='speed-result'>
                <span className='material-icons speed-type-icon'>download</span>
                <div className='speed-result-text' data-testid='downloadSpeed' >{ ! loading && ! error ? downloadSpeed : loading ? 'Testing...' : 'No connection' }</div>    
                <div className='mbps-text'>mbps</div>
            </div>

            <div className='ping-result-container'>

                <span className='ping-text'>Latency</span>
                <span className='ping-result-text' data-testid='latency'>{ latency }</span>
            </div>

        </StyledTestResults>
    )
}



================================================
FILE: client/src/pages/speed-test/components/TestResults/TestResults.test.js
================================================
import {render, screen} from '@testing-library/react'
import '@testing-library/jest-dom'
import TestResults from './TestResults';
import { AppAttributesProvider } from '../../../../contexts/appAttributes'
import { TestDetailsProvider } from '../../../../contexts/testDetails'

it( 'Shows loading text when waiting for server response', () => {
    
    //Arrange
    render( 

        <TestDetailsProvider>

            <AppAttributesProvider value={ { error: false, loading: true } }>

                <TestResults/>

            </AppAttributesProvider>
            
        </TestDetailsProvider>
    )
    
    //Assert
    const speedResultText = screen.getByTestId( 'downloadSpeed' )
    expect( speedResultText ).toHaveTextContent( 'Testing...' )

})

it( 'Shows Error text when error is thrown', () => {
    
    //Arrange
    render( 

        <TestDetailsProvider>

            <AppAttributesProvider value={ { error: true, loading: false } }>

                <TestResults/>

            </AppAttributesProvider>
            
        </TestDetailsProvider>
    )
    
    //Assert
    const speedResultText = screen.getByTestId( 'downloadSpeed' )
    expect( speedResultText ).toHaveTextContent( 'No connection' )

})

================================================
FILE: client/src/setupTests.js
================================================
import fetchMock from "jest-fetch-mock";

fetchMock.enableMocks();




================================================
FILE: client/src/utils/api.js
================================================
import axios from 'axios';
const API_URL = process.env.REACT_APP_API_URL

export async function getData( url ) {

    const fetchUrl = url ? API_URL + url : API_URL 

    return await axios.get( fetchUrl )
} 

================================================
FILE: package.json
================================================
{
  "name": "internet-speed-test-react",
  "version": "1.0.0",
  "description": "This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).",
  "main": "index.js",
  "scripts": {
    "start": "cd server && npm start",
    "server": "cd server && npm run dev",
    "client": "cd client && npm start",
    "dev": "concurrently \"npm run server\" \"npm run client\"",
    "install:client": "cd client && npm install --legacy-peer-deps",
    "install:server": "cd server && npm install",
    "install:all": "npm install && npm run install:client && npm run install:server",
    "build:client": "npm run install:client && cd client && npm run build && mkdir -p ../server/public && cp -r build/* ../server/public",
    "build:server": "npm run install:server",  
    "build": "npm run build:client && npm run build:server"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "concurrently": "^8.0.1"
  }
}


================================================
FILE: server/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.env 

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*


================================================
FILE: server/api-handlers-helpers.js
================================================
const { exec } = require('child_process');
class os_func{

    execCommand( cmd )
    {
        return new Promise( ( resolve, reject ) => {
            exec( cmd, ( err, stdout, stderr ) => {   
                
                if ( err ) 
                {
                    reject( err )
                    return
                }

                if ( stderr ) 
                {
                    reject( stderr )
                    return
                }
                
                resolve( stdout)
            });
        })
    }
}

exports.getExecOutput = async function( command ){

    let execOutput
    const osFunction = new os_func()
    await osFunction.execCommand( command )
    .then( res => {

        execOutput = { 'status': 200, 'data': res }
    })
    .catch( err =>{

        execOutput = { 'status': 400, 'data': err }
    })

    return execOutput
}

================================================
FILE: server/api-handlers.js
================================================
const { getExecOutput } = require( './api-handlers-helpers' );
var os = require('os');

exports.testSpeedHandler = async() => {

    const testCommandOutput = await getExecOutput( 'fast --upload --json' )

    //Handle no internet error
    if ( testCommandOutput.data == '› Please check your internet connection\n\n\n' ) 
    {    
        testCommandOutput.status = 400
    }

    if ( testCommandOutput.status !== 200 ) 
    {    
        return testCommandOutput
    }

    return {
        ...testCommandOutput,
        data: {
            ...JSON.parse( testCommandOutput.data ),
            server: os.hostname(),
            os: process.platform,
        }
    }
}

================================================
FILE: server/index.js
================================================
const cors = require('cors')
const express = require("express");
const app = express();
const PORT = process.env.SERVER_PORT || 8000
const { testSpeedHandler } = require( './api-handlers' )

require('dotenv').config();
const corsOptions = {
    origin: '*',
    optionsSuccessStatus: 200,
}
app.use(cors(corsOptions));

app.get("/", async ( req, res ) => { 

    const speedTestData = await testSpeedHandler()
    res.status( speedTestData.status )
    res.send( speedTestData.data )    
});

app.listen( PORT, () => {

    console.log( `Listening on port ${ PORT }` );
});

================================================
FILE: server/package.json
================================================
{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.0.2",
    "express": "^4.18.1",
    "fast-cli": "^3.2.0",
    "network-speed": "^2.1.1",
    "nodemon": "^2.0.19"
  },
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
Download .txt
gitextract_krgre3tb/

├── .gitignore
├── README.md
├── client/
│   ├── .gitignore
│   ├── package.json
│   ├── public/
│   │   └── index.html
│   └── src/
│       ├── App.js
│       ├── components/
│       │   ├── Header.js
│       │   ├── Navigation.js
│       │   ├── UserDetails.js
│       │   └── view-components/
│       │       ├── Button.js
│       │       ├── Circle.js
│       │       ├── LinesSpeedometer/
│       │       │   ├── LinesSpeedometer.js
│       │       │   ├── LongLine.js
│       │       │   ├── ShortLine.js
│       │       │   └── StyledLinesSpeedometer.js
│       │       ├── Link.js
│       │       └── Logo.js
│       ├── contexts/
│       │   ├── appAttributesContext.js
│       │   └── testResultContext.js
│       ├── index.css
│       ├── index.js
│       ├── pages/
│       │   └── speed-test/
│       │       ├── SpeedTestPage.js
│       │       ├── SpeedTestPage.test.js
│       │       └── components/
│       │           ├── ClientDetail.js
│       │           ├── ClientDetails.js
│       │           ├── LiveChat.js
│       │           ├── ResultCard.js
│       │           ├── ResultsCardsContainer.js
│       │           ├── SpeedDetails/
│       │           │   ├── SpeedDetails.js
│       │           │   └── SpeedDetails.test.js
│       │           ├── SpeedTestChart.js
│       │           ├── Speedometer/
│       │           │   ├── Speedometer.js
│       │           │   └── StyledSpeedometer.js
│       │           ├── TestButton.js
│       │           ├── TestExtraDetail.js
│       │           ├── TestExtraDetails.js
│       │           └── TestResults/
│       │               ├── TestResults.js
│       │               └── TestResults.test.js
│       ├── setupTests.js
│       └── utils/
│           └── api.js
├── package.json
└── server/
    ├── .gitignore
    ├── api-handlers-helpers.js
    ├── api-handlers.js
    ├── index.js
    └── package.json
Download .txt
SYMBOL INDEX (30 symbols across 27 files)

FILE: client/src/App.js
  function App (line 4) | function App() {

FILE: client/src/components/Header.js
  function Header (line 27) | function Header() {

FILE: client/src/components/Navigation.js
  function Navigation (line 29) | function Navigation() {

FILE: client/src/components/UserDetails.js
  function UserDetails (line 45) | function UserDetails() {

FILE: client/src/components/view-components/LinesSpeedometer/LinesSpeedometer.js
  function getSpeedometerLines (line 7) | function getSpeedometerLines( markedCount, linesCount, loading )
  function LinesSpeedometer (line 28) | function LinesSpeedometer( { linesCount, markedCount }) {

FILE: client/src/components/view-components/LinesSpeedometer/LongLine.js
  function LongLine (line 3) | function LongLine( { i, rot, linesCount, loading, markedCount } ) {

FILE: client/src/components/view-components/LinesSpeedometer/ShortLine.js
  function ShortLine (line 3) | function ShortLine( { i, rot, linesCount, loading, markedCount } ) {

FILE: client/src/components/view-components/Link.js
  function Link (line 81) | function Link( { icon, size, color, url } ) {

FILE: client/src/components/view-components/Logo.js
  function Logo (line 48) | function Logo( { size } ) {

FILE: client/src/pages/speed-test/SpeedTestPage.js
  function SpeedTestPage (line 38) | function SpeedTestPage() {

FILE: client/src/pages/speed-test/SpeedTestPage.test.js
  constant MOCK_TEST_DETAILS (line 11) | const MOCK_TEST_DETAILS =

FILE: client/src/pages/speed-test/components/ClientDetail.js
  function ClientDetail (line 46) | function ClientDetail( { detailKey, icon, data, title } ) {

FILE: client/src/pages/speed-test/components/ClientDetails.js
  function ClientDetails (line 21) | function ClientDetails() {

FILE: client/src/pages/speed-test/components/LiveChat.js
  function LiveChat (line 69) | function LiveChat() {

FILE: client/src/pages/speed-test/components/ResultCard.js
  function ResultCard (line 50) | function ResultCard( { dataKey, icon, text, unit, data }) {

FILE: client/src/pages/speed-test/components/ResultsCardsContainer.js
  function ResultsCardsContainer (line 21) | function ResultsCardsContainer()

FILE: client/src/pages/speed-test/components/SpeedDetails/SpeedDetails.js
  function SpeedDetails (line 19) | function SpeedDetails() {

FILE: client/src/pages/speed-test/components/SpeedDetails/SpeedDetails.test.js
  constant MOCK_TEST_DETAILS (line 11) | const MOCK_TEST_DETAILS =

FILE: client/src/pages/speed-test/components/SpeedTestChart.js
  function SpeedTestChart (line 24) | function SpeedTestChart() {

FILE: client/src/pages/speed-test/components/Speedometer/Speedometer.js
  function Speedometer (line 8) | function Speedometer( ) {

FILE: client/src/pages/speed-test/components/TestButton.js
  function TestButton (line 41) | function TestButton(  ) {

FILE: client/src/pages/speed-test/components/TestExtraDetail.js
  function TestExtraDetail (line 21) | function TestExtraDetail( { detailKey, name, value } ) {

FILE: client/src/pages/speed-test/components/TestExtraDetails.js
  function TestExtraDetails (line 22) | function TestExtraDetails() {

FILE: client/src/pages/speed-test/components/TestResults/TestResults.js
  function TestResults (line 56) | function TestResults() {

FILE: client/src/utils/api.js
  constant API_URL (line 2) | const API_URL = process.env.REACT_APP_API_URL
  function getData (line 4) | async function getData( url ) {

FILE: server/api-handlers-helpers.js
  class os_func (line 2) | class os_func{
    method execCommand (line 4) | execCommand( cmd )

FILE: server/index.js
  constant PORT (line 4) | const PORT = process.env.SERVER_PORT || 8000
Condensed preview — 46 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (59K chars).
[
  {
    "path": ".gitignore",
    "chars": 316,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n.env \n\n# dependencies\n/node_module"
  },
  {
    "path": "README.md",
    "chars": 2497,
    "preview": "\n<h1 align=\"center\">\n  <br>\n  <img src=\"https://cdn-icons-png.flaticon.com/512/10321/10321953.png\" alt=\"Speed Test App\" "
  },
  {
    "path": "client/.gitignore",
    "chars": 315,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n.env\n\n# dependencies\n/node_modules"
  },
  {
    "path": "client/package.json",
    "chars": 1171,
    "preview": "{\n  \"name\": \"internet-speed-test-react\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@testing-libr"
  },
  {
    "path": "client/public/index.html",
    "chars": 1390,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.i"
  },
  {
    "path": "client/src/App.js",
    "chars": 240,
    "preview": "import Header from './components/Header';\nimport SpeedTestPage from './pages/speed-test/SpeedTestPage';\n\nfunction App() "
  },
  {
    "path": "client/src/components/Header.js",
    "chars": 678,
    "preview": "import React from 'react'\nimport Logo from './view-components/Logo'\nimport Navigation from './Navigation'\nimport styled "
  },
  {
    "path": "client/src/components/Navigation.js",
    "chars": 1053,
    "preview": "import styled from 'styled-components'\nimport Link from './view-components/Link';\n\nconst StyledNavigation = styled.div`\n"
  },
  {
    "path": "client/src/components/UserDetails.js",
    "chars": 1554,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\nimport { Circle } from './view-components/Circle'\nimpor"
  },
  {
    "path": "client/src/components/view-components/Button.js",
    "chars": 1992,
    "preview": "import styled from 'styled-components'\nimport PropTypes from 'prop-types'\n\n\nconst sizesMap ={\n    xxs: {\n        fontSiz"
  },
  {
    "path": "client/src/components/view-components/Circle.js",
    "chars": 1813,
    "preview": "import styled from 'styled-components'\nimport PropTypes from 'prop-types'\n\n\nconst sizesMap ={\n    xxs: {\n        fontSiz"
  },
  {
    "path": "client/src/components/view-components/LinesSpeedometer/LinesSpeedometer.js",
    "chars": 1155,
    "preview": "import React from 'react'\nimport { useAppAttributes } from '../../../contexts/appAttributesContext'\nimport LongLine from"
  },
  {
    "path": "client/src/components/view-components/LinesSpeedometer/LongLine.js",
    "chars": 549,
    "preview": "import React from 'react'\n\nexport default function LongLine( { i, rot, linesCount, loading, markedCount } ) {\n    return"
  },
  {
    "path": "client/src/components/view-components/LinesSpeedometer/ShortLine.js",
    "chars": 321,
    "preview": "import React from 'react'\n\nexport default function ShortLine( { i, rot, linesCount, loading, markedCount } ) {\n    retur"
  },
  {
    "path": "client/src/components/view-components/LinesSpeedometer/StyledLinesSpeedometer.js",
    "chars": 3805,
    "preview": "import styled from \"styled-components\";\n\nexport const StyledLinesSpeedometer = styled.div`\n    \n    position: absolute;\n"
  },
  {
    "path": "client/src/components/view-components/Link.js",
    "chars": 1690,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\nimport PropTypes from 'prop-types'\n\nconst sizesMap ={ \n"
  },
  {
    "path": "client/src/components/view-components/Logo.js",
    "chars": 1037,
    "preview": "import React from 'react'\nimport styled from 'styled-components';\nimport PropTypes from 'prop-types'\n\nconst sizeMap = {\n"
  },
  {
    "path": "client/src/contexts/appAttributesContext.js",
    "chars": 572,
    "preview": "import React, { useContext, useState } from \"react\";\n\nconst AppAttributesContext = React.createContext( null );\n\nexport "
  },
  {
    "path": "client/src/contexts/testResultContext.js",
    "chars": 616,
    "preview": "import React, { useContext, useState } from \"react\";\n\nconst TestResultContext = React.createContext( null );\n\nexport con"
  },
  {
    "path": "client/src/index.css",
    "chars": 1502,
    "preview": "@import url('https://fonts.googleapis.com/css?family=Georama:wght@500&display=swap');\n\nbody {\n  background: rgb(10,11,59"
  },
  {
    "path": "client/src/index.js",
    "chars": 306,
    "preview": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport './index.css';\nimport App from './App';\nimpor"
  },
  {
    "path": "client/src/pages/speed-test/SpeedTestPage.js",
    "chars": 1247,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\nimport { TestResultProvider } from '../../contexts/test"
  },
  {
    "path": "client/src/pages/speed-test/SpeedTestPage.test.js",
    "chars": 1540,
    "preview": "import axios from \"axios\";\nimport { fireEvent, render, screen } from '@testing-library/react'\nimport '@testing-library/j"
  },
  {
    "path": "client/src/pages/speed-test/components/ClientDetail.js",
    "chars": 1478,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\n\nconst StyledClientDetail = styled.div`\n\n    padding: 3"
  },
  {
    "path": "client/src/pages/speed-test/components/ClientDetails.js",
    "chars": 1077,
    "preview": "import styled from 'styled-components'\nimport { useTestResult } from '../../../contexts/testResultContext'\nimport Client"
  },
  {
    "path": "client/src/pages/speed-test/components/LiveChat.js",
    "chars": 1768,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\n\nconst StyledLiveChat = styled.div`\n\n    display: flex;"
  },
  {
    "path": "client/src/pages/speed-test/components/ResultCard.js",
    "chars": 1557,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\n\nconst StyledResultCard = styled.div`\n\n    z-index: 1;\n"
  },
  {
    "path": "client/src/pages/speed-test/components/ResultsCardsContainer.js",
    "chars": 1098,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\nimport { useTestResult } from '../../../contexts/testRe"
  },
  {
    "path": "client/src/pages/speed-test/components/SpeedDetails/SpeedDetails.js",
    "chars": 823,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\nimport { AppAttributesProvider } from '../../../../cont"
  },
  {
    "path": "client/src/pages/speed-test/components/SpeedDetails/SpeedDetails.test.js",
    "chars": 1084,
    "preview": "import axios from \"axios\";\nimport { fireEvent, render, screen, waitFor } from '@testing-library/react'\nimport '@testing-"
  },
  {
    "path": "client/src/pages/speed-test/components/SpeedTestChart.js",
    "chars": 863,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\nimport Speedometer from './Speedometer/Speedometer'\n\nco"
  },
  {
    "path": "client/src/pages/speed-test/components/Speedometer/Speedometer.js",
    "chars": 1405,
    "preview": "import React from 'react'\nimport LinesSpeedometer from '../../../../components/view-components/LinesSpeedometer/LinesSpe"
  },
  {
    "path": "client/src/pages/speed-test/components/Speedometer/StyledSpeedometer.js",
    "chars": 3584,
    "preview": "import styled from \"styled-components\";\n\nexport const StyledSpeedometer = styled.div`\n    \n    @property --percentage {\n"
  },
  {
    "path": "client/src/pages/speed-test/components/TestButton.js",
    "chars": 1660,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\nimport { Button } from '../../../components/view-compon"
  },
  {
    "path": "client/src/pages/speed-test/components/TestExtraDetail.js",
    "chars": 709,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\n\nconst StyledExtraDetail = styled.div`\n\n    display: fl"
  },
  {
    "path": "client/src/pages/speed-test/components/TestExtraDetails.js",
    "chars": 923,
    "preview": "import styled from 'styled-components'\nimport { useTestResult } from '../../../contexts/testResultContext'\nimport TestEx"
  },
  {
    "path": "client/src/pages/speed-test/components/TestResults/TestResults.js",
    "chars": 2079,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\nimport { useAppAttributes } from '../../../../contexts/"
  },
  {
    "path": "client/src/pages/speed-test/components/TestResults/TestResults.test.js",
    "chars": 1228,
    "preview": "import {render, screen} from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport TestResults from './Test"
  },
  {
    "path": "client/src/setupTests.js",
    "chars": 69,
    "preview": "import fetchMock from \"jest-fetch-mock\";\n\nfetchMock.enableMocks();\n\n\n"
  },
  {
    "path": "client/src/utils/api.js",
    "chars": 208,
    "preview": "import axios from 'axios';\nconst API_URL = process.env.REACT_APP_API_URL\n\nexport async function getData( url ) {\n\n    co"
  },
  {
    "path": "package.json",
    "chars": 958,
    "preview": "{\n  \"name\": \"internet-speed-test-react\",\n  \"version\": \"1.0.0\",\n  \"description\": \"This project was bootstrapped with [Cre"
  },
  {
    "path": "server/.gitignore",
    "chars": 316,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n.env \n\n# dependencies\n/node_module"
  },
  {
    "path": "server/api-handlers-helpers.js",
    "chars": 892,
    "preview": "const { exec } = require('child_process');\nclass os_func{\n\n    execCommand( cmd )\n    {\n        return new Promise( ( re"
  },
  {
    "path": "server/api-handlers.js",
    "chars": 672,
    "preview": "const { getExecOutput } = require( './api-handlers-helpers' );\nvar os = require('os');\n\nexports.testSpeedHandler = async"
  },
  {
    "path": "server/index.js",
    "chars": 573,
    "preview": "const cors = require('cors')\nconst express = require(\"express\");\nconst app = express();\nconst PORT = process.env.SERVER_"
  },
  {
    "path": "server/package.json",
    "chars": 402,
    "preview": "{\n  \"name\": \"server\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"dependencies\": {\n    \"cors\": \""
  }
]

About this extraction

This page contains the full source code of the Omerisra6/internet-speed-test-react GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 46 files (51.5 KB), approximately 15.2k tokens, and a symbol index with 30 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!