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
A single page internet speed test app built with React and Node.js .
Key Features •
How To Use •
Credits •
License
Screenshots
## 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
You need to enable JavaScript to run this app.
================================================
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 (
)
}
================================================
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"
}