├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── FEATURE_REQUEST.yml │ └── BUG_REPORT.yml ├── pull_request_template.md └── workflows │ └── deploy.yml ├── src ├── vite-env.d.ts ├── css │ ├── App.css │ ├── Section.css │ ├── WorkflowAnalysis.css │ ├── GuidePage.css │ ├── Dashboard.css │ ├── Guide.css │ ├── Recommendation.css │ ├── Sidebar.css │ ├── Link.css │ ├── fonts.css │ ├── NotFound.css │ ├── fade-in.css │ ├── Guides.css │ └── Header.css ├── main.tsx ├── components │ ├── pages │ │ ├── NotFound.tsx │ │ ├── Guides │ │ │ ├── GuidePage.tsx │ │ │ ├── GuideMenuItem.tsx │ │ │ ├── GuidePages.tsx │ │ │ ├── GuideMenu.tsx │ │ │ ├── TypesOfSustainabilityGuide.tsx │ │ │ ├── EnergyConsumptionGuide.tsx │ │ │ └── InclusiveLanguageGuide.tsx │ │ ├── Dashboard │ │ │ ├── DashboardInfo.tsx │ │ │ ├── DashboardComponents.tsx │ │ │ └── Dashboard.tsx │ │ └── Home.tsx │ ├── metrics │ │ ├── Language │ │ │ ├── LanguageScores.ts │ │ │ ├── ProgrammingLanguage.tsx │ │ │ ├── LanguageAdvise.tsx │ │ │ ├── LanguageTooltips.ts │ │ │ └── LanguagePiechart.tsx │ │ ├── General │ │ │ └── Info.tsx │ │ ├── Sentiment │ │ │ ├── Explanation.tsx │ │ │ ├── analysis.ts │ │ │ └── IssuesSentiment.tsx │ │ ├── Inclusivity │ │ │ ├── Recommendation.tsx │ │ │ ├── Inclusive.tsx │ │ │ └── checkLanguage.ts │ │ ├── Contributors │ │ │ ├── ContributorLogic.ts │ │ │ ├── Contributors.tsx │ │ │ └── ContributorPiechart.tsx │ │ ├── Workflows │ │ │ └── WorkflowAnalysis.tsx │ │ └── Governance │ │ │ └── Governance.tsx │ └── structure │ │ ├── DropDown.tsx │ │ ├── Section.tsx │ │ ├── Header.tsx │ │ ├── Sidebar.tsx │ │ └── SearchBar.tsx ├── App.tsx ├── assets │ └── react.svg └── logic │ └── fetcher.ts ├── public ├── fonts │ ├── Lora-Regular.ttf │ ├── PlayfairDisplay-Bold.ttf │ ├── PlayfairDisplay-Black.ttf │ └── PlayfairDisplay-Regular.ttf ├── vite.svg └── susie.svg ├── vite.config.ts ├── tsconfig.node.json ├── .gitignore ├── index.html ├── tsconfig.json ├── susie-changelog-template.hbs ├── package.json ├── CONTRIBUTING.md ├── README.md ├── CODE_OF_CONDUCT.md └── CHANGELOG.md /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/fonts/Lora-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippedeb/susie/HEAD/public/fonts/Lora-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/PlayfairDisplay-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippedeb/susie/HEAD/public/fonts/PlayfairDisplay-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/PlayfairDisplay-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippedeb/susie/HEAD/public/fonts/PlayfairDisplay-Black.ttf -------------------------------------------------------------------------------- /public/fonts/PlayfairDisplay-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippedeb/susie/HEAD/public/fonts/PlayfairDisplay-Regular.ttf -------------------------------------------------------------------------------- /src/css/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: flex-start; 5 | align-items: stretch; 6 | } 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What does this PR do? 2 | 3 | 4 | ## Which issue does it close? 5 | 6 | 7 | ## Checklist before merging 8 | - [ ] Tested 9 | - [ ] Documented -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | base: "/susie/" 8 | }) 9 | -------------------------------------------------------------------------------- /src/css/Section.css: -------------------------------------------------------------------------------- 1 | .section { 2 | background-color: #343a40; 3 | border-radius: 15px; 4 | padding: 20px; 5 | margin-bottom: 20px; 6 | } 7 | 8 | .section h3 { 9 | margin-bottom: 15px; 10 | color: #ffffff; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/css/WorkflowAnalysis.css: -------------------------------------------------------------------------------- 1 | .workflow-progressbar { 2 | height: 30px; 3 | margin: 10px; 4 | margin-bottom: 20px; 5 | border-radius: 50px; 6 | } 7 | 8 | .workflow-legend { 9 | margin-right: 10px; 10 | } 11 | 12 | .legend { 13 | margin-bottom: 10px; 14 | } 15 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import 'bootstrap/dist/css/bootstrap.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | package-lock.json 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /src/components/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import "../../css/NotFound.css"; 2 | 3 | interface NotFoundProps { 4 | item: string; 5 | } 6 | 7 | function NotFound(props: NotFoundProps) { 8 | return ( 9 |
10 |

11 | Sorry, we could not find the {props.item} you were looking for... 💚 12 |

13 |
14 | ); 15 | } 16 | 17 | export default NotFound; 18 | -------------------------------------------------------------------------------- /src/css/GuidePage.css: -------------------------------------------------------------------------------- 1 | .guide-page { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 100%; 7 | margin-top: 50px; 8 | margin-left: 10px; 9 | margin-right: 10px; 10 | } 11 | 12 | .guide-page-title { 13 | color: #d4c2a1; 14 | text-align: center; 15 | margin-bottom: 50px; 16 | } 17 | 18 | .guide-page-container { 19 | max-width: 800px; 20 | margin-bottom: 20px; 21 | } 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Susie 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/css/Dashboard.css: -------------------------------------------------------------------------------- 1 | .sections-col { 2 | margin-left: auto; 3 | margin-right: auto; 4 | } 5 | 6 | .dashboard-info { 7 | margin-top: 40px; 8 | } 9 | 10 | .experimental-button { 11 | border-radius: 20px !important; 12 | padding: 5px 15px !important; 13 | background: #343a40 !important; 14 | border-color: #484e54 !important; 15 | border: none !important; 16 | color: #b9c1c9 !important; 17 | font-weight: 500 !important; 18 | transition: transform 0.3s ease-in-out !important; 19 | } 20 | 21 | .experimental-button:hover { 22 | transform: scale(1.05); 23 | } 24 | -------------------------------------------------------------------------------- /src/css/Guide.css: -------------------------------------------------------------------------------- 1 | .guide { 2 | cursor: pointer; 3 | background-color: #343a40 !important; 4 | border-radius: 15px !important; 5 | text-decoration: none; 6 | color: #ffffff; 7 | transition: background-color 0.3s ease-in-out; 8 | } 9 | 10 | .guide:hover { 11 | background-color: #7b7468 !important; 12 | } 13 | 14 | .guide .card-body { 15 | padding: 20px; 16 | } 17 | 18 | .card-title { 19 | color: rgb(117, 180, 228); 20 | } 21 | 22 | .card-description { 23 | font-size: 1.15rem; 24 | font-weight: 420; 25 | line-height: 1.5; 26 | margin-bottom: 0; 27 | color: white; 28 | } 29 | -------------------------------------------------------------------------------- /src/css/Recommendation.css: -------------------------------------------------------------------------------- 1 | .recommendation-container { 2 | background-color: #484e54; 3 | border-radius: 10px; 4 | padding-left: 20px; 5 | padding-right: 20px; 6 | padding-top: 10px; 7 | padding-bottom: 5px; 8 | margin-top: 15px; 9 | margin-bottom: 15px; 10 | } 11 | 12 | .recommendation-header { 13 | display: flex; 14 | align-items: center; 15 | justify-content: space-between; 16 | cursor: pointer; 17 | } 18 | 19 | .dropdown-icon { 20 | font-size: 1.5rem; 21 | } 22 | 23 | .recommendation-body { 24 | margin-top: 10px; 25 | margin-bottom: 10px; 26 | color: #dedede; 27 | } 28 | -------------------------------------------------------------------------------- /src/css/Sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | right: 0; 6 | width: 200px; 7 | padding: 15px; 8 | color: white; 9 | overflow-y: auto; 10 | z-index: 2; 11 | margin-top: 72px; /* Add margin-top to push the Sidebar below the Header */ 12 | } 13 | 14 | .list-group-item { 15 | background-color: #343a40 !important; 16 | border-color: #454d55 !important; 17 | cursor: pointer; 18 | color: white !important; 19 | } 20 | 21 | .list-group-item:hover { 22 | background-color: #454d55 !important; 23 | } 24 | 25 | .sidebar-title { 26 | text-align: center; 27 | font-weight: bold; 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /src/components/pages/Guides/GuidePage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Col, Container, Row } from "react-bootstrap"; 3 | import "../../../css/GuidePage.css"; 4 | 5 | type GuidePageProps = { 6 | title: string; 7 | children: React.ReactNode; 8 | }; 9 | 10 | function GuidePage(props: GuidePageProps) { 11 | return ( 12 |
13 |

{props.title}

14 | 15 | 16 | {props.children} 17 | 18 | 19 |
20 | ); 21 | } 22 | 23 | export default GuidePage; 24 | -------------------------------------------------------------------------------- /src/css/Link.css: -------------------------------------------------------------------------------- 1 | .susie-link { 2 | color: #7bc7ed; 3 | text-decoration: none; 4 | font-weight: bold; 5 | } 6 | 7 | .susie-link:hover { 8 | color: #7bc7ed; 9 | text-decoration: underline; 10 | font-weight: bold; 11 | } 12 | 13 | .susie-link-dark { 14 | color: #435b9c; 15 | text-decoration: none; 16 | font-weight: bold; 17 | } 18 | 19 | .susie-link-dark:hover { 20 | color: #435b9c; 21 | text-decoration: underline; 22 | font-weight: bold; 23 | } 24 | 25 | .susie-link-light { 26 | color: #7bc7ed; 27 | text-decoration: none; 28 | } 29 | 30 | .susie-link-light:hover { 31 | color: #7bc7ed; 32 | text-decoration: underline; 33 | } 34 | -------------------------------------------------------------------------------- /src/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Lora"; 3 | src: url("./fonts/Lora-Regular.ttf") format("truetype"); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | @font-face { 8 | font-family: "Playfair Display"; 9 | src: url("./fonts/PlayfairDisplay-Regular.ttf") format("truetype"); 10 | font-weight: normal; 11 | font-style: normal; 12 | } 13 | @font-face { 14 | font-family: "Playfair Display"; 15 | src: url("./fonts/PlayfairDisplay-Bold.ttf") format("truetype"); 16 | font-weight: bold; 17 | font-style: normal; 18 | } 19 | @font-face { 20 | font-family: "Playfair Display"; 21 | src: url("./fonts/PlayfairDisplay-Black.ttf") format("truetype"); 22 | font-weight: 900; 23 | font-style: normal; 24 | } 25 | -------------------------------------------------------------------------------- /src/css/NotFound.css: -------------------------------------------------------------------------------- 1 | .not-found { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 40vh; 6 | } 7 | 8 | .not-found-text { 9 | font-size: 2em; 10 | color: white; /* Text is white */ 11 | text-align: center; 12 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2); 13 | animation: float 3s ease-in-out infinite; 14 | } 15 | 16 | @keyframes float { 17 | 0% { 18 | transform: translateY(0); 19 | } 20 | 50% { 21 | transform: translateY(-2vh); 22 | } 23 | 100% { 24 | transform: translateY(0); 25 | } 26 | } 27 | 28 | @keyframes float-object { 29 | 0% { 30 | transform: translateY(0); 31 | } 32 | 50% { 33 | transform: translateY(-10px); 34 | } 35 | 100% { 36 | transform: translateY(0); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/metrics/Language/LanguageScores.ts: -------------------------------------------------------------------------------- 1 | // https://greenlab.di.uminho.pt/wp-content/uploads/2017/10/sleFinal.pdf 2 | export const LanguageScores: Record = { 3 | "C": 1, 4 | "Rust": 1.03, 5 | "C++": 1.34, 6 | "Ada": 1.70, 7 | "Java": 1.98, 8 | "Pascal": 2.14, 9 | "Chapel": 2.18, 10 | "Lisp": 2.27, 11 | "Ocaml": 2.40, 12 | "Fortran": 2.52, 13 | "Swift": 2.79, 14 | "Haskell": 3.10, 15 | "C#": 3.14, 16 | "Go":3.23, 17 | "Dart":3.83, 18 | "F#": 4.13, 19 | "JavaScript":4.45, 20 | "Racket": 7.91, 21 | "TypeScript": 21.50, 22 | "Hack":24.02, 23 | "PHP": 29.30, 24 | "Erlang":42.23, 25 | "Lua":45.98, 26 | "Jruby":46.54, 27 | "Ruby":69.91, 28 | "Python":75.88, 29 | "Perl":79.58 30 | }; -------------------------------------------------------------------------------- /src/components/structure/DropDown.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Collapse } from "react-bootstrap"; 3 | import "../../css/Recommendation.css"; 4 | 5 | interface Props { 6 | header: string; 7 | collapsed: boolean; 8 | children: React.ReactNode; 9 | } 10 | 11 | function DropDown(props: Props) { 12 | const [isOpen, setIsOpen] = useState(!props.collapsed); 13 | 14 | const toggleDropdown = () => { 15 | setIsOpen(!isOpen); 16 | }; 17 | 18 | return ( 19 |
20 |
21 |
{props.header}
22 |
23 | {props.children && ( 24 | 25 |
{props.children}
26 |
27 | )} 28 |
29 | ); 30 | } 31 | 32 | export default DropDown; 33 | -------------------------------------------------------------------------------- /src/css/fade-in.css: -------------------------------------------------------------------------------- 1 | .fade-in { 2 | animation: fadeIn 1s ease-in-out forwards; 3 | opacity: 0; 4 | } 5 | 6 | .fade-in-up { 7 | animation: fadeInUp 1s ease-in-out forwards; 8 | opacity: 0; 9 | } 10 | 11 | .fade-in-down { 12 | animation: fadeInDown 0.5s ease-in-out forwards; 13 | opacity: 0; 14 | } 15 | 16 | .fade-in-after { 17 | animation: fadeIn 1s ease-in-out forwards; 18 | opacity: 0; 19 | animation-delay: 0.6s; 20 | } 21 | 22 | @keyframes fadeIn { 23 | 0% { 24 | opacity: 0; 25 | } 26 | 100% { 27 | opacity: 1; 28 | } 29 | } 30 | 31 | @keyframes fadeInUp { 32 | 0% { 33 | opacity: 0; 34 | transform: translateY(10px); 35 | } 36 | 100% { 37 | opacity: 1; 38 | transform: translateY(0); 39 | } 40 | } 41 | 42 | @keyframes fadeInDown { 43 | 0% { 44 | opacity: 0; 45 | transform: translateY(-20px); 46 | } 47 | 100% { 48 | opacity: 1; 49 | transform: translateY(0); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: "💡 Feature Request" 2 | description: Create a new ticket for a new feature request 3 | title: "💡 [REQUEST] - " 4 | labels: [ 5 | "question" 6 | ] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: "Description" 12 | description: Provide a brief explanation of the feature 13 | placeholder: Describe in a few lines your feature request 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: reference_issues 18 | attributes: 19 | label: "Reference Issues" 20 | description: Common issues 21 | placeholder: "#Issues IDs" 22 | validations: 23 | required: false 24 | - type: textarea 25 | id: implementation_pr 26 | attributes: 27 | label: "Implementation PR" 28 | description: Pull request used 29 | placeholder: "#Pull Request ID" 30 | validations: 31 | required: false -------------------------------------------------------------------------------- /src/components/structure/Section.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useState } from "react"; 2 | import "../../css/Section.css"; 3 | 4 | interface SectionProps { 5 | title: string; 6 | children: ReactNode; 7 | } 8 | 9 | function Section(props: SectionProps) { 10 | const [isMobile, setIsMobile] = useState(false); 11 | 12 | useEffect(() => { 13 | function handleResize() { 14 | setIsMobile(window.innerWidth <= 800); 15 | } 16 | window.addEventListener("resize", handleResize); 17 | handleResize(); 18 | return () => window.removeEventListener("resize", handleResize); 19 | }, []); 20 | 21 | return ( 22 | <div 23 | id={props.title.toLowerCase().replace(" ", "-")} 24 | className="section mb-3" 25 | > 26 | {isMobile ? <h4>{props.title}</h4> : <h3>{props.title}</h3>} 27 | <hr /> 28 | <div className="section-content">{props.children}</div> 29 | </div> 30 | ); 31 | } 32 | 33 | export default Section; 34 | -------------------------------------------------------------------------------- /src/css/Guides.css: -------------------------------------------------------------------------------- 1 | .main-title { 2 | font-size: 3rem; 3 | text-align: center; 4 | margin-top: 2rem; 5 | margin-bottom: 0.5rem; 6 | color: #d4c2a1; 7 | } 8 | 9 | .sub-title { 10 | font-size: 1.5rem; 11 | text-align: center; 12 | margin-top: 0.5rem; 13 | margin-bottom: 2rem; 14 | margin-right: 1.25rem; 15 | margin-left: 1.25rem; 16 | color: #ffffff; 17 | } 18 | 19 | .guides-container { 20 | display: grid; 21 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 22 | gap: 1.5rem; 23 | } 24 | 25 | .guides-container a { 26 | text-decoration: none; 27 | color: #ffffff; 28 | } 29 | 30 | .guides-container:hover a { 31 | text-decoration: none; 32 | color: #ffffff; 33 | } 34 | 35 | .guides-container .guide { 36 | border: none; 37 | transition: transform 0.3s ease-in-out; 38 | } 39 | 40 | .guides-container .guide:hover { 41 | border: none; 42 | box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.1); 43 | transform: scale(1.05); 44 | } 45 | -------------------------------------------------------------------------------- /susie-changelog-template.hbs: -------------------------------------------------------------------------------- 1 | <p align="center"> 2 | <img src='public/susie.svg' style="display: block; 3 | margin-left: auto; 4 | margin-right: auto; 5 | width: 5%;"> 6 | </p> 7 | 8 | ## Changelog 9 | All notable changes to Susie will be documented in this file. 10 | 11 | To update after using `npm version`, run the following command: 12 | 13 | ```bash 14 | auto-changelog --template susie-changelog-template.hbs --commit-limit 500 --backfill-limit 500 --sort-commits date-desc 15 | ``` 16 | 17 | > Note: be aware of the manually entered limit, you might need to increase it. 18 | 19 | {{#each releases}} 20 | {{#if href}} 21 | ## [{{title}}]({{href}}){{#if tag}} - {{isoDate}}{{/if}} 22 | {{else}} 23 | ## {{title}}{{#if tag}} - {{isoDate}}{{/if}} 24 | {{/if}} 25 | 26 | {{#commit-list commits heading='### Commits'}} 27 | - {{#if breaking}}**Breaking change:** {{/if}}{{subject}} {{#if href}}[`{{shorthash}}`]({{href}}){{/if}} 28 | {{/commit-list}} 29 | 30 | {{/each}} -------------------------------------------------------------------------------- /src/components/pages/Dashboard/DashboardInfo.tsx: -------------------------------------------------------------------------------- 1 | import { OverlayTrigger, Tooltip } from "react-bootstrap"; 2 | import { extractGitHubOwnerAndRepo } from "../../../logic/fetcher"; 3 | 4 | interface Props { 5 | repoLink: string; 6 | } 7 | 8 | function DashboardInfo(props: Props) { 9 | const [owner, repo] = extractGitHubOwnerAndRepo(props.repoLink); 10 | 11 | return ( 12 | <div className="dashboard-info text-center my-4"> 13 | <h1>Dashboard</h1> 14 | <h5> 15 | <OverlayTrigger 16 | placement="top" 17 | overlay={<Tooltip>Repository Name</Tooltip>} 18 | > 19 | <span role="img" aria-label="repo"> 20 | 📁 21 | </span> 22 | </OverlayTrigger>{" "} 23 | {repo}{" "} 24 | <OverlayTrigger placement="top" overlay={<Tooltip>Owner</Tooltip>}> 25 | <span role="img" aria-label="owner"> 26 | 🛠️ 27 | </span> 28 | </OverlayTrigger>{" "} 29 | {owner} 30 | </h5> 31 | </div> 32 | ); 33 | } 34 | 35 | export default DashboardInfo; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "susie", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "pnpm vite", 8 | "build": "tsc && pnpm vite build" 9 | }, 10 | "homepage": "/susie/#", 11 | "dependencies": { 12 | "@fortawesome/free-solid-svg-icons": "^6.3.0", 13 | "@fortawesome/react-fontawesome": "^0.2.0", 14 | "@types/sentiment": "^5.0.1", 15 | "bad-words": "^3.0.4", 16 | "bootstrap": "^5.2.3", 17 | "chart.js": "^4.2.1", 18 | "pnpm": "^8.6.8", 19 | "react": "^18.2.0", 20 | "react-bootstrap": "^2.7.2", 21 | "react-chartjs-2": "^5.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-native": "^0.71.4", 24 | "react-native-paper": "^5.5.1", 25 | "react-native-progress": "^5.0.0", 26 | "react-router-dom": "^6.9.0", 27 | "sentiment": "^5.0.2" 28 | }, 29 | "devDependencies": { 30 | "@types/bad-words": "^3.0.1", 31 | "@types/colorbrewer": "^1.0.29", 32 | "@types/node": "^18.15.7", 33 | "@types/react": "^18.0.27", 34 | "@types/react-dom": "^18.0.10", 35 | "@vitejs/plugin-react": "^3.1.0", 36 | "auto-changelog": "^2.4.0", 37 | "typescript": "^4.9.3", 38 | "vite": "^4.5.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/pages/Guides/GuideMenuItem.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { Card } from "react-bootstrap"; 3 | import "../../../css/Guide.css"; 4 | import { useEffect, useState } from "react"; 5 | 6 | type GuideProps = { 7 | guideTitle: string; 8 | description: string; 9 | guideKey: string; 10 | }; 11 | 12 | function Guide(props: GuideProps) { 13 | const [isMobile, setIsMobile] = useState(false); 14 | 15 | useEffect(() => { 16 | function handleResize() { 17 | setIsMobile(window.innerWidth <= 800); 18 | } 19 | window.addEventListener("resize", handleResize); 20 | handleResize(); 21 | return () => window.removeEventListener("resize", handleResize); 22 | }, []); 23 | 24 | return ( 25 | <Card className="guide"> 26 | <Link to={`/guide?name=${props.guideKey}`}> 27 | <Card.Body> 28 | <Card.Title className="card-title" key={props.guideKey}> 29 | {props.guideTitle} 30 | </Card.Title> 31 | <Card.Text 32 | className="card-description" 33 | style={{ fontSize: isMobile ? "1rem" : "1.15rem" }} 34 | > 35 | {props.description} 36 | </Card.Text> 37 | </Card.Body> 38 | </Link> 39 | </Card> 40 | ); 41 | } 42 | 43 | export default Guide; 44 | -------------------------------------------------------------------------------- /src/components/pages/Guides/GuidePages.tsx: -------------------------------------------------------------------------------- 1 | import NotFound from "../NotFound"; 2 | import GuidePage from "./GuidePage"; 3 | import EnergyConsumptionGuide from "./EnergyConsumptionGuide"; 4 | import InclusiveLanguageGuide from "./InclusiveLanguageGuide"; 5 | import TypesOfSustainabilityGuide from "./TypesOfSustainabilityGuide"; 6 | 7 | interface GuidePagesProps { 8 | guideKey: string; 9 | } 10 | 11 | type GuidePages = { 12 | [key: string]: JSX.Element; 13 | }; 14 | 15 | function GuidePages(props: GuidePagesProps) { 16 | // All the guides! 📚 17 | // Instruction: add a guide by adding a new key-value pair with the key being the guideKey and the value being the guide page. 18 | const guidePages: GuidePages = { 19 | "types-of-sustainability": ( 20 | <GuidePage title="Sustainability Types"> 21 | <TypesOfSustainabilityGuide /> 22 | </GuidePage> 23 | ), 24 | "inclusive-language": ( 25 | <GuidePage title="Inclusive Language"> 26 | <InclusiveLanguageGuide /> 27 | </GuidePage> 28 | ), 29 | "programming-languages": ( 30 | <GuidePage title="Programming Languages"> 31 | <EnergyConsumptionGuide /> 32 | </GuidePage> 33 | ), 34 | }; 35 | 36 | if (props.guideKey in guidePages) { 37 | return guidePages[props.guideKey]; 38 | } 39 | return <NotFound item="guide" />; 40 | } 41 | 42 | export default GuidePages; 43 | -------------------------------------------------------------------------------- /src/components/metrics/General/Info.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from "react-bootstrap"; 2 | 3 | interface Props { 4 | commits: number; 5 | pullRequests: number; 6 | branches: number; 7 | issues: number; 8 | } 9 | 10 | function Info(props: Props) { 11 | const possiblyMaxCapacityReached = 12 | props.commits == 100 || 13 | props.pullRequests == 100 || 14 | props.branches == 100 || 15 | props.issues == 100; 16 | 17 | return ( 18 | <> 19 | <ul> 20 | <li> 21 | <strong>Commits:</strong> {props.commits} 22 | </li> 23 | <li> 24 | <strong>Pull Requests:</strong> {props.pullRequests} 25 | </li> 26 | <li> 27 | <strong>Branches:</strong> {props.branches} 28 | </li> 29 | <li> 30 | <strong>Issues:</strong> {props.issues} 31 | </li> 32 | </ul> 33 | {possiblyMaxCapacityReached && ( 34 | <> 35 | <Alert variant="warning"> 36 | {" "} 37 | ⚠️ The maximum load capacity of 100 is reached; any further 38 | information is not included in the analysis.{" "} 39 | </Alert> 40 | <Alert variant="secondary"> 41 | 🎯 While not all past data might be relevant anyway, we are planning 42 | on releasing this feature soon! 43 | </Alert> 44 | </> 45 | )} 46 | </> 47 | ); 48 | } 49 | 50 | export default Info; 51 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./css/App.css"; 2 | import "./css/fonts.css"; 3 | import Header from "./components/structure/Header"; 4 | import { 5 | BrowserRouter as Router, 6 | Routes, 7 | Route, 8 | useLocation, 9 | HashRouter, 10 | } from "react-router-dom"; 11 | import Dashboard from "./components/pages/Dashboard/Dashboard"; 12 | import Home from "./components/pages/Home"; 13 | import NotFound from "./components/pages/NotFound"; 14 | import Guides from "./components/pages/Guides/GuideMenu"; 15 | import GuidePages from "./components/pages/Guides/GuidePages"; 16 | 17 | function App() { 18 | return ( 19 | <HashRouter> 20 | <div 21 | className="App" 22 | style={{ 23 | background: "#292a2d", 24 | color: "#fff", 25 | minHeight: "100vh", 26 | }} 27 | > 28 | <Header /> 29 | <Routes> 30 | <Route path="/" element={<Home />} /> 31 | <Route path="/dashboard" element={<Dashboard />} /> 32 | <Route path="/guides" element={<Guides />} /> 33 | <Route path="/guide" element={<GuidePageRoute />} /> 34 | <Route path="*" element={<NotFound item={"page"} />} /> 35 | </Routes> 36 | </div> 37 | </HashRouter> 38 | ); 39 | } 40 | 41 | function GuidePageRoute() { 42 | const location = useLocation(); 43 | const guideKey = new URLSearchParams(location.search).get("name"); 44 | if (guideKey === null) { 45 | return <NotFound item="guide" />; 46 | } 47 | return <GuidePages guideKey={guideKey} />; 48 | } 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /src/components/metrics/Language/ProgrammingLanguage.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from "react-bootstrap"; 2 | import DropDown from "../../structure/DropDown"; 3 | import LanguageAdvise from "./LanguageAdvise"; 4 | import LanguagePiechart from "./LanguagePiechart"; 5 | 6 | interface ProgrammingLanguageProps { 7 | languages: { [key: string]: number }; 8 | } 9 | 10 | function detailedAnalysis(languages: { [key: string]: number }) { 11 | return ( 12 | <> 13 | <DropDown header={"Tech Stack of the Repository 💻"} collapsed={false}> 14 | <LanguagePiechart languages={languages} /> 15 | </DropDown> 16 | <LanguageAdvise languages={languages} threshold={20.0} /> 17 | </> 18 | ); 19 | } 20 | 21 | function noLanguagesFound() { 22 | return ( 23 | <Alert variant="success"> 24 | No languages found.. Perhaps, the highest achievement of efficiency! 👑{" "} 25 | </Alert> 26 | ); 27 | } 28 | 29 | function ProgrammingLanguage(props: ProgrammingLanguageProps) { 30 | return ( 31 | <> 32 | <p> 33 | Did you know that your choice of programming language can have an 34 | enviromental impact? 🌱 35 | <br /> Read more about the topic in{" "} 36 | <a 37 | className="susie-link" 38 | href="./#/guide?name=inclusive-language" 39 | target="_blank" 40 | rel="noopener noreferrer" 41 | > 42 | this guide 43 | </a>{" "} 44 | or look at what Susie has found: 45 | </p> 46 | 47 | {Object.keys(props.languages).length > 0 48 | ? detailedAnalysis(props.languages) 49 | : noLanguagesFound()} 50 | </> 51 | ); 52 | } 53 | 54 | export default ProgrammingLanguage; 55 | -------------------------------------------------------------------------------- /src/components/metrics/Language/LanguageAdvise.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from "react-bootstrap"; 2 | import DropDown from "../../structure/DropDown"; 3 | import { LanguageTooltips } from "./LanguageTooltips"; 4 | 5 | interface Props { 6 | languages: { [key: string]: number }; 7 | threshold: number; 8 | } 9 | 10 | function analyseLanguage([language, usagePercentage]: [string, number]) { 11 | const tooltip = LanguageTooltips[language]; 12 | return `${usagePercentage}% of your code is ${language}. ${tooltip}`; 13 | } 14 | 15 | function LanguageAdvise(props: Props) { 16 | const labels = Object.keys(props.languages); 17 | const data = Object.values(props.languages); 18 | 19 | const total = data.reduce((acc, curr) => acc + curr, 0); 20 | const percentages = data.map((value) => ((value / total) * 100).toFixed(2)); 21 | 22 | const filteredLabels = labels 23 | .map((label, index) => [label, percentages[index]]) 24 | .filter(([label, percentage]) => { 25 | // Only show languages that are above the threshold and have a tooltip 26 | return +percentage >= props.threshold && label in LanguageTooltips; 27 | }); 28 | 29 | return ( 30 | <> 31 | <h5 className="mt-3">Insights</h5> 32 | {filteredLabels.length == 0 ? ( 33 | <Alert variant="success"> 34 | You are using efficient languages, well done! 35 | </Alert> 36 | ) : ( 37 | filteredLabels.map(([label, usagePercentage], index) => ( 38 | <DropDown 39 | key={index} 40 | header={label} 41 | collapsed={false} 42 | children={<p>{analyseLanguage([label, +usagePercentage])}</p>} 43 | ></DropDown> 44 | )) 45 | )} 46 | </> 47 | ); 48 | } 49 | 50 | export default LanguageAdvise; 51 | -------------------------------------------------------------------------------- /src/components/pages/Guides/GuideMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Container } from "react-bootstrap"; 3 | import Guide from "./GuideMenuItem"; 4 | import "../../../css/Guides.css"; 5 | import "../../../css/Guide.css"; 6 | 7 | function Guides() { 8 | const [isMobile, setIsMobile] = useState(false); 9 | 10 | useEffect(() => { 11 | function handleResize() { 12 | setIsMobile(window.innerWidth <= 768); 13 | } 14 | window.addEventListener("resize", handleResize); 15 | handleResize(); 16 | return () => window.removeEventListener("resize", handleResize); 17 | }, []); 18 | 19 | return ( 20 | <Container> 21 | <div className="title-container fade-in-down"> 22 | <h2 className="main-title">Guides</h2> 23 | <h4 24 | className="sub-title" 25 | style={{ fontSize: isMobile ? "1.35rem" : "1.5rem" }} 26 | > 27 | {isMobile 28 | ? "Explore more about sustainability" 29 | : "Explore more about sustainability here.. 🗂️"} 30 | </h4> 31 | </div> 32 | <div className="guides-container fade-in-after"> 33 | <Guide 34 | guideTitle="Sustainability Types" 35 | description="No, it's not all environmental.. 🌱" 36 | guideKey={"types-of-sustainability"} 37 | /> 38 | <Guide 39 | guideTitle="Inclusive Language" 40 | description="Change your lingo! 💬" 41 | guideKey={"inclusive-language"} 42 | /> 43 | <Guide 44 | guideTitle="Programming Languages" 45 | description="How energy efficient are they? 🔬" 46 | guideKey={"programming-languages"} 47 | /> 48 | </div> 49 | </Container> 50 | ); 51 | } 52 | 53 | export default Guides; 54 | -------------------------------------------------------------------------------- /public/susie.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="iso-8859-1"?> 2 | <svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 3 | viewBox="0 0 495 495" xml:space="preserve"> 4 | <g> 5 | <path style="fill:#B39A7C;" d="M428.22,380.075c-6.965,0-13.683-1.075-20-3.063V455h40v-77.988 6 | C441.902,379,435.185,380.075,428.22,380.075z"/> 7 | <path style="fill:#B39A7C;" d="M86.78,377.012c-6.317,1.987-13.035,3.063-20,3.063s-13.683-1.075-20-3.063V455h40V377.012z"/> 8 | <path style="fill:#A78966;" d="M267.5,377.927c-6.448,1.403-13.138,2.148-20,2.148s-13.552-0.746-20-2.148V455h40V377.927z"/> 9 | <path style="fill:#91DC5A;" d="M428.22,146c-36.823,0-66.78,29.958-66.78,66.78v100.515c0,29.858,19.698,55.198,46.78,63.718 10 | c6.317,1.987,13.035,3.063,20,3.063V146z"/> 11 | <path style="fill:#91DC5A;" d="M66.78,146C29.957,146,0,175.958,0,212.78v100.515c0,29.858,19.698,55.198,46.78,63.718 12 | c6.317,1.987,13.035,3.063,20,3.063V146z"/> 13 | <path style="fill:#91DC5A;" d="M247.5,0c-51.837,0-94.01,42.173-94.01,94.01v192.055c0,44.975,31.749,82.668,74.01,91.861 14 | c6.448,1.403,13.138,2.148,20,2.148V0z"/> 15 | <path style="fill:#6DC82A;" d="M428.22,380.075c6.965,0,13.683-1.075,20-3.063c27.082-8.519,46.78-33.86,46.78-63.718V212.78 16 | c0-36.823-29.957-66.78-66.78-66.78V380.075z"/> 17 | <path style="fill:#6DC82A;" d="M66.78,380.075c6.965,0,13.683-1.075,20-3.063c27.082-8.519,46.78-33.86,46.78-63.718V212.78 18 | c0-36.823-29.957-66.78-66.78-66.78V380.075z"/> 19 | <path style="fill:#6DC82A;" d="M247.5,380.075c6.862,0,13.552-0.746,20-2.148c42.261-9.193,74.01-46.886,74.01-91.861V94.01 20 | C341.51,42.173,299.337,0,247.5,0V380.075z"/> 21 | <polygon style="fill:#00665A;" points="267.5,455 227.5,455 86.78,455 46.78,455 0,455 0,495 495,495 495,455 448.22,455 22 | 408.22,455 "/> 23 | </g> 24 | </svg> -------------------------------------------------------------------------------- /src/css/Header.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | z-index: 100; 7 | color: white; 8 | } 9 | 10 | @keyframes shake { 11 | 0% { 12 | transform: rotate(0deg); 13 | } 14 | 10% { 15 | transform: rotate(-1.5deg); 16 | } 17 | 20% { 18 | transform: rotate(1.5deg); 19 | } 20 | 30% { 21 | transform: rotate(-1.5deg); 22 | } 23 | 40% { 24 | transform: rotate(1.5deg); 25 | } 26 | 50% { 27 | transform: rotate(-1.5deg); 28 | } 29 | 60% { 30 | transform: rotate(1.5deg); 31 | } 32 | 70% { 33 | transform: rotate(-1.5deg); 34 | } 35 | 80% { 36 | transform: rotate(1.5deg); 37 | } 38 | 90% { 39 | transform: rotate(-1.5deg); 40 | } 41 | 100% { 42 | transform: rotate(0deg); 43 | } 44 | } 45 | 46 | .shake:hover { 47 | animation: shake 1.5s; 48 | } 49 | 50 | .header-button { 51 | background-color: #343a40; 52 | border-radius: 20px; 53 | border: 2px solid #343a40; 54 | color: rgb(221, 221, 221) !important; 55 | text-decoration: none !important; 56 | font-weight: bold !important; 57 | margin-left: 10px; 58 | margin-right: 10px; 59 | margin-top: 3px; 60 | margin-bottom: 3px; 61 | padding: 5px 15px; 62 | transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out; 63 | } 64 | 65 | .header-button:hover { 66 | background-color: #61856f; 67 | border: 2px solid rgb(221, 221, 221); 68 | } 69 | 70 | .navbar-collapse { 71 | padding-top: 8px; 72 | padding-bottom: 5px; 73 | } 74 | 75 | .navbar-toggle { 76 | outline: none; 77 | box-shadow: none; 78 | color: white; 79 | } 80 | 81 | .navbar-dark .navbar-toggler-icon { 82 | background-image: url("data:image/svg+xml;.."); 83 | } 84 | 85 | .navbar-toggler { 86 | outline: none !important; 87 | box-shadow: none !important; 88 | border: 0px !important; 89 | } 90 | -------------------------------------------------------------------------------- /src/components/metrics/Language/LanguageTooltips.ts: -------------------------------------------------------------------------------- 1 | export const LanguageTooltips: Record<string, string> = { 2 | "C": "C code is very energy efficient!", 3 | "Rust": "Rust code is very energy efficient!", 4 | "C++": "C++ code almost as energy efficient as C code.", 5 | "Ada": "Ada code almost as energy efficient as C code.", 6 | "Java": "C code is about twice as energy efficient as Java, but overall Java is not too bad!", 7 | "Pascal": "C code is about twice as energy efficient as Pascal, but overall Pascal is not too bad!", 8 | "Chapel": "C code is about twice as energy efficient as Chapel, but overall Chapel is not too bad!", 9 | "Lisp": "C code is about twice as energy efficient as Lisp, but overall Lisp is not too bad!", 10 | "Ocaml": "C code is about 2.5 times more energy efficient than Ocaml, but overall Ocaml is not too bad!", 11 | "Fortran": "C code is about 2.5 times more energy efficient than Fortran, but overall Fortran is not too bad!", 12 | // "Swift": 2.79, 13 | // "Haskell": 3.10, 14 | // "C#": 3.14, 15 | // "Go":3.23, 16 | // "Dart":3.83, 17 | // "F#": 4.13, 18 | "JavaScript": "Plain Javascript code is about 5 times more energy efficient than Typescript!", 19 | // "Racket": 7.91, 20 | "TypeScript": "Plain Javascript code is about 5 times more energy efficient than Typescript!", 21 | // "Hack":24.02, 22 | // "PHP": 29.30, 23 | // "Erlang":42.23, 24 | // "Lua":45.98, 25 | // "Jruby":46.54, 26 | // "Ruby":69.91, 27 | "Python": "Python is very energy inefficient. Maybe Java would also be feasible for future projects instead of Python?", 28 | "Perl": "Perl is very energy inefficient. If you'd like to make more sustainable software, consider using another language such as C or Rust! They are 75x more efficient energy-wise." 29 | }; -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Install Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | 22 | - uses: pnpm/action-setup@v2 23 | name: Install pnpm 24 | id: pnpm-install 25 | with: 26 | version: 7 27 | run_install: false 28 | 29 | - name: Get pnpm store directory 30 | id: pnpm-cache 31 | shell: bash 32 | run: | 33 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 34 | 35 | - uses: actions/cache@v3 36 | name: Setup pnpm cache 37 | with: 38 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 39 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 40 | restore-keys: | 41 | ${{ runner.os }}-pnpm-store- 42 | 43 | - name: Install dependencies 44 | run: pnpm install 45 | 46 | - name: Build project 47 | run: pnpm run build 48 | 49 | - name: Upload production-ready build files 50 | uses: actions/upload-artifact@v2 51 | with: 52 | name: production-files 53 | path: ./dist 54 | 55 | deploy: 56 | name: Deploy 57 | needs: build 58 | runs-on: ubuntu-latest 59 | if: github.ref == 'refs/heads/main' 60 | 61 | steps: 62 | - name: Download artifact 63 | uses: actions/download-artifact@v2 64 | with: 65 | name: production-files 66 | path: ./dist 67 | 68 | - name: Deploy to GitHub Pages 69 | uses: peaceiris/actions-gh-pages@v3 70 | with: 71 | github_token: ${{ secrets.GITHUB_TOKEN }} 72 | publish_dir: ./dist 73 | -------------------------------------------------------------------------------- /src/components/metrics/Sentiment/Explanation.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Collapse, Table } from "react-bootstrap"; 3 | import "../../../css/Recommendation.css"; 4 | 5 | interface Props { 6 | title: string; 7 | score: number; 8 | calculation: Array<{ 9 | [token: string]: number; 10 | }>; 11 | } 12 | 13 | function Explanation(props: Props) { 14 | const [isOpen, setIsOpen] = useState(false); 15 | const toggleDropdown = () => { 16 | setIsOpen(!isOpen); 17 | } 18 | 19 | return ( 20 | <div className="recommendation-container"> 21 | <div className="recommendation-header" onClick={toggleDropdown}> 22 | <h6> 23 | {props.title}{" "} 24 | {props.calculation.length > 0 25 | ? " → Sentiment Score: " + props.score 26 | : ""} 27 | </h6> 28 | </div> 29 | <Collapse in={isOpen}> 30 | <div className="recommendation-body"> 31 | <table className="table table-dark table-striped"> 32 | <thead> 33 | <tr> 34 | <th scope="col">Word</th> 35 | <th scope="col">Score</th> 36 | </tr> 37 | </thead> 38 | <tbody> 39 | {props.calculation.map((calculation) => ( 40 | <tr> 41 | <td>{Object.keys(calculation)[0]}</td> 42 | <td>{Object.values(calculation)[0]}</td> 43 | </tr> 44 | ))} 45 | </tbody> 46 | </table> 47 | </div> 48 | </Collapse> 49 | </div> 50 | ); 51 | } 52 | 53 | export default Explanation; -------------------------------------------------------------------------------- /src/components/structure/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Navbar, Nav } from "react-bootstrap"; 2 | import { Link } from "react-router-dom"; 3 | import logo from "/susie.svg"; 4 | import "../../css/Header.css"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import { faBars } from "@fortawesome/free-solid-svg-icons"; 7 | 8 | function Header() { 9 | return ( 10 | <Navbar bg="dark" expand="lg" className="header"> 11 | <Container> 12 | <Link className="shake" to="/" style={{ textDecoration: "none" }}> 13 | <Navbar.Brand style={{ color: "#fff", fontWeight: "bold" }}> 14 | <img 15 | src={logo} 16 | width="30" 17 | height="30" 18 | className="d-inline-block align-top" 19 | alt="Susie Logo" 20 | /> 21 | {" Susie"} 22 | </Navbar.Brand> 23 | </Link> 24 | <Navbar.Toggle aria-controls="basic-navbar-nav"> 25 | <FontAwesomeIcon icon={faBars} className="navbar-toggle" /> 26 | </Navbar.Toggle> 27 | <Navbar.Collapse id="basic-navbar-nav"> 28 | <Nav className="me-auto justify-content-center"> 29 | <Link to="/" style={{ textDecoration: "none" }}> 30 | <div className="nav-link-style header-button"> 31 | Analyse Repository 32 | </div> 33 | </Link> 34 | <Link to="/guides/" style={{ textDecoration: "none" }}> 35 | <div className="nav-link-style header-button">Guides</div> 36 | </Link> 37 | <Link 38 | to="https://github.com/philippedeb/susie/blob/main/CONTRIBUTING.md" 39 | style={{ textDecoration: "none" }} 40 | > 41 | <div className="nav-link-style header-button">Contribute</div> 42 | </Link> 43 | </Nav> 44 | </Navbar.Collapse> 45 | </Container> 46 | </Navbar> 47 | ); 48 | } 49 | 50 | export default Header; 51 | -------------------------------------------------------------------------------- /src/components/metrics/Language/LanguagePiechart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Pie } from "react-chartjs-2"; 3 | import { Container, Row, Col } from "react-bootstrap"; 4 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js"; 5 | ChartJS.register([ArcElement, Tooltip, Legend]); 6 | 7 | interface Props { 8 | languages: { [key: string]: number }; 9 | } 10 | 11 | const LanguagePiechart: React.FC<Props> = ({ languages }) => { 12 | const labels = Object.keys(languages); 13 | const data = Object.values(languages); 14 | 15 | const total = data.reduce((acc, curr) => acc + curr, 0); 16 | 17 | const chartData = { 18 | labels: labels, 19 | datasets: [ 20 | { 21 | data: data, 22 | backgroundColor: [ 23 | "#e60049", 24 | "#0bb4ff", 25 | "#50e991", 26 | "#e6d800", 27 | "#9b19f5", 28 | "#ffa300", 29 | "#dc0ab4", 30 | "#b3d4ff", 31 | "#00bfa0", 32 | ], 33 | }, 34 | ], 35 | }; 36 | 37 | const options = { 38 | animate: true, 39 | responsive: true, 40 | maintainAspectRatio: false, 41 | plugins: { 42 | tooltip: { 43 | callbacks: { 44 | label: function (context: any) { 45 | const label = context.label; 46 | const value = context.raw; 47 | const percentage = ((value / total) * 100).toFixed(2); 48 | return `${label}: ${percentage}%`; 49 | }, 50 | }, 51 | }, 52 | legend: { 53 | labels: { 54 | color: "#fff", 55 | fontWeight: "bold", 56 | }, 57 | position: "top" as "top", 58 | align: "start" as "start", 59 | }, 60 | }, 61 | }; 62 | 63 | return ( 64 | <Container> 65 | <Row> 66 | <Col> 67 | <Pie data={chartData} options={options} /> 68 | </Col> 69 | </Row> 70 | </Container> 71 | ); 72 | }; 73 | 74 | export default LanguagePiechart; 75 | -------------------------------------------------------------------------------- /src/components/structure/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { ListGroup, OverlayTrigger, Tooltip } from "react-bootstrap"; 3 | import "../../css/Sidebar.css"; 4 | 5 | interface SidebarProps { 6 | sections: { title: string; content: ReactNode }[]; 7 | experimentalSections: { title: string; content: ReactNode }[]; 8 | experimentalMode: boolean; 9 | } 10 | 11 | function Sidebar(props: SidebarProps) { 12 | const handleClick = (title: string) => { 13 | const element = document.getElementById( 14 | title.toLowerCase().replace(" ", "-") 15 | ); 16 | if (element) { 17 | element.scrollIntoView({ behavior: "smooth" }); 18 | } 19 | }; 20 | 21 | const tooltip = <Tooltip id="sidebar-tooltip">Click any section 🛸 </Tooltip>; 22 | 23 | return ( 24 | <div className="sidebar d-none d-xl-block"> 25 | <div className="sidebar-title"> 26 | <OverlayTrigger placement="top" overlay={tooltip}> 27 | <h6>Sections</h6> 28 | </OverlayTrigger> 29 | </div> 30 | <ListGroup> 31 | {props.sections.map((section, index) => ( 32 | <ListGroup.Item 33 | onClick={() => handleClick(section.title)} 34 | key={index} 35 | > 36 | {section.title} 37 | </ListGroup.Item> 38 | ))} 39 | {props.experimentalMode ? ( 40 | props.experimentalSections.map((section, index) => ( 41 | <ListGroup.Item 42 | onClick={() => handleClick(section.title)} 43 | key={props.sections.length + index} 44 | > 45 | {section.title} 46 | </ListGroup.Item> 47 | )) 48 | ) : ( 49 | <ListGroup.Item 50 | onClick={() => handleClick("Experimental metrics")} 51 | key={props.sections.length + props.experimentalSections.length} 52 | > 53 | {" "} 54 | Experimental metrics{" "} 55 | </ListGroup.Item> 56 | )} 57 | </ListGroup> 58 | </div> 59 | ); 60 | } 61 | 62 | export default Sidebar; 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | <p align="center"> 2 | <img src='public/susie.svg' style="display: block; 3 | margin-left: auto; 4 | margin-right: auto; 5 | width: 5%;"> 6 | </p> 7 | 8 | # Contributing to Susie 9 | 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 10 | 11 | All types of contributions are encouraged and valued. If you like the project, but just don’t have time to contribute, that’s fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 12 | 13 | * ⭐ Star the project 14 | * ✉️ Post about it (e.g. Twitter, LinkedIn, Reddit, etc.) 15 | * 🔗 Refer this project in your project’s readme 16 | * 💬 Mention the project at local meetups and tell your friends/colleagues 17 | 18 | ### Legal Notice 19 | Yes.. this is **important** to read! ⚖️ When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license and enforced laws. You also agree to follow the [code of conduct](CODE_OF_CONDUCT.md). 20 | 21 | ### Styleguides 22 | 23 | Please follow the styleguides below when contributing to this project. 24 | 25 | ##### Issues and Pull Requests 26 | * Use a clear and descriptive title 27 | * Select the appropiate labels 28 | * Provide as many details as possible in the description 29 | * Use the templates (should automatically appear when creating an issue or pull request) 30 | 31 | ##### Git Commit Messages 32 | * Use the present tense ("Add feature" not "Added feature") 33 | 34 | ##### Branches 35 | * Should be named after the issue they are addressing 36 | * e.g. *Issue #123* should be addressed in a branch named `123_fix-bug` 37 | 38 | ##### Code 39 | * All code should be formatted with [Prettier](https://prettier.io/) 40 | * Code should be commented where necessary 41 | * Using new packages should be discussed first 42 | * Code should be tested where necessary 43 | * No code should be pushed directly to the `main` branch (unless it is a hotfix or minor change by the project maintainers) 44 | * Refer to the [readme](README.md) for more information on the tech stack of the project and how to set it up 45 | 46 | 47 | Thank you for reading this far! 🙏 Happy contributing! 🎉 48 | -------------------------------------------------------------------------------- /src/components/metrics/Inclusivity/Recommendation.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Collapse } from "react-bootstrap"; 3 | import "../../../css/Recommendation.css"; 4 | 5 | interface Props { 6 | title: string; 7 | replacements: string[]; 8 | locations: [string, string][]; 9 | } 10 | 11 | function Recommendation(props: Props) { 12 | const [isOpen, setIsOpen] = useState(false); 13 | 14 | const toggleDropdown = () => { 15 | setIsOpen(!isOpen); 16 | }; 17 | 18 | return ( 19 | <div className="recommendation-container"> 20 | <div className="recommendation-header" onClick={toggleDropdown}> 21 | <h6> 22 | {props.title}{" "} 23 | {props.replacements.length > 0 24 | ? " → " + props.replacements.join(", ") 25 | : ""} 26 | </h6> 27 | </div> 28 | <Collapse in={isOpen}> 29 | <div className="recommendation-body"> 30 | <p> 31 | {props.replacements.length > 0 32 | ? 'The term "' + 33 | props.title + 34 | '" was located in the following places:' 35 | : "Located in the following places:"} 36 | </p> 37 | <ul> 38 | {props.locations.map((item, index) => ( 39 | <li key={index} className="mt-2"> 40 | {displayItemType(item)} 41 | </li> 42 | ))} 43 | </ul> 44 | </div> 45 | </Collapse> 46 | </div> 47 | ); 48 | } 49 | 50 | function displayItemType(entry: [string, string]): string { 51 | const [item_type, item] = entry; 52 | switch (item_type) { 53 | case "branch": 54 | return "🌳 Branch: " + item; 55 | case "changelog": 56 | return "📋 Changelog"; 57 | case "license": 58 | return "⚖️ License"; 59 | case "code_of_conduct": 60 | return "📜 Code of Conduct"; 61 | case "contributing": 62 | return "🤝 Contributing guidelines"; 63 | case "readme": 64 | return "📖 Readme"; 65 | case "issue": 66 | return "🚩 Issue: " + item; 67 | case "issue_template": 68 | return "🗺️ Issue Template"; 69 | case "commit_message": 70 | return "🔗 Commit: " + item; 71 | case "workflow": 72 | return "🏃 Workflow: " + item; 73 | case "pull_request": 74 | return "🔀 Pull Request: " + item; 75 | case "pull_request_template": 76 | return "🗺️ Pull Request Template"; 77 | } 78 | return "(❔) " + item_type + ": " + item; 79 | } 80 | 81 | export default Recommendation; 82 | -------------------------------------------------------------------------------- /src/components/metrics/Contributors/ContributorLogic.ts: -------------------------------------------------------------------------------- 1 | export { getContributionCounts, getPonyFactor, getTopContributorPower }; 2 | 3 | function getContributionCounts(commitAuthorDates: [string, string][]): { 4 | [key: string]: number; 5 | } { 6 | const authorCommitCount: { [key: string]: number } = {}; 7 | 8 | for (let i of commitAuthorDates) { 9 | if (i[0] in authorCommitCount) { 10 | authorCommitCount[i[0]] += 1; 11 | } else { 12 | authorCommitCount[i[0]] = 1; 13 | } 14 | } 15 | return authorCommitCount; 16 | } 17 | 18 | function latestCommitDate(commitAuthorDates: [string, string][]): string { 19 | return commitAuthorDates[0][1]; 20 | } 21 | 22 | /** 23 | * Get the pony factor of a repository. 24 | * The pony factor is the number of contributors needed to replace the current top contributor. 25 | * 26 | * @param commitAuthorDates The commit author dates of the repository. 27 | * @returns The pony factor of the repository. 28 | */ 29 | function getPonyFactor(commitAuthorDates: [string, string][]): number { 30 | const authorCommitCount = getContributionCounts(commitAuthorDates); 31 | const totalCommits = Object.values(authorCommitCount).reduce( 32 | (acc, val) => acc + val, 33 | 0 34 | ); 35 | const sortedContributors = Object.entries(authorCommitCount).sort( 36 | (a, b) => b[1] - a[1] 37 | ); 38 | 39 | var ponyFactor = 0; 40 | var commitCount = 0; 41 | for (let i = 0; i < sortedContributors.length; i++) { 42 | const [_, amount] = sortedContributors[i]; 43 | commitCount += amount; 44 | ponyFactor += 1; 45 | 46 | if (commitCount >= totalCommits / 2.0) { 47 | break; 48 | } 49 | } 50 | return ponyFactor; 51 | } 52 | 53 | /** 54 | * Number of contributors as the factor of the biggest contributor, in the form of a percentage. 55 | * 56 | * @param commitAuthorDates The commit author dates of the repository. 57 | * @returns The top contributor power of the repository. 58 | */ 59 | function getTopContributorPower(commitAuthorDates: [string, string][]): number { 60 | const authorCommitCount = getContributionCounts(commitAuthorDates); 61 | const sortedContributors = Object.entries(authorCommitCount).sort( 62 | (a, b) => b[1] - a[1] 63 | ); 64 | 65 | if (sortedContributors.length === 0) { 66 | return 0; 67 | } 68 | 69 | const [_, topContributorAmount] = sortedContributors[0]; 70 | 71 | const totalCommits = Object.values(authorCommitCount).reduce( 72 | (acc, val) => acc + val, 73 | 0 74 | ); 75 | 76 | return totalCommits / topContributorAmount; 77 | } 78 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: "🐛 Bug Report" 2 | description: Create a new ticket for a bug. 3 | title: "🐛 [BUG] - <title>" 4 | labels: [ 5 | "bug" 6 | ] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: "Description" 12 | description: Please enter an explicit description of your issue 13 | placeholder: Short and explicit description of your incident... 14 | validations: 15 | required: true 16 | - type: input 17 | id: reprod-url 18 | attributes: 19 | label: "Reproduction URL" 20 | description: Please enter your GitHub URL to provide a reproduction of the issue 21 | placeholder: ex. https://github.com/USERNAME/REPO-NAME 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: reprod 26 | attributes: 27 | label: "Reproduction steps" 28 | description: Please enter an explicit description of your issue 29 | value: | 30 | 1. Go to '...' 31 | 2. Click on '....' 32 | 3. Scroll down to '....' 33 | 4. See error 34 | render: bash 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: screenshot 39 | attributes: 40 | label: "Screenshots" 41 | description: If applicable, add screenshots to help explain your problem. 42 | value: | 43 | ![DESCRIPTION](LINK.png) 44 | render: bash 45 | validations: 46 | required: false 47 | - type: textarea 48 | id: logs 49 | attributes: 50 | label: "Logs" 51 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 52 | render: bash 53 | validations: 54 | required: false 55 | - type: dropdown 56 | id: browsers 57 | attributes: 58 | label: "Browsers" 59 | description: What browsers are you seeing the problem on ? 60 | multiple: true 61 | options: 62 | - Firefox 63 | - Chrome 64 | - Safari 65 | - Microsoft Edge 66 | - Opera 67 | - Other 68 | validations: 69 | required: false 70 | - type: dropdown 71 | id: os 72 | attributes: 73 | label: "OS" 74 | description: What is the impacted environment ? 75 | multiple: true 76 | options: 77 | - Windows 78 | - Linux 79 | - Mac 80 | - Android 81 | - iOS 82 | - ipadOS 83 | - Desktop 84 | - Tablet 85 | - Mobile 86 | - Other 87 | validations: 88 | required: false 89 | -------------------------------------------------------------------------------- /src/components/metrics/Sentiment/analysis.ts: -------------------------------------------------------------------------------- 1 | export { getSentiment, calcAverageSentiment, getNegativeSentiment, getPositiveSentiment, type SentenceSentiments }; 2 | 3 | import Sentiment from 'sentiment'; 4 | 5 | const sentiment = new Sentiment(); 6 | 7 | interface SentenceSentiments { 8 | sentiments: { [sentence: string]: Sentiment.AnalysisResult }; 9 | } 10 | 11 | function getSentiment(data: string[]): SentenceSentiments { 12 | const sentiments: SentenceSentiments = { 13 | sentiments: {}, 14 | }; 15 | for (const item of data) { 16 | sentiments.sentiments[item] = sentiment.analyze(item); 17 | } 18 | return sentiments; 19 | } 20 | 21 | function calcAverageSentiment(sentiments: SentenceSentiments): number { 22 | let total = 0; 23 | let count = 0; 24 | for (const item of Object.values(sentiments.sentiments)) { 25 | total += item.score; 26 | count += 1; 27 | } 28 | return total / count; 29 | } 30 | 31 | function getPositiveSentiment(sentiments: SentenceSentiments, n_sentiments: number = 3): SentenceSentiments { 32 | const positiveSentiments: SentenceSentiments = { 33 | sentiments: {}, 34 | }; 35 | // Filter the sentiments to only include them if the length of calculation is greater than 0 36 | const filteredSentiments = Object.entries(sentiments.sentiments).filter((item) => item[1].calculation.length > 0&& item[1].score > 0).sort((a, b) => b[1].score - a[1].score); 37 | 38 | // Check if legnth of filtered sentiments is less than n_sentiments 39 | if (filteredSentiments.length < n_sentiments) { 40 | n_sentiments = filteredSentiments.length; 41 | } 42 | 43 | for (let i = 0; i < n_sentiments; i++) { 44 | positiveSentiments.sentiments[filteredSentiments[i][0]] = filteredSentiments[i][1]; 45 | } 46 | return positiveSentiments; 47 | } 48 | 49 | function getNegativeSentiment(sentiments: SentenceSentiments, n_sentiments: number = 3): SentenceSentiments { 50 | const negativeSentiments: SentenceSentiments = { 51 | sentiments: {}, 52 | }; 53 | // Filter the sentiments to only include them if the length of calculation is greater than 0 54 | const filteredSentiments = Object.entries(sentiments.sentiments).filter((item) => item[1].calculation.length > 0 && item[1].score < 0).sort((a, b) => a[1].score - b[1].score); 55 | 56 | // Check if legnth of filtered sentiments is less than n_sentiments 57 | if (filteredSentiments.length < n_sentiments) { 58 | n_sentiments = filteredSentiments.length; 59 | } 60 | 61 | for (let i = 0; i < n_sentiments; i++) { 62 | negativeSentiments.sentiments[filteredSentiments[i][0]] = filteredSentiments[i][1]; 63 | } 64 | return negativeSentiments; 65 | } 66 | -------------------------------------------------------------------------------- /src/components/metrics/Workflows/WorkflowAnalysis.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Badge } from "react-bootstrap"; 2 | import { ProgressBar } from "react-bootstrap"; 3 | import "../../../css/WorkflowAnalysis.css"; 4 | 5 | // Minimum ratio of successes 6 | const THRESHOLD = 0.1; 7 | 8 | interface Props { 9 | statusses: string[]; 10 | } 11 | 12 | function WorkflowAnalysis(props: Props) { 13 | var occurrences: { [key: string]: number } = {}; 14 | for (var i = 0, j = props.statusses.length; i < j; i++) { 15 | occurrences[props.statusses[i]] = 16 | (occurrences[props.statusses[i]] || 0) + 1; 17 | } 18 | const successes = 19 | props.statusses.length > 0 && occurrences["success"] 20 | ? occurrences["success"] / props.statusses.length 21 | : 0.0; 22 | const failures = 23 | props.statusses.length > 0 && occurrences["failure"] 24 | ? occurrences["failure"] / props.statusses.length 25 | : 0.0; 26 | const other = 1.0 - successes - failures; 27 | 28 | const progressBar = ( 29 | <> 30 | <ProgressBar className="workflow-progressbar"> 31 | <ProgressBar variant="success" now={successes * 100.0} key={1} /> 32 | <ProgressBar variant="warning" now={other * 100.0} key={2} /> 33 | <ProgressBar variant="danger" now={failures * 100.0} key={3} /> 34 | </ProgressBar> 35 | <div className="legend d-flex justify-content-center"> 36 | <Badge bg="success" className="workflow-legend"> 37 | Successful 38 | </Badge> 39 | <Badge bg="warning" className="workflow-legend"> 40 | Other 41 | </Badge> 42 | <Badge bg="danger" className="workflow-legend"> 43 | Fails 44 | </Badge> 45 | </div> 46 | </> 47 | ); 48 | 49 | const unknownProgressBar = ( 50 | <ProgressBar className="workflow-progressbar"> 51 | <ProgressBar 52 | variant="secondary" 53 | now={100.0} 54 | key={1} 55 | label={"No data available"} 56 | /> 57 | </ProgressBar> 58 | ); 59 | 60 | const workflowSuggestion = 61 | failures > THRESHOLD ? ( 62 | <Alert variant="warning"> 63 | Your builds are failing quite often {"(> 10%)"}, please consider running 64 | them locally before pushing! 65 | </Alert> 66 | ) : ( 67 | <Alert variant="success"> 68 | Your builds are quite successful. Running locally before pushing saves 69 | energy, so good job! 70 | </Alert> 71 | ); 72 | 73 | return ( 74 | <> 75 | <p>Let's see how often builds are failing.. 🔭 </p> 76 | {props.statusses.length > 0 ? progressBar : unknownProgressBar} 77 | <p> 78 | <b>Why?</b> Running workflows locally before executing them remotely is 79 | beneficial for saving on energy usage: executing workflows remotely 80 | involves utilizing resources such as servers and data centers that 81 | require a significant amount of energy compared to running them locally. 82 | </p> 83 | {props.statusses.length > 0 && workflowSuggestion} 84 | </> 85 | ); 86 | } 87 | 88 | export default WorkflowAnalysis; 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <p align="center"> 2 | <img src='public/susie.svg' style="display: block; 3 | margin-left: auto; 4 | margin-right: auto; 5 | width: 75px;"> 6 | </p> 7 | 8 | 9 | <p align="center"> 10 | <img src='https://img.shields.io/badge/version-v1.0.0-yellow'> 11 | <img src='https://img.shields.io/badge/release%20date-april%202023-red'> 12 | <img src='https://img.shields.io/website-up-down-green-red/http/philippedeb.github.io/susie/.svg'> 13 | </p> 14 | 15 | <p align="center"> 16 | <img src='https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white'> 17 | <img src='https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB'> 18 | <img src='https://img.shields.io/badge/bootstrap-%23563D7C.svg?style=for-the-badge&logo=bootstrap&logoColor=white'> 19 | <img src='https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white'> 20 | <img src='https://img.shields.io/badge/github%20pages-121013?style=for-the-badge&logo=github&logoColor=white'> 21 | 22 | </p> 23 | 24 | **Susie** is a <a href="https://philippedeb.github.io/susie/" target="_blank" rel="noopener noreferrer">website</a> aimed at Sustainable Software Development and consists of: 25 | 26 | * **Analysis Tool 🔍** - Check the sustainability of a GitHub repository 27 | * **Guides 📰** - Learn more about sustainable software development 28 | 29 | 30 | 31 | ## Installation 32 | 33 | You should have [Node.js](https://nodejs.org/en/), [npm](https://www.npmjs.com/) and [git](https://git-scm.com/) installed on your machine. 34 | 35 | 1. Clone the repository to your local machine. 36 | ```bash 37 | git clone https://github.com/philippedeb/susie.git 38 | ``` 39 | 40 | 41 | 2. Navigate to the project directory. 42 | ```bash 43 | cd your-repository 44 | ``` 45 | 46 | 3. Install the dependencies using [`pnpm`](https://www.npmjs.com/package/pnpm) ([faster and more efficient](https://www.npmjs.com/package/pnpm) compared to `npm`). 47 | ```bash 48 | npm i -g pnpm 49 | pnpm i 50 | ``` 51 | 52 | 4. Run the project. 53 | ```bash 54 | pnpm run dev 55 | ``` 56 | 57 | ## Contributing 58 | 59 | Contributions are always welcome! 👋 60 | 61 | Read the [contribution guidelines](CONTRIBUTING.md) and [code of conduct](CODE_OF_CONDUCT.md). 62 | 63 | If you like the project, but just don’t have time to contribute, that’s fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 64 | 65 | * ⭐ Star the project 66 | * ✉️ Post about it (e.g. Twitter, LinkedIn, Reddit, etc.) 67 | * 🔗 Refer this project in your project’s readme 68 | * 💬 Mention the project at local meetups and tell your friends/colleagues 69 | 70 | > Note: early contributions of the maintainers did not follow the [contribution guidelines](CONTRIBUTING.md) due to a rushed production environment (Susie started as a university project!). We are working on improving the codebase and will be following these guidelines from now on. 71 | 72 | ## License 73 | This project is licensed under the terms of the **[GNU AGPLv3](LICENSE)** license. 74 | -------------------------------------------------------------------------------- /src/components/structure/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Form, 3 | FormControl, 4 | Button, 5 | OverlayTrigger, 6 | Tooltip, 7 | Alert, 8 | InputGroup, 9 | } from "react-bootstrap"; 10 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 11 | import { faSearch } from "@fortawesome/free-solid-svg-icons"; 12 | import { useState, useEffect } from "react"; 13 | import "../../css/fade-in.css"; 14 | 15 | interface Props { 16 | onSearch: (value: string) => void; 17 | } 18 | 19 | function SearchBar(props: Props) { 20 | const [searchValue, setSearchValue] = useState(""); 21 | const [showWarning, setShowWarning] = useState(false); 22 | const [isMobile, setIsMobile] = useState(false); 23 | 24 | useEffect(() => { 25 | function handleResize() { 26 | setIsMobile(window.innerWidth <= 800); 27 | } 28 | window.addEventListener("resize", handleResize); 29 | handleResize(); 30 | return () => window.removeEventListener("resize", handleResize); 31 | }, []); 32 | 33 | function handleSearch() { 34 | if (isValidUrl(searchValue)) { 35 | if (isMobile && document.activeElement instanceof HTMLElement) { 36 | // Check if a mobile device and the keyboard is still open 37 | document.activeElement.blur(); 38 | } else { 39 | props.onSearch(searchValue); 40 | } 41 | } else { 42 | setShowWarning(true); 43 | } 44 | } 45 | 46 | function handleChange(event: React.ChangeEvent<HTMLInputElement>) { 47 | const value = event.target.value; 48 | setSearchValue(value); 49 | } 50 | 51 | function isValidUrl(url: string) { 52 | const regex = 53 | /^https:\/\/github.com\/[a-z\d](?:[a-z\d]|-(?=[a-z\d])){1,39}\/[a-z\d_.-]{1,256}$/i; 54 | return regex.test(url); 55 | } 56 | 57 | const formInput = isMobile 58 | ? "Enter Github Repository URL" 59 | : "Enter GitHub repository URL here"; 60 | 61 | return ( 62 | <> 63 | <InputGroup className={`mb-3 ${isMobile ? "w-100" : "w-75"}`}> 64 | <Form.Control 65 | type="text" 66 | size="sm" 67 | placeholder={formInput} 68 | aria-label={formInput} 69 | aria-describedby="basic-addon2" 70 | value={searchValue} 71 | onChange={handleChange} 72 | onKeyDown={(event) => { 73 | if (event.key === "Enter") { 74 | event.preventDefault(); 75 | props.onSearch(searchValue); 76 | } 77 | }} 78 | /> 79 | <Button 80 | variant="secondary" 81 | id="button-addon2" 82 | onClick={handleSearch} 83 | onTouchEnd={handleSearch} 84 | > 85 | <FontAwesomeIcon icon={faSearch} /> 86 | </Button> 87 | </InputGroup> 88 | <div className={`mt-2 ${isMobile ? "w-100" : "w-75"}`}> 89 | {showWarning && ( 90 | <Alert 91 | variant="warning" 92 | className="fade-in-up" 93 | dismissible 94 | onClose={() => setShowWarning(false)} 95 | > 96 | The URL must be in the format https://www.github.com/owner/repo. 97 | </Alert> 98 | )} 99 | </div> 100 | </> 101 | ); 102 | } 103 | 104 | export default SearchBar; 105 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | <p align="center"> 3 | <img src='public/susie.svg' style="display: block; 4 | margin-left: auto; 5 | margin-right: auto; 6 | width: 5%;"> 7 | </p> 8 | 9 | ## Code of Conduct 10 | 11 | ### Our Pledge 12 | 13 | In the interest of fostering an open and welcoming environment, we as 14 | contributors and maintainers pledge to making participation in our project and 15 | our community a harassment-free experience for everyone, regardless of age, body 16 | size, disability, ethnicity, gender identity and expression, level of experience, 17 | nationality, personal appearance, race, religion, or sexual identity and 18 | orientation. 19 | 20 | ### Our Standards 21 | 22 | Examples of behavior that contributes to creating a positive environment 23 | include: 24 | 25 | * Using welcoming and inclusive language 26 | * Being respectful of differing viewpoints and experiences 27 | * Gracefully accepting constructive criticism 28 | * Focusing on what is best for the community 29 | * Showing empathy towards other community members 30 | 31 | Examples of unacceptable behavior by participants include: 32 | 33 | * The use of sexualized language or imagery and unwelcome sexual attention or 34 | advances 35 | * Trolling, insulting/derogatory comments, and personal or political attacks 36 | * Public or private harassment 37 | * Publishing others' private information, such as a physical or electronic 38 | address, without explicit permission 39 | * Other conduct which could reasonably be considered inappropriate in a 40 | professional setting 41 | 42 | ### Our Responsibilities 43 | 44 | Project maintainers are responsible for clarifying the standards of acceptable 45 | behavior and are expected to take appropriate and fair corrective action in 46 | response to any instances of unacceptable behavior. 47 | 48 | Project maintainers have the right and responsibility to remove, edit, or 49 | reject comments, commits, code, wiki edits, issues, and other contributions 50 | that are not aligned to this Code of Conduct, or to ban temporarily or 51 | permanently any contributor for other behaviors that they deem inappropriate, 52 | threatening, offensive, or harmful. 53 | 54 | ### Scope 55 | 56 | This Code of Conduct applies both within project spaces and in public spaces 57 | when an individual is representing the project or its community. Examples of 58 | representing a project or community include using an official project e-mail 59 | address, posting via an official social media account, or acting as an appointed 60 | representative at an online or offline event. Representation of a project may be 61 | further defined and clarified by project maintainers. 62 | 63 | ### Enforcement 64 | 65 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 66 | reported by contacting the project team. All 67 | complaints will be reviewed and investigated and will result in a response that 68 | is deemed necessary and appropriate to the circumstances. The project team is 69 | obligated to maintain confidentiality with regard to the reporter of an incident. 70 | Further details of specific enforcement policies may be posted separately. 71 | 72 | Project maintainers who do not follow or enforce the Code of Conduct in good 73 | faith may face temporary or permanent repercussions as determined by other 74 | members of the project's leadership. 75 | 76 | ### Attribution 77 | 78 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 79 | available at [http://contributor-covenant.org/version/1/4][version] 80 | 81 | [homepage]: http://contributor-covenant.org 82 | [version]: http://contributor-covenant.org/version/1/4/ 83 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> -------------------------------------------------------------------------------- /src/components/metrics/Contributors/Contributors.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { 3 | getPonyFactor, 4 | getContributionCounts, 5 | getTopContributorPower, 6 | } from "./ContributorLogic"; 7 | import ContributorPiechart from "./ContributorPiechart"; 8 | import DropDown from "../../structure/DropDown"; 9 | import { Badge } from "react-bootstrap"; 10 | 11 | interface Props { 12 | commitAuthorDates: [string, string][]; 13 | } 14 | 15 | function Contributors(props: Props) { 16 | const [contributors, setContributors] = useState<{ [key: string]: number }>( 17 | {} 18 | ); 19 | const [ponyFactor, setPonyFactor] = useState<number>(0); 20 | const [topContributorPower, setTopContributorPower] = useState<number>(0); 21 | 22 | useEffect(() => { 23 | async function fetchData() { 24 | const contributors: { [key: string]: number } = getContributionCounts( 25 | props.commitAuthorDates 26 | ); 27 | setContributors(contributors); 28 | 29 | const ponyFactor: number = getPonyFactor(props.commitAuthorDates); 30 | setPonyFactor(ponyFactor); 31 | 32 | const topContributorPower: number = getTopContributorPower( 33 | props.commitAuthorDates 34 | ); 35 | setTopContributorPower(topContributorPower); 36 | } 37 | 38 | fetchData(); 39 | }, [props.commitAuthorDates]); 40 | 41 | return ( 42 | <> 43 | <div> 44 | <ContributorPiechart 45 | commitAuthorDates={contributors} 46 | ></ContributorPiechart> 47 | </div> 48 | 49 | <hr /> 50 | <h5>Bus Factor 🚌</h5> 51 | <div> 52 | The bus factor (or truck factor) of a project is the number of team 53 | members whose absence would jeopardize the project ( 54 | <i>hypothetically speaking</i>, hit by a bus). The smallest bus factor 55 | is 1 and any low number represents a crucial point of failure within the 56 | team, thus, larger values are preferred. And of course, buses aren't 57 | usually the biggest threat to teams: illness, vacation, and departure 58 | from the company are all frequent occurrences on projects. Therefore, 59 | efforts should be made to increase the bus factor on any project that is 60 | critical to the organization. 61 | <br /> 62 | <br /> 63 | <Badge bg="secondary">Note</Badge> There are different ways to calculate 64 | the bus factor, click on them to learn more. 65 | </div> 66 | <DropDown header={`🐴 Pony factor: ${ponyFactor}`} collapsed={true}> 67 | <p> 68 | The pony factor is the number of top contributors covering 50% or more 69 | of all time contributions ( 70 | <a 71 | href="https://humbedooh.com/Chapter%203,%20part%20one_%20Codebase%20development%20resilience.pdf" 72 | className="susie-link" 73 | target="_blank" 74 | rel="noopener noreferrer" 75 | > 76 | source 77 | </a> 78 | ). 79 | </p> 80 | </DropDown> 81 | <DropDown 82 | header={`⚡Top contributor power: ${topContributorPower.toFixed(1)}`} 83 | collapsed={true} 84 | > 85 | <p>Number of contributors as the factor of the biggest contributor.</p> 86 | </DropDown> 87 | <DropDown header={`📂 Ownership detection: -`} collapsed={true}> 88 | <p> 89 | Detect file ownership, for example, by looking at developer aliases 90 | and trace change history ( 91 | <a 92 | href="https://arxiv.org/pdf/1604.06766.pdf" 93 | className="susie-link" 94 | target="_blank" 95 | rel="noopener noreferrer" 96 | > 97 | source 98 | </a> 99 | ). Help us implement this by contributing to our{" "} 100 | <a 101 | href="https://github.com/philippedeb/susie" 102 | className="susie-link" 103 | target="_blank" 104 | rel="noopener noreferrer" 105 | > 106 | open-source repository 107 | </a> 108 | ! 109 | </p> 110 | </DropDown> 111 | </> 112 | ); 113 | } 114 | 115 | export default Contributors; 116 | -------------------------------------------------------------------------------- /src/components/metrics/Inclusivity/Inclusive.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useEffect, useState } from "react"; 2 | import { Alert, Badge, Spinner } from "react-bootstrap"; 3 | import { 4 | checkLanguage, 5 | Recommendations, 6 | TermRecommendation, 7 | } from "./checkLanguage"; 8 | import Recommendation from "./Recommendation"; 9 | import "../../../css/Link.css"; 10 | 11 | interface Props { 12 | data: [string, string][]; 13 | } 14 | 15 | function inclusiveStatement(children: React.ReactNode): ReactElement { 16 | return ( 17 | <div> 18 | <p> 19 | {" "} 20 | Inclusive language is language that is free from words, phrases or tones 21 | that reflect prejudiced, stereotyped or discriminatory views of 22 | particular people or groups. It is also language that does not 23 | deliberately or inadvertently exclude people from being seen as part of 24 | a group. 25 | </p> 26 | <p> 27 | Susie checks your repository for common terms that may be considered as 28 | not inclusive, though it is not an exhaustive list. To learn more about 29 | inclusive language and this metric, please visit{" "} 30 | <a 31 | className="susie-link" 32 | href="./#/guide?name=inclusive-language" 33 | target="_blank" 34 | rel="noopener noreferrer" 35 | > 36 | our guide 37 | </a> 38 | . 39 | </p> 40 | {children} 41 | </div> 42 | ); 43 | } 44 | 45 | function Inclusive(props: Props) { 46 | const [recommendations, setRecommendations] = useState<Recommendations>({ 47 | terms: {}, 48 | profanity_locations: [], 49 | }); 50 | const [number_of_recommendations, setNumberOfRecommendations] = 51 | useState<number>(0); 52 | const [showAlert, setShowAlert] = useState<boolean>(false); 53 | const [isLoading, setIsLoading] = useState<boolean>(true); 54 | 55 | useEffect(() => { 56 | async function fetchData() { 57 | // Get recommendations from checkLanguage function 58 | const recommendations: Recommendations = checkLanguage(props.data); 59 | setRecommendations(recommendations); 60 | 61 | // Show alert if there are no recommendations 62 | setNumberOfRecommendations( 63 | Object.keys(recommendations.terms).length + 64 | (recommendations.profanity_locations.length > 0 ? 1 : 0) 65 | ); 66 | setShowAlert(number_of_recommendations === 0); 67 | 68 | // Stop loading symbol after recommendations are fetched 69 | setIsLoading(false); 70 | } 71 | fetchData(); 72 | }, [props.data]); 73 | 74 | return ( 75 | <div> 76 | {isLoading ? ( 77 | <div className="d-flex justify-content-center my-4"> 78 | <Spinner animation="border" variant="primary" /> 79 | </div> 80 | ) : number_of_recommendations > 0 ? ( 81 | inclusiveStatement( 82 | <div> 83 | <h5> 84 | {"Results "}{" "} 85 | <Badge bg="success">{number_of_recommendations}</Badge> 86 | </h5> 87 | <p> 88 | We found the following terms that may be considered as not 89 | inclusive and have provided some suggestions for replacements. 90 | Click on a term to see where it was found. 91 | </p> 92 | {Object.entries(recommendations.terms).map( 93 | ([title, { replacements, locations }]) => ( 94 | <Recommendation 95 | key={title} 96 | title={title} 97 | replacements={replacements} 98 | locations={locations} 99 | /> 100 | ) 101 | )} 102 | {recommendations.profanity_locations.length > 0 && ( 103 | <Recommendation 104 | key={"Profanity"} 105 | title={"Profane language 😮"} 106 | replacements={[]} 107 | locations={recommendations.profanity_locations} 108 | /> 109 | )} 110 | </div> 111 | ) 112 | ) : ( 113 | inclusiveStatement( 114 | <Alert show={showAlert} variant="success"> 115 | Based on our analysis, your repository is inclusive! 💚 116 | </Alert> 117 | ) 118 | )} 119 | </div> 120 | ); 121 | } 122 | 123 | export default Inclusive; 124 | -------------------------------------------------------------------------------- /src/components/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Button, Container } from "react-bootstrap"; 3 | import SearchBar from "../structure/SearchBar"; 4 | import "../../css/fade-in.css"; 5 | import { useNavigate } from "react-router-dom"; 6 | 7 | function Home() { 8 | const [loaded, setLoaded] = useState(false); 9 | const [isMobile, setIsMobile] = useState(false); 10 | 11 | useEffect(() => { 12 | function handleResize() { 13 | setIsMobile(window.innerWidth <= 800); 14 | } 15 | window.addEventListener("resize", handleResize); 16 | handleResize(); 17 | return () => window.removeEventListener("resize", handleResize); 18 | }, []); 19 | 20 | const navigate = useNavigate(); 21 | 22 | useEffect(() => { 23 | setLoaded(true); 24 | }, []); 25 | 26 | const handleSubmit = (value: string) => { 27 | navigate(`./dashboard?search=${value}`); 28 | }; 29 | 30 | return ( 31 | <div 32 | className={isMobile ? "mt-3" : "mt-5"} 33 | style={{ 34 | marginTop: "50px", 35 | paddingLeft: "20px", 36 | paddingRight: "20px", 37 | }} 38 | > 39 | <Container 40 | className={ 41 | "d-flex flex-column justify-content-center align-items-center " + 42 | isMobile 43 | ? "mb-4" 44 | : "mb-5" 45 | } 46 | > 47 | <h1 48 | className="text-center" 49 | style={{ 50 | fontSize: isMobile ? "50px" : "60px", 51 | fontFamily: "Playfair Display, serif", 52 | fontWeight: 900, 53 | }} 54 | > 55 | Susie. 56 | </h1> 57 | <h2 58 | className="text-center" 59 | style={{ 60 | fontSize: isMobile ? "28px" : "40px", 61 | fontFamily: "Playfair Display, serif", 62 | fontWeight: "light", 63 | }} 64 | > 65 | Sustainable software development. 66 | </h2> 67 | </Container> 68 | <Container 69 | className={`d-flex flex-column justify-content-center align-items-center${ 70 | loaded ? " fade-in-down" : "" 71 | }`} 72 | style={{ 73 | backgroundColor: "#4f4d4a", 74 | paddingTop: "20px", 75 | paddingBottom: "10px", 76 | paddingLeft: "20px", 77 | paddingRight: "20px", 78 | borderRadius: "15px", 79 | }} 80 | > 81 | <h3 82 | className="text-center font-weight-bold mb-2" 83 | style={{ 84 | fontSize: "24px", 85 | fontWeight: 700, 86 | color: "#ccc6be", 87 | }} 88 | > 89 | Analysis Tool 🔍 90 | </h3> 91 | <p 92 | className="text-center font-weight-bold mb-4" 93 | style={{ 94 | fontSize: isMobile ? "16px" : "18px", 95 | }} 96 | > 97 | {isMobile 98 | ? "How sustainable is your repository?" 99 | : "How sustainable is your GitHub repository?"} 100 | </p> 101 | <SearchBar onSearch={handleSubmit} /> 102 | </Container> 103 | <Container 104 | className={`d-flex flex-column justify-content-center align-items-center mt-4${ 105 | loaded ? " fade-in-down" : "" 106 | }`} 107 | style={{ 108 | backgroundColor: "#444a46", 109 | paddingTop: "20px", 110 | paddingBottom: "10px", 111 | paddingLeft: "20px", 112 | paddingRight: "20px", 113 | borderRadius: "15px", 114 | }} 115 | > 116 | <h3 117 | className="text-center font-weight-bold mb-2" 118 | style={{ 119 | fontSize: "24px", 120 | fontWeight: 700, 121 | color: "#c4ccbe", 122 | }} 123 | > 124 | Guides 📰 125 | </h3> 126 | <p 127 | className={`text-center font-weight-bold ${ 128 | isMobile ? "mb-3" : "mb-4" 129 | }`} 130 | style={{ 131 | fontSize: isMobile ? "16px" : "18px", 132 | }} 133 | > 134 | {isMobile 135 | ? "Learn more about sustainability!" 136 | : "Perhaps you want to learn more about sustainability? Look no further!"} 137 | </p> 138 | <Button 139 | variant="outline-light" 140 | className="mb-3" 141 | onClick={() => navigate("./guides")} 142 | > 143 | View Guides 144 | </Button> 145 | </Container> 146 | </div> 147 | ); 148 | } 149 | 150 | export default Home; 151 | -------------------------------------------------------------------------------- /src/components/pages/Guides/TypesOfSustainabilityGuide.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Col, Container, Row, Table } from "react-bootstrap"; 3 | import "../../../css/GuidePage.css"; 4 | import "../../../css/Link.css"; 5 | import Section from "../../structure/Section"; 6 | 7 | function TypesOfSustainabilityGuide() { 8 | return ( 9 | <Container className="guide-page-container"> 10 | <Row> 11 | <Col> 12 | <h3>The Different Types of Sustainability in Software Development</h3> 13 | <p> 14 | When we talk about sustainability, we usually think about 15 | environmental sustainability - and for good reason! But did you know 16 | there are actually four more forms of sustainability that are just 17 | as important? Let's dive into the five different types of 18 | sustainability: 19 | </p> 20 | <Section title="Environmental Sustainability 🌍"> 21 | <p> 22 | In software development, environmental sustainability is all about 23 | reducing the carbon footprint of our digital systems. This can 24 | include optimizing code to reduce energy consumption, using 25 | renewable energy to power data centers, and designing systems that 26 | can scale sustainably. By practicing environmental sustainability 27 | in software development, we can help ensure a healthy planet for 28 | generations to come. 29 | </p> 30 | </Section> 31 | <Section title="Social Sustainability 🌆"> 32 | <p> 33 | Social sustainability in software development focuses on creating 34 | systems that are accessible and inclusive for all users. This 35 | includes promoting diversity and inclusion in the tech industry, 36 | designing user interfaces that are easy to use for people with 37 | different abilities, and ensuring that digital services are 38 | available to people regardless of their socio-economic status. 39 | Social sustainability also involves creating safe and healthy 40 | online communities where people can thrive. 41 | </p> 42 | </Section> 43 | <Section title="Individual Sustainability 🪷"> 44 | <p> 45 | Individual sustainability in software development is all about 46 | taking care of the people who create and maintain our digital 47 | systems. It's about promoting work-life balance, managing stress, 48 | and prioritizing mental and physical health. This can involve 49 | practices like mindfulness, meditation, and exercise. By focusing 50 | on individual sustainability, we can create a more resilient and 51 | productive workforce that is better equipped to tackle the 52 | challenges of sustainable software development. 53 | </p> 54 | </Section> 55 | <Section title="Economic Sustainability 💰"> 56 | <p> 57 | Economic sustainability in software development is about creating 58 | business models that are financially viable in the long term, 59 | while also considering the impact on society and the environment. 60 | This can involve implementing sustainable procurement practices, 61 | promoting fair labor standards in the tech industry, and ensuring 62 | that digital systems are designed to support local economies. By 63 | practicing economic sustainability, we can create a more equitable 64 | and sustainable tech industry that benefits everyone. 65 | </p> 66 | </Section> 67 | <Section title="Technical Sustainability 🛠️"> 68 | <p> 69 | Technical sustainability in software development is all about 70 | maintaining the digital infrastructure and systems that support 71 | our daily lives. This can include everything from optimizing code 72 | for performance and reliability to designing systems that can 73 | adapt to changing needs over time. By practicing technical 74 | sustainability, we can ensure that our digital systems are 75 | reliable, efficient, and resilient, while also reducing the 76 | environmental impact of our digital footprint. 77 | </p> 78 | </Section> 79 | 80 | <p> 81 | By understanding and practicing all five types of sustainability in 82 | the context of software development, we can create a more resilient, 83 | equitable, and sustainable tech industry for everyone. 84 | </p> 85 | </Col> 86 | </Row> 87 | </Container> 88 | ); 89 | } 90 | 91 | export default TypesOfSustainabilityGuide; 92 | -------------------------------------------------------------------------------- /src/components/pages/Guides/EnergyConsumptionGuide.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Col, Container, Row, Table } from "react-bootstrap"; 3 | import "../../../css/GuidePage.css"; 4 | 5 | function EnergyConsumptionGuide() { 6 | return ( 7 | <Container className="guide-page-container"> 8 | <Row> 9 | <Col> 10 | <h3>Energy Efficient Software Development</h3> 11 | <p> 12 | Programming languages can vary significantly in terms of their 13 | energy consumption. As such, understanding the energy consumption of 14 | programming languages is crucial to reduce the carbon footprint and 15 | promote sustainable practices in software development. In this 16 | table, an overview of the energy consumption scores for different 17 | programming languages is provided. 18 | <br /> <br /> 19 | Susie uses these scores to give advice on the programming language 20 | usage in a repository. Hopefully, you can also learn something from 21 | it and create energy efficient projects in the future! 🔮 22 | <br /> 23 | </p> 24 | <Table className="table-dark table-bordered table-striped"> 25 | <thead> 26 | <tr> 27 | <th>Programming Language</th> 28 | <th>Energy Consumption Score</th> 29 | </tr> 30 | </thead> 31 | <tbody> 32 | <tr> 33 | <td>C</td> 34 | <td>1.00</td> 35 | </tr> 36 | <tr> 37 | <td>Rust</td> 38 | <td>1.03</td> 39 | </tr> 40 | <tr> 41 | <td>C++</td> 42 | <td>1.34</td> 43 | </tr> 44 | <tr> 45 | <td>Ada</td> 46 | <td>1.70</td> 47 | </tr> 48 | <tr> 49 | <td>Java</td> 50 | <td>1.98</td> 51 | </tr> 52 | <tr> 53 | <td>Pascal</td> 54 | <td>2.14</td> 55 | </tr> 56 | <tr> 57 | <td>Chapel</td> 58 | <td>2.18</td> 59 | </tr> 60 | <tr> 61 | <td>Lisp</td> 62 | <td>2.27</td> 63 | </tr> 64 | <tr> 65 | <td>Ocaml</td> 66 | <td>2.40</td> 67 | </tr> 68 | <tr> 69 | <td>Fortran</td> 70 | <td>2.52</td> 71 | </tr> 72 | <tr> 73 | <td>Swift</td> 74 | <td>2.79</td> 75 | </tr> 76 | <tr> 77 | <td>Haskell</td> 78 | <td>3.10</td> 79 | </tr> 80 | <tr> 81 | <td>C#</td> 82 | <td>3.14</td> 83 | </tr> 84 | <tr> 85 | <td>Go</td> 86 | <td>3.23</td> 87 | </tr> 88 | <tr> 89 | <td>Dart</td> 90 | <td>3.83</td> 91 | </tr> 92 | <tr> 93 | <td>F#</td> 94 | <td>4.13</td> 95 | </tr> 96 | <tr> 97 | <td>JavaScript</td> 98 | <td>4.45</td> 99 | </tr> 100 | <tr> 101 | <td>Racket</td> 102 | <td>7.91</td> 103 | </tr> 104 | <tr> 105 | <td>TypeScript</td> 106 | <td>21.50</td> 107 | </tr> 108 | <tr> 109 | <td>Hack</td> 110 | <td>24.02</td> 111 | </tr> 112 | <tr> 113 | <td>PHP</td> 114 | <td>29.30</td> 115 | </tr> 116 | <tr> 117 | <td>Erlang</td> 118 | <td>42.23</td> 119 | </tr> 120 | <tr> 121 | <td>Lua</td> 122 | <td>45.98</td> 123 | </tr> 124 | <tr> 125 | <td>Jruby</td> 126 | <td>46.54</td> 127 | </tr> 128 | <tr> 129 | <td>Ruby</td> 130 | <td>69.91</td> 131 | </tr> 132 | <tr> 133 | <td>Python</td> 134 | <td>75.88</td> 135 | </tr> 136 | <tr> 137 | <td>Perl</td> 138 | <td>79.58</td> 139 | </tr> 140 | </tbody> 141 | </Table> 142 | <p> 143 | <b> Source: </b> 144 | <a 145 | href="https://greenlab.di.uminho.pt/wp-content/uploads/2017/10/sleFinal.pdf." 146 | className="susie-link-light" 147 | target="_blank" 148 | rel="noopener noreferrer" 149 | > 150 | {" "} 151 | https://greenlab.di.uminho.pt/wp-content/uploads/2017/10/sleFinal.pdf.{" "} 152 | </a> 153 | </p> 154 | </Col> 155 | </Row> 156 | </Container> 157 | ); 158 | } 159 | 160 | export default EnergyConsumptionGuide; 161 | -------------------------------------------------------------------------------- /src/components/metrics/Inclusivity/checkLanguage.ts: -------------------------------------------------------------------------------- 1 | export { checkLanguage, type Recommendations, type TermRecommendation }; 2 | 3 | import Filter from "bad-words"; 4 | const filter = new Filter(); 5 | 6 | const inclusiveLanguageChecks = [ 7 | { word: "master", replacements: ["main", "leader", "primary"] }, 8 | { word: "slave", replacements: ["follower", "replica", "secondary"] }, 9 | { 10 | word: "whitelist", 11 | replacements: ["allow list", "inclusion list", "safe list"], 12 | }, 13 | { 14 | word: "blacklist", 15 | replacements: ["deny list", "exclusion list", "block list", "banned list"], 16 | }, 17 | { 18 | word: "man hours", 19 | replacements: [ 20 | "labor hours", 21 | "work hours", 22 | "person hours", 23 | "engineer hours", 24 | ], 25 | }, 26 | { word: "manpower", replacements: ["labor", "workforce"] }, 27 | { word: "guys", replacements: ["folks", "people", "you all"] }, 28 | { word: "girl", replacements: ["woman"] }, 29 | { word: "girls", replacements: ["women"] }, 30 | { word: "middleman", replacements: ["middle person", "mediator", "liaison"] }, 31 | { word: "he", replacements: ["they"] }, 32 | { word: "she", replacements: ["they"] }, 33 | { word: "him", replacements: ["them"] }, 34 | { word: "her", replacements: ["them"] }, 35 | { word: "his", replacements: ["theirs"] }, 36 | { word: "hers", replacements: ["theirs"] }, 37 | { word: "crazy", replacements: ["unpredictable", "unexpected"] }, 38 | { word: "insane", replacements: ["unpredictable", "unexpected"] }, 39 | { word: "normal", replacements: ["typical"] }, 40 | { word: "abnormal", replacements: ["atypical"] }, 41 | { 42 | word: "grandfather", 43 | replacements: ["flagship", "established", "rollover", "carryover"], 44 | }, 45 | { 46 | word: "grandfathering", 47 | replacements: ["flagship", "established", "rollover", "carryover"], 48 | }, 49 | { 50 | word: "legacy", 51 | replacements: ["flagship", "established", "rollover", "carryover"], 52 | }, 53 | { 54 | word: "crushing it", 55 | replacements: ["elevating", "exceeding expectations", "excelling"], 56 | }, 57 | { 58 | word: "killing it", 59 | replacements: ["elevating", "exceeding expectations", "excelling"], 60 | }, 61 | { word: "owner", replacements: ["lead", "manager", "expert"] }, 62 | { 63 | word: "sanity check", 64 | replacements: ["quick check", "confidence check", "coherence check"], 65 | }, 66 | { word: "dummy value", replacements: ["placeholder value", "sample value"] }, 67 | { 68 | word: "native feature", 69 | replacements: ["core feature", "built-in feature"], 70 | }, 71 | { word: "culture fit", replacements: ["values fit"] }, 72 | { word: "housekeeping", replacements: ["cleanup", "maintenance"] }, 73 | { word: "sanity", replacements: ["confidence", "coherence"] }, 74 | { word: "dummy", replacements: ["placeholder", "sample"] }, 75 | // https://www.aihr.com/blog/lgbtq-inclusive-language-in-the-workplace/ 76 | { word: "chairman", replacements: ["chair", "chairperson"] }, 77 | { word: "mailman", replacements: ["mail carrier", "mail clerk"] }, 78 | { word: "salesman", replacements: ["salesperson"] }, 79 | { word: "salesmen", replacements: ["salespeople"] }, 80 | { word: "saleswoman", replacements: ["salesperson"] }, 81 | { word: "saleswomen", replacements: ["salespeople"] }, 82 | { word: "spokesman", replacements: ["spokesperson"] }, 83 | { word: "spokesmen", replacements: ["spokespeople"] }, 84 | { word: "spokeswoman", replacements: ["spokesperson"] }, 85 | { word: "spokeswomen", replacements: ["spokespeople"] }, 86 | { word: "stewardess", replacements: ["flight attendant"] }, 87 | { word: "stewardesses", replacements: ["flight attendants"] }, 88 | { word: "policeman", replacements: ["police officer"] }, 89 | { word: "policemen", replacements: ["police officers"] }, 90 | { word: "policewoman", replacements: ["police officer"] }, 91 | { word: "policewomen", replacements: ["police officers"] }, 92 | { word: "fireman", replacements: ["firefighter"] }, 93 | { word: "firemen", replacements: ["firefighters"] }, 94 | { word: "firewoman", replacements: ["firefighter"] }, 95 | { word: "firewomen", replacements: ["firefighters"] }, 96 | ]; 97 | 98 | interface Recommendations { 99 | terms: { [key: string]: TermRecommendation }; 100 | profanity_locations: [string, string][]; 101 | } 102 | 103 | interface TermRecommendation { 104 | replacements: string[]; 105 | locations: [string, string][]; 106 | } 107 | 108 | function checkLanguage(data: [string, string][]): Recommendations { 109 | const recommendations: Recommendations = { 110 | terms: {}, 111 | profanity_locations: [], 112 | }; 113 | 114 | for (const [type_item, item] of data) { 115 | // Check if the item contains any of the target words 116 | for (const check of inclusiveLanguageChecks) { 117 | const checkRegex = new RegExp("\\b" + check.word + "\\b"); 118 | if (checkRegex.test(item)) { 119 | if (recommendations.terms[check.word]) { 120 | recommendations.terms[check.word].locations.push([type_item, item]); 121 | } else { 122 | recommendations.terms[check.word] = { 123 | replacements: check.replacements, 124 | locations: [[type_item, item]], 125 | }; 126 | } 127 | } 128 | } 129 | // Check if the item contains any profanity 130 | if (filter.isProfane(item)) { 131 | recommendations.profanity_locations.push([type_item, item]); 132 | } 133 | } 134 | return recommendations; 135 | } 136 | -------------------------------------------------------------------------------- /src/components/metrics/Sentiment/IssuesSentiment.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useEffect, useState } from "react"; 2 | import { Alert, Badge, Spinner } from "react-bootstrap"; 3 | import { 4 | getSentiment, 5 | calcAverageSentiment, 6 | getNegativeSentiment, 7 | getPositiveSentiment, 8 | SentenceSentiments, 9 | } from "./analysis"; 10 | import "../../../css/Link.css"; 11 | import Explanation from "./Explanation"; 12 | 13 | interface Props { 14 | data: string[]; 15 | } 16 | 17 | function IssuesSentiment(props: Props) { 18 | const [sentiments, setSentiments] = useState<SentenceSentiments>({ 19 | sentiments: {}, 20 | }); 21 | const [negativeSentiments, setNegativeSentiments] = 22 | useState<SentenceSentiments>({ sentiments: {} }); 23 | const [positiveSentiments, setPositiveSentiments] = 24 | useState<SentenceSentiments>({ sentiments: {} }); 25 | const [averageSentiment, setAverageSentiment] = useState<number>(0); 26 | const [isLoading, setIsLoading] = useState<boolean>(true); 27 | const [showAlert, setShowAlert] = useState<boolean>(false); 28 | 29 | useEffect(() => { 30 | async function fetchData() { 31 | // Get sentiments from analysis function 32 | const sentiments: SentenceSentiments = getSentiment(props.data); 33 | setSentiments(sentiments); 34 | 35 | // Get negative sentiments from analysis function 36 | const negativeSentiments: SentenceSentiments = 37 | getNegativeSentiment(sentiments); 38 | setNegativeSentiments(negativeSentiments); 39 | 40 | // Get positive sentiments from analysis function 41 | const positiveSentiments: SentenceSentiments = 42 | getPositiveSentiment(sentiments); 43 | setPositiveSentiments(positiveSentiments); 44 | 45 | // Show alert if the average sentiment is positive 46 | const averageSentiment = calcAverageSentiment(sentiments); 47 | setAverageSentiment(averageSentiment); 48 | setShowAlert(averageSentiment > 0); 49 | 50 | setIsLoading(false); 51 | } 52 | fetchData(); 53 | }, [props.data]); 54 | 55 | return ( 56 | <div> 57 | {isLoading ? ( 58 | <div className="d-flex justify-content-center my-4"> 59 | <Spinner animation="border" variant="primary" /> 60 | </div> 61 | ) : ( 62 | <div> 63 | <p> 64 | ⚠️ <b>Experimental:</b> This metric is not accurate due to software 65 | developer jargon being substantially different than the context most 66 | sentiment analysis packages consider. Therefore, this metric might 67 | only be relevant for few repositories. Please consider contributing 68 | to our{" "} 69 | <a 70 | className="susie-link" 71 | href="https://github.com/philippedeb/susie" 72 | target="_blank" 73 | rel="noopener noreferrer" 74 | > 75 | open-source repository 76 | </a>{" "} 77 | to improve it. 78 | </p> 79 | <hr /> 80 | <p> 81 | We have analyzed the sentiment in your issues and have found the 82 | following issues that may be considered as positive or negative. 83 | Click on an issue to see the sentiment analysis. ( 84 | <a 85 | className="susie-link" 86 | href="https://www.npmjs.com/package/sentiment" 87 | target="_blank" 88 | rel="noopener noreferrer" 89 | > 90 | Source 91 | </a> 92 | ) 93 | </p> 94 | {/* Create a Badge with the amount of positive sentiments */} 95 | <h5> 96 | {Object.keys(positiveSentiments.sentiments).length > 0 97 | ? "Positive Issues " 98 | : ""} 99 | {Object.keys(positiveSentiments.sentiments).length > 0 ? ( 100 | <Badge bg="success"> 101 | {Object.keys(positiveSentiments.sentiments).length} 102 | </Badge> 103 | ) : ( 104 | "" 105 | )} 106 | </h5> 107 | <p> 108 | {Object.keys(positiveSentiments.sentiments).length > 0 109 | ? "The list below shows the issues that have the highest positive sentiment." 110 | : ""} 111 | </p> 112 | {Object.entries(positiveSentiments.sentiments).map( 113 | ([title, { score, calculation }]) => ( 114 | <Explanation 115 | key={title} 116 | title={title} 117 | score={score} 118 | calculation={calculation} 119 | /> 120 | ) 121 | )} 122 | {/* Create a Badge with the amount of negative sentiments */} 123 | <h5> 124 | {Object.keys(negativeSentiments.sentiments).length > 0 125 | ? "Negative Issues " 126 | : ""} 127 | {Object.keys(negativeSentiments.sentiments).length > 0 ? ( 128 | <Badge bg="danger"> 129 | {Object.keys(negativeSentiments.sentiments).length} 130 | </Badge> 131 | ) : ( 132 | "" 133 | )} 134 | </h5> 135 | <p> 136 | {Object.keys(negativeSentiments.sentiments).length > 0 137 | ? "The list below shows the issues that have the lowest negative sentiment." 138 | : ""} 139 | </p> 140 | {/* Here is the list of all negative sentiments */} 141 | {Object.entries(negativeSentiments.sentiments).map( 142 | ([title, { score, calculation }]) => ( 143 | <Explanation 144 | key={title} 145 | title={title} 146 | score={score} 147 | calculation={calculation} 148 | /> 149 | ) 150 | )} 151 | <Alert show={showAlert} variant="success"> 152 | Based on our analysis, the average sentiment in your issues is 153 | positive! 💚 154 | </Alert> 155 | <Alert show={!showAlert} variant="warning"> 156 | Based on our analysis, the average sentiment in your issues is 157 | negative. 😢 158 | </Alert> 159 | </div> 160 | )} 161 | </div> 162 | ); 163 | } 164 | 165 | export default IssuesSentiment; 166 | -------------------------------------------------------------------------------- /src/components/pages/Guides/InclusiveLanguageGuide.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Col, Container, Row, Table } from "react-bootstrap"; 3 | import "../../../css/GuidePage.css"; 4 | import "../../../css/Link.css"; 5 | 6 | function InclusiveLanguageGuide() { 7 | return ( 8 | <Container className="guide-page-container"> 9 | <Row> 10 | <Col> 11 | <h3>Language in GitHub repositories</h3> 12 | <p> 13 | Inclusive language is crucial in GitHub repositories because it 14 | helps to create a welcoming and respectful environment for all 15 | contributors, regardless of their background or identity. Using 16 | inclusive language means choosing words and phrases that do not 17 | exclude or marginalize certain groups of people. This includes 18 | avoiding the use of gendered pronouns or terms that may be offensive 19 | or insensitive to particular cultures or identities. By using 20 | inclusive language in GitHub repositories, developers can foster a 21 | more diverse and inclusive community that encourages participation 22 | from individuals of all backgrounds, ultimately leading to better 23 | collaboration, innovation, and success of the project. Additionally, 24 | using inclusive language can also help to mitigate the risk of 25 | misunderstandings or conflicts that may arise from insensitive 26 | language use. 27 | </p> 28 | <p> 29 | The table below can be used as a reference for conducting inclusive 30 | language checks in GitHub repositories. It provides examples of 31 | common terms and phrases that may be considered insensitive or 32 | exclusionary to certain groups of people. By using this table as a 33 | guide, individuals can be more mindful of their language choices and 34 | make conscious efforts to use language that is inclusive and 35 | respectful to all. Be aware, this table is not exhaustive (e.g. 36 | cursewords are missing) and there may be other terms and phrases 37 | that are considered insensitive or exclusionary. If you are unsure 38 | about the appropriateness of a particular term or phrase, it is 39 | recommended to consult with individuals who are part of the group 40 | that may be affected by the language. 41 | </p> 42 | <Table className="table-dark table-bordered table-striped"> 43 | <thead> 44 | <tr> 45 | <th>Common Terms/Phrases</th> 46 | <th>Inclusive Suggestions</th> 47 | </tr> 48 | </thead> 49 | <tbody> 50 | <tr> 51 | <td>master/slave</td> 52 | <td>main/replica, leader/follower, primary/secondary</td> 53 | </tr> 54 | <tr> 55 | <td>whitelist</td> 56 | <td>allow list, inclusion list, safe list</td> 57 | </tr> 58 | <tr> 59 | <td>blacklist</td> 60 | <td>deny list, exclusion list, block list, banned list</td> 61 | </tr> 62 | <tr> 63 | <td>man hours</td> 64 | <td>labor hours, work hours, person hours, engineer hours</td> 65 | </tr> 66 | <tr> 67 | <td>manpower</td> 68 | <td>labor, workforce</td> 69 | </tr> 70 | <tr> 71 | <td>guys</td> 72 | <td>folks, people, you all</td> 73 | </tr> 74 | <tr> 75 | <td>girl/girls</td> 76 | <td>woman/women</td> 77 | </tr> 78 | <tr> 79 | <td>middleman</td> 80 | <td>middle person, mediator, liaison</td> 81 | </tr> 82 | <tr> 83 | <td>he/she, him/her, his/hers</td> 84 | <td>they, them, theirs</td> 85 | </tr> 86 | <tr> 87 | <td>crazy/insane</td> 88 | <td>unpredictable, unexpected</td> 89 | </tr> 90 | <tr> 91 | <td>normal/abnormal</td> 92 | <td>typical/atypical</td> 93 | </tr> 94 | <tr> 95 | <td>grandfather</td> 96 | <td>flagship, established, rollover, carryover</td> 97 | </tr> 98 | <tr> 99 | <td>grandfathering/legacy</td> 100 | <td>flagship, established, rollover, carryover</td> 101 | </tr> 102 | <tr> 103 | <td>crushing it/killing it</td> 104 | <td>elevating, exceeding expectations, excelling</td> 105 | </tr> 106 | <tr> 107 | <td>owner</td> 108 | <td>lead, manager, expert</td> 109 | </tr> 110 | <tr> 111 | <td>sanity check</td> 112 | <td>quick check, confidence check, coherence check</td> 113 | </tr> 114 | <tr> 115 | <td>dummy value</td> 116 | <td>placeholder value, sample value</td> 117 | </tr> 118 | <tr> 119 | <td>native feature</td> 120 | <td>core feature, built-in feature</td> 121 | </tr> 122 | <tr> 123 | <td>culture fit</td> 124 | <td>values fit</td> 125 | </tr> 126 | <tr> 127 | <td>housekeeping</td> 128 | <td>cleanup, maintenance</td> 129 | </tr> 130 | </tbody> 131 | </Table> 132 | <p> 133 | <b> Source: </b> 134 | <a 135 | href="https://www.aswf.io/blog/inclusive-language/" 136 | className="susie-link-light" 137 | target="_blank" 138 | rel="noopener noreferrer" 139 | > 140 | {" "} 141 | https://www.aswf.io/blog/inclusive-language/{" "} 142 | </a> 143 | </p> 144 | </Col> 145 | </Row> 146 | </Container> 147 | ); 148 | } 149 | 150 | export default InclusiveLanguageGuide; 151 | -------------------------------------------------------------------------------- /src/components/metrics/Contributors/ContributorPiechart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Pie } from "react-chartjs-2"; 3 | import { Container, Row, Col, Table, Alert } from "react-bootstrap"; 4 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js"; 5 | import DropDown from "../../structure/DropDown"; 6 | ChartJS.register([ArcElement, Tooltip, Legend]); 7 | 8 | interface Props { 9 | commitAuthorDates: { [key: string]: number }; 10 | } 11 | 12 | interface contributor { 13 | name: string; 14 | amount: number; 15 | color: string; 16 | } 17 | 18 | function sortAndPruneTupleList( 19 | labels: string[], 20 | data: number[], 21 | n: number 22 | ): contributor[] { 23 | const backgroundColors: string[] = [ 24 | "#e60049", 25 | "#0bb4ff", 26 | "#50e991", 27 | "#e6d800", 28 | "#9b19f5", 29 | "#ffa300", 30 | "#dc0ab4", 31 | "#b3d4ff", 32 | "#00bfa0", 33 | ]; 34 | 35 | const labelDataArray: contributor[] = labels.map((label, index) => ({ 36 | name: label, 37 | amount: data[index], 38 | color: "#b9babd", // placeholder color 39 | })); 40 | 41 | // Sort the array in descending order by amount (= number of commits) 42 | labelDataArray.sort((b, a) => a.amount - b.amount); 43 | 44 | // Prune the array to only include the top n contributors, and add an "Other" category 45 | // -- Other 46 | const otherList = labelDataArray.slice(n, labelDataArray.length); 47 | const otherpercentage = otherList.reduce( 48 | (acc, value) => acc + value.amount, 49 | 0 50 | ); 51 | const other: contributor = { 52 | name: "Other", 53 | amount: otherpercentage, 54 | color: "#7d7a74", 55 | }; 56 | 57 | // -- Top n contributors (add colors) 58 | const prunedArray = labelDataArray.slice(0, n); 59 | for (let i = 0; i < prunedArray.length; i++) { 60 | prunedArray[i].color = backgroundColors[i % backgroundColors.length]; 61 | } 62 | 63 | // Only add the "Other" category if it exists 64 | if (otherpercentage > 0) prunedArray.push(other); 65 | return prunedArray; 66 | } 67 | 68 | const ContributorPiechart: React.FC<Props> = ({ commitAuthorDates }) => { 69 | const [isMobile, setIsMobile] = useState(false); 70 | 71 | useEffect(() => { 72 | function handleResize() { 73 | setIsMobile(window.innerWidth <= 800); 74 | } 75 | window.addEventListener("resize", handleResize); 76 | handleResize(); 77 | return () => window.removeEventListener("resize", handleResize); 78 | }, []); 79 | 80 | const labels = Object.keys(commitAuthorDates); 81 | const data = Object.values(commitAuthorDates); 82 | 83 | const totalCommits = data.reduce((acc, curr) => acc + curr, 0); 84 | 85 | if (totalCommits === 0) { 86 | return ( 87 | <Alert variant="warning"> 88 | {" "} 89 | Uh oh.. no contributors found for this repository! 🥹{" "} 90 | </Alert> 91 | ); 92 | } 93 | 94 | const labelDataArray = sortAndPruneTupleList(labels, data, 8); 95 | const maxWidthName = 23; // Max width of name in table, in characters 96 | const tableRows = labelDataArray.map((contributor) => ( 97 | <tr key={contributor.name}> 98 | <td> 99 | {contributor.name.length > maxWidthName 100 | ? `${contributor.name.slice(0, maxWidthName)}..` 101 | : contributor.name} 102 | </td> 103 | <td> 104 | {contributor.amount / totalCommits <= 0.01 105 | ? "< 1" 106 | : ((contributor.amount / totalCommits) * 100.0).toFixed(0)} 107 | % 108 | </td> 109 | <td>{contributor.amount}</td> 110 | {!isMobile && ( 111 | <td> 112 | <span 113 | style={{ 114 | backgroundColor: contributor.color, 115 | display: "inline-block", 116 | width: 16, 117 | height: 16, 118 | borderRadius: "50%", 119 | }} 120 | /> 121 | </td> 122 | )} 123 | </tr> 124 | )); 125 | 126 | // Separate the label and data values into separate arrays again 127 | const sortedLabels = labelDataArray.map((obj) => obj.name); 128 | const sortedData = labelDataArray.map((obj) => obj.amount); 129 | const colors = labelDataArray.map((obj) => obj.color); 130 | 131 | const total = data.reduce((acc, curr) => acc + curr, 0); 132 | 133 | const mobileWidthLegendLabels = 11; // Max width of legend labels in mobile view, in characters 134 | const chartData = { 135 | labels: sortedLabels.map( 136 | (lbl) => 137 | `${ 138 | lbl.length > mobileWidthLegendLabels 139 | ? `${lbl.slice(0, mobileWidthLegendLabels)}..` 140 | : lbl 141 | }` 142 | ), 143 | datasets: [ 144 | { 145 | data: sortedData, 146 | backgroundColor: colors, 147 | }, 148 | ], 149 | }; 150 | 151 | const options = { 152 | animate: true, 153 | responsive: true, 154 | maintainAspectRatio: false, 155 | plugins: { 156 | tooltip: { 157 | callbacks: { 158 | label: function (context: any) { 159 | const label = context.label; 160 | const value = context.raw; 161 | const percentage = ((value / total) * 100).toFixed(2); 162 | return `${label}: ${value} (${percentage}%)`; 163 | }, 164 | }, 165 | }, 166 | legend: { 167 | labels: { 168 | color: "#fff", 169 | fontWeight: "bold", 170 | }, 171 | position: "right" as "right", 172 | align: "start" as "start", 173 | }, 174 | }, 175 | }; 176 | 177 | return ( 178 | <> 179 | <Container> 180 | <Row> 181 | <Table style={{ color: "#fff" }}> 182 | <thead> 183 | <tr> 184 | <th>Name</th> 185 | <th>Percentage</th> 186 | <th>Commits</th> 187 | {!isMobile && <th>Color</th>} 188 | </tr> 189 | </thead> 190 | <tbody>{tableRows}</tbody> 191 | </Table> 192 | </Row> 193 | <Row> 194 | <DropDown header="Contributor Piechart 🍰" collapsed={true}> 195 | <Container> 196 | <Row> 197 | <Col> 198 | <Pie data={chartData} options={options} /> 199 | </Col> 200 | </Row> 201 | </Container> 202 | </DropDown> 203 | </Row> 204 | </Container> 205 | </> 206 | ); 207 | }; 208 | 209 | export default ContributorPiechart; 210 | -------------------------------------------------------------------------------- /src/components/pages/Dashboard/DashboardComponents.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Container, Row, Col, Spinner, Alert, Button } from "react-bootstrap"; 3 | import DropDown from "../../structure/DropDown"; 4 | import Section from "../../structure/Section"; 5 | import Sidebar from "../../structure/Sidebar"; 6 | 7 | interface Props { 8 | sections: { title: string; content: JSX.Element }[]; 9 | experimentalSections: { title: string; content: JSX.Element }[]; 10 | isLoading: boolean; 11 | errorMsg: string; 12 | } 13 | 14 | function DashboardComponents(props: Props) { 15 | const [isMobile, setIsMobile] = useState(false); 16 | const [experimentalMode, setExperimentalMode] = useState(false); 17 | 18 | const checkIfMobile = function () { 19 | let check = false; 20 | const mediaQuery = window.matchMedia("(max-width: 768px)"); 21 | check = mediaQuery.matches; 22 | if (check) return check; 23 | (function (a) { 24 | if ( 25 | /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( 26 | a 27 | ) || 28 | /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( 29 | a.substr(0, 4) 30 | ) 31 | ) 32 | check = true; 33 | })(navigator.userAgent || navigator.vendor || (window as any).opera); 34 | return check; 35 | }; 36 | 37 | useEffect(() => { 38 | setIsMobile(checkIfMobile()); 39 | window.addEventListener("resize", checkIfMobile); 40 | return () => window.removeEventListener("resize", checkIfMobile); 41 | }, []); 42 | 43 | const sections = props.sections.map((section, index) => ( 44 | <Section key={index} title={section.title}> 45 | {section.content} 46 | </Section> 47 | )); 48 | 49 | const experimentalSections = props.experimentalSections.map( 50 | (section, index) => ( 51 | <Section key={props.sections.length + index} title={section.title}> 52 | {section.content} 53 | </Section> 54 | ) 55 | ); 56 | 57 | function toggleExperimentalMode() { 58 | setExperimentalMode(!experimentalMode); 59 | } 60 | 61 | const experimentalButton = ( 62 | <Container 63 | id="experimental-metrics" 64 | className="d-flex flex-column align-items-center justify-content-center mb-3" 65 | > 66 | <DropDown header={"See experimental metrics 🛸"} collapsed={true}> 67 | <Alert variant="secondary" id="experimental-info" className="mt-3"> 68 | <Alert.Heading>Important Note ⚠️</Alert.Heading> 69 | <p> 70 | Some of the ongoing developments for Susie's dashboard have 71 | suboptimal accuracy or a non-evident purpose. Therefore, to keep 72 | Susie credible, these are omitted by default. 73 | <br /> <br /> 74 | However, who are we to stop you from exploring them? 🤷‍♂️ If you are 75 | cautious yet curious, you can enable them by clicking the button 76 | below. Just know, there is no way back.. until the next analysis.{" "} 77 | <br /> <br /> 78 | Please consider contributing to our{" "} 79 | <a 80 | className="susie-link-dark" 81 | href="https://github.com/philippedeb/susie" 82 | target="_blank" 83 | rel="noopener noreferrer" 84 | > 85 | GitHub repository 86 | </a>{" "} 87 | to help us improve and expand Susie. Together, we can make a 88 | difference! 🤝 89 | </p> 90 | </Alert> 91 | <Row> 92 | <Col> 93 | <Button 94 | variant="secondary" 95 | onClick={toggleExperimentalMode} 96 | className="experimental-button" 97 | > 98 | {" "} 99 | Enter experimental space 👽{" "} 100 | </Button> 101 | </Col> 102 | </Row> 103 | </DropDown> 104 | </Container> 105 | ); 106 | 107 | const listOfSections: JSX.Element = ( 108 | <> 109 | {sections} 110 | {experimentalMode ? experimentalSections : experimentalButton} 111 | </> 112 | ); 113 | 114 | const rateLimited: JSX.Element = ( 115 | <> 116 | <Alert variant="danger"> 117 | You have been rate-limited by the API for exceeding the number of 118 | requests allowed per hour per IP.. 😭 119 | </Alert> 120 | <Alert variant="warning"> 121 | Why does this happen? Learn more{" "} 122 | <a 123 | href="https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting" 124 | target="_blank" 125 | rel="noopener noreferrer" 126 | > 127 | about GitHub rate-limiting here 128 | </a> 129 | .{" "} 130 | </Alert> 131 | <Alert variant="success"> 132 | Fix it by switching IPs or contact the developers for a (paid) secret 133 | key.. 🔑{" "} 134 | </Alert> 135 | </> 136 | ); 137 | 138 | const getErrorDisplay = (errorMessage: string) => { 139 | if (errorMessage.includes("API rate limit")) { 140 | console.log("Rate limited! (403)"); 141 | return rateLimited as JSX.Element; 142 | } 143 | if (errorMessage.includes("ERROR in fetching data")) { 144 | console.log("Uh oh, could not load all data (404).."); 145 | return listOfSections; 146 | } 147 | console.log("*unknown error appeared* 💀"); 148 | return ( 149 | <Alert variant="danger"> 150 | <b>ERROR 💀</b> <br></br> 151 | {"Details: " + errorMessage} 152 | </Alert> 153 | ) as JSX.Element; 154 | }; 155 | 156 | return ( 157 | <div> 158 | <Container> 159 | <Row> 160 | {window.innerWidth > 800 && !isMobile && ( 161 | <Col sm={4}> 162 | <Sidebar 163 | sections={props.sections} 164 | experimentalSections={props.experimentalSections} 165 | experimentalMode={experimentalMode} 166 | /> 167 | </Col> 168 | )} 169 | <Col sm={{ span: 8, offset: 2 }} className="sections-col"> 170 | {props.isLoading ? ( 171 | <div className="d-flex justify-content-center"> 172 | <Spinner animation="border" /> 173 | </div> 174 | ) : props.errorMsg !== "" ? ( 175 | getErrorDisplay(props.errorMsg) 176 | ) : ( 177 | listOfSections 178 | )} 179 | </Col> 180 | </Row> 181 | </Container> 182 | </div> 183 | ); 184 | } 185 | 186 | export default DashboardComponents; 187 | -------------------------------------------------------------------------------- /src/components/pages/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from "react-router-dom"; 2 | import { useEffect, useState } from "react"; 3 | import { getData } from "../../../logic/fetcher"; 4 | import Inclusive from "../../metrics/Inclusivity/Inclusive"; 5 | import "../../../css/Dashboard.css"; 6 | import DashboardInfo from "./DashboardInfo"; 7 | import WorkflowAnalysis from "../../metrics/Workflows/WorkflowAnalysis"; 8 | import Info from "../../metrics/General/Info"; 9 | import Governance from "../../metrics/Governance/Governance"; 10 | import DashboardComponents from "./DashboardComponents"; 11 | import "../../../css/Link.css"; 12 | import ProgrammingLanguage from "../../metrics/Language/ProgrammingLanguage"; 13 | import Contributors from "../../metrics/Contributors/Contributors"; 14 | import IssuesSentiment from "../../metrics/Sentiment/IssuesSentiment"; 15 | 16 | function Dashboard() { 17 | const location = useLocation(); 18 | const searchParams = new URLSearchParams(location.search); 19 | const searchValue = searchParams.get("search") || ""; 20 | 21 | const [branches, setBranches] = useState<string[]>([]); 22 | const [pullRequests, setPullRequests] = useState<string[]>([]); 23 | const [commitMessages, setCommitMessages] = useState<string[]>([]); 24 | const [languages, setLanguages] = useState<{ [key: string]: number }>({}); 25 | const [issues, setIssues] = useState<string[]>([]); 26 | const [workflows, setWorkflows] = useState<string[]>([]); 27 | 28 | const [contributorData, setContributorData] = useState<[string, string][]>( 29 | [] 30 | ); 31 | 32 | const [readme, setReadMe] = useState<string>(""); // README.md 33 | const [license, setLicense] = useState<string>(""); // LICENSE.md 34 | const [changeLog, setChangeLog] = useState<string>(""); // CHANGELOG.md 35 | const [codeOfConduct, setCodeOfConduct] = useState<string>(""); // CODE_OF_CONDUCT.md 36 | const [contributing, setContributing] = useState<string>(""); // CONTRIBUTING.md 37 | const [issueTemplate, setIssueTemplate] = useState<string>(""); // ISSUE_TEMPLATE.md 38 | const [pullRequestTemplate, setPullRequestTemplate] = useState<string>(""); // PULL_REQUEST_TEMPLATE.md 39 | const [communityProfile, setCommunityProfile] = useState<any>(null); // community_profile.md 40 | 41 | const [inclusiveData, setInclusiveData] = useState<[string, string][]>([]); 42 | 43 | const [errorMsg, setErrorMsg] = useState<string>(""); 44 | 45 | const [isLoading, setIsLoading] = useState(true); 46 | 47 | useEffect(() => { 48 | const handleErrorMsg = (error: Error) => { 49 | if (!errorMsg.includes("API rate limit")) { 50 | setErrorMsg(error.message); 51 | } 52 | }; 53 | 54 | const fetchData = async () => { 55 | try { 56 | const data = await getData(searchValue); 57 | 58 | if (data instanceof Error) { 59 | throw data; 60 | } 61 | 62 | var inclusiveArray: [string, string][] = []; 63 | 64 | var dataIsError: boolean = false; 65 | if (!(data.branches instanceof Error)) { 66 | setBranches(data.branches as string[]); 67 | inclusiveArray.push( 68 | ...data.branches.map((item) => ["branch", item] as [string, string]) 69 | ); 70 | } else { 71 | dataIsError = true; 72 | handleErrorMsg(data.branches); 73 | } 74 | if (!(data.pull_requests instanceof Error)) { 75 | setPullRequests(data.pull_requests as string[]); 76 | inclusiveArray.push( 77 | ...data.pull_requests.map( 78 | (item) => ["pull_request", item] as [string, string] 79 | ) 80 | ); 81 | } else { 82 | dataIsError = true; 83 | handleErrorMsg(data.pull_requests); 84 | } 85 | if (!(data.commitMessages instanceof Error)) { 86 | setCommitMessages(data.commitMessages as string[]); 87 | inclusiveArray.push( 88 | ...data.commitMessages.map( 89 | (item) => ["commit_message", item] as [string, string] 90 | ) 91 | ); 92 | } else { 93 | dataIsError = true; 94 | handleErrorMsg(data.commitMessages); 95 | } 96 | if (!(data.languages instanceof Error)) { 97 | setLanguages(data.languages as { [key: string]: number }); 98 | } else { 99 | dataIsError = true; 100 | handleErrorMsg(data.languages); 101 | } 102 | if (!(data.issues instanceof Error)) { 103 | setIssues(data.issues as string[]); 104 | inclusiveArray.push( 105 | ...data.issues.map((item) => ["issue", item] as [string, string]) 106 | ); 107 | } else { 108 | dataIsError = true; 109 | handleErrorMsg(data.issues); 110 | } 111 | if (!(data.commitAuthorDates instanceof Error)) { 112 | setContributorData(data.commitAuthorDates as [string, string][]); 113 | } else { 114 | dataIsError = true; 115 | handleErrorMsg(data.commitAuthorDates); 116 | } 117 | if (!(data.runs instanceof Error)) { 118 | setWorkflows(data.runs as string[]); 119 | inclusiveArray.push( 120 | ...data.runs.map((item) => ["workflow", item] as [string, string]) 121 | ); 122 | } else { 123 | dataIsError = true; 124 | handleErrorMsg(data.runs); 125 | } 126 | 127 | if (!(data.readme instanceof Error)) { 128 | setReadMe(data.readme as string); 129 | inclusiveArray.push(["readme", data.readme]); 130 | } else { 131 | dataIsError = true; 132 | handleErrorMsg(data.readme); 133 | } 134 | if (!(data.license instanceof Error)) { 135 | setLicense(data.license as string); 136 | } else { 137 | dataIsError = true; 138 | handleErrorMsg(data.license); 139 | } 140 | if (!(data.changelog instanceof Error)) { 141 | setChangeLog(data.changelog as string); 142 | inclusiveArray.push(["changelog", data.changelog]); 143 | } else { 144 | dataIsError = true; 145 | handleErrorMsg(data.changelog); 146 | } 147 | if (!(data.codeOfConduct instanceof Error)) { 148 | setCodeOfConduct(data.codeOfConduct as string); 149 | inclusiveArray.push(["code_of_conduct", data.codeOfConduct]); 150 | } else { 151 | dataIsError = true; 152 | handleErrorMsg(data.codeOfConduct); 153 | } 154 | if (!(data.contributingGuidelines instanceof Error)) { 155 | setContributing(data.contributingGuidelines as string); 156 | inclusiveArray.push(["contributing", data.contributingGuidelines]); 157 | } else { 158 | dataIsError = true; 159 | handleErrorMsg(data.contributingGuidelines); 160 | } 161 | if (!(data.issueTemplate instanceof Error)) { 162 | setIssueTemplate(data.issueTemplate as string); 163 | inclusiveArray.push(["issue_template", data.issueTemplate]); 164 | } else { 165 | dataIsError = true; 166 | handleErrorMsg(data.issueTemplate); 167 | } 168 | if (!(data.prTemplate instanceof Error)) { 169 | setPullRequestTemplate(data.prTemplate as string); 170 | inclusiveArray.push(["pull_request_template", data.prTemplate]); 171 | } else { 172 | dataIsError = true; 173 | handleErrorMsg(data.prTemplate); 174 | } 175 | if (!(data.communityCheck instanceof Error)) { 176 | setCommunityProfile(data.communityCheck as any); 177 | } else { 178 | dataIsError = true; 179 | handleErrorMsg(data.communityCheck); 180 | } 181 | 182 | setInclusiveData(inclusiveArray); 183 | } catch (error) { 184 | setErrorMsg(error instanceof Error ? error.message : "Unknown error"); 185 | } 186 | 187 | // ! Must be last in fetchData to ensure all data is fetched before setting isLoading to false (loading icon) and successLoading to true (no error message) 188 | setIsLoading(false); 189 | }; 190 | fetchData(); 191 | }, [searchValue]); // only run when searchValue changes 192 | 193 | const sections = [ 194 | { 195 | title: "Info", 196 | content: ( 197 | <Info 198 | commits={commitMessages.length} 199 | pullRequests={pullRequests.length} 200 | branches={branches.length} 201 | issues={issues.length} 202 | /> 203 | ), 204 | }, 205 | { 206 | title: "Inclusive Language", 207 | content: <Inclusive data={inclusiveData} />, 208 | }, 209 | { 210 | title: "Contributors", 211 | content: <Contributors commitAuthorDates={contributorData} />, 212 | }, 213 | { 214 | title: "Workflow", 215 | content: <WorkflowAnalysis statusses={workflows} />, 216 | }, 217 | { 218 | title: "Governance", 219 | content: ( 220 | <Governance 221 | readme={readme} 222 | license={license} 223 | changeLog={changeLog} 224 | codeOfConduct={codeOfConduct} 225 | contributing={contributing} 226 | issueTemplate={issueTemplate} 227 | pullRequestTemplate={pullRequestTemplate} 228 | communityProfile={communityProfile} 229 | /> 230 | ), 231 | }, 232 | { 233 | title: "Sustainable Programming Languages", 234 | content: <ProgrammingLanguage languages={languages} />, 235 | }, 236 | ]; 237 | 238 | const experimental_sections = [ 239 | { 240 | title: "Issue Sentiment", 241 | content: <IssuesSentiment data={issues} />, 242 | }, 243 | ]; 244 | 245 | return ( 246 | <> 247 | <DashboardInfo repoLink={searchValue} /> 248 | <DashboardComponents 249 | sections={sections} 250 | experimentalSections={experimental_sections} 251 | isLoading={isLoading} 252 | errorMsg={errorMsg} 253 | /> 254 | </> 255 | ); 256 | } 257 | 258 | export default Dashboard; 259 | -------------------------------------------------------------------------------- /src/components/metrics/Governance/Governance.tsx: -------------------------------------------------------------------------------- 1 | import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { useEffect, useState } from "react"; 4 | import { Badge } from "react-bootstrap"; 5 | import DropDown from "../../structure/DropDown"; 6 | 7 | interface Props { 8 | readme: string; 9 | license: string; 10 | changeLog: string; 11 | codeOfConduct: string; 12 | contributing: string; 13 | issueTemplate: string; 14 | pullRequestTemplate: string; 15 | communityProfile: any; 16 | } 17 | 18 | function Governance(props: Props) { 19 | const [hasEnergyStatement, setHasEnergyStatement] = useState(false); 20 | const [isMobile, setIsMobile] = useState(false); 21 | 22 | useEffect(() => { 23 | function handleResize() { 24 | setIsMobile(window.innerWidth <= 800); 25 | } 26 | window.addEventListener("resize", handleResize); 27 | handleResize(); 28 | return () => window.removeEventListener("resize", handleResize); 29 | }, []); 30 | 31 | useEffect(() => { 32 | // Check for energy statement keywords in readme and license files 33 | const text = props.readme.toLowerCase() + props.license.toLowerCase(); 34 | if ( 35 | text.includes("energy statement") || 36 | text.includes("carbon footprint") || 37 | text.includes("sustainability") || 38 | text.includes("environmental impact") || 39 | text.includes("carbon emission") || 40 | text.includes("CO2 emission") || 41 | text.includes("sustainable") || 42 | text.includes("biodegradable") || 43 | text.includes("recyclable") || 44 | text.includes("recycling") || 45 | text.includes("carbon offset") || 46 | text.includes("carbon neutral") || 47 | text.includes("carbon positive") || 48 | text.includes("climate action") || 49 | text.includes("climate change") || 50 | text.includes("circular economy") || 51 | text.includes("e-waste") || 52 | text.includes("paris agreement") || 53 | text.includes("zero waste") || 54 | text.includes("zero carbon") || 55 | text.includes("energy efficiency") || 56 | text.includes("energy consumption") || 57 | text.includes("energy saving") || 58 | text.includes("ecological footprint") || 59 | text.includes("ecological impact") || 60 | text.includes("leed certification") || 61 | text.includes("landfill-free") || 62 | text.includes("pollution") || 63 | text.includes("organic") || 64 | text.includes("waste-to-profit") || 65 | text.includes("waste-to-energy") 66 | ) { 67 | setHasEnergyStatement(true); 68 | } 69 | }, [props.readme, props.license]); 70 | 71 | const communityProfile = props.communityProfile !== null; 72 | const communityProfileHealthPercentage = 73 | communityProfile && 74 | "health_percentage" in props.communityProfile && 75 | props.communityProfile.health_percentage !== null; 76 | const communityProfileDocumentation = 77 | communityProfile && 78 | "documentation" in props.communityProfile && 79 | props.communityProfile.documentation !== null; 80 | const communityProfileFiles = 81 | communityProfile && 82 | "files" in props.communityProfile && 83 | props.communityProfile.files !== null; 84 | const communityProfileReadme = 85 | communityProfileFiles && 86 | "readme" in props.communityProfile.files && 87 | props.communityProfile.files.readme !== null; 88 | const communityProfileLicense = 89 | communityProfileFiles && 90 | "license" in props.communityProfile.files && 91 | props.communityProfile.files.license !== null; 92 | const communityProfileCodeOfConduct = 93 | communityProfileFiles && 94 | "code_of_conduct_file" in props.communityProfile.files && 95 | props.communityProfile.files.code_of_conduct_file !== null; 96 | const communityProfileContributing = 97 | communityProfileFiles && 98 | "contributing" in props.communityProfile.files && 99 | props.communityProfile.files.contributing !== null; 100 | const communityProfileIssueTemplate = 101 | communityProfileFiles && 102 | "issue_template" in props.communityProfile.files && 103 | props.communityProfile.files.issue_template !== null; 104 | const communityProfilePullRequestTemplate = 105 | communityProfileFiles && 106 | "pull_request_template" in props.communityProfile.files && 107 | props.communityProfile.files.pull_request_template !== null; 108 | 109 | const includedGovernance: JSX.Element = ( 110 | <> 111 | <h6> 112 | ➡️ The repository <b>includes</b> the following: 113 | </h6> 114 | <ul> 115 | {communityProfileDocumentation && ( 116 | <li key={0}> 117 | <a 118 | href={props.communityProfile.documentation} 119 | className="susie-link" 120 | target="_blank" 121 | rel="noopener noreferrer" 122 | > 123 | Website <FontAwesomeIcon icon={faExternalLinkAlt} /> 124 | </a> 125 | </li> 126 | )} 127 | {communityProfileReadme && ( 128 | <li key={1}> 129 | <a 130 | href={props.communityProfile.files.readme.html_url} 131 | className="susie-link" 132 | target="_blank" 133 | rel="noopener noreferrer" 134 | > 135 | Readme <FontAwesomeIcon icon={faExternalLinkAlt} /> 136 | </a> 137 | </li> 138 | )} 139 | {communityProfileLicense && ( 140 | <li key={2}> 141 | <a 142 | href={props.communityProfile.files.license.html_url} 143 | className="susie-link" 144 | target="_blank" 145 | rel="noopener noreferrer" 146 | > 147 | License <FontAwesomeIcon icon={faExternalLinkAlt} /> 148 | </a>{" "} 149 | ({props.communityProfile.files.license.name}) 150 | </li> 151 | )} 152 | {communityProfileCodeOfConduct && ( 153 | <li key={3}> 154 | <a 155 | href={props.communityProfile.files.code_of_conduct_file.html_url} 156 | className="susie-link" 157 | target="_blank" 158 | rel="noopener noreferrer" 159 | > 160 | Code of Conduct <FontAwesomeIcon icon={faExternalLinkAlt} /> 161 | </a> 162 | </li> 163 | )} 164 | {communityProfileContributing && ( 165 | <li key={4}> 166 | <a 167 | href={props.communityProfile.files.contributing.html_url} 168 | className="susie-link" 169 | target="_blank" 170 | rel="noopener noreferrer" 171 | > 172 | Contributing <FontAwesomeIcon icon={faExternalLinkAlt} /> 173 | </a> 174 | </li> 175 | )} 176 | {communityProfileIssueTemplate && ( 177 | <li key={5}> 178 | <a 179 | href={props.communityProfile.files.issue_template.html_url} 180 | className="susie-link" 181 | target="_blank" 182 | rel="noopener noreferrer" 183 | > 184 | Issue Template <FontAwesomeIcon icon={faExternalLinkAlt} /> 185 | </a> 186 | </li> 187 | )} 188 | {communityProfilePullRequestTemplate && ( 189 | <li key={6}> 190 | <a 191 | href={props.communityProfile.files.pull_request_template.html_url} 192 | className="susie-link" 193 | target="_blank" 194 | rel="noopener noreferrer" 195 | > 196 | Pull Request Template <FontAwesomeIcon icon={faExternalLinkAlt} /> 197 | </a> 198 | </li> 199 | )} 200 | {props.changeLog !== "" && <li key={7}>Changelog</li>} 201 | </ul> 202 | </> 203 | ); 204 | 205 | const excludedGovernance: JSX.Element = ( 206 | <> 207 | <h6> 208 | ➡️ The repository seems to <b>miss</b> the following: 209 | </h6> 210 | <ul> 211 | {!communityProfileDocumentation && <li key={0}>Website</li>} 212 | {!communityProfileReadme && <li key={1}>Readme</li>} 213 | {!communityProfileLicense && <li key={2}>License</li>} 214 | {!communityProfileCodeOfConduct && <li key={3}>Code of Conduct</li>} 215 | {!communityProfileContributing && <li key={4}>Contributing</li>} 216 | {!communityProfileIssueTemplate && <li key={5}>Issue Template</li>} 217 | {!communityProfilePullRequestTemplate && ( 218 | <li key={6}>Pull Request Template</li> 219 | )} 220 | {props.changeLog === "" && <li key={7}>Changelog</li>} 221 | </ul> 222 | </> 223 | ); 224 | 225 | const scoreGovernance: JSX.Element = ( 226 | <> 227 | <h5 className="mb-3"> 228 | Governance Score:{" "} 229 | <Badge 230 | bg={ 231 | communityProfileHealthPercentage 232 | ? props.communityProfile.health_percentage < 40 233 | ? "danger" 234 | : props.communityProfile.health_percentage < 75 235 | ? "primary" 236 | : "success" 237 | : "secondary" 238 | } 239 | > 240 | {communityProfileHealthPercentage 241 | ? props.communityProfile.health_percentage 242 | : "N/A"} 243 | </Badge> 244 | </h5> 245 | </> 246 | ); 247 | 248 | const somethingIsIncluded = 249 | communityProfileDocumentation || 250 | communityProfileReadme || 251 | communityProfileLicense || 252 | communityProfileCodeOfConduct || 253 | communityProfileContributing || 254 | communityProfileIssueTemplate || 255 | communityProfilePullRequestTemplate || 256 | props.changeLog !== ""; 257 | 258 | const somethingIsExcluded = 259 | !communityProfileDocumentation || 260 | !communityProfileReadme || 261 | !communityProfileLicense || 262 | !communityProfileCodeOfConduct || 263 | !communityProfileContributing || 264 | !communityProfileIssueTemplate || 265 | !communityProfilePullRequestTemplate || 266 | props.changeLog === ""; 267 | 268 | return ( 269 | <> 270 | <p> 271 | Make more informed decisions about which projects to contribute to or 272 | use in your own work by looking at the commitment of a repository to 273 | sustainability. 274 | </p> 275 | 276 | {scoreGovernance} 277 | {somethingIsIncluded && includedGovernance} 278 | {somethingIsExcluded && excludedGovernance} 279 | {hasEnergyStatement ? ( 280 | <h6>➡️ The repository addresses environmental sustainability 👌</h6> 281 | ) : ( 282 | <h6> 283 | ➡️ The repository does not seem to address environmental 284 | sustainability ⛔ 285 | </h6> 286 | )} 287 | <hr /> 288 | <DropDown header={"How is the score calculated? 🧮"} collapsed={true}> 289 | <p> 290 | Based on the components present in a GitHub's community profile, the 291 | GitHub API can return this health percentage for any repository. The 292 | score is between 0 and 100. Above, you can find more details on which 293 | components are present or absent in the repository. 294 | </p> 295 | </DropDown> 296 | <DropDown 297 | header="When does a project address sustainability? 💭" 298 | collapsed={true} 299 | > 300 | <p> 301 | Currently, Susie checks the <b>README.md</b> for certain phrases. The 302 | data is based on the most popular search terms and glossaries of 303 | sustainability terms and definitions. Note, this is not an exhaustive 304 | list: 305 | </p> 306 | <ul style={{ listStyleType: "circle", columns: isMobile ? "1" : "2" }}> 307 | <li>biodegradable</li> 308 | <li>carbon emission</li> 309 | <li>carbon footprint</li> 310 | <li>carbon neutral</li> 311 | <li>carbon offset</li> 312 | <li>carbon positive</li> 313 | <li>circular economy</li> 314 | <li>climate action</li> 315 | <li>climate change</li> 316 | <li>CO2 emission</li> 317 | <li>ecological footprint</li> 318 | <li>ecological impact</li> 319 | <li>energy consumption</li> 320 | <li>energy efficiency</li> 321 | <li>energy saving</li> 322 | <li>energy statement</li> 323 | <li>e-waste</li> 324 | <li>environmental impact</li> 325 | <li>landfill-free</li> 326 | <li>leed certification</li> 327 | <li>organic</li> 328 | <li>paris agreement</li> 329 | <li>pollution</li> 330 | <li>recyclable</li> 331 | <li>recycling</li> 332 | <li>sustainable</li> 333 | <li>sustainability</li> 334 | <li>waste-to-energy</li> 335 | <li>waste-to-profit</li> 336 | <li>zero carbon</li> 337 | <li>zero waste</li> 338 | </ul> 339 | <p> 340 | If you want to help improving Susie by implementing NLP to detect 341 | sustainability statements, please contribute to our{" "} 342 | <a 343 | href="https://github.com/philippedeb/susie" 344 | className="susie-link" 345 | target="_blank" 346 | rel="noopener noreferrer" 347 | > 348 | open-source repository 349 | </a> 350 | ! 351 | </p> 352 | </DropDown> 353 | </> 354 | ); 355 | } 356 | 357 | export default Governance; 358 | -------------------------------------------------------------------------------- /src/logic/fetcher.ts: -------------------------------------------------------------------------------- 1 | export { extractGitHubOwnerAndRepo, getData, getSlash }; 2 | 3 | async function getData(searchValue: string): Promise< 4 | | { 5 | branches: string[] | Error; 6 | commitMessages: string[] | Error; 7 | pull_requests: string[] | Error; 8 | languages: { [key: string]: number } | Error; 9 | issues: string[] | Error; 10 | commitAuthorDates: [string, string][] | Error; 11 | runs: string[] | Error; 12 | readme: string | Error; 13 | license: string | Error; 14 | changelog: string | Error; 15 | codeOfConduct: string | Error; 16 | contributingGuidelines: string | Error; 17 | issueTemplate: string | Error; 18 | prTemplate: string | Error; 19 | communityCheck: any | Error; 20 | } 21 | | Error 22 | > { 23 | try { 24 | const repo = extractGitHubRepoPath(searchValue); 25 | const branches = await getBranches(repo); 26 | const commitMessages = await getCommitMessages(repo); 27 | const pull_requests = await getPullRequests(repo); 28 | const languages = await getLanguages(repo); 29 | const issues = await getIssues(repo); 30 | const commitAuthorDates = await getCommitAuthorDates(repo); 31 | const runs = await getRuns(repo); 32 | const readme = await getFileContent(repo, "README.md"); 33 | const license = await getFileContent(repo, "LICENSE"); 34 | const changelog = await getFileContent(repo, "CHANGELOG.md"); 35 | const codeOfConduct = await getCodeOfConduct(repo); 36 | const contributingGuidelines = await getContributingGuidelines(repo); 37 | const issueTemplate = await getIssueTemplate(repo); 38 | const prTemplate = await getPrTemplate(repo); 39 | const communityCheck = await getCommunityProfile(repo); 40 | return { 41 | branches, 42 | commitMessages, 43 | pull_requests, 44 | languages, 45 | issues, 46 | commitAuthorDates, 47 | runs, 48 | readme, 49 | license, 50 | changelog, 51 | codeOfConduct, 52 | contributingGuidelines, 53 | issueTemplate, 54 | prTemplate, 55 | communityCheck, 56 | }; 57 | } catch (error) { 58 | return new Error(error instanceof Error ? error.message : "Unknown error"); 59 | } 60 | } 61 | 62 | interface GitBranch { 63 | name: string; 64 | } 65 | 66 | interface GitCommit { 67 | commit: { 68 | message: string; 69 | author: { 70 | name: string; 71 | date: string; 72 | }; 73 | date: string; 74 | }; 75 | } 76 | 77 | interface GitPull { 78 | title: string; 79 | } 80 | 81 | interface GitIssue { 82 | title: string; 83 | } 84 | 85 | interface WorkflowRuns { 86 | workflow_runs: [{ conclusion: string }]; 87 | } 88 | 89 | async function getCodeOfConduct(repo: string): Promise<string | Error> { 90 | try { 91 | const response = await fetch( 92 | `https://api.github.com/repos/${repo}/community/code_of_conduct` 93 | ); 94 | if (response.status === 403) { 95 | return new Error("API rate limit exceeded"); 96 | } 97 | if (response.status === 404) { 98 | return new Error("ERROR in fetching data"); 99 | } 100 | const data = await response.json(); 101 | if (data.code_of_conduct) { 102 | return atob(data.code_of_conduct.body); 103 | } else { 104 | return ""; 105 | } 106 | } catch (error) { 107 | return new Error(error instanceof Error ? error.message : "Unknown error"); 108 | } 109 | } 110 | 111 | async function getContributingGuidelines( 112 | repo: string 113 | ): Promise<string | Error> { 114 | try { 115 | const response = await fetch( 116 | `https://api.github.com/repos/${repo}/contents/CONTRIBUTING.md` 117 | ); 118 | if (response.status === 403) { 119 | return new Error("API rate limit exceeded"); 120 | } 121 | if (response.status === 404) { 122 | return new Error("ERROR in fetching data"); 123 | } 124 | const data = await response.json(); 125 | if (data.content) { 126 | return atob(data.content); 127 | } else { 128 | return ""; 129 | } 130 | } catch (error) { 131 | return new Error(error instanceof Error ? error.message : "Unknown error"); 132 | } 133 | } 134 | 135 | async function getIssueTemplate(repo: string): Promise<string | Error> { 136 | try { 137 | const response = await fetch( 138 | `https://api.github.com/repos/${repo}/contents/.github/ISSUE_TEMPLATE.md` 139 | ); 140 | if (response.status === 403) { 141 | return new Error("API rate limit exceeded"); 142 | } 143 | if (response.status === 404) { 144 | return new Error("ERROR in fetching data"); 145 | } 146 | const data = await response.json(); 147 | if (data.content) { 148 | return atob(data.content); 149 | } else { 150 | return ""; 151 | } 152 | } catch (error) { 153 | return new Error(error instanceof Error ? error.message : "Unknown error"); 154 | } 155 | } 156 | 157 | async function getPrTemplate(repo: string): Promise<string | Error> { 158 | try { 159 | const response = await fetch( 160 | `https://api.github.com/repos/${repo}/contents/.github/PULL_REQUEST_TEMPLATE.md` 161 | ); 162 | if (response.status === 403) { 163 | return new Error("API rate limit exceeded"); 164 | } 165 | if (response.status === 404) { 166 | return new Error("ERROR in fetching data"); 167 | } 168 | const data = await response.json(); 169 | if (data.content) { 170 | return atob(data.content); 171 | } else { 172 | return ""; 173 | } 174 | } catch (error) { 175 | return new Error(error instanceof Error ? error.message : "Unknown error"); 176 | } 177 | } 178 | 179 | async function getBranches(repo: string): Promise<string[] | Error> { 180 | try { 181 | const response = await fetch( 182 | "https://api.github.com/repos/" + repo + "/branches?per_page=100" 183 | ); 184 | if (response.status === 403) { 185 | return new Error("API rate limit exceeded"); 186 | } 187 | if (response.status === 404) { 188 | return new Error("ERROR in fetching data"); 189 | } 190 | const data: GitBranch[] = await response.json(); 191 | const branchNames = data.map((item) => item.name.toLowerCase()); 192 | return branchNames; 193 | } catch (error) { 194 | return new Error(error instanceof Error ? error.message : "Unknown error"); 195 | } 196 | } 197 | 198 | async function getPullRequests(repo: string): Promise<string[] | Error> { 199 | try { 200 | const response = await fetch( 201 | "https://api.github.com/repos/" + repo + "/pulls?per_page=100&state=all" 202 | ); 203 | if (response.status === 403) { 204 | return new Error("API rate limit exceeded"); 205 | } 206 | if (response.status === 404) { 207 | return new Error("ERROR in fetching data"); 208 | } 209 | const data: GitPull[] = await response.json(); 210 | const pullNames = data.map((item) => item.title.toLowerCase()); 211 | return pullNames; 212 | } catch (error) { 213 | return new Error(error instanceof Error ? error.message : "Unknown error"); 214 | } 215 | } 216 | 217 | async function getCommitMessages( 218 | repo: string, 219 | since: string = "2008-02-08T12:00:00Z" 220 | ): Promise<string[] | Error> { 221 | try { 222 | const response = await fetch( 223 | "https://api.github.com/repos/" + 224 | repo + 225 | "/commits?per_page=100&since=" + 226 | since 227 | ); 228 | if (response.status === 403) { 229 | return new Error("API rate limit exceeded"); 230 | } 231 | if (response.status === 404) { 232 | return new Error("ERROR in fetching data"); 233 | } 234 | const data: GitCommit[] = await response.json(); 235 | const commitNames = data.map((item) => item.commit.message.toLowerCase()); 236 | return commitNames; 237 | } catch (error) { 238 | return new Error(error instanceof Error ? error.message : "Unknown error"); 239 | } 240 | } 241 | 242 | async function getLanguages( 243 | repo: string 244 | ): Promise<{ [key: string]: number } | Error> { 245 | try { 246 | const response = await fetch( 247 | "https://api.github.com/repos/" + repo + "/languages" 248 | ); 249 | if (response.status === 403) { 250 | return new Error("API rate limit exceeded"); 251 | } 252 | if (response.status === 404) { 253 | return new Error("ERROR in fetching data"); 254 | } 255 | const data = await response.json(); 256 | return data; 257 | } catch (error) { 258 | return new Error(error instanceof Error ? error.message : "Unknown error"); 259 | } 260 | } 261 | 262 | async function getIssues( 263 | repo: string, 264 | since: string = "2008-02-08T12:00:00Z" 265 | ): Promise<string[] | Error> { 266 | try { 267 | const response = await fetch( 268 | "https://api.github.com/repos/" + 269 | repo + 270 | "/issues?per_page=100&since=" + 271 | since 272 | ); 273 | if (response.status === 403) { 274 | return new Error("API rate limit exceeded"); 275 | } 276 | if (response.status === 404) { 277 | return new Error("ERROR in fetching data"); 278 | } 279 | const data: GitIssue[] = await response.json(); 280 | const issueNames = data.map((item) => item.title.toLowerCase()); 281 | return issueNames; 282 | } catch (error) { 283 | return new Error(error instanceof Error ? error.message : "Unknown error"); 284 | } 285 | } 286 | 287 | async function getCommitAuthorDates( 288 | repo: string, 289 | since: string = "2008-02-08T12:00:00Z" 290 | ): Promise<[string, string][] | Error> { 291 | let n: number = 1; 292 | let dateList: [string, string][] = []; 293 | while (n < 5) { 294 | try { 295 | const response = await fetch( 296 | "https://api.github.com/repos/" + 297 | repo + 298 | "/commits?per_page=100&page=" + 299 | n.toString() + 300 | "/since=" + 301 | since 302 | ); 303 | if (response.status === 403) { 304 | return new Error("API rate limit exceeded"); 305 | } 306 | if (response.status === 404) { 307 | return new Error("ERROR in fetching data"); 308 | } 309 | const data: GitCommit[] = await response.json(); 310 | 311 | const commitDates: [string, string][] = data.map((item) => [ 312 | item.commit.author.name, 313 | item.commit.author.date, 314 | ]); 315 | dateList = dateList.concat(commitDates); 316 | if (commitDates.length < 30) { 317 | break; 318 | } 319 | n++; 320 | } catch (error) { 321 | return new Error( 322 | error instanceof Error ? error.message : "Unknown error" 323 | ); 324 | } 325 | } 326 | return dateList; 327 | } 328 | 329 | /* 330 | * Use param "readme" to check for a README file 331 | * Use param "license" to check for a LICENSE file 332 | */ 333 | async function getSlash(url: string, param: string): Promise<boolean | Error> { 334 | const [owner, repo] = extractGitHubOwnerAndRepo(url); 335 | try { 336 | const response = await fetch( 337 | `https://api.github.com/repos/${owner}/${repo}/${param}` 338 | ); 339 | if (response.status === 403) { 340 | return new Error("API rate limit exceeded"); 341 | } 342 | if (response.status === 404) { 343 | return new Error("ERROR in fetching data"); 344 | } 345 | const data = await response.json(); 346 | if (data.message && data.message === "Not Found") { 347 | return false; 348 | } else if (data.path) { 349 | return true; 350 | } 351 | return false; 352 | } catch (error) { 353 | return new Error(error instanceof Error ? error.message : "Unknown error"); 354 | } 355 | } 356 | 357 | async function getRuns(repo: string): Promise<string[] | Error> { 358 | try { 359 | const response = await fetch( 360 | "https://api.github.com/repos/" + repo + "/actions/runs?per_page=100" 361 | ); 362 | if (response.status === 403) { 363 | return new Error("API rate limit exceeded"); 364 | } 365 | if (response.status === 404) { 366 | return new Error("ERROR in fetching data"); 367 | } 368 | const data: WorkflowRuns = await response.json(); 369 | const statusses = data.workflow_runs.map((item) => 370 | item.conclusion.toLowerCase() 371 | ); 372 | return statusses; 373 | } catch (error) { 374 | return new Error(error instanceof Error ? error.message : "Unknown error"); 375 | } 376 | } 377 | 378 | async function getCommunityProfile(repo: string): Promise<any | Error> { 379 | try { 380 | const response = await fetch( 381 | "https://api.github.com/repos/" + repo + "/community/profile" 382 | ); 383 | if (response.status === 403) { 384 | return new Error("API rate limit exceeded"); 385 | } 386 | if (response.status === 404) { 387 | return new Error("ERROR in fetching data"); 388 | } 389 | const data = await response.json(); 390 | return data; 391 | } catch (error) { 392 | return new Error(error instanceof Error ? error.message : "Unknown error"); 393 | } 394 | } 395 | 396 | /** 397 | * https://www.seancdavis.com/posts/extract-github-repo-name-from-url-using-javascript/ 398 | */ 399 | function extractGitHubRepoPath(url: string): string { 400 | if (!url) return "URL not found"; 401 | const match = url.match( 402 | /(^https?:\/\/(www\.)?)?github.com\/(?<owner>[\w.-]+)\/(?<name>[\w.-]+)/ 403 | ); 404 | if (!match || !(match.groups?.owner && match.groups?.name)) 405 | return "URL not found"; 406 | return `${match.groups.owner}/${match.groups.name}`; 407 | } 408 | 409 | function extractGitHubOwnerAndRepo(url: string): [string, string] { 410 | if (!url) return ["URL not found", ""]; 411 | const match = url.match( 412 | /(^https?:\/\/(www\.)?)?github.com\/(?<owner>[\w.-]+)\/(?<name>[\w.-]+)/ 413 | ); 414 | if (!match || !(match.groups?.owner && match.groups?.name)) 415 | return ["URL not found", ""]; 416 | return [match.groups.owner, match.groups.name]; 417 | } 418 | 419 | async function getFileContent( 420 | repo: string, 421 | filename: string 422 | ): Promise<string | Error> { 423 | try { 424 | const response = await fetch( 425 | `https://api.github.com/repos/${repo}/contents/${filename}` 426 | ); 427 | if (response.status === 403) { 428 | return new Error("API rate limit exceeded"); 429 | } 430 | if (response.status === 404) { 431 | return new Error("ERROR in fetching data"); 432 | } 433 | const data = await response.json(); 434 | if (data.content) { 435 | return atob(data.content); 436 | } else { 437 | return ""; 438 | } 439 | } catch (error) { 440 | return new Error(error instanceof Error ? error.message : "Unknown error"); 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | <p align="center"> 2 | <img src='public/susie.svg' style="display: block; 3 | margin-left: auto; 4 | margin-right: auto; 5 | width: 5%;"> 6 | </p> 7 | 8 | ## Changelog 9 | All notable changes to Susie will be documented in this file. 10 | 11 | To update after using `npm version`, run the following command: 12 | 13 | ```bash 14 | auto-changelog --template susie-changelog-template.hbs --commit-limit 500 --backfill-limit 500 --sort-commits date-desc 15 | ``` 16 | 17 | > Note: be aware of the manually entered limit, you might need to increase it. 18 | 19 | ## v1.0.0 - 2023-04-14 20 | 21 | ### Commits 22 | 23 | - Fix colors in Contributors 🎨 [`88adb8b`](https://github.com/philippedeb/susie/commit/88adb8bf639eea1f50e5ac2f9bef3d810cbf92df) 24 | - Fix colors in Contributors 🎨 [`56b41e8`](https://github.com/philippedeb/susie/commit/56b41e8dff74be97f00e25b41281a204630a6e02) 25 | - Fix experimental mode component link and style 👀 [`2d0c41e`](https://github.com/philippedeb/susie/commit/2d0c41e6b11f9cfc2a208084795a12542a5dec88) 26 | - Add dark link style 🔗 [`55afb11`](https://github.com/philippedeb/susie/commit/55afb11c37aae878ae4384154d4c09b2bd25ed71) 27 | - Add experimental mode 👽 [`fc01c6f`](https://github.com/philippedeb/susie/commit/fc01c6f8d8d7d27972a29dd8de66c41f25ff7d45) 28 | - Add experimental mode 👽 [`bc9919a`](https://github.com/philippedeb/susie/commit/bc9919a55d62311e32f71e9362c5708185eaebb1) 29 | - Update BUG_REPORT.yml [`3dd73dc`](https://github.com/philippedeb/susie/commit/3dd73dccd954bd61da53f222649a273da699c027) 30 | - Update Governance text according to UI update [`5b64f6b`](https://github.com/philippedeb/susie/commit/5b64f6bf144deca47ce90ea5d43e5c4de4d96950) 31 | - Optimize guides menu for mobile view 📱 [`3a7c0f4`](https://github.com/philippedeb/susie/commit/3a7c0f4116388b693aa9887d047098f411ba28f1) 32 | - Display different item types properly in inclusive language recommendations instead of displaying the full content (e.g. readme, code of conduct) [`d6a9e74`](https://github.com/philippedeb/susie/commit/d6a9e749ee3fe790e6124a8c7ab5ed07f98c279b) 33 | - Make headers of sections smaller on mobile view 📱 [`15b93ae`](https://github.com/philippedeb/susie/commit/15b93ae198eba652bba68d63e05e110d6b78ccdd) 34 | - Remove unnecessary linebreak [`866db0d`](https://github.com/philippedeb/susie/commit/866db0da8181e5ca5a64f2c4002f468351744292) 35 | - Change URLs to open in new tab 🔗 [`c9a6255`](https://github.com/philippedeb/susie/commit/c9a62553ee4acefef555b3517138c76f90c2c0b1) 36 | - Update UI margins [`84fd352`](https://github.com/philippedeb/susie/commit/84fd3529d9398a6eb2472a11a990b116ebe24e13) 37 | - Governance V2 🚀 Accurate and hyperlinks! [`f4c48a6`](https://github.com/philippedeb/susie/commit/f4c48a690bc3d73788b28557977a003783061d58) 38 | - Contributor component optimized for mobile view 🚀 [`37dd346`](https://github.com/philippedeb/susie/commit/37dd3463314fd8eba5d06280ad2466f3a9600b94) 39 | - Add Contribute Link to open source repository in NavBar [`1348d3d`](https://github.com/philippedeb/susie/commit/1348d3db292cb180e35808dbca7ce17787f7acfc) 40 | - Added Guide about Types of Sustainability [`131435e`](https://github.com/philippedeb/susie/commit/131435e200e22541d6bdd6dbbe5c464ba13ec910) 41 | - Add warning for sentiment metric as it is not fully accurate [`df76425`](https://github.com/philippedeb/susie/commit/df76425e64e88e2f044726c146c66050022e9c41) 42 | - Improve Contributors layout when none are found [`ecaf69e`](https://github.com/philippedeb/susie/commit/ecaf69e58de22cf6314e0c480c68b79d5d4ecbd0) 43 | - Add explanations to Governance metric [`160338e`](https://github.com/philippedeb/susie/commit/160338ecb9e74ecced99c87bcec3814c0e50b4b8) 44 | - Improve font loading [`aabb2c5`](https://github.com/philippedeb/susie/commit/aabb2c5bdf5e45729ff97717a837ff01fe453339) 45 | - Remove redundant console logs [`ca14b69`](https://github.com/philippedeb/susie/commit/ca14b699bb1f547374aa942894ddb0c2e5ec7023) 46 | - Fix alert bug in Workflows [`25f7287`](https://github.com/philippedeb/susie/commit/25f728754b7cedb3ad6bf551a8f5fc0938763855) 47 | - Use local font instead of importing [`18c6b5f`](https://github.com/philippedeb/susie/commit/18c6b5f494307f829d31ae9e7e7ad8f7f9c7bfdd) 48 | - Add 'Contributor' component (v1.0) [`d77b725`](https://github.com/philippedeb/susie/commit/d77b725292c8f540f8e5d2230e5f9d08e1e96aa5) 49 | - Large update for Contributor section [`cf89796`](https://github.com/philippedeb/susie/commit/cf8979619bbb3a1ec7b3a9ed96ee746f7c53c379) 50 | - Remove empty file [`7107700`](https://github.com/philippedeb/susie/commit/710770021d77847395eba66971247815b0014b5e) 51 | - Add more metrics for calculating the bus factor [`d1c9271`](https://github.com/philippedeb/susie/commit/d1c92716f5f63cd98363bd7e1574d867232da7a7) 52 | - Add source and pony factor for bus factor [`7057bd7`](https://github.com/philippedeb/susie/commit/7057bd7da418a9e05d1c83952439f94482d64f9d) 53 | - Update code maintenance branch with main branch [`6c1f058`](https://github.com/philippedeb/susie/commit/6c1f058009c541e911710e1de29d8e5579c9c6fb) 54 | - Add component for bus factor [`c3e88d9`](https://github.com/philippedeb/susie/commit/c3e88d9c3b9d1420d3596fa6e9e96635b236fb91) 55 | - Clean up the sentiment analysis UI [`9a43b0c`](https://github.com/philippedeb/susie/commit/9a43b0c88bafbd44528035e83f9814f4c0443eab) 56 | - Add extra explanation to sentiment analysis component [`8183caf`](https://github.com/philippedeb/susie/commit/8183cafc1e3e6b9fe824ab51fa8cce5d4c560630) 57 | - Change source of inclusive language guide [`c8f822a`](https://github.com/philippedeb/susie/commit/c8f822a57307dfdc48b9836a338dceb3875bfb03) 58 | - Fix final things for sentiment analysis UI [`75a7a08`](https://github.com/philippedeb/susie/commit/75a7a0871ce772ac89afe34cce7adfa7e2720265) 59 | - Implement sentiment analysis [`cc59141`](https://github.com/philippedeb/susie/commit/cc59141edb81f6afb2d259985f6e1976a33bc741) 60 | - Fix bug in inclusive language checks [`0b62eee`](https://github.com/philippedeb/susie/commit/0b62eeecc0f944a8ac27a92e7493f66eaa7241e4) 61 | - Add source to inclusive language guide [`fb8b8e4`](https://github.com/philippedeb/susie/commit/fb8b8e485ccf7417b7e3e45b662b2465f404fe95) 62 | - Create files for sentiment analysis [`87f5a35`](https://github.com/philippedeb/susie/commit/87f5a351435fc10ada2879066d7ffb75a580acf1) 63 | - Update README.md [`939c7f3`](https://github.com/philippedeb/susie/commit/939c7f3b766180c9d31f70af4b6613ff753a45cb) 64 | - Create README.md [`844c75c`](https://github.com/philippedeb/susie/commit/844c75c364f376c4a7a77361e27698b6486abfc6) 65 | - Create CONTRIBUTING.md [`2a93102`](https://github.com/philippedeb/susie/commit/2a93102d853bbb3d5320a31b68997cbe4946ca19) 66 | - Create CODE_OF_CONDUCT.md [`bc78d39`](https://github.com/philippedeb/susie/commit/bc78d39917514027392f988eb91a4f203787bc75) 67 | - Add LICENSE [`8f6e172`](https://github.com/philippedeb/susie/commit/8f6e1727091d62e35bf1b0b8fece35a7f8f3e03b) 68 | - Dashboard scaled for mobile view 🔍 [`1f2c015`](https://github.com/philippedeb/susie/commit/1f2c0159539b947edc1be61a6f27ce855b13e296) 69 | - Scale elements for mobile view 🔍 [`8cd1ded`](https://github.com/philippedeb/susie/commit/8cd1dedcbf645b6d700be074518ced5eb8ce0091) 70 | - Mobile support for new home page [`f6dda87`](https://github.com/philippedeb/susie/commit/f6dda87b7a3a579c7fd5f67f7b506d900f78667e) 71 | - Important homepage fix 🚀 [`3629593`](https://github.com/philippedeb/susie/commit/3629593c1b2c7877bff3df372339c2f9edd5edfe) 72 | - Add pruneData function to add other in pie chart [`daa0486`](https://github.com/philippedeb/susie/commit/daa0486c5aa8011c65548b46cddf1089cf2290f6) 73 | - Add more functionality [`a26734b`](https://github.com/philippedeb/susie/commit/a26734b1268a8f5cdbf9129580c03e05a3932e38) 74 | - Use language piechart for contributors (temporary) [`2420e51`](https://github.com/philippedeb/susie/commit/2420e513d440d40855d3c4b6c7efa3244a8729b7) 75 | - Commit out other fetchings [`6f33729`](https://github.com/philippedeb/susie/commit/6f3372974f4214e58af414c83852d3409dfb0113) 76 | - Add logic file to contributors [`8b5501c`](https://github.com/philippedeb/susie/commit/8b5501c8c454bf8c444201005a8c072b0fc700cb) 77 | - Adjust data handling commitAuthorDates [`af38290`](https://github.com/philippedeb/susie/commit/af382903480aa27db9ec943b3fce856a941e8333) 78 | - Delete package-lock.json [`7930063`](https://github.com/philippedeb/susie/commit/793006369cd8655f9ac79f677d73f70f911db080) 79 | - Update gitignore [`21ac128`](https://github.com/philippedeb/susie/commit/21ac128528a687027da62ca3cd9412f5e7e29faa) 80 | - Add contributors page [`d386a34`](https://github.com/philippedeb/susie/commit/d386a34c4d63440e15437720b21bcce87622e46c) 81 | - Merge with main [`0233ca1`](https://github.com/philippedeb/susie/commit/0233ca1a13fe0e25828bf76e69c6e136e7549d94) 82 | - Added console logs for debugging error status [`4d0d19e`](https://github.com/philippedeb/susie/commit/4d0d19ee6572523d7b516cb9972ab680d92a25c9) 83 | - Allow 404s for fetching specific data [`1e70fb4`](https://github.com/philippedeb/susie/commit/1e70fb46b59a2c2b7035b0dfdf6eb3cd5c31bc12) 84 | - Sophisticated error handling v1.0 [`55c9d06`](https://github.com/philippedeb/susie/commit/55c9d068832d32a087e0e3c30394761660a5cf45) 85 | - Hide sidebar on mobile and small tablets [`b8a17d8`](https://github.com/philippedeb/susie/commit/b8a17d8dc28c462edeec558a9aa4bad12e2076c5) 86 | - Improve mobile layout validation warning searchbar [`e8fb928`](https://github.com/philippedeb/susie/commit/e8fb928099887907d9367f20bdb12338d7c79819) 87 | - Remove feature causing bugs (TODO for later) [`070691c`](https://github.com/philippedeb/susie/commit/070691c1e889f837ff12aa01fd7ad6c6d9c513fa) 88 | - Attempt to remove sidebar on mobile [`6e3177e`](https://github.com/philippedeb/susie/commit/6e3177eace4fe80696ce9afe849b7ac6dae3d86a) 89 | - Many new features and GUI upgrades [`7e3c4ef`](https://github.com/philippedeb/susie/commit/7e3c4efdc24167f56e75ed87e88cf55bd0504d40) 90 | - Added more governance checks and data not loading checks [`4df7b12`](https://github.com/philippedeb/susie/commit/4df7b128c25c7ab87100cbf2e36a82f992850790) 91 | - Add warning when max capacity is reached [`8841759`](https://github.com/philippedeb/susie/commit/8841759409ebc3ff4559275124ef1149c8f8bc93) 92 | - Updated inclusive language guide [`de19e8f`](https://github.com/philippedeb/susie/commit/de19e8f2b156ede702d636e8b516aa464ab3c2f9) 93 | - Updated energy consumption guide [`64c0f3f`](https://github.com/philippedeb/susie/commit/64c0f3f7e9ac39db64f0acec16719804e761d878) 94 | - Fix CI/CD progress bar and change layout [`f5d4db3`](https://github.com/philippedeb/susie/commit/f5d4db310e3adc0c5c97471f5efc1d1d9bcbe3a9) 95 | - GUI update programming languages [`ae1a56c`](https://github.com/philippedeb/susie/commit/ae1a56c9ce598e0aeb2c7ac4b958f26b078ff6f6) 96 | - Added GUI for no programming languages found [`1efc692`](https://github.com/philippedeb/susie/commit/1efc69216268b9abf9805511aae52f66ebd55dda) 97 | - Add profanity check to inclusivity check [`86b9ab8`](https://github.com/philippedeb/susie/commit/86b9ab8df2a52cd175d2ee60f03e94c08cbb88f4) 98 | - Mobile improvement searchbar [`baf19a0`](https://github.com/philippedeb/susie/commit/baf19a01878c65fe372fcacd1d135436a83a4cb1) 99 | - Improve sidebar (mobile support) [`5e125a6`](https://github.com/philippedeb/susie/commit/5e125a6071dde1d321dc10cad95a912bcc42be25) 100 | - Changed Dashboard layout [`02487ac`](https://github.com/philippedeb/susie/commit/02487acdddda717f7857f43a07014c9f6f8f0d62) 101 | - Added inclusive statement and proper looking recommendations listed by target [`a2818f1`](https://github.com/philippedeb/susie/commit/a2818f1463bfe10d046813601c70342ea1630667) 102 | - Remove unnecessary print [`65cdb0e`](https://github.com/philippedeb/susie/commit/65cdb0ef90db3fc3946e46f07b074746700ae903) 103 | - Finalise workflow analyser [`d34c629`](https://github.com/philippedeb/susie/commit/d34c62929e8feabc7f23fb03efccaa8fad49055c) 104 | - Added profanity check [`8d90e81`](https://github.com/philippedeb/susie/commit/8d90e8178086d2c79080c95151aae1d5a475d3ba) 105 | - Move get dates functionality to new fetcher [`fe97fd1`](https://github.com/philippedeb/susie/commit/fe97fd10edff835cfb44bef0ce7b54f19ace2a10) 106 | - Add working progressbar [`d9f761b`](https://github.com/philippedeb/susie/commit/d9f761b59be484a803d9021050152d364d9b0c53) 107 | - Add guide to guide page [`2cd7b8e`](https://github.com/philippedeb/susie/commit/2cd7b8e50f413ec609f85bf4410083a6a8e5f19f) 108 | - Start on dashboard page [`7fa9b1d`](https://github.com/philippedeb/susie/commit/7fa9b1d9603e7f60d542758147d7088a778183e5) 109 | - Add energy consumption guide [`4b8ca7f`](https://github.com/philippedeb/susie/commit/4b8ca7f8dcfd42835c7801ca0aaafff59fe4e870) 110 | - Fix percentage calculation [`40295ef`](https://github.com/philippedeb/susie/commit/40295ef63cbd228980e45e1f4824f5d1d45d381e) 111 | - Add margin at the bottom of guide [`ae6c036`](https://github.com/philippedeb/susie/commit/ae6c036d8cc731a10a866c7db4862e8b746f9242) 112 | - Implement per_page=100 for all items in the fetcher [`6837ae4`](https://github.com/philippedeb/susie/commit/6837ae4cdd5659bc87beaafa0ba490dc5cffe129) 113 | - Show amount of failures instead of successes [`b29d1c7`](https://github.com/philippedeb/susie/commit/b29d1c77ca33544fdd584aa7f0141a3b8245e625) 114 | - Create Language Inclusivity Guide page [`947f1ef`](https://github.com/philippedeb/susie/commit/947f1ef967ca6c11b22d8b365ef021ec64ab5dba) 115 | - Add since parameter to getCommits and getIssues [`f2cd861`](https://github.com/philippedeb/susie/commit/f2cd861ae01462fa1a9de36df0aeb3288e6ab7b8) 116 | - Implement getting the issues from a repo and checking for inclusivity [`bec81d7`](https://github.com/philippedeb/susie/commit/bec81d7f3e96545d164d666a67b12d25763f5c4a) 117 | - Add simple CI/CD check [`d2aa64f`](https://github.com/philippedeb/susie/commit/d2aa64f9be42cbc270080ef3f212ab3f2d13895e) 118 | - Adapt dependencies in language analyser [`27e6b71`](https://github.com/philippedeb/susie/commit/27e6b7105535b572c69ae985985cca602a72b21f) 119 | - Clean up language datastructures [`ca27711`](https://github.com/philippedeb/susie/commit/ca27711252a791790268bfa101f57f6293ee1093) 120 | - Improve mobile layout (homescreen/searchbar) [`08f8466`](https://github.com/philippedeb/susie/commit/08f8466c6cf24af19def6de1be9f209cc6aee9bb) 121 | - Fix deployment issues due to local IDE refactoring bug [`2b7b767`](https://github.com/philippedeb/susie/commit/2b7b76788e02f6d8b93abf52cc8aea3576f42230) 122 | - Fixed local refactoring glitch (never use "key" in your Props!) [`44f2910`](https://github.com/philippedeb/susie/commit/44f29101190b7b562da18b15a0d11be88209e75f) 123 | - Added Dashboard and Guides [`f246536`](https://github.com/philippedeb/susie/commit/f24653657770ac27306d73ba837be9b3f8e6b1b4) 124 | - Fixed search bar on mobile [`b8b8699`](https://github.com/philippedeb/susie/commit/b8b8699b05c3e909e39f046b043c7654e80d5236) 125 | - Fixed mobile navbar menu [`bfc0ce9`](https://github.com/philippedeb/susie/commit/bfc0ce99360b0bc551c5c2f5be64901a5690c149) 126 | - Big update: Guides are here! [`dbe9100`](https://github.com/philippedeb/susie/commit/dbe91008c5bfe8ffe22a25c134fbbd6e51548135) 127 | - Clean up project structure and Dashboard [`fdade86`](https://github.com/philippedeb/susie/commit/fdade86dc4a0dd43f06e6d0fe1612e72220acee6) 128 | - Added Piechart and recommendations for programming languages [`80c4167`](https://github.com/philippedeb/susie/commit/80c4167a64a4d3ca685d9b9cb52b38a72463c35a) 129 | - Merged + restructured + implemented in GUI: 3-programming-language-detection [`aee9b66`](https://github.com/philippedeb/susie/commit/aee9b66fd57e86633a6b4e8375f53960a9857c83) 130 | - Add Tooltip functionality [`b69e2ff`](https://github.com/philippedeb/susie/commit/b69e2ffbf4c0d16a76130a8124dfddee9621bbdb) 131 | - Write some tooltips [`dd1cdb8`](https://github.com/philippedeb/susie/commit/dd1cdb8c46800e882fde2822eda3df39a7f0c12c) 132 | - Change message after analysis [`9c53d13`](https://github.com/philippedeb/susie/commit/9c53d1387a158a70a9e83ad2af339df67f25b1c2) 133 | - Fill table of programming language energy usage [`7bea1e4`](https://github.com/philippedeb/susie/commit/7bea1e43f570d65d4a304302636c769eaaf2b8ef) 134 | - Checks for license and readme [`b0f8836`](https://github.com/philippedeb/susie/commit/b0f8836f1aacf52b2c4ea03a41528ee477b249a6) 135 | - Created high quality Dashboard (big update) [`9622dcf`](https://github.com/philippedeb/susie/commit/9622dcf3a3025d75afe7115dd8010421c8801e8b) 136 | - Basic language analysis [`a3b51d2`](https://github.com/philippedeb/susie/commit/a3b51d207957c93c34c7a8417390cafbc9804998) 137 | - Added search bar validation with warnings [`8489855`](https://github.com/philippedeb/susie/commit/8489855cf8f18c0f51e0a467c51844a638f7d62d) 138 | - Add inclusive language recommendations to Dashboard [`c50791c`](https://github.com/philippedeb/susie/commit/c50791c74ca10728bb452d1b87a88ebb7bfed0ed) 139 | - Link fetcher to Dashboard (simple) [`d7f76f5`](https://github.com/philippedeb/susie/commit/d7f76f5e7d44e819c35f83175bf54981ba078e17) 140 | - Fix passing URL SearchBar to Dashboard [`4b5a65c`](https://github.com/philippedeb/susie/commit/4b5a65c7a80948f7f55ba3a6faa299cbbee62a9e) 141 | - Add animation Home [`e31cbbc`](https://github.com/philippedeb/susie/commit/e31cbbc50bc951871e42d636055d84cc9ceb0cc1) 142 | - Restructured the project and added routing (dashboard + 404) [`f81c723`](https://github.com/philippedeb/susie/commit/f81c723d56dcfde7b4ebca8698939efe1a1cb325) 143 | - Fix bug in regex + implement returning the language recommendations [`868432c`](https://github.com/philippedeb/susie/commit/868432c681ee991fe0a19d629394a26505802944) 144 | - Add get dates function [`ad07dda`](https://github.com/philippedeb/susie/commit/ad07ddac80257abc028ccffd6f1b285e05481cad) 145 | - Merge with dev and add technical_metrics file [`14c5b7b`](https://github.com/philippedeb/susie/commit/14c5b7b13e66cd0325b188f3b20dcf575dce46e4) 146 | - Implement getting branches, commits and pull requests by using the UI [`f6ed773`](https://github.com/philippedeb/susie/commit/f6ed7739a289a0135512cc25c393e583bff9b87d) 147 | - Remove useless comment [`30fd7d4`](https://github.com/philippedeb/susie/commit/30fd7d4803bb0a9fe2814240ff3f3c26268495e3) 148 | - Fix typo in inclusivity checks [`9fc05d7`](https://github.com/philippedeb/susie/commit/9fc05d711f022035abe9fbfbf4e94add14029a52) 149 | - Implement getting the pull requests of a repo [`5d70637`](https://github.com/philippedeb/susie/commit/5d70637b4e974e629b0f01e7365bdf64a2323b77) 150 | - Start on language analysis [`526b871`](https://github.com/philippedeb/susie/commit/526b87163536bf20dca6d2eddddcd84e0e1a6160) 151 | - Implement checking for inclusive languages on branches and commits [`f9fbc54`](https://github.com/philippedeb/susie/commit/f9fbc545e8e85fbffe069eeeb2a2008222e0525c) 152 | - Fix response link [`0fbc57e`](https://github.com/philippedeb/susie/commit/0fbc57e72181b8284ca065033e9afdb7df8f23f2) 153 | - Add button functionality [`a6f0654`](https://github.com/philippedeb/susie/commit/a6f06542dfff1cb100803431c2679aaf121649f3) 154 | - Fix mobile layout [`4f0684d`](https://github.com/philippedeb/susie/commit/4f0684de27c624573634c8cc58b9aaabd7d630d4) 155 | - Fix logo bug [`672cca3`](https://github.com/philippedeb/susie/commit/672cca3a48333eab88432f80644c3f500770b6cd) 156 | - Deploy landing page Susie [`0d7e4f6`](https://github.com/philippedeb/susie/commit/0d7e4f6caf342ae4d167817089f27be0ea62431d) 157 | - Added simple landing page [`bc4d2cc`](https://github.com/philippedeb/susie/commit/bc4d2cc0aad93928d3629071f68a78d5282c79ff) 158 | - Added simple landing page [`8187515`](https://github.com/philippedeb/susie/commit/81875152c561a1d6c10d0c122366e01df8cbfa00) 159 | - Update deploy.yml [`c924756`](https://github.com/philippedeb/susie/commit/c9247564cc08f3420a5ee915add02a8811ce0038) 160 | - fix setup [`78c9433`](https://github.com/philippedeb/susie/commit/78c94334ff9889e617cedcc3270d4df7fd6f6514) 161 | - add deploy workflow [`3fe7ce1`](https://github.com/philippedeb/susie/commit/3fe7ce19c4e0b264c6f696aa855a7b49cfa1e737) 162 | - set up initial files [`17c7625`](https://github.com/philippedeb/susie/commit/17c7625b30429220b5f1b31ef277bb157313ac6f) 163 | 164 | --------------------------------------------------------------------------------