├── jest.setup.js ├── __mocks__ └── fileMock.js ├── .gitignore ├── public ├── Ariel.jpeg ├── Sign_Up.gif ├── Tanner.jpeg ├── Vicky.jpeg ├── Charmie.jpeg ├── Check_Url.gif ├── Login_In.gif ├── Sign_Out.gif ├── image_png.png ├── renderpup.png ├── data_model.png ├── runningDog.gif ├── wavingdoggo.gif └── Fetch_Metrics.gif ├── .env ├── babel.config.js ├── src ├── index.js ├── index.html ├── components │ ├── Doughnut.jsx │ ├── LineChartNSL.jsx │ ├── LineChartTTFB.jsx │ ├── Logout.jsx │ ├── BarGraph.jsx │ ├── App.jsx │ ├── SignIn.jsx │ ├── SignUp.jsx │ ├── About.jsx │ └── dashboard.jsx └── stylesheets │ └── dashboard.css ├── jest.config.js ├── server ├── models │ └── model.js ├── server.js ├── routers │ └── api.js └── controllers │ ├── lighthouseController.js │ ├── userController.js │ └── metrics.js ├── testing ├── react_tests │ ├── SignIn.test.js │ ├── About.test.js │ └── Logout.test.js └── userController.test.js ├── LICENSE ├── webpack.config.js ├── package.json └── README.md /jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | build 4 | coverage -------------------------------------------------------------------------------- /public/Ariel.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/Ariel.jpeg -------------------------------------------------------------------------------- /public/Sign_Up.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/Sign_Up.gif -------------------------------------------------------------------------------- /public/Tanner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/Tanner.jpeg -------------------------------------------------------------------------------- /public/Vicky.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/Vicky.jpeg -------------------------------------------------------------------------------- /public/Charmie.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/Charmie.jpeg -------------------------------------------------------------------------------- /public/Check_Url.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/Check_Url.gif -------------------------------------------------------------------------------- /public/Login_In.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/Login_In.gif -------------------------------------------------------------------------------- /public/Sign_Out.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/Sign_Out.gif -------------------------------------------------------------------------------- /public/image_png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/image_png.png -------------------------------------------------------------------------------- /public/renderpup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/renderpup.png -------------------------------------------------------------------------------- /public/data_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/data_model.png -------------------------------------------------------------------------------- /public/runningDog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/runningDog.gif -------------------------------------------------------------------------------- /public/wavingdoggo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/wavingdoggo.gif -------------------------------------------------------------------------------- /public/Fetch_Metrics.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RenderPup/HEAD/public/Fetch_Metrics.gif -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # PG_URI = # < Your postgreSQL URI string > 2 | # Add .env file to gitignore before pushing to github for security -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-syntax-jsx"] 4 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import App from './components/App.jsx'; 3 | import { createRoot } from 'react-dom/client'; 4 | 5 | 6 | const root = createRoot(document.getElementById('app')); 7 | root.render(); -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RenderPup 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | collectCoverageFrom: ['src/**/*.{js,jsx}'], 4 | coverageDirectory: 'coverage', 5 | testEnvironment: 'jsdom', 6 | setupFilesAfterEnv: ['/jest.setup.js'], 7 | moduleNameMapper: { 8 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|ico)$': 9 | '/__mocks__/fileMock.js', 10 | '\\.(s?css|less)$': 'identity-obj-proxy', 11 | }, 12 | globals: { 13 | fetch: global.fetch, 14 | } 15 | }; -------------------------------------------------------------------------------- /server/models/model.js: -------------------------------------------------------------------------------- 1 | // Import the required module 2 | require('dotenv').config(); 3 | const { Pool } = require("pg"); 4 | 5 | // Create a new instance of the Pool class with the connection string 6 | const pool = new Pool({ 7 | connectionString: process.env.PG_URI, 8 | }); 9 | 10 | 11 | // Export the pool object to be used in other modules 12 | module.exports = { 13 | query: (text, params, callback) => { 14 | console.log("Executed query:", text); 15 | // Fix the syntax error by defining query as a function 16 | return pool.query(text, params, callback); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /testing/react_tests/SignIn.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import SignIn from '../../src/components/SignIn'; 5 | 6 | 7 | describe("Testing button type in SignIn component", () => { 8 | it("button has type attribute set to 'submit'", () => { 9 | // Render the SignIn button on the signin page within a BrowserRouter 10 | const { getByText } = render( 11 | 12 | 13 | 14 | ); 15 | 16 | // Get the button element by its text content 17 | const signInButton = getByText('Sign In'); 18 | 19 | // Assert that the button has type attribute set to 'submit' 20 | expect(signInButton).toHaveAttribute('type', 'submit'); 21 | }); 22 | }); 23 | 24 | 25 | -------------------------------------------------------------------------------- /testing/react_tests/About.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import About from '../../src/components/About'; 5 | 6 | 7 | describe('About component', () => { 8 | // Render render button in about component 9 | it('should render button in About component', () => { 10 | render( 11 | 12 | < About /> 13 | 14 | ); 15 | // Test button tag 16 | const button = screen.getByText('Back to Dashboard'); 17 | expect(button).toBeInTheDocument(); 18 | 19 | // Use getByAltText to find the image by its alt text 20 | const image = screen.getByAltText('Charmie'); 21 | 22 | // Assert that the image is in the document 23 | expect(image).toBeInTheDocument(); 24 | }); 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /testing/react_tests/Logout.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import Logout from '../../src/components/Logout'; 5 | 6 | describe('Logout component', () => { 7 | it('should render an image element', () => { 8 | // Render the Logout component within a BrowserRouter 9 | //getByRole = built in function in react testing library that allows you to find an element 10 | // must be deconstructed from the result of the render function to use 11 | // alternatively, can you 'screen' keyword in react testing library without needing to deconstruct getByRole 12 | // as in DashboardTwo react tests 13 | const { getByRole } = render( 14 | 15 | 16 | 17 | ); 18 | 19 | // Get the image element by its role 20 | const dogWavingBye = getByRole('img'); 21 | 22 | // Assert that the image element is present 23 | expect(dogWavingBye).toBeInTheDocument(); 24 | }); 25 | }); 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 RenderPup 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require ('express'); 2 | const path = require('path'); 3 | const bodyParser = require('body-parser'); 4 | const app = express(); 5 | const cookieParser = require('cookie-parser'); 6 | const userController = require('./controllers/userController'); 7 | 8 | app.use(bodyParser.json()); 9 | 10 | const PORT = 3000; 11 | 12 | app.use(express.static(path.resolve(__dirname, '../build'))); 13 | 14 | app.use(express.json()) 15 | app.use(cookieParser()); 16 | const apiRouter = require(path.resolve(__dirname, './routers/api.js')) 17 | 18 | app.use('/api', apiRouter) 19 | 20 | app.get('/dashboard', (req, res) => { 21 | res.redirect('/') 22 | }) 23 | 24 | //global error handler 25 | app.use((err, req, res, next) => { 26 | const defaultErr = { 27 | log: 'Express error handler caught unknown middleware error', 28 | status: 500, 29 | message: { err: 'An error occurred' }, 30 | }; 31 | const errorObj = Object.assign({}, defaultErr, err); 32 | console.log(errorObj.log); 33 | return res.status(errorObj.status).json(errorObj.message); 34 | }); 35 | 36 | app.listen(PORT, () => console.log(`Listening on PORT: ${PORT}`)); 37 | module.exports = app; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | const config = { 5 | mode: process.env.NODE_ENV || 'development', 6 | entry: [ 7 | // 'react-hot-loader/patch', 8 | './src/index.js' 9 | ], 10 | output: { 11 | path: path.resolve(__dirname, 'build'), 12 | filename: 'bundle.js' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(js|jsx)$/, 18 | exclude: [/node_modules/], 19 | use: { 20 | loader: 'babel-loader', 21 | options: { 22 | presets: ['@babel/preset-env', '@babel/preset-react'] 23 | } 24 | } 25 | }, 26 | { 27 | test: /.(css|scss)$/, 28 | exclude: /node_modules/, 29 | use: ['style-loader', 'css-loader'], 30 | }, 31 | { 32 | test: /\.(png|jpe?g|svg|gif)$/, 33 | loader: 'file-loader' 34 | }, 35 | ] 36 | }, 37 | plugins: [ 38 | new HtmlWebpackPlugin({ 39 | template: './src/index.html' 40 | }) 41 | ], 42 | devServer: { 43 | static: { 44 | directory: './build' 45 | }, 46 | proxy: { '/api': 'http://localhost:3000', '/':'http://localhost:3000'} 47 | }, 48 | resolve: { 49 | extensions: [ 50 | '.js', 51 | '.jsx' 52 | ] 53 | } 54 | }; 55 | 56 | module.exports = config; -------------------------------------------------------------------------------- /server/routers/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const metricsController = require('../controllers/metrics') 3 | const lighthouseController = require('../controllers/lighthouseController'); 4 | const userController = require('../controllers/userController'); 5 | 6 | const router = express.Router(); 7 | 8 | //handles getting new website info on go fetch button click 9 | router.post('/', metricsController.timeToFirstByte, metricsController.getScriptSize, lighthouseController.analyzeUrl, metricsController.saveMetrics, (req, res) => { 10 | return res.status(200).json({data: {metrics: res.locals.metrics, opportunities: res.locals.diagnostics, performanceScore: res.locals.performanceScore}, response: res.locals.data}) 11 | }) 12 | 13 | router.get('/', metricsController.getUrls, (req, res) => { 14 | return res.status(200).json({urls: res.locals.urls}); 15 | }) 16 | 17 | //handles getting database info on page load 18 | router.post('/urls', metricsController.getDatabaseData, (req, res) => { 19 | return res.status(200).json({data: res.locals.databaseData}); 20 | }) 21 | 22 | router.post('/login', userController.verifyUser, userController.createUserIdCookie, (req, res) => { 23 | return res.status(200).json(res.locals.passwordMatches); 24 | }) 25 | 26 | router.post('/signup', userController.createUser, userController.createUserIdCookie, (req, res) => { 27 | return res.status(200).json(res.locals.userCreated); 28 | }) 29 | 30 | router.post('/logout', userController.deleteCookie, (req, res) => { 31 | return res.status(200).json({ success: true, message: 'Logout successful' }); 32 | }) 33 | 34 | 35 | module.exports = router; -------------------------------------------------------------------------------- /src/components/Doughnut.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Doughnut } from 'react-chartjs-2'; 3 | 4 | // donut chart template from chart.js 5 | const doughnutChart = (props) => { 6 | //pulls bundle size out of props 7 | const chartData = props.data.data[props.data.data.length - 1].bs; 8 | //grabs keys and values from bundle size data and stores them as arrays 9 | const keys = Object.keys(chartData) 10 | const values = Object.values(chartData) 11 | const backgroundColors = [ 12 | 'rgba(7,123,247,1)', 13 | 'rgba(255,99,132,1)', 14 | 'rgba(54,162,235,1)', 15 | 'rgb(242, 140, 40)', 16 | '#bde0fe', 17 | '#219ebc', 18 | '#023047', 19 | '#ffb703', 20 | '#fb8500' 21 | // Add more colors as needed 22 | ]; 23 | const config = { 24 | //sets labels from the keys array 25 | labels: keys.map(item => item), 26 | datasets: [ 27 | { 28 | //sets labels from the keys array 29 | labels: keys.map(item => item), 30 | //sets dataset from values array 31 | data: values.map(item => item), 32 | backgroundColor: backgroundColors, 33 | borderWidth: 1, 34 | borderAlign: 'center' 35 | } 36 | ], 37 | } 38 | const options = { 39 | plugins: { 40 | legend: { 41 | title: { 42 | display: true, 43 | text: 'Bundle Size (bytes)', // Your chart title 44 | }, 45 | position: 'bottom' //changes lengend to below the chart 46 | } 47 | } 48 | }; 49 | return ( 50 | // chart.js functionality to render bundle sizes on donut chart 51 |
52 | 56 |
57 | ); 58 | }; 59 | 60 | export default doughnutChart; -------------------------------------------------------------------------------- /src/components/LineChartNSL.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | 4 | //functional component to render a line chart for Network Server Latency 5 | const LineChartNSL = ({data, convertDate}) => { 6 | //extract the data from the passed 'data' prop 7 | const chartData = data.data; 8 | //configuration object for the chart 9 | const config = { 10 | //setting the labels for the x-axis by mapping over the data and converting dates 11 | labels: chartData.map(item => convertDate(item.date)), 12 | datasets: [ 13 | { 14 | label: 'Network Server Latency (NSL)', 15 | fill: false, 16 | lineTension: 0.7, 17 | backgroundColor: '#ffb703', 18 | borderColor: '#0077b6', 19 | data: chartData.map(item => item.nsl), 20 | pointRadius: 5 21 | } 22 | ] 23 | } 24 | //setting the options for the chart, including the scales and titles 25 | const options = { 26 | scales: { 27 | x: { 28 | title: { 29 | display: true, 30 | text: 'Date', 31 | font: { 32 | size: 14 33 | } 34 | } 35 | }, 36 | y: { 37 | title: { 38 | display: true, 39 | text: 'Time (m/s)', 40 | font: { 41 | size: 14 42 | } 43 | } 44 | } 45 | } 46 | } 47 | //rendering the Line chart with the configured data and options 48 | return ( 49 |
50 | 53 |
54 | ) 55 | } 56 | 57 | export default LineChartNSL; 58 | -------------------------------------------------------------------------------- /src/components/LineChartTTFB.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | 4 | //functional component to render a line chart for Time To First Byte 5 | const LineChartTTFB = ({data, convertDate}) => { 6 | //extracting the data from the passed 'data' prop 7 | const chartData = data.data; 8 | //configuration object for the line chart 9 | const config = { 10 | //setting the labels for the x-axis by mapping over the date and converting date 11 | labels: chartData.map(item => convertDate(item.date)), 12 | datasets: [ 13 | { 14 | label: 'Time To First Byte (TTFB)', 15 | fill: false, 16 | lineTension: 0.7, 17 | backgroundColor: '#0077b6', 18 | borderColor: '#ffb703', 19 | data: chartData.map(item => item.ttfb), 20 | pointRadius: 5 21 | } 22 | ] 23 | } 24 | //setting the options for the line chart, including the scales and titles 25 | const options = { 26 | scales: { 27 | x: { 28 | title: { 29 | display: true, 30 | text: 'Date', 31 | font: { 32 | size: 14 33 | } 34 | } 35 | }, 36 | y: { 37 | title: { 38 | display: true, 39 | text: 'Time (m/s)', 40 | font: { 41 | size: 14 42 | } 43 | } 44 | } 45 | } 46 | } 47 | //rendering the line chart with the configured data and options 48 | return ( 49 |
50 | 53 |
54 | ) 55 | } 56 | 57 | export default LineChartTTFB; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "renderpup", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon ./server/server.js", 8 | "dev": "NODE_ENV=development webpack-dev-server --open --progress --color & nodemon ./server/server.js", 9 | "build": "webpack --mode production", 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.11.3", 14 | "@emotion/styled": "^11.11.0", 15 | "@mui/icons-material": "^5.15.6", 16 | "@mui/material": "^5.15.6", 17 | "bcrypt": "^5.1.1", 18 | "body-parser": "^1.20.2", 19 | "chart.js": "^4.4.1", 20 | "cookie-parser": "^1.4.6", 21 | "dotenv": "^16.3.1", 22 | "express": "^4.18.2", 23 | "identity-obj-proxy": "^3.0.0", 24 | "node-fetch": "^3.3.2", 25 | "nodemon": "^3.0.3", 26 | "pg": "^8.11.3", 27 | "react": "^18.2.0", 28 | "react-chartjs-2": "^5.2.0", 29 | "react-dom": "^18.2.0", 30 | "react-router-dom": "^6.21.3" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.23.9", 34 | "@babel/plugin-syntax-jsx": "^7.23.3", 35 | "@babel/preset-env": "^7.23.9", 36 | "@babel/preset-react": "^7.23.3", 37 | "@testing-library/jest-dom": "^6.4.2", 38 | "@testing-library/react": "^14.2.1", 39 | "babel-jest": "^29.7.0", 40 | "babel-loader": "^9.1.3", 41 | "chrome-launcher": "^1.1.0", 42 | "css-loader": "^6.9.0", 43 | "dotenv": "^16.3.2", 44 | "eslint-plugin-jest": "^27.6.3", 45 | "file-loader": "^6.2.0", 46 | "html-webpack-plugin": "^5.6.0", 47 | "jest": "^29.7.0", 48 | "jest-environment-jsdom": "^29.7.0", 49 | "lighthouse": "^11.4.0", 50 | "style-loader": "^3.3.4", 51 | "supertest": "^6.3.4", 52 | "url-loader": "^4.1.1", 53 | "webpack": "^5.89.0", 54 | "webpack-bundle-analyzer": "^4.10.1", 55 | "webpack-cli": "^5.1.4", 56 | "webpack-dev-server": "^4.15.1" 57 | }, 58 | "engines": { 59 | "node": ">=18.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/Logout.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import wavingdoggo from '../../public/wavingdoggo.gif'; 4 | 5 | 6 | const Logout = () => { 7 | //declare this hook at the top level component!! 8 | const navigate = useNavigate(); 9 | // use useState hook to render a message before being redirected to login page 10 | const [ message, setMessage ] = useState(''); 11 | // invoke wavingDoggo outside useEffect so return statement can refer to it 12 | const wavingDoggo = ; 13 | 14 | useEffect(() => { 15 | const handleLogout = async () => { 16 | 17 | try { 18 | //post req from server with path /logout 19 | const response = await fetch('/api/logout', { 20 | method: 'POST', 21 | headers: { 'Content-type': 'application/json' }, 22 | }); 23 | // If HTTP response status is within 200 to 299 (inclusive) range, .ok will be true, indicating successful request 24 | if (response.ok) { 25 | const responseData = await response.json(); 26 | 27 | if (responseData.success) { 28 | // setMessage function that reassigns initial state to string below 29 | sessionStorage.clear() 30 | setMessage('Logging out... goodbye!'); 31 | 32 | setTimeout(() => { 33 | //clear the message after timer ends 34 | setMessage('') 35 | 36 | 37 | navigate('/'); 38 | }, 3000) //4 sec delay before redirecting to signin 39 | } else { 40 | console.error('Logout failed:', responseData.message); 41 | } 42 | } else { 43 | console.error('Logout failed with status:', response.status); 44 | } 45 | } catch (error) { 46 | console.error('Error during logout:', error); 47 | } 48 | }; 49 | 50 | handleLogout(); 51 | }, [navigate]); 52 | 53 | return ( 54 | //render waving dog gif & message from useState hook 55 |
56 | { wavingDoggo } 57 | { message } 58 |
59 | ); 60 | }; 61 | 62 | export default Logout; 63 | -------------------------------------------------------------------------------- /src/components/BarGraph.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Bar } from 'react-chartjs-2'; 3 | 4 | //functional component to render a bar graph 5 | const Bargraph = (props) => { 6 | //extracting the data from the passed 'props.data' object 7 | const chartData = props.data.data 8 | //configuration object for the bar graph 9 | const config = { 10 | //setting the labels for the x-axis by mapping over tha data and coverted date 11 | labels: chartData.map(item => props.convertDate(item.date)), 12 | datasets: [ 13 | { 14 | //configuring the first dataset for FCP 15 | label: "First Contentful Paint (FCP)", 16 | data: chartData.map(item => item.fcp), 17 | backgroundColor: '#ffb703', 18 | borderColor: '#ffb703', 19 | borderWidth: 1, 20 | borderRadius: 5, 21 | hoverBackgroundColor: '#fb8500', 22 | hoverBorderColor: '#0077b6', 23 | hoverBorderWidth: 3, 24 | }, 25 | { 26 | //configuring the second dataset for LCP 27 | label: "Largest Contentful Paint (LCP)", 28 | data: chartData.map(item => item.lcp), 29 | backgroundColor: '#0077b6', 30 | borderColor: 'rgba(255,255,255,0.5)', 31 | borderWidth: 1, 32 | borderRadius: 5, 33 | hoverBackgroundColor: '#03045e', 34 | hoverBorderColor: '#fb8500', 35 | hoverBorderWidth: 3, 36 | }, 37 | ], 38 | } 39 | //setting the options for the bar graph 40 | const options = { 41 | scales: { 42 | x: { 43 | title: { 44 | display: true, 45 | text: 'Date', 46 | font: { 47 | size: 14 48 | } 49 | } 50 | }, 51 | y: { 52 | title: { 53 | display: true, 54 | text: 'Time (m/s)', 55 | font: { 56 | size: 14 57 | } 58 | } 59 | } 60 | } 61 | } 62 | //rendering the bar graph with the configured data and options 63 | return ( 64 |
65 | 68 |
69 | ); 70 | }; 71 | 72 | export default Bargraph; 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/stylesheets/dashboard.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-image:radial-gradient(ellipse at center, rgb(234, 246, 255) 0%, rgb(220, 246, 255) 35%, #54cdf9 100%); 3 | background-attachment: fixed; 4 | font: 1em monaco; 5 | } 6 | 7 | h1{ 8 | text-align: center; 9 | margin-bottom: 0; 10 | } 11 | 12 | .slogan { 13 | display: flex; 14 | align-items:center; 15 | justify-content: center; 16 | 17 | } 18 | 19 | /* ------- app form ------- */ 20 | .app-form { 21 | text-align: center; 22 | } 23 | 24 | .app-input-field { 25 | height: 2em; 26 | width: 150%; 27 | margin-left: 1em; 28 | margin-right: 1em; 29 | border-radius: 0.4em; 30 | border: solid; 31 | border-color: rgb(152, 152, 157); 32 | } 33 | 34 | 35 | .go-fetch-bttn { 36 | background-color: #023047; 37 | color: white; 38 | border-radius: 0.4em; 39 | padding: 0.6em; 40 | border: none; 41 | margin-left: 9em; 42 | } 43 | 44 | button { 45 | background-color: #023047; 46 | color: white; 47 | border-radius: 0.4em; 48 | padding: 0.6em; 49 | margin: 0.6em; 50 | border: none; 51 | text-align: right; 52 | } 53 | /* ------------------------ */ 54 | 55 | #dogFetchingBall { 56 | width: 5%; 57 | margin-right: 0.5em; 58 | } 59 | 60 | .graphSizes { 61 | width: 50%; 62 | } 63 | 64 | .dropdown { 65 | text-align: center; 66 | } 67 | 68 | .firstSite { 69 | list-style: none; 70 | } 71 | 72 | #graphs { 73 | display: flex; 74 | flex-wrap: wrap; 75 | } 76 | 77 | .saved-urls { 78 | background-color: #219ebc; 79 | } 80 | 81 | #logo { 82 | height: auto; 83 | width: auto; 84 | max-width: 50px; 85 | max-height: 50px; 86 | 87 | } 88 | 89 | .logoAndSearch { 90 | display: flex; 91 | justify-content: center; 92 | align-items: center; 93 | width: 100%; 94 | } 95 | 96 | #loadingPage { 97 | display: flex; 98 | justify-content: center; 99 | align-items: flex-end; 100 | } 101 | 102 | #runningDog { 103 | height: 4em; 104 | } 105 | 106 | #logoutPage { 107 | display: flex; 108 | justify-content: center; 109 | align-items: center; 110 | height: 100vh; 111 | } 112 | 113 | #logoutDog { 114 | height: 20vh; /* Set the height of the GIF to 20% of the viewport height */ 115 | width: auto; /* Maintain the aspect ratio */ 116 | } 117 | 118 | #about { 119 | height: 200px; 120 | width: 200px; 121 | } 122 | 123 | #aboutimage { 124 | height: 22px; 125 | } 126 | 127 | #aboutimage:hover { 128 | transform: scale(0.8); 129 | } 130 | 131 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; 3 | import { Chart as ChartJS } from 'chart.js/auto'; 4 | import Bargraph from './BarGraph'; 5 | import Doughnut from './Doughnut'; 6 | import LineChartTTFB from './LineChartTTFB'; 7 | import LineChartNSL from './LineChartNSL'; 8 | import SignIn from './SignIn'; 9 | import SignUp from './SignUp'; 10 | import Logout from './Logout'; 11 | import Dashboard from './dashboard'; 12 | import About from './About'; 13 | 14 | 15 | const App = () => { 16 | // use state to set initial state of form 17 | const [currentForm, setCurrentForm] = useState('login'); 18 | //have initial state to be the same format that's returned from the server 19 | const [chartData, setData] = useState({data:[{url:0, bs: {DummyData: 1}}]}); 20 | //use toggleForm to change current state (login) to SignIn 21 | const toggleForm = (formName) => { 22 | setCurrentForm(formName); 23 | }; 24 | 25 | // functionality to make the date look better on graphs/charts 26 | function convertDate(date) { 27 | if (date === undefined) return 28 | const dateArr = date.split('') 29 | for (let i = 0; i < dateArr.length; i++) { 30 | // T = letter in the middle of long date string, replace T with a space 31 | if (dateArr[i] == 'T') { 32 | dateArr[i] = ' ' 33 | } 34 | // delete date string from '.' to rest of date string 35 | else if (dateArr[i] == '.') { 36 | dateArr.splice(i, dateArr.length + 1) 37 | } 38 | } 39 | return dateArr.join('') 40 | } 41 | 42 | return ( 43 | // create data-testid div to test for app component on testing file 44 |
45 | {/* implement react router to to allow client-side routing of react compoennts*/} 46 | {/* used to set paths to different components to use useNavigate hook (lines 49, 50, 62, 63*/} 47 |
48 | 49 | 50 | {currentForm === "login" ? ( ) : ( )} } /> 51 | 53 | 54 |
55 |
56 | {} 57 | {} 58 | {} 59 | {} 60 |
61 |
62 | } /> 63 | } /> 64 | } /> 65 | } /> 66 | 67 | 68 |
69 | 70 | 71 | ); 72 | 73 | } 74 | 75 | export default App; -------------------------------------------------------------------------------- /server/controllers/lighthouseController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/model') 2 | 3 | //function to dynamically imprt Lighthouse and Chrome Launder modules 4 | const importLighthouse = async () => { 5 | const lighthouse = await import('lighthouse'); 6 | const chromeLauncher = await import('chrome-launcher'); 7 | 8 | //return both imported modules for use in the controller 9 | return { lighthouse, chromeLauncher }; 10 | } 11 | //create an object to hold the controller functions 12 | const lighthouseController = {}; 13 | 14 | //add a function to anaylze a url using Lighthouse 15 | lighthouseController.analyzeUrl = async (req, res, next) => { 16 | //inner function to run Lighthouse on a url 17 | const runLighthouse = async (url) => { 18 | const { lighthouse, chromeLauncher } = await importLighthouse(); 19 | const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] }); 20 | const options = { logLevel: 'info', output: 'json', onlyCategories: ['performance'], port: chrome.port }; 21 | const runnerResult = await lighthouse.default(url, options); 22 | await chrome.kill(); 23 | 24 | //extract the audit results from the Lighthouse report 25 | const audits = runnerResult.lhr.audits; 26 | const fcp = audits['first-contentful-paint'].numericValue; 27 | const lcp = audits['largest-contentful-paint'].numericValue; 28 | const nsl = audits['network-server-latency'].numericValue; 29 | 30 | //extracting the performance score from the Lighthouse report 31 | const performanceScore = runnerResult.lhr.categories.performance.score * 100; 32 | 33 | //extract the opportunities list from the performance audit 34 | const opportunities = []; 35 | if (audits) { 36 | Object.values(audits).forEach(audit => { 37 | // Check if the audit is an opportunity and has potential savings 38 | if (audit.details && audit.details.type === 'opportunity' && audit.score !== 1 && audit.details.overallSavingsMs > 0) { 39 | opportunities.push(audit.description); 40 | } 41 | }); 42 | } 43 | //return the extracted metrics, performance score, and opportunities 44 | return { fcp, lcp, nsl, performanceScore, opportunities }; 45 | }; 46 | 47 | const { url } = req.body; 48 | 49 | //running the Lighthouse analysis on the provided url and storing the result on the response object 50 | try { 51 | const { fcp, lcp, nsl, performanceScore, opportunities } = await runLighthouse(url); 52 | res.locals.metrics.fcp = fcp; 53 | res.locals.metrics.lcp = lcp; 54 | res.locals.metrics.nsl = nsl; 55 | res.locals.performanceScore = performanceScore; 56 | res.locals.opportunities = opportunities; 57 | 58 | //pass control to the next middleware function 59 | next(); 60 | } catch (error) { 61 | //log any errors that occur during the Lighthouse analysis process 62 | console.log('error in analyzeUrl controller:', error); 63 | } 64 | }; 65 | 66 | module.exports = lighthouseController; -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/model'); 2 | const bcrypt = require('bcrypt'); 3 | 4 | const userController = {}; 5 | 6 | 7 | userController.createUser = async (req, res, next) => { 8 | //extract user information from the request body 9 | const { firstName, lastName, email, username, password } = req.body; 10 | //generate a salt and hash the password using bcrypt for secure storage 11 | const salt = await bcrypt.genSalt(10); 12 | const hashedPassword = await bcrypt.hash(password, salt); 13 | 14 | //insert the new user into the database with the hashed password 15 | db.query('INSERT INTO users (username, password, firstname, lastname, email) ' + `VALUES ('${username}', '${hashedPassword}', '${firstName}', '${lastName}', '${email}') RETURNING _id`) 16 | .then(result => { 17 | console.log('result:', result) 18 | //indicate that the user was created successfully 19 | res.locals.userCreated = 'User created successfully'; 20 | //pass control over to the next middleware function 21 | return next(); 22 | }) 23 | //pass an error to the next error-handling middleware function 24 | .catch(err => next({ 25 | log: 'Express error handler caught unknown middleware error', 26 | message: { err: 'An error occurred' }, 27 | err 28 | })); 29 | }; 30 | 31 | userController.verifyUser = async (req, res, next) => { 32 | //extract the login credentials from the request body 33 | const { username, password } = req.body; 34 | const inputPassword = password; 35 | //retrieve the hashed password from the database for the given username 36 | db.query(`SELECT password FROM users WHERE username = '${username}' `) 37 | .then( async (hashPassword) => { 38 | //compare the input password with the hashed password from the database 39 | res.locals.passwordMatches = await bcrypt.compare(inputPassword, hashPassword.rows[0].password); 40 | //set a cookie if the password matches 41 | if (res.locals.passwordMatches){ 42 | res.cookie('token', 'user'); 43 | //pass control to the next middleware function 44 | return next(); 45 | } 46 | }) 47 | //pass an error to the next error-handling middleware function 48 | .catch(err => next({log: 'Express error handler caught in usercontroller.verifyUser middleware', 49 | message: { err: 'An error occurred during login' }, 50 | err})) 51 | } 52 | 53 | userController.createUserIdCookie = (req, res, next) => { 54 | //retrieve the user ID from the database based on the username 55 | db.query(`SELECT _id FROM users WHERE username = '${req.body.username}'`) 56 | .then(id => { 57 | //set a cookie with the user ID 58 | res.cookie('userId', id.rows[0]._id) 59 | //pass control to the next middleware function 60 | return next() 61 | }) 62 | //pass an error to the next error-handling middleware function 63 | .catch(err => next({ 64 | log: 'Express error handler caught unknown middleware error', 65 | message: { err: 'An error occurred' }, 66 | err 67 | })); 68 | } 69 | 70 | userController.deleteCookie = (req, res, next) => { 71 | //retrieve the 'token' and 'userId' cookie from the request 72 | const token = req.cookies.token; 73 | const userId = req.cookies.userId; 74 | //clear the 'token' and 'userId' cookie if it exists 75 | if (token) { 76 | res.clearCookie('token',{ 77 | httpOnly: true, 78 | secure: true 79 | }); 80 | } 81 | if (userId) { 82 | res.clearCookie('userId',{ 83 | httpOnly: true, 84 | secure: true 85 | }); 86 | } 87 | //pass control to the next middleware function 88 | return next(); 89 | } 90 | 91 | module.exports = userController; -------------------------------------------------------------------------------- /testing/userController.test.js: -------------------------------------------------------------------------------- 1 | const supertest = require('supertest'); 2 | const express = require('express'); 3 | const bcrypt = require('bcrypt'); 4 | const cookieParser = require('cookie-parser'); 5 | const db = require('../server/models/model'); 6 | const userController = require('../server/controllers/userController'); 7 | 8 | 9 | jest.mock('bcrypt', () => ({ 10 | genSalt: jest.fn().mockResolvedValue('someSalt'), 11 | hash: jest.fn().mockResolvedValue('hashedPassword'), 12 | compare: jest.fn().mockResolvedValue(true), 13 | })); 14 | 15 | jest.mock('../server/models/model', () => ({ 16 | query: jest.fn(), 17 | })); 18 | 19 | describe('User controller middleware', () => { 20 | let app; 21 | 22 | beforeAll(() => { 23 | app = express(); 24 | app.use(express.json()); 25 | app.use(cookieParser()); 26 | 27 | app.post('/signup', userController.createUser, (req, res) => { 28 | res.status(201).json({ message: res.locals.userCreated }) 29 | }); 30 | app.post('/login', userController.verifyUser, (req, res) => { 31 | return res.status(200).json(res.locals.passwordMatches); 32 | }); 33 | app.post('/login', userController.createUserIdCookie, (req, res) => { 34 | return res.status(200).json(); 35 | }); 36 | app.post('/logout', userController.deleteCookie, (req, res) => { 37 | return res.status(200).json({ success: true, message: 'Logout successful' }); 38 | }); 39 | }) 40 | 41 | describe('createUser', () => { 42 | it('should create a user and respond with a success message', async () => { 43 | const newUser = { 44 | firstName: 'Tom', 45 | lastName: 'Hanks', 46 | email: 'wilson@example.com.', 47 | username: 'wilson', 48 | password: 'password123' 49 | }; 50 | 51 | db.query.mockResolvedValueOnce({ rows: [{ _id: 1 }] }); 52 | 53 | const response = await supertest(app) 54 | .post('/signup') 55 | .send(newUser); 56 | console.log(newUser); 57 | 58 | expect(response.statusCode).toBe(201); 59 | expect(response.body.message).toBe('User created successfully'); 60 | }); 61 | }); 62 | 63 | describe('verifyUser', () => { 64 | it('should verify to see if a username and password match', async () => { 65 | const user = { 66 | username: 'vicky', 67 | password: 'password' 68 | }; 69 | db.query.mockResolvedValueOnce({ rows: [{ password: 'hashedPassword'}] }) 70 | 71 | const response = await supertest(app) 72 | .post('/login') 73 | .send(user) 74 | 75 | expect(response.statusCode).toBe(200); 76 | expect(bcrypt.compare).toHaveBeenCalledWith(user.password, 'hashedPassword'); 77 | expect(db.query).toHaveBeenCalledWith(expect.stringContaining(user.username)) 78 | }) 79 | }) 80 | 81 | describe('createUserIdCookie', () => { 82 | it('Should create a cookie with userId', async () => { 83 | const user = { 84 | username: 'charmie', 85 | password: 'password' 86 | }; 87 | db.query.mockResolvedValueOnce({ rows: [{_id: '1'}]}); 88 | 89 | const response = await supertest(app) 90 | .post('/login') 91 | .send(user) 92 | 93 | expect(response.headers['set-cookie']).toBeDefined(); 94 | expect(response.statusCode).toBe(200); 95 | }) 96 | }) 97 | describe('deleteCookie', () => { 98 | it('should delete a cookie when logging out', async() => { 99 | 100 | const response = await supertest(app) 101 | 102 | .post('/logout') 103 | .send('cookie') 104 | 105 | expect(response.statusCode).toBe(200); 106 | expect(response.body).toEqual({ success: true, message: 'Logout successful' }) 107 | }) 108 | }) 109 | }); 110 | 111 | -------------------------------------------------------------------------------- /src/components/SignIn.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Avatar from '@mui/material/Avatar'; 3 | import Button from '@mui/material/Button'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import TextField from '@mui/material/TextField'; 6 | import FormControlLabel from '@mui/material/FormControlLabel'; 7 | import Checkbox from '@mui/material/Checkbox'; 8 | import Link from '@mui/material/Link'; 9 | import Grid from '@mui/material/Grid'; 10 | import Box from '@mui/material/Box'; 11 | import Typography from '@mui/material/Typography'; 12 | import Container from '@mui/material/Container'; 13 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 14 | import { useNavigate } from 'react-router-dom'; 15 | import renderpup from '../../public/renderpup.png'; 16 | 17 | // MUI functionality to add copyright functionality beneath signin form 18 | function Copyright(props) { 19 | return ( 20 | 21 | {'Copyright © '} 22 | 23 | https://renderpup.com/ 24 | {' '} 25 | {new Date().getFullYear()} 26 | {'.'} 27 | 28 | ); 29 | } 30 | 31 | 32 | // MUI functionality that generates sign form theme 33 | const defaultTheme = createTheme(); 34 | 35 | export default function SignIn({onFormSwitch}) { 36 | //useNavigate hook to navigate to different react components 37 | const navigate = useNavigate(); 38 | useEffect(() => { 39 | if (sessionStorage.getItem('loggedIn') !== null) { 40 | navigate(sessionStorage.getItem('loggedIn')) 41 | } 42 | }, []) 43 | 44 | // handle submit functionality to redirect to dashboard 45 | const handleSubmit = (event) => { 46 | // event to prevent entire page from being reloaded 47 | event.preventDefault(); 48 | const data = new FormData(event.currentTarget); 49 | // fetch req from endpoint that takes in username/pw data 50 | fetch('/api/login', { 51 | method: 'POST', 52 | headers: {'Content-Type': 'application/json'}, 53 | //expect login data to to be in an object format that's extracted from data.get object 54 | body: JSON.stringify( 55 | { 56 | username: data.get('username'), 57 | password: data.get('password') 58 | }) 59 | }) 60 | // 61 | .then(async data => { 62 | // parse response from fetch req into json format 63 | const response = await data.json(); 64 | // if there's sa truthy response (correct username/pw), redirect to dashboard component using usenavigate hook 65 | if (response === true) { 66 | navigate('/dashboard') 67 | } else { 68 | console.log('Incorrect username or password'); 69 | } 70 | }) 71 | .catch(err => { 72 | console.error(err, 'error when logging in') 73 | }); 74 | 75 | }; 76 | 77 | return ( 78 | // MUI functionality for signin form 79 |
80 | 81 | 82 | 83 | 91 | 92 | {/* have signin page render renderpup logo */} 93 | 98 | 99 | 100 | Sign in 101 | 102 | 103 | 113 | 123 | 131 | 132 | 133 | 134 | onFormSwitch('signup')}> 135 | {"Don't have an account? Sign up!"} 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 |
146 | ); 147 | } -------------------------------------------------------------------------------- /src/components/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Avatar from '@mui/material/Avatar'; 3 | import Button from '@mui/material/Button'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import TextField from '@mui/material/TextField'; 6 | import Link from '@mui/material/Link'; 7 | import Grid from '@mui/material/Grid'; 8 | import Box from '@mui/material/Box'; 9 | import Typography from '@mui/material/Typography'; 10 | import Container from '@mui/material/Container'; 11 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 12 | import { useNavigate } from 'react-router-dom'; 13 | import renderpup from '../../public/renderpup.png'; 14 | 15 | // MUI functionality to add copyright functionality beneath signup form 16 | function Copyright(props) { 17 | return ( 18 | 19 | {'Copyright © '} 20 | 21 | https://renderpup.com/ 22 | {' '} 23 | {new Date().getFullYear()} 24 | {'.'} 25 | 26 | ); 27 | } 28 | 29 | // MUI functionality that generates signup form theme 30 | const defaultTheme = createTheme(); 31 | 32 | // signup functionality with onFormSwitch prop drilled from App to allow toggling signin/signup page 33 | export default function SignUp({onFormSwitch}) { 34 | 35 | //useNavigate hook to navigate between react components 36 | const history = useNavigate(); 37 | 38 | // handleSignup functionality to input new user data to database, then redirect to SignIn component 39 | const handleSignup = (event) => { 40 | event.preventDefault(); 41 | const data = new FormData(event.currentTarget); 42 | 43 | fetch('/api/signup', { 44 | method: 'POST', 45 | headers: {'Content-Type': 'application/json'}, 46 | // takes user data through data object 47 | body: JSON.stringify( 48 | { 49 | username: data.get('username'), 50 | password: data.get('password'), 51 | firstName: data.get('firstName'), 52 | lastName: data.get('lastName'), 53 | email: data.get('email') 54 | }) 55 | }) 56 | .then(response => { 57 | return response.json(); 58 | }) 59 | .then(data => { 60 | history('/signin') 61 | }) 62 | .catch(err => console.error(err, 'Signup not successfull')) 63 | }; 64 | 65 | return ( 66 | // MUI functionality to render signup form theme 67 |
68 | 69 | 70 | 71 | 79 | {/* have signup page render renderpup logo */} 80 | 85 | 86 | 87 | Sign up 88 | 89 | 90 | 91 | 92 | 101 | 102 | 103 | 111 | 112 | 113 | 121 | 122 | 123 | 131 | 132 | 133 | 142 | 143 | 144 | 145 | 153 | 154 | 155 | 156 | 157 | onFormSwitch('login')}> 158 | {"Already have an account? Sign in!"} 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 |
170 | ); 171 | } 172 | 173 | -------------------------------------------------------------------------------- /src/components/About.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useNavigate } from 'react-router-dom'; 3 | import Charmie from '../../public/Charmie.jpeg'; 4 | import Tanner from '../../public/Tanner.jpeg'; 5 | import Ariel from '../../public/Ariel.jpeg'; 6 | import Vicky from '../../public/Vicky.jpeg'; 7 | import renderpup from '../../public/renderpup.png'; 8 | 9 | const About = () => { 10 | const navigate = useNavigate(); 11 | // handleSignout function = functionality to redirect user to dashboard component on about page 12 | const handleDashboard = () => { 13 | // navigate to dashboard component 14 | navigate('/dashboard'); 15 | } 16 | return ( 17 |
18 | 19 | 30 |
31 | {/* renderpup 'about info' with CSS */} 32 | {/* sorry i did most of the CSS on the same file :') */} 33 |
34 | renderpup 35 |
36 |

RenderPup

37 |

38 | RenderPup is a web application designed to analyze Next.js websites by running various performance tests and providing insightful metrics. With RenderPup, users can input a URL for a Next.js website, and the application will conduct tests to measure important performance metrics such as time to first byte, first and largest contentful paint, network server latency, and bundle size. 39 |

40 | 41 |

Description

42 | 43 |

44 | RenderPup addresses the critical need for developers and website owners to understand the performance characteristics of their Next.js websites. By providing comprehensive metrics, RenderPup enables users to identify potential bottlenecks and optimize their websites for better user experience and search engine rankings. 45 |

46 | 47 |

Key Features

48 | 49 |
    50 |
  • Performance Testing: RenderPup conducts various tests to assess the performance of Next.js websites.
  • 51 |
  • Real-Time Metric Analysis: It provides detailed metrics including time to first byte, first and largest contentful paint, network server latency, and bundle size.
  • 52 |
  • User-friendly Interface: RenderPup features a simple and intuitive interface, making it easy for users to input URLs and view performance metrics.
  • 53 |
  • Dashboard: A user-friendly dashboard to visualize performance metrics and gain actionable insights.
  • 54 |
55 |
56 |
57 | 58 |


59 |

Meet the engineers!

60 |
61 |
62 |
63 | {/* CSS to center charmie's image */} 64 |
65 |
66 | Charmie
67 | Charmie Dubongco
68 | {/* links to charmie's github & linkedin */} 69 |
70 | 71 | LinkedIn 72 | 73 |
74 |
75 | 76 | github 77 | 78 |
79 |
80 | 81 | {/* CSS to center tanner's image */} 82 |
83 | Tanner
84 | Tanner Robertson
85 |
86 | {/* links to tanner's github & linkedin */} 87 | 88 | LinkedIn 89 | 90 |
91 |
92 | 93 | github 94 | 95 |
96 |
97 | {/* CSS to center ariel's image */} 98 |
99 | Ariel
100 | Ariel Maor
101 |
102 | {/* links to ariel's github & linkedin */} 103 | 104 | LinkedIn 105 | 106 |
107 |
108 | 109 | github 110 | 111 |
112 |
113 | 114 | 115 |
116 | {/* CSS to center vicky's image */} 117 | Vicky
118 | Vicky Hoang
119 |
120 | {/* links to vicky's github & linkedin */} 121 | 122 | LinkedIn 123 | 124 |
125 |
126 | 127 | github 128 | 129 |
130 |
131 |
132 |
133 | ); 134 | } 135 | 136 | export default About; 137 | -------------------------------------------------------------------------------- /src/components/dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { AppBar, Toolbar, Button, Typography, Container, Menu, MenuItem, Box } from '@mui/material'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import '../stylesheets/dashboard.css'; 5 | import image_png from '../../public/image_png.png'; 6 | import renderpup from '../../public/renderpup.png'; 7 | import runningDog from '../../public/runningDog.gif'; 8 | 9 | const Dashboard = ({ updateState, currState }) => { 10 | const [open, setOpen] = useState(false); 11 | // use state hook to control the position of the dropdown on 'Fetch Metrics' button in navbar 12 | const [anchorEl, setAnchorEl] = useState(null); 13 | // use state hook to handle receiving peformance metrics data 14 | const [loading, setLoading] = useState(false); 15 | //use state hook to update url's 16 | const [urlList, setUrls] = useState(new Set()); 17 | //use navigate hook to navigate between different react components 18 | const navigate = useNavigate(); 19 | 20 | useEffect(() => { 21 | 22 | function fetchUrls() { 23 | fetch('/api') 24 | .then(response => response.json()) 25 | .then(data => { 26 | let newUrl 27 | const uniqueUrls = new Set() 28 | // update url's not to include '/' with each query 29 | for (const url of data.urls) { 30 | if (url.url[url.url.length - 1] === '/') { 31 | const tempArr = url.url.split('') 32 | tempArr.splice(url.url.length - 1, 1) 33 | newUrl = tempArr.join('') 34 | } 35 | uniqueUrls.add(newUrl) 36 | } 37 | setUrls(uniqueUrls) 38 | }) 39 | .catch(err => console.log('error in fetchUrls', err)) 40 | } 41 | 42 | fetchUrls() 43 | sessionStorage.setItem("loggedIn", '/dashboard') 44 | }, [currState]); 45 | 46 | //handleOpen function opens list of user's queries saved from database on 'fetch metrics' button on navbar 47 | const handleOpen = (event) => { 48 | setAnchorEl(event.currentTarget); 49 | setOpen(!open); 50 | }; 51 | //handleOpen function closes list of user's queries saved from database on 'fetch metrics' button on navbar 52 | const handleClose = () => { 53 | setAnchorEl(null); 54 | setOpen(false); 55 | }; 56 | 57 | // handleAbout function = functionality to redirect user to logout component when clicking 'About' on navbar 58 | const handleAbout = () => { 59 | navigate('/about'); 60 | } 61 | // handleSignout function = functionality to redirect user to logout component when clicking 'Sign Out' on navbar 62 | const handleSignOut = () => { 63 | // navigate to logout component 64 | navigate('/logout'); 65 | } 66 | 67 | const buttons = [] 68 | 69 | const urls = Array.from(urlList) 70 | for (let i = 0; i < urls.length; i++) { 71 | buttons.push( handleWebsite(`${urls[i]}`)} key={crypto.randomUUID()}>{`${urls[i]}`}) 72 | } 73 | 74 | // handles logic when a user clicks on anything in the dashboard related to grabbing existing data 75 | // ex. saved metrics from user when clicking 'FETCH METRICS', querying metrics from new websites when clicking 'GO FETCH' 76 | const handleWebsite = async (url) => { 77 | const data = await getExistingData(url) 78 | }; 79 | 80 | // Handle fetching new data from server 81 | function getNewData() { 82 | setLoading(true); 83 | //grabs data from search bar and clears it 84 | const urlField = document.querySelector('.app-input-field') 85 | let currUrl = urlField.value 86 | urlField.value = '' 87 | 88 | currUrl = JSON.stringify({url: currUrl}) 89 | 90 | fetch('/api', { 91 | method: 'POST', 92 | headers: { 93 | 'Content-Type': 'application/json' 94 | }, 95 | body: currUrl, 96 | }) 97 | .then(response => { 98 | setLoading(false); 99 | //then checks of status code is ok (200-299); if not, throw 404 error 100 | if (!response.ok) { 101 | console.error(`Network response is not rendering, ${response.status} error`) 102 | throw new Error('response not ok') 103 | } 104 | return response.json(); 105 | }) 106 | //use useState to access TTFB 107 | .then(async data => { 108 | if (currState.data[0].url === 0) { 109 | const strippedUrl = data.data.metrics.url.slice(0, data.data.metrics.url.length - 1) 110 | await getExistingData(strippedUrl) 111 | } 112 | else if (data.data.metrics.url === currState.data[0].url) { 113 | // in order to return data back as mutable, take all data from currState as indiv elements (spread) 114 | // & make new arr tempArr to store it in to be able to update state 115 | const tempArr = [...currState.data] 116 | tempArr.push(data.data.metrics) 117 | const newData = {data: tempArr} 118 | // updates state with new data from post req that's in the same format as initial state (refer to app) 119 | updateState(newData) 120 | } 121 | setLoading(false); //ends the loading time window 122 | }) 123 | .catch(error => { 124 | console.log('UNEXPECTED ERROR: ', error); 125 | }); 126 | } 127 | 128 | // Handle fetching existing data from server based on URL 129 | function getExistingData(currUrl) { 130 | // make a http request to /api/urls 131 | fetch('/api/urls', { 132 | method: 'POST', 133 | headers: { 134 | 'Content-Type': 'application/json' 135 | }, 136 | body: JSON.stringify({url: currUrl}), 137 | }) 138 | .then(response => { 139 | //then checks of status code is ok (200-299); if not, throw 404 error 140 | if (!response.ok) { 141 | console.error(`Network response is not rendering, ${response.status} error`) 142 | throw new Error('response not okay') 143 | } 144 | return response.json(); 145 | }) 146 | //use useState to access TTFB 147 | .then(data => { 148 | updateState(data); 149 | }) 150 | .catch(error => { 151 | console.log('UNEXPECTED ERROR: ', error) 152 | }); 153 | } 154 | 155 | const fetchingDog = ; 156 | 157 | return ( 158 | //div testid for testing dashboardTwo 159 |
160 | {/* MUI functionality to render navbar */} 161 |
162 | 163 | 164 | 165 | 166 | logo 167 | 168 | 169 | RenderPup 170 | 171 | 174 | 177 | 180 | 185 | {buttons} 186 | 187 | 188 | 189 | 190 | 191 | 192 |
193 | 194 | dogFetchingBall 195 |

Sniffing Out Performance and Fetching Results!

196 |
197 | 198 |
199 | 200 |
201 | 204 |

205 | 206 | 207 | 208 |
209 | {/* functionality that renders corgi gif after hitting 'fetch' buton */} 210 | { loading ? ( 211 |
212 |

Fetching...

213 | {fetchingDog} 214 |
215 | ) : null} 216 |
217 |
218 | ); 219 | }; 220 | 221 | export default Dashboard; 222 | -------------------------------------------------------------------------------- /server/controllers/metrics.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/model') 2 | 3 | metricsController = {} 4 | 5 | metricsController.timeToFirstByte = async (req, res, next) => { 6 | try { 7 | let totalTime = 0 8 | let responseData 9 | for (let i = 0; i < 1; i++) { 10 | const startTime = new Date() 11 | 12 | //await keyword pauses for loop where promise chaining did not 13 | response = await fetch(`${req.body.url}`) 14 | totalTime += new Date() - startTime 15 | responseData = response 16 | } 17 | 18 | //converts fetch response to blob data 19 | const blobData = await responseData.blob() 20 | 21 | //converts blob data to a readable stream 22 | const streamData = blobData.stream() 23 | 24 | //gets a reader that can read that can read the readable stream 25 | const reader = streamData.getReader() 26 | 27 | //reads the readable stream into a uint8array 28 | const readerData = await reader.read() 29 | 30 | //takes the uint8array and converts each number back into the character they represent 31 | const integerStrings = readerData.value.toString().split(','); 32 | const integers = integerStrings.map(value => parseInt(value, 10)); 33 | const characters = integers.map(code => String.fromCharCode(code)); 34 | const resultHtml = characters.join(''); 35 | 36 | //stores the ttfb and response html on the res.locals object 37 | res.locals.data = resultHtml; 38 | res.locals.metrics = {ttfb: totalTime / 1}; 39 | res.locals.metrics.url = req.body.url; 40 | 41 | next(); 42 | } 43 | catch(error) { 44 | next({log: `invalid url: '${req.body.url}'`, status: 404, message: 'Please enter a valid url'}); 45 | } 46 | } 47 | 48 | metricsController.getDatabaseData = async (req, res, next) => { 49 | //Selects all data from the metrics table and attaches the rows to the res.locals object 50 | const data = await db.query(`SELECT * FROM metrics WHERE url='${req.body.url}/' AND user_id=${req.cookies.userId}`) 51 | res.locals.databaseData = data.rows 52 | 53 | next() 54 | } 55 | 56 | metricsController.getUrls = async (req, res, next) => { 57 | //Selects all urls from the metrics table and attaches the rows to the res.locals object 58 | const data = await db.query(`SELECT url FROM metrics WHERE user_id = '${req.cookies.userId}'`) 59 | res.locals.urls = data.rows 60 | 61 | next() 62 | } 63 | 64 | metricsController.saveMetrics = async (req, res, next) => { 65 | const { url } = req.body 66 | const { ttfb, fcp, lcp, nsl, bs } = res.locals.metrics 67 | const performanceScore = res.locals.performanceScore 68 | // lighthouse names diagnostics as opportunities, renaming to diagnotics 69 | const diagnostics = res.locals.diagnostics 70 | // const opportunities = res.locals.opportunities 71 | res.locals.metrics.date = new Date() 72 | await db.query('INSERT INTO metrics (url, user_id, ttfb, fcp, lcp, nsl, bs) ' + `VALUES ('${url}', ${req.cookies.userId}, ${ttfb}, ${fcp}, ${lcp}, ${nsl}, '${JSON.stringify(bs)}')`); 73 | next() 74 | }; 75 | 76 | metricsController.getScriptSize = async (req, res, next) => { 77 | 78 | //takes html text in as a parameter and returns size of each script tag 79 | async function helpGetSize(html) { 80 | //initialized variables to be used later on 81 | const Html = html.split(''); 82 | let startScript = false 83 | let size = 0; 84 | let sizes = {'root': 0} 85 | let start; 86 | 87 | //loops through all html characters 88 | for (let i = 0; i < Html.length; i++) { 89 | //if current character is the start of a tag, it will go through to check what tag 90 | if (Html[i] === '<') { 91 | 92 | //if a starting script tag closes without any attributes we just record the starting point of the content 93 | //and change a startScript variable to true 94 | let strSlice = Html.slice(i, i + 8); 95 | if (strSlice.join('') === '') { 187 | if (startScript) { 188 | const thisSize = Html.slice(start, i).join('').length; 189 | size += thisSize 190 | sizes.root += thisSize 191 | start = ''; 192 | startScript = false 193 | } 194 | } 195 | } 196 | } 197 | return sizes 198 | } 199 | 200 | try { 201 | const totalSize = await helpGetSize(res.locals.data) 202 | res.locals.metrics.bs = totalSize 203 | next() 204 | } 205 | catch { 206 | next({log: 'Error in getScriptSize middleware', message: 'Error occured in fetching bundle size'}) 207 | } 208 | } 209 | 210 | module.exports = metricsController; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RenderPup 2 | 3 |
4 | renderpup-logo 5 |
6 | 7 |
8 | 9 | [![JavaScript](https://img.shields.io/badge/javascript-yellow?style=for-the-badge&logo=javascript&logoColor=white)](https://www.javascript.com/) 10 | [![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB)](https://react.dev/) 11 | [![ReactRouter](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white)](https://reactrouter.com/en/main) 12 | [![Node](https://img.shields.io/badge/-node-339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/en) 13 | ![Express](https://img.shields.io/badge/-Express-000000?style=for-the-badge&logo=express&logoColor=white) 14 | ![Bcrypt](https://img.shields.io/badge/BCRYPT-grey?style=for-the-badge&logo=letsencrypt) 15 | [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white)](https://www.postgresql.org/) 16 | [![MaterialUI](https://img.shields.io/badge/Material%20UI-007FFF?style=for-the-badge&logo=mui&logoColor=white)](https://mui.com/) 17 | [![ChartJS](https://img.shields.io/badge/Chart%20js-FF6384?style=for-the-badge&logo=chartdotjs&logoColor=white)](https://www.chartjs.org/) 18 | ![HTML](https://img.shields.io/badge/HTML5-E34F26?style=for-the-badge&logo=html5&logoColor=white) 19 | ![CSS](https://img.shields.io/badge/CSS3-1572B6?style=for-the-badge&logo=css3&logoColor=white) 20 | [![Jest](https://img.shields.io/badge/-jest-C21325?style=for-the-badge&logo=jest&logoColor=white)](https://jestjs.io/) 21 | [![Webpack](https://img.shields.io/badge/Webpack-8DD6F9?style=for-the-badge&logo=Webpack&logoColor=white)](https://webpack.js.org/) 22 | [![Miro](https://img.shields.io/badge/Miro-F7C922?style=for-the-badge&logo=Miro&logoColor=050036)](https://miro.com/) 23 | [![Github](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/) 24 |
25 | 26 | RenderPup is a web application designed to analyze Next.js websites by running various performance tests and providing insightful metrics. With RenderPup, users can input a URL for a Next.js website, and the application will conduct tests to measure important performance metrics such as time to first byte, first and largest contentful paint, network server latency, and bundle size. 27 | 28 | ## Description 29 | 30 | RenderPup addresses the critical need for developers and website owners to understand the performance characteristics of their Next.js websites. By providing comprehensive metrics, RenderPup enables users to identify potential bottlenecks and optimize their websites for better user experience and search engine rankings. 31 | 32 | ### Key Features 33 | 34 | - **Performance Testing**: RenderPup conducts various tests to assess the performance of Next.js websites. 35 | - **Real-Time Metric Analysis**: It provides detailed metrics including time to first byte, first and largest contentful paint, network server latency, and bundle size. 36 | - **User-friendly Interface**: RenderPup features a simple and intuitive interface, making it easy for users to input URLs and view performance metrics. 37 | - **Dashboard**: A user-friendly dashboard to visualize performance metrics and gain actionable insights. 38 | 39 | ## How to Use 40 | 41 | Prerequisites 42 | 43 | 1. Fork this repo and clone using the following command: 44 | ```sh 45 | git clone https://github.com/oslabs-beta/RenderPup.git 46 | ``` 47 | 2. In the root folder, install all the dependencies needed for the application using the following command: 48 | ```sh 49 | npm i 50 | ``` 51 | 3. Add postgreSQL connection string to PG_URI in .env file 52 | ```sh 53 | PG_URI = 54 | ``` 55 | 4. Add .env to .gitignore file for security before committing and pushing to github 56 | 57 | 5. Run the following command to ensure all dependancy updates are reflected: 58 | ```sh 59 | npm run build 60 | ``` 61 | 6. Run the application in the root folder using the following command: 62 | ```sh 63 | npm start 64 | ``` 65 | 66 | ## Setting up PostgreSQL Database 67 | 68 | 1. Create a 'Users' table with columns '_id', 'username', 'password', 'firstName', 'lastName' and 'email'. 69 | - '_id' will act as the primary key for the 'Users' table. 70 | 2. Create a 'Metrics' table with columns '_id', 'user_id', 'url', 'date', 'ttfb', 'fcp', 'lcp', 'nsl', and 'bs'. 71 | - '_id' will act as the primary key for the 'Metrics' table. 72 | - 'user_id' will act as the foreign key to the 'Users' table. 73 | 3. Create a 'Diagnostics' table with columns '_id', 'user_id', 'performance_score', 'diagnostics_info', and 'url'. 74 | - '_id' will act as the primary key. 75 | - 'user_id' will act as the foreign key to the 'Users' table. 76 | 77 |

78 | 79 |

80 | 81 | ## Running The Application 82 | 83 | 1. Create a new user by clicking on the "Don't have an account? Sign up!" button near the bottom right of the "Sign In" button: 84 | 85 |

86 | 87 |

88 | 89 | 2. Once signed up, use the login in credentials you just created to log in to your dashboard: 90 | 91 |

92 | 93 |

94 | 95 | 3. To begin searching performance metrics, input a web application url in the empty text input bar, followed by pressing the "Go Fetch" button directly to the right: 96 | 97 |

98 | 99 |

100 | 101 | 4. View any web application url performance metrics previously searched by clicking the "Fetch Metrics" button in the top right corner inside of the navigation bar: 102 | 103 |

104 | 105 |

106 | 107 | - To view updated performance metrics charts/data, navigate to the intended url in your "Fetch Metrics" tab, followed by an additional run on said url in the input bar. This will show the most recent metrics in combination with previous runs on said url for comparison. 108 | 109 | - You may also hover over any graph data to view the singular metric data point. 110 | 111 | 5. Once you have completed your work with the application, you may sign out using the "Sign Out" button in the top right corner on the navbar. This will sign you out and lead you back to the log in page. 112 | 113 |

114 | 115 |

116 | 117 | ## How to Contribute 118 | 119 | We welcome contributions to the RenderPup repository! To contribute, follow these steps: 120 | 121 | 1. Fork the RenderPup repository to your GitHub account. 122 | 2. Clone the forked repository to your local machine (make sure to change where it says your-github-username to your username): 123 | ```bash 124 | git clone https://github.com/your-github-username/oslabs-beta/RenderPup.git 125 | ``` 126 | 3. Create a new branch for your feature or bug fix: 127 | ```bash 128 | git checkout -b feature-name 129 | ``` 130 | 4. Make your changes and commit them with descriptive commit messages: 131 | ```bash 132 | git commit -m "Description of changes" 133 | ``` 134 | 5. Push your changes to your forked repository: 135 | ```bash 136 | git push origin feature-name 137 | ``` 138 | 6. Create a pull request from your forked repository to the main RenderPup repository. 139 | 7. Our team will review your pull request and merge it if it aligns with the project's goals. 140 | 141 | Thank you for contributing to RenderPup! Your contributions help improve the performance analysis capabilities of Next.js websites. 142 | 143 | ## Iteration Opportunities 144 | 145 | | Feature | Status | 146 | |---------------------------------------------------------------------------------------|-----------| 147 | | Create component for rendering performance score and opportunities provided through lighthouse report on a per fetched url basis | ✅ | 148 | | Add additional metric data gathered through lighthouse with ability for user to select which metrics to collect and view | ⏳ | 149 | | Convert time stamp to display local time zone of user | ⏳ | 150 | | Add a landing page for app | 🙏🏻 | 151 | | Add functionality for O-Auth for accessible user creation and login | 🙏🏻 | 152 | | Convert to desktop based application | 🙏🏻 | 153 | | 154 | 155 | - ✅ = Ready to use 156 | - ⏳ = In progress 157 | - 🙏🏻 = Looking for contributors 158 | 159 | ## Our Team 160 |
161 | 162 | 163 | 172 | 181 | 190 | 199 | 200 |
164 | 165 |
166 | Tanner Robertson 167 |
168 | 🐙 GitHub 169 |
170 | 🖇️ LinkedIn 171 |
173 | 174 |
175 | Ariel Maor 176 |
177 | 🐙 GitHub 178 |
179 | 🖇️ LinkedIn 180 |
182 | 183 |
184 | Charmie Dubongco 185 |
186 | 🐙 GitHub 187 |
188 | 🖇️ LinkedIn 189 |
191 | 192 |
193 | Vicky Hoang 194 |
195 | 🐙 GitHub 196 |
197 | 🖇️ LinkedIn 198 |
201 |
202 | --------------------------------------------------------------------------------