From 6dd9fb860a9350fa490a2b2bb0a636101f905d56 Mon Sep 17 00:00:00 2001 From: martinbagshaw Date: Sun, 25 Oct 2020 00:22:06 +0100 Subject: [PATCH] convert logbook list to typescript, organise data and types better --- src/components/App.tsx | 67 +++++++++-------- .../logbook/{Logbook.jsx => Logbook.tsx} | 49 +++++++------ .../logbook/{PageNav.jsx => PageNav.tsx} | 26 +++++-- .../logbook/{Results.jsx => Results.tsx} | 17 +++-- .../logbook/{Search.jsx => Search.tsx} | 50 ++++++++----- .../{SearchReset.jsx => SearchReset.tsx} | 12 ++-- src/components/stats/Stats.jsx | 2 +- src/data/getData.ts | 20 ------ src/utils/common-types.ts | 48 +++++++++++++ src/utils/constants.ts | 7 +- src/utils/{getDate.ts => get-date.ts} | 7 +- src/utils/{formatData.ts => process-date.ts} | 72 +++---------------- src/utils/processed-data.ts | 37 ++++++++++ 13 files changed, 241 insertions(+), 173 deletions(-) rename src/components/logbook/{Logbook.jsx => Logbook.tsx} (52%) rename src/components/logbook/{PageNav.jsx => PageNav.tsx} (81%) rename src/components/logbook/{Results.jsx => Results.tsx} (86%) rename src/components/logbook/{Search.jsx => Search.tsx} (69%) rename src/components/logbook/{SearchReset.jsx => SearchReset.tsx} (74%) delete mode 100644 src/data/getData.ts create mode 100644 src/utils/common-types.ts rename src/utils/{getDate.ts => get-date.ts} (63%) rename src/utils/{formatData.ts => process-date.ts} (59%) create mode 100644 src/utils/processed-data.ts diff --git a/src/components/App.tsx b/src/components/App.tsx index 415ff6d..2dd1c40 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState, FC } from "react"; import styled from "styled-components"; -import { allLogs, OutputObject } from "../utils/formatData"; +import { OutputObject } from "../utils/common-types"; +import { allLogs } from "../utils/processed-data"; import Stats from "./stats/Stats.jsx"; -import Logbook from "./logbook/Logbook.jsx"; +import Logbook from "./logbook/Logbook"; import { breakpoint, colors, fonts, fontSize } from "./common/styleVars"; import { buttonBase } from "./common/Buttons.jsx"; @@ -19,7 +20,7 @@ const Header = styled.header` display: flex; `; -const Button = styled.button<{ readonly isActive: boolean }>` +const ViewButton = styled.button<{ readonly isActive: boolean }>` ${buttonBase}; display: flex; width: 50%; @@ -33,12 +34,6 @@ const Button = styled.button<{ readonly isActive: boolean }>` &:first-child { justify-content: flex-end; } - > span { - user-select: none; - max-width: 23rem; - width: 100%; - display: block; - } &:hover { background-color: ${({ isActive }) => colors[isActive ? "lightRed" : "midGrey"]}; } @@ -50,6 +45,13 @@ const Button = styled.button<{ readonly isActive: boolean }>` `} `; +const ViewButtonText = styled.span` + user-select: none; + max-width: 23rem; + width: 100%; + display: block; +`; + const DailyMessage = styled.p` text-align: center; padding: 0.25rem; @@ -63,7 +65,7 @@ const ViewContainer = styled.div` overflow: hidden; `; -const Views = styled.div<{ readonly isLogbook: boolean }>` +const ViewPanel = styled.div<{ readonly isLogbook: boolean }>` display: flex; width: 200%; ${({ isLogbook }) => @@ -78,19 +80,28 @@ const Views = styled.div<{ readonly isLogbook: boolean }>` `} `; +interface Views { + s: string; + l: string; +} +const views: Views = { + s: "Stats", + l: "Logbook" +}; + interface Filter { day: string; month: string; year: string; } const App: FC = () => { - const [view, setView] = useState("Stats"); + const [activeView, setActiveView] = useState(views.s); const [logs, setLogs] = useState(allLogs); const [message, setMessage] = useState(null); const handleSingleDay = (logs: OutputObject[], filter: Filter) => { const { day, month, year } = filter; - setView("Logbook"); + setActiveView(views.l); setLogs(logs); setMessage( `Showing ${logs.length} ${logs.length === 1 ? "log" : "logs"} for: ${day} ${month} ${year}` @@ -99,37 +110,33 @@ const App: FC = () => { // reset to original useEffect(() => { - if (message && view === "Stats") { + if (message && activeView === views.s) { setLogs(allLogs); setMessage(null); } - }, [message, view]); + }, [message, activeView]); return ( {message && {message}}
- - + {Object.values(views).map(view => ( + setActiveView(view)} + isActive={activeView === view} + aria-label={`${view} View`} + > + {view} + + ))}
- + - +
); diff --git a/src/components/logbook/Logbook.jsx b/src/components/logbook/Logbook.tsx similarity index 52% rename from src/components/logbook/Logbook.jsx rename to src/components/logbook/Logbook.tsx index 4c88c7e..a24f929 100644 --- a/src/components/logbook/Logbook.jsx +++ b/src/components/logbook/Logbook.tsx @@ -1,29 +1,34 @@ -import React, { useState, Fragment } from "react"; +import React, { useState, Fragment, FC } from "react"; import styled from "styled-components"; -import Search from "./Search.jsx"; -import SearchReset from "./SearchReset.jsx"; -import PageNav from "./PageNav.jsx"; -import Results from "./Results.jsx"; +import { OutputObject, DefaultSearch } from "../../utils/common-types"; + +import Search from "./Search"; +import SearchReset from "./SearchReset"; +import PageNav from "./PageNav"; +import Results from "./Results"; import SingleLog from "../singleLog/SingleLog.jsx"; const LogContainer = styled.div` width: 50%; `; -const defaultSearch = { +const defaultSearch: DefaultSearch = { placeholder: "Search by Climb or Crag name...", searchTerm: "", - results: [], + results: undefined, }; -const Logbook = ({ logs }) => { - const [search, setSearch] = useState({ ...defaultSearch }); - const [page, setPage] = useState({ low: 0, high: 50 }); - const [singleLog, setSingleLog] = useState(null); +interface Props { + logs: OutputObject[]; +} +const Logbook: FC = ({ logs }) => { + const [search, setSearch] = useState(defaultSearch); + const [page, setPage] = useState<{low: number, high: number}>({ low: 0, high: 50 }); + const [singleLog, setSingleLog] = useState(undefined); - const handleSearch = value => { - const resultsFind = (value, logs) => { + const handleSearch = (value: string): DefaultSearch | void => { + const findResults = (value: string, logs: OutputObject[]): OutputObject[] | void => { if (value.length < 3) return; return logs.filter(log => { const regex = new RegExp(value, "gi"); @@ -32,11 +37,11 @@ const Logbook = ({ logs }) => { return cl.match(regex) || cr.match(regex); }); }; - const placeholder = value.length > 0 ? "" : "Search by Climb or Crag name..."; - return setSearch({ placeholder, searchTerm: value, results: resultsFind(value, logs) }); + const placeholder: string = value.length > 0 ? "" : "Search by Climb or Crag name..."; + return setSearch({ placeholder, searchTerm: value, results: findResults(value, logs) }); }; - const handlePageChange = direction => { + const handlePageChange = (direction: string) => { let { low: newLow, high: newHigh } = page; if (direction === "older") { setPage({ low: (newLow += 50), high: (newHigh += 50) }); @@ -46,7 +51,8 @@ const Logbook = ({ logs }) => { } }; - const handleSingleView = index => { + // TODO: stricter checking here. Should be ascent-, see OutputObject + const handleSingleView = (index: string): void => { return setSingleLog(logs.find(i => i.key === index)); }; @@ -56,9 +62,12 @@ const Logbook = ({ logs }) => { {!singleLog && ( handleSearch(e.target.value)} + handleSearch={handleSearch} handleSingleView={handleSingleView} + {...search} + // placeholder={search.placeholder} + // results={search.results} + // searchTerm={search.searchTerm} /> @@ -68,7 +77,7 @@ const Logbook = ({ logs }) => { { - setSearch({ ...defaultSearch }); + setSearch(defaultSearch); }} /> diff --git a/src/components/logbook/PageNav.jsx b/src/components/logbook/PageNav.tsx similarity index 81% rename from src/components/logbook/PageNav.jsx rename to src/components/logbook/PageNav.tsx index 50f5886..3fba304 100644 --- a/src/components/logbook/PageNav.jsx +++ b/src/components/logbook/PageNav.tsx @@ -1,6 +1,8 @@ -import React from "react"; +import React, { FC } from "react"; import styled, { css } from "styled-components"; +import { OutputObject } from "../../utils/common-types"; + import useIsWidth from "../common/useIsWidth.jsx"; import Chevron from "../common/icons/Chevron.jsx"; import { buttonBase } from "../common/Buttons.jsx"; @@ -45,6 +47,10 @@ const High = styled(Low)` background-color: ${colors.lightRed}; `; +type ButtonOptions = "left" | "right" +type ButtonCss = { + [key in ButtonOptions]: string; +} const buttonCss = { left: css` padding-right: 1rem; @@ -55,7 +61,7 @@ const buttonCss = { `, }; -const Button = styled.button` +const Button = styled.button<{ readonly direction: keyof ButtonCss }>` ${buttonBase}; line-height: 1; display: flex; @@ -81,9 +87,19 @@ const Button = styled.button` } `; -const PageNav = ({ logs, low, high, handlePageChange }) => { +interface Buttons { + [key: string]: { condition: boolean, direction: ButtonOptions } +} + +interface Props { + logs: OutputObject[]; + low: number; + high: number; + handlePageChange: (direction: string) => void; +} +const PageNav: FC = ({ logs, low, high, handlePageChange }) => { const { isWidth: isDesktop } = useIsWidth("large"); - const buttons = { + const buttons: Buttons = { older: { condition: high < logs.length, direction: "left", @@ -102,7 +118,7 @@ const PageNav = ({ logs, low, high, handlePageChange }) => { {`${logs.length - low}`} {`of ${logs.length} logs.`} )} - {Object.keys(buttons).map(i => { + {Object.keys(buttons).map((i) => { const { condition, direction } = buttons[i]; return ( condition && ( diff --git a/src/components/logbook/Results.jsx b/src/components/logbook/Results.tsx similarity index 86% rename from src/components/logbook/Results.jsx rename to src/components/logbook/Results.tsx index 6d1b097..96b8953 100644 --- a/src/components/logbook/Results.jsx +++ b/src/components/logbook/Results.tsx @@ -1,13 +1,14 @@ -import React from "react"; +import React, { FC } from "react"; import styled from "styled-components"; +import { OutputObject } from "../../utils/common-types"; + import useIsWidth from "../common/useIsWidth.jsx"; import Chevron from "../common/icons/Chevron.jsx"; import { breakpoint, colors } from "../common/styleVars"; import { searchResultText } from "../common/Layout.jsx"; import { buttonBase } from "../common/Buttons.jsx"; - -import { getDate } from "../../utils/getDate"; +import { getDate } from "../../utils/get-date"; const ResultsList = styled.ul` margin: 0 0 3rem; @@ -82,9 +83,15 @@ const Date = styled.span` align-items: center; `; -const Results = ({ logs, low, high, handleSingleView }) => { +interface Props { + logs: OutputObject[]; + low: number; + high: number; + handleSingleView: (index: string) => void; +} +const Results: FC = ({ logs, low, high, handleSingleView }) => { const { isWidth: isDesktop } = useIsWidth("large"); - + // TODO: run getDate in processed-data.ts return ( {logs diff --git a/src/components/logbook/Search.jsx b/src/components/logbook/Search.tsx similarity index 69% rename from src/components/logbook/Search.jsx rename to src/components/logbook/Search.tsx index cd13282..a7bc8cf 100644 --- a/src/components/logbook/Search.jsx +++ b/src/components/logbook/Search.tsx @@ -1,12 +1,13 @@ -import React from "react"; +import React, { FC } from "react"; import styled from "styled-components"; +import { DefaultSearch } from "../../utils/common-types"; + import useIsWidth from "../common/useIsWidth.jsx"; import { searchResultText } from "../common/Layout.jsx"; import { buttonBase } from "../common/Buttons.jsx"; import { colors, fonts, boxShadow, breakpoint } from "../common/styleVars"; - -import { getDate } from "../../utils/getDate"; +import { getDate } from "../../utils/get-date"; const SearchContainer = styled.div` position: relative; @@ -77,8 +78,9 @@ const Results = styled.ul` @media only screen and (min-width: ${breakpoint.small}) { top: 71px; } + margin-top: 1rem; left: 0.5rem; - z-index: 1; + z-index: 2; width: calc(100% - 1rem); max-height: 65vh; overflow-y: scroll; @@ -126,7 +128,15 @@ const Date = styled.span` font-weight: 600; `; -const Search = ({ handleSearch, handleSingleView, placeholder, results, searchTerm }) => { +// seems to be problems combining interfaces +// https://github.com/typescript-cheatsheets/react/issues/61 +// https://github.com/microsoft/TypeScript/issues/21417 +// https://stackoverflow.com/questions/59969756/not-assignable-to-type-intrinsicattributes-intrinsicclassattributes-react-js +interface Props extends DefaultSearch { + handleSearch: (value: string) => void; + handleSingleView: (index: string) => void; +} +const Search: FC = ({ handleSearch, handleSingleView, placeholder, results, searchTerm }) => { const { isWidth: isTablet } = useIsWidth("tablet"); const { isWidth: isDesktop } = useIsWidth("large"); @@ -138,23 +148,27 @@ const Search = ({ handleSearch, handleSingleView, placeholder, results, searchTe type="text" placeholder={placeholder} value={searchTerm} - onChange={handleSearch} + onChange={(e: React.ChangeEvent) => handleSearch(e.currentTarget.value)} /> {results && ( - {results.map(i => ( -
  • - handleSingleView(i.key)} - > - {i.climbName} - {isTablet && {i.cragName}} - {getDate(i.date.processed, isDesktop)} - -
  • - ))} + {results.map(({ climbName, cragName, date: { processed }, key }) => { + // TODO: do this in processed-data.ts + const formattedDate = getDate(processed, isDesktop); + return ( +
  • + handleSingleView(key)} + > + {climbName} + {isTablet && {cragName}} + {formattedDate} + +
  • + ) + })}
    )} diff --git a/src/components/logbook/SearchReset.jsx b/src/components/logbook/SearchReset.tsx similarity index 74% rename from src/components/logbook/SearchReset.jsx rename to src/components/logbook/SearchReset.tsx index 693d813..12e8787 100644 --- a/src/components/logbook/SearchReset.jsx +++ b/src/components/logbook/SearchReset.tsx @@ -1,7 +1,11 @@ -import React, { useCallback, useEffect, useRef, useState, Fragment } from "react"; +import React, { useCallback, useEffect, useRef, FC } from "react"; -const SearchReset = ({ onClose, children }) => { - const ref = useRef(null); +interface Props { + onClose: () => void; +} + +const SearchReset: FC = ({ onClose }) => { + const ref = useRef(null); const escapeListener = useCallback( e => { if (e.key === "Escape") { @@ -28,7 +32,7 @@ const SearchReset = ({ onClose, children }) => { document.removeEventListener("keyup", escapeListener); }; }, [clickListener, escapeListener]); - return
    {children}
    ; + return
    ; }; export default SearchReset; diff --git a/src/components/stats/Stats.jsx b/src/components/stats/Stats.jsx index 2db9510..379dde9 100644 --- a/src/components/stats/Stats.jsx +++ b/src/components/stats/Stats.jsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, Fragment } from "react"; import * as d3 from "d3"; import styled from "styled-components"; -import { defaultSettings, months } from "../../utils/constants.ts"; +import { defaultSettings, months } from "../../utils/constants"; import StatsHeader from "./StatsHeader.jsx"; import PieChart from "./PieChart.jsx"; import Legend from "./Legend.tsx"; diff --git a/src/data/getData.ts b/src/data/getData.ts deleted file mode 100644 index 26f56d3..0000000 --- a/src/data/getData.ts +++ /dev/null @@ -1,20 +0,0 @@ -interface ResponseObject { - status: number; - json: any; -} -const checkResponse = (response: ResponseObject): any => { - if (response.status !== 200) { - console.log(`Error with the request! ${response.status}`); - return; - } - return response.json(); -}; - -// get user data -export const climbData = (): object | string => { - return fetch("mb-logbook") - .then(checkResponse) - .catch(err => { - throw new Error(`fetch climbsData failed ${err}`); - }); -}; diff --git a/src/utils/common-types.ts b/src/utils/common-types.ts new file mode 100644 index 0000000..336dac0 --- /dev/null +++ b/src/utils/common-types.ts @@ -0,0 +1,48 @@ +// Notes is optional on this +type MonthOptions = + | "Jan" + | "Feb" + | "Mar" + | "Apr" + | "May" + | "Jun" + | "Jul" + | "Aug" + | "Sep" + | "Oct" + | "Nov" + | "Dec"; + +interface DateOptions { + year: string; + yearInt?: string; + month: string; + monthLong: string; + monthInt?: string; + day: string; + dayLong?: string; +} + +interface Date { + original: string; + processed: DateOptions; +} + +interface OutputObject { + climbName: string; + cragName: string; + date: Date; + grade: string; + key: string; + notes?: string; + partners: string; + style: string; +} + +interface DefaultSearch { + placeholder: string; + results: OutputObject[] | void | undefined; + searchTerm: string; +} + +export { MonthOptions, DateOptions, OutputObject, DefaultSearch }; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b831868..b1bcd80 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,4 +1,6 @@ -// applies to +import { MonthOptions } from "./common-types"; + +// for type DateOptions = "Year" | "Month"; type DisciplineOptions = "Bouldering" | "Ice" | "Mixed" | "Sport" | "Trad"; type GradeOptions = "Low" | "High"; // order low to high @@ -26,7 +28,6 @@ const defaultSettings: DefaultSettings = { type IntOptions = "01" | "02" | "03" | "04" | "05" | "06" | "07" | "08" | "09" | "10" | "11" | "12"; type MonthOptionsLong = "January" | "February" | "March" | "April" | "May" | "June" | "July" | "August" | "September" | "October" | "November" | "December"; -type MonthOptions = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"; interface Month { text: MonthOptionsLong; @@ -52,4 +53,4 @@ const months: Months = { Dec: { text: "December", int: "12" }, }; -export { defaultSettings, months, MonthOptions }; \ No newline at end of file +export { defaultSettings, months }; \ No newline at end of file diff --git a/src/utils/getDate.ts b/src/utils/get-date.ts similarity index 63% rename from src/utils/getDate.ts rename to src/utils/get-date.ts index 5b3acbb..6e1cff0 100644 --- a/src/utils/getDate.ts +++ b/src/utils/get-date.ts @@ -1,7 +1,6 @@ -interface DateObject { - [key: string]: string; -} -const getDate = (dateData: DateObject, desktop: boolean): string => { +import { DateOptions } from "./common-types"; + +const getDate = (dateData: DateOptions, desktop: boolean): string => { const { day, dayLong, monthInt, monthLong, year, yearInt } = dateData; if (desktop) { return `${dayLong} ${monthLong} ${year}`; diff --git a/src/utils/formatData.ts b/src/utils/process-date.ts similarity index 59% rename from src/utils/formatData.ts rename to src/utils/process-date.ts index 5d9292c..1ca5ac8 100644 --- a/src/utils/formatData.ts +++ b/src/utils/process-date.ts @@ -1,15 +1,7 @@ -/* formatData.ts - * - format logbook data from json - * - id = index of climb in logbook - */ -import climbData from "../data/mb-logbook.json"; -import { months, MonthOptions } from "./constants"; -const twoDigitYear = new Date() - .getFullYear() - .toString() - .substr(2); +import { MonthOptions, DateOptions } from "./common-types"; +import { months } from "./constants"; type MonthYearInputOptions = "day" | "month" | "monthLong" | "year" type MonthYearInput = { @@ -22,6 +14,11 @@ interface MonthYearOutput extends MonthYearInput { yearInt?: string; } +const twoDigitYear = new Date() + .getFullYear() + .toString() + .substr(2); + const processMonthYear = (month: keyof typeof months, year: string, inputObj: MonthYearInput): MonthYearOutput => { const retObj: MonthYearOutput = { ...inputObj }; if (Object.keys(months).includes(month)) { @@ -59,17 +56,7 @@ const processDaySuffix = (day: string): string => { return `${num}th`; }; -interface DateOptions { - year: string, - yearInt?: string, - month: string, - monthLong: string, - monthInt?: string, - day: string, - dayLong?: string, -} - -const processDate = (date: string): DateOptions => { +export const processDate = (date: string): DateOptions => { const defaultRes: DateOptions = { year: "unknown", month: "unknown", @@ -100,45 +87,4 @@ const processDate = (date: string): DateOptions => { return processMonthYear(month, year, newRes); } return defaultRes; -}; - - -type InputOptions = "Climb name" | "Grade" | "Style" | "Partner(s)" | "Notes" | "Date" | "Crag name"; -interface Date { - original: string, - processed: DateOptions -} - -interface OutputObject { - climbName: string, - cragName: string, - date: Date, - grade: string, - notes: string, - partners: string, - style: string, - key: string, -} - -const formatData = (rawData: Record[]): OutputObject[] => { - return rawData.map((item, index: number) => { - return { - climbName: item["Climb name"], - cragName: item["Crag name"], - date: { - original: item.Date, - processed: processDate(item.Date), - }, - grade: `${item.Grade}`.replace(/\*+$/, "").trim(), - notes: item.Notes, - partners: item["Partner(s)"] || "climbed alone / no partner listed", - style: `${item.Style}`.replace(/β|_/, "flash"), - key: `ascent-${index}`, - }; - }); -}; - -// perhaps avoid using a record to make this work: -const allLogs = formatData(climbData) - -export { allLogs, InputOptions, OutputObject }; +}; \ No newline at end of file diff --git a/src/utils/processed-data.ts b/src/utils/processed-data.ts new file mode 100644 index 0000000..a383ce6 --- /dev/null +++ b/src/utils/processed-data.ts @@ -0,0 +1,37 @@ +import { OutputObject } from "./common-types"; + +import sourceData from "../data/mb-logbook.json"; +import { processDate } from "./process-date"; + +interface InputObject { + "Climb name": string; + "Crag name": string; + "Date": string; + "Grade": string; + "Notes"?: string; + "Partner(s)"?: string; + "Style": string; +} + +const formatData = (rawData: InputObject[]): OutputObject[] => { + const ret: OutputObject[] = []; + rawData.forEach((item, index: number) => { + ret.push({ + climbName: item["Climb name"], + cragName: item["Crag name"], + date: { + original: item.Date, + processed: processDate(item.Date), + }, + grade: `${item.Grade}`.replace(/\*+$/, "").trim(), + notes: item.Notes, + partners: item["Partner(s)"] || "climbed alone / no partner listed", + style: `${item.Style}`.replace(/β|_/, "flash"), + key: `ascent-${index}`, + }); + }); + return ret; +}; + +const allLogs = formatData(sourceData) +export { allLogs };