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 ================================================


Speed Test App

Speed Test App


A single page internet speed test app built with React and Node.js.

Key FeaturesHow To UseCreditsLicense

Screenshots

localhost-3000
localhost-3000-1

## 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.
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).
```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)  ·  > 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 ================================================ Speed Test
================================================ FILE: client/src/App.js ================================================ import Header from './components/Header'; import SpeedTestPage from './pages/speed-test/SpeedTestPage'; function App() { return (
); } 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 ( ) } ================================================ 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 ( { navButtons.map( ( navButton, index ) => { return })} ) } ================================================ 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 (
1
) } ================================================ 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( ) : lines.push( ) } return lines } export default function LinesSpeedometer( { linesCount, markedCount }) { const { loading } = useAppAttributes() const lines = getSpeedometerLines( markedCount, linesCount, loading ) return ( { lines } ) } ================================================ FILE: client/src/components/view-components/LinesSpeedometer/LongLine.js ================================================ import React from 'react' export default function LongLine( { i, rot, linesCount, loading, markedCount } ) { return (
{ ( Math.floor( ( i ) * 3.3334 )) }
) } ================================================ FILE: client/src/components/view-components/LinesSpeedometer/ShortLine.js ================================================ import React from 'react' export default function ShortLine( { i, rot, linesCount, loading, markedCount } ) { return ( ) } ================================================ 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 ( { icon } ) } ================================================ 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( PRO EARTH ) } ================================================ 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 ; }; ================================================ 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 ; }; ================================================ 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( ); ================================================ 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 ( ` ) } ================================================ 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( ) //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 ( { icon } { ! data ? '?' : data } { title } ) } ================================================ 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 ( { Object.keys( clientDetailsObject ).map( ( detailKey ) => { return })} ) } ================================================ 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 (
How can we
help you?
Live chat
headset_mic
) } ================================================ 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 (
{ icon } { text }
{ data }
{ unit }
) } ================================================ 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 ( { Object.keys( resultsCardsDetails ).map( ( cardKey ) =>{ const currentCard = resultsCardsDetails[ cardKey ] return }) } ) } ================================================ 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 ( ) } ================================================ 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( ) //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 (
SPEED
TEST
) } ================================================ 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 (
) } ================================================ FILE: client/src/pages/speed-test/components/Speedometer/StyledSpeedometer.js ================================================ import styled from "styled-components"; export const StyledSpeedometer = styled.div` @property --percentage { syntax: ''; 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 ( { testSpeed( setTestResult, setLoading, setError ) } }> replay ) 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 ( { name } { ! value ? '?' : value } ) } ================================================ 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 ( { Object.keys( extraDetails ).map( ( detailKey ) => { const currentDetail = extraDetails[ detailKey ] return })} ) } ================================================ 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 (
download
{ ! loading && ! error ? downloadSpeed : loading ? 'Testing...' : 'No connection' }
mbps
Latency { latency }
) } ================================================ 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( ) //Assert const speedResultText = screen.getByTestId( 'downloadSpeed' ) expect( speedResultText ).toHaveTextContent( 'Testing...' ) }) it( 'Shows Error text when error is thrown', () => { //Arrange render( ) //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" }