diff --git a/.gitignore b/.gitignore index 477cabb..a57bf9f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,15 @@ src/api/firebase-config.js # production /build +# firebase +*.cache +.firebaserc + # misc .DS_Store .env +.env.development +.env.production .env.local .env.development.local .env.test.local diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..340ed5b --- /dev/null +++ b/firebase.json @@ -0,0 +1,16 @@ +{ + "hosting": { + "public": "build", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/package.json b/package.json index 1e055cd..bcc04d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "habit-tracker", - "version": "0.1.0", + "version": "1.0.0-alpha.1", "private": true, "dependencies": { "@emotion/react": "^11.1.1", diff --git a/src/api/firebase.js b/src/api/firebase.js index 0275ce9..e6736e8 100644 --- a/src/api/firebase.js +++ b/src/api/firebase.js @@ -4,11 +4,20 @@ import 'firebase/auth'; import 'firebase/database'; import 'firebase/firestore'; -import firebaseConfig from './firebase-config'; +const config = { + apiKey: process.env.REACT_APP_API_KEY, + authDomain: process.env.REACT_APP_AUTH_DOMAIN, + databaseURL: process.env.REACT_APP_DATABASE_URL, + projectId: process.env.REACT_APP_PROJECT_ID, + storageBucket: process.env.REACT_APP_STORAGE_BUCKET, + messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID, + appId: process.env.REACT_APP_APP_ID, + measurementId: process.env.REACT_APP_MEASUREMENT_ID, +}; class Firebase { constructor() { - firebase.initializeApp(firebaseConfig); + firebase.initializeApp(config); this.firebase = firebase; this.auth = firebase.auth(); diff --git a/src/components/checkmark/checkmark.js b/src/components/checkmark.js similarity index 98% rename from src/components/checkmark/checkmark.js rename to src/components/checkmark.js index 0e29bda..bd0ccb0 100644 --- a/src/components/checkmark/checkmark.js +++ b/src/components/checkmark.js @@ -23,6 +23,7 @@ const variants = { empty: { icon: <EmptyCheckmarkIcon />, label: 'empty', + color: 'default', }, }; @@ -39,7 +40,7 @@ function Checkmark({ id, initialValue, habitId, date, disabled }) { const debouncedUpdate = React.useRef( debounce(({ id, newValue }) => { updateCheckmarkInDb({ checkmarkId: id, value: newValue, habitId, date }); - }, 300) + }, 200) ).current; // Handles clicking on checkmark diff --git a/src/components/checkmark/checkmark-variants.js b/src/components/checkmark/checkmark-variants.js deleted file mode 100644 index 1e45726..0000000 --- a/src/components/checkmark/checkmark-variants.js +++ /dev/null @@ -1,49 +0,0 @@ -import { IconButton } from '@material-ui/core'; -import { - CheckBox as CheckBoxIcon, - CheckBoxOutlineBlank as CheckBoxBlankIcon, - IndeterminateCheckBox as IndeterminateCheckBoxIcon, -} from '@material-ui/icons'; - -// Completed Checkmark -function CompletedCheckmark(props) { - return ( - <IconButton - aria-label="Completed checkmark" - data-testid="checkmark-completed" - color="primary" - {...props} - > - <CheckBoxIcon /> - </IconButton> - ); -} - -// Failed Checkmark -function FailedCheckmark(props) { - return ( - <IconButton - aria-label="Failed checkmark" - data-testid="checkmark-failed" - color="secondary" - {...props} - > - <IndeterminateCheckBoxIcon /> - </IconButton> - ); -} - -// Empty Checkmark -function EmptyCheckmark(props) { - return ( - <IconButton - aria-label="Empty checkmark" - data-testid="checkmark-failed" - {...props} - > - <CheckBoxBlankIcon /> - </IconButton> - ); -} - -export { CompletedCheckmark, FailedCheckmark, EmptyCheckmark }; diff --git a/src/components/checkmark/index.js b/src/components/checkmark/index.js deleted file mode 100644 index f23bdb6..0000000 --- a/src/components/checkmark/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Checkmark } from './checkmark'; \ No newline at end of file diff --git a/src/components/github-repo-link.js b/src/components/github-repo-link.js index 2bc3b23..0be828c 100644 --- a/src/components/github-repo-link.js +++ b/src/components/github-repo-link.js @@ -15,7 +15,7 @@ function GithubRepoLink(props) { target="_blank" label={t('githubRepo')} rel="noopener noreferrer" - href="https://github.com/sitek94/pocket-globe-app" + href="https://github.com/sitek94/habit-tracker" {...props} > <GitHubIcon /> diff --git a/src/components/habit-row.js b/src/components/habit-row.js index 1dffbd9..1939b8e 100644 --- a/src/components/habit-row.js +++ b/src/components/habit-row.js @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { Checkmark } from 'components/checkmark'; import { makeStyles, TableCell, TableRow, Typography } from '@material-ui/core'; -import { getDay, isFuture } from 'date-fns'; +import { getDay, parseISO } from 'date-fns'; import { EMPTY } from 'data/constants'; // Styles @@ -17,7 +17,7 @@ const useStyles = makeStyles((theme) => ({ // Habit row function HabitRow({ habit, dates, checkmarks }) { const classes = useStyles(); - + const { id, name, frequency /* position */ } = habit; return ( @@ -39,11 +39,10 @@ function HabitRow({ habit, dates, checkmarks }) { {/* Dates */} {dates.map((date) => { - const dateObj = new Date(date); + const dateObj = parseISO(date); - // Checkmark is disabled if the date is not tracked or date is in the future - const disabled = - !frequency.includes(getDay(dateObj)) || isFuture(dateObj); + // Checkmark is disabled if the date is not tracked + const disabled = !frequency.includes(getDay(dateObj)); // Find checkmark const checkmark = checkmarks.find((d) => d.date === date); diff --git a/src/components/habits-table.js b/src/components/habits-table.js index d83de4a..383503e 100644 --- a/src/components/habits-table.js +++ b/src/components/habits-table.js @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { format, isToday } from 'date-fns'; +import { format, isToday, parseISO } from 'date-fns'; import { getComparator } from 'utils/misc'; import { HabitRow } from './habit-row'; import { useLocale } from 'localization'; @@ -66,7 +66,7 @@ function HabitsTable({ habits, checkmarks, dates }) { // Currently selected date range cells const datesCells = dates.map((d) => { - const date = new Date(d); + const date = parseISO(d); return { date, diff --git a/src/components/performance-panel/helpers.js b/src/components/performance-panel/helpers.js new file mode 100644 index 0000000..367eebd --- /dev/null +++ b/src/components/performance-panel/helpers.js @@ -0,0 +1,52 @@ +import { COMPLETED } from 'data/constants'; + +/** + * Checkmark object + * @typedef Checkmark + * + * @property {string} id + * @property {string} date + * @property {string} habitId + * @property {number} value + */ + +/** + * Calculate score + * + * Calculates the score for an array of checkmark values. + * The perfomance is the count of COMPLETED checkmarks divided + * by the total count. + * + * @param {number[]} values + * + * @returns {number} floored score between 0 and 100 + */ +export function calculateScore(values) { + if (!values.length) return 0; + + const completedCount = values.reduce((sum, value) => { + return value === COMPLETED ? sum + 1 : sum; + }, 0); + + return Math.floor((completedCount / values.length) * 100); +} + +/** + * Create pie chart data + * + * @returns an array of data for pie chart + */ +export function createPieChartData(values) { + const score = calculateScore(values); + + return [ + { + id: 'value', + value: score, + }, + { + id: 'empty', + value: 100 - score, + }, + ]; +} diff --git a/src/components/performance-panel/index.js b/src/components/performance-panel/index.js new file mode 100644 index 0000000..bedce85 --- /dev/null +++ b/src/components/performance-panel/index.js @@ -0,0 +1 @@ +export { PerformancePanel } from './performance-panel'; \ No newline at end of file diff --git a/src/components/user-scores/user-scores.js b/src/components/performance-panel/performance-panel.js similarity index 81% rename from src/components/user-scores/user-scores.js rename to src/components/performance-panel/performance-panel.js index aa847bb..f617664 100644 --- a/src/components/user-scores/user-scores.js +++ b/src/components/performance-panel/performance-panel.js @@ -4,29 +4,31 @@ import { Done as DoneIcon } from '@material-ui/icons'; import { Pie } from '@nivo/pie'; import { calculateScore, - createScoreType, - getScoreTypeDataList, - isCheckmarkLastWeek, - isCheckmarkThisWeek, - isCheckmarkToday, + createPieChartData } from './helpers'; import { useTranslation } from 'translations'; +import { getWeek, isThisWeek, isToday, parseISO } from 'date-fns'; -function UserScores({ checkmarks, goal }) { +function PerformancePanel({ checkmarks, goal }) { const t = useTranslation(); - // Score types that we track is 'last week', 'this week' and 'today' - const scoreTypeList = React.useMemo( - () => [ - createScoreType(t('lastWeek'), isCheckmarkLastWeek), - createScoreType(t('thisWeek'), isCheckmarkThisWeek), - createScoreType(t('today'), isCheckmarkToday), - ], - [t] - ); + const todayValues = checkmarks + .filter((c) => isToday(parseISO(c.date))) + .map((c) => c.value); + + const thisWeekValues = checkmarks + .filter((c) => isThisWeek(parseISO(c.date))) + .map((c) => c.value); + + const lastWeekValues = checkmarks + .filter((c) => getWeek(parseISO(c.date)) === getWeek(new Date()) - 1) + .map((c) => c.value); - // Use user's checkmarks and score types list to generate the data for pie chart - const scoreTypeDataList = getScoreTypeDataList(checkmarks, scoreTypeList); + const dataList = [ + { label: t('today'), data: createPieChartData(todayValues) }, + { label: t('thisWeek'), data: createPieChartData(thisWeekValues) }, + { label: t('lastWeek'), data: createPieChartData(lastWeekValues) }, + ]; // Calculate all time user score const allTimeValues = checkmarks.map((d) => d.value); @@ -43,7 +45,7 @@ function UserScores({ checkmarks, goal }) { {/* Pie charts */} <Grid container justifyContent="space-evenly"> - {scoreTypeDataList.map(({ label, data }) => { + {dataList.map(({ label, data }) => { const completedValue = data[0].value; const hasReachedGoal = completedValue > goal; @@ -183,4 +185,4 @@ function PieChart({ data }) { ); } -export { UserScores }; +export { PerformancePanel }; diff --git a/src/components/user-scores/helpers.js b/src/components/user-scores/helpers.js deleted file mode 100644 index 83e6a68..0000000 --- a/src/components/user-scores/helpers.js +++ /dev/null @@ -1,131 +0,0 @@ -import { COMPLETED } from 'data/constants'; -import { getWeek, isThisWeek, isToday } from 'date-fns'; - -/** - * Checkmark object - * @typedef Checkmark - * - * @property {string} id - * @property {string} date - * @property {string} habitId - * @property {number} value - */ - -/** - * Calculate score - * - * Calculates the score for an array of checkmark values. - * The perfomance is the count of COMPLETED checkmarks divided - * by the total count. - * - * @param {number[]} values - * - * @returns {number} floored score between 0 and 100 - */ -export function calculateScore(values) { - if (!values.length) return 0; - - const completedCount = values.reduce((sum, value) => { - return value === COMPLETED ? sum + 1 : sum; - }, 0); - - return Math.floor((completedCount / values.length) * 100); -} - -/** - * Is checkmark today? - * - * Check if the checkmark's date is today - * - * @param {Checkmark} checkmark - * - * @returns {boolean} true/false - */ -export function isCheckmarkToday(checkmark) { - return isToday(new Date(checkmark.date)); -} - -/** - * Is checkmark this week? - * - * Check if the checkmark's date is within this week - * - * @param {Checkmark} checkmark - * - * @returns {boolean} true/false - */ -export function isCheckmarkThisWeek(checkmark) { - return isThisWeek(new Date(checkmark.date)); -} - -/** - * Is checkmark last week? - * - * Check if the checkmark's date is within lat week - * - * @param {Checkmark} checkmark - * - * @returns {boolean} true/false - */ -export function isCheckmarkLastWeek(checkmark) { - return getWeek(new Date(checkmark.date)) === getWeek(new Date()) - 1; -} - -/** - * Create pie chart data - * - * Prepares the data that can be used by pie chart. - * - * @param {number} score an integer between 0 and 100. - * - * @returns an array of data for pie chart - */ -export function createPieChartData(score) { - return [ - { - id: 'value', - value: score, - }, - { - id: 'empty', - value: 100 - score, - }, - ]; -} - -/** - * Create ScoreType object - * - * @param {string} label score type description - * @param {(checkmark: Checkmark) => boolean} filterFn function used to filter the checkmarks - */ -export function createScoreType(label, filterFn) { - return { label, filterFn }; -} - -/** - * Generates a list of objects with a label and pie chart data. - * - * @param {Checkmark[]} checkmarks - * @param {ScoreType[]} scoreTypeList - * - */ -export function getScoreTypeDataList(checkmarks, scoreTypeList) { - // Map over each score type - return scoreTypeList.map(({ label, filterFn }) => { - const values = checkmarks - // Filter the checkmarks - .filter(filterFn) - // Get only checkmark value - .map((d) => d.value); - - // Calculate the score - const score = calculateScore(values); - - // Return an object that can be in render - return { - label, - data: createPieChartData(score), - }; - }); -} diff --git a/src/components/user-scores/index.js b/src/components/user-scores/index.js deleted file mode 100644 index 959170a..0000000 --- a/src/components/user-scores/index.js +++ /dev/null @@ -1 +0,0 @@ -export { UserScores } from './user-scores'; \ No newline at end of file diff --git a/src/components/week-bar-chart.js b/src/components/week-bar-chart.js index 6026f43..86d481d 100644 --- a/src/components/week-bar-chart.js +++ b/src/components/week-bar-chart.js @@ -2,7 +2,7 @@ import * as React from 'react'; import { ResponsiveBar } from '@nivo/bar'; import { useTheme } from '@material-ui/core'; import { countBy } from 'lodash'; -import { format } from 'date-fns'; +import { format, parseISO } from 'date-fns'; import { COMPLETED, FAILED } from 'data/constants'; import { useLocale } from 'localization'; @@ -46,7 +46,7 @@ function WeekBarChart({ checkmarks, dates }) { const locale = useLocale(); // Label formats - const xValueFormat = (date) => format(new Date(date), 'd-MMM', { locale }); + const xValueFormat = (date) => format(parseISO(date), 'd-MMM', { locale }); // Check if value exists to prevent funny outputs like `null%` const yValueFormat = (v) => v ? v + '%' : ''; diff --git a/src/screens/dashboard.js b/src/screens/dashboard.js index e21bb81..7a82fa7 100644 --- a/src/screens/dashboard.js +++ b/src/screens/dashboard.js @@ -7,7 +7,7 @@ import { useLocale } from 'localization'; import { WeekBarChart } from 'components/week-bar-chart'; import { HabitsTable } from 'components/habits-table'; import { FullPageErrorFallback, FullPageSpinner } from 'components/lib'; -import { UserScores } from 'components/user-scores'; +import { PerformancePanel } from 'components/performance-panel'; import { WeekPicker } from 'components/week-picker'; import { Box, Container, Grid, Hidden, Paper } from '@material-ui/core'; import { @@ -113,10 +113,10 @@ function DashboardScreen() { </LargePaper> ); - // User scores panel - const userScores = ( + // Performance panel + const performancePanel = ( <SmallPaper> - <UserScores checkmarks={checkmarks} goal={performanceGoal} /> + <PerformancePanel checkmarks={checkmarks} goal={performanceGoal} /> </SmallPaper> ); @@ -134,7 +134,7 @@ function DashboardScreen() { {habitsTable} </Grid> <Grid item xs={12}> - {userScores} + {performancePanel} </Grid> <Grid item xs={12}> {barChart} @@ -147,7 +147,7 @@ function DashboardScreen() { <Box sx={{ p: 1 }}> <Grid container spacing={1}> <Grid item sm={6}> - {userScores} + {performancePanel} </Grid> <Grid item sm={6}> {weekPicker} @@ -170,7 +170,7 @@ function DashboardScreen() { {barChart} </Grid> <Grid item md={4}> - {userScores} + {performancePanel} </Grid> <Grid item md={4}> {weekPicker} diff --git a/src/utils/checkmark-helpers.js b/src/utils/checkmark-helpers.js deleted file mode 100644 index 3af26e4..0000000 --- a/src/utils/checkmark-helpers.js +++ /dev/null @@ -1,41 +0,0 @@ -import { getWeek, isThisWeek, isToday } from 'date-fns'; - -/** - * Checkmark object - * - * @typedef Checkmark - * - * @property {string} id - * @property {string} date - * @property {string} habitId - * @property {number} value - */ - -/** - * Is checkmark today? - * - * @returns {boolean} boolean - */ -export function isCheckmarkToday(checkmark) { - return isToday(new Date(checkmark.date)); -} - -/** - * Is checkmark this week? - * - * @param {Checkmark} checkmark - * @returns {boolean} boolean - */ -export function isCheckmarkThisWeek(checkmark) { - return isThisWeek(new Date(checkmark.date)); -} - -/** - * Is checkmark last week? - * - * @returns {boolean} boolean - */ -export function isCheckmarkLastWeek(checkmark) { - return getWeek(new Date(checkmark.date)) === getWeek(new Date()) - 1; -} -