├── .npmrc ├── .eslintignore ├── img ├── icon-32.png ├── icon-180.png ├── icon-192.png ├── icon-270.png ├── icon-512.png ├── screenshot.png ├── logo-white.svg └── logo.svg ├── screenshot.png ├── fonts ├── roboto.woff └── roboto-bold.woff ├── src ├── components │ ├── tooltip │ │ ├── _style.scss │ │ └── index.js │ ├── placeholder │ │ ├── _style.scss │ │ └── index.js │ ├── autosize-textarea │ │ ├── _style.scss │ │ └── index.js │ ├── tag-pagination │ │ ├── _style.scss │ │ └── index.js │ ├── date-navigation │ │ ├── _style.scss │ │ └── index.js │ ├── button-group │ │ ├── index.js │ │ └── _style.scss │ ├── done-text │ │ ├── _style.scss │ │ └── index.js │ ├── user │ │ ├── _style.scss │ │ └── index.js │ ├── icon │ │ └── _style.scss │ ├── tags-list │ │ ├── _style.scss │ │ └── index.js │ ├── root │ │ └── index.js │ ├── tag-dones │ │ ├── _style.scss │ │ ├── user.js │ │ ├── date.js │ │ └── index.js │ ├── page │ │ ├── _style.scss │ │ └── index.js │ ├── date-picker │ │ ├── _style.scss │ │ └── index.js │ ├── user-dones │ │ ├── _style.scss │ │ └── index.js │ ├── document-head │ │ └── index.js │ ├── sidebar │ │ ├── action.js │ │ ├── index.js │ │ └── _style.scss │ ├── notices │ │ ├── index.js │ │ └── _style.scss │ ├── popover-menu │ │ ├── _style.scss │ │ ├── index.js │ │ └── test │ │ │ └── index.js │ ├── done-status │ │ ├── _style.scss │ │ └── index.js │ ├── card │ │ ├── index.js │ │ └── _style.scss │ ├── mutative-request-prompt │ │ └── index.js │ ├── done-input │ │ ├── _style.scss │ │ ├── test │ │ │ └── index.js │ │ └── textarea.js │ ├── dones-list │ │ ├── _style.scss │ │ └── index.js │ ├── button │ │ ├── _style.scss │ │ ├── index.js │ │ └── test │ │ │ └── index.js │ ├── popover │ │ ├── index.js │ │ └── _style.scss │ └── link │ │ └── index.js ├── state │ ├── selectors │ │ ├── get-tags.js │ │ ├── get-route-path.js │ │ ├── get-title.js │ │ ├── get-done.js │ │ ├── get-request-nonce.js │ │ ├── get-user.js │ │ ├── has-received-tags.js │ │ ├── get-route-param.js │ │ ├── is-initial-route.js │ │ ├── get-formatted-title.js │ │ ├── is-capturing-request-preload.js │ │ ├── is-preloading-path.js │ │ ├── get-matched-route.js │ │ ├── get-users.js │ │ ├── get-notices.js │ │ ├── test │ │ │ ├── get-notices.js │ │ │ ├── has-received-tags.js │ │ │ ├── is-initial-route.js │ │ │ ├── has-received-dones.js │ │ │ ├── get-preloaded-response.js │ │ │ ├── is-mutative-request-in-flight.js │ │ │ ├── get-path-request.js │ │ │ ├── get-dones-for-user.js │ │ │ ├── get-sorted-users-by-date.js │ │ │ └── get-dones.js │ │ ├── is-mutative-request-in-flight.js │ │ ├── get-path-request.js │ │ ├── get-preloaded-response.js │ │ ├── has-received-dones.js │ │ ├── get-dones-for-user.js │ │ ├── get-dones-total-pages.js │ │ ├── get-sorted-dones.js │ │ ├── get-sorted-users-by-date.js │ │ ├── index.js │ │ └── get-dones.js │ ├── document-head │ │ ├── actions.js │ │ └── reducer.js │ ├── tags │ │ ├── actions.js │ │ ├── effects.js │ │ ├── reducer.js │ │ └── test │ │ │ └── reducer.js │ ├── users │ │ ├── actions.js │ │ ├── effects.js │ │ └── reducer.js │ ├── middlewares.js │ ├── effects.js │ ├── notices │ │ ├── effects.js │ │ ├── reducer.js │ │ ├── actions.js │ │ └── test │ │ │ └── reducer.js │ ├── test │ │ └── index.js │ ├── index.js │ ├── reducer.js │ ├── routing │ │ ├── actions.js │ │ ├── reducer.js │ │ ├── effects.js │ │ ├── middleware.js │ │ └── test │ │ │ └── reducer.js │ ├── requests │ │ ├── effects.js │ │ ├── actions.js │ │ ├── middleware.js │ │ ├── test │ │ │ └── middleware.js │ │ └── reducer.js │ ├── dones │ │ ├── actions.js │ │ ├── effects.js │ │ └── reducer.js │ └── action-types.js ├── assets │ └── stylesheets │ │ ├── _animation.scss │ │ ├── _placeholders.scss │ │ ├── _typography.scss │ │ ├── _vendor.scss │ │ ├── _variables.scss │ │ ├── _mixins.scss │ │ ├── main.scss │ │ └── _elements.scss ├── routes │ ├── not-found.js │ ├── tags.js │ ├── home.js │ ├── tag.js │ ├── date.js │ └── index.js ├── lib │ ├── callbackify │ │ ├── index.js │ │ └── test │ │ │ └── index.js │ ├── i18n │ │ └── index.js │ └── selection │ │ └── index.js ├── constant.js ├── test │ └── utils │ │ └── index.js └── index.js ├── .prettierrc.js ├── CREDITS.md ├── cypress.json ├── e2e ├── tests │ ├── .eslintrc.json │ └── create-done.js └── www │ └── index.html ├── .gitignore ├── postcss.config.js ├── composer.json ├── .editorconfig ├── jest.setup.js ├── style.css ├── bin ├── build-theme.sh └── download-languages.js ├── .travis.yml ├── .eslintrc.json ├── phpcs.xml ├── docker-compose.yml ├── manifest.json ├── inc ├── admin │ ├── about.css │ └── about.php ├── endpoints │ ├── class-wp-rest-dones-tags-controller.php │ └── class-wp-rest-dones-users-controller.php ├── tags.php └── updater.php ├── index.php ├── .babelrc ├── webpack.config.js ├── package.json └── CHANGELOG.md /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | vendor 4 | cypress 5 | -------------------------------------------------------------------------------- /img/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aduth/dones/HEAD/img/icon-32.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aduth/dones/HEAD/screenshot.png -------------------------------------------------------------------------------- /fonts/roboto.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aduth/dones/HEAD/fonts/roboto.woff -------------------------------------------------------------------------------- /img/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aduth/dones/HEAD/img/icon-180.png -------------------------------------------------------------------------------- /img/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aduth/dones/HEAD/img/icon-192.png -------------------------------------------------------------------------------- /img/icon-270.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aduth/dones/HEAD/img/icon-270.png -------------------------------------------------------------------------------- /img/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aduth/dones/HEAD/img/icon-512.png -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aduth/dones/HEAD/img/screenshot.png -------------------------------------------------------------------------------- /src/components/tooltip/_style.scss: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | pointer-events: none; 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require( '@wordpress/scripts/config/.prettierrc.js' ); 2 | -------------------------------------------------------------------------------- /fonts/roboto-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aduth/dones/HEAD/fonts/roboto-bold.woff -------------------------------------------------------------------------------- /src/components/placeholder/_style.scss: -------------------------------------------------------------------------------- 1 | .placeholder { 2 | @include loading-pulse-text; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/autosize-textarea/_style.scss: -------------------------------------------------------------------------------- 1 | .autosize-textarea { 2 | overflow-y: hidden; 3 | } 4 | -------------------------------------------------------------------------------- /src/state/selectors/get-tags.js: -------------------------------------------------------------------------------- 1 | export default function getTags( state ) { 2 | return state.tags; 3 | } 4 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | "Complete Tasks" icon by bezier master, with modifications, from the Noun Project (CC BY 3.0 US) 2 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrationFolder": "e2e/tests", 3 | "fileServerFolder": "e2e/www", 4 | "video": false 5 | } 6 | -------------------------------------------------------------------------------- /e2e/tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "cy": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/tag-pagination/_style.scss: -------------------------------------------------------------------------------- 1 | .tag-pagination { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | -------------------------------------------------------------------------------- /src/state/selectors/get-route-path.js: -------------------------------------------------------------------------------- 1 | export default function getRoutePath( state ) { 2 | return state.routing.path; 3 | } 4 | -------------------------------------------------------------------------------- /src/state/selectors/get-title.js: -------------------------------------------------------------------------------- 1 | export default function getTitle( state ) { 2 | return state.documentHead.title; 3 | } 4 | -------------------------------------------------------------------------------- /src/state/selectors/get-done.js: -------------------------------------------------------------------------------- 1 | export default function getDone( state, id ) { 2 | return state.dones.items[ id ] || null; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/date-navigation/_style.scss: -------------------------------------------------------------------------------- 1 | .date-navigation .card__controls .button { 2 | color: darken( $border-color, 40% ); 3 | } 4 | -------------------------------------------------------------------------------- /src/state/selectors/get-request-nonce.js: -------------------------------------------------------------------------------- 1 | export default function getRequestNonce( state ) { 2 | return state.requests.nonce; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/stylesheets/_animation.scss: -------------------------------------------------------------------------------- 1 | @keyframes loading-pulse { 2 | 0% { opacity: .5; } 3 | 50% { opacity: 1; } 4 | 100% { opacity: .5; } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/button-group/index.js: -------------------------------------------------------------------------------- 1 | export default function ButtonGroup( { children } ) { 2 | return
{ children }
; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | vendor 4 | 5 | # Generated 6 | dist/ 7 | dones.zip 8 | cypress 9 | languages/*.mo 10 | 11 | # Logs 12 | *.log 13 | 14 | # System 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /src/assets/stylesheets/_placeholders.scss: -------------------------------------------------------------------------------- 1 | %screen-reader-text { 2 | position: absolute !important; 3 | overflow: hidden; 4 | clip: rect( 1px, 1px, 1px, 1px ); 5 | height: 1px; 6 | width: 1px; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/done-text/_style.scss: -------------------------------------------------------------------------------- 1 | .done-text { 2 | outline: none; 3 | overflow-y: visible; 4 | } 5 | 6 | .done-text__overflow { 7 | overflow-x: hidden; 8 | text-overflow: ellipsis; 9 | } 10 | -------------------------------------------------------------------------------- /src/state/selectors/get-user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { get } from 'lodash'; 5 | 6 | export default function getUser( state, userId ) { 7 | return get( state.users, userId, null ); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/placeholder/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | 5 | export default function Placeholder( { height } ) { 6 | return
; 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require( 'autoprefixer' ), 4 | require( 'postcss-critical-css' )( { 5 | outputPath: 'dist', 6 | outputDest: 'critical.css.php', 7 | } ), 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/user/_style.scss: -------------------------------------------------------------------------------- 1 | .user { 2 | line-height: 30px; 3 | display: flex; 4 | } 5 | 6 | .user__avatar-image { 7 | display: inline-block; 8 | margin-right: 0.8rem; 9 | width: 30px; 10 | height: 30px; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/icon/_style.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | fill: currentColor; 3 | 4 | &.is-icon-chevron-left, 5 | &.is-icon-chevron-right { 6 | position: relative; 7 | top: 1px; 8 | } 9 | } 10 | 11 | .icon__screen-reader-text { 12 | @extend %screen-reader-text; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/tags-list/_style.scss: -------------------------------------------------------------------------------- 1 | .tags-list__list { 2 | list-style-type: none; 3 | margin: 0; 4 | padding-left: 0; 5 | } 6 | 7 | .tags-list__list-item { 8 | display: inline-block; 9 | padding: 0.2rem; 10 | 11 | .button { 12 | text-transform: none; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/routes/not-found.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import Page from 'components/page'; 5 | import { translate } from 'lib/i18n'; 6 | 7 | export default function NotFoundRoute() { 8 | return Not Found; 9 | } 10 | -------------------------------------------------------------------------------- /src/state/document-head/actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { DOCUMENT_HEAD_TITLE_SET } from 'state/action-types'; 5 | 6 | export function setDocumentHeadTitle( title ) { 7 | return { 8 | type: DOCUMENT_HEAD_TITLE_SET, 9 | title, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require-dev": { 3 | "dealerdirect/phpcodesniffer-composer-installer": "^0.4", 4 | "wimg/php-compatibility": "dev-master", 5 | "wp-coding-standards/wpcs": "dev-develop" 6 | }, 7 | "scripts": { 8 | "lint": "phpcs", 9 | "test": "composer lint" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/stylesheets/_typography.scss: -------------------------------------------------------------------------------- 1 | %sans { 2 | font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif; 3 | } 4 | 5 | %light { 6 | font-weight: 300; 7 | } 8 | 9 | %bold { 10 | font-weight: 700; 11 | } 12 | 13 | %subtitle { 14 | color: lighten( $font-color-body, 20% ); 15 | } 16 | -------------------------------------------------------------------------------- /src/state/selectors/has-received-tags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if tags have been received, or false otherwise. 3 | * 4 | * @param {Object} state Global state object 5 | * @return {boolean} Whether tags received 6 | */ 7 | export default function hasReceivedTags( state ) { 8 | return null !== state.tags; 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/stylesheets/_vendor.scss: -------------------------------------------------------------------------------- 1 | @import '~flatpickr/dist/flatpickr'; 2 | 3 | .flatpickr-calendar { 4 | font-size: 75%; 5 | margin-left: 3px; 6 | 7 | &.arrowTop { 8 | margin-top: 3px; 9 | } 10 | 11 | &.arrowBottom { 12 | margin-bottom: 3px; 13 | } 14 | 15 | &.rightMost { 16 | margin-right: 3px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/state/selectors/get-route-param.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { get } from 'lodash'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { getMatchedRoute } from './'; 10 | 11 | export default function getRouteParam( state, param ) { 12 | return get( getMatchedRoute( state ), [ 'params', param ], null ); 13 | } 14 | -------------------------------------------------------------------------------- /src/state/selectors/is-initial-route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if navigation has not occurred within the app, or false 3 | * otherwise. 4 | * 5 | * @param {Object} state Global state object 6 | * @return {boolean} Whether initial route 7 | */ 8 | export default function isInitialRoute( state ) { 9 | return state.routing.isInitial; 10 | } 11 | -------------------------------------------------------------------------------- /src/state/tags/actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { TAGS_REQUEST, TAGS_RECEIVE } from 'state/action-types'; 5 | 6 | export function receiveTags( tags ) { 7 | return { 8 | type: TAGS_RECEIVE, 9 | tags, 10 | }; 11 | } 12 | 13 | export function requestTags() { 14 | return { 15 | type: TAGS_REQUEST, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/state/users/actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { USERS_REQUEST, USERS_RECEIVE } from 'state/action-types'; 5 | 6 | export function requestUsers() { 7 | return { 8 | type: USERS_REQUEST, 9 | }; 10 | } 11 | 12 | export function receiveUsers( users ) { 13 | return { 14 | type: USERS_RECEIVE, 15 | users, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/root/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useSelector } from 'prsh'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { getMatchedRoute } from 'state/selectors'; 10 | 11 | function Root() { 12 | const { Route } = useSelector( getMatchedRoute ); 13 | 14 | return ; 15 | } 16 | 17 | export default Root; 18 | -------------------------------------------------------------------------------- /src/state/selectors/get-formatted-title.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { SITE_NAME } from 'constant'; 5 | import { getTitle } from './'; 6 | 7 | export default function getFormattedTitle( state ) { 8 | const title = getTitle( state ); 9 | if ( title ) { 10 | return `${ SITE_NAME } | ${ title }`; 11 | } 12 | 13 | return SITE_NAME; 14 | } 15 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // Force synchronous Preact rerenders during test, allowing assertions to be 2 | // made immediately following a rerender. 3 | require( 'preact' ).options.debounceRendering = ( render ) => render(); 4 | 5 | // Constant initialization 6 | global.dones = { 7 | siteUrl: window.location.origin, 8 | userId: 1, 9 | i18n: {}, 10 | }; 11 | 12 | window.scrollTo = () => {}; 13 | -------------------------------------------------------------------------------- /src/state/tags/effects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { REQUEST, TAGS_REQUEST } from 'state/action-types'; 5 | import { receiveTags } from 'state/tags/actions'; 6 | 7 | export default { 8 | [ TAGS_REQUEST ]() { 9 | return { 10 | type: REQUEST, 11 | path: '/dones/v1/tags', 12 | success: ( { body } ) => receiveTags( body ), 13 | }; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/state/middlewares.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import refx from 'refx'; 5 | import multi from 'redux-multi'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import routing from './routing/middleware'; 11 | import requests from './requests/middleware'; 12 | import effects from './effects'; 13 | 14 | export default [ multi, refx( effects ), routing, requests ]; 15 | -------------------------------------------------------------------------------- /src/state/users/effects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { REQUEST, USERS_REQUEST } from 'state/action-types'; 5 | import { receiveUsers } from 'state/users/actions'; 6 | 7 | export default { 8 | [ USERS_REQUEST ]() { 9 | return { 10 | type: REQUEST, 11 | path: '/dones/v1/users', 12 | success: ( { body } ) => receiveUsers( body ), 13 | }; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/state/effects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import dones from './dones/effects'; 5 | import notices from './notices/effects'; 6 | import requests from './requests/effects'; 7 | import routing from './routing/effects'; 8 | import tags from './tags/effects'; 9 | import users from './users/effects'; 10 | 11 | export default [ dones, notices, requests, routing, tags, users ]; 12 | -------------------------------------------------------------------------------- /src/lib/callbackify/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert specified promise resolution to Node-style callback. 3 | * 4 | * @param {Promise} promise Promise object 5 | * @param {Function} callback Node-style callback function 6 | */ 7 | export default function callbackify( promise, callback ) { 8 | promise.then( 9 | ( result ) => callback( null, result ), 10 | ( error ) => callback( error ) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/state/selectors/is-capturing-request-preload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if requests are currently being captured for preload, or false 3 | * otherwise. 4 | * 5 | * @param {Object} state Global state object 6 | * @return {boolean} Whether requests are captured for preload 7 | */ 8 | export default function isCapturingRequestPreload( state ) { 9 | return state.requests.isCapturingPreload; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/tag-dones/_style.scss: -------------------------------------------------------------------------------- 1 | .tag-dones, 2 | .tag-dones__date-list, 3 | .tag-dones__user-list { 4 | list-style-type: none; 5 | padding-left: 0; 6 | } 7 | 8 | .tag-dones__date-list-item:not( :last-child ) { 9 | margin-bottom: 1rem; 10 | } 11 | 12 | .tag-dones__user-label { 13 | @extend %bold; 14 | margin-top: 0; 15 | font-size: 1rem; 16 | } 17 | 18 | .tag-dones__user-list-item { 19 | margin: 0.2rem 0; 20 | } 21 | -------------------------------------------------------------------------------- /src/state/notices/effects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { NOTICE_DISPLAY } from 'state/action-types'; 5 | import { hideNotice } from './actions'; 6 | 7 | export default { 8 | [ NOTICE_DISPLAY ]( action, store ) { 9 | const { id } = action; 10 | const { dispatch } = store; 11 | 12 | // Automatically hide notice after timeout 13 | setTimeout( () => dispatch( hideNotice( id ) ), 5000 ); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/state/selectors/is-preloading-path.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if a request is in progress to preload the specified path, or 3 | * false otherwise. 4 | * 5 | * @param {Object} state Global state object 6 | * @param {string} path Request path 7 | * @return {boolean} Whether request is in progress to preload 8 | */ 9 | export default function isPreloadingPath( state, path ) { 10 | return !! state.requests.preloading[ path ]; 11 | } 12 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Theme Name: Dones 3 | * Theme URI: https://github.com/aduth/dones 4 | * Author: Andrew Duthie 5 | * Author URI: http://andrewduthie.com 6 | * Description: Simple team task management and tracking 7 | * Version: 1.1.3 8 | * License: GNU General Public License v2 or later 9 | * License URI: http://www.gnu.org/licenses/gpl-2.0.html 10 | * Tags: tasks, todo, done, teams, project-management 11 | * Text Domain: dones 12 | */ 13 | -------------------------------------------------------------------------------- /img/logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/tags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import Page from 'components/page'; 5 | import TagsList from 'components/tags-list'; 6 | import { requestTags } from 'state/tags/actions'; 7 | import { translate } from 'lib/i18n'; 8 | 9 | export default function TagsRoute() { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | TagsRoute.prepareRoute = () => [ requestTags() ]; 18 | -------------------------------------------------------------------------------- /src/state/selectors/get-matched-route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { getRouteByPath } from 'routes'; 5 | import { getRoutePath } from './'; 6 | 7 | /** 8 | * Returns the route configuration for the current path. 9 | * 10 | * @param {Object} state Global state object 11 | * @return {Object} Route configuration 12 | */ 13 | export default function getMatchedRoute( state ) { 14 | return getRouteByPath( getRoutePath( state ) ); 15 | } 16 | -------------------------------------------------------------------------------- /src/state/test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | 5 | /** 6 | * Internal dependencies 7 | */ 8 | import configureReduxStore from '../'; 9 | 10 | describe( 'configureReduxStore()', () => { 11 | it( 'returns same reference on unhandled action', () => { 12 | const store = configureReduxStore(); 13 | const state = store.getState(); 14 | 15 | store.dispatch( { type: '__UNHANDLED_ACTION__' } ); 16 | 17 | expect( store.getState() ).toBe( state ); 18 | } ); 19 | } ); 20 | -------------------------------------------------------------------------------- /src/state/document-head/reducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { combineReducers } from 'redux'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { DOCUMENT_HEAD_TITLE_SET } from 'state/action-types'; 10 | 11 | export function title( state = null, action ) { 12 | switch ( action.type ) { 13 | case DOCUMENT_HEAD_TITLE_SET: 14 | return action.title; 15 | } 16 | 17 | return state; 18 | } 19 | 20 | export default combineReducers( { 21 | title, 22 | } ); 23 | -------------------------------------------------------------------------------- /src/state/selectors/get-users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import createSelector from 'rememo'; 5 | import { values } from 'lodash'; 6 | 7 | /** 8 | * Returns an array of all site user objects with editable permissions. 9 | * 10 | * @param {Object} state Global state object 11 | * @return {Object[]} Site users 12 | */ 13 | export function getUsers( state ) { 14 | return values( state.users ); 15 | } 16 | 17 | export default createSelector( getUsers, ( state ) => [ state.users ] ); 18 | -------------------------------------------------------------------------------- /src/state/selectors/get-notices.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import createSelector from 'rememo'; 5 | import { map } from 'lodash'; 6 | 7 | /** 8 | * Returns an array of all active notices. 9 | * 10 | * @param {Object} state Global state object 11 | * @return {Object[]} Notices 12 | */ 13 | export function getNotices( state ) { 14 | return map( state.notices, ( notice, id ) => ( { ...notice, id } ) ); 15 | } 16 | 17 | export default createSelector( getNotices, ( state ) => [ state.notices ] ); 18 | -------------------------------------------------------------------------------- /src/state/selectors/test/get-notices.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | 5 | /** 6 | * Internal dependencies 7 | */ 8 | import { getNotices } from '../get-notices'; 9 | 10 | describe( 'getNotices()', () => { 11 | it( 'returns an array of notices', () => { 12 | const notices = getNotices( { 13 | notices: { 14 | 1: { 15 | status: 'error', 16 | text: 'foo', 17 | }, 18 | }, 19 | } ); 20 | 21 | expect( notices ).toEqual( [ 22 | { id: '1', status: 'error', text: 'foo' }, 23 | ] ); 24 | } ); 25 | } ); 26 | -------------------------------------------------------------------------------- /bin/build-theme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PATH:=./node_modules/.bin:$(PATH) 4 | 5 | set -e 6 | cd "$(dirname "$0")" 7 | cd .. 8 | npm run clean 9 | npm install 10 | npm-run-all --parallel build download-languages 11 | rm -f dones.zip 12 | zip -r dones.zip \ 13 | dist \ 14 | img \ 15 | inc \ 16 | languages \ 17 | CHANGELOG.md \ 18 | CREDITS.md \ 19 | functions.php \ 20 | index.php \ 21 | LICENSE.md \ 22 | manifest.json \ 23 | README.md \ 24 | screenshot.png \ 25 | style.css \ 26 | -x "*.DS_Store" \ 27 | -x "languages/dones.pot" \ 28 | -x "languages/*.po" 29 | -------------------------------------------------------------------------------- /src/components/button-group/_style.scss: -------------------------------------------------------------------------------- 1 | .button-group .button { 2 | display: inline-block; 3 | vertical-align: top; 4 | line-height: 1; 5 | 6 | &:not( :first-child ) { 7 | border-top-left-radius: 0; 8 | border-bottom-left-radius: 0; 9 | } 10 | 11 | &:not( :last-child ) { 12 | margin-right: -1px; 13 | border-top-right-radius: 0; 14 | border-bottom-right-radius: 0; 15 | } 16 | 17 | &:hover { 18 | position: relative; 19 | z-index: 10; 20 | } 21 | 22 | .icon { 23 | display: block; 24 | margin-left: auto; 25 | margin-right: auto; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/page/_style.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | display: block; 3 | 4 | @include medium() { 5 | padding-top: $sidebar-width-medium; 6 | } 7 | 8 | @include desktop() { 9 | padding-top: 0; 10 | padding-left: $sidebar-width-medium; 11 | } 12 | } 13 | 14 | .page__content { 15 | width: $content-width; 16 | max-width: 100%; 17 | margin: 0.6rem auto; 18 | padding: 0 0.6rem; 19 | 20 | @include medium() { 21 | margin: 1.6rem auto; 22 | padding: 0 1.6rem; 23 | } 24 | 25 | @include desktop() { 26 | margin: 2.6rem auto; 27 | padding: 0 2.6rem; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/state/selectors/is-mutative-request-in-flight.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { some } from 'lodash'; 5 | 6 | /** 7 | * Returns true if a mutative (non-GET) request is currently in flight, or 8 | * false otherwise. 9 | * 10 | * @param {Object} state Global state object 11 | * @return {boolean} Whether mutative request is in flight 12 | */ 13 | export default function isMutativeRequestInFlight( state ) { 14 | return some( state.requests.items, ( methods ) => 15 | some( methods, ( request, method ) => 'GET' !== method ) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/date-picker/_style.scss: -------------------------------------------------------------------------------- 1 | .date-picker { 2 | position: relative; 3 | 4 | .icon { 5 | display: block; 6 | } 7 | } 8 | 9 | .date-picker__input { 10 | position: absolute; 11 | top: 0; 12 | right: 0; 13 | left: 0; 14 | bottom: 0; 15 | width: 100%; 16 | opacity: 0; 17 | 18 | &[type="date"] { 19 | -webkit-appearance: none; 20 | 21 | &::-webkit-calendar-picker-indicator { 22 | position: absolute; 23 | top: 0; 24 | right: 0; 25 | bottom: 0; 26 | left: 0; 27 | width: 100%; 28 | height: 100%; 29 | padding: 0; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - language: php 4 | php: 5.6 5 | before_script: "composer install" 6 | script: "composer test" 7 | cache: 8 | directories: 9 | - $HOME/.composer/cache/files 10 | 11 | - language: php 12 | php: 7.1 13 | before_script: "composer install" 14 | script: "composer test" 15 | cache: 16 | directories: 17 | - $HOME/.composer/cache/files 18 | 19 | - language: node_js 20 | node_js: "node" 21 | cache: 22 | directories: 23 | - node_modules 24 | -------------------------------------------------------------------------------- /src/components/user-dones/_style.scss: -------------------------------------------------------------------------------- 1 | .user-dones__input { 2 | margin-top: 1rem; 3 | position: fixed; 4 | z-index: 10; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | border-radius: 0; 9 | border-width: 0; 10 | border-top-width: 1px; 11 | backface-visibility: hidden; 12 | background-color: #fff; 13 | padding: 0.2rem; 14 | 15 | @include medium() { 16 | position: static; 17 | z-index: auto; 18 | padding: 0; 19 | border-radius: $border-radius; 20 | border-width: 1px; 21 | background-color: transparent; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/document-head/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useEffect } from 'preact/hooks'; 5 | import { useSelector } from 'prsh'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { getFormattedTitle } from 'state/selectors'; 11 | 12 | function DocumentHead() { 13 | const title = useSelector( getFormattedTitle ); 14 | 15 | useEffect( () => { 16 | if ( typeof document !== 'undefined' && document.title !== title ) { 17 | document.title = title; 18 | } 19 | }, [ title ] ); 20 | 21 | return null; 22 | } 23 | 24 | export default DocumentHead; 25 | -------------------------------------------------------------------------------- /src/state/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { createStore, applyMiddleware } from 'redux'; 5 | import { flowRight } from 'lodash'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import reducer from './reducer'; 11 | import middlewares from './middlewares'; 12 | 13 | export default function configureReduxStore() { 14 | const enhancers = [ applyMiddleware( ...middlewares ) ]; 15 | 16 | if ( window.__REDUX_DEVTOOLS_EXTENSION__ ) { 17 | enhancers.push( window.__REDUX_DEVTOOLS_EXTENSION__() ); 18 | } 19 | 20 | return createStore( reducer, flowRight( enhancers ) ); 21 | } 22 | -------------------------------------------------------------------------------- /src/state/selectors/get-path-request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { get } from 'lodash'; 5 | 6 | /** 7 | * Returns the request object for the specified path, or null if there is no 8 | * request in progress. 9 | * 10 | * @param {Object} state Global state object 11 | * @param {string} path Request path 12 | * @param {?string} method Optional method for path to retrieve 13 | * @return {Promise} Request object 14 | */ 15 | export default function getPathRequest( state, path, method = 'GET' ) { 16 | return get( state.requests.items, [ path, method ], null ); 17 | } 18 | -------------------------------------------------------------------------------- /src/state/selectors/get-preloaded-response.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the preloaded response payload for a given path, or null if response 3 | * is not preloaded. A response payload includes both body and headers. 4 | * 5 | * @param {Object} state Global state object 6 | * @param {string} path Path to check 7 | * @return {?Object} Preloaded result 8 | */ 9 | export default function getPreloadedResponse( state, path ) { 10 | const preloaded = state.requests.preload[ path ]; 11 | if ( ! preloaded ) { 12 | return null; 13 | } 14 | 15 | const [ response ] = preloaded; 16 | return response; 17 | } 18 | -------------------------------------------------------------------------------- /src/state/reducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { combineReducers } from 'redux'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import documentHead from './document-head/reducer'; 10 | import dones from './dones/reducer'; 11 | import notices from './notices/reducer'; 12 | import requests from './requests/reducer'; 13 | import routing from './routing/reducer'; 14 | import tags from './tags/reducer'; 15 | import users from './users/reducer'; 16 | 17 | export default combineReducers( { 18 | documentHead, 19 | dones, 20 | notices, 21 | requests, 22 | routing, 23 | tags, 24 | users, 25 | } ); 26 | -------------------------------------------------------------------------------- /src/routes/home.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useEffect } from 'preact/hooks'; 5 | import { useStore } from 'prsh'; 6 | import { date as phpdate } from 'phpdate'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { replaceRoute } from 'state/routing/actions'; 12 | import { toSiteTime } from 'lib/i18n'; 13 | 14 | function HomeRoute() { 15 | const { dispatch } = useStore(); 16 | 17 | useEffect( () => { 18 | const date = phpdate( 'Y-m-d', toSiteTime() ); 19 | dispatch( replaceRoute( `/date/${ date }/` ) ); 20 | }, [] ); 21 | 22 | return null; 23 | } 24 | 25 | export default HomeRoute; 26 | -------------------------------------------------------------------------------- /src/state/selectors/test/has-received-tags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | 5 | /** 6 | * Internal dependencies 7 | */ 8 | import hasReceivedTags from '../has-received-tags'; 9 | 10 | describe( 'hasReceivedTags()', () => { 11 | it( 'returns false if tags have not been received', () => { 12 | const hasReceived = hasReceivedTags( { 13 | tags: null, 14 | } ); 15 | 16 | expect( hasReceived ).toBe( false ); 17 | } ); 18 | 19 | it( 'returns true if tags have been received', () => { 20 | const hasReceived = hasReceivedTags( { 21 | tags: [], 22 | } ); 23 | 24 | expect( hasReceived ).toBe( true ); 25 | } ); 26 | } ); 27 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "plugin:@wordpress/eslint-plugin/recommended", 4 | "parser": "babel-eslint", 5 | "env": { 6 | "node": true, 7 | "browser": true, 8 | "jest": true 9 | }, 10 | "settings": { 11 | "react": { 12 | "pragma": "createElement", 13 | "version": "16.3" 14 | } 15 | }, 16 | "rules": { 17 | "arrow-parens": "error", 18 | "no-cond-assign": "off", 19 | "no-process-exit": "off", 20 | "no-restricted-globals": [ "error", "event" ], 21 | "react/jsx-key": "off", 22 | "react/jsx-no-bind": "off", 23 | "react/jsx-space-before-closing": "off", 24 | "react/jsx-tag-spacing": "error" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/sidebar/action.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames from 'classcat'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import Link from 'components/link'; 10 | import Icon from 'components/icon'; 11 | 12 | export default function SidebarAction( { icon, to, selected, children } ) { 13 | const classes = classNames( [ 14 | 'sidebar__action', 15 | { 16 | 'is-selected': selected, 17 | }, 18 | ] ); 19 | 20 | return ( 21 | 22 | 23 |
{ children }
24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/user-dones/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { USER_ID } from 'constant'; 5 | import Card from 'components/card'; 6 | import DoneInput from 'components/done-input'; 7 | import DonesList from 'components/dones-list'; 8 | import User from 'components/user'; 9 | 10 | export default function UserDones( { userId, date } ) { 11 | return ( 12 | }> 13 | 14 | { userId === USER_ID && ( 15 | 20 | ) } 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/notices/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { isEmpty, map } from 'lodash'; 5 | import { useSelector } from 'prsh'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { getNotices } from 'state/selectors'; 11 | 12 | function Notices() { 13 | const notices = useSelector( getNotices ); 14 | if ( isEmpty( notices ) ) { 15 | return null; 16 | } 17 | 18 | return ( 19 |
    20 | { map( notices, ( { id, status, text } ) => ( 21 |
  • 22 | { text } 23 |
  • 24 | ) ) } 25 |
26 | ); 27 | } 28 | 29 | export default Notices; 30 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sniffs for WordPress plugins 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ./inc 17 | ./functions.php 18 | 19 | 20 | inc/endpoints/* 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/state/selectors/test/is-initial-route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | 5 | /** 6 | * Internal dependencies 7 | */ 8 | import isInitialRoute from '../is-initial-route'; 9 | 10 | describe( 'isInitialRoute()', () => { 11 | it( 'should return true if initial route', () => { 12 | const isInitial = isInitialRoute( { 13 | routing: { 14 | isInitial: true, 15 | }, 16 | } ); 17 | 18 | expect( isInitial ).toBe( true ); 19 | } ); 20 | 21 | it( 'should return false if not initial route', () => { 22 | const isInitial = isInitialRoute( { 23 | routing: { 24 | isInitial: false, 25 | }, 26 | } ); 27 | 28 | expect( isInitial ).toBe( false ); 29 | } ); 30 | } ); 31 | -------------------------------------------------------------------------------- /src/components/tag-dones/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { map } from 'lodash'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import DoneText from 'components/done-text'; 10 | import User from 'components/user'; 11 | 12 | export default function TagDonesUser( { userId, dones } ) { 13 | return ( 14 |
15 |

16 | 17 |

18 |
    19 | { map( dones, ( done ) => ( 20 |
  • 21 | { done.text } 22 |
  • 23 | ) ) } 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | wordpress: 5 | image: wordpress:5.3 6 | ports: 7 | - 3600:80 8 | environment: 9 | WORDPRESS_DB_HOST: db 10 | WORDPRESS_DB_USER: root 11 | WORDPRESS_DB_PASSWORD: password 12 | WORDPRESS_DB_NAME: wordpress 13 | volumes: 14 | - wordpress:/var/www/html 15 | - .:/var/www/html/wp-content/themes/dones 16 | 17 | db: 18 | image: mariadb:10.4.7 19 | environment: 20 | MYSQL_DATABASE: wordpress 21 | MYSQL_USER: root 22 | MYSQL_PASSWORD: password 23 | MYSQL_ROOT_PASSWORD: password 24 | volumes: 25 | - data:/var/lib/mysql 26 | 27 | volumes: 28 | wordpress: 29 | data: 30 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dones", 3 | "short_name": "Dones", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#F7F7F8", 7 | "description": "Simple team task management and tracking", 8 | "icons": [ { 9 | "src": "img/icon-32.png", 10 | "sizes": "32x32", 11 | "type": "image/png" 12 | }, { 13 | "src": "img/icon-180.png", 14 | "sizes": "180x180", 15 | "type": "image/png" 16 | }, { 17 | "src": "img/icon-192.png", 18 | "sizes": "192x192", 19 | "type": "image/png" 20 | }, { 21 | "src": "img/icon-270.png", 22 | "sizes": "270x270", 23 | "type": "image/png" 24 | }, { 25 | "src": "img/icon-512.png", 26 | "sizes": "512x512", 27 | "type": "image/png" 28 | } ] 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/stylesheets/_variables.scss: -------------------------------------------------------------------------------- 1 | // Layout 2 | $content-width: 720px; 3 | $medium-width: 600px; 4 | $desktop-width: 782px; 5 | $border-radius: 4px; 6 | $sidebar-width: 4.6rem; 7 | $sidebar-width-medium: 6rem; 8 | $admin-bar-height: 46px; 9 | $admin-bar-height-medium: 32px; 10 | 11 | // Colors 12 | $background-color: #f7f7f8; 13 | $font-color-body: #3b454d; 14 | $border-color: #e6e6e6; 15 | $border-hover-color: darken( $border-color, 15% ); 16 | $loading-pulse-color: #f7f7f7; 17 | $mono-hover-color: #f9f9f9; 18 | $color-gray: #f3f3f3; 19 | $color-green: #83c61a; 20 | $color-red: #da635d; 21 | $color-blue: #16c6eb; 22 | 23 | // Typography 24 | $font-size: 16px; 25 | 26 | // Animation 27 | $transition-duration: 0.2s; 28 | -------------------------------------------------------------------------------- /src/constant.js: -------------------------------------------------------------------------------- 1 | const config = global.dones || {}; 2 | 3 | export const SITE_NAME = config.siteName; 4 | export const SITE_URL = config.siteUrl; 5 | export const API_ROOT = config.apiRoot; 6 | export const API_NONCE = config.apiNonce; 7 | export const DONE_API_ROOT = `${ API_ROOT }/dones/v1`; 8 | export const BRAND_COLOR = config.brandColor; 9 | export const LOGO = config.logo; 10 | export const I18N = config.i18n; 11 | export const PRELOADED_REQUESTS = config.preload; 12 | export const GMT_OFFSET = Number( config.gmtOffset ); 13 | export const DATE_FORMAT = config.dateFormat; 14 | export const USER_ID = Number( config.userId ); 15 | export const LOGIN_URL = config.loginUrl; 16 | export const LOGOUT_URL = config.logoutUrl; 17 | -------------------------------------------------------------------------------- /src/lib/callbackify/test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | 5 | /** 6 | * Internal dependencies 7 | */ 8 | import callbackify from '../'; 9 | 10 | describe( 'callbackify', () => { 11 | it( 'resolves callback with success', ( done ) => { 12 | callbackify( Promise.resolve( 'success' ), ( error, result ) => { 13 | expect( error ).toBe( null ); 14 | expect( result ).toBe( 'success' ); 15 | 16 | done(); 17 | } ); 18 | } ); 19 | 20 | it( 'resolves callback with error', ( done ) => { 21 | callbackify( Promise.reject( 'failure' ), ( error, result ) => { 22 | expect( error ).toBe( 'failure' ); 23 | expect( result ).toBeUndefined(); 24 | 25 | done(); 26 | } ); 27 | } ); 28 | } ); 29 | -------------------------------------------------------------------------------- /src/state/users/reducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { reduce, isEqual } from 'lodash'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { USERS_RECEIVE } from 'state/action-types'; 10 | 11 | export default function( state = null, action ) { 12 | switch ( action.type ) { 13 | case USERS_RECEIVE: 14 | return reduce( 15 | action.users, 16 | ( memo, user ) => { 17 | if ( isEqual( memo[ user.id ], user ) ) { 18 | return memo; 19 | } 20 | 21 | if ( memo === state ) { 22 | memo = { ...state }; 23 | } 24 | 25 | memo[ user.id ] = user; 26 | return memo; 27 | }, 28 | state || {} 29 | ); 30 | } 31 | 32 | return state; 33 | } 34 | -------------------------------------------------------------------------------- /src/test/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render as _render } from 'preact'; 5 | import { act } from 'preact/test-utils'; 6 | import { StoreContext } from 'prsh'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import createReduxStore from 'state'; 12 | 13 | export function render( element, store = createReduxStore() ) { 14 | document.body.innerHTML = ''; 15 | 16 | const wrapper = document.createElement( 'div' ); 17 | document.body.appendChild( wrapper ); 18 | 19 | act( () => { 20 | _render( 21 | 22 | { element } 23 | , 24 | wrapper 25 | ); 26 | } ); 27 | 28 | return wrapper.firstChild; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/user/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useSelector } from 'prsh'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { translate } from 'lib/i18n'; 10 | import { getUser } from 'state/selectors'; 11 | 12 | function User( { userId } ) { 13 | const { name, avatars } = useSelector( ( state ) => 14 | getUser( state, userId ) 15 | ); 16 | 17 | return ( 18 | 19 | { 27 | { name } 28 | 29 | ); 30 | } 31 | 32 | export default User; 33 | -------------------------------------------------------------------------------- /src/state/selectors/has-received-dones.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import createSelector from 'rememo'; 5 | import stringify from 'fast-stable-stringify'; 6 | 7 | /** 8 | * Returns true if the done result has been received for the specified query, 9 | * or false otherwise (including if a request has not been made). 10 | * 11 | * @param {Object} state Global state object 12 | * @param {Object} query Query object 13 | * @return {boolean} Whether dones have been received 14 | */ 15 | export function hasReceivedDones( state, query ) { 16 | return !! state.dones.received[ stringify( query ) ]; 17 | } 18 | 19 | export default createSelector( hasReceivedDones, ( state ) => [ 20 | state.dones.received, 21 | ] ); 22 | -------------------------------------------------------------------------------- /src/components/popover-menu/_style.scss: -------------------------------------------------------------------------------- 1 | .popover-menu { 2 | width: 12rem; 3 | } 4 | 5 | .popover-menu__list { 6 | list-style-type: none; 7 | padding-left: 0; 8 | margin: 0 -0.8rem; 9 | } 10 | 11 | .popover-menu__button { 12 | width: 100%; 13 | padding: 0.1rem 0.8rem; 14 | white-space: nowrap; 15 | overflow: hidden; 16 | text-overflow: ellipsis; 17 | text-align: left; 18 | 19 | .popover-menu__list-item.is-selected &, 20 | .popover-menu__list-item:hover & { 21 | color: #fff; 22 | cursor: pointer; 23 | } 24 | 25 | .popover-menu__list-item.is-selected & { 26 | background-color: $color-blue; 27 | } 28 | 29 | .popover-menu__list-item:hover:not( .is-selected ) & { 30 | background-color: rgba( $color-blue, 0.6 ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/state/routing/actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { ROUTE_PRELOAD, ROUTE_PUSH, ROUTE_REPLACE } from 'state/action-types'; 5 | 6 | export function pushRoute( path ) { 7 | return { 8 | type: ROUTE_PUSH, 9 | path, 10 | }; 11 | } 12 | 13 | export function replaceRoute( path ) { 14 | return { 15 | type: ROUTE_REPLACE, 16 | path, 17 | }; 18 | } 19 | 20 | /** 21 | * Returns an action object used in signalling that data requirements for a 22 | * given path should be fetched as preload. 23 | * 24 | * @param {string} path Route path to preload 25 | * @return {Object} Action object 26 | */ 27 | export function preloadRoute( path ) { 28 | return { 29 | type: ROUTE_PRELOAD, 30 | path, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/tag-dones/date.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { map, groupBy } from 'lodash'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import Card from 'components/card'; 10 | import { formatSiteDate } from 'lib/i18n'; 11 | import TagDonesUser from './user'; 12 | 13 | export default function TagDonesDate( { date, dones } ) { 14 | return ( 15 | 16 |
    17 | { map( groupBy( dones, 'user' ), ( userDones, userId ) => ( 18 |
  • 19 | 20 |
  • 21 | ) ) } 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/state/tags/reducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { uniq, without } from 'lodash'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { TAGS_RECEIVE, DONE_CREATE, DONE_UPDATE } from 'state/action-types'; 10 | 11 | export default function( state = null, action ) { 12 | switch ( action.type ) { 13 | case TAGS_RECEIVE: 14 | return uniq( ( state || [] ).concat( action.tags ) ); 15 | 16 | case DONE_CREATE: 17 | case DONE_UPDATE: 18 | const pattern = /(^|\s)#(\S+)/g; 19 | 20 | let match; 21 | while ( ( match = pattern.exec( action.text ) ) ) { 22 | const [ , , tag ] = match; 23 | state = [ tag, ...without( state, tag ) ]; 24 | } 25 | 26 | return state; 27 | } 28 | 29 | return state; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/done-status/_style.scss: -------------------------------------------------------------------------------- 1 | .done-status { 2 | position: relative; 3 | display: inline-block; 4 | margin: 0 0.6rem; 5 | border: 1px solid $border-color; 6 | border-radius: 50%; 7 | height: 1.5rem; 8 | width: 1.5rem; 9 | line-height: 1.5rem; 10 | text-align: center; 11 | color: $border-color; 12 | vertical-align: top; 13 | 14 | &.is-toggleable { 15 | cursor: pointer; 16 | } 17 | 18 | &.is-done { 19 | background-color: $color-green; 20 | border-color: $color-green; 21 | color: #fff; 22 | } 23 | 24 | .icon { 25 | position: relative; 26 | display: block; 27 | margin: 0 auto; 28 | } 29 | } 30 | 31 | .done-status__check { 32 | position: absolute; 33 | top: 50%; 34 | left: 50%; 35 | transform: translate( -50%, -50% ); 36 | } 37 | -------------------------------------------------------------------------------- /inc/admin/about.css: -------------------------------------------------------------------------------- 1 | .about-dones, 2 | .about-dones * { 3 | box-sizing: border-box; 4 | } 5 | 6 | .about-dones { 7 | max-width: 680px; 8 | margin: 20px 0; 9 | padding-right: 10px; 10 | } 11 | 12 | .about-dones__brand { 13 | padding: 20px; 14 | margin-bottom: 20px; 15 | background-color: #986dda; 16 | text-align: center; 17 | } 18 | 19 | .about-dones__screenshot { 20 | display: block; 21 | margin: 0 auto; 22 | } 23 | 24 | @media ( min-width: 480px ) { 25 | .about-dones__screenshot { 26 | float: left; 27 | margin-right: 10px; 28 | } 29 | } 30 | 31 | .about-dones__faq dt { 32 | display: block; 33 | font-weight: bold; 34 | } 35 | 36 | .about-dones__faq dd { 37 | margin: 10px 0; 38 | } 39 | 40 | .about-dones__faq dd + dt { 41 | margin-top: 20px; 42 | } 43 | -------------------------------------------------------------------------------- /src/state/selectors/get-dones-for-user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import createSelector from 'rememo'; 5 | import { filter } from 'lodash'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { getDones } from './'; 11 | 12 | /** 13 | * Returns dones filtered by query and user ID. 14 | * 15 | * @param {Object} state Global state object 16 | * @param {Object} query Query object to filter 17 | * @param {number} userId User ID to filter 18 | * @return {?Object} Dones, or null if not known 19 | */ 20 | export function getDonesForUser( state, query, userId ) { 21 | return filter( getDones( state, query ), { user: userId } ); 22 | } 23 | 24 | export default createSelector( getDonesForUser, ( state ) => [ state.dones ] ); 25 | -------------------------------------------------------------------------------- /src/state/selectors/get-dones-total-pages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import createSelector from 'rememo'; 5 | import stringify from 'fast-stable-stringify'; 6 | import { get, omit } from 'lodash'; 7 | 8 | /** 9 | * Returns the total number of pages for the given query, or null if the page 10 | * count is not yet known. 11 | * 12 | * @param {Object} state Global state object 13 | * @param {Object} query Query object 14 | * @return {?number} Page count if known 15 | */ 16 | export function getDonesTotalPages( state, query ) { 17 | query = stringify( omit( query, 'page' ) ); 18 | return get( state.dones.totalPages, query, null ); 19 | } 20 | 21 | export default createSelector( getDonesTotalPages, ( state ) => [ 22 | state.dones.totalPages, 23 | ] ); 24 | -------------------------------------------------------------------------------- /src/assets/stylesheets/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin medium() { 2 | @media ( min-width: #{ ( $medium-width + 1px ) } ) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin desktop() { 8 | @media ( min-width: #{ ( $desktop-width + 1px ) } ) { 9 | @content; 10 | } 11 | } 12 | 13 | @mixin loading-pulse( $color: $loading-pulse-color ) { 14 | background-color: $color; 15 | animation: loading-pulse 1.6s ease-in-out infinite; 16 | } 17 | 18 | @mixin loading-pulse-text( $color: $loading-pulse-color ) { 19 | position: relative; 20 | color: transparent !important; 21 | text-shadow: none; 22 | 23 | &:before { 24 | @include loading-pulse( $color ); 25 | content: ''; 26 | position: absolute; 27 | top: 1px; 28 | right: 0; 29 | bottom: 1px; 30 | left: 0; 31 | } 32 | 33 | &:after { 34 | content: 'Loading'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/state/selectors/test/has-received-dones.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | 5 | /** 6 | * Internal dependencies 7 | */ 8 | import { hasReceivedDones } from '../has-received-dones'; 9 | 10 | describe( 'hasReceivedDones()', () => { 11 | it( 'returns false if query not tracked', () => { 12 | const result = hasReceivedDones( 13 | { 14 | dones: { 15 | received: {}, 16 | }, 17 | }, 18 | { date: '2017-05-09' } 19 | ); 20 | 21 | expect( result ).toBe( false ); 22 | } ); 23 | 24 | it( 'returns received value for query', () => { 25 | const result = hasReceivedDones( 26 | { 27 | dones: { 28 | received: { 29 | '{"date":"2017-05-09"}': true, 30 | }, 31 | }, 32 | }, 33 | { date: '2017-05-09' } 34 | ); 35 | 36 | expect( result ).toBe( true ); 37 | } ); 38 | } ); 39 | -------------------------------------------------------------------------------- /src/components/autosize-textarea/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { over, compact } from 'lodash'; 5 | import { useEffect, useRef } from 'preact/hooks'; 6 | import classcat from 'classcat'; 7 | 8 | function AutosizeTextarea( { className, value, onInput, ...additionalProps } ) { 9 | const node = useRef(); 10 | 11 | useEffect( resize, [ value ] ); 12 | 13 | function resize() { 14 | node.current.style.height = '0'; 15 | node.current.style.height = node.current.scrollHeight + 'px'; 16 | } 17 | 18 | const classes = classcat( [ 'autosize-textarea', className ] ); 19 | 20 | return ( 21 |