├── .nvmrc ├── .prettierrc.json ├── .prettierignore ├── .vscode └── settings.json ├── src ├── components │ ├── Loader │ │ ├── loader.less │ │ └── Loader.tsx │ ├── Icon │ │ ├── icon.less │ │ └── Icon.tsx │ ├── Footer │ │ ├── footer.less │ │ └── Footer.tsx │ ├── Page │ │ ├── page.less │ │ └── Page.tsx │ ├── Status │ │ ├── status.less │ │ └── Status.tsx │ ├── Text │ │ ├── text.less │ │ └── Text.tsx │ ├── Content │ │ ├── content.less │ │ └── Content.tsx │ ├── Chart │ │ ├── chart.less │ │ ├── Tooltip.tsx │ │ └── Chart.tsx │ ├── Header │ │ ├── header.less │ │ ├── Header.tsx │ │ └── Auth.tsx │ ├── Link │ │ ├── link.less │ │ └── Link.tsx │ ├── Button │ │ ├── Button.tsx │ │ └── button.less │ ├── Box │ │ ├── Box.tsx │ │ └── box.less │ ├── __tests__ │ │ ├── Link.test.tsx │ │ └── Error.test.tsx │ ├── ProgressBar │ │ ├── progressBar.less │ │ └── ProgressBar.tsx │ ├── TextInput │ │ ├── textInput.less │ │ └── TextInput.tsx │ ├── Table │ │ ├── table.less │ │ └── Table.tsx │ ├── Error │ │ └── Error.tsx │ └── Menu │ │ ├── Menu.tsx │ │ └── menu.less ├── index.tsx ├── hooks │ ├── useRemount.ts │ ├── useFirebase.ts │ ├── useTokenStore.ts │ ├── useStateRef.ts │ ├── useRouteParams.ts │ ├── useClickOutside.ts │ ├── useLocalStorage.ts │ ├── useRouter.ts │ ├── __tests__ │ │ └── useReposStore.test.ts │ ├── useReposStore.ts │ └── useIssues.ts ├── utils │ ├── keys.ts │ ├── lines │ │ ├── index.ts │ │ ├── __tests__ │ │ │ ├── idealLine.test.ts │ │ │ ├── actualLine.test.ts │ │ │ └── trendLine.test.ts │ │ ├── actualLine.ts │ │ ├── idealLine.ts │ │ └── trendLine.ts │ ├── css.ts │ ├── object.ts │ ├── __tests__ │ │ ├── scales.test.ts │ │ ├── map.test.ts │ │ ├── format.test.ts │ │ └── sort.test.ts │ ├── map.ts │ ├── format.ts │ ├── scales.ts │ ├── sort.ts │ ├── addStats.ts │ └── getIssues.ts ├── styles │ ├── app.less │ └── fonts.less ├── config.ts ├── pages │ ├── NotFound.tsx │ ├── AddRepo.tsx │ ├── Repos.tsx │ ├── Milestone.tsx │ └── Milestones.tsx ├── App.tsx ├── providers │ ├── RemountProvider.tsx │ └── FirebaseProvider.tsx ├── routes.ts ├── interfaces.ts └── queries │ ├── GetMilestoneIssues.ts │ └── GetRepoIssues.ts ├── screenshot.png ├── public ├── fonts │ ├── fontello.eot │ ├── fontello.ttf │ ├── fontello.woff │ ├── museo-sans-500.eot │ ├── museo-sans-500.otf │ ├── museo-sans-500.ttf │ ├── museo-sans-500.woff │ ├── museo-slab-500.eot │ ├── museo-slab-500.otf │ ├── museo-slab-500.ttf │ ├── museo-slab-500.woff │ └── fontello.svg └── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── about.txt ├── .husky └── pre-commit ├── vitest.config.js ├── vite.config.ts ├── TODO.md ├── index.html ├── tsconfig.json ├── cli.js ├── package.json ├── .gitignore ├── README.md └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.12.1 -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/queries/ 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2 3 | } 4 | -------------------------------------------------------------------------------- /src/components/Loader/loader.less: -------------------------------------------------------------------------------- 1 | .loader { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/components/Icon/icon.less: -------------------------------------------------------------------------------- 1 | .icon--family-fontello { 2 | font-family: "Fontello"; 3 | } 4 | -------------------------------------------------------------------------------- /public/fonts/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/fonts/fontello.eot -------------------------------------------------------------------------------- /public/fonts/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/fonts/fontello.ttf -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/fonts/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/fonts/fontello.woff -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/fonts/museo-sans-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/fonts/museo-sans-500.eot -------------------------------------------------------------------------------- /public/fonts/museo-sans-500.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/fonts/museo-sans-500.otf -------------------------------------------------------------------------------- /public/fonts/museo-sans-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/fonts/museo-sans-500.ttf -------------------------------------------------------------------------------- /public/fonts/museo-sans-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/fonts/museo-sans-500.woff -------------------------------------------------------------------------------- /public/fonts/museo-slab-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/fonts/museo-slab-500.eot -------------------------------------------------------------------------------- /public/fonts/museo-slab-500.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/fonts/museo-slab-500.otf -------------------------------------------------------------------------------- /public/fonts/museo-slab-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/fonts/museo-slab-500.ttf -------------------------------------------------------------------------------- /public/fonts/museo-slab-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radekstepan/burnchart/HEAD/public/fonts/museo-slab-500.woff -------------------------------------------------------------------------------- /src/components/Footer/footer.less: -------------------------------------------------------------------------------- 1 | #footer { 2 | padding: 16px; 3 | font-size: 14px; 4 | color: #696f8c; 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Page/page.less: -------------------------------------------------------------------------------- 1 | .page { 2 | max-width: 800px; 3 | justify-content: center; 4 | display: flex; 5 | margin: 0 auto; 6 | padding: 16px; 7 | } 8 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: "happy-dom", 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/Status/status.less: -------------------------------------------------------------------------------- 1 | .status { 2 | position: relative; 3 | 4 | &__sub { 5 | margin-top: 10px; 6 | font-size: 14px; 7 | color: #696f8c; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Text/text.less: -------------------------------------------------------------------------------- 1 | .text { 2 | &__title { 3 | font-size: 24px; 4 | font-weight: 700; 5 | } 6 | 7 | &__paragraph { 8 | margin-bottom: 12px; 9 | color: #696f8c; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./App"; 4 | 5 | const root = createRoot(document.getElementById("root")!); 6 | 7 | root.render(); 8 | -------------------------------------------------------------------------------- /src/hooks/useRemount.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { RemountContext } from "../providers/RemountProvider"; 3 | 4 | const useRemount = () => useContext(RemountContext); 5 | 6 | export default useRemount; 7 | -------------------------------------------------------------------------------- /src/hooks/useFirebase.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { FirebaseContext } from "../providers/FirebaseProvider"; 3 | 4 | const useFirebase = () => useContext(FirebaseContext); 5 | 6 | export default useFirebase; 7 | -------------------------------------------------------------------------------- /src/components/Content/content.less: -------------------------------------------------------------------------------- 1 | .content { 2 | flex: 1; 3 | 4 | &--slim { 5 | max-width: 560px; 6 | } 7 | 8 | .text__title { 9 | margin: 0 auto; 10 | margin-top: 10px; 11 | margin-bottom: 20px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/keys.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates a key by joining a list of strings on forward slashes (/). 3 | * @param args The list of strings to join. 4 | * @returns The joined string. 5 | */ 6 | const k = (...args: any[]) => args.flat().join("/"); 7 | 8 | export default k; 9 | -------------------------------------------------------------------------------- /src/styles/app.less: -------------------------------------------------------------------------------- 1 | @import "/node_modules/normalize.css/normalize.css"; 2 | 3 | // Fonts. 4 | @serif_font: "MuseoSlab500Regular", serif; 5 | @sans_serif_font: "MuseoSans500Regular", sans-serif; 6 | 7 | #root { 8 | font-family: @sans_serif_font; 9 | color: #3e4457; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/lines/index.ts: -------------------------------------------------------------------------------- 1 | import actualLine from "./actualLine"; 2 | import idealLine from "./idealLine"; 3 | import trendLine from "./trendLine"; 4 | 5 | export const FORMAT = "YYYY-MM-DDTHH:mm:ss[Z]"; 6 | 7 | export const actual = actualLine; 8 | export const ideal = idealLine; 9 | export const trend = trendLine; 10 | -------------------------------------------------------------------------------- /src/utils/css.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Concatenates a list of strings into a single string, with spaces between them. 3 | * Only truthy values will be included. 4 | * @param list The list of strings to concatenate. 5 | * @returns The concatenated string. 6 | */ 7 | export const cls = (...list: any[]): string => list.filter(Boolean).join(" "); 8 | -------------------------------------------------------------------------------- /src/hooks/useTokenStore.ts: -------------------------------------------------------------------------------- 1 | import useLocalStorage from "./useLocalStorage"; 2 | 3 | /** 4 | * A hook to manage and persist the GitHub personal access token used for API requests. 5 | * @returns A tuple with the token, a function to update it, and a function to delete it. 6 | */ 7 | const useTokenStore = () => useLocalStorage("token"); 8 | 9 | export default useTokenStore; 10 | -------------------------------------------------------------------------------- /public/favicon/about.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following font: 2 | 3 | - Font Title: Fira Code 4 | - Font Author: Copyright 2014-2020 The Fira Code Project Authors (https://github.com/tonsky/FiraCode) 5 | - Font Source: http://fonts.gstatic.com/s/firacode/v21/uU9eCBsR6Z2vfE9aq3bL0fxyUs4tcw4W_ONrFVfxN87gsj0.ttf 6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL)) 7 | -------------------------------------------------------------------------------- /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 | server: { 7 | port: 1234, 8 | }, 9 | plugins: [react()], 10 | resolve: { 11 | alias: { 12 | "node-fetch": "cross-fetch", 13 | }, 14 | }, 15 | define: { 16 | global: "window", 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/Chart/chart.less: -------------------------------------------------------------------------------- 1 | .chart { 2 | position: relative; 3 | margin-bottom: 20px; 4 | 5 | .tooltip { 6 | position: absolute; 7 | font-size: 10px; 8 | line-height: 14px; 9 | font-family: monospace; 10 | border-radius: 1px; 11 | padding: 6px; 12 | color: #fff; 13 | background: rgba(0, 0, 0, 0.7); 14 | pointer-events: none; 15 | user-select: none; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Header/header.less: -------------------------------------------------------------------------------- 1 | #header { 2 | display: flex; 3 | align-items: center; 4 | padding: 16px; 5 | border-bottom: 1px solid #e6e8f0; 6 | 7 | .links { 8 | display: flex; 9 | flex: 1; 10 | align-items: center; 11 | 12 | .logo { 13 | padding: 10px; 14 | font-size: 26px; 15 | } 16 | 17 | .item { 18 | padding: 4px; 19 | margin: 0 8px; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ... 4 | 5 | ## Nice to have 6 | 7 | - do not refetch milestones when navigating from repos 8 | - convert `x` to a Date so we don't convert multiple times over 9 | - render milestone end date as a tick if overdue; https://apexcharts.com/docs/annotations/ 10 | - back button to navigate away from milestone page 11 | - d3/d3 has no milestones 12 | - use Vercel and Next.js Github auth instead of Firebase 13 | - semantic-release, GitHub actions, push to npm 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/utils/object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a new object by picking the specified keys from an existing object. 3 | * @param obj The object to pick keys from. 4 | * @param keys The keys to pick from the object. 5 | * @returns A partial object with only the specified keys. 6 | */ 7 | export function pick(obj: T, keys: K[]): Pick { 8 | const result: any = {}; 9 | for (const key of keys) { 10 | result[key] = obj[key]; 11 | } 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Link/link.less: -------------------------------------------------------------------------------- 1 | .link { 2 | color: inherit; 3 | text-decoration: none; 4 | user-select: none; 5 | transition: 120ms all ease-in-out; 6 | cursor: pointer; 7 | 8 | &:hover { 9 | color: #212532; 10 | } 11 | 12 | &:active { 13 | color: #111; 14 | } 15 | 16 | &--styled { 17 | color: #3366ff; 18 | user-select: initial; 19 | 20 | &:hover { 21 | color: #2952cc; 22 | } 23 | 24 | &:active { 25 | color: #1f3d99; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/useStateRef.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | 3 | /** 4 | * A hook that allows creating a reference to a DOM element. 5 | * @returns An array that contains the DOM element reference and a function to set it. 6 | */ 7 | const useStateRef = (): [T | null, (node: unknown) => void] => { 8 | const [el, setEl] = useState(null); 9 | 10 | const setRef = useCallback((node: unknown) => { 11 | setEl(node as T); 12 | }, []); 13 | 14 | return [el, setRef]; 15 | }; 16 | 17 | export default useStateRef; 18 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | // Firebase. 3 | firebase: { 4 | apiKey: "AIzaSyD_kfzkAPA87PoRFIZa8JEzZkT66CqUDpU", 5 | authDomain: "burnchart.firebaseapp.com", 6 | }, 7 | // Data source provider. 8 | provider: "github", 9 | // Chart configuration. 10 | chart: { 11 | // Days we are not working. Mon = 1 12 | off_days: [], 13 | // How does a size label look like? 14 | size_label: /^size (\d+)$/, 15 | // Process all issues as one size (ONE_SIZE) or use labels (LABELS). 16 | points: "ONE_SIZE", 17 | }, 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /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" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "../Link/Link"; 3 | import "./footer.less"; 4 | 5 | function Footer() { 6 | return ( 7 | 20 | ); 21 | } 22 | 23 | export default Footer; 24 | -------------------------------------------------------------------------------- /src/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Content from "../components/Content/Content"; 3 | import Box, { BoxType } from "../components/Box/Box"; 4 | 5 | const NotFound: React.FC = () => { 6 | useEffect(() => { 7 | document.title = "404"; 8 | }, []); 9 | 10 | return ( 11 | 12 | 13 | We're sorry, but the page you are looking for could not be found. Please 14 | check the URL or try navigating to a different page. 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default NotFound; 21 | -------------------------------------------------------------------------------- /src/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { cls } from "../../utils/css"; 3 | import "./button.less"; 4 | 5 | interface Props { 6 | withInput?: boolean; 7 | onClick: (evt: unknown) => void; 8 | children: ReactNode; 9 | } 10 | 11 | const Button: React.FC = ({ withInput, onClick, children }) => { 12 | const className = "button"; 13 | 14 | return ( 15 | 21 | ); 22 | }; 23 | 24 | export default Button; 25 | -------------------------------------------------------------------------------- /src/components/Text/Text.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { cls } from "../../utils/css"; 3 | import "./text.less"; 4 | 5 | interface Props { 6 | className?: string; 7 | children: ReactNode; 8 | } 9 | 10 | const Text: React.FC = ({ 11 | suffix, 12 | className, 13 | children, 14 | }) =>
{children}
; 15 | 16 | export const Title: React.FC = (props) => ( 17 | 18 | ); 19 | 20 | export const Paragraph: React.FC = (props) => ( 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RemountProvider from "./providers/RemountProvider"; 3 | import FirebaseProvider from "./providers/FirebaseProvider"; 4 | import Page from "./components/Page/Page"; 5 | import Header from "./components/Header/Header"; 6 | import Footer from "./components/Footer/Footer"; 7 | import "./styles/app.less"; 8 | import "./styles/fonts.less"; 9 | 10 | function App() { 11 | return ( 12 | 13 | 14 |
15 | 16 |