├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── astrospeed ├── bundle.js ├── index.html └── results.js ├── gitHookInstall.js ├── gitHookUninstall.js ├── package.json ├── public └── results.js ├── server.js ├── src ├── App.tsx ├── assets │ └── moreArrow.svg ├── components │ ├── Card.jsx │ ├── DialChart.jsx │ ├── LineChart.jsx │ ├── ListContainer │ │ ├── DiagnosticsItem.jsx │ │ ├── ListContainer.jsx │ │ └── styles.module.css │ └── chartdata.js ├── index.tsx └── styles.css ├── tsconfig.json ├── webpack.config.js └── website ├── .npmrc ├── astro.config.mjs ├── demo ├── bundle.js ├── index.html └── results.js ├── package.json ├── public ├── Audrey-Park.webp ├── How-it-works-chart.webp ├── Luke-McInerney.webp ├── Marco-Gonzalez.webp ├── Rami-Abdelghafar.webp ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── astronaut.png ├── colored-logo.png ├── crater-footer.webp ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── favicon.svg ├── moonTelescope.png ├── site.webmanifest ├── space-bg.png ├── tether.png └── white-logo.png ├── server.js ├── src ├── components │ ├── Astronaut.astro │ ├── Button.astro │ ├── Comparisons.astro │ ├── ContactCard.astro │ ├── ContactUs.astro │ ├── NavBar.astro │ ├── RocketChart.astro │ └── SplashPoster.astro ├── env.d.ts ├── layouts │ └── main.astro └── pages │ ├── getstarted.astro │ ├── index.astro │ ├── login.astro │ └── watchdemo.astro ├── tailwind.config.cjs └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | modules: false 7 | } 8 | ], 9 | '@babel/preset-react' 10 | ] 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | package-lock.json 5 | .vscode 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .vscode 5 | package-lock.json 6 | src 7 | .babelrc 8 | tsconfig.json 9 | webpack.config.js 10 | astrospeed/results.js 11 | public/results.js 12 | website -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OSLabs Beta 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # astroSpeed 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 16 |
17 | 18 |

19 | 20 | [![Contributors][contributors-shield]][contributors-url] 21 | [![Stargazers][stars-shield]][stars-url] 22 | [![Issues][issues-shield]][issues-url] 23 | [![MIT License][license-shield]][license-url] 24 | [![LinkedIn][linkedin-shield]][linkedin-url] 25 | 26 |
27 | 28 | 29 |
30 |
31 | 32 | Logo 33 | 34 | 35 |

astroSpeed

36 | 37 |

38 | Performance monitor for Astro & Next.js web applications. 39 |
40 | Explore astrospeed.io » 41 |
42 |
43 | View Demo 44 | · 45 | Report Bug 46 | · 47 | Request Feature 48 |

49 |
50 | 51 | 52 | 53 | 54 |
55 | Table of Contents 56 |
    57 |
  1. 58 | About The Project 59 | 62 |
  2. 63 |
  3. 64 | Getting Started 65 | 69 |
  4. 70 |
  5. About
  6. 71 | 75 |
  7. Roadmap
  8. 76 |
  9. Contributing & Contacts
  10. 77 |
  11. Acknowledgments & License
  12. 78 |
79 |
80 | 81 | 82 | 83 | 84 |
85 | 86 | ## Summary 87 | 88 | astroSpeed is a data collection and reporting suite that uses the Google Lighthouse SDK to automatically collect application metrics such as Performance and SEO in the background during the development process, after each commit. 89 | 90 | 91 | sample 92 | 93 | 94 | ### Built With 95 | 96 | * [![Astro][Astro-shield]][Astro-url] 97 | * [![React][React.js]][React-url] 98 | * [![Google LightHouse][GLH-shield]][GLH-url] 99 | * [![TypeScript][Typescript-shield]][Typescript-url] 100 | * [![Tailwind][Tailwind-shield]][Tailwind-url] 101 | 102 | 103 | ## Getting Started 104 | 105 | astroSpeed is available in Node Package Manager and Github. The following steps detail installation from NPM. 106 | 107 | ### Prerequisites 108 | 109 | * Node.js - 14.18.0 or higher. 110 | * Astro - 1.2.0 or higher. 111 | * Git 112 | ### Installation & Usage 113 | 114 | · 115 | watch the install demo here -or- follow instructions below... 116 | 117 | 1. Start or open an Astro project. 118 | 2. Install astroSpeed into the project as a dev dependency. 119 | ```sh 120 | npm install astrospeed --save-dev 121 | ``` 122 | 3. Optional - create astrospeed.config.json in your project's root directory if you want to manually configure astroSpeed. Add 1 or more of the below configurables to the config file, below values indicate the default. 123 | ```json 124 | { 125 | "endpoints": ["/"], 126 | "port": 3000, 127 | "buildCommand": "npm run build", 128 | "outputDir": "dist", 129 | "useVite": 1 130 | } 131 | ``` 132 | 4. Commit new changes to your project's git repository. The post-commit Git hook will automatically start the astroSpeed report generation. 133 | ```sh 134 | git commit -a -m "your commit message here" 135 | ``` 136 | 5. The path to the newly generated astroSpeed report is written to stdout in your terminal (by default under the "astrospeed" folder in your project's root directory). 137 | ```sh 138 | git commit -a -m "your commit message here" 139 | [main 5958617] your commit message here 140 | 1 files changed, 3 insertions(+), 2 deletions(-) 141 | Astrospeed report in progress... 142 | Astrospeed report available at /astrospeed/index.html 143 | ``` 144 | 6. You've launched! Open the report in a web browser to review the latest Google Lighthouse findings and compare them to previous commits. 145 | 146 | 7. Each commit will regenerate the astroSpeed report, appending the latest Google Lighthouse scores to the report. 147 | 148 | ## About 149 | ### Google Lighthouse 150 | Google Lighthouse is an open-source tool developed by Google for understanding the performance, quality, and correctness of your web apps ([see Google Lighthouse on Github](https://github.com/GoogleChrome/lighthouse)). astroSpeed uses Google Lighthouse metrics for Performance, Search Engine Optimization, Best Practices, and Accessibility diagnostics. 151 | ### Git post-commit hook details 152 | 153 | astroSpeed is configured to create a report after every commit using a git hook. astroSpeed's installation script attempts to add a post-commit hook. The installation script will notify you via the terminal if the attempt was successful or not. If successful, the the following line will be added to the file ./.git/hooks/post-commit. 154 | ```sh 155 | npx astrospeed-snap 156 | ``` 157 | npx astrospeed-snap can also be used to generate a new report manually whenever you want. If you would would like to remove the post-commit hook, please don't uninstall astroSpeed. Simply remove the hook by using 158 | ```sh 159 | npx astrospeed-hookuninstall 160 | ``` 161 | If you change your mind later and wish to reinstate the post-commit hook, you can add it back using 162 | ```sh 163 | npx astrospeed-hookinstall 164 | ``` 165 | If you unfortunately decided to stop using astroSpeed, please uninstall the hook BEFORE uninstalling astroSpeed. Failure to do so will result in error messages after every commit about not being able to find astroSpeed. Your commits should still be successful, however. This unfortunate behavior is due to Node Package Manager removing the ability to create post uninstall scripts. 166 | 167 | ## Roadmap 168 | 169 | - [ ] Upload report to astroSpeed.io and receive a shareable link. 170 | - [ ] Add Deep-dive performance metrics. 171 | - [ ] Create user authentication on astroSpeed.io. 172 | 173 | See the [open issues](https://github.com/oslabs-beta/astrospeed/issues) for a full list of proposed features (and known issues). 174 | 175 | ## Contributing 176 | 177 | Contributions are welcome! For feature requests, please create a new issue with the "enhancement" tag. 178 | 179 | To contribute: 180 | 1. Fork the Project. 181 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`). 182 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`). Be sure to Lint your code! 183 | 4. Push to the Branch (`git push origin feature/AmazingFeature`). 184 | 5. Open a Pull Request to [https://github.com/oslabs-beta/astrospeed/](https://github.com/oslabs-beta/astrospeed/). 185 | 186 | Join astroSpeed contributors on [LinkedIn](https://linkedin.com/in/astrospeed). 187 | 188 | ## Acknowledgments & License 189 | 190 | * [OpenSource Labs](https://opensourcelabs.io/) 191 | * [Astro Community](https://discord.com/invite/grF4GTXXYm) 192 | * [You, our users <3](http://www.astrospeed.io) 193 | 194 | Distributed under the MIT License. See `LICENSE` for more information. 195 | 196 |

(back to top)

197 | 198 | 199 | 200 | 201 | 202 | [contributors-shield]: https://img.shields.io/github/contributors/oslabs-beta/astrospeed.svg?style=for-the-badge 203 | [contributors-url]: https://github.com/oslabs-beta/astrospeed/graphs/contributors 204 | [forks-shield]: https://img.shields.io/github/forks/oslabs-beta/astrospeed.svg?style=for-the-badge 205 | [forks-url]: https://github.com/oslabs-beta/astrospeed/network/members 206 | [stars-shield]: https://img.shields.io/github/stars/oslabs-beta/astrospeed.svg?style=for-the-badge 207 | [stars-url]: https://github.com/oslabs-beta/astrospeed/stargazers 208 | [issues-shield]: https://img.shields.io/github/issues/oslabs-beta/astrospeed.svg?style=for-the-badge 209 | [issues-url]: https://github.com/oslabs-beta/astrospeed/issues 210 | [license-shield]: https://img.shields.io/github/license/oslabs-beta/astrospeed.svg?style=for-the-badge 211 | [license-url]: https://github.com/oslabs-beta/astrospeed/blob/master/LICENSE.txt 212 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 213 | [linkedin-url]: https://linkedin.com/company/astrospeed 214 | [product-screenshot]: https://i.ibb.co/pz6nzrz/Screen-Shot-2022-10-12-at-3-20-46-PM.png 215 | 216 | [Astro-url]: https://astro.build/ 217 | [Astro-shield]: https://i.ibb.co/F8HcbtD/Screen-Shot-2022-10-12-at-12-34-42-PM.png 218 | [React.js]: https://i.ibb.co/MBLkXB6/Screen-Shot-2022-10-12-at-12-21-22-PM.png 219 | [React-url]: https://reactjs.org/ 220 | [GLH-shield]: https://i.ibb.co/LRZ9mgh/Screen-Shot-2022-10-12-at-12-23-35-PM.png 221 | [GLH-url]: https://developers.google.com/web 222 | [Typescript-shield]: https://i.ibb.co/7tT9vy1/Screen-Shot-2022-10-12-at-12-25-59-PM.png 223 | [Typescript-url]: https://www.typescriptlang.org 224 | [Tailwind-url]: https://tailwindcss.com 225 | [Tailwind-shield]: https://i.ibb.co/H7M7D3b/Screen-Shot-2022-10-12-at-12-32-22-PM.png 226 | 227 | -------------------------------------------------------------------------------- /astrospeed/index.html: -------------------------------------------------------------------------------- 1 | astroSpeed Report
-------------------------------------------------------------------------------- /gitHookInstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs"); 4 | const { resolve } = require("path"); 5 | 6 | const hook = "post-commit"; 7 | 8 | function installHooks() { 9 | //Make sure there is a .git folder 10 | const gitRoot = resolve(process.env.INIT_CWD + "/.git"); 11 | if (fs.existsSync(gitRoot)) { 12 | const hooksDir = resolve(gitRoot, "hooks"); 13 | ensureDirExists(hooksDir); //Add hooks folder if it doesn't exist 14 | const hookFile = resolve(hooksDir, hook); 15 | if (fs.existsSync(hookFile)) { 16 | let fileContents = fs.readFileSync(hookFile, 'utf-8'); 17 | if (fileContents.includes('npx astrospeed-snap')) { 18 | console.warn("Correct git hook already in place"); 19 | } else { 20 | fs.appendFileSync(hookFile, '\nnpx astrospeed-snap\n'); 21 | console.log('astroSpeed hook appended to existing post-commit hooks'); 22 | } 23 | return; 24 | } 25 | fs.writeFileSync( 26 | hookFile, 27 | `#!/bin/sh\nnpx astrospeed-snap\n` 28 | ); //create hook file 29 | fs.chmodSync(hookFile, "755"); //make hook file executable 30 | console.log("astroSpeed git integration was successful!"); 31 | } else { 32 | console.warn("This does not seem to be a git project."); 33 | } 34 | } 35 | 36 | function ensureDirExists(dir) { 37 | fs.existsSync(dir) || fs.mkdirSync(dir); 38 | } 39 | 40 | installHooks(); 41 | -------------------------------------------------------------------------------- /gitHookUninstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs"); 4 | const { resolve } = require("path"); 5 | 6 | const hookString = "npx astrospeed-snap"; 7 | //Make sure there is a .git folder 8 | const gitRoot = resolve(process.env.INIT_CWD + "/.git"); 9 | 10 | if (fs.existsSync(gitRoot)) { 11 | const hookFile = resolve(gitRoot, "hooks/post-commit"); 12 | //check if hook file exists 13 | if (fs.existsSync(hookFile)) { 14 | let fileContents = fs.readFileSync(hookFile, 'utf-8'); 15 | //check if our hook string is present in hook file 16 | if (fileContents.includes(hookString)) { 17 | const newContents = fileContents.replaceAll(hookString,''); 18 | fs.writeFileSync(hookFile, newContents, 'utf-8'); 19 | console.log('astroSpeed post-commit hooks removed'); 20 | } else { 21 | //else hookstring is not present 22 | console.warn("No astroSpeed hooks found in post-commit hooks"); 23 | } 24 | } else { 25 | // no post-commit file found in hooks 26 | console.warn('No post-commit hooks found'); 27 | } 28 | 29 | } else { 30 | // else no .git folder 31 | console.warn("This does not seem to be a git project."); 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astrospeed", 3 | "version": "1.0.0", 4 | "description": "Astrospeed is a performance monitor for your AstroJS codebase", 5 | "main": "index.js", 6 | "scripts": { 7 | "postinstall": "./gitHookInstall.js", 8 | "clean": "rm dist/bundle.js", 9 | "build-dev": "webpack --mode development", 10 | "build-prod": "webpack --mode production", 11 | "start": "webpack-dev-server --mode development --open --hot" 12 | }, 13 | "bin": { 14 | "astrospeed-snap": "./server.js", 15 | "astrospeed-hookinstall": "./gitHookInstall.js", 16 | "astrospeed-hookuninstall": "./githookUninstall.js" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "express": "^4.18.1", 22 | "lighthouse": "^9.6.7", 23 | "puppeteer": "^18.0.4", 24 | "tree-kill": "^1.2.2" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.19.1", 28 | "@babel/preset-env": "^7.19.1", 29 | "@babel/preset-react": "^7.18.6", 30 | "@svgr/webpack": "^6.3.1", 31 | "@types/react": "^18.0.21", 32 | "@types/react-dom": "^18.0.6", 33 | "babel-loader": "^8.2.5", 34 | "css-loader": "^6.7.1", 35 | "html-webpack-plugin": "^5.5.0", 36 | "react": "^18.2.0", 37 | "react-apexcharts": "^1.4.0", 38 | "react-dom": "^18.2.0", 39 | "style-loader": "^3.3.1", 40 | "ts-loader": "^9.4.1", 41 | "typescript": "^4.8.3", 42 | "url-loader": "^4.1.1", 43 | "webpack": "^5.74.0", 44 | "webpack-cli": "^4.10.0", 45 | "webpack-dev-server": "^4.11.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const lighthouse = require('lighthouse'); 4 | const puppeteer = require('puppeteer'); 5 | const express = require('express'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | const { execSync, exec } = require("child_process"); 9 | const kill = require('tree-kill'); 10 | 11 | const { endpoints, port, buildCommand, outputDir, localServer } = readConfig(); 12 | let server; 13 | let viteProc; 14 | 15 | function readConfig() { 16 | //check if user-defined custom config exists, else use defaults 17 | const defaultCfg = { 18 | endpoints: ['/index.html'], 19 | port: 3000, 20 | buildCommand: 'npm run build', 21 | outputDir: 'dist', 22 | localServer: 'vite' 23 | } 24 | try { 25 | const config = fs.readFileSync(path.resolve(path.join(__dirname, '../../astrospeed.config.json'))); 26 | return Object.assign({}, defaultCfg, JSON.parse(config)); 27 | } catch (err){ 28 | return defaultCfg 29 | } 30 | } 31 | 32 | function readExistingData () { 33 | //check if results.js already exists, if so return the contents, otherwise return empty array 34 | try { 35 | const pathToFile = './astrospeed/results.js' 36 | const oldData = fs.readFileSync(pathToFile); 37 | //remove the window.results assignment to just get the array 38 | const oldDataParsed = oldData.slice(16) 39 | return JSON.parse(oldDataParsed); 40 | } catch (err){ 41 | return {}; 42 | } 43 | } 44 | 45 | console.log('Astrospeed report in progress...') 46 | 47 | buildApp(); 48 | serveAppAndRunLHR(); 49 | 50 | //build the user's astro app to the 'dist' folder 51 | function buildApp() { 52 | if (localServer == 'vite'){ 53 | execSync('npx astro build') 54 | viteProc = exec('npx astro preview'); 55 | } else{ 56 | execSync(buildCommand) 57 | } 58 | 59 | } 60 | 61 | async function serveAppAndRunLHR() { 62 | if (localServer == 'vite') { 63 | getReport(endpoints) 64 | } else { 65 | //serve the astro app on port 3500 66 | const app = express(); 67 | app.use('*', express.static(outputDir)); 68 | server = app.listen(port, () => getReport(endpoints)); 69 | } 70 | } 71 | 72 | async function getLighthouseResultsPuppeteer(endpoints) { 73 | const chrome = await puppeteer.launch({args: ['--remote-debugging-port=9224'],}); 74 | const options = { 75 | logLevel: 'silent', 76 | output: 'html', 77 | maxWaitForLoad: 10000, 78 | port: 9224 79 | }; 80 | const runnerResults = {}; 81 | 82 | for (let i = 0; i < endpoints.length; i++) { 83 | const result = await lighthouse(`http://localhost:${port}` + endpoints[i], options) 84 | runnerResults[endpoints[i]] = result.lhr; 85 | } 86 | // const runnerResult = await lighthouse(url, options); 87 | await chrome.close(); 88 | return runnerResults; 89 | } 90 | 91 | function getCommitDetails() { 92 | //get latest commit from git log, format it to include commit body (%B), timestamp (%ai), hash (%h) 93 | let commitMsg = execSync('git log -1 --pretty="%B%ai%n%h"').toString().replace(/\n/g,',').split(',').slice(0,3); 94 | let commitProperties = ['msg', 'time', 'hash'] 95 | let newCommitData = {}; 96 | for (let i = 0; i < 3; i++) { 97 | newCommitData[commitProperties[i]] = commitMsg[i]; 98 | } 99 | //return latest object with latest commit details 100 | return newCommitData; 101 | } 102 | 103 | async function getReport(endpoints) { 104 | 105 | //use puppeteer to get lighthouse results object and store it in lhr 106 | const lhr = await getLighthouseResultsPuppeteer(endpoints); 107 | //close express server after lighthouse returns results 108 | if (localServer != 'vite') server.close(); 109 | else kill(viteProc.pid) 110 | // read results.js 111 | const data = readExistingData(); 112 | //remove unused screenshots from lhr to save space 113 | endpoints.forEach(endpoint => { 114 | lhr[endpoint]['audits']['screenshot-thumbnails']['details'] = null; 115 | lhr[endpoint]['audits']['final-screenshot']['details']['data'] = null; 116 | lhr[endpoint]['audits']['full-page-screenshot']['details'] = null; 117 | //add git details to the lighthouse report under key 'git' 118 | lhr[endpoint]['git'] = getCommitDetails(); 119 | 120 | if (!(endpoint in data)) data[endpoint] = [lhr[endpoint]] 121 | else { 122 | data[endpoint].push(lhr[endpoint]) 123 | } 124 | }) 125 | //resultsOutput is a JS expression that assigns window.results to the data array. 126 | const resultsOutput = 'window.results = ' + JSON.stringify(data); 127 | //outputDir is the astro project two levels up + folder 'astrospeed'. (cwd is inside node_modules/astrospeed) 128 | const outputDir = path.resolve(path.join(__dirname, '../../astrospeed/')); 129 | 130 | if (!fs.existsSync(outputDir)) { 131 | //if the outputDir doesn't exist, create it 132 | fs.mkdirSync(outputDir) 133 | } 134 | //write resultsOutput to results.js 135 | fs.writeFileSync('./astrospeed/results.js', resultsOutput); 136 | 137 | if (!fs.existsSync(path.join(outputDir, 'bundle.js')) || !fs.existsSync(path.join(outputDir, 'index.html'))) { 138 | //if bundle.js or index.html doesn't exist in the astro project folder under 'astrospeed' dir, copy them over from node_modules/astrospeed 139 | fs.copyFileSync(path.resolve(path.join(__dirname, './astrospeed/index.html')), path.join(outputDir, 'index.html')); 140 | fs.copyFileSync(path.resolve(path.join(__dirname, './astrospeed/bundle.js')), path.join(outputDir, 'bundle.js')); 141 | } 142 | 143 | //write to user's terminal the path of the astrospeed report. 144 | console.log('Astrospeed report available at', path.resolve(__dirname, '../../astrospeed/index.html')) 145 | } 146 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { useState } from 'react'; 3 | const lhr = (window as any).results; 4 | import ListContainer from "./components/ListContainer/ListContainer.jsx" 5 | import LineChart from "./components/LineChart.jsx"; 6 | import DialChart from "./components/DialChart.jsx"; 7 | import Card from "./components/Card.jsx" 8 | 9 | // interface Props { 10 | // currentMetric: string 11 | // } 12 | 13 | class App extends React.Component<{}, {currentMetric: string, currentEndpoint: string}> { 14 | constructor(props: any) { 15 | super(props); 16 | this.state = { 17 | currentMetric: 'performance', 18 | currentEndpoint: Object.keys(lhr)[0] 19 | }; 20 | } 21 | render() { 22 | // const [currentMetric, setcurrentMetric] = useState('Performance'); 23 | 24 | const reportTime = lhr[this.state.currentEndpoint][lhr[this.state.currentEndpoint].length - 1].git.time; 25 | const currPerf = lhr[this.state.currentEndpoint][lhr[this.state.currentEndpoint].length-1].categories.performance.score * 100 26 | const currSeo = lhr[this.state.currentEndpoint][lhr[this.state.currentEndpoint].length-1].categories.seo.score * 100; 27 | const currBP = lhr[this.state.currentEndpoint][lhr[this.state.currentEndpoint].length-1].categories['best-practices'].score * 100; 28 | const currAcc = lhr[this.state.currentEndpoint][lhr[this.state.currentEndpoint].length-1].categories.accessibility.score * 100; 29 | 30 | const divStyle = { 31 | display:'flex' 32 | } 33 | 34 | const availableEndpoints = Object.keys(lhr).map(endpoint =>
  • this.setState({currentEndpoint: endpoint})}>{endpoint}
  • ) 35 | 36 | return ( 37 | <> 38 | 39 |
    40 | {/* */} 41 | 63 | 64 | {/* */} 65 |
    66 |
    67 |

    Current Commit Metrics

    68 | {/* {reportTime} */} 69 |
    70 | 71 |
    72 | this.setState({currentMetric: 'performance'})} 76 | data={currPerf} 77 | /> 78 | this.setState({currentMetric: 'seo'})} 82 | // onClick={() => this.setState({currentMetric: 'seo'})} 83 | data={currSeo} 84 | /> 85 | this.setState({currentMetric: 'best-practices'})} 89 | data={currBP} 90 | /> 91 | this.setState({currentMetric: 'accessibility'})} 95 | data={currAcc} 96 | /> 97 |
    98 | 99 |
    100 |

    History & Diagnostics

    101 | {/*

    */} 102 |
    103 | 104 |
    105 |
    106 |

    Commit History

    107 |
    108 | 109 |
    110 |
    111 | 112 |
    113 |

    Current Commit Details

    114 |
    115 | 116 |
    117 |
    118 | 119 |
    120 | Metrics via Google Lighthouse. Report generated at: {reportTime}
    121 | 122 |
    123 | 124 |
    125 | 126 | 127 | ) 128 | 129 | // // const {fetchTime} = lhr[lhr.length-1]; 130 | // // const reportDate = new Date(fetchTime.slice(0, -1)); 131 | // const reportTime = git_commits[git_commits.length - 1][1] 132 | // return ( 133 | // <> 134 | // {/*

    135 | // Hello {name} 136 | //

    */} 137 | // Report generated at: {reportTime}
    138 | // Performance: {lhr[lhr.length-1].categories.performance.score}
    139 | // Search Engine Optimization (SEO): {lhr[lhr.length-1].categories.seo.score}
    140 | 141 | // 142 | // ); 143 | } 144 | } 145 | 146 | export default App; 147 | -------------------------------------------------------------------------------- /src/assets/moreArrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DialChart from "./DialChart"; 3 | 4 | export default function Card (props) { 5 | const { name, data, icon, diagnosticsState } = props; 6 | return ( 7 |
    8 |
    9 |

    {name}

    10 | {icon} 11 |
    12 | {/* */} 13 | { data } 14 |
    15 | ) 16 | } -------------------------------------------------------------------------------- /src/components/DialChart.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactApexChart from "react-apexcharts"; 3 | 4 | 5 | class DialChart extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | series: [props.data], 11 | options: { 12 | chart: { 13 | height: 350, 14 | type: 'radialBar', 15 | offsetY: -10 16 | }, 17 | plotOptions: { 18 | radialBar: { 19 | startAngle: -135, 20 | endAngle: 135, 21 | dataLabels: { 22 | name: { 23 | fontSize: '6px', 24 | color: '#000000', 25 | offsetY: 120 26 | }, 27 | value: { 28 | offsetY: -10, 29 | fontSize: '15px', 30 | color: undefined, 31 | formatter: function (val) { 32 | return val + "%"; 33 | } 34 | } 35 | } 36 | } 37 | }, 38 | fill: { 39 | colors: ['#3700A4', '#000000'], 40 | type: 'gradient', 41 | gradient: { 42 | shade: 'dark', 43 | shadeIntensity: 0.15, 44 | inverseColors: false, 45 | opacityFrom: 1, 46 | opacityTo: 1, 47 | stops: [0, 50, 65, 91] 48 | }, 49 | }, 50 | stroke: { 51 | dashArray: 4 52 | }, 53 | labels: [' '], 54 | // labels: [`${props.name}`], 55 | }, 56 | 57 | 58 | }; 59 | } 60 | 61 | render() { 62 | const divStyle = { 63 | width: '50%', 64 | } 65 | return ( 66 |
    67 | 68 |
    69 | ); 70 | } 71 | } 72 | 73 | export default DialChart; 74 | 75 | -------------------------------------------------------------------------------- /src/components/LineChart.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactApexChart from "react-apexcharts"; 3 | import { lineChartData, lineChartOptions } from "./chartdata"; 4 | const lhr = window.results; 5 | 6 | 7 | class LineChart extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | chartData: [], 13 | chartOptions: {}, 14 | currentEndpoint: '' 15 | }; 16 | } 17 | 18 | 19 | componentDidMount() { 20 | const currentChartData = []; 21 | for (let i = 0; i < 4; i++) { 22 | currentChartData.push(Object.assign({}, lineChartData[i], {data: lineChartData[i]['data'][this.props.currentEndpoint]})) 23 | } 24 | // console.log(lhr[this.props.currentEndpoint][0]) 25 | const currentEndpoint = this.props.currentEndpoint; 26 | lineChartOptions.tooltip.x = { 27 | formatter: function(val) { 28 | return `Commit #${val}
    ${lhr[currentEndpoint][Number(val) - 1].git.time}
    ${lhr[currentEndpoint][Number(val) - 1].git.msg}` 29 | } 30 | } 31 | this.setState({ 32 | chartData: currentChartData, 33 | chartOptions: lineChartOptions, 34 | currentEndpoint: currentEndpoint 35 | }); 36 | } 37 | 38 | componentDidUpdate() { 39 | if (this.state.currentEndpoint != this.props.currentEndpoint) { 40 | const currentEndpoint = this.props.currentEndpoint; 41 | const currentChartData = []; 42 | for (let i = 0; i < 4; i++) { 43 | currentChartData.push(Object.assign({}, lineChartData[i], {data: lineChartData[i]['data'][this.props.currentEndpoint]})) 44 | } 45 | 46 | lineChartOptions.tooltip.x = { 47 | formatter: function(val) { 48 | return `Commit #${val}
    ${lhr[currentEndpoint][Number(val) - 1].git.time}
    ${lhr[currentEndpoint][Number(val) - 1].git.msg}` 49 | } 50 | } 51 | 52 | this.setState({ 53 | chartData: currentChartData, 54 | chartOptions: lineChartOptions, 55 | currentEndpoint: currentEndpoint 56 | }); 57 | } 58 | // } 59 | 60 | } 61 | 62 | render() { 63 | const divStyle = { 64 | height: '50vh', 65 | minHeight: '400px', 66 | } 67 | // console.log(this.state.chartData[this.props.currentEndpoint]) 68 | return ( 69 |
    70 | 77 |
    78 | ); 79 | } 80 | } 81 | 82 | export default LineChart; 83 | -------------------------------------------------------------------------------- /src/components/ListContainer/DiagnosticsItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styles from "./styles.module.css"; 3 | 4 | const DiagnosticsItem = ({ data }) => { 5 | const [open, setOpen] = useState(false); 6 | 7 | const handleClick = () => { 8 | setOpen(!open); 9 | }; 10 | 11 | // console.log('diag item', data.link) 12 | return ( 13 |
    0.96 ? styles.green : styles.red 17 | }`} 18 | > 19 |
    20 |
    21 |
    {data.title}
    22 |
    {data.displayValue || String(data.score * 100) + '%' }
    23 |
    24 | 25 | {open && ( 26 |
    27 |
    {data.description}{data.link && (Learn More)}
    28 |
    29 | )} 30 |
    31 | ); 32 | }; 33 | 34 | export default DiagnosticsItem; 35 | -------------------------------------------------------------------------------- /src/components/ListContainer/ListContainer.jsx: -------------------------------------------------------------------------------- 1 | //import styles from "./styles.module.css"; 2 | import React, { useState, useEffect } from "react"; 3 | // import OppurtunityItem from "./OppurtunityItem.jsx"; 4 | const lhr = window.results; 5 | import DiagnosticsItem from "./DiagnosticsItem"; 6 | import styles from "./styles.module.css"; 7 | 8 | const ListContainer = (props) => { 9 | console.log('rendering list container.', props) 10 | /* 11 | Performance breakdown 12 | Audit Weight 13 | First Contentful Paint 10% 14 | Speed Index 10% 15 | Largest Contentful Paint 25% 16 | Time to Interactive 10% 17 | Total Blocking Time 30% 18 | Cumulative Layout Shift 15% 19 | */ 20 | const diagnostics = {}; 21 | for (const category of Object.keys(lhr[props.currentEndpoint][lhr[props.currentEndpoint].length - 1]['categories'])) { 22 | const refs = lhr[props.currentEndpoint][lhr[props.currentEndpoint].length - 1]['categories'][category]['auditRefs']; 23 | diagnostics[category] = refs.map(ref => { 24 | const fullAudit = Object.assign({}, lhr[props.currentEndpoint][lhr[props.currentEndpoint].length - 1]['audits'][ref.id]); 25 | if (fullAudit.score == null) return null 26 | // console.log(fullAudit.description) 27 | fullAudit.link = fullAudit.description.match(/https:\/\/web.dev.*\//); 28 | fullAudit.description = fullAudit.description.replace(/\[Learn.*/, '') 29 | return fullAudit 30 | }).filter(ref => ref).sort((a, z) => a.score - z.score); 31 | }; 32 | 33 | return ( 34 | 35 |
    36 | {diagnostics[props.currentMetric].map((data) => ( 37 | 38 | ))} 39 |
    40 | 41 | ); 42 | }; 43 | 44 | export default ListContainer; 45 | 46 | //line 91 47 | // {opportData.map((data) => ( 48 | // 49 | // ))} 50 | -------------------------------------------------------------------------------- /src/components/ListContainer/styles.module.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .navbar{ 4 | width: 100%; 5 | height: 70px; 6 | background-color: rgb(242, 169, 160); 7 | display: flex; 8 | align-items: center; 9 | justify-content: space-between; 10 | } 11 | 12 | .navbar h1{ 13 | color: white; 14 | font-size: 25px; 15 | margin-left: 20px; 16 | } 17 | 18 | .form_container{ 19 | background-color:rgb(128, 153, 237); 20 | width: 100%; 21 | height: 300px; 22 | border-radius: 5px; 23 | } 24 | 25 | .input{ 26 | outline: none; 27 | border: solid; 28 | width: auto; 29 | padding: 15px; 30 | border-radius: 10px; 31 | background-color: rgb(208, 199, 233); 32 | margin: 5px 0; 33 | font-size: 14px; 34 | } 35 | 36 | .inputInsideContainer{ 37 | outline: none; 38 | border: solid; 39 | /* width: 92%; */ 40 | padding: 15px; 41 | border-radius: 10px; 42 | background-color: rgb(208, 199, 233); 43 | margin: 5px 0; 44 | font-size: 14px; 45 | cursor: pointer; 46 | } 47 | 48 | .inputInsideContainer:hover{ 49 | /* background-color: rgb(189, 176, 223); */ 50 | opacity: 50%; 51 | /* background-color: #4f35a1; */ 52 | } 53 | 54 | .addresses{ 55 | background-color: lightgray; 56 | border: 2px double black; 57 | margin-bottom: 20px; 58 | padding: 20px; 59 | 60 | } 61 | 62 | 63 | .white_btn{ 64 | border: none; 65 | outline: none; 66 | padding: 12px 0; 67 | background-color: white; 68 | border-radius: 20px; 69 | width: 120px; 70 | font-weight: bold; 71 | font-size: 14px; 72 | cursor: pointer; 73 | margin-right: 20px; 74 | } 75 | 76 | .yours{ 77 | margin-left: 30%; 78 | margin-top: 5%; 79 | 80 | } 81 | 82 | .green_btn{ 83 | border: none; 84 | outline: none; 85 | padding: 12px 0; 86 | background-color: white; 87 | border-radius: 20px; 88 | width: 180px; 89 | font-weight: bold; 90 | font-size: 14px; 91 | margin: 10px 5px 5px 0px; 92 | cursor: pointer; 93 | } 94 | .span{ 95 | font-weight: bold; 96 | } 97 | 98 | .diagnostics { 99 | color: white; 100 | } 101 | 102 | .green { 103 | background-color: #367952; 104 | } 105 | 106 | .red { 107 | background-color: #cc3c43; 108 | } 109 | 110 | .dataTitle { 111 | font-weight: bold; 112 | font-size: 16px; 113 | font-family: system-ui; 114 | color: white; 115 | } 116 | 117 | .description { 118 | font-size: 12px; 119 | font-family: system-ui; 120 | color: white; 121 | } 122 | 123 | .mainItem { 124 | display: flex; 125 | justify-content: space-between; 126 | padding-right: 30px; 127 | color: white; 128 | } 129 | 130 | .bottomData { 131 | margin-top: 10px; 132 | } 133 | 134 | .arrow { 135 | cursor: pointer; 136 | display: flex; 137 | justify-content: end; 138 | color: white; 139 | } 140 | 141 | .arrow::before{ 142 | border-style: solid; 143 | border-width: 0.30em 0.30em 0 0; 144 | content: ''; 145 | display: inline-block; 146 | height: 0.6em; 147 | left: 0.15em; 148 | top: 0.15em; 149 | vertical-align: top; 150 | width: 0.6em; 151 | transform: rotate(135deg); 152 | } 153 | 154 | .blinking{ 155 | animation: blinker 1s infinite; 156 | } 157 | @keyframes blinker { 158 | from { opacity: 1.0; } 159 | 50% { opacity: 0.5; } 160 | to { opacity: 1.0; } 161 | } -------------------------------------------------------------------------------- /src/components/chartdata.js: -------------------------------------------------------------------------------- 1 | const lhr = window.results; 2 | 3 | const perfScores = {}, seoScores = {}, commitNum = {}, bestPracScores = {}, a11yScores = {}; 4 | const endpoints = Object.keys(lhr); 5 | 6 | // { 7 | // 'index.html': [{'performance': 99}] 8 | // } 9 | //iterate through each endpoint 10 | for (let k = 0; k < endpoints.length; k++) { 11 | //iterate through each run in that endpoint 12 | for (let i = 0; i < lhr[endpoints[k]].length; i++) { 13 | if (!(endpoints[k] in commitNum)) commitNum[endpoints[k]] = 0 14 | commitNum[endpoints[k]]++; 15 | if (!(endpoints[k] in perfScores)) perfScores[endpoints[k]] = [] 16 | perfScores[endpoints[k]].push(lhr[endpoints[k]][i].categories.performance.score * 100); 17 | if (!(endpoints[k] in seoScores)) seoScores[endpoints[k]] = [] 18 | seoScores[endpoints[k]].push(lhr[endpoints[k]][i].categories.seo.score * 100); 19 | if (!(endpoints[k] in bestPracScores)) bestPracScores[endpoints[k]] = [] 20 | bestPracScores[endpoints[k]].push(lhr[endpoints[k]][i].categories['best-practices'].score * 100); 21 | if (!(endpoints[k] in a11yScores)) a11yScores[endpoints[k]] = [] 22 | a11yScores[endpoints[k]].push(lhr[endpoints[k]][i].categories.accessibility.score * 100); 23 | } 24 | } 25 | // console.log(perfScores) 26 | 27 | 28 | export const lineChartData = [ 29 | { 30 | name: "Performance", 31 | data: perfScores, 32 | }, 33 | { 34 | name: "Search Engine Optimization", 35 | data: seoScores, 36 | }, 37 | { 38 | name: "Best Practices", 39 | data: bestPracScores, 40 | }, 41 | { 42 | name: "Accessibility", 43 | data: a11yScores, 44 | }, 45 | ]; 46 | export const lineChartOptions = { 47 | chart: { 48 | toolbar: { 49 | show: false, 50 | }, 51 | }, 52 | tooltip: { 53 | // followCursor: false, 54 | theme: "dark", 55 | intersect: false, 56 | x: { 57 | // formatter: function(val) {return `Commit #${val}
    ${lhr[Number(val) - 1].git.time}
    ${lhr[Number(val) - 1].git.msg}`} 58 | }, 59 | y: { 60 | formatter: function(val) {return `${val}%`} 61 | }, 62 | 63 | }, 64 | dataLabels: { 65 | enabled: true, 66 | // style: {colors: [ "#E4E684", "#3700A4"]}//['#FFFFFF', '#000000']} 67 | }, 68 | stroke: { 69 | curve: "straight", 70 | }, 71 | xaxis: { 72 | type: "numeric", 73 | categories: commitNum, 74 | labels: { 75 | style: { 76 | colors: "#c8cfca", 77 | fontSize: "12px", 78 | }, 79 | }, 80 | title: {text:"Commit #"}, 81 | tooltip: { 82 | enabled: false 83 | } 84 | }, 85 | yaxis: { 86 | labels: { 87 | style: { 88 | colors: "#c8cfca", 89 | fontSize: "12px", 90 | }, 91 | }, 92 | max: 100, 93 | min: 0, 94 | tickAmount: 4 95 | }, 96 | legend: { 97 | show: true, 98 | }, 99 | grid: { 100 | strokeDashArray: 5, 101 | }, 102 | fill: { 103 | type: "gradient", 104 | gradient: { 105 | shade: "light", 106 | type: "vertical", 107 | shadeIntensity: 0.5, 108 | gradientToColors: undefined, // optional, if not defined - uses the shades of same color in series 109 | inverseColors: true, 110 | opacityFrom: 0.8, 111 | opacityTo: 0, 112 | stops: [], 113 | }, 114 | colors: ["#246dec", "#f5b74f", "#367952", "#cc3c43"], 115 | }, 116 | colors: ["#246dec", "#f5b74f", "#367952", "#cc3c43"], 117 | }; 118 | 119 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | import App from './App'; 5 | import "./styles.css"; 6 | 7 | var mountNode = document.getElementById("app"); 8 | ReactDOM.render(, mountNode); 9 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@100;300;500&display=swap'); 2 | 3 | html { 4 | font-family: 'Montserrat', sans-serif; 5 | } 6 | 7 | h1 { 8 | color: white; 9 | background-color: black; 10 | } 11 | 12 | body { 13 | margin: 0px 14 | } 15 | 16 | .text-primary { 17 | color: #666666; 18 | } 19 | 20 | .text-blue { 21 | color: #246dec; 22 | } 23 | 24 | .text-red { 25 | color: #367952; 26 | } 27 | 28 | .text-green { 29 | color: #367952; 30 | } 31 | 32 | .text-orange { 33 | color: #f5b74f; 34 | } 35 | 36 | .font-weight-bold { 37 | font-weight: 600; 38 | } 39 | 40 | .grid-container { 41 | display: grid; 42 | /* how many columns, width values. nav bar fixed to 260px */ 43 | grid-template-columns: 260px 1fr 1fr 1fr; 44 | grid-template-rows: 0fr 3fr; 45 | grid-template-areas: 46 | "sidebar header header header" 47 | "sidebar main main main"; 48 | /* 100 of the viewport area */ 49 | height: 100vh; 50 | } 51 | 52 | /* -------- HEADER ----------- */ 53 | 54 | .header { 55 | grid-area: header; 56 | height:70px; 57 | background-color: #FFFFFF; 58 | display: flex; 59 | align-items: center; 60 | justify-content: space-between; 61 | padding: 0 30px 0 30px; 62 | box-shadow: 0 6px 7px -4px rgba(0, 0, 0, 0.2); 63 | } 64 | 65 | /* responsive view of the menu icon */ 66 | .menu-icon { 67 | display: none; 68 | } 69 | 70 | /* -------- SIDE NAV ----------- */ 71 | 72 | #sidebar { 73 | grid-area: sidebar; 74 | height:100%; 75 | background-color: #21232d; 76 | color: #9799ab; 77 | overflow-y: auto; 78 | transition: all 0.5s; 79 | -webkit-transition: all 0.5s; 80 | } 81 | 82 | .sidebar-title { 83 | display: flex; 84 | justify-content: space-between; 85 | align-items: center; 86 | padding: 20px 20px 20px 20px; 87 | margin-bottom: 30px; 88 | } 89 | 90 | /* CSS child selector .parent-el > all child spans */ 91 | /* want this to be responsive hide*/ 92 | 93 | .sidebar-title > span { 94 | display: none; 95 | } 96 | 97 | .sidebar-brand { 98 | margin-top: 15px; 99 | font-size: 20px; 100 | font-weight: 700; 101 | } 102 | 103 | .sidebar-list { 104 | padding: 0px; 105 | margin-top: 15px; 106 | list-style-type: none; 107 | } 108 | 109 | .sidebar-list-item { 110 | padding: 20px 20px 20px 20px; 111 | } 112 | .endpoint { 113 | padding: 10px 10px 10px 50px; 114 | background-color: #333747 115 | } 116 | .endpoint:hover { 117 | background-color: rgba(255, 255, 255, 0.2); 118 | cursor: pointer; 119 | } 120 | 121 | /* use CSS hover selector */ 122 | .sidebar-list-item:hover { 123 | background-color: rgba(255, 255, 255, 0.2); 124 | cursor: pointer; 125 | } 126 | 127 | /* adds important with !, it ovride previous rules on this el */ 128 | /* 129 | the z-index of the ApexCharts is 11 130 | we want the z-index of the sidebar higher so that 131 | the charts are not showing over the sidebar 132 | on small screens 133 | */ 134 | .sidebar-responsive { 135 | display: inline !important; 136 | position: absolute; 137 | z-index: 12 !important 138 | } 139 | 140 | /* -------- MAIN ----------- */ 141 | 142 | .main-container { 143 | grid-area: main; 144 | /* overflow-y: auto; */ 145 | padding: 0px 20px 20px 20px; 146 | } 147 | 148 | .main-title { 149 | display: flex; 150 | justify-content: space-between; 151 | } 152 | 153 | .main-title > .timestamp { 154 | flex-direction: row-reverse; 155 | } 156 | 157 | .main-title > p { 158 | font-size: 20px; 159 | } 160 | 161 | /* formats the top cards to be beside each other 4 */ 162 | .main-cards { 163 | display: grid; 164 | grid-template-columns: 1fr 1fr 1fr 1fr; 165 | gap: 20px; 166 | /* margin: 20px 0; */ 167 | } 168 | 169 | /* format cards: color, bg */ 170 | .card { 171 | display: flex; 172 | flex-direction: column; 173 | justify-content: space-around; 174 | padding: 20px; 175 | background-color: #ffffff; 176 | box-sizing: border-box; 177 | border: solid #d2d2d2; 178 | border-radius: 5px; 179 | box-shadow: 0 6px 7px -4px rbga(0, 0, 0, 0.2); 180 | } 181 | 182 | .card:hover { 183 | display: flex; 184 | flex-direction: column; 185 | justify-content: space-around; 186 | padding: 20px; 187 | background-color: #ffffff; 188 | box-sizing: border-box; 189 | border: solid #d2d2d2; 190 | border-radius: 5px; 191 | box-shadow: 0 6px 7px -4px rbga(0, 0, 0, 0.2); 192 | cursor: pointer; 193 | } 194 | 195 | /* use first child selector selects the first card */ 196 | .card:first-child { 197 | border-left: 7px solid #246dec; 198 | } 199 | 200 | /* select the 2nd card of the 4, the second within the parent el */ 201 | .card:nth-child(2) { 202 | border-left: 7px solid #f5b74f; 203 | } 204 | 205 | .card:nth-child(3) { 206 | border-left: 7px solid #367952; 207 | } 208 | 209 | .card:nth-child(4) { 210 | border-left: 7px solid #cc3c43; 211 | } 212 | 213 | .card > span { 214 | font-size: 20px; 215 | font-weight: 600; 216 | } 217 | 218 | /* card TITLE w/ ICON beside it */ 219 | .card-inner { 220 | display: flex; 221 | align-items: center; 222 | justify-content: space-between; 223 | } 224 | 225 | .card-inner > p { 226 | font-size: 18px; 227 | } 228 | 229 | .card-inner > span { 230 | font-size: 35px; 231 | } 232 | 233 | /* -------- MAIN: CHARTS ----------- */ 234 | 235 | .charts { 236 | display: grid; 237 | grid-template-columns: 1fr 1fr; 238 | gap: 20px; 239 | } 240 | 241 | .charts-card { 242 | background-color: #ffffff; 243 | margin-bottom: 20px; 244 | padding: 25px; 245 | box-sizing: border-box; 246 | -webkit-column-break-inside: avoid; 247 | border: 1px solid #d2d2d3; 248 | border-radius: 5px; 249 | box-shadow: 0 6px 7px -4px rbga(0, 0, 0, 0.2); 250 | max-height: 550px; 251 | } 252 | 253 | .recommendations { 254 | background-color: #ffffff; 255 | margin-bottom: 20px; 256 | padding: 25px; 257 | box-sizing: border-box; 258 | -webkit-column-break-inside: avoid; 259 | border: 1px solid #d2d2d3; 260 | border-radius: 5px; 261 | box-shadow: 0 6px 7px -4px rbga(0, 0, 0, 0.2); 262 | overflow-y: scroll; 263 | max-height: 550px; 264 | } 265 | 266 | .chart-title { 267 | display: flex; 268 | align-items: center; 269 | justify-content: center; 270 | font-size: 22px; 271 | font-weight: 600; 272 | margin-top: 0px; 273 | } 274 | 275 | 276 | /* ---------- SCROLLBARS ---------- */ 277 | 278 | ::-webkit-scrollbar { 279 | width: 5px; 280 | height: 6px; 281 | } 282 | 283 | ::-webkit-scrollbar-track { 284 | box-shadow: inset 0 0 5px #a5aaad; 285 | border-radius: 10px; 286 | } 287 | 288 | ::-webkit-scrollbar-thumb { 289 | background-color: #4f35a1; 290 | border-radius: 10px; 291 | } 292 | 293 | ::-webkit-scrollbar-thumb:hover { 294 | background-color: #a5aaad; 295 | } 296 | 297 | 298 | /* ---------- MEDIA QUERIES ---------- */ 299 | 300 | 301 | /* Medium <= 992px */ 302 | @media screen and (max-width: 992px) { 303 | .grid-container { 304 | grid-template-columns: 1fr; 305 | grid-template-rows: 0.2fr 3fr; 306 | grid-template-areas: 307 | "header" 308 | "main"; 309 | } 310 | 311 | #sidebar { 312 | display: none; 313 | } 314 | 315 | .menu-icon { 316 | display: inline; 317 | } 318 | 319 | .sidebar-title > span { 320 | display: inline; 321 | } 322 | } 323 | 324 | /* Small <= 768px */ 325 | @media screen and (max-width: 768px) { 326 | .main-cards { 327 | grid-template-columns: 1fr; 328 | gap: 10px; 329 | margin-bottom: 0; 330 | } 331 | 332 | .charts { 333 | grid-template-columns: 1fr; 334 | margin-top: 30px; 335 | } 336 | } 337 | 338 | /* Extra Small <= 576px */ 339 | @media screen and (max-width: 576px) { 340 | .header-left { 341 | display: none; 342 | } 343 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "strict": true, 6 | "noImplicitReturns": true, 7 | "noImplicitAny": true, 8 | "module": "es6", 9 | "moduleResolution": "node", 10 | "target": "es5", 11 | "allowJs": true, 12 | "jsx": "react", 13 | "resolveJsonModule": true, 14 | "esModuleInterop": true, 15 | }, 16 | "include": [ 17 | "./src/**/*" 18 | ] 19 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | 5 | const config = { 6 | entry: "./src/index.tsx", 7 | output: { 8 | path: path.resolve(__dirname, './astrospeed'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.(js|jsx)$/, 15 | use: "babel-loader", 16 | exclude: [ 17 | /node_modules\/(?!astrospeed).*/ 18 | ] 19 | }, 20 | { 21 | test: /\.css$/, 22 | use: ["style-loader", "css-loader"], 23 | }, 24 | { 25 | test: /\.ts(x)?$/, 26 | loader: "ts-loader", 27 | exclude: [ 28 | /node_modules\/(?!astrospeed).*/ 29 | ] 30 | }, 31 | { test: /\.svg$/, use: ["@svgr/webpack", "url-loader"] }, 32 | ], 33 | }, 34 | plugins: [ 35 | new HtmlWebpackPlugin({ 36 | templateContent: ({ htmlWebpackPlugin }) => 37 | 'astroSpeed Report
    ', 38 | filename: "index.html", 39 | chunks: 'bundle' 40 | }), 41 | ], 42 | resolve: { 43 | extensions: [".tsx", ".ts", ".js", ".jsx"], 44 | } 45 | }; 46 | 47 | module.exports = config; 48 | -------------------------------------------------------------------------------- /website/.npmrc: -------------------------------------------------------------------------------- 1 | # Expose Astro dependencies for `pnpm` users 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /website/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import tailwind from '@astrojs/tailwind'; 3 | 4 | // https://astro.build/config 5 | export default defineConfig({ 6 | integrations: [tailwind()], 7 | }); 8 | -------------------------------------------------------------------------------- /website/demo/index.html: -------------------------------------------------------------------------------- 1 | astroSpeed Report
    -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astrospeed", 3 | "type": "commonjs", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro build", 10 | "preview": "astro preview", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "@astrojs/tailwind": "^2.0.1", 15 | "astro": "^1.2.8", 16 | "express": "^4.18.1", 17 | "postcss": "^8.4.17", 18 | "tailwindcss": "^3.1.8" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /website/public/Audrey-Park.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/Audrey-Park.webp -------------------------------------------------------------------------------- /website/public/How-it-works-chart.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/How-it-works-chart.webp -------------------------------------------------------------------------------- /website/public/Luke-McInerney.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/Luke-McInerney.webp -------------------------------------------------------------------------------- /website/public/Marco-Gonzalez.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/Marco-Gonzalez.webp -------------------------------------------------------------------------------- /website/public/Rami-Abdelghafar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/Rami-Abdelghafar.webp -------------------------------------------------------------------------------- /website/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /website/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /website/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/apple-touch-icon.png -------------------------------------------------------------------------------- /website/public/astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/astronaut.png -------------------------------------------------------------------------------- /website/public/colored-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/colored-logo.png -------------------------------------------------------------------------------- /website/public/crater-footer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/crater-footer.webp -------------------------------------------------------------------------------- /website/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/favicon-16x16.png -------------------------------------------------------------------------------- /website/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/favicon-32x32.png -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /website/public/moonTelescope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/moonTelescope.png -------------------------------------------------------------------------------- /website/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /website/public/space-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/space-bg.png -------------------------------------------------------------------------------- /website/public/tether.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/tether.png -------------------------------------------------------------------------------- /website/public/white-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/astrospeed/be62f71f469b34cf4e37df399224c0ffc9583591/website/public/white-logo.png -------------------------------------------------------------------------------- /website/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const PORT = 80; 4 | 5 | app.use('/demo', express.static('demo')) 6 | app.use(express.static('dist')); 7 | 8 | 9 | app.listen(PORT); -------------------------------------------------------------------------------- /website/src/components/Astronaut.astro: -------------------------------------------------------------------------------- 1 | --- 2 | //Component Script Block 3 | 4 | 5 | --- 6 | 7 | 13 |
    14 | 16 | We believe that with a great developer experience, comes a great user experience. Combining the blazingly fast runtime of AstroJS and industry-standard metrics of Google Lighthouse, we created an industry first, astroSpeed. 17 | 18 |
    -------------------------------------------------------------------------------- /website/src/components/Button.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const {color} = Astro.props; 3 | let tailClass; 4 | if (color === 'stone') tailClass = `bg-stone-100 hover:bg-stone-300 transition ease-in-out delay-50 hover:-translate-y-1 hover:scale-110 w-48 h-10 py-2 px-2 m-1 font-semibold rounded-lg`; 5 | if (color === 'yellow') tailClass = `bg-yellow-200 hover:bg-yellow-400 transition ease-in-out delay-50 hover:-translate-y-1 hover:scale-110 w-48 h-10 py-2 px-2 m-1 font-semibold rounded-lg`; 6 | --- 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/components/Comparisons.astro: -------------------------------------------------------------------------------- 1 | --- 2 | //Component Script Block 3 | 4 | 5 | --- 6 | 7 | 8 | 18 | 19 |
    20 |

    COMPARE YOUR CODE PERFORMANCE

    21 | 22 | Without 23 | vs. 24 | astroSpeed 25 | 26 | X 27 | Only see metrics after your site is published 28 | 29 | On-demand performance & SEO metrics 30 | 31 | X 32 | Forced to wait 33 | 34 | Pivot faster with early insight 35 | 36 | X 37 | Low visibility to where your code needs improvement 38 | 39 | Pinpoint least optimal code as you build 40 | 41 | X 42 | Time lost scanning pages of code 43 | 44 | More control & visibility 45 |
    46 | 47 | -------------------------------------------------------------------------------- /website/src/components/ContactCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | //Component Script Block 3 | const {name, github, linkedin, picture} = Astro.props; 4 | 5 | --- 6 | 7 | 81 | 82 |
    83 | Logo 84 |
    85 |
    {name}
    86 | Software Engineer 87 | 91 |
    92 |
    -------------------------------------------------------------------------------- /website/src/components/ContactUs.astro: -------------------------------------------------------------------------------- 1 | --- 2 | //Component Script Block 3 | import ContactCard from "./ContactCard.astro"; 4 | 5 | --- 6 | 7 | 8 | 17 | 18 |
    19 | 25 | 31 | 37 | 43 | 44 |
    -------------------------------------------------------------------------------- /website/src/components/NavBar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Component Imports 3 | import Button from './Button.astro'; 4 | --- 5 | 6 | -------------------------------------------------------------------------------- /website/src/components/RocketChart.astro: -------------------------------------------------------------------------------- 1 | --- 2 | //Component Script Block 3 | 4 | 5 | --- 6 | 7 | 8 |
    9 | 10 |
    -------------------------------------------------------------------------------- /website/src/components/SplashPoster.astro: -------------------------------------------------------------------------------- 1 | --- 2 | //Component Script Block 3 | 4 | 5 | --- 6 | 7 | 109 | 110 | 111 |
    112 | 113 |

    astroSpeed

    114 |

    astro projects deserve performance metrics, on-demand

    115 | 116 |
    117 | -------------------------------------------------------------------------------- /website/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /website/src/layouts/main.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import NavBar from '../components/NavBar.astro'; 3 | --- 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | astroSpeed 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /website/src/pages/getstarted.astro: -------------------------------------------------------------------------------- 1 | --- 2 | //Component Script Block 3 | import Main from '../layouts/main.astro'; 4 | 5 | --- 6 | 7 |
    8 | Get Started Content Goes Here 9 |
    -------------------------------------------------------------------------------- /website/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Component Imports 3 | import Main from '../layouts/main.astro'; 4 | import SplashPoster from '../components/SplashPoster.astro'; 5 | import Comparisons from '../components/Comparisons.astro'; 6 | import ContactUs from '../components/ContactUs.astro'; 7 | import RocketChart from '../components/RocketChart.astro'; 8 | import Astronaut from '../components/Astronaut.astro'; 9 | 10 | // Full Astro Component Syntax: 11 | // https://docs.astro.build/core-concepts/astro-components/ 12 | --- 13 | 14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 |
    -------------------------------------------------------------------------------- /website/src/pages/login.astro: -------------------------------------------------------------------------------- 1 | --- 2 | //Component Script Block 3 | import Main from '../layouts/main.astro'; 4 | 5 | --- 6 | 7 |
    8 | Coming soon... 9 |
    -------------------------------------------------------------------------------- /website/src/pages/watchdemo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | //Component Script Block 3 | import Main from '../layouts/main.astro'; 4 | 5 | --- 6 | 7 |
    8 | 9 |
    -------------------------------------------------------------------------------- /website/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], 3 | plugins: [], 4 | }; 5 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/base" 3 | } 4 | --------------------------------------------------------------------------------