diff --git a/src/App.tsx b/src/App.tsx index f73a7786d8c334e030cf5667e465ff533e426649..98fff5e26f51cd8fda9b2fb2ba7300d680806f04 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,42 @@ import { ThemeProvider } from '@material-ui/styles'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import './App.css'; import CommonLogic from './components/CommonLogic/index'; import NavBar from './components/NavBar'; import { OpenSettingsContext } from './helpers/context'; -import { backgroundTheme } from './helpers/themes'; +import { themes } from './helpers/themes'; import Home from './pages/Home/index'; import SettingsPage from './pages/SettingsPage/index'; import FeatsVsFixesPage from './pages/FeatsVsFixesPage'; import TimePerIssueLabelPage from './pages/TimePerIssueLabelPage/index'; +import { Theme } from '@material-ui/core'; +import { useLocalStorage } from './helpers/hooks'; function App() { const [openSettings, setOpenSettings] = useState(false); + const [theme, setTheme] = useState<Theme>(themes.light); + const [themeName, setThemeName] = useLocalStorage<keyof typeof themes>(`theme`, 'light'); + + // Get the theme from localstorage + useEffect(() => { + if (themeName !== undefined) { + setTheme(themes[themeName]); + } + }, []); + return ( <div className="App"> <OpenSettingsContext.Provider value={[openSettings, setOpenSettings]}> - <ThemeProvider theme={backgroundTheme}> + <ThemeProvider theme={theme}> + <SettingsPage + open={openSettings} + onClose={() => setOpenSettings(false)} + setTheme={setTheme} + themeName={themeName} + setThemeName={setThemeName} + /> <NavBar title="Gitlab data visualization" /> - <SettingsPage open={openSettings} onClose={() => setOpenSettings(false)} /> <Router> <CommonLogic /> <Switch> diff --git a/src/components/NavBar/index.tsx b/src/components/NavBar/index.tsx index 4a1364624fa4226a948beae8de96195b61666377..0decf5959d49ee620b503112875922d5a92cb36f 100644 --- a/src/components/NavBar/index.tsx +++ b/src/components/NavBar/index.tsx @@ -38,7 +38,7 @@ export default function NavBar(props: MenuProps) { return ( <div> - <AppBar position="sticky" color="secondary"> + <AppBar position="sticky" className={classes.appBar}> <Toolbar className={classes.toolBar}> <Link href={'/'} underline={'none'}> <Typography variant="h6" component="div" className={classes.title}> diff --git a/src/components/NavBar/styles.ts b/src/components/NavBar/styles.ts index 54fbef3b5c9dbb920828911742eadb86978967a6..88b7006e4c4b8d027b14251237cb06351a7e4201 100644 --- a/src/components/NavBar/styles.ts +++ b/src/components/NavBar/styles.ts @@ -9,12 +9,15 @@ const useStyles = makeStyles((theme) => color: theme.palette.secondary.contrastText, }, menuItem: { - color: theme.palette.secondary.main, + color: 'black', }, toolBar: { display: 'flex', justifyContent: 'space-between', }, + appBar: { + backgroundColor: theme.palette.secondary.main, + }, linkContainer: { display: 'flex', }, diff --git a/src/components/PageContainer/index.tsx b/src/components/PageContainer/index.tsx index aa4f62d6b5f41c4bc5f35b9e00155f05cc87a2a1..4ca9d0c9d297603e0fc0e6402a2ecd5441ea04b7 100644 --- a/src/components/PageContainer/index.tsx +++ b/src/components/PageContainer/index.tsx @@ -9,7 +9,7 @@ type PageContainerProps = { export default function PageContainer(props: PageContainerProps) { const style = useStyles(); return ( - <Grid container> + <Grid container className={style.background}> <Container className={style.main} maxWidth="md"> {props.title ? <h3>{props.title}</h3> : false} {props.children} diff --git a/src/components/PageContainer/styles.ts b/src/components/PageContainer/styles.ts index d908698745a17b124e315edcb6773a35f5542d9e..a60d0a81e77e4501e5edd37cf039f27dadc56911 100644 --- a/src/components/PageContainer/styles.ts +++ b/src/components/PageContainer/styles.ts @@ -11,6 +11,11 @@ const useStyles = makeStyles((theme) => borderRadius: '0 0 10px 10px', }, }, + background: { + backgroundColor: theme.palette.background.default, + width: '100%', + minHeight: '100vh', + }, }), ); diff --git a/src/components/Popup/index.tsx b/src/components/Popup/index.tsx index 71054d2956b528eb0437ff786a9d799c80de7c5e..7d86312e1cecef3056651a6062e7becb103db26d 100644 --- a/src/components/Popup/index.tsx +++ b/src/components/Popup/index.tsx @@ -2,9 +2,10 @@ import { XIcon } from '@heroicons/react/outline'; import { Dialog, DialogProps } from '@material-ui/core'; import IconButton from '@material-ui/core/IconButton'; import { useStyles } from './styles'; +import { ReactNode } from 'react'; type PopupProps = { - children?: JSX.Element; + children?: ReactNode; open: boolean; onClose: () => void; title: string; diff --git a/src/components/Popup/styles.ts b/src/components/Popup/styles.ts index d21a1914f23c45d6870dfd14fa4acbefcd1a0076..2f4b62813efc5dc42338f723d33709a62d890b8f 100644 --- a/src/components/Popup/styles.ts +++ b/src/components/Popup/styles.ts @@ -16,6 +16,7 @@ export const useStyles = makeStyles((theme: Theme) => }, dialog: { backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, }, }), ); diff --git a/src/components/ThemeRadioGroup/index.tsx b/src/components/ThemeRadioGroup/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dfbc7fe9467ad834f9558152929c3ae4a7b241a4 --- /dev/null +++ b/src/components/ThemeRadioGroup/index.tsx @@ -0,0 +1,37 @@ +import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@material-ui/core'; +import { useStyles } from './styles'; +import { ChangeEvent } from 'react'; +import { themes } from '../../helpers/themes'; + +type ThemeRadioGroupProps = { + onChange: (event: ChangeEvent<HTMLInputElement>, value: string) => void; + themeName: string; +}; + +/** + * A component giving the user the choice between all the current themes + */ +export default function ThemeRadioGroup(props: ThemeRadioGroupProps) { + const classes = useStyles(); + return ( + <FormControl component="fieldset"> + <FormLabel component="legend" className={classes.legend}> + Theme + </FormLabel> + <RadioGroup onChange={props.onChange}> + {Object.keys(themes).map((themeName) => { + return ( + <FormControlLabel + className={classes.radio} + key={themeName} + value={themeName} + checked={themeName === props.themeName} + label={themeName} + control={<Radio className={classes.radio} />} + /> + ); + })} + </RadioGroup> + </FormControl> + ); +} diff --git a/src/components/ThemeRadioGroup/styles.ts b/src/components/ThemeRadioGroup/styles.ts new file mode 100644 index 0000000000000000000000000000000000000000..cbbbcf24c46115196dea244993d1a5deffc478fd --- /dev/null +++ b/src/components/ThemeRadioGroup/styles.ts @@ -0,0 +1,13 @@ +import { createStyles, makeStyles, Theme } from '@material-ui/core'; + +export const useStyles = makeStyles((theme: Theme) => + createStyles({ + radio: { + color: theme.palette.primary.contrastText + '!important', + }, + legend: { + color: theme.palette.primary.contrastText + '!important', + marginBottom: '1rem', + }, + }), +); diff --git a/src/helpers/hooks.ts b/src/helpers/hooks.ts index cfa5af6651476cbe6629d5f1705118d15b328811..96bd49f21ffef581c81d9954c8ac099ec54a3efb 100644 --- a/src/helpers/hooks.ts +++ b/src/helpers/hooks.ts @@ -1,7 +1,6 @@ -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; type StorageObject = typeof window.localStorage | typeof window.sessionStorage; -type PossibleValues = string | number | boolean; // useStorage, useLocalStorage and useSessionStorage is taken from: // https://github.com/WebDevSimplified/useful-custom-react-hooks/blob/main/src/8-useStorage/useStorage.js /** @@ -11,14 +10,16 @@ type PossibleValues = string | number | boolean; * @param storageObject Either localStorage or sessionStorage * @returns An array of [value, setValue, remove] */ -function useStorage<ValueType extends PossibleValues>( +function useStorage<ValueType>( key: string, defaultValue: ValueType, storageObject: StorageObject, -): [ValueType | undefined, Dispatch<SetStateAction<ValueType | undefined>>, () => void] { - const [value, setValue] = useState<ValueType | undefined>(() => { - const jsonValue = storageObject.getItem(key); - if (jsonValue != null) return JSON.parse(jsonValue); +): [ValueType, Dispatch<SetStateAction<ValueType>>] { + const [value, setValue] = useState<ValueType>(() => { + const value = storageObject.getItem(key); + if (value != null) { + return JSON.parse(value); + } return defaultValue; }); @@ -27,11 +28,7 @@ function useStorage<ValueType extends PossibleValues>( storageObject.setItem(key, JSON.stringify(value)); }, [key, value, storageObject]); - const remove = useCallback(() => { - setValue(undefined); - }, []); - - return [value, setValue, remove]; + return [value, setValue]; } /** @@ -40,10 +37,7 @@ function useStorage<ValueType extends PossibleValues>( * @param defaultValue The default value of the item in localStorage. * @returns An array of [value, setValue, remove] */ -export function useLocalStorage<ValueType extends PossibleValues>( - key: string, - defaultValue: ValueType, -) { +export function useLocalStorage<ValueType>(key: string, defaultValue: ValueType) { return useStorage(key, defaultValue, window.localStorage); } @@ -53,9 +47,6 @@ export function useLocalStorage<ValueType extends PossibleValues>( * @param defaultValue The default value of the item in sessionStorage. * @returns An array of [value, setValue, remove] */ -export function useSessionStorage<ValueType extends PossibleValues>( - key: string, - defaultValue: ValueType, -) { +export function useSessionStorage<ValueType>(key: string, defaultValue: ValueType) { return useStorage(key, defaultValue, window.sessionStorage); } diff --git a/src/helpers/themes.ts b/src/helpers/themes.ts index 3887d325a53ed4d15234fdbb1d3a3c06657dd4c7..663c60ef040a7472f65b61b3ab90be7c0ad14aae 100644 --- a/src/helpers/themes.ts +++ b/src/helpers/themes.ts @@ -1,18 +1,72 @@ import { createTheme } from '@material-ui/core'; -export const backgroundTheme = createTheme({ - palette: { - primary: { - main: '#f1f1e9', // Temporary secondary theme - contrastText: '#1b1f28', +const lightTheme = createTheme( + { + palette: { + primary: { + main: '#f8f6f3', + contrastText: '#1b1f28', + }, + secondary: { + main: '#1f1f1e', + contrastText: '#dedede', + }, + info: { + main: '#f1f6f6', + contrastText: '#3d393e', + }, + background: { + default: '#f4fffe', + }, }, - secondary: { - main: '#342f32', // Temporary primary theme - contrastText: '#dedede', + }, + { name: 'light' }, +); + +const darkTheme = createTheme( + { + palette: { + primary: { + main: '#0c0c0c', + contrastText: '#d4daec', + }, + secondary: { + main: '#222020', + contrastText: '#d7cdcd', + }, + info: { + main: '#323737', + contrastText: '#afa7b1', + }, + background: { + default: '#070606', + }, }, - info: { - main: '#f1f6f6', // Temporary info theme - contrastText: '#3d393e', + }, + { name: 'dark' }, +); + +const funkTheme = createTheme( + { + palette: { + primary: { + main: '#a70f0f', + contrastText: '#022c53', + }, + secondary: { + main: '#316d72', + contrastText: '#31ff0d', + }, + info: { + main: '#941681', + contrastText: '#e9cb14', + }, + background: { + default: '#03801f', + }, }, }, -}); + { name: 'dark' }, +); + +export const themes = { light: lightTheme, dark: darkTheme, wayTooManyColors: funkTheme }; diff --git a/src/pages/FeatsVsFixesPage/index.tsx b/src/pages/FeatsVsFixesPage/index.tsx index 24e24f0743287b64a37f3dd35a374cc497087c3c..607980aaa94be86f8e011c5ef9bf85e3f91d2554 100644 --- a/src/pages/FeatsVsFixesPage/index.tsx +++ b/src/pages/FeatsVsFixesPage/index.tsx @@ -1,14 +1,23 @@ -import PageContainer from '../../components/PageContainer/index'; +import { Checkbox } from '@material-ui/core'; +import { useEffect, useState } from 'react'; import ChartPie from '../../components/ChartPie'; +import PageContainer from '../../components/PageContainer/index'; import { getAllCommitsFromAPI } from '../../helpers/api-calls'; -import { useEffect, useState } from 'react'; +import { useSessionStorage } from '../../helpers/hooks'; import { CommitAuthor } from '../../helpers/types'; -import { Checkbox } from '@material-ui/core'; import { parseCommitData } from './utils'; +import { useStyles } from './styles'; export default function FeatsVsFixesPage() { + // The retrieved athor data form the api. const [authorData, setAuthorData] = useState<CommitAuthor[]>([]); + // The selected showing authors (default all selected), settings are saved in sessions. + const [selectedAuthors, setSelectedAuthors] = useSessionStorage<boolean[]>( + 'selectedAuthorsFeatsVsFixes', + new Array(authorData.length).fill(true), + ); + const classes = useStyles(); const featsFixesGraphData: Array<{ commitType: string; val: number }> = [ { commitType: 'feat', val: 0 }, { commitType: 'fix', val: 0 }, @@ -20,7 +29,7 @@ export default function FeatsVsFixesPage() { ]; for (let i = 0; i < authorData.length; i++) { - if (authorData[i].active) { + if (selectedAuthors[i]) { featsFixesGraphData[0].val += authorData[i].feats; featsFixesGraphData[1].val += authorData[i].fixes; additionsDeletionsGraphData[0].val += authorData[i].additions; @@ -44,14 +53,16 @@ export default function FeatsVsFixesPage() { {authorData.map((m, i) => { if (m.feats || m.fixes) { return ( - <div key={i}> + <div key={JSON.stringify(m)}> Person {i + 1} <Checkbox - checked={m.active} + className={classes.checkbox} + checked={selectedAuthors[i]} onChange={() => { - const temp_list = [...authorData]; - temp_list[i].active = !temp_list[i].active; - setAuthorData(temp_list); + if (!selectedAuthors) return; // selectedAuthors will never be undefined + const tempList = [...selectedAuthors]; + tempList[i] = !tempList[i]; + setSelectedAuthors(tempList); }} /> </div> diff --git a/src/pages/FeatsVsFixesPage/styles.ts b/src/pages/FeatsVsFixesPage/styles.ts new file mode 100644 index 0000000000000000000000000000000000000000..24785b8ae7ccafb8d0c8e2ea6b4409ef378b2bfb --- /dev/null +++ b/src/pages/FeatsVsFixesPage/styles.ts @@ -0,0 +1,9 @@ +import { createStyles, makeStyles, Theme } from '@material-ui/core'; + +export const useStyles = makeStyles((theme: Theme) => + createStyles({ + checkbox: { + color: theme.palette.primary.contrastText + '!important', + }, + }), +); diff --git a/src/pages/SettingsPage/index.tsx b/src/pages/SettingsPage/index.tsx index 987e0998707f220999e7d493638698ef91825c40..441c7266b9c711b613da2f0ac8ab97ee89bc4f7f 100644 --- a/src/pages/SettingsPage/index.tsx +++ b/src/pages/SettingsPage/index.tsx @@ -1,14 +1,26 @@ import Popup from '../../components/Popup'; +import { Theme } from '@material-ui/core'; +import { themes } from '../../helpers/themes'; +import { ChangeEvent } from 'react'; +import ThemeRadioGroup from '../../components/ThemeRadioGroup'; type SettingsPageProps = { open: boolean; onClose: () => void; + setTheme: (t: Theme) => void; + themeName: string; + setThemeName: (theme: keyof typeof themes) => void; }; export default function SettingsPage(props: SettingsPageProps) { + function changeTheme(event: ChangeEvent<HTMLInputElement>, value: string) { + const theme = value as keyof typeof themes; + props.setTheme(themes[theme]); + props.setThemeName(theme); + } return ( <Popup title="Settings" open={props.open} onClose={props.onClose} maxWidth="sm"> - <div>Here comes the settings page</div> + <ThemeRadioGroup onChange={changeTheme} themeName={props.themeName} /> </Popup> ); } diff --git a/src/pages/TimePerIssueLabelPage/index.tsx b/src/pages/TimePerIssueLabelPage/index.tsx index 2422da8ca48afffe982d044ce7e57ebe6726b8f3..e2b504f1e0d63bd3a23b527e9e4b1f5f893b59b2 100644 --- a/src/pages/TimePerIssueLabelPage/index.tsx +++ b/src/pages/TimePerIssueLabelPage/index.tsx @@ -3,25 +3,30 @@ import { useEffect, useState } from 'react'; import ChartBar from '../../components/ChartBar/index'; import PageContainer from '../../components/PageContainer'; import { getIssuesFromAPI } from '../../helpers/api-calls'; -import { - difficultyLabels, - Label, - otherLabels, - techDescriptionLabels, -} from '../../helpers/constants'; +import { difficultyLabels, otherLabels, techDescriptionLabels } from '../../helpers/constants'; +import { useSessionStorage } from '../../helpers/hooks'; import { BarDataItem, Issue } from '../../helpers/types'; import useStyles from './styles'; import { avgTimePerIssueLabel } from './utils'; +const difficultyLabelsString = JSON.stringify(difficultyLabels); +const techLabelsString = JSON.stringify(techDescriptionLabels); +const otherLabelsString = JSON.stringify(otherLabels); + export default function TimePerIssueLabelPage() { const [allIssueData, setAllIssueData] = useState<Issue[] | null>(null); const [data, setData] = useState<BarDataItem[] | null>(null); - const [selected, setSelected] = useState<Label[]>(difficultyLabels); + // The selected issueLabel groups are saved in each session. + const [selected, setSelected] = useSessionStorage<string>( + 'issueLabelsSelected', + difficultyLabelsString, + ); const classes = useStyles(); // On page load, get the issues from the API and load it into a state. useEffect(() => { getIssuesFromAPI().then((res) => { + // Since console.error makes sense here. // eslint-disable-next-line if (!res.ok) return console.error(res.status, res.data); setAllIssueData(res.data); @@ -32,10 +37,10 @@ export default function TimePerIssueLabelPage() { useEffect(() => { // If allIssueData or selected is null, something is not selected and or the data is not if (allIssueData == null) return; - if (selected == null) return; // Find the average time used to close an issue with one of the selected labels - const dataForRelevantIssues = avgTimePerIssueLabel(allIssueData, selected); + const selectedArray = JSON.parse(selected); + const dataForRelevantIssues = avgTimePerIssueLabel(allIssueData, selectedArray); setData(dataForRelevantIssues); }, [selected, allIssueData]); @@ -47,23 +52,24 @@ export default function TimePerIssueLabelPage() { </p> <div> <FormControl className={classes.dropdown}> - <InputLabel id="demo-simple-select-label">Showing labels</InputLabel> + <InputLabel>Showing labels</InputLabel> <Select labelId="demo-simple-select-label" id="demo-simple-select" + className={classes.select} value={selected} onChange={(e) => { - const newValue = e.target.value as Label[]; + const newValue = e.target.value as string; setSelected(newValue); }} > - <MenuItem key={JSON.stringify(difficultyLabels)} value={difficultyLabels}> + <MenuItem key={difficultyLabelsString} value={difficultyLabelsString}> <div>Difficulty</div> </MenuItem> - <MenuItem key={JSON.stringify(techDescriptionLabels)} value={techDescriptionLabels}> + <MenuItem key={techLabelsString} value={techLabelsString}> <div>Tech Description</div> </MenuItem> - <MenuItem key={JSON.stringify(otherLabels)} value={otherLabels}> + <MenuItem key={otherLabelsString} value={otherLabelsString}> <div>Other</div> </MenuItem> </Select> diff --git a/src/pages/TimePerIssueLabelPage/styles.ts b/src/pages/TimePerIssueLabelPage/styles.ts index 8d699d24497bbdf13da4b371cc493efa9abe4e76..37c009a2807b5ac7a2e2c5f277a5b52a378898c0 100644 --- a/src/pages/TimePerIssueLabelPage/styles.ts +++ b/src/pages/TimePerIssueLabelPage/styles.ts @@ -1,9 +1,17 @@ -import { createStyles, makeStyles } from '@material-ui/core'; +import { createStyles, makeStyles, Theme } from '@material-ui/core'; -const useStyles = makeStyles(() => +const useStyles = makeStyles((theme: Theme) => createStyles({ dropdown: { width: '10em', + '& label': { + color: theme.palette.primary.contrastText + '!important', + }, + }, + select: { + color: theme.palette.info.contrastText + '!important', + backgroundColor: theme.palette.info.main, + paddingLeft: '1rem', }, }), );