diff --git a/components/IssueComparison.tsx b/components/IssueComparison.tsx index de0dc8e..9da55e9 100644 --- a/components/IssueComparison.tsx +++ b/components/IssueComparison.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState } from "react"; +import { ReactNode, useEffect, useState } from "react"; import { Context } from "../utils/AppContext"; import { IssueDetail } from "../utils/linear"; import useIssues from "../utils/useIssues"; @@ -26,6 +26,10 @@ const IssueComparison = ({ localStorageKey ); + useEffect(() => { + loadIssues(); + }, [loadIssues]); + const [order, setOrder] = useState>([]); return issueList.length > 0 ? ( diff --git a/package.json b/package.json index 94001c6..c91125c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "next": "12.2.5", "react": "18.2.0", "react-dom": "18.2.0", - "react-use": "^17.4.0" + "react-use": "^17.4.0", + "tiny-invariant": "^1.3.1", + "uuid": "^9.0.0" }, "devDependencies": { "@types/dedent": "^0.7.0", @@ -25,6 +27,7 @@ "@types/node": "18.7.3", "@types/react": "18.0.17", "@types/react-dom": "18.0.6", + "@types/uuid": "^8.3.4", "eslint": "8.22.0", "eslint-config-next": "12.2.5", "typescript": "4.7.4" diff --git a/pages/settings.page.tsx b/pages/settings.page.tsx index d810ec2..c0bcdbe 100644 --- a/pages/settings.page.tsx +++ b/pages/settings.page.tsx @@ -1,11 +1,123 @@ +import compareDesc from "date-fns/compareDesc"; +import formatDistance from "date-fns/formatDistance"; +import parseISO from "date-fns/parseISO"; import type { NextPage } from "next"; +import { Fragment, useContext } from "react"; +import invariant from "tiny-invariant"; import Layout from "../components/Layout"; import SettingsForm from "../components/SettingsForm"; +import AppContext from "../utils/AppContext"; +import { EloTournament } from "../utils/elo"; +import useIssues from "../utils/useIssues"; -const SettingsPage: NextPage = () => ( - - - -); +const EFFORT_LOCAL_STORAGE_KEY = "effort_comparisons"; +const VALUE_LOCAL_STORAGE_KEY = "value_comparisons"; + +enum ComparisonType { + Effort = "Effort", + Value = "Value", +} + +const SettingsPage: NextPage = () => { + const { data } = useContext(AppContext); + + const effortComparisons: EloTournament["comparisons"] = JSON.parse( + window.localStorage.getItem(EFFORT_LOCAL_STORAGE_KEY) ?? "[]" + ); + const valueComparisons: EloTournament["comparisons"] = JSON.parse( + window.localStorage.getItem(VALUE_LOCAL_STORAGE_KEY) ?? "[]" + ); + + const comparisons = effortComparisons + .map((c) => ({ ...c, type: ComparisonType.Effort })) + .concat(valueComparisons.map((c) => ({ ...c, type: ComparisonType.Value }))) + .sort((a, b) => compareDesc(parseISO(a.date), parseISO(b.date))); + + return ( + +

Config

+ +

History

+
+ {comparisons.map((comparison) => { + return ( + +
+ {formatDistance(parseISO(comparison.date), new Date(), { + addSuffix: true, + includeSeconds: true, + })} +
+
+
+
+ ); + })} +
+
+ ); +}; + +const Label = ({ + comparison, +}: { + comparison: EloTournament["comparisons"][number] & { type: ComparisonType }; +}) => { + const context = useContext(AppContext); + + const { removeComparison: removeEffortComparison } = useIssues( + context.credentials, + context.data.issues, + EFFORT_LOCAL_STORAGE_KEY + ); + const { removeComparison: removeValueComparison } = useIssues( + context.credentials, + context.data.issues, + VALUE_LOCAL_STORAGE_KEY + ); + + const issueA = context.data.issues.find( + ({ id }) => id === comparison.entities[0] + ); + + const issueB = context.data.issues.find( + ({ id }) => id === comparison.entities[1] + ); + + invariant(issueA); + invariant(issueB); + + switch (comparison.type) { + case ComparisonType.Effort: + return ( + + {issueA.key} is quicker to resolve + than {issueB.key}{" "} + + + ); + case ComparisonType.Value: + return ( + + {issueA.key} should be resolved + before {issueB.key}{" "} + + + ); + } +}; export default SettingsPage; diff --git a/utils/elo.ts b/utils/elo.ts index 67bbd1f..3a94004 100644 --- a/utils/elo.ts +++ b/utils/elo.ts @@ -12,6 +12,7 @@ type Config = { }; type Comparison = { + id: string; entities: [string, string]; result: number; date: string; diff --git a/utils/useIssues.ts b/utils/useIssues.ts index c20bb46..b9e3e66 100644 --- a/utils/useIssues.ts +++ b/utils/useIssues.ts @@ -5,6 +5,7 @@ import getStats from "./getStats"; import { getIssue, IssueDetail, IssueSummary } from "./linear"; import weightedRandomPick from "./weightedRandomPick"; import formatISO from "date-fns/formatISO"; +import { v4 as uuid } from "uuid"; const useIssues = ( credentials: Credentials, @@ -57,17 +58,37 @@ const useIssues = ( } }, [credentials, issues, localStorageKey]); - const addComparison = (entities: [string, string], result: number) => { - const comparisons: EloTournament["comparisons"] = JSON.parse( - window.localStorage.getItem(localStorageKey) ?? "[]" - ); - comparisons.push({ entities, result, date: formatISO(new Date()) }); - window.localStorage.setItem(localStorageKey, JSON.stringify(comparisons)); - }; + const addComparison = useCallback( + (entities: [string, string], result: number) => { + const comparisons: EloTournament["comparisons"] = JSON.parse( + window.localStorage.getItem(localStorageKey) ?? "[]" + ); + comparisons.push({ + id: uuid(), + entities, + result, + date: formatISO(new Date()), + }); + window.localStorage.setItem(localStorageKey, JSON.stringify(comparisons)); + }, + [localStorageKey] + ); + + const removeComparison = useCallback( + (id: string) => { + const comparisons: EloTournament["comparisons"] = JSON.parse( + window.localStorage.getItem(localStorageKey) ?? "[]" + ); + window.localStorage.setItem( + localStorageKey, + JSON.stringify(comparisons.filter((issue) => issue.id !== id)) + ); + }, + [localStorageKey] + ); useEffect(() => { alive.current = true; - if (issues) loadIssues(); return () => { alive.current = false; }; @@ -77,6 +98,7 @@ const useIssues = ( addComparison, issueList, loadIssues, + removeComparison, }; }; diff --git a/yarn.lock b/yarn.lock index 539dae0..d648b74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -241,6 +241,11 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@typescript-eslint/parser@^5.21.0": version "5.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.33.0.tgz#26ec3235b74f0667414613727cb98f9b69dc5383" @@ -2107,6 +2112,11 @@ throttle-debounce@^3.0.1: resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb" integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg== +tiny-invariant@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -2198,6 +2208,11 @@ use-sync-external-store@1.2.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"