├── .nvmrc ├── .npmrc ├── codecov.yml ├── .czrc ├── .env.local.example ├── .env.production ├── .env.test ├── .prettierrc.json ├── features ├── tasks │ ├── components │ │ ├── Board │ │ │ ├── index.js │ │ │ └── Board.js │ │ ├── Column │ │ │ ├── index.js │ │ │ └── Column.js │ │ ├── DraggableTask │ │ │ ├── index.js │ │ │ ├── handlers.js │ │ │ └── DraggableTask.js │ │ ├── DeleteTaskModal.js │ │ └── EditTaskModal.js │ ├── constants.js │ ├── hooks │ │ ├── useEditTaskModal.js │ │ ├── useDeleteConfirmation.js │ │ ├── useTask.js │ │ ├── useTasks.js │ │ └── useDeleteConfirmation.test.js │ ├── containers │ │ └── EditTask.js │ ├── helpers.js │ ├── handlers.js │ └── helpers.test.js ├── common │ ├── components │ │ ├── MainLayout │ │ │ ├── index.js │ │ │ ├── __snapshots__ │ │ │ │ └── MainLayout.test.js.snap │ │ │ ├── MainLayout.test.js │ │ │ └── MainLayout.js │ │ ├── UserHeader │ │ │ ├── index.js │ │ │ ├── UserHeader.module.css │ │ │ ├── UserHeader.js │ │ │ ├── __snapshots__ │ │ │ │ └── UserHeader.test.js.snap │ │ │ └── UserHeader.test.js │ │ ├── NavigationMenu │ │ │ ├── index.js │ │ │ ├── NavigationMenu.test.js │ │ │ ├── __snapshots__ │ │ │ │ └── NavigationMenu.test.js.snap │ │ │ └── NavigationMenu.js │ │ └── ToggleColorScheme │ │ │ ├── index.js │ │ │ ├── handlers.js │ │ │ ├── __snapshots__ │ │ │ └── ToggleColorScheme.test.js.snap │ │ │ ├── ToggleColorScheme.js │ │ │ ├── ToggleColorScheme.test.js │ │ │ ├── helpers.js │ │ │ ├── handlers.test.js │ │ │ └── helpers.test.js │ └── hooks │ │ ├── useDialog.js │ │ ├── useToggle.js │ │ ├── useLocalData.js │ │ ├── useInterval.js │ │ ├── useBreakpoints.js │ │ ├── useTime.js │ │ └── useColorScheme.js ├── focusSession │ ├── api.js │ ├── hooks │ │ ├── useBreaktimeConfirmation.js │ │ ├── useBreaktimeTimer.js │ │ ├── useChronometer.js │ │ ├── useFocusSessions.js │ │ ├── useFocusSession.js │ │ ├── useBreaktimeConfirmation.test.js │ │ └── useFocusSessions.test.js │ ├── components │ │ ├── FocusSessionFooter.js │ │ ├── __snapshots__ │ │ │ ├── FocusSessionFooter.test.js.snap │ │ │ └── BreaktimeConfirmation.test.js.snap │ │ ├── BreaktimeConfirmation.test.js │ │ ├── FocusSessionFooter.test.js │ │ ├── PauseTimer.js │ │ ├── BreaktimeTimer.js │ │ ├── BreaktimeConfirmation.js │ │ └── Chronometer.js │ ├── helpers.js │ ├── handlers.js │ ├── helpers.test.js │ ├── handlers.test.js │ └── containers │ │ └── FocusSession.js ├── planning │ ├── api.js │ ├── components │ │ ├── __snapshots__ │ │ │ ├── AddTaskButton.test.js.snap │ │ │ └── PlanningFooter.test.js.snap │ │ ├── AddTaskButton.test.js │ │ ├── AddTaskButton.js │ │ ├── PlanningFooter.js │ │ ├── PlanningOnboarding.js │ │ └── PlanningFooter.test.js │ └── containers │ │ └── Planning.js └── retrospective │ ├── components │ └── RetrospectiveFooter.js │ └── containers │ └── Retrospective.js ├── .commitlintrc.json ├── public ├── favicon.ico ├── vercel.svg └── images │ ├── yoga-pause.svg │ └── forest-pause.svg ├── .husky ├── pre-commit └── commit-msg ├── pages ├── api │ ├── auth │ │ └── [...auth0].js │ ├── test │ │ ├── focus-sessions.js │ │ └── tasks.js │ └── local │ │ ├── tasks │ │ ├── [id] │ │ │ ├── index.js │ │ │ ├── reset.js │ │ │ └── complete.js │ │ └── index.js │ │ ├── [...entity].js │ │ └── focus-sessions │ │ ├── active.js │ │ ├── resume.js │ │ ├── pause.js │ │ ├── index.js │ │ └── finish.js ├── retrospective.js ├── planning.js ├── focus-session.js ├── form.js ├── _app.js └── home.js ├── utils ├── httpCodes.js ├── testUtils │ ├── svgrMock.js │ ├── dateNowMock.js │ ├── dummyRender.test.js │ └── dummyRender.js ├── isObject.js ├── time.js ├── buildLocalApiUrl.js ├── isEmpty.js ├── formatMilliseconds.js ├── fetchJsonServer.js ├── timeAgo.js ├── buildLocalApiUrl.test.js ├── timeAgo.test.js ├── isObject.test.js ├── isEmpty.test.js └── formatMilliseconds.test.js ├── .lintstagedrc.json ├── api ├── index.js ├── focusSessions.js ├── request.js └── tasks.js ├── next.config.js ├── config ├── redirects.js ├── index.js └── webpack.js ├── db.json.example ├── .editorconfig ├── tests └── integration │ └── planning.integration.test.js ├── .stylelintrc.json ├── .env.development ├── .gitignore ├── jest.config.js ├── .eslintrc.json ├── jest.setup.js ├── .github └── workflows │ ├── review.yml │ └── release.yml ├── styles └── globals.scss ├── README.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v15 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | github_checks: 2 | annotations: false 3 | -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | # AUTH0 2 | AUTH0_SECRET= 3 | AUTH0_CLIENT_SECRET= 4 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # URLS 2 | NEXT_PUBLIC_API_URL=http://localhost:8080 3 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # URLS 2 | NEXT_PUBLIC_API_URL=http://localhost:3000/api/test 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /features/tasks/components/Board/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Board' 2 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /features/tasks/components/Column/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Column' 2 | -------------------------------------------------------------------------------- /features/common/components/MainLayout/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './MainLayout' 2 | -------------------------------------------------------------------------------- /features/common/components/UserHeader/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './UserHeader' 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glrodasz/cero-web/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /features/common/components/NavigationMenu/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './NavigationMenu' 2 | -------------------------------------------------------------------------------- /features/tasks/components/DraggableTask/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './DraggableTask' 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /features/common/components/ToggleColorScheme/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ToggleColorScheme' 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /features/focusSession/api.js: -------------------------------------------------------------------------------- 1 | export { tasks as tasksApi, focusSessions as focusSessionsApi } from '../../api' 2 | -------------------------------------------------------------------------------- /features/planning/api.js: -------------------------------------------------------------------------------- 1 | export { tasks as tasksApi, focusSessions as focusSessionsApi } from '../../api' 2 | -------------------------------------------------------------------------------- /pages/api/auth/[...auth0].js: -------------------------------------------------------------------------------- 1 | import { handleAuth } from '@auth0/nextjs-auth0' 2 | 3 | export default handleAuth() 4 | -------------------------------------------------------------------------------- /utils/httpCodes.js: -------------------------------------------------------------------------------- 1 | const httpCodes = { 2 | // 3xx Redirection 3 | FOUND: 302, 4 | } 5 | 6 | export default httpCodes 7 | -------------------------------------------------------------------------------- /utils/testUtils/svgrMock.js: -------------------------------------------------------------------------------- 1 | export const ReactComponent = 'div' 2 | 3 | const SvgrURL = 'SvgrURL' 4 | export default SvgrURL 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": "yarn lint:js:fix", 3 | "*.json": "yarn lint:json:fix", 4 | "*.css": "yarn lint:css:fix" 5 | } 6 | -------------------------------------------------------------------------------- /utils/isObject.js: -------------------------------------------------------------------------------- 1 | const isObject = (obj) => { 2 | return Object.prototype.toString.call(obj) === '[object Object]' 3 | } 4 | 5 | export default isObject 6 | -------------------------------------------------------------------------------- /utils/testUtils/dateNowMock.js: -------------------------------------------------------------------------------- 1 | const dateNowMock = () => 2 | jest.fn(() => new Date('1970-01-01T02:00:00.000Z').getTime()) 3 | 4 | export default dateNowMock 5 | -------------------------------------------------------------------------------- /features/tasks/constants.js: -------------------------------------------------------------------------------- 1 | export const IN_PROGRESS_COLUMN_ID = 'in-progress' 2 | export const PENDING_COLUMN_ID = 'pending' 3 | export const COMPLETED_COLUMN_ID = 'completed' 4 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import Tasks from './tasks' 2 | import FocusSession from './focusSessions' 3 | 4 | export const tasks = new Tasks('tasks') 5 | export const focusSessions = new FocusSession('focus-sessions') 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('./config/webpack') 2 | const redirectsConfig = require('./config/redirects') 3 | 4 | module.exports = { 5 | ...redirectsConfig, 6 | ...webpackConfig, 7 | } 8 | -------------------------------------------------------------------------------- /features/common/components/UserHeader/UserHeader.module.css: -------------------------------------------------------------------------------- 1 | .user-header { 2 | display: flex; 3 | } 4 | 5 | .text { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: flex-start; 9 | } 10 | -------------------------------------------------------------------------------- /features/focusSession/hooks/useBreaktimeConfirmation.js: -------------------------------------------------------------------------------- 1 | import useDialog from '../../common/hooks/useDialog' 2 | 3 | const useBreaktimeConfirmation = () => { 4 | return useDialog() 5 | } 6 | 7 | export default useBreaktimeConfirmation 8 | -------------------------------------------------------------------------------- /pages/api/test/focus-sessions.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default (req, res) => { 4 | res.statusCode = 200 5 | // FIXME: Mock filter active sessionss 6 | res.json([]) 7 | } 8 | -------------------------------------------------------------------------------- /features/common/hooks/useDialog.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | const useDialog = () => { 4 | const [showDialog, setShowDialog] = useState(false) 5 | 6 | return { showDialog, setShowDialog } 7 | } 8 | 9 | export default useDialog 10 | -------------------------------------------------------------------------------- /features/common/components/ToggleColorScheme/handlers.js: -------------------------------------------------------------------------------- 1 | import { persistColorScheme } from './helpers' 2 | 3 | export const handleClick = 4 | ({ isDarkMode, setIsDarkMode }) => 5 | () => { 6 | persistColorScheme({ isDarkMode: !isDarkMode, setIsDarkMode }) 7 | } 8 | -------------------------------------------------------------------------------- /config/redirects.js: -------------------------------------------------------------------------------- 1 | const redirectConfig = { 2 | async redirects() { 3 | return [ 4 | { 5 | source: '/', 6 | destination: '/planning', 7 | permanent: true, 8 | }, 9 | ] 10 | }, 11 | } 12 | 13 | module.exports = redirectConfig 14 | -------------------------------------------------------------------------------- /db.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "description": "This is my first task", 5 | "priority": 0, 6 | "status": "in-progress", 7 | "id": 1 8 | } 9 | ], 10 | "focus-sessions": [], 11 | "profile": { 12 | "name": "typicode" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | export const API_URL = process.env.NEXT_PUBLIC_API_URL 2 | export const MAXIMUN_IN_PRIORITY_TASKS = Number( 3 | process.env.NEXT_PUBLIC_MAXIMUN_IN_PRIORITY_TASKS 4 | ) 5 | export const MAXIMUM_BACKLOG_QUANTITY = Number( 6 | process.env.NEXT_PUBLIC_MAXIMUM_BACKLOG_QUANTITY 7 | ) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://github.com/airbnb/javascript/blob/master/.editorconfig 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | end_of_line = lf 11 | 12 | [CHANGELOG.md] 13 | indent_size = false 14 | -------------------------------------------------------------------------------- /features/common/hooks/useToggle.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | const useToggle = (initialState = false) => { 4 | const [isOn, setIsOn] = useState(initialState) 5 | 6 | const toggle = () => setIsOn((prevState) => !prevState) 7 | 8 | return { isOn, toggle } 9 | } 10 | 11 | export default useToggle 12 | -------------------------------------------------------------------------------- /features/focusSession/hooks/useBreaktimeTimer.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import useDialog from '../../common/hooks/useDialog' 3 | 4 | const useBreaktimeTimer = () => { 5 | const [time, setTime] = useState(null) 6 | 7 | return { ...useDialog(), time, setTime } 8 | } 9 | 10 | export default useBreaktimeTimer 11 | -------------------------------------------------------------------------------- /features/tasks/hooks/useEditTaskModal.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import useDialog from '../../common/hooks/useDialog' 3 | 4 | const useEditTaskModal = () => { 5 | const [taskId, setTaskId] = useState(null) 6 | 7 | return { ...useDialog(), taskId, setTaskId } 8 | } 9 | 10 | export default useEditTaskModal 11 | -------------------------------------------------------------------------------- /pages/retrospective.js: -------------------------------------------------------------------------------- 1 | import { withPageAuthRequired } from '@auth0/nextjs-auth0' 2 | import RetrospectiveContainer from '../features/retrospective/containers/Retrospective' 3 | 4 | export const getServerSideProps = withPageAuthRequired() 5 | 6 | export default function Retrospective() { 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /features/tasks/hooks/useDeleteConfirmation.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import useDialog from '../../common/hooks/useDialog' 3 | 4 | const useDeleteConfirmation = () => { 5 | const [taskId, setTaskId] = useState(null) 6 | 7 | return { ...useDialog(), taskId, setTaskId } 8 | } 9 | 10 | export default useDeleteConfirmation 11 | -------------------------------------------------------------------------------- /pages/api/local/tasks/[id]/index.js: -------------------------------------------------------------------------------- 1 | import buildLocalApiUrl from '../../../../../utils/buildLocalApiUrl' 2 | import fetchJsonServer from '../../../../../utils/fetchJsonServer' 3 | 4 | export default function handler(req, res) { 5 | const { url, options } = buildLocalApiUrl(req) 6 | fetchJsonServer({ resource: 'task', url, options, res }) 7 | } 8 | -------------------------------------------------------------------------------- /tests/integration/planning.integration.test.js: -------------------------------------------------------------------------------- 1 | // import { Selector } from 'testcafe' 2 | 3 | fixture`Planning Testing`.page`http://localhost:3000/planning` 4 | 5 | test('when a new task is added it should send a POST request', async (t) => { 6 | await t 7 | .click('#planning-add-button') 8 | .typeText('#planning-add-button-input', 'John Smith') 9 | }) 10 | -------------------------------------------------------------------------------- /features/common/hooks/useLocalData.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | const useLocalData = (fetchedData) => { 4 | const [localData, setLocalData] = useState(fetchedData) 5 | 6 | useEffect(() => { 7 | setLocalData(fetchedData) 8 | }, [fetchedData]) 9 | 10 | return { localData, setLocalData } 11 | } 12 | 13 | export default useLocalData 14 | -------------------------------------------------------------------------------- /pages/api/local/[...entity].js: -------------------------------------------------------------------------------- 1 | import buildLocalApiUrl from '../../../utils/buildLocalApiUrl' 2 | import fetchJsonServer from '../../../utils/fetchJsonServer' 3 | 4 | export default async function handler(req, res) { 5 | let { url, options } = buildLocalApiUrl(req) 6 | const [resource] = req.query.entity 7 | 8 | fetchJsonServer({ resource, url, options, res }) 9 | } 10 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-idiomatic-order" 5 | ], 6 | "rules": { 7 | "selector-pseudo-class-no-unknown": [ 8 | true, 9 | { 10 | "ignorePseudoClasses": ["global"] 11 | } 12 | ], 13 | "at-rule-no-unknown": [ 14 | true, 15 | { 16 | "ignoreAtRules": ["mixin", "include"] 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /features/tasks/components/DraggableTask/handlers.js: -------------------------------------------------------------------------------- 1 | export const handleCompleteTask = 2 | ({ id, onCompleteTask }) => 3 | ({ isChecked }) => { 4 | onCompleteTask({ id, isChecked }) 5 | } 6 | 7 | export const handleDeleteTask = 8 | ({ id, onDeleteTask }) => 9 | () => { 10 | onDeleteTask({ id }) 11 | } 12 | 13 | export const handleEditTask = 14 | ({ id, onEditTask }) => 15 | () => { 16 | onEditTask({ id }) 17 | } 18 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # CONFIG 2 | NEXT_PUBLIC_MAXIMUN_IN_PRIORITY_TASKS=2 3 | NEXT_PUBLIC_MAXIMUM_BACKLOG_QUANTITY=4 4 | 5 | # URLS 6 | NEXT_PUBLIC_BASE_URL=http://localhost:3000 7 | NEXT_PUBLIC_API_URL=http://localhost:3000/api/local 8 | 9 | # JSON SERVER 10 | JSON_SERVER_URL=http://localhost:3001 11 | 12 | # AUTH0 13 | AUTH0_BASE_URL=$NEXT_PUBLIC_BASE_URL 14 | AUTH0_ISSUER_BASE_URL=https://cero-dev.eu.auth0.com 15 | AUTH0_CLIENT_ID=UZZS9FCtzyO052aIcKmicXfNEkY5X9Ra 16 | -------------------------------------------------------------------------------- /features/planning/components/__snapshots__/AddTaskButton.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`[ features / planning / components / AddTaskButton ] when \`AddTaskButton\` is mounted should render 1`] = ` 4 | 5 | 6 | [Spacer.Vertical {"size":"md"} /] 7 | [Button {"id":"","focusHelpText":"Presiona enter","blurHelpText":"Clic para continuar"}]Toca para agregar la tarea[/Button] 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /utils/time.js: -------------------------------------------------------------------------------- 1 | const time = { 2 | FIVE_MINUTES_IN_MS: 300000, 3 | TEN_MINUTES_IN_MS: 600000, 4 | FIFTY_MINUTES_IN_MS: 900000, 5 | ONE_SECOND_IN_MS: 1000, 6 | ONE_HOUR_IN_MS: 3600000, 7 | HALF_HOUR_IN_MS: 1800000, 8 | TWO_HOUR_IN_MS: 7200000, 9 | ONE_DAY_IN_SECONDS: 86400, 10 | ONE_DAY_IN_MS: 86400000, 11 | ONE_HOUR_IN_SECONDS: 3600, 12 | ONE_MINUTE_IN_SECONDS: 60, 13 | ONE_MINUTE_IN_MS: 60000, 14 | ONE_SECOND: 1, 15 | } 16 | 17 | export default time 18 | -------------------------------------------------------------------------------- /features/common/components/ToggleColorScheme/__snapshots__/ToggleColorScheme.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`[ features / common / components / ToggleColorScheme ] when \`ToggleColorScheme\` is mounted should render 1`] = ` 4 | 5 |
8 | 9 | [Check {"isChecked":false} /] 10 | [Heading]Dark Mode[/Heading] 11 |
12 |
13 | `; 14 | -------------------------------------------------------------------------------- /utils/buildLocalApiUrl.js: -------------------------------------------------------------------------------- 1 | const API_BASENAME = '/api/local/' 2 | 3 | const buildLocalApiUrl = (req, fetchOptions = {}) => { 4 | const url = `${req.url.replace(API_BASENAME, '')}` 5 | // https://fetch.spec.whatwg.org/#methods 6 | const normalizedMethod = req.method.toUpperCase() 7 | 8 | const options = { 9 | method: normalizedMethod, 10 | body: normalizedMethod !== 'GET' ? req.body : undefined, 11 | ...fetchOptions, 12 | } 13 | 14 | return { url, options } 15 | } 16 | 17 | export default buildLocalApiUrl 18 | -------------------------------------------------------------------------------- /features/focusSession/components/FocusSessionFooter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import { Spacer, Button } from '@glrodasz/components' 3 | 4 | const FocusSessionFooter = ({ onClickEndSession }) => { 5 | return ( 6 | <> 7 | 8 | 11 | 12 | ) 13 | } 14 | 15 | FocusSessionFooter.propTypes = { 16 | onClickEndSession: PropTypes.func.isRequired, 17 | } 18 | 19 | export default FocusSessionFooter 20 | -------------------------------------------------------------------------------- /features/focusSession/components/__snapshots__/FocusSessionFooter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`[ features / focusSession / components / FocusSessionFooter ] when \`FocusSessionFooter\` is mounted should render 1`] = ` 4 | 5 | 6 | [Spacer.Vertical {"size":"lg"} /] 7 | 16 | 17 | `; 18 | -------------------------------------------------------------------------------- /features/common/components/MainLayout/__snapshots__/MainLayout.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`[ features / common / components / MainLayout ] when \`MainLayout\` is mounted should render 1`] = ` 4 | 5 |
8 | 13 |
16 | content-component 17 |
18 |
19 |
20 | `; 21 | -------------------------------------------------------------------------------- /features/focusSession/helpers.js: -------------------------------------------------------------------------------- 1 | import time from '../../utils/time' 2 | 3 | export const getBarWidth = ( 4 | currentTime, 5 | filledBarTime = time.ONE_HOUR_IN_MS 6 | ) => { 7 | const timeRatio = currentTime % filledBarTime 8 | return (timeRatio * 100) / filledBarTime 9 | } 10 | 11 | export const getChronometerStartTime = ({ startTime, pauseStartTime }) => { 12 | const nowTime = Date.now() 13 | 14 | if (startTime && pauseStartTime) { 15 | return nowTime - (startTime + nowTime - pauseStartTime) 16 | } 17 | 18 | if (startTime) { 19 | return nowTime - startTime 20 | } 21 | 22 | return 0 23 | } 24 | -------------------------------------------------------------------------------- /features/common/hooks/useInterval.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react' 2 | 3 | const useInterval = (callback, delay) => { 4 | const [intervalId, setIntervalId] = useState(null) 5 | const savedCallback = useRef() 6 | 7 | useEffect(() => { 8 | savedCallback.current = callback 9 | }, [callback]) 10 | 11 | useEffect(() => { 12 | const tick = () => savedCallback.current() 13 | 14 | if (delay !== null) { 15 | setIntervalId(setInterval(tick, delay)) 16 | return () => clearInterval(intervalId) 17 | } else { 18 | clearInterval(intervalId) 19 | } 20 | }, [delay]) 21 | } 22 | 23 | export default useInterval 24 | -------------------------------------------------------------------------------- /features/common/components/MainLayout/MainLayout.test.js: -------------------------------------------------------------------------------- 1 | import MainLayout from './MainLayout' 2 | import { render } from '@testing-library/react' 3 | 4 | describe('[ features / common / components / MainLayout ]', () => { 5 | describe('when `MainLayout` is mounted', () => { 6 | it('should render', () => { 7 | // Arrange 8 | const props = { 9 | menu: 'menu-component', 10 | content: 'content-component', 11 | isPlayground: true, 12 | } 13 | 14 | // Act 15 | const { asFragment } = render() 16 | 17 | // Assert 18 | expect(asFragment()).toMatchSnapshot() 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # Optional eslint cache 37 | .eslintcache 38 | 39 | # npm 40 | package-lock.json 41 | 42 | # storybook 43 | storybook-static 44 | 45 | # JSON Server 46 | db.json 47 | -------------------------------------------------------------------------------- /utils/isEmpty.js: -------------------------------------------------------------------------------- 1 | import isObject from './isObject' 2 | 3 | const NOT_EMPTIABLE_TYPES = [ 4 | 'number', 5 | 'undefined', 6 | 'symbol', 7 | 'boolean', 8 | 'bigint', 9 | 'function', 10 | ] 11 | 12 | const isEmpty = (value) => { 13 | if (value === null || NOT_EMPTIABLE_TYPES.includes(typeof value)) { 14 | throw new Error('The value is not an object, an array or a string') 15 | } 16 | 17 | if (isObject(value)) { 18 | return ( 19 | Object.keys(value).length === 0 && 20 | Object.keys(Object.getPrototypeOf(value)).length === 0 21 | ) 22 | } 23 | 24 | if (Array.isArray(value) || typeof value === 'string') { 25 | return value.length === 0 26 | } 27 | } 28 | export default isEmpty 29 | -------------------------------------------------------------------------------- /features/common/components/ToggleColorScheme/ToggleColorScheme.js: -------------------------------------------------------------------------------- 1 | import { Check, Heading } from '@glrodasz/components' 2 | 3 | import { handleClick } from './handlers' 4 | import useColorScheme from '../../hooks/useColorScheme' 5 | 6 | const ToggleColorScheme = () => { 7 | const { isDarkMode, setIsDarkMode } = useColorScheme() 8 | 9 | return ( 10 |
19 | Dark Mode 20 |
21 | ) 22 | } 23 | 24 | export default ToggleColorScheme 25 | -------------------------------------------------------------------------------- /utils/formatMilliseconds.js: -------------------------------------------------------------------------------- 1 | const formatMilliseconds = (milliseconds = 0) => { 2 | if (typeof milliseconds !== 'number') { 3 | throw new Error('milliseconds is not a number') 4 | } 5 | 6 | const seconds = milliseconds / 1000 7 | const minutes = Math.floor(seconds / 60) 8 | const hours = Math.floor(minutes / 60) 9 | 10 | const currentHours = String(hours).padStart(2, '0') 11 | const currentMinutes = String(minutes % 60).padStart(2, '0') 12 | const currentSeconds = String(parseInt(seconds % 60)).padStart(2, '0') 13 | 14 | return currentHours !== '00' 15 | ? `${currentHours}:${currentMinutes}:${currentSeconds}` 16 | : `${currentMinutes}:${currentSeconds}` 17 | } 18 | 19 | export default formatMilliseconds 20 | -------------------------------------------------------------------------------- /api/focusSessions.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | 3 | class FocusSession extends Request { 4 | create() { 5 | return this.fetch('focus-sessions', { 6 | method: 'post', 7 | }) 8 | } 9 | 10 | finish() { 11 | return this.fetch(`focus-sessions/finish`, { 12 | method: 'patch', 13 | }) 14 | } 15 | 16 | getActive() { 17 | return this.fetch('focus-sessions/active') 18 | } 19 | 20 | pause({ time } = {}) { 21 | return this.fetch(`focus-sessions/pause`, { 22 | method: 'patch', 23 | body: { time }, 24 | }) 25 | } 26 | 27 | resume() { 28 | return this.fetch(`focus-sessions/resume`, { 29 | method: 'patch', 30 | }) 31 | } 32 | } 33 | 34 | export default FocusSession 35 | -------------------------------------------------------------------------------- /features/planning/components/__snapshots__/PlanningFooter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`[ features / planning / components / PlanningFooter ] when \`tasksLength\` is greater or equal to one should return the footer 1`] = ` 4 | 5 | 6 | [Spacer.Vertical {"size":"lg"} /] 7 | [Paragraph {"size":"sm"}]Basados en la matriz de Eisenhower priorizamos tus tareas evitando listas de pendientes saturadas.[/Paragraph] 8 | [Spacer.Vertical {"size":"sm"} /] 9 | 18 | 19 | `; 20 | -------------------------------------------------------------------------------- /utils/fetchJsonServer.js: -------------------------------------------------------------------------------- 1 | import Request from '../api/request' 2 | const JSON_SERVER_URL = process.env.JSON_SERVER_URL 3 | 4 | const fetchJsonServer = async ({ 5 | resource, 6 | url, 7 | options, 8 | res, 9 | singular = false, 10 | }) => { 11 | try { 12 | const request = new Request(resource, JSON_SERVER_URL) 13 | let result = await request.fetch(url, options) 14 | 15 | if (singular && Array.isArray(result)) { 16 | result = result[0] ?? {} 17 | } 18 | 19 | return res ? res.status(200).json(result) : Promise.resolve(result) 20 | } catch (error) { 21 | return res 22 | ? res.status(500).json({ error: error.message }) 23 | : Promise.reject(error) 24 | } 25 | } 26 | 27 | export default fetchJsonServer 28 | -------------------------------------------------------------------------------- /features/focusSession/hooks/useChronometer.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react' 2 | import { getChronometerStartTime } from '../helpers' 3 | import useTime from '../../common/hooks/useTime' 4 | 5 | const useChronometer = ({ startTime, pauseStartTime, isPaused }) => { 6 | const totalStartTime = useCallback( 7 | () => 8 | getChronometerStartTime({ 9 | startTime, 10 | pauseStartTime, 11 | }), 12 | [startTime, pauseStartTime] 13 | ) 14 | 15 | const { currentTime, clearTime, resumeTime } = useTime({ 16 | startTime: totalStartTime, 17 | }) 18 | 19 | useEffect(() => { 20 | if (isPaused) { 21 | clearTime() 22 | } 23 | }, [isPaused]) 24 | 25 | return { currentTime, clearTime, resumeTime } 26 | } 27 | 28 | export default useChronometer 29 | -------------------------------------------------------------------------------- /features/planning/components/AddTaskButton.test.js: -------------------------------------------------------------------------------- 1 | import AddTaskButton from './AddTaskButton' 2 | import { render } from '@testing-library/react' 3 | 4 | jest.mock('@glrodasz/components', () => { 5 | const { dummyRender } = require('../../../utils/testUtils/dummyRender') 6 | return { 7 | Spacer: { Vertical: dummyRender('Spacer.Vertical') }, 8 | AddButton: dummyRender('Button'), 9 | } 10 | }) 11 | 12 | describe('[ features / planning / components / AddTaskButton ]', () => { 13 | describe('when `AddTaskButton` is mounted', () => { 14 | it('should render', () => { 15 | const props = { 16 | isShown: true, 17 | onAddTask: () => {}, 18 | } 19 | const { asFragment } = render() 20 | expect(asFragment()).toMatchSnapshot() 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: [ 3 | '{features,api}/**/!(index|constants).js', 4 | '{helpers,scripts}/*.js', 5 | ], 6 | coverageThreshold: { 7 | global: { 8 | branches: 60, 9 | functions: 60, 10 | lines: 60, 11 | statements: 60, 12 | }, 13 | }, 14 | moduleNameMapper: { 15 | '\\.css$': 'identity-obj-proxy', 16 | '\\.svg$': '/utils/testUtils/svgrMock.js', 17 | }, 18 | setupFilesAfterEnv: ['./jest.setup.js'], 19 | testEnvironment: 'jsdom', 20 | testMatch: [ 21 | '**/__tests__/**/*.[jt]s?(x)', 22 | '**/?!(*.integration.)+(spec|test).[jt]s?(x)', 23 | ], 24 | transform: { 25 | '^.+\\.jsx?$': ['babel-jest', { presets: ['next/babel'] }], 26 | }, 27 | transformIgnorePatterns: ['node_modules/(?!@glrodasz/components)'], 28 | } 29 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "jest": true, 7 | "node": true 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:react/recommended", 12 | "next", 13 | "plugin:prettier/recommended", 14 | "plugin:testcafe/recommended" 15 | ], 16 | "globals": { 17 | "Atomics": "readonly", 18 | "SharedArrayBuffer": "readonly" 19 | }, 20 | "parserOptions": { 21 | "ecmaVersion": 2020, 22 | "sourceType": "module", 23 | "ecmaFeatures": { 24 | "jsx": true 25 | } 26 | }, 27 | "plugins": ["react"], 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | }, 33 | "rules": { 34 | "react/react-in-jsx-scope": "off", 35 | "react/forbid-prop-types": "warn" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /api/request.js: -------------------------------------------------------------------------------- 1 | import { API_URL } from '../config' 2 | 3 | class Request { 4 | constructor(resource, baseUrl) { 5 | this.resource = resource 6 | this.baseUrl = baseUrl 7 | } 8 | 9 | fetch(resource = this.resource, options = {}) { 10 | const method = options.method ? options.method.toUpperCase() : 'GET' 11 | const requestOptions = { ...options, method } 12 | 13 | requestOptions.headers = new Headers({ 14 | 'Content-Type': 'application/json', 15 | ...options.headers, 16 | }) 17 | 18 | if (options.body) { 19 | requestOptions.body = JSON.stringify(options.body) 20 | } 21 | 22 | const baseUrl = this.baseUrl ?? API_URL 23 | 24 | return fetch(`${baseUrl}/${resource}`, requestOptions).then((data) => 25 | data.json() 26 | ) 27 | } 28 | } 29 | 30 | export default Request 31 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // https://console.spec.whatwg.org/#loglevel-severity 2 | const CONSOLE_LEVELS = ['debug', 'log', 'info', 'warn', 'error'] 3 | 4 | // Notice that by default the levels will be ["error"] 5 | const allowedConsoleLevels = CONSOLE_LEVELS.slice( 6 | CONSOLE_LEVELS.indexOf(process.env.CONSOLE_LEVEL) 7 | ) 8 | 9 | global.console = CONSOLE_LEVELS.reduce((levels, level) => { 10 | return allowedConsoleLevels.includes(level) 11 | ? { ...levels, [level]: console[level] } 12 | : { ...levels, [level]: jest.fn() } 13 | }, {}) 14 | 15 | // https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-server-rendering-apis 16 | // TODO: dummyRender.js needs to be updated to use the Suspense SSR Architectue instead of renderToStaticMarkup 17 | const { TextEncoder } = require('util') 18 | global.TextEncoder = TextEncoder 19 | -------------------------------------------------------------------------------- /features/retrospective/components/RetrospectiveFooter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import { Spacer, Button } from '@glrodasz/components' 3 | 4 | const RetrospectiveFooter = ({ 5 | onClickRegisterSession, 6 | onClickSkipRegisterSession, 7 | }) => { 8 | return ( 9 | <> 10 | 11 | 14 | 15 | 18 | 19 | ) 20 | } 21 | 22 | RetrospectiveFooter.propTypes = { 23 | onClickRegisterSession: PropTypes.func.isRequired, 24 | onClickSkipRegisterSession: PropTypes.func.isRequired, 25 | } 26 | 27 | export default RetrospectiveFooter 28 | -------------------------------------------------------------------------------- /features/common/components/ToggleColorScheme/ToggleColorScheme.test.js: -------------------------------------------------------------------------------- 1 | import ToggleColorScheme from './ToggleColorScheme' 2 | import { render } from '@testing-library/react' 3 | 4 | jest.mock('@glrodasz/components', () => { 5 | const { dummyRender } = require('../../../../utils/testUtils/dummyRender') 6 | return { 7 | Check: dummyRender('Check'), 8 | Heading: dummyRender('Heading'), 9 | } 10 | }) 11 | 12 | jest.mock('../../hooks/useColorScheme', () => () => ({ 13 | isDarkMode: false, 14 | setIsDarkMode: () => {}, 15 | })) 16 | 17 | describe('[ features / common / components / ToggleColorScheme ]', () => { 18 | describe('when `ToggleColorScheme` is mounted', () => { 19 | it('should render', () => { 20 | const { asFragment } = render() 21 | expect(asFragment()).toMatchSnapshot() 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /features/planning/components/AddTaskButton.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import { Spacer, AddButton } from '@glrodasz/components' 3 | 4 | const AddTaskButton = ({ id, onAddTask, isShown }) => { 5 | if (isShown) { 6 | return ( 7 | <> 8 | 9 | 15 | Toca para agregar la tarea 16 | 17 | 18 | ) 19 | } 20 | 21 | return null 22 | } 23 | 24 | AddTaskButton.defaultProps = { 25 | id: '', 26 | } 27 | 28 | AddTaskButton.propTypes = { 29 | id: PropTypes.string, 30 | onAddTask: PropTypes.func.isRequired, 31 | isShown: PropTypes.bool, 32 | } 33 | 34 | export default AddTaskButton 35 | -------------------------------------------------------------------------------- /config/webpack.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const transpileModules = require('next-transpile-modules') 3 | 4 | const customConfig = { 5 | webpack: (config, options) => { 6 | // resolve unique version of react 7 | // https://github.com/martpie/next-transpile-modules#i-have-trouble-with-duplicated-dependencies-or-the-invalid-hook-call-error-in-react 8 | if (options.isServer) { 9 | config.externals = ['react', ...config.externals] 10 | } 11 | 12 | config.resolve.alias['react'] = path.resolve( 13 | __dirname, 14 | '../node_modules/react' 15 | ) 16 | 17 | // Load SVG as inline React component 18 | config.module.rules.push({ 19 | test: /\.svg$/i, 20 | issuer: /\.[jt]sx?$/, 21 | use: ['@svgr/webpack'], 22 | }) 23 | 24 | return config 25 | }, 26 | } 27 | 28 | module.exports = transpileModules(['@glrodasz/components'])(customConfig) 29 | -------------------------------------------------------------------------------- /features/planning/components/PlanningFooter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import { Spacer, Paragraph, Button } from '@glrodasz/components' 3 | 4 | const PlanningFooter = ({ tasksLength, onClickStartSession }) => { 5 | if (!!tasksLength >= 1) { 6 | return ( 7 | <> 8 | 9 | 10 | Basados en la matriz de Eisenhower priorizamos tus tareas evitando 11 | listas de pendientes saturadas. 12 | 13 | 14 | 17 | 18 | ) 19 | } 20 | 21 | return null 22 | } 23 | 24 | PlanningFooter.propTypes = { 25 | onClickStartSession: PropTypes.func.isRequired, 26 | tasksLength: PropTypes.number, 27 | } 28 | 29 | export default PlanningFooter 30 | -------------------------------------------------------------------------------- /features/focusSession/hooks/useFocusSessions.js: -------------------------------------------------------------------------------- 1 | import { focusSessionsApi } from '../../planning/api' 2 | import { useMutation, useQueryClient } from '@tanstack/react-query' 3 | 4 | const QUERY_KEY = 'focus-sessions' 5 | 6 | export const createMutation = (params) => focusSessionsApi.create(params) 7 | export const finishMutation = (params) => focusSessionsApi.finish(params) 8 | 9 | const useFocusSessions = () => { 10 | const queryClient = useQueryClient() 11 | 12 | const { mutateAsync: create } = useMutation(createMutation, { 13 | onSuccess: () => { 14 | queryClient.invalidateQueries(QUERY_KEY) 15 | }, 16 | }) 17 | 18 | const { mutateAsync: finish } = useMutation(finishMutation, { 19 | onSuccess: () => { 20 | queryClient.invalidateQueries(QUERY_KEY) 21 | }, 22 | }) 23 | 24 | return { 25 | api: { 26 | create, 27 | finish, 28 | }, 29 | } 30 | } 31 | 32 | export default useFocusSessions 33 | -------------------------------------------------------------------------------- /features/planning/components/PlanningOnboarding.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import { Spacer, Heading } from '@glrodasz/components' 3 | 4 | const PlanningOnboarding = ({ tasksLength, children }) => { 5 | return ( 6 | <> 7 | {tasksLength == 0 && ( 8 | <> 9 | 10 | 11 | ¿Cuál es la primera tarea en la que trabajarás hoy? 12 | 13 | 14 | )} 15 | {children} 16 | {tasksLength === 1 && ( 17 | <> 18 | 19 | 20 | Continúa listando las demás tareas de tu día... 21 | 22 | 23 | )} 24 | 25 | ) 26 | } 27 | 28 | PlanningOnboarding.propTypes = { 29 | children: PropTypes.node.isRequired, 30 | tasksLength: PropTypes.number, 31 | } 32 | 33 | export default PlanningOnboarding 34 | -------------------------------------------------------------------------------- /features/common/components/NavigationMenu/NavigationMenu.test.js: -------------------------------------------------------------------------------- 1 | import NavigationMenu from './NavigationMenu' 2 | import { render } from '@testing-library/react' 3 | 4 | jest.mock('next/router', () => ({ 5 | useRouter: () => ({ 6 | pathname: '/home', 7 | }), 8 | })) 9 | 10 | jest.mock('../../hooks/useBreakpoints', () => () => ({ 11 | isDesktop: true, 12 | })) 13 | 14 | jest.mock('@glrodasz/components', () => { 15 | const { dummyRender } = require('../../../../utils/testUtils/dummyRender') 16 | return { 17 | IconLabel: dummyRender('IconLabel'), 18 | } 19 | }) 20 | 21 | describe('[ features / common / components / NavigationMenu ]', () => { 22 | describe('when `NavigationMenu` is mounted', () => { 23 | it('should render', () => { 24 | // Arrange 25 | const props = {} 26 | 27 | // Act 28 | const { asFragment } = render() 29 | 30 | // Assert 31 | expect(asFragment()).toMatchSnapshot() 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /features/common/components/UserHeader/UserHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Avatar, Spacer, Heading, Paragraph } from '@glrodasz/components' 4 | 5 | import styles from './UserHeader.module.css' 6 | 7 | const UserHeader = ({ avatar, title, text, isPrimary }) => { 8 | return ( 9 |
10 | 11 | 12 |
13 | {title} 14 | {isPrimary ? ( 15 | {text} 16 | ) : ( 17 | {text} 18 | )} 19 |
20 |
21 | ) 22 | } 23 | 24 | UserHeader.propTypes = { 25 | avatar: PropTypes.string.isRequired, 26 | title: PropTypes.string.isRequired, 27 | text: PropTypes.string.isRequired, 28 | isPrimary: PropTypes.bool, 29 | } 30 | 31 | export default UserHeader 32 | -------------------------------------------------------------------------------- /features/tasks/hooks/useTask.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { tasksApi } from '../../planning/api' 3 | import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' 4 | 5 | const QUERY_KEY = 'task' 6 | 7 | const useTask = ({ id }) => { 8 | const queryClient = useQueryClient() 9 | 10 | const { 11 | isLoading, 12 | error, 13 | data: serverData, 14 | } = useQuery([QUERY_KEY, id], () => tasksApi.getById({ id })) 15 | 16 | const { mutateAsync: update } = useMutation( 17 | (params) => tasksApi.update(params), 18 | { 19 | onSuccess: () => { 20 | queryClient.invalidateQueries(QUERY_KEY) 21 | }, 22 | } 23 | ) 24 | 25 | const [localData, setLocalData] = useState(serverData) 26 | 27 | useEffect(() => { 28 | setLocalData(serverData) 29 | }, [serverData]) 30 | 31 | return { 32 | isLoading, 33 | error, 34 | data: localData, 35 | setLocalData, 36 | api: { 37 | update, 38 | }, 39 | } 40 | } 41 | 42 | export default useTask 43 | -------------------------------------------------------------------------------- /features/focusSession/components/__snapshots__/BreaktimeConfirmation.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`[ features / focusSession / components / BreaktimeConfirmation ] when \`BreaktimeConfirmation\` is mounted should render 1`] = ` 4 | 5 | 6 | [Modal {"isCentered":true}] 7 | [CenteredContent] 8 | [Picture {"src":"/images/couch-pause.svg","width":200} /] 9 | [Spacer.Vertical {"size":"md"} /] 10 | [Heading {"size":"xl","color":"tertiary","isCentered":true}]Tomate un tiempo para refrescarte[/Heading] 11 | [Spacer.Vertical {"size":"sm"} /] 12 | [Paragraph {"color":"inverted","isCentered":true}]Siempre hay que celebrar los pequeños triunfos, por eso te invitamos a tomar un descanso para despejar tu mente.[/Paragraph] 13 | [Spacer.Vertical {"size":"lg"} /][div style="display:flex;gap:0 20px;width:100%"] 14 | [Button {"isMuted":true}]5 min[/Button] 15 | [Button {"isMuted":true}]10 min[/Button] 16 | [Button {"isMuted":true}]15 min[/Button][/div][/CenteredContent][/Modal] 17 | 18 | `; 19 | -------------------------------------------------------------------------------- /utils/testUtils/dummyRender.test.js: -------------------------------------------------------------------------------- 1 | import { dummyRender } from './dummyRender' 2 | 3 | describe('[ utils / testUtils / dummyRender ]', () => { 4 | describe('when the component is childless', () => { 5 | it('should render without children', () => { 6 | // Arrange 7 | const name = 'Childless' 8 | const props = { foo: 'bar', foobar: true } 9 | 10 | // Act 11 | const result = dummyRender(name)(props) 12 | const expected = '\n[Childless {"foo":"bar","foobar":true} /]' 13 | 14 | // Assert 15 | expect(result).toBe(expected) 16 | }) 17 | }) 18 | 19 | describe('when the component has children', () => { 20 | it('should render with children', () => { 21 | // Arrange 22 | const name = 'WithChildren' 23 | const props = { foo: 'bar', children: 'children' } 24 | 25 | // Act 26 | const result = dummyRender(name)(props) 27 | const expected = '\n[WithChildren {"foo":"bar"}]children[/WithChildren]' 28 | 29 | // Assert 30 | expect(result).toBe(expected) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /api/tasks.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | 3 | class Task extends Request { 4 | getAll() { 5 | return this.fetch() 6 | } 7 | 8 | getById({ id }) { 9 | return id && this.fetch(`tasks/${id}`) 10 | } 11 | 12 | create({ description }) { 13 | return this.fetch('tasks', { 14 | method: 'post', 15 | body: { description }, 16 | }) 17 | } 18 | 19 | updateStatus({ id, isChecked }) { 20 | const status = isChecked ? 'complete' : 'reset' 21 | return this.fetch(`tasks/${id}/${status}`, { method: 'patch' }) 22 | } 23 | 24 | updatePriorities({ tasks }) { 25 | return Promise.all( 26 | tasks.map(({ id, priority, status }) => 27 | this.fetch(`tasks/${id}`, { 28 | method: 'patch', 29 | body: { priority, status }, 30 | }) 31 | ) 32 | ) 33 | } 34 | 35 | delete({ id }) { 36 | return this.fetch(`tasks/${id}`, { method: 'delete' }) 37 | } 38 | 39 | update({ id, task }) { 40 | return this.fetch(`tasks/${id}`, { method: 'patch', body: task }) 41 | } 42 | } 43 | 44 | export default Task 45 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /features/common/components/UserHeader/__snapshots__/UserHeader.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`[ features / common / components / UserHeader ] when \`UserHeader\` is mounted should render 1`] = ` 4 | 5 |
8 | 9 | [Avatar {"src":"avatar"} /] 10 | [Space.Horizontal {"size":"sm"} /] 11 |
14 | 15 | [Heading {"size":"lg"}]title[/Heading] 16 | [Paragraph {"size":"lg"}]text[/Paragraph] 17 |
18 |
19 |
20 | `; 21 | 22 | exports[`[ features / common / components / UserHeader ] when \`UserHeader\` is mounted with \`isPrimary\` as \`true\` should render 1`] = ` 23 | 24 |
27 | 28 | [Avatar {"src":"avatar"} /] 29 | [Space.Horizontal {"size":"sm"} /] 30 |
33 | 34 | [Heading {"size":"lg"}]title[/Heading] 35 | [Heading {"color":"primary"}]text[/Heading] 36 |
37 |
38 |
39 | `; 40 | -------------------------------------------------------------------------------- /features/focusSession/components/BreaktimeConfirmation.test.js: -------------------------------------------------------------------------------- 1 | import BreaktimeConfirmation from './BreaktimeConfirmation' 2 | import { render } from '@testing-library/react' 3 | 4 | jest.mock('@glrodasz/components', () => { 5 | const { dummyRender } = require('../../../utils/testUtils/dummyRender') 6 | return { 7 | Modal: dummyRender('Modal'), 8 | CenteredContent: dummyRender('CenteredContent'), 9 | Picture: dummyRender('Picture'), 10 | Heading: dummyRender('Heading'), 11 | Spacer: { Vertical: dummyRender('Spacer.Vertical') }, 12 | Paragraph: dummyRender('Paragraph'), 13 | Button: dummyRender('Button'), 14 | } 15 | }) 16 | 17 | describe('[ features / focusSession / components / BreaktimeConfirmation ]', () => { 18 | describe('when `BreaktimeConfirmation` is mounted', () => { 19 | it('should render', () => { 20 | const props = { 21 | onClose: () => {}, 22 | onChoose: () => {}, 23 | } 24 | const { asFragment } = render() 25 | expect(asFragment()).toMatchSnapshot() 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /pages/planning.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import { resetServerContext } from 'react-beautiful-dnd' 3 | import { withPageAuthRequired } from '@auth0/nextjs-auth0' 4 | 5 | import PlanningContainer from '../features/planning/containers/Planning' 6 | import { tasksApi, focusSessionsApi } from '../features/planning/api' 7 | import isEmpty from '../utils/isEmpty' 8 | import httpCodes from '../utils/httpCodes' 9 | 10 | export const getServerSideProps = withPageAuthRequired({ 11 | getServerSideProps: async ({ res }) => { 12 | resetServerContext() 13 | 14 | const activeFocusSession = await focusSessionsApi.getActive() 15 | 16 | if (!isEmpty(activeFocusSession)) { 17 | res.statusCode = httpCodes.FOUND 18 | res.setHeader('Location', '/focus-session') 19 | return { props: {} } 20 | } 21 | 22 | const tasks = await tasksApi.getAll() 23 | return { props: { tasks } } 24 | }, 25 | }) 26 | 27 | function Planning({ tasks }) { 28 | return 29 | } 30 | 31 | Planning.propTypes = { 32 | tasks: PropTypes.array, 33 | } 34 | 35 | export default Planning 36 | -------------------------------------------------------------------------------- /features/common/components/ToggleColorScheme/helpers.js: -------------------------------------------------------------------------------- 1 | export const persistColorScheme = ({ isDarkMode, setIsDarkMode }) => { 2 | const colorScheme = isDarkMode ? 'dark' : 'light' 3 | document.querySelector('html').dataset.colorScheme = colorScheme 4 | localStorage.setItem('prefers-color-scheme', colorScheme) 5 | setIsDarkMode && setIsDarkMode(isDarkMode) 6 | } 7 | 8 | export const loadAndListenColorScheme = ({ 9 | setIsDarkMode, 10 | __persistColorScheme = persistColorScheme, 11 | } = {}) => { 12 | const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)') 13 | darkModeMediaQuery?.addListener((event) => { 14 | const isDarkMode = event.matches 15 | __persistColorScheme({ isDarkMode, setIsDarkMode }) 16 | }) 17 | 18 | const localStorageColorScheme = localStorage.getItem('prefers-color-scheme') 19 | 20 | if (localStorageColorScheme) { 21 | const isDarkMode = localStorageColorScheme === 'dark' 22 | __persistColorScheme({ isDarkMode, setIsDarkMode }) 23 | } else { 24 | const isDarkMode = darkModeMediaQuery?.matches 25 | __persistColorScheme({ isDarkMode, setIsDarkMode }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /utils/timeAgo.js: -------------------------------------------------------------------------------- 1 | import time from './time' 2 | 3 | const DEAFULT_LOCALE = 'es-CO' 4 | 5 | const TIME_UNITS_IN_SECONDS = { 6 | day: time.ONE_DAY_IN_SECONDS, 7 | hour: time.ONE_HOUR_IN_SECONDS, 8 | minute: time.ONE_MINUTE_IN_SECONDS, 9 | second: time.ONE_SECOND, 10 | } 11 | 12 | const getSecondsDiff = (timestamp) => 13 | (Date.now() - timestamp) / time.ONE_SECOND_IN_MS 14 | 15 | const getUnitAndValueTime = (secondsElapsed) => { 16 | const entries = Object.entries(TIME_UNITS_IN_SECONDS) 17 | 18 | for (const [unit, unitInSeconds] of entries) { 19 | const match = secondsElapsed >= unitInSeconds || unit === 'second' 20 | 21 | if (match) { 22 | const value = Math.floor(secondsElapsed / unitInSeconds) 23 | return { value, unit } 24 | } 25 | } 26 | } 27 | 28 | const timeAgo = (timestamp, locale = DEAFULT_LOCALE) => { 29 | const relativeTimeFormat = new Intl.RelativeTimeFormat(locale) 30 | 31 | const secondsElapsed = getSecondsDiff(timestamp) 32 | const { value, unit } = getUnitAndValueTime(secondsElapsed) 33 | 34 | return relativeTimeFormat.format(value * -1, unit) 35 | } 36 | 37 | export default timeAgo 38 | -------------------------------------------------------------------------------- /features/tasks/components/DeleteTaskModal.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import { 3 | Modal, 4 | CenteredContent, 5 | Heading, 6 | Button, 7 | Spacer, 8 | Paragraph, 9 | } from '@glrodasz/components' 10 | 11 | const DeleteTaskModal = ({ onClickCancel, onClickConfirm }) => { 12 | return ( 13 | 14 | 15 | 16 | ¿Estás seguro de querer eliminar esta tarea? 17 | 18 | 19 | La tarea se eliminará de manera permanente. 20 | 21 | 24 | 25 | 28 | 29 | 30 | ) 31 | } 32 | 33 | DeleteTaskModal.propTypes = { 34 | onClickCancel: PropTypes.func.isRequired, 35 | onClickConfirm: PropTypes.func.isRequired, 36 | } 37 | 38 | export default DeleteTaskModal 39 | -------------------------------------------------------------------------------- /features/common/components/NavigationMenu/__snapshots__/NavigationMenu.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`[ features / common / components / NavigationMenu ] when \`NavigationMenu\` is mounted should render 1`] = ` 4 | 5 | 33 | 34 | `; 35 | -------------------------------------------------------------------------------- /.github/workflows/review.yml: -------------------------------------------------------------------------------- 1 | name: Code Review 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | name: Build, Lint, and Test 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | - name: Setup Node.js v16 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 16 17 | - name: Install dependencies 18 | run: yarn install --frozen-lockfile 19 | - name: Lint code 20 | run: yarn lint 21 | - name: Run tests 22 | run: yarn test 23 | - name: Build project 24 | run: yarn build 25 | coverage: 26 | name: Coverage 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v2 31 | - name: Setup Node.js v16 32 | uses: actions/setup-node@v1 33 | with: 34 | node-version: 16 35 | - name: Install dependencies 36 | run: yarn install --frozen-lockfile 37 | - name: Run coverage tests 38 | run: yarn test:coverage 39 | - name: Upload coverage 40 | uses: codecov/codecov-action@v1 41 | -------------------------------------------------------------------------------- /features/common/hooks/useBreakpoints.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const queries = [ 4 | { 5 | name: 'desktop', 6 | media: '(min-width: 992px)', 7 | }, 8 | ] 9 | 10 | const useBreakpoints = () => { 11 | const [breakpoints, setBreakpoints] = useState({ 12 | isDesktop: false, 13 | isMobile: false, 14 | }) 15 | 16 | useEffect(() => { 17 | function handleResize() { 18 | const mediaQueries = queries.map((query) => { 19 | const mediaQuery = window.matchMedia(query.media) 20 | return { 21 | name: query.name, 22 | media: mediaQuery.media, 23 | matches: mediaQuery.matches, 24 | } 25 | }) 26 | 27 | const matchedQuery = mediaQueries.find((mediaQuery) => mediaQuery.matches) 28 | 29 | if (matchedQuery?.name === 'desktop') { 30 | setBreakpoints({ isDesktop: true, isMobile: false }) 31 | } else { 32 | setBreakpoints({ isDesktop: false, isMobile: true }) 33 | } 34 | } 35 | 36 | window.addEventListener('resize', handleResize) 37 | handleResize() 38 | 39 | return () => window.removeEventListener('resize', handleResize) 40 | }, []) 41 | 42 | return breakpoints 43 | } 44 | 45 | export default useBreakpoints 46 | -------------------------------------------------------------------------------- /features/common/hooks/useTime.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react' 2 | import time from '../../../utils/time' 3 | import useInterval from './useInterval' 4 | 5 | const tick = 6 | ({ currentTime, setCurrentTime, isTimer }) => 7 | () => { 8 | const modifier = isTimer 9 | ? time.ONE_SECOND_IN_MS * -1 10 | : time.ONE_SECOND_IN_MS 11 | setCurrentTime(currentTime + modifier) 12 | } 13 | 14 | const useTime = ( 15 | { isTimer = false, startTime = 0, endTime = null, callback = () => {} } = { 16 | startTime: 0, 17 | } 18 | ) => { 19 | const [currentTime, setCurrentTime] = useState(startTime) 20 | const [delay, setDelay] = useState(time.ONE_SECOND_IN_MS) 21 | 22 | useInterval(tick({ currentTime, setCurrentTime, isTimer }), delay) 23 | 24 | const clearTime = useCallback(() => setDelay(null)) 25 | const resumeTime = useCallback(() => setDelay(time.ONE_SECOND_IN_MS)) 26 | 27 | useEffect(() => { 28 | if ((isTimer && currentTime <= 0) || (endTime && currentTime >= endTime)) { 29 | callback() 30 | } 31 | }, [currentTime]) 32 | 33 | useEffect(() => { 34 | setCurrentTime(startTime) 35 | }, [startTime]) 36 | 37 | return { currentTime, clearTime, resumeTime } 38 | } 39 | 40 | export default useTime 41 | -------------------------------------------------------------------------------- /pages/focus-session.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import { withPageAuthRequired } from '@auth0/nextjs-auth0' 3 | import FocusSessionContainer from '../features/focusSession/containers/FocusSession' 4 | import { resetServerContext } from 'react-beautiful-dnd' 5 | import isEmpty from '../utils/isEmpty' 6 | import httpCodes from '../utils/httpCodes' 7 | 8 | import { tasksApi, focusSessionsApi } from '../features/planning/api' 9 | 10 | export const getServerSideProps = withPageAuthRequired({ 11 | getServerSideProps: async ({ res }) => { 12 | resetServerContext() 13 | 14 | const tasks = await tasksApi.getAll() 15 | const activeFocusSession = await focusSessionsApi.getActive() 16 | 17 | if (isEmpty(activeFocusSession)) { 18 | res.statusCode = httpCodes.FOUND 19 | res.setHeader('Location', '/planning') 20 | 21 | return { props: {} } 22 | } 23 | 24 | return { props: { tasks, activeFocusSession } } 25 | }, 26 | }) 27 | 28 | const FocusSession = ({ tasks, activeFocusSession }) => { 29 | return 30 | } 31 | 32 | FocusSession.propTypes = { 33 | tasks: PropTypes.array, 34 | activeFocusSession: PropTypes.object, 35 | } 36 | 37 | export default FocusSession 38 | -------------------------------------------------------------------------------- /utils/testUtils/dummyRender.js: -------------------------------------------------------------------------------- 1 | import { renderToStaticMarkup } from 'react-dom/server' 2 | import isEmpty from '../isEmpty' 3 | 4 | const EMPTY_SPACE = ' ' 5 | const NEW_LINE = '\n' 6 | 7 | const objectStringify = (obj) => 8 | !isEmpty(obj) ? `${EMPTY_SPACE}${JSON.stringify(obj)}` : '' 9 | 10 | const cleanMarkup = (markup) => 11 | markup 12 | .replace(/&quot;/g, '"') 13 | .replace(/"/g, '"') 14 | .replace(/</g, '[') 15 | .replace(/>/g, ']') 16 | 17 | const renderStatic = (children) => { 18 | const isAnString = typeof children === 'string' 19 | return !isAnString 20 | ? cleanMarkup(`${renderToStaticMarkup(children)}`) 21 | : children 22 | } 23 | 24 | const dummyComponent = ({ name, props, children }) => { 25 | const stringifiedProps = objectStringify(props) 26 | const closeTag = children ? `]${children}[/${name}]` : ' /]' 27 | 28 | return `${NEW_LINE}[${name}${stringifiedProps}${closeTag}` 29 | } 30 | 31 | export const dummyRender = (name) => (props) => { 32 | if (props.children) { 33 | const { children, ...rest } = props 34 | return dummyComponent({ 35 | name, 36 | props: rest, 37 | children: `${renderStatic(children)}`, 38 | }) 39 | } 40 | 41 | return `${dummyComponent({ name, props })}` 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | name: Build, Lint, and Test 9 | if: "!contains(github.event.head_commit.message, 'skip ci')" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Setup Node.js v16 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 16 18 | - name: Install dependencies 19 | run: yarn install --frozen-lockfile 20 | - name: Lint code 21 | run: yarn lint 22 | - name: Run tests 23 | run: yarn test 24 | - name: Build project 25 | run: yarn build 26 | coverage: 27 | name: Coverage 28 | if: "!contains(github.event.head_commit.message, 'skip ci')" 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout code 32 | uses: actions/checkout@v2 33 | - name: Setup Node.js v16 34 | uses: actions/setup-node@v1 35 | with: 36 | node-version: 16 37 | - name: Install dependencies 38 | run: yarn install --frozen-lockfile 39 | - name: Run coverage tests 40 | run: yarn test:coverage 41 | - name: Upload coverage 42 | uses: codecov/codecov-action@v1 43 | -------------------------------------------------------------------------------- /features/common/components/UserHeader/UserHeader.test.js: -------------------------------------------------------------------------------- 1 | import UserHeader from './UserHeader' 2 | import { render } from '@testing-library/react' 3 | 4 | jest.mock('@glrodasz/components', () => { 5 | const { dummyRender } = require('../../../../utils/testUtils/dummyRender') 6 | 7 | return { 8 | Avatar: dummyRender('Avatar'), 9 | Spacer: { Horizontal: dummyRender('Space.Horizontal') }, 10 | Heading: dummyRender('Heading'), 11 | Paragraph: dummyRender('Paragraph'), 12 | } 13 | }) 14 | 15 | describe('[ features / common / components / UserHeader ]', () => { 16 | describe('when `UserHeader` is mounted', () => { 17 | it('should render', () => { 18 | const props = { 19 | avatar: 'avatar', 20 | title: 'title', 21 | text: 'text', 22 | } 23 | const { asFragment } = render() 24 | expect(asFragment()).toMatchSnapshot() 25 | }) 26 | }) 27 | 28 | describe('when `UserHeader` is mounted with `isPrimary` as `true`', () => { 29 | it('should render', () => { 30 | const props = { 31 | avatar: 'avatar', 32 | title: 'title', 33 | text: 'text', 34 | isPrimary: true, 35 | } 36 | const { asFragment } = render() 37 | expect(asFragment()).toMatchSnapshot() 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /features/common/hooks/useColorScheme.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const persistColorScheme = ({ isDarkMode, setIsDarkMode }) => { 4 | const colorScheme = isDarkMode ? 'dark' : 'light' 5 | document.querySelector('html').dataset.colorScheme = colorScheme 6 | localStorage.setItem('prefers-color-scheme', colorScheme) 7 | setIsDarkMode && setIsDarkMode(isDarkMode) 8 | } 9 | const useColorScheme = (__persistColorScheme = persistColorScheme) => { 10 | const [isDarkMode, setIsDarkMode] = useState(false) 11 | 12 | useEffect(() => { 13 | const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)') 14 | darkModeMediaQuery?.addListener((event) => { 15 | const isDarkMode = event.matches 16 | __persistColorScheme({ isDarkMode, setIsDarkMode }) 17 | }) 18 | 19 | const localStorageColorScheme = localStorage.getItem('prefers-color-scheme') 20 | 21 | if (localStorageColorScheme) { 22 | const isDarkMode = localStorageColorScheme === 'dark' 23 | __persistColorScheme({ isDarkMode, setIsDarkMode }) 24 | } else { 25 | const isDarkMode = darkModeMediaQuery?.matches 26 | __persistColorScheme({ isDarkMode, setIsDarkMode }) 27 | } 28 | }, []) 29 | 30 | return { 31 | isDarkMode, 32 | setIsDarkMode, 33 | } 34 | } 35 | 36 | export default useColorScheme 37 | -------------------------------------------------------------------------------- /features/focusSession/hooks/useFocusSession.js: -------------------------------------------------------------------------------- 1 | import { focusSessionsApi } from '../api' 2 | import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' 3 | import useLocalData from '../../common/hooks/useLocalData' 4 | 5 | const QUERY_KEY = 'focus-session' 6 | 7 | export const pauseMutation = (params) => focusSessionsApi.pause(params) 8 | export const resumeMutation = () => focusSessionsApi.resume() 9 | 10 | const useFocusSession = ({ initialData, onResume }) => { 11 | const queryClient = useQueryClient() 12 | 13 | const { 14 | isLoading, 15 | error, 16 | data: fetchedData, 17 | } = useQuery([QUERY_KEY], () => focusSessionsApi.getActive(), { initialData }) 18 | 19 | const { mutateAsync: pause } = useMutation(pauseMutation, { 20 | onSuccess: () => { 21 | queryClient.invalidateQueries(QUERY_KEY) 22 | }, 23 | }) 24 | 25 | const { mutateAsync: resume } = useMutation(resumeMutation, { 26 | onSuccess: () => { 27 | queryClient.invalidateQueries(QUERY_KEY) 28 | onResume?.() 29 | }, 30 | }) 31 | 32 | const { localData, setLocalData } = useLocalData(fetchedData) 33 | 34 | return { 35 | isLoading, 36 | error, 37 | data: localData, 38 | setLocalData, 39 | api: { 40 | pause, 41 | resume, 42 | }, 43 | } 44 | } 45 | 46 | export default useFocusSession 47 | -------------------------------------------------------------------------------- /features/common/components/ToggleColorScheme/handlers.test.js: -------------------------------------------------------------------------------- 1 | import { handleClick } from './handlers' 2 | 3 | import { persistColorScheme } from './helpers' 4 | jest.mock('./helpers', () => ({ 5 | persistColorScheme: jest.fn(), 6 | })) 7 | 8 | describe('[ features / common / ToggleColorSheme / handlers ]', () => { 9 | describe('#handleClick', () => { 10 | describe('when `handleClick` is called', () => { 11 | it('should return a function', () => { 12 | // Arrange 13 | const params = {} 14 | 15 | // Act 16 | const result = typeof handleClick(params) 17 | const expected = 'function' 18 | 19 | // Assert 20 | expect(result).toBe(expected) 21 | }) 22 | }) 23 | 24 | describe('when `handleClick` returned function is called', () => { 25 | it('should called `persistColorScheme` with parameters', () => { 26 | // Arrange 27 | const setIsDarkModeMock = () => {} 28 | const params = { 29 | isDarkMode: true, 30 | setIsDarkMode: setIsDarkModeMock, 31 | } 32 | 33 | // Act 34 | handleClick(params)() 35 | 36 | // Assert 37 | expect(persistColorScheme).toHaveBeenCalledWith({ 38 | isDarkMode: false, 39 | setIsDarkMode: setIsDarkModeMock, 40 | }) 41 | }) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /utils/buildLocalApiUrl.test.js: -------------------------------------------------------------------------------- 1 | import buildLocalApiUrl from './buildLocalApiUrl' 2 | 3 | describe('[ utils / buildLocalApiUrl ]', () => { 4 | describe("when `req.method` is `'get'`", () => { 5 | it('should return `{ url, options } with an `undefined` `body``', () => { 6 | // Arrange 7 | const req = { 8 | url: '/api/local/myurl', 9 | method: 'get', 10 | body: 'body', 11 | } 12 | const fetchOptions = {} 13 | 14 | // Act 15 | const result = buildLocalApiUrl(req, fetchOptions) 16 | const expected = { 17 | url: 'myurl', 18 | options: { 19 | method: 'GET', 20 | body: undefined, 21 | }, 22 | } 23 | 24 | // Assert 25 | expect(result).toEqual(expected) 26 | }) 27 | }) 28 | 29 | describe("when `req.method` is `'patch'`", () => { 30 | it('should return `{ url, options }` with an valid `body`', () => { 31 | // Arrange 32 | const req = { 33 | url: '/api/local/myurl', 34 | method: 'patch', 35 | body: 'body', 36 | } 37 | const fetchOptions = {} 38 | 39 | // Act 40 | const result = buildLocalApiUrl(req, fetchOptions) 41 | const expected = { 42 | url: 'myurl', 43 | options: { 44 | method: 'PATCH', 45 | body: 'body', 46 | }, 47 | } 48 | 49 | // Assert 50 | expect(result).toEqual(expected) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /features/focusSession/components/FocusSessionFooter.test.js: -------------------------------------------------------------------------------- 1 | import FocusSessionFooter from './FocusSessionFooter' 2 | import { render, screen, fireEvent } from '@testing-library/react' 3 | 4 | jest.mock('@glrodasz/components', () => { 5 | const { dummyRender } = require('../../../utils/testUtils/dummyRender') 6 | const originalModule = jest.requireActual('@glrodasz/components') 7 | return { 8 | ...originalModule, 9 | Spacer: { Vertical: dummyRender('Spacer.Vertical') }, 10 | } 11 | }) 12 | 13 | describe('[ features / focusSession / components / FocusSessionFooter ]', () => { 14 | describe('when `FocusSessionFooter` is mounted', () => { 15 | it('should render', () => { 16 | // Arrange 17 | const props = { 18 | onClickEndSession: () => {}, 19 | } 20 | 21 | // Act 22 | const { asFragment } = render() 23 | 24 | // Assert 25 | expect(asFragment()).toMatchSnapshot() 26 | }) 27 | }) 28 | 29 | describe('when then `Button` is clicked', () => { 30 | it('should call `onClickEndSession`', () => { 31 | // Arrange 32 | const onClickEndSessionMock = jest.fn() 33 | const props = { 34 | onClickEndSession: onClickEndSessionMock, 35 | } 36 | 37 | // Act 38 | render() 39 | fireEvent.click(screen.getByText('Finalizar tu sesión')) 40 | 41 | // Assert 42 | expect(onClickEndSessionMock).toHaveBeenCalled() 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /features/tasks/containers/EditTask.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import EditTaskModal from '../components/EditTaskModal' 4 | import useTask from '../hooks/useTask' 5 | 6 | import { 7 | handleDeleteTask, 8 | handleCloseEditTaskModal, 9 | handleUpdateTask, 10 | } from '../../tasks/handlers' 11 | 12 | const EditTask = ({ editTaskModal, deleteConfirmation }) => { 13 | const { taskId } = editTaskModal 14 | const task = useTask({ 15 | id: taskId, 16 | }) 17 | 18 | return ( 19 | <> 20 | {editTaskModal.showDialog && ( 21 | 31 | )} 32 | 33 | ) 34 | } 35 | 36 | EditTask.propTypes = { 37 | editTaskModal: PropTypes.shape({ 38 | taskId: PropTypes.string.isRequired, 39 | setTaskId: PropTypes.func.isRequired, 40 | showDialog: PropTypes.bool.isRequired, 41 | setShowDialog: PropTypes.func.isRequired, 42 | }).isRequired, 43 | deleteConfirmation: PropTypes.shape({ 44 | taskId: PropTypes.string.isRequired, 45 | setTaskId: PropTypes.func.isRequired, 46 | showDialog: PropTypes.bool.isRequired, 47 | setShowDialog: PropTypes.func.isRequired, 48 | }).isRequired, 49 | } 50 | 51 | export default EditTask 52 | -------------------------------------------------------------------------------- /pages/form.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { 4 | Heading, 5 | Input, 6 | Button, 7 | Spacer, 8 | CenteredContent, 9 | } from '@glrodasz/components' 10 | 11 | export default function Index() { 12 | const [formValues, setFormValues] = useState({}) 13 | 14 | const onChange = (key) => (event) => { 15 | const { value } = event.target 16 | setFormValues({ ...formValues, [key]: value }) 17 | } 18 | 19 | return ( 20 | 21 | Cuentame sobre ti 22 | 23 | 28 | 29 | 34 | 35 | 40 | 41 |
48 | 49 | 50 | 51 | 52 |
53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /features/focusSession/components/PauseTimer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { 4 | Modal, 5 | CenteredContent, 6 | Picture, 7 | Spacer, 8 | Heading, 9 | Paragraph, 10 | Button, 11 | } from '@glrodasz/components' 12 | 13 | import useTime from '../../common/hooks/useTime' 14 | import formatMilliseconds from '../../../utils/formatMilliseconds' 15 | 16 | const createHandleClose = 17 | ({ onClose }) => 18 | () => { 19 | onClose() 20 | } 21 | 22 | const PauseTimer = ({ onClose }) => { 23 | const { currentTime } = useTime({ 24 | isTimer: false, // TODO: type: { isTimer: true, isStopwatch: false } 25 | }) 26 | 27 | return ( 28 | 29 | 30 | 31 | {formatMilliseconds(currentTime)} 32 | 33 | 34 | 35 | 36 | 37 | TU RETO ESTA EN PAUSA 38 | 39 | 40 | 41 | Durante esta pausa tu tiempo de productividad no será registrado. 42 | 43 | 44 | 47 | 48 | 49 | ) 50 | } 51 | 52 | export default PauseTimer 53 | -------------------------------------------------------------------------------- /pages/api/local/tasks/[id]/reset.js: -------------------------------------------------------------------------------- 1 | import buildLocalApiUrl from '../../../../../utils/buildLocalApiUrl' 2 | import fetchJsonServer from '../../../../../utils/fetchJsonServer' 3 | 4 | async function getPendingTasks({ options }) { 5 | const fetchOptions = { 6 | ...options, 7 | method: 'get', 8 | body: undefined, 9 | } 10 | 11 | return fetchJsonServer({ 12 | resource: 'task', 13 | url: 'tasks?status=pending', 14 | options: fetchOptions, 15 | }) 16 | } 17 | 18 | async function updatePendingTasksPriority({ tasks, options }) { 19 | return await Promise.all( 20 | tasks.map(({ id, ...body }, index) => { 21 | const fetchOptions = { 22 | ...options, 23 | method: 'patch', 24 | body: { ...body, priority: index + 1 }, 25 | } 26 | 27 | return fetchJsonServer({ 28 | resource: 'task', 29 | url: `tasks/${id}`, 30 | options: fetchOptions, 31 | }) 32 | }) 33 | ) 34 | } 35 | 36 | async function resetTask({ taskId, options, res }) { 37 | const fetchOptions = { 38 | ...options, 39 | body: { status: 'pending', priority: 0 }, 40 | } 41 | return fetchJsonServer({ 42 | resource: 'task', 43 | url: `tasks/${taskId}`, 44 | options: fetchOptions, 45 | res, 46 | }) 47 | } 48 | 49 | export default async function handler(req, res) { 50 | const { options } = buildLocalApiUrl(req) 51 | 52 | if (req.method === 'PATCH') { 53 | const pendingTasks = await getPendingTasks({ options }) 54 | await updatePendingTasksPriority({ tasks: pendingTasks, options }) 55 | await resetTask({ taskId: req.query.id, options, res }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pages/api/local/focus-sessions/active.js: -------------------------------------------------------------------------------- 1 | import buildLocalApiUrl from '../../../../utils/buildLocalApiUrl' 2 | import fetchJsonServer from '../../../../utils/fetchJsonServer' 3 | import isEmpty from '../../../../utils/isEmpty' 4 | 5 | async function getActiveFocusSession({ options }) { 6 | const fetchOptions = { 7 | ...options, 8 | method: 'get', 9 | body: undefined, 10 | } 11 | 12 | return fetchJsonServer({ 13 | resource: 'focus-sessions', 14 | url: 'focus-sessions?status=active', 15 | options: fetchOptions, 16 | singular: true, 17 | }) 18 | } 19 | 20 | function getTotalPauseTime(totalPauseTime, pause) { 21 | if (pause.endTime != null) { 22 | const pauseTime = pause.endTime - pause.startTime 23 | return totalPauseTime + pauseTime 24 | } 25 | return totalPauseTime 26 | } 27 | 28 | function getActiveFocusSessionWithPauseTime({ activeFocusSession, res }) { 29 | const currentPauses = activeFocusSession?.pauses ?? [] 30 | const totalPauseTime = currentPauses.reduce(getTotalPauseTime, 0) 31 | 32 | const calculatedActiveFocusSession = { 33 | ...activeFocusSession, 34 | startTime: activeFocusSession.startTime + totalPauseTime, 35 | } 36 | 37 | res.status(200).json(calculatedActiveFocusSession) 38 | } 39 | 40 | export default async function handler(req, res) { 41 | const { options } = buildLocalApiUrl(req) 42 | 43 | if (req.method === 'GET') { 44 | const activeFocusSession = await getActiveFocusSession({ options }) 45 | if (isEmpty(activeFocusSession)) { 46 | res.status(200).json({}) 47 | } else { 48 | getActiveFocusSessionWithPauseTime({ activeFocusSession, res }) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pages/api/local/tasks/[id]/complete.js: -------------------------------------------------------------------------------- 1 | import buildLocalApiUrl from '../../../../../utils/buildLocalApiUrl' 2 | import fetchJsonServer from '../../../../../utils/fetchJsonServer' 3 | 4 | async function getCompletedTasks({ options }) { 5 | const fetchOptions = { 6 | ...options, 7 | method: 'get', 8 | body: undefined, 9 | } 10 | 11 | return fetchJsonServer({ 12 | resource: 'task', 13 | url: 'tasks?status=completed', 14 | options: fetchOptions, 15 | }) 16 | } 17 | 18 | async function updateCompletedTasksPriority({ tasks, options }) { 19 | return await Promise.all( 20 | tasks.map(({ id, ...body }, index) => { 21 | const fetchOptions = { 22 | ...options, 23 | method: 'patch', 24 | body: { ...body, priority: index + 1 }, 25 | } 26 | 27 | return fetchJsonServer({ 28 | resource: 'task', 29 | url: `tasks/${id}`, 30 | options: fetchOptions, 31 | }) 32 | }) 33 | ) 34 | } 35 | 36 | async function completeTask({ taskId, options, res }) { 37 | const fetchOptions = { 38 | ...options, 39 | body: { status: 'completed', priority: 0 }, 40 | } 41 | 42 | return fetchJsonServer({ 43 | resource: 'task', 44 | url: `tasks/${taskId}`, 45 | options: fetchOptions, 46 | res, 47 | }) 48 | } 49 | 50 | export default async function handler(req, res) { 51 | const { options } = buildLocalApiUrl(req) 52 | 53 | if (req.method === 'PATCH') { 54 | const completedTasks = await getCompletedTasks({ options }) 55 | await updateCompletedTasksPriority({ tasks: completedTasks, options }) 56 | await completeTask({ taskId: req.query.id, options, res }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /features/tasks/components/EditTaskModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Modal, Heading, Paragraph } from '@glrodasz/components' 4 | 5 | import timeAgo from '../../../utils/timeAgo' 6 | 7 | const handleClose = 8 | ({ onClose }) => 9 | () => { 10 | onClose() 11 | } 12 | 13 | const handleDelete = 14 | ({ id, onDelete }) => 15 | () => { 16 | onDelete({ id }) 17 | } 18 | 19 | const handleUpdate = 20 | ({ id, onUpdate }) => 21 | (event) => { 22 | const description = event.currentTarget.textContent 23 | onUpdate({ id, data: { description } }) 24 | } 25 | 26 | const EditTaskModal = ({ task, onClose, onDelete, onUpdate }) => { 27 | return ( 28 | 36 |
37 | 42 | {task?.description} 43 | 44 | {task?.createdAt && ( 45 | 46 | Creada {timeAgo(task?.createdAt)} 47 | 48 | )} 49 |
50 |
51 | ) 52 | } 53 | 54 | EditTaskModal.propTypes = { 55 | task: PropTypes.object, 56 | onClose: PropTypes.func, 57 | onDelete: PropTypes.func, 58 | } 59 | 60 | EditTaskModal.defaultProps = { 61 | onClose: () => {}, 62 | onDelete: () => {}, 63 | } 64 | 65 | export default EditTaskModal 66 | -------------------------------------------------------------------------------- /pages/api/test/tasks.js: -------------------------------------------------------------------------------- 1 | export default (req, res) => { 2 | res.statusCode = 200 3 | res.json([ 4 | { 5 | description: 6 | "Crevice returns knocking sleeping Thranduil venture enters roll. Sisters Luin relative wax country wished pouf travel they've self moonlight fashioning.", 7 | priority: 0, 8 | status: 'in-progress', 9 | id: 2, 10 | }, 11 | { 12 | description: 13 | "One Ring to rule them all. Foreign paid pushing hair strength's regurgitation hot Isildur's manage torches slightest", 14 | priority: 1, 15 | status: 'in-progress', 16 | id: 32, 17 | }, 18 | { 19 | description: 20 | "One Ring to rule them all. Foreign paid pushing hair strength's regurgitation hot Isildur's manage torches slightest", 21 | priority: 2, 22 | status: 'in-progress', 23 | id: 33, 24 | }, 25 | { 26 | description: 27 | 'Done possess candles hairy heir rune odds 30 Caradhras crispy swish labyrinth?', 28 | priority: 3, 29 | status: 'pending', 30 | id: 5, 31 | }, 32 | { 33 | description: 34 | 'Done possess candles hairy heir rune odds 30 Caradhras crispy swish labyrinth?', 35 | priority: 4, 36 | status: 'pending', 37 | id: 6, 38 | }, 39 | { 40 | description: 41 | 'Done possess candles hairy heir rune odds 30 Caradhras crispy swish labyrinth?', 42 | priority: 5, 43 | status: 'pending', 44 | id: 7, 45 | }, 46 | { 47 | description: "Pity's wheel King's. Nazgûl butter youngest report.", 48 | priority: 6, 49 | status: 'completed', 50 | id: 1, 51 | }, 52 | ]) 53 | } 54 | -------------------------------------------------------------------------------- /features/focusSession/hooks/useBreaktimeConfirmation.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import { renderHook, act } from '@testing-library/react-hooks' 3 | import useBreaktimeConfirmation from './useBreaktimeConfirmation' 4 | 5 | describe('[ features / focusSession / hooks / useBreaktimeConfirmation ]', () => { 6 | describe('when `useBreaktimeConfirmation` is called', () => { 7 | it('should return a `showDialog` as `false`', () => { 8 | // Arrange 9 | const hook = () => useBreaktimeConfirmation() 10 | 11 | // Act 12 | const { result: hookResult } = renderHook(hook) 13 | const result = hookResult.current.showDialog 14 | const expected = false 15 | 16 | // Assert 17 | expect(result).toBe(expected) 18 | }) 19 | 20 | it('should return a `setShowDialog` as a `function`', () => { 21 | // Arrange 22 | const hook = () => useBreaktimeConfirmation() 23 | 24 | // Act 25 | const { result: hookResult } = renderHook(hook) 26 | const result = typeof hookResult.current.setShowDialog 27 | const expected = 'function' 28 | 29 | // Assert 30 | expect(result).toBe(expected) 31 | }) 32 | 33 | it('should change `showDialog` when `setShowDialog` called', () => { 34 | // Arrange 35 | const hook = () => useBreaktimeConfirmation() 36 | 37 | // Act 38 | const { result: hookResult } = renderHook(hook) 39 | act(() => { 40 | hookResult.current.setShowDialog(true) 41 | }) 42 | const result = hookResult.current.showDialog 43 | const expected = true 44 | 45 | // Assert 46 | expect(result).toBe(expected) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /pages/api/local/focus-sessions/resume.js: -------------------------------------------------------------------------------- 1 | import buildLocalApiUrl from '../../../../utils/buildLocalApiUrl' 2 | import fetchJsonServer from '../../../../utils/fetchJsonServer' 3 | 4 | async function getActiveFocusSession({ options }) { 5 | const fetchOptions = { 6 | ...options, 7 | method: 'get', 8 | body: undefined, 9 | } 10 | 11 | return fetchJsonServer({ 12 | resource: 'focus-sessions', 13 | url: 'focus-sessions?status=active', 14 | options: fetchOptions, 15 | singular: true, 16 | }) 17 | } 18 | 19 | export async function resumeActiveFocusSession({ 20 | activeFocusSession, 21 | options, 22 | res, 23 | }) { 24 | const currentPauses = activeFocusSession.pauses ?? [] 25 | 26 | const activePause = currentPauses.find((pause) => pause.endTime === null) 27 | const resumedPauses = currentPauses.filter((pause) => pause.endTime !== null) 28 | 29 | if (!activePause) { 30 | return res.status(200).json(activeFocusSession) 31 | } 32 | 33 | const pauseToResume = { 34 | ...activePause, 35 | endTime: Date.now(), 36 | } 37 | 38 | const fetchOptions = { 39 | ...options, 40 | body: { pauses: [...resumedPauses, pauseToResume] }, 41 | } 42 | 43 | return fetchJsonServer({ 44 | resource: 'focus-sessions', 45 | url: `focus-sessions/${activeFocusSession.id}`, 46 | options: fetchOptions, 47 | res, 48 | }) 49 | } 50 | 51 | export default async function handler(req, res) { 52 | const { options } = buildLocalApiUrl(req) 53 | 54 | if (req.method === 'PATCH') { 55 | const activeFocusSession = await getActiveFocusSession({ options }) 56 | await resumeActiveFocusSession({ activeFocusSession, options, res }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /features/common/components/MainLayout/MainLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | import PropTypes from 'prop-types' 4 | 5 | const MainLayout = ({ menu, content, isPlayground }) => { 6 | return ( 7 | <> 8 |
11 |
{menu}
12 |
{content}
13 |
14 | 53 | 54 | ) 55 | } 56 | 57 | MainLayout.propTypes = { 58 | menu: PropTypes.node, 59 | content: PropTypes.node, 60 | isPlayground: PropTypes.bool, 61 | } 62 | 63 | export default MainLayout 64 | -------------------------------------------------------------------------------- /styles/globals.scss: -------------------------------------------------------------------------------- 1 | @mixin dark-color-scheme { 2 | --color-primary: var(--color-amber-600); 3 | --color-primary-muted: var(--color-amber-800); 4 | --color-primary-highlight: var(--color-orange-800); 5 | --color-primary-inverted: var(--color-gray-50); 6 | 7 | --color-secondary: var(--color-rose-600); 8 | --color-secondary-muted: var(--color-rose-400); 9 | --color-secondary-inverted: var(--color-rose-900); 10 | 11 | --color-tertiary: var(--color-lime-50); 12 | 13 | --color-font-base: var(--color-gray-50); 14 | --color-font-muted: var(--color-gray-500); 15 | --color-font-highlight: var(--color-gray-400); 16 | --color-font-inverted: var(--color-cool-gray-800); 17 | 18 | --background-color-primary: var(--color-cool-gray-900); 19 | --background-color-primary-highlight: var(--color-cool-gray-800); 20 | 21 | --button-border-radius-sm: var(--border-radius-xs); 22 | --button-border-radius-md: var(--border-radius-xs); 23 | --button-border-radius-lg: var(--border-radius-xs); 24 | 25 | --input-height: 55px; 26 | --input-background: var(--color-cool-gray-800); 27 | --input-border-radius: 2; 28 | 29 | --picture-border: 1px solid var(--color-cool-gray-200); 30 | 31 | --card-border-radius: var(--border-radius-xs); 32 | 33 | --task-border-radius: var(--border-radius-xs); 34 | } 35 | 36 | @media (prefers-color-scheme: dark) { 37 | :root:not([data-color-scheme='light']) { 38 | @include dark-color-scheme; 39 | } 40 | } 41 | 42 | :root[data-color-scheme='dark'] { 43 | @include dark-color-scheme; 44 | } 45 | 46 | html, 47 | body { 48 | height: 100%; 49 | background: var(--background-color-primary); 50 | font-family: var(--font-family-sans); 51 | font-size: var(--font-size-base); 52 | } 53 | 54 | #__next { 55 | height: 100%; 56 | } 57 | -------------------------------------------------------------------------------- /features/focusSession/components/BreaktimeTimer.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | 3 | import { 4 | Modal, 5 | CenteredContent, 6 | Picture, 7 | Heading, 8 | Spacer, 9 | Paragraph, 10 | Link, 11 | } from '@glrodasz/components' 12 | 13 | import useTime from '../../common/hooks/useTime' 14 | import formatMilliseconds from '../../../utils/formatMilliseconds' 15 | 16 | const createHandleClose = 17 | ({ onClose }) => 18 | () => { 19 | onClose() 20 | } 21 | 22 | const BreaktimeTimer = ({ onClose, breaktime }) => { 23 | const { currentTime } = useTime({ 24 | isTimer: true, 25 | startTime: breaktime, 26 | callback: onClose, 27 | }) 28 | 29 | return ( 30 | 31 | 32 | 33 | {formatMilliseconds(currentTime)} 34 | 35 | 36 | 37 | 38 | 39 | DESCONÉCTATE 40 | 41 | 42 | 43 | Trabajar cuando te levantas no te permite despertar completamente. Por 44 | eso, date un tiempo antes de empezar tu día. 45 | 46 | 47 | 48 | Leer más 49 | 50 | 51 | 52 | ) 53 | } 54 | 55 | BreaktimeTimer.propTypes = { 56 | onClose: PropTypes.func.isRequired, 57 | breaktime: PropTypes.number.isRequired, 58 | } 59 | 60 | export default BreaktimeTimer 61 | -------------------------------------------------------------------------------- /features/tasks/components/Board/Board.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Spacer } from '@glrodasz/components' 3 | import PropTypes from 'prop-types' 4 | import { DragDropContext } from 'react-beautiful-dnd' 5 | 6 | import Column from '../Column/Column' 7 | 8 | import { normalizeData, filterColumns } from '../../helpers' 9 | 10 | const Board = ({ tasks, isActive, onDragEnd, actions }) => { 11 | const [data, setData] = useState(normalizeData(tasks)) 12 | 13 | useEffect(() => { 14 | setData(normalizeData(tasks)) 15 | }, [tasks]) 16 | 17 | const tasksLength = tasks.length 18 | 19 | return ( 20 | <> 21 | 22 | {!!tasksLength && 23 | data.columnOrder 24 | .filter(filterColumns({ tasksLength, isActive })) 25 | .map((columnId) => { 26 | const column = data.columns[columnId] 27 | const tasks = column.taskIds.map((taskId) => data.tasks[taskId]) 28 | 29 | return ( 30 | <> 31 | 32 | 39 | 40 | ) 41 | })} 42 | 43 | 44 | ) 45 | } 46 | 47 | Board.propTypes = { 48 | tasks: PropTypes.object, 49 | isActive: PropTypes.bool, 50 | onDragEnd: PropTypes.bool, 51 | actions: PropTypes.shape({ 52 | onDeleteTask: PropTypes.func, 53 | onCompleteTask: PropTypes.func, 54 | onEditTask: PropTypes.func, 55 | }), 56 | } 57 | 58 | export default Board 59 | -------------------------------------------------------------------------------- /features/tasks/hooks/useTasks.js: -------------------------------------------------------------------------------- 1 | import { tasksApi } from '../../planning/api' 2 | import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' 3 | import useLocalData from '../../common/hooks/useLocalData' 4 | 5 | const QUERY_KEY = 'tasks' 6 | 7 | const useTasks = ({ initialData, onRemove }) => { 8 | const queryClient = useQueryClient() 9 | 10 | const { 11 | isLoading, 12 | error, 13 | data: fetchedData, 14 | } = useQuery([QUERY_KEY], () => tasksApi.getAll(), { 15 | initialData, 16 | }) 17 | 18 | const { mutateAsync: create } = useMutation( 19 | (params) => tasksApi.create(params), 20 | { 21 | onSuccess: () => { 22 | queryClient.invalidateQueries(QUERY_KEY) 23 | }, 24 | } 25 | ) 26 | 27 | const { mutateAsync: remove } = useMutation( 28 | (params) => tasksApi.delete(params), 29 | { 30 | onSuccess: () => { 31 | queryClient.invalidateQueries(QUERY_KEY) 32 | onRemove?.() 33 | }, 34 | } 35 | ) 36 | 37 | const { mutateAsync: updateStatus } = useMutation( 38 | (params) => tasksApi.updateStatus(params), 39 | { 40 | onSuccess: () => { 41 | queryClient.invalidateQueries(QUERY_KEY) 42 | }, 43 | } 44 | ) 45 | 46 | const { mutateAsync: updatePriorities } = useMutation( 47 | (params) => tasksApi.updatePriorities(params), 48 | { 49 | onSuccess: () => { 50 | queryClient.invalidateQueries(QUERY_KEY) 51 | }, 52 | } 53 | ) 54 | 55 | const { localData, setLocalData } = useLocalData(fetchedData) 56 | 57 | return { 58 | isLoading, 59 | error, 60 | data: localData, 61 | setLocalData, 62 | api: { 63 | create, 64 | remove, 65 | updatePriorities, 66 | updateStatus, 67 | }, 68 | } 69 | } 70 | 71 | export default useTasks 72 | -------------------------------------------------------------------------------- /features/retrospective/containers/Retrospective.js: -------------------------------------------------------------------------------- 1 | import { 2 | Spacer, 3 | Heading, 4 | Accordion, 5 | FullHeightContent, 6 | Paragraph, 7 | Score, 8 | Textarea, 9 | } from '@glrodasz/components' 10 | 11 | import RetrospectiveFooter from '../components/RetrospectiveFooter' 12 | 13 | const Retrospective = () => { 14 | return ( 15 | 18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 |
32 | <> 33 | 34 |
35 | ¿Cómo te sentiste el día de hoy? 36 | 37 | 38 |
39 | 40 | ¿Qué bloqueos tuviste? 41 | 42 | 43 | Durante el día, existen distractores y escribirlos te ayuda a 44 | identificarlos para mantenerte enfocado y saludable. 45 | 46 | 47 |