├── .editorconfig ├── .firebaserc ├── .gitignore ├── firebase.json ├── gulpfile.js ├── package-lock.json ├── package.json ├── readme.md └── src ├── 404.html ├── android-chrome-192x192.png ├── apple-touch-icon.png ├── blocks ├── board │ └── board.less ├── bricks │ └── bricks.less ├── cartonbox │ ├── cartonbox.js │ └── cartonbox.less ├── chisel │ └── chisel.less ├── donate │ └── donate.less ├── focus │ ├── focus.js │ └── focus.less ├── form │ ├── form.js │ └── form.less ├── grid │ ├── grid.js │ └── grid.less ├── list │ ├── list.js │ └── list.less ├── logo │ └── logo.less ├── menu │ └── menu.less ├── share │ └── share.less ├── sync │ ├── sync.js │ └── sync.less ├── task │ ├── task.js │ └── task.less ├── tinycon │ └── tinycon.js └── view │ └── view.less ├── favicon-32x32.png ├── img ├── favicon-today.png ├── interface.png ├── notify.png ├── settings.png └── themes │ ├── bush-lg.jpg │ ├── bush-md.jpg │ ├── bush-sm.jpg │ ├── bush-xs.jpg │ ├── clouds-lg.jpg │ ├── clouds-md.jpg │ ├── clouds-sm.jpg │ ├── clouds-xs.jpg │ ├── flow-lg.jpg │ ├── flow-md.jpg │ ├── flow-sm.jpg │ ├── flow-xs.jpg │ ├── forest-lg.jpg │ ├── forest-md.jpg │ ├── forest-sm.jpg │ ├── forest-xs.jpg │ ├── galaxy-lg.jpg │ ├── galaxy-md.jpg │ ├── galaxy-sm.jpg │ ├── galaxy-xs.jpg │ ├── hill-lg.jpg │ ├── hill-md.jpg │ ├── hill-sm.jpg │ ├── hill-xs.jpg │ ├── leaves-lg.jpg │ ├── leaves-md.jpg │ ├── leaves-sm.jpg │ ├── leaves-xs.jpg │ ├── lighthouse-lg.jpg │ ├── lighthouse-md.jpg │ ├── lighthouse-sm.jpg │ ├── lighthouse-xs.jpg │ ├── lighting-lg.jpg │ ├── lighting-md.jpg │ ├── lighting-sm.jpg │ ├── lighting-xs.jpg │ ├── mountains-lg.jpg │ ├── mountains-md.jpg │ ├── mountains-sm.jpg │ ├── mountains-xs.jpg │ ├── skyscrapers-lg.jpg │ ├── skyscrapers-md.jpg │ ├── skyscrapers-sm.jpg │ ├── skyscrapers-xs.jpg │ ├── table-lg.jpg │ ├── table-md.jpg │ ├── table-sm.jpg │ ├── table-xs.jpg │ ├── troposphere-lg.jpg │ ├── troposphere-md.jpg │ ├── troposphere-sm.jpg │ ├── troposphere-xs.jpg │ ├── twigs-lg.jpg │ ├── twigs-md.jpg │ ├── twigs-sm.jpg │ ├── twigs-xs.jpg │ ├── umbrella-lg.jpg │ ├── umbrella-md.jpg │ ├── umbrella-sm.jpg │ ├── umbrella-xs.jpg │ ├── valley-lg.jpg │ ├── valley-md.jpg │ ├── valley-sm.jpg │ ├── valley-xs.jpg │ ├── water-lg.jpg │ ├── water-md.jpg │ ├── water-sm.jpg │ ├── water-xs.jpg │ ├── waves-lg.jpg │ ├── waves-md.jpg │ ├── waves-sm.jpg │ └── waves-xs.jpg ├── inc ├── metrika.html ├── popup-about.html ├── popup-bye.html ├── popup-hello.html ├── popup-privacy.html ├── popup-settings.html ├── popup-terms.html └── popup-tour.html ├── index.html ├── js ├── app.js └── global.js ├── less ├── global.less ├── main.less └── variables.less ├── manifest.json ├── robots.txt ├── safari-pinned-tab.svg └── sitemap.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_style = space 13 | indent_size = 2 14 | trim_trailing_whitespace = false 15 | 16 | [*.json] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "mahoweek-8c3db" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | debug.log 5 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "headers": [ 5 | { 6 | "source" : "**/*.html", 7 | "headers" : [ 8 | { 9 | "key" : "Cache-Control", 10 | "value" : "max-age=3600" 11 | } 12 | ] 13 | }, 14 | { 15 | "source" : "**/*.css", 16 | "headers" : [ 17 | { 18 | "key" : "Cache-Control", 19 | "value" : "max-age=31536000" 20 | } 21 | ] 22 | }, 23 | { 24 | "source" : "**/*.js", 25 | "headers" : [ 26 | { 27 | "key" : "Cache-Control", 28 | "value" : "max-age=31536000" 29 | } 30 | ] 31 | }, 32 | { 33 | "source" : "**/*.{jpg,png,svg}", 34 | "headers" : [ 35 | { 36 | "key" : "Cache-Control", 37 | "value" : "max-age=31536000" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | // Packages 5 | //------------------------------------------------------------------------------ 6 | 7 | const gulp = require('gulp'); 8 | const concat = require('gulp-concat'); 9 | const htmlmin = require('gulp-html-minifier'); 10 | const csso = require('gulp-csso'); 11 | const less = require('gulp-less'); 12 | const autoprefixer = require('gulp-autoprefixer'); 13 | const uglify = require('gulp-uglify-es').default; 14 | const del = require('del'); 15 | const csscomb = require('gulp-csscomb'); 16 | const fileinclude = require('gulp-file-include'); 17 | const multipipe = require('multipipe'); 18 | const browserSync = require('browser-sync').create(); 19 | const gulpsync = require('gulp-sync')(gulp); 20 | const gulpHtmlVersion = require('gulp-html-version'); 21 | const typograf = require('gulp-typograf'); 22 | 23 | 24 | // Paths 25 | //------------------------------------------------------------------------------ 26 | 27 | const paths = { 28 | build: 'build/', 29 | dist: { 30 | base: 'dist/', 31 | css: 'dist/css/', 32 | img: 'dist/img/', 33 | js: 'dist/js/' 34 | }, 35 | src: { 36 | base: [ 37 | 'src/*.*', 38 | 'src/CNAME' 39 | ], 40 | html: [ 41 | 'src/*.html' 42 | ], 43 | img: [ 44 | 'src/img/**/*.{jpg,png,svg}' 45 | ], 46 | css: { 47 | libs: [ 48 | 'node_modules/normalize.css/normalize.css', 49 | 'node_modules/cartonbox/docs/css/cartonbox.min.css' 50 | ], 51 | main: [ 52 | 'src/less/main.less' 53 | ] 54 | }, 55 | js: { 56 | libs: [ 57 | 'node_modules/firebase/firebase-app.js', 58 | 'node_modules/firebase/firebase-auth.js', 59 | 'node_modules/firebase/firebase-database.js', 60 | 'node_modules/jquery/dist/jquery.min.js', 61 | 'node_modules/cartonbox/docs/js/cartonbox.min.js', 62 | 'node_modules/sortablejs/Sortable.min.js' 63 | ], 64 | app: [ 65 | 'src/js/app.js' 66 | ] 67 | } 68 | }, 69 | watch: { 70 | html: [ 71 | 'src/inc/*.html', 72 | 'src/*.html' 73 | ], 74 | css: [ 75 | 'src/blocks/**/*.less', 76 | 'src/less/*.less' 77 | ], 78 | js: [ 79 | 'src/blocks/**/*.js', 80 | 'src/js/*.js' 81 | ] 82 | } 83 | }; 84 | 85 | 86 | // Dist 87 | //------------------------------------------------------------------------------ 88 | 89 | gulp.task('base', function() { 90 | return multipipe( 91 | gulp.src(paths.src.base), 92 | gulp.dest(paths.dist.base) 93 | ); 94 | }); 95 | 96 | gulp.task('html', function() { 97 | return multipipe( 98 | gulp.src(paths.src.html), 99 | fileinclude(), 100 | gulpHtmlVersion(), 101 | typograf({ 102 | locale: ['ru', 'en-US'], 103 | enableRule: ['common/nbsp/afterNumber'], 104 | disableRule: ['common/space/replaceTab'], 105 | processingSeparateParts: false 106 | }), 107 | htmlmin({ 108 | collapseWhitespace: true, 109 | removeComments: true 110 | }), 111 | gulp.dest(paths.dist.base), 112 | browserSync.stream() 113 | ); 114 | }); 115 | 116 | gulp.task('img', function() { 117 | return multipipe( 118 | gulp.src(paths.src.img), 119 | gulp.dest(paths.dist.img) 120 | ); 121 | }); 122 | 123 | gulp.task('css:libs', function() { 124 | return multipipe( 125 | gulp.src(paths.src.css.libs), 126 | concat('libs.min.css'), 127 | csso({ 128 | restructure: false 129 | }), 130 | gulp.dest(paths.dist.css) 131 | ); 132 | }); 133 | 134 | gulp.task('css:main', function() { 135 | return multipipe( 136 | gulp.src(paths.src.css.main), 137 | concat('main.min.css'), 138 | less(), 139 | autoprefixer(), 140 | csscomb(), 141 | csso(), 142 | gulp.dest(paths.dist.css), 143 | browserSync.stream() 144 | ); 145 | }); 146 | 147 | gulp.task('js:libs', function() { 148 | return multipipe( 149 | gulp.src(paths.src.js.libs), 150 | concat('libs.min.js'), 151 | uglify({ 152 | mangle: false 153 | }), 154 | gulp.dest(paths.dist.js) 155 | ); 156 | }); 157 | 158 | gulp.task('js:app', function() { 159 | return multipipe( 160 | gulp.src(paths.src.js.app), 161 | fileinclude(), 162 | concat('app.min.js'), 163 | uglify(), 164 | gulp.dest(paths.dist.js), 165 | browserSync.stream() 166 | ); 167 | }); 168 | 169 | 170 | // Build 171 | //------------------------------------------------------------------------------ 172 | 173 | gulp.task('build', ['clean:build', 'default'], function() { 174 | return multipipe( 175 | gulp.src(paths.dist.base + '**'), 176 | gulp.dest(paths.build) 177 | ); 178 | }); 179 | 180 | 181 | // Watch 182 | //------------------------------------------------------------------------------ 183 | 184 | gulp.task('watch', ['default'], function() { 185 | browserSync.init({ 186 | server: { 187 | baseDir: paths.dist.base 188 | }, 189 | notify: false 190 | }); 191 | 192 | gulp.watch(paths.watch.html, ['html']); 193 | gulp.watch(paths.watch.css, ['css:main']); 194 | gulp.watch(paths.watch.js, ['js:app']); 195 | }); 196 | 197 | 198 | // Clean 199 | //------------------------------------------------------------------------------ 200 | 201 | gulp.task('clean:dist', function() { 202 | return del.sync(['dist/**', '!dist']); 203 | }); 204 | 205 | gulp.task('clean:build', function() { 206 | return del.sync(['build/**', '!build']); 207 | }); 208 | 209 | 210 | // Default 211 | //------------------------------------------------------------------------------ 212 | 213 | gulp.task('default', gulpsync.sync(['clean:dist', 'base', 'html', 'img', 'css:libs', 'css:main', 'js:libs', 'js:app'])); 214 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mahoweek", 3 | "version": "3.1.5", 4 | "dependencies": { 5 | "cartonbox": "^1.5.4", 6 | "firebase": "^5.7.1", 7 | "jquery": "^3.3.1", 8 | "normalize.css": "^8.0.1", 9 | "sortablejs": "^1.7.0" 10 | }, 11 | "devDependencies": { 12 | "browser-sync": "^2.26.3", 13 | "del": "^3.0.0", 14 | "gulp": "^3.9.1", 15 | "gulp-autoprefixer": "^6.0.0", 16 | "gulp-concat": "^2.6.1", 17 | "gulp-csscomb": "^3.0.8", 18 | "gulp-csso": "^3.0.1", 19 | "gulp-file-include": "^2.0.1", 20 | "gulp-html-minifier": "^0.1.8", 21 | "gulp-html-version": "^1.0.2", 22 | "gulp-less": "^4.0.1", 23 | "gulp-sync": "^0.1.4", 24 | "gulp-typograf": "^3.1.0", 25 | "gulp-uglify-es": "^1.0.4", 26 | "multipipe": "^2.0.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Интерфейс](https://mahoweek.com/img/interface.png?v=4) 2 | 3 | # [Mahoweek](https://mahoweek.com) 4 | 5 | Веб-приложение по ведению краткосрочного плана дел. Поможет просто организовать список задач и наметить на календарной сетке даты их выполнения на одну-две недели вперёд. 6 | 7 | Mahoweek полностью бесплатен, не перенасыщен функционалом и одинаково хорошо выглядит как на ноутбуке, так и на телефоне. Его можно добавить на домашний экран и использовать как мобильное приложение для Айос или Андроид. 8 | 9 | Составляйте списки личных дел и проектов, организуйте план путешествия, работы и учёбы, планируйте встречи и поездки, ставьте цели и напоминания о событиях и ещё многое другое. 10 | 11 | ∼∼∼ 12 | 13 | Любые вопросы, предложения, сомнения, критика или замечания помогают нам стать лучше, а вам — счастливее. Пишите на электронную почту app@mahoweek.com. 14 | 15 | Автор и разработчик [Максим Софронов](https://github.com/imaxsof). 16 | -------------------------------------------------------------------------------- /src/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Страница не найдена 7 | 8 | 9 | 10 | 11 | 12 | @@include('inc/metrika.html') 13 | 14 | 15 | 16 |
17 |

Страница не найдена

18 | 19 |

Неправильно набран адрес или такой страницы на сайте больше не существует

20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/apple-touch-icon.png -------------------------------------------------------------------------------- /src/blocks/bricks/bricks.less: -------------------------------------------------------------------------------- 1 | // Bricks 2 | //------------------------------------------------------------------------------ 3 | 4 | .bricks { 5 | display: flex; 6 | flex-direction: column; 7 | margin-right: -7.5px; 8 | margin-left: -7.5px; 9 | 10 | @media (min-width: 768px) { 11 | flex-direction: row; 12 | margin-right: -15px; 13 | margin-left: -15px; 14 | } 15 | 16 | .view + & { 17 | margin-top: 30px; 18 | 19 | @media (min-width: 768px) { 20 | margin-top: 60px; 21 | } 22 | } 23 | } 24 | 25 | .bricks__col { 26 | padding-right: 7.5px; 27 | padding-left: 7.5px; 28 | 29 | @media (max-width: 767px) { 30 | width: 100% !important; 31 | } 32 | 33 | @media (min-width: 768px) { 34 | padding-right: 15px; 35 | padding-left: 15px; 36 | } 37 | 38 | &--grow { 39 | flex: 1 0 0%; 40 | } 41 | 42 | > *:first-child { 43 | margin-top: 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/blocks/cartonbox/cartonbox.js: -------------------------------------------------------------------------------- 1 | // Cartonbox 2 | //------------------------------------------------------------------------------ 3 | 4 | (function($) { 5 | 6 | // Настраиваем Картонбокс 7 | var cartonboxConfig = { 8 | speed: SPEED, 9 | nav: false, 10 | preload: false, 11 | closeHtml: '', 12 | onStartBefore: function() { 13 | // Переносим кнопку закрытия внутрь окна 14 | $('.cartonbox-close').attr({ 15 | "role": "button", 16 | "tabindex": 0, 17 | "aria-label": "Закрыть" 18 | }).prependTo('.cartonbox-container'); 19 | }, 20 | onShowBefore: function() { 21 | // Добавляем класс, что модальное окно открыто 22 | $('body').addClass('modal-open'); 23 | }, 24 | onClosedBefore: function() { 25 | // Удаляем класс, что модальное окно должно запуститься при загрузке страницы 26 | $('body').removeClass('modal-start'); 27 | 28 | // Удаляем класс, что модальное окно открыто 29 | $('body').removeClass('modal-open'); 30 | 31 | // Добавляем класс, что модальное окно закрывается 32 | $('body').addClass('modal-closed'); 33 | }, 34 | onClosedAfter: function() { 35 | // Удаляем класс, что модальное окно закрывается 36 | $('body').removeClass('modal-closed'); 37 | } 38 | }; 39 | 40 | // Инициализируем Картонбокс 41 | $.cartonbox(cartonboxConfig); 42 | 43 | // Если хеш содержит вывод приветственного сообщения 44 | if (window.location.hash === '#hello') { 45 | // Открываем окно приветствия 46 | $('.js-open-hello').trigger('click'); 47 | } 48 | 49 | // Если присутствует любой хеш 50 | if (window.location.hash !== '') { 51 | // Добавляем класс, что модальное окно должно запуститься при загрузке страницы 52 | $('body').addClass('modal-start'); 53 | } 54 | 55 | // Закрываем окно приветствия 56 | $('.js-close-hello').on('click', function() { 57 | $('.cartonbox-close').trigger('click'); 58 | }); 59 | 60 | }(jQuery)); 61 | -------------------------------------------------------------------------------- /src/blocks/cartonbox/cartonbox.less: -------------------------------------------------------------------------------- 1 | // Cartonbox 2 | //------------------------------------------------------------------------------ 3 | 4 | .cartonbox-back { 5 | background-color: fade(@dark-primary, 60%); 6 | visibility: hidden; 7 | animation-name: none; 8 | animation-duration: @duration; 9 | animation-timing-function: @timing-function; 10 | animation-fill-mode: both; 11 | 12 | .modal-start & { 13 | animation-delay: @duration; 14 | } 15 | 16 | .ready.modal-open & { 17 | visibility: visible; 18 | animation-name: fadeIn; 19 | } 20 | 21 | .ready.modal-closed & { 22 | visibility: visible; 23 | animation-name: fadeOut; 24 | } 25 | } 26 | 27 | .cartonbox-preloader { 28 | display: none !important; 29 | } 30 | 31 | .cartonbox-wrap { 32 | visibility: hidden; 33 | animation-name: none; 34 | animation-duration: @duration; 35 | animation-timing-function: @timing-function; 36 | animation-fill-mode: both; 37 | 38 | .modal-start & { 39 | animation-delay: @duration; 40 | } 41 | 42 | .ready.modal-open & { 43 | visibility: visible; 44 | animation-name: fadeInUp; 45 | } 46 | 47 | .ready.modal-closed & { 48 | visibility: visible; 49 | animation-name: fadeOut; 50 | } 51 | 52 | &:not(.cartonbox-img) { 53 | width: auto; 54 | padding: 0 5px; 55 | 56 | @media (min-width: 480px) { 57 | max-width: 480px; 58 | } 59 | 60 | @media (min-width: 768px) { 61 | max-width: 768px; 62 | padding: 0 20px; 63 | } 64 | } 65 | } 66 | 67 | .cartonbox-flex { 68 | padding: 30px 0; 69 | } 70 | 71 | .cartonbox-container { 72 | position: relative; 73 | width: 100% !important; 74 | height: auto !important; 75 | padding: 0; 76 | overflow: visible; 77 | border-radius: (@radius * 2); 78 | 79 | .cartonbox-no-close &::before { 80 | content: ""; 81 | position: fixed; 82 | top: -100px; 83 | left: -100px; 84 | width: calc(~"100% + 200px"); 85 | height: calc(~"100% + 200px"); 86 | } 87 | } 88 | 89 | .cartonbox-content { 90 | position: relative; 91 | padding: 30px 15px 15px; 92 | overflow: hidden; 93 | 94 | @media (min-width: 768px) { 95 | padding: 60px 60px 45px; 96 | } 97 | 98 | > [id] { 99 | margin-top: -60px; 100 | padding-top: 60px; 101 | 102 | @media (min-width: 768px) { 103 | margin-top: -90px; 104 | padding-top: 90px; 105 | } 106 | } 107 | 108 | h2 { 109 | padding-right: 20px; 110 | padding-left: 20px; 111 | } 112 | } 113 | 114 | .cartonbox-close { 115 | position: absolute; 116 | box-sizing: content-box; 117 | width: 25px; 118 | height: 25px; 119 | padding: 10px; 120 | background: none; 121 | border: 0; 122 | 123 | .cartonbox-wrap & { 124 | display: block !important; 125 | } 126 | 127 | @media (min-width: 768px) { 128 | padding: 15px; 129 | } 130 | 131 | svg { 132 | fill: @dark-secondary !important; 133 | transition: none !important; 134 | } 135 | 136 | body:not(.mobile) &:hover svg { 137 | fill: @dark-primary !important; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/blocks/chisel/chisel.less: -------------------------------------------------------------------------------- 1 | // Chisel 2 | //------------------------------------------------------------------------------ 3 | 4 | .chisel { 5 | position: relative; 6 | margin-top: 30px; 7 | margin-bottom: 30px; 8 | padding-top: 30px; 9 | padding-right: 15px; 10 | padding-bottom: 15px; 11 | padding-left: 15px; 12 | border-radius: @radius; 13 | 14 | &:last-child { 15 | margin-bottom: 0; 16 | } 17 | 18 | @media (min-width: 768px) { 19 | margin-top: 60px; 20 | margin-bottom: 60px; 21 | padding-top: 60px; 22 | padding-right: 60px; 23 | padding-bottom: 45px; 24 | padding-left: 60px; 25 | 26 | &:last-child { 27 | margin-bottom: 15px; 28 | } 29 | } 30 | 31 | .view + &, 32 | .share + & { 33 | margin-top: 35px; 34 | 35 | @media (min-width: 768px) { 36 | margin-top: 65px; 37 | } 38 | } 39 | 40 | .bricks + & { 41 | margin-top: 15px; 42 | 43 | @media (min-width: 768px) { 44 | margin-top: 45px; 45 | } 46 | } 47 | 48 | &--inset { 49 | @media (min-width: 768px) { 50 | padding-right: 30px; 51 | padding-left: 30px; 52 | } 53 | } 54 | 55 | &--border { 56 | border: 5px solid; 57 | 58 | @media (min-width: 768px) { 59 | border-width: 10px; 60 | } 61 | 62 | &::before { 63 | content: ""; 64 | position: absolute; 65 | top: 0; 66 | right: 0; 67 | bottom: 0; 68 | left: 0; 69 | background-color: @light-primary; 70 | border-radius: (@radius / 2); 71 | } 72 | 73 | > * { 74 | position: relative; 75 | } 76 | } 77 | 78 | &--default { 79 | background-color: #f5f5f5; 80 | border-color: #f5f5f5; 81 | } 82 | 83 | &--warning { 84 | background-color: #ffecb3; 85 | border-color: #ffecb3; 86 | } 87 | 88 | &--email { 89 | background-image: linear-gradient(135deg, #f5f5f5 12.5%, #64b5f6 12.5%, #64b5f6 25%, #f5f5f5 25%, #f5f5f5 37.5%, #e57373 37.5%, #e57373 50%, #f5f5f5 50%, #f5f5f5 62.5%, #64b5f6 62.5%, #64b5f6 75%, #f5f5f5 75%, #f5f5f5 87.5%, #e57373 87.5%); 90 | background-repeat: repeat; 91 | background-size: 50px 50px; 92 | border-color: transparent; 93 | 94 | &::before { 95 | background-color: #f5f5f5; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/blocks/donate/donate.less: -------------------------------------------------------------------------------- 1 | // Donate 2 | //------------------------------------------------------------------------------ 3 | 4 | .donate { 5 | width: 201px; 6 | margin-left: auto; 7 | margin-right: auto; 8 | text-align: center; 9 | border-radius: @radius; 10 | background-color: #F0F4C3; 11 | padding: 30px 15px 15px; 12 | margin-bottom: 15px; 13 | position: relative; 14 | overflow: hidden; 15 | 16 | svg { 17 | width: 96px; 18 | height: 96px; 19 | display: block; 20 | margin: 0 auto 5px; 21 | } 22 | 23 | button { 24 | min-width: auto; 25 | margin-top: 10px; 26 | margin-bottom: 10px; 27 | 28 | &::before { 29 | top: -500px; 30 | left: -500px; 31 | right: -500px; 32 | bottom: -500px; 33 | position: absolute; 34 | content: ""; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/blocks/focus/focus.js: -------------------------------------------------------------------------------- 1 | // Focus 2 | //------------------------------------------------------------------------------ 3 | 4 | Array.prototype.forEach.call(document.querySelectorAll('.js-choose-focus'), function(el) { 5 | el.addEventListener('change', function(value) { 6 | changeFocus(this.value); 7 | }); 8 | }); 9 | 10 | function changeFocus(value) { 11 | if (value === 'all') { 12 | document.body.classList.remove('focus-today', 'focus-planned', 'focus-someday', 'focus-completed'); 13 | 14 | document.body.classList.add('focus-all'); 15 | } else if (value === 'today') { 16 | document.body.classList.remove('focus-all', 'focus-planned', 'focus-someday', 'focus-completed'); 17 | 18 | document.body.classList.add('focus-today'); 19 | } else if (value === 'planned') { 20 | document.body.classList.remove('focus-all', 'focus-today', 'focus-someday', 'focus-completed'); 21 | 22 | document.body.classList.add('focus-planned'); 23 | } else if (value === 'someday') { 24 | document.body.classList.remove('focus-all', 'focus-today', 'focus-planned', 'focus-completed'); 25 | 26 | document.body.classList.add('focus-someday'); 27 | } else if (value === 'completed') { 28 | document.body.classList.remove('focus-all', 'focus-today', 'focus-planned', 'focus-someday'); 29 | 30 | document.body.classList.add('focus-completed'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/blocks/focus/focus.less: -------------------------------------------------------------------------------- 1 | // Focus 2 | //------------------------------------------------------------------------------ 3 | 4 | .focus { 5 | width: 100%; 6 | // display: flex; 7 | display: none; 8 | justify-content: space-between; 9 | margin-top: 10px; 10 | 11 | @media (min-width: 768px) { 12 | margin-top: -30px; 13 | justify-content: center; 14 | } 15 | } 16 | 17 | .focus__item { 18 | @media (min-width: 768px) { 19 | margin-right: 20px; 20 | 21 | &:last-child { 22 | margin-right: 0; 23 | } 24 | } 25 | 26 | input { 27 | position: absolute; 28 | width: 1px; 29 | height: 1px; 30 | margin: -1px; 31 | padding: 0; 32 | border: 0; 33 | overflow: hidden; 34 | clip: rect(0 0 0 0); 35 | white-space: nowrap; 36 | } 37 | 38 | input + span { 39 | position: relative; 40 | display: block; 41 | cursor: pointer; 42 | color: @light-primary; 43 | font-size: @font-size-small; 44 | 45 | @media (min-width: 768px) { 46 | line-height: 30px; 47 | } 48 | 49 | body:not(.mobile) & { 50 | color: fade(@light-primary, 70%); 51 | 52 | &:hover { 53 | color: @light-primary; 54 | } 55 | } 56 | 57 | &::before { 58 | position: absolute; 59 | bottom: 0; 60 | left: 50%; 61 | content: ""; 62 | transform: translate3d(-50%, 0, 0) scaleX(0); 63 | width: 50%; 64 | height: 1px; 65 | background-color: @light-primary; 66 | opacity: 0; 67 | transition: @duration @timing-function; 68 | transition-property: transform, opacity; 69 | } 70 | } 71 | 72 | input:checked + span { 73 | cursor: default; 74 | color: @light-primary !important; 75 | 76 | &::before { 77 | opacity: 1; 78 | transform: translate3d(-50%, 0, 0) scaleX(1); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/blocks/form/form.js: -------------------------------------------------------------------------------- 1 | // Form 2 | //------------------------------------------------------------------------------ 3 | 4 | // Настраиваем счетчик в фавиконке 5 | //------------------------------------------------------------------------------ 6 | 7 | (function($) { 8 | 9 | // Если в браузере поддерживается смена фавиконки 10 | if (CHANGE_FAVICON) { 11 | // Показываем настройку 12 | SETTINGS_FORM.find('.js-choose-favicon-counter').parents('.form__group').removeClass('form__group--hidden'); 13 | 14 | // Парсим Хранилище и находим настройку счетчика в фавиконке 15 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 16 | var faviconCounter = mahoweekStorage.settings.faviconCounter; 17 | 18 | // Выставляем состояние чекбокса 19 | if (faviconCounter === true) { 20 | SETTINGS_FORM.find('.js-choose-favicon-counter').attr('checked', 'checked'); 21 | } else { 22 | SETTINGS_FORM.find('.js-choose-favicon-counter').removeAttr('checked', 'checked'); 23 | } 24 | 25 | // Меняем состояние чекбокса 26 | SETTINGS_FORM.find('.js-choose-favicon-counter').on('change', function() { 27 | // Получаем настройку счетчика в фавиконке 28 | faviconCounter = SETTINGS_FORM.find('.js-choose-favicon-counter').prop('checked'); 29 | 30 | // Парсим Хранилище и меняем настройку 31 | mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 32 | mahoweekStorage.settings.faviconCounter = faviconCounter; 33 | 34 | // Обновляем Хранилище 35 | updateStorage(mahoweekStorage); 36 | 37 | // Меняем фавиконку 38 | changeFavicon(); 39 | }); 40 | } 41 | 42 | }(jQuery)); 43 | 44 | 45 | // Настраиваем оповещения 46 | //------------------------------------------------------------------------------ 47 | 48 | (function($) { 49 | 50 | // Если в браузере поддерживаются оповещения 51 | if (NOTIFICATION) { 52 | // Показываем настройку 53 | SETTINGS_FORM.find('.js-choose-notify').parents('.form__group').removeClass('form__group--hidden'); 54 | 55 | // Если время оповещения ранее выставлялось 56 | // и пользователь разрешил оповещения 57 | if (localStorage.getItem('mwNotify') && Notification.permission === 'granted') { 58 | // Показываем выбранный пункт 59 | SETTINGS_FORM.find('.js-choose-notify option[value="' + localStorage.getItem('mwNotify') + '"]').attr('selected', 'selected'); 60 | } else { 61 | localStorage.removeItem('mwNotify'); 62 | } 63 | 64 | // Меняем время оповещения 65 | SETTINGS_FORM.find('.js-choose-notify').on('change', function() { 66 | // Получаем текущее значение 67 | var notifyValue = $(this).val(); 68 | 69 | // Строим сообщение оповещения 70 | var notificationTitle = 'Оповещения включены'; 71 | var notificationBody = 'Теперь добавьте время выполнения делам и держите сайт открытым в браузере, чтобы оповещения приходили'; 72 | var notificationIcon = 'img/notify.png?v=2'; 73 | 74 | if (notifyValue === 'none') { 75 | // Выключаем оповещения 76 | localStorage.setItem('mwNotify', 'none'); 77 | } else { 78 | // Если пользователь ранее разрешил оповещения 79 | if (Notification.permission === 'granted') { 80 | // Если до изменения оповещения не были заданы или были выключены 81 | if (!localStorage.getItem('mwNotify') || localStorage.getItem('mwNotify') === 'none') { 82 | // Показываем оповещение с краткой справкой 83 | var notification = new Notification(notificationTitle, { 84 | body: notificationBody, 85 | icon: notificationIcon, 86 | requireInteraction: true 87 | }); 88 | } 89 | 90 | // Меняем время оповещения 91 | localStorage.setItem('mwNotify', notifyValue); 92 | 93 | // Если пользователь еще не включал оповещения 94 | } else if (Notification.permission === 'default') { 95 | // Запрашиваем права 96 | Notification.requestPermission(function(permission) { 97 | // И если пользователь разрешил оповещения 98 | if (permission === 'granted') { 99 | // Записываем время оповещения 100 | localStorage.setItem('mwNotify', notifyValue); 101 | 102 | // Показываем оповещение с краткой справкой 103 | var notification = new Notification(notificationTitle, { 104 | body: notificationBody, 105 | icon: notificationIcon, 106 | requireInteraction: true 107 | }); 108 | 109 | // А если пользователь заблокировал оповещения 110 | } else { 111 | // Выключаем оповещения 112 | localStorage.setItem('mwNotify', 'none'); 113 | 114 | // Делаем состояние селекта по-умолчанию 115 | SETTINGS_FORM.find('.js-choose-notify option').removeAttr('selected', 'selected'); 116 | } 117 | }); 118 | 119 | // Если пользователь ранее блокировал оповещения 120 | } else if (Notification.permission === 'denied') { 121 | // Показываем алерт 122 | alert('Ранее вы блокировали отправку вам оповещений. Пожалуйста, разрешите оповещения в настройках сайта в браузере'); 123 | 124 | // Делаем состояние селекта по-умолчанию 125 | SETTINGS_FORM.find('.js-choose-notify option').removeAttr('selected', 'selected'); 126 | } 127 | } 128 | }); 129 | } 130 | 131 | }(jQuery)); 132 | 133 | 134 | // Настраиваем доску 135 | //------------------------------------------------------------------------------ 136 | 137 | (function($) { 138 | 139 | // Отменяем отправку формы по сабмиту 140 | SETTINGS_FORM.on('submit', function(event) { 141 | event.preventDefault(); 142 | }); 143 | 144 | // Парсим Хранилище 145 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 146 | 147 | // Определяем текущие параметры 148 | var theme = mahoweekStorage.settings.theme; 149 | var deleteCompletedTasks = mahoweekStorage.settings.deleteCompletedTasks; 150 | 151 | // Выделяем текущую тему как активную 152 | SETTINGS_FORM.find('.js-choose-theme[value="' + theme + '"]').attr('checked', 'checked'); 153 | 154 | // Выставляем состояние чекбокса настройки удаления выполненных дел 155 | if (deleteCompletedTasks === true) { 156 | SETTINGS_FORM.find('.js-choose-delete-completed-tasks').attr('checked', 'checked'); 157 | } else { 158 | SETTINGS_FORM.find('.js-choose-delete-completed-tasks').removeAttr('checked', 'checked'); 159 | } 160 | 161 | // Сохраняем параметры 162 | SETTINGS_FORM.find('.js-choose-theme, .js-choose-delete-completed-tasks').on('change', function() { 163 | // Определяем новые параметры 164 | theme = SETTINGS_FORM.find('.js-choose-theme[name="theme"]:checked').val(); 165 | deleteCompletedTasks = SETTINGS_FORM.find('.js-choose-delete-completed-tasks').prop('checked'); 166 | 167 | // Изменяем тему у доски 168 | THEME_BOARD.attr('class', 'board__theme board__theme--' + theme); 169 | 170 | // Парсим Хранилище и изменяем параметры 171 | mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 172 | mahoweekStorage.settings.theme = theme; 173 | mahoweekStorage.settings.deleteCompletedTasks = deleteCompletedTasks; 174 | 175 | // Обновляем Хранилище 176 | updateStorage(mahoweekStorage); 177 | }); 178 | 179 | }(jQuery)); 180 | -------------------------------------------------------------------------------- /src/blocks/form/form.less: -------------------------------------------------------------------------------- 1 | // Form 2 | //------------------------------------------------------------------------------ 3 | 4 | .form__options { 5 | max-width: 390px; 6 | margin-right: auto; 7 | margin-left: auto; 8 | } 9 | 10 | .form__group { 11 | display: flex; 12 | margin-bottom: 15px; 13 | padding-bottom: 15px; 14 | border-bottom: 1px solid @dark-divider; 15 | 16 | .form__options &:last-child { 17 | padding-bottom: 0; 18 | border-bottom: 0; 19 | } 20 | 21 | &--hidden { 22 | display: none; 23 | } 24 | 25 | label { 26 | flex: 1 0 0%; 27 | padding-right: 15px; 28 | } 29 | 30 | [for="favicon-counter"], 31 | [for="delete-completed-tasks"] { 32 | cursor: pointer; 33 | } 34 | } 35 | 36 | .form__checkbox { 37 | position: relative; 38 | overflow: hidden; 39 | display: flex; 40 | 41 | input { 42 | position: absolute; 43 | z-index: 1; 44 | width: 100px; 45 | height: 100px; 46 | cursor: pointer; 47 | opacity: 0; 48 | } 49 | 50 | span { 51 | position: relative; 52 | display: block; 53 | width: 40px; 54 | height: 24px; 55 | margin: auto; 56 | overflow: hidden; 57 | background-color: @checkbox-bg; 58 | border: 1px solid transparent; 59 | border-radius: (@radius * 3); 60 | transition: @duration @timing-function; 61 | transition-property: background-color; 62 | 63 | &::before { 64 | content: ""; 65 | position: absolute; 66 | top: 0; 67 | left: 0; 68 | width: 22px; 69 | height: 22px; 70 | background-color: @light-primary; 71 | border-radius: (@radius * 2.75); 72 | transition: @duration @timing-function; 73 | transition-property: transform; 74 | } 75 | } 76 | 77 | body:not(.mobile) & input:hover + span::before { 78 | transform: translate3d(2px, 0, 0); 79 | } 80 | 81 | input:checked + span { 82 | background-color: @checkbox-checked-bg; 83 | 84 | &::before { 85 | transform: translate3d(16px, 0, 0); 86 | } 87 | } 88 | 89 | body:not(.mobile) & input:hover:checked + span::before { 90 | transform: translate3d(14px, 0, 0); 91 | } 92 | } 93 | 94 | .form__theme { 95 | margin-right: -1px; 96 | margin-bottom: -1px; 97 | 98 | @media (min-width: 768px) { 99 | margin-bottom: 14px; 100 | } 101 | 102 | legend { 103 | margin-bottom: 20px; 104 | } 105 | 106 | label { 107 | position: relative; 108 | display: block; 109 | float: left; 110 | width: calc(~"100% / 3"); 111 | padding: 0 1px 1px 0; 112 | } 113 | 114 | input { 115 | position: absolute; 116 | left: -9999px; 117 | } 118 | 119 | span { 120 | position: relative; 121 | display: block; 122 | padding-bottom: 100%; 123 | background-color: @light-secondary; 124 | background-position: 50% 50%; 125 | background-size: cover; 126 | cursor: pointer; 127 | 128 | body:not(.mobile) &:hover { 129 | opacity: 0.9; 130 | } 131 | 132 | &::before { 133 | content: ""; 134 | position: absolute; 135 | top: 5px; 136 | right: 5px; 137 | bottom: 5px; 138 | left: 5px; 139 | border: 1px solid fade(@light-primary, 50%); 140 | opacity: 0; 141 | transition: @duration @timing-function; 142 | transition-property: opacity; 143 | 144 | @media (min-width: 768px) { 145 | top: 10px; 146 | right: 10px; 147 | bottom: 10px; 148 | left: 10px; 149 | } 150 | } 151 | } 152 | 153 | label:first-of-type span { 154 | border-radius: @radius 0 0 0; 155 | 156 | &::before { 157 | border-radius: (@radius / 2) 0 0 0; 158 | } 159 | } 160 | 161 | label:nth-of-type(3) span { 162 | border-radius: 0 @radius 0 0; 163 | 164 | &::before { 165 | border-radius: 0 (@radius / 2) 0 0; 166 | } 167 | } 168 | 169 | label:nth-last-of-type(4) span { 170 | border-radius: 0 0 0 @radius; 171 | 172 | &::before { 173 | border-radius: 0 0 0 (@radius / 2); 174 | } 175 | } 176 | 177 | label:last-of-type span { 178 | border-radius: 0 0 @radius; 179 | 180 | &::before { 181 | border-radius: 0 0 (@radius / 2); 182 | } 183 | } 184 | 185 | input:checked + span { 186 | top: 0 !important; 187 | cursor: default; 188 | opacity: 1 !important; 189 | 190 | &::before { 191 | opacity: 1; 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/blocks/grid/grid.js: -------------------------------------------------------------------------------- 1 | // Grid 2 | //------------------------------------------------------------------------------ 3 | 4 | // Добавляем/убираем метку 5 | //------------------------------------------------------------------------------ 6 | 7 | (function($) { 8 | 9 | LIST_BOARD.on('mousedown click', '.js-marker-task:not(.grid__date--past):not(.grid__date--completed)', function(event) { 10 | var isThis = $(this); 11 | 12 | // Получаем дело и дату 13 | var task = isThis.parents('.task'); 14 | var taskDate = isThis.attr('data-date'); 15 | 16 | // Если метка ставилась при добавлении дела 17 | if (task.hasClass('task--filled') && event.type === 'mousedown') { 18 | // Запоминаем метку для нового дела 19 | localStorage.setItem('mwTempNewTaskMarker', taskDate); 20 | 21 | // Если метка ставилась у существующего дела 22 | } else if (!task.hasClass('task--add') && event.type === 'click') { 23 | // Получаем хеш списка и хеш дела 24 | var listId = isThis.parents('.list').attr('data-id'); 25 | var taskId = task.attr('data-id'); 26 | 27 | // Парсим Хранилище 28 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 29 | 30 | // Получаем элемент дела в Хранилище 31 | var taskElement = mahoweekStorage.tasks.filter(function(value) { 32 | return value.id == taskId; 33 | }); 34 | 35 | // Получаем индекс дела в Хранилище 36 | var taskIndex = mahoweekStorage.tasks.indexOf(taskElement[0]); 37 | 38 | // Если массива маркеров не существовало 39 | if (!mahoweekStorage.tasks[taskIndex].markers) { 40 | // Создаем массив маркеров и заполняем 41 | mahoweekStorage.tasks[taskIndex].markers = [{ 42 | date: taskDate, 43 | label: 'bull' 44 | }]; 45 | 46 | // Добавляем метку в сетку дат 47 | isThis.addClass('grid__date--bull'); 48 | 49 | // Если дело было выполнено 50 | if (task.hasClass('task--completed')) { 51 | // Помечаем дело как невыполненное 52 | delete mahoweekStorage.tasks[taskIndex].completed; 53 | delete mahoweekStorage.tasks[taskIndex].completedTime; 54 | 55 | // Обновляем дело в списке 56 | task.removeClass('task--completed'); 57 | } 58 | 59 | // Если существовало 60 | } else { 61 | // Проверяем существовала ли уже метка на это число 62 | var markerElement = mahoweekStorage.tasks[taskIndex].markers.filter(function(value) { 63 | return value.date == taskDate; 64 | }); 65 | 66 | // Если метка существовала 67 | if (markerElement != '') { 68 | // Получаем индекс метки 69 | var markerIndex = mahoweekStorage.tasks[taskIndex].markers.indexOf(markerElement[0]); 70 | 71 | // Удаляем метку 72 | mahoweekStorage.tasks[taskIndex].markers.splice(markerIndex, 1); 73 | 74 | // Убираем метку из сетки дат 75 | isThis.removeClass('grid__date--bull'); 76 | 77 | // Если метка не существовала 78 | } else { 79 | // Добавляем метку 80 | mahoweekStorage.tasks[taskIndex].markers.push({ 81 | date: taskDate, 82 | label: 'bull' 83 | }); 84 | 85 | // Добавляем метку в сетку дат 86 | isThis.addClass('grid__date--bull'); 87 | 88 | // Если дело было выполнено 89 | if (task.hasClass('task--completed')) { 90 | // Помечаем дело как невыполненное 91 | delete mahoweekStorage.tasks[taskIndex].completed; 92 | delete mahoweekStorage.tasks[taskIndex].completedTime; 93 | 94 | // Обновляем дело в списке 95 | task.removeClass('task--completed'); 96 | } 97 | } 98 | } 99 | 100 | // Обновляем Хранилище 101 | updateStorage(mahoweekStorage); 102 | 103 | // Изменяем стиль статуса дела 104 | changeStyleTaskStatus(task); 105 | 106 | // Рассчитываем прогресс выполнения списка 107 | makeProgress(listId); 108 | 109 | // Меняем фавиконку 110 | changeFavicon(); 111 | } 112 | }); 113 | 114 | }(jQuery)); 115 | 116 | 117 | // Генерируем сетку дат 118 | //------------------------------------------------------------------------------ 119 | 120 | function makeGrid(type, data) { 121 | // Создаем объект с официальными и неофициальными 122 | // праздниками и праздничными днями РФ 123 | var holidays = { 124 | // "2019-01-01": { 125 | // icon: "🎅", 126 | // title: "Новый год, праздничный день", 127 | // dayoff: true 128 | // }, 129 | // "2019-01-02": { 130 | // title: "Праздничный день", 131 | // dayoff: true 132 | // }, 133 | // "2019-01-03": { 134 | // title: "Праздничный день", 135 | // dayoff: true 136 | // }, 137 | // "2019-01-04": { 138 | // title: "Праздничный день", 139 | // dayoff: true 140 | // }, 141 | // "2019-01-05": { 142 | // title: "Праздничный день (выходной перенесён на 2 мая)", 143 | // dayoff: true 144 | // }, 145 | // "2019-01-06": { 146 | // title: "Праздничный день (выходной перенесён на 3 мая)", 147 | // dayoff: true 148 | // }, 149 | // "2019-01-07": { 150 | // icon: "👼", 151 | // title: "Рождество Христово, праздничный день", 152 | // dayoff: true 153 | // }, 154 | // "2019-01-08": { 155 | // title: "Праздничный день", 156 | // dayoff: true 157 | // }, 158 | // "2019-01-13": { 159 | // icon: "🎄", 160 | // title: "Старый Новый год" 161 | // }, 162 | // "2019-01-25": { 163 | // icon: "🎓", 164 | // title: "Татьянин день" 165 | // }, 166 | // "2019-02-02": { 167 | // icon: "🐹", 168 | // title: "День сурка" 169 | // }, 170 | // "2019-02-14": { 171 | // icon: "❤", 172 | // title: "День всех влюблённых" 173 | // }, 174 | // "2019-02-22": { 175 | // title: "Сокращённый рабочий день", 176 | // dayoff: false 177 | // }, 178 | // "2019-02-23": { 179 | // icon: "💪", 180 | // title: "День защитника Отечества, праздничный день (выходной перенесён на 10 мая)", 181 | // dayoff: true 182 | // }, 183 | // "2019-03-01": { 184 | // icon: "🌷", 185 | // title: "Первый день весны" 186 | // }, 187 | // "2019-03-07": { 188 | // title: "Сокращённый рабочий день", 189 | // dayoff: false 190 | // }, 191 | // "2019-03-08": { 192 | // icon: "👩", 193 | // title: "Международный женский день, праздничный день", 194 | // dayoff: true 195 | // }, 196 | // "2019-03-10": { 197 | // icon: "🌞", 198 | // title: "Масленица" 199 | // }, 200 | // "2019-03-17": { 201 | // icon: "🍀", 202 | // title: "День святого Патрика" 203 | // }, 204 | "2019-04-01": { 205 | icon: "😄", 206 | title: "День смеха" 207 | }, 208 | "2019-04-12": { 209 | icon: "🚀", 210 | title: "День космонавтики" 211 | }, 212 | "2019-04-28": { 213 | icon: "🐇", 214 | title: "Пасха" 215 | }, 216 | "2019-04-30": { 217 | title: "Сокращённый рабочий день" 218 | }, 219 | "2019-05-01": { 220 | icon: "🌷", 221 | title: "Праздник Весны и Труда, праздничный день", 222 | dayoff: true 223 | }, 224 | "2019-05-02": { 225 | title: "Праздничный день (выходной за счёт 5-го января)", 226 | dayoff: true 227 | }, 228 | "2019-05-03": { 229 | title: "Праздничный день (выходной за счёт 6-го января)", 230 | dayoff: true 231 | }, 232 | "2019-05-08": { 233 | title: "Сокращённый рабочий день" 234 | }, 235 | "2019-05-09": { 236 | icon: "🎆", 237 | title: "День Победы, праздничный день", 238 | dayoff: true 239 | }, 240 | "2019-05-10": { 241 | title: "Праздничный день (выходной за счёт 23-го февраля)", 242 | dayoff: true 243 | }, 244 | "2019-06-01": { 245 | icon: "👶", 246 | title: "Международный день защиты детей", 247 | }, 248 | "2019-06-11": { 249 | title: "Сокращённый рабочий день" 250 | }, 251 | "2019-06-12": { 252 | icon: "🇷🇺", 253 | title: "День России, праздничный день", 254 | dayoff: true 255 | }, 256 | "2019-09-01": { 257 | icon: "📚", 258 | title: "День Знаний" 259 | }, 260 | "2019-09-13": { 261 | icon: "😈", 262 | title: "Пятница 13" 263 | }, 264 | "2019-10-31": { 265 | icon: "🎃", 266 | title: "Хэллоуин" 267 | }, 268 | "2019-11-04": { 269 | icon: "✊", 270 | title: "День народного единства, праздничный день", 271 | dayoff: true 272 | }, 273 | "2019-11-24": { 274 | icon: "👩", 275 | title: "День матери" 276 | }, 277 | "2019-12-13": { 278 | icon: "😈", 279 | title: "Пятница 13" 280 | }, 281 | "2019-12-31": { 282 | title: "Сокращённый рабочий день" 283 | } 284 | } 285 | 286 | // Получаем метку реального времени 287 | var date = new Date(); 288 | 289 | // Вычисляем номер дня 290 | var dayNumber = date.getDay(); 291 | 292 | // Задаем воскресенью порядковый номер 7 293 | if (dayNumber == 0) { 294 | dayNumber = 7; 295 | } 296 | 297 | // Делаем по умолчанию день прошедший 298 | var past = 1; 299 | 300 | // Работаем с data 301 | if (data) { 302 | // Создаем объект с метками 303 | var markerList = {}; 304 | 305 | // Добавляем метки в объект 306 | for (var i = 0; i < data.length; i ++) { 307 | var key = data[i].date; 308 | markerList[key] = data[i].label + '|' + (data[i].completed ? 1 : 0); 309 | } 310 | } 311 | 312 | // Начинаем генерировать дни 313 | var grid = ''; 314 | 315 | // Генерируем каждый день, 316 | // причем сдвигаем дни недели если неделя закончилась 317 | for (var i = 1 - dayNumber; i <= 14 - dayNumber; i ++) { 318 | // Определяем переменные 319 | var dateClass = ''; 320 | var newDate = new Date(); 321 | var time = newDate.setDate(date.getDate() + i); 322 | var day = newDate.getDate(time); 323 | var newDayNumber = newDate.getDay(); 324 | var dataDate = makeDate(time, 'grid'); 325 | 326 | // Определяем текущий день 327 | if (date.getDate() == day) { 328 | dateClass += ' grid__date--today'; 329 | 330 | // Меняем все последующие дни на непрошедшие 331 | past = 0; 332 | } 333 | 334 | // Определяем прошедшие дни 335 | if (past) { 336 | dateClass += ' grid__date--past'; 337 | } 338 | 339 | // Если это шапка списка 340 | if (type == 'list') { 341 | if (dataDate in holidays) { 342 | // Определяем выходной ли или праздничный день 343 | if (holidays[dataDate].dayoff || ((newDayNumber === 6 || newDayNumber === 0) && holidays[dataDate].dayoff !== false)) { 344 | dateClass += ' grid__date--holiday'; 345 | } else { 346 | dateClass += ''; 347 | } 348 | 349 | // Выводим день 350 | grid += '
' + (holidays[dataDate].icon !== undefined ? holidays[dataDate].icon : day) + '
'; 351 | } else { 352 | // Определяем выходной ли день 353 | if (newDayNumber == 6 || newDayNumber == 0) { 354 | dateClass += ' grid__date--holiday'; 355 | } 356 | 357 | // Выводим день 358 | grid += '
' + day + '
'; 359 | } 360 | 361 | // Если это дело 362 | } else if (type == 'task') { 363 | // Если у дела есть метки 364 | if (markerList) { 365 | // Смотрим есть ли метка на этот день 366 | if (dataDate in markerList) { 367 | // Смотрим есть ли конкретно метка 368 | if (markerList[dataDate].split('|')[0] == 'bull') { 369 | dateClass += ' grid__date--bull'; 370 | } 371 | 372 | // Смотрим выполнено ли дело в этот день 373 | if (markerList[dataDate].split('|')[1] == 1) { 374 | dateClass += ' grid__date--completed'; 375 | } 376 | } 377 | } 378 | 379 | if (past) { 380 | // Выводим прошедший день 381 | grid += '
'; 382 | } else { 383 | // Выводим день настоящий или будущий 384 | grid += ''; 385 | } 386 | } 387 | } 388 | 389 | // Выводим дни 390 | return grid; 391 | } 392 | -------------------------------------------------------------------------------- /src/blocks/grid/grid.less: -------------------------------------------------------------------------------- 1 | // Grid 2 | //------------------------------------------------------------------------------ 3 | 4 | .grid { 5 | display: flex; 6 | } 7 | 8 | .grid__date { 9 | width: 40px; 10 | border-top: 1px solid @grid-border; 11 | border-left: 1px solid @grid-border; 12 | border-bottom: 1px solid @grid-border; 13 | position: relative; 14 | text-align: center; 15 | border-right: 0; 16 | padding: 0; 17 | background: none; 18 | cursor: pointer; 19 | 20 | &:first-child { 21 | width: 41px; 22 | border-left-width: 2px; 23 | } 24 | 25 | &--past { 26 | color: @dark-hint; 27 | background-image: linear-gradient(135deg, fade(@grid-past-bg, 0) 3px, @grid-past-bg 3px, fade(@grid-past-bg, 0) 4px); 28 | background-repeat: repeat; 29 | background-position: 2px 50%; 30 | background-size: 5px 5px; 31 | cursor: default; 32 | } 33 | 34 | &--today { 35 | z-index: 1; 36 | width: 41px; 37 | margin-right: -1px; 38 | background-color: @grid-today-bg; 39 | border-color: @grid-today-border !important; 40 | border-right: 1px solid @grid-today-border; 41 | 42 | &:first-child { 43 | width: 42px; 44 | } 45 | } 46 | 47 | &:nth-child(8) { 48 | border-left-color: @grid-separator !important; 49 | z-index: 2; 50 | } 51 | 52 | &::before { 53 | position: absolute; 54 | content: ""; 55 | width: 11px; 56 | height: 11px; 57 | border-radius: 50%; 58 | top: 14px; 59 | left: 14px; 60 | background-color: @grid-pea; 61 | opacity: 0; 62 | transform: scale3d(0.6363, 0.6363, 1); 63 | transition-property: opacity, transform; 64 | transition: @duration @timing-function; 65 | } 66 | 67 | body:not(.mobile) .task:not(.task--add) &:not(.grid__date--past):hover::before { 68 | opacity: 1; 69 | } 70 | 71 | body:not(.mobile) .task--filled &:not(.grid__date--past):hover::before { 72 | opacity: 1; 73 | } 74 | 75 | &--bull, 76 | &--completed { 77 | &::before { 78 | opacity: 1; 79 | transform: scale3d(1, 1, 1); 80 | } 81 | } 82 | 83 | &--today { 84 | &::before { 85 | background-color: @grid-pea-today; 86 | } 87 | } 88 | 89 | &--bull { 90 | &::before { 91 | .grid__date--past& { 92 | background-color: @grid-pea-past; 93 | transform: scale3d(0.6363, 0.6363, 1); 94 | } 95 | 96 | .grid__date--today:not(.grid__date--completed)& { 97 | background-color: @grid-pea-today; 98 | } 99 | } 100 | 101 | body:not(.mobile) &:not(.grid__date--past):not(.grid__date--completed):hover::before { 102 | background-color: @grid-pea-hover; 103 | } 104 | 105 | body:not(.mobile) &.grid__date--today:not(.grid__date--completed):hover::before { 106 | background-color: @grid-pea-today-hover; 107 | } 108 | } 109 | 110 | &--completed { 111 | cursor: default; 112 | 113 | &::before { 114 | background-color: @grid-pea-complete; 115 | transform: scale3d(0.6363, 0.6363, 1); 116 | 117 | .grid__date--past& { 118 | background-color: @grid-pea-past-complete; 119 | } 120 | } 121 | } 122 | 123 | .list__head & { 124 | border-top-width: 0; 125 | border-bottom-width: 2px; 126 | padding: 8px 0 7px; 127 | cursor: default; 128 | 129 | &::before { 130 | display: none; 131 | } 132 | 133 | &--holiday::after { 134 | position: absolute; 135 | content: ""; 136 | left: 0; 137 | right: 0; 138 | bottom: -2px; 139 | height: 2px; 140 | background-color: @grid-holiday-border; 141 | z-index: 1; 142 | } 143 | 144 | &--holiday + .grid__date--holiday::after { 145 | left: -1px; 146 | } 147 | } 148 | 149 | .task--add & { 150 | border-bottom-width: 0; 151 | } 152 | 153 | .task--add:not(.task--filled) & { 154 | cursor: default; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/blocks/list/list.js: -------------------------------------------------------------------------------- 1 | // List 2 | //------------------------------------------------------------------------------ 3 | 4 | // Выводим списки на доске 5 | //------------------------------------------------------------------------------ 6 | 7 | function loadList() { 8 | 9 | // Начинаем генерировать списки 10 | var listBoardCreate = ''; 11 | 12 | // Парсим Хранилище 13 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 14 | 15 | // Пробегаемся по каждому списку 16 | for (var i = 0; i < mahoweekStorage.lists.length; i ++) { 17 | listBoardCreate += makeList(mahoweekStorage.lists[i].id, mahoweekStorage.lists[i].name); 18 | } 19 | 20 | // Выводим списки 21 | LIST_BOARD.prepend(listBoardCreate); 22 | 23 | // Выводим сетку дат в шапку существующих списков 24 | // и в строки добавления дела 25 | LIST_BOARD.find('.list__grid').html(makeGrid('list')); 26 | LIST_BOARD.find('.task__grid').html(makeGrid('task')); 27 | 28 | // Включаем сортировку списков 29 | sortableList(); 30 | 31 | } 32 | 33 | 34 | // Добавляем список 35 | //------------------------------------------------------------------------------ 36 | 37 | (function($) { 38 | 39 | $('.js-add-list').on('click', function() { 40 | // Варианты именования списка 41 | let listNameArr = [ 42 | 'Задачи', 43 | 'Дела', 44 | 'Ту-Ду лист', 45 | 'План', 46 | 'Сделать', 47 | 'Выполнить' 48 | ] 49 | 50 | // Создаем данные для списка 51 | let listId = makeHash(); 52 | let listName = listNameArr[Math.floor(Math.random() * listNameArr.length)]; 53 | let listCreatedTime = new Date().getTime(); 54 | 55 | // Парсим Хранилище 56 | let mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 57 | 58 | // Добавляем новый список 59 | mahoweekStorage.lists.push({ 60 | id: listId, 61 | name: listName, 62 | createdTime: listCreatedTime 63 | }); 64 | 65 | // Обновляем Хранилище 66 | updateStorage(mahoweekStorage); 67 | 68 | // Выводим список на доске 69 | LIST_BOARD.append(makeList(listId, listName, 'list--new')); 70 | 71 | // Находим созданный список 72 | let listNew = LIST_BOARD.find('.list:last-child'); 73 | 74 | // Выводим сетку дат в шапку созданного списка 75 | // и в строку добавления дела 76 | listNew.find('.list__grid').html(makeGrid('list')); 77 | listNew.find('.task__grid').html(makeGrid('task')); 78 | 79 | // Включаем сортировку дел 80 | sortableTask(document.querySelector('.list:last-child .list__tasks')); 81 | 82 | // Берем данные окна 83 | // и определяем задержку для анимации 84 | let win = $(window); 85 | let delay = 0; 86 | 87 | // Если созданный список выходит за рамки видимости, 88 | // то перезаписываем задержку для анимации 89 | if (listNew.offset().top > win.scrollTop() + win.height() - 30 - 81) { 90 | delay = SPEED; 91 | } 92 | 93 | // Добавляем задержку анимации 94 | listNew.css('animation-delay', delay / 1000 + 's'); 95 | 96 | // Удаляем метку о том что это новосозданный лист 97 | setTimeout(function() { 98 | listNew.removeClass('list--new').css('animation-delay', ''); 99 | }, delay + SPEED); 100 | 101 | // Смещаем позицию прокрутки до созданного списка 102 | $('body, html').stop().animate({ 103 | scrollTop: listNew.offset().top 104 | }, SPEED * 2); 105 | 106 | // Показываем поле редактирования имени списка 107 | setTimeout(function() { 108 | listNew.find('.js-name').trigger('mouseup', ['run']); 109 | }, delay); 110 | 111 | // Добавляем данные в Метрику 112 | yaCounter43856389.reachGoal('ya-add-list'); 113 | }); 114 | 115 | }(jQuery)); 116 | 117 | 118 | // Сохраняем заголовок списка при изменении 119 | //------------------------------------------------------------------------------ 120 | 121 | (function($) { 122 | 123 | LIST_BOARD.on('keyup change', '.js-edit-list', function(event) { 124 | var isThis = $(this); 125 | 126 | // Если был нажат Enter или пропал фокус и были изменения 127 | if (event.keyCode == 13 || event.type == 'change') { 128 | // Получаем объект списка, его хеш и заголовок 129 | var list = isThis.parents('.list'); 130 | var listId = list.attr('data-id'); 131 | var listName = isThis.val(); 132 | 133 | // Парсим Хранилище 134 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 135 | 136 | // Получаем элемент списка в Хранилище 137 | var listElement = mahoweekStorage.lists.filter(function(value) { 138 | return value.id == listId; 139 | }); 140 | 141 | // Получаем индекс списка в Хранилище 142 | var listIndex = mahoweekStorage.lists.indexOf(listElement[0]); 143 | 144 | // Изменяем заголовок списка 145 | mahoweekStorage.lists[listIndex].name = listName; 146 | 147 | // Обновляем Хранилище 148 | updateStorage(mahoweekStorage); 149 | 150 | // Если в списке нет ни одного дела 151 | if (list.find('.list__tasks .task').length == 0) { 152 | // Ставим фокус в поле добавления дел в созданном списке 153 | list.find('.js-add-task').focus(); 154 | } else { 155 | // Убираем фокус с этого поля 156 | isThis.blur(); 157 | } 158 | } 159 | }); 160 | 161 | }(jQuery)); 162 | 163 | 164 | // Удаляем список 165 | //------------------------------------------------------------------------------ 166 | 167 | (function($) { 168 | 169 | LIST_BOARD.on('click', '.js-remove-list', function() { 170 | var isThis = $(this); 171 | 172 | // Получаем объект списка, его хеш и кол-во дел 173 | var list = isThis.parents('.list'); 174 | var listId = list.attr('data-id'); 175 | var taskTotal = list.find('.list__tasks .task').length; 176 | 177 | // Если в удаляемом списке были дела 178 | if (taskTotal) { 179 | // Задаем вопрос 180 | var question = confirm('При удалении списка, все дела, находящиеся в нём, также будут удалены'); 181 | } 182 | 183 | // Если в списке не было дел 184 | // или ответом на вопрос было «Да» 185 | if (!taskTotal || question) { 186 | // Парсим Хранилище 187 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 188 | 189 | // Получаем элемент списка в Хранилище 190 | var listElement = mahoweekStorage.lists.filter(function(value) { 191 | return value.id == listId; 192 | }); 193 | 194 | // Получаем индекс списка в Хранилище 195 | var listIndex = mahoweekStorage.lists.indexOf(listElement[0]); 196 | 197 | // Удаляем список 198 | mahoweekStorage.lists.splice(listIndex, 1); 199 | 200 | // Если в удаляемом списке были дела 201 | if (taskTotal) { 202 | // Готовим новый массив для дел 203 | var tasksNew = []; 204 | 205 | // Помещаем в него те дела, которые не надо удалять 206 | for (var i = 0; i < mahoweekStorage.tasks.length; i ++) { 207 | if (mahoweekStorage.tasks[i].listId != listId) { 208 | tasksNew.push(mahoweekStorage.tasks[i]); 209 | } 210 | } 211 | 212 | // Заменяем старый массив дел на новый с удаленными делами 213 | mahoweekStorage.tasks = tasksNew; 214 | } 215 | 216 | // Обновляем Хранилище 217 | updateStorage(mahoweekStorage); 218 | 219 | // Запускаем процесс удаления 220 | list.addClass('list--remove'); 221 | 222 | // Удаляем список из доски 223 | setTimeout(function() { 224 | list.remove(); 225 | 226 | // Меняем фавиконку 227 | changeFavicon(); 228 | }, SPEED); 229 | } 230 | }); 231 | 232 | }(jQuery)); 233 | 234 | 235 | // Сортируем вручную списки 236 | //------------------------------------------------------------------------------ 237 | 238 | function sortableList() { 239 | 240 | Sortable.create(document.querySelector('.board__lists'), { 241 | delay: SPEED, 242 | animation: SPEED, 243 | handle: '.list__name', 244 | filter: '.list__input', 245 | preventOnFilter: false, 246 | ghostClass: 'list--ghost', 247 | chosenClass: 'list--chosen', 248 | dragClass: 'list--drag', 249 | forceFallback: true, 250 | fallbackClass: 'list--fallback', 251 | fallbackOnBody: true, 252 | scrollSensitivity: 100, 253 | onChoose: function() { 254 | // Добавляем класс, что выполняется сортировка 255 | LIST_BOARD.addClass('board__lists--drag'); 256 | }, 257 | onEnd: function() { 258 | // Удаляем класс, что выполняется сортировка 259 | LIST_BOARD.removeClass('board__lists--drag'); 260 | }, 261 | onSort: function(event) { 262 | // Парсим Хранилище 263 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 264 | 265 | // Получаем удаленный элемент 266 | var listRemove = mahoweekStorage.lists.splice(event.oldIndex, 1)[0]; 267 | 268 | // Сортируем 269 | mahoweekStorage.lists.splice(event.newIndex, 0, listRemove); 270 | 271 | // Обновляем Хранилище 272 | updateStorage(mahoweekStorage); 273 | } 274 | }); 275 | 276 | } 277 | 278 | 279 | // Форматируем заголовок списка 280 | //------------------------------------------------------------------------------ 281 | 282 | function remakeListName(name) { 283 | 284 | let remakeName = name; 285 | 286 | // Работаем с УРЛ 287 | if (/(http(s)?:\/\/)/i.test(name)) { 288 | if (/https:\/\/mahoweek\.com\/#/.test(name)) { 289 | remakeName = remakeName.replace(/(https:\/\/)(mahoweek\.com\/#)([\S]+[^ ,\.!])/ig, '$2$3'); 290 | } else { 291 | remakeName = remakeName.replace(/(http(s)?:\/\/(www\.)?)([\S]+[^ ,\.!])/ig, '$4'); 292 | } 293 | } 294 | 295 | // Выводим отформатированный заголовок 296 | return remakeName; 297 | 298 | } 299 | 300 | 301 | // Генерируем прогресс списка 302 | //------------------------------------------------------------------------------ 303 | 304 | function makeProgress(id) { 305 | 306 | // Считаем общее кол-во дел 307 | var taskTotal = LIST_BOARD.find('.list[data-id="' + id + '"] .list__tasks .task').length; 308 | 309 | // Считаем кол-во выполненных дел 310 | var taskCompleted = LIST_BOARD.find('.list[data-id="' + id + '"] .task--completed').length; 311 | 312 | // Высчитываем прогресс 313 | if (taskTotal > 0) { 314 | var progress = taskCompleted * 100 / taskTotal / 100; 315 | } else { 316 | var progress = 0; 317 | } 318 | 319 | // Выводим прогресс 320 | LIST_BOARD.find('.list[data-id="' + id + '"] .list__progress').css({ 321 | '-webkit-transform': 'scaleX(' + progress + ')', 322 | 'transform': 'scaleX(' + progress + ')' 323 | }); 324 | 325 | } 326 | 327 | 328 | // Генерируем списки 329 | //------------------------------------------------------------------------------ 330 | 331 | function makeList(id, name, modifier) { 332 | 333 | modifier = modifier === undefined ? '' : modifier; 334 | 335 | // Генерируем код 336 | return '' + 337 | '
' + 338 | '
' + 339 | '
' + 340 | '
' + 341 | remakeListName(name) + 342 | '
' + 343 | '
' + 344 | '' + 349 | '
' + 350 | '
' + 351 | '
' + 352 | '
' + 353 | '
' + 354 | '
' + 355 | '
' + 356 | '
' + 357 | '
' + 358 | '' + 365 | '
' + 366 | '' + 367 | '
' + 368 | '
' + 369 | '
' + 370 | '
' + 371 | '
' + 372 | '
'; 373 | 374 | } 375 | -------------------------------------------------------------------------------- /src/blocks/list/list.less: -------------------------------------------------------------------------------- 1 | // List 2 | //------------------------------------------------------------------------------ 3 | 4 | .list { 5 | position: relative; 6 | margin-bottom: 20px; 7 | padding-bottom: 39px; 8 | overflow-x: auto; 9 | -webkit-overflow-scrolling: touch; 10 | 11 | @media (min-width: 992px) { 12 | overflow-x: initial !important; 13 | } 14 | 15 | &::before { 16 | content: ""; 17 | position: absolute; 18 | top: 0; 19 | bottom: 0; 20 | left: 5px; 21 | width: calc(~"100vw + 544px"); 22 | max-width: 931px; 23 | background-color: @list-bg; 24 | border-radius: (@radius * 2); 25 | 26 | @media (min-width: 768px) { 27 | left: 20px; 28 | max-width: 1017px; 29 | } 30 | 31 | @media (min-width: 992px) { 32 | right: 20px; 33 | width: auto; 34 | max-width: none; 35 | } 36 | } 37 | 38 | &--new { 39 | animation-name: fadeInUp; 40 | animation-duration: @duration; 41 | animation-fill-mode: both; 42 | animation-timing-function: @timing-function; 43 | } 44 | 45 | &--remove { 46 | animation-name: fadeOut; 47 | animation-duration: @duration; 48 | animation-fill-mode: both; 49 | animation-timing-function: @timing-function; 50 | } 51 | 52 | &--chosen { 53 | animation-name: chosen; 54 | animation-duration: @duration; 55 | animation-fill-mode: both; 56 | animation-timing-function: linear; 57 | } 58 | 59 | &--fallback, 60 | &--ghost { 61 | animation-name: none; 62 | } 63 | } 64 | 65 | .list__head { 66 | position: relative; 67 | top: 0; 68 | z-index: 10; 69 | display: flex; 70 | width: calc(~"100vw + 554px"); 71 | max-width: 941px; 72 | padding: 0 5px; 73 | font-size: @font-size-small; 74 | 75 | @media (min-width: 768px) { 76 | max-width: 1057px; 77 | padding-right: 20px; 78 | padding-left: 20px; 79 | } 80 | 81 | @media (min-width: 992px) { 82 | position: sticky; 83 | width: calc(~"100vw"); 84 | max-width: none; 85 | } 86 | 87 | @media (min-width: 1200px) { 88 | width: 1160px; 89 | } 90 | 91 | .list--chosen & { 92 | opacity: 0.5; 93 | transition: @duration @timing-function; 94 | transition-property: opacity; 95 | } 96 | 97 | .list--ghost & { 98 | opacity: 0; 99 | } 100 | } 101 | 102 | .list__wrap { 103 | position: relative; 104 | display: flex; 105 | flex: 1 0 0%; 106 | width: calc(~"100vw - 17px"); 107 | max-width: 370px; 108 | overflow: hidden; 109 | background-color: @list-bg-header; 110 | border-radius: (@radius * 2) 0 0 0; 111 | 112 | @media (min-width: 768px) { 113 | max-width: 456px; 114 | } 115 | 116 | @media (min-width: 992px) { 117 | max-width: calc(~"100vw - 561px - 40px"); 118 | } 119 | 120 | @media (min-width: 1200px) { 121 | width: 559px; 122 | } 123 | 124 | &::before { 125 | content: ""; 126 | position: absolute; 127 | right: 0; 128 | bottom: 0; 129 | left: 0; 130 | height: 2px; 131 | background-color: @list-border; 132 | } 133 | } 134 | 135 | .list__name { 136 | flex: 1 0 0%; 137 | padding: 8px 0 7px 15px; 138 | overflow: hidden; 139 | color: @dark-secondary; 140 | white-space: nowrap; 141 | text-overflow: ellipsis; 142 | } 143 | 144 | .list__input[type=text] { 145 | height: 39px; 146 | margin: -8px 0 -7px; 147 | padding: 8px 0 7px; 148 | color: @dark-secondary; 149 | } 150 | 151 | .list__options { 152 | position: relative; 153 | width: 39px; 154 | } 155 | 156 | .list__trash { 157 | position: absolute; 158 | top: 0; 159 | right: 0; 160 | bottom: 0; 161 | left: 0; 162 | width: 39px; 163 | padding: 0; 164 | fill: @list-trash; 165 | background: none; 166 | border: 0; 167 | transform: scale3d(0.8, 0.8, 1); 168 | visibility: hidden; 169 | cursor: pointer; 170 | opacity: 0; 171 | transition: @duration @timing-function; 172 | transition-property: visibility, opacity, transform; 173 | 174 | body:not(.mobile) &:hover { 175 | fill: @list-trash-hover; 176 | } 177 | 178 | body:not(.mobile) .list__wrap:hover & { 179 | transform: scale3d(1, 1, 1); 180 | visibility: visible; 181 | opacity: 1; 182 | transition-delay: (@duration * 2); 183 | } 184 | 185 | .mobile & { 186 | transform: scale3d(1, 1, 1); 187 | visibility: visible; 188 | opacity: 1; 189 | } 190 | 191 | .board__lists--drag & { 192 | transform: scale3d(0.8, 0.8, 1) !important; 193 | visibility: hidden; 194 | opacity: 0 !important; 195 | transition-delay: 0s !important; 196 | } 197 | 198 | svg { 199 | position: absolute; 200 | top: 8px; 201 | left: 8px; 202 | width: 25px; 203 | height: 25px; 204 | } 205 | } 206 | 207 | .list__progress { 208 | position: absolute; 209 | bottom: 0; 210 | left: 0; 211 | width: 100%; 212 | height: 2px; 213 | background-image: linear-gradient(90deg, @list-progress-first, @list-progress-second); 214 | transform: scaleX(0); 215 | transform-origin: 0 50%; 216 | transition: (@duration * 2) @timing-function; 217 | transition-property: transform; 218 | } 219 | 220 | .list__grid { 221 | position: relative; 222 | background-color: @grid-bg-header; 223 | border-radius: 0 (@radius * 2) 0 0; 224 | } 225 | 226 | .list__tasks { 227 | position: relative; 228 | width: calc(~"100vw + 554px"); 229 | max-width: 941px; 230 | padding: 0 5px; 231 | 232 | @media (min-width: 768px) { 233 | max-width: 1057px; 234 | padding-right: 20px; 235 | padding-left: 20px; 236 | } 237 | 238 | @media (min-width: 992px) { 239 | width: calc(~"100vw"); 240 | max-width: none; 241 | } 242 | 243 | @media (min-width: 1200px) { 244 | width: 1160px; 245 | } 246 | 247 | .board__lists--drag & { 248 | z-index: 5; 249 | margin-bottom: -40px; 250 | padding-bottom: 39px; 251 | } 252 | 253 | .list--chosen & { 254 | opacity: 0.5; 255 | transition: @duration @timing-function; 256 | transition-property: opacity; 257 | } 258 | 259 | .list--ghost & { 260 | opacity: 0; 261 | } 262 | } 263 | 264 | .list__foot { 265 | position: relative; 266 | width: calc(~"100vw + 554px"); 267 | max-width: 941px; 268 | margin-bottom: -39px; 269 | padding: 0 5px; 270 | 271 | @media (min-width: 768px) { 272 | max-width: 1057px; 273 | padding-right: 20px; 274 | padding-left: 20px; 275 | } 276 | 277 | @media (min-width: 992px) { 278 | width: calc(~"100vw"); 279 | max-width: none; 280 | } 281 | 282 | @media (min-width: 1200px) { 283 | width: 1160px; 284 | } 285 | 286 | .list--chosen & { 287 | opacity: 0.5; 288 | transition: @duration @timing-function; 289 | transition-property: opacity; 290 | } 291 | 292 | .list--ghost & { 293 | opacity: 0; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/blocks/logo/logo.less: -------------------------------------------------------------------------------- 1 | // Logo 2 | //------------------------------------------------------------------------------ 3 | 4 | .logo { 5 | color: @logo-light; 6 | 7 | h1 { 8 | padding: 0 13px; 9 | 10 | &::before { 11 | content: ""; 12 | position: absolute; 13 | top: 12px; 14 | left: -1px; 15 | width: 7px; 16 | height: 7px; 17 | background-color: @logo-pea; 18 | border-radius: 50%; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/blocks/menu/menu.less: -------------------------------------------------------------------------------- 1 | // Menu 2 | //------------------------------------------------------------------------------ 3 | 4 | .menu { 5 | display: flex; 6 | margin: 0 -10px 0 0; 7 | padding-left: 0; 8 | list-style-type: none; 9 | 10 | @media (min-width: 768px) { 11 | margin-right: -15px; 12 | } 13 | } 14 | 15 | .menu__item { 16 | margin-left: 1px; 17 | 18 | &:first-child { 19 | margin-left: 0; 20 | } 21 | 22 | .btn { 23 | position: relative; 24 | top: 0 !important; 25 | display: block; 26 | margin-right: 5px; 27 | margin-left: 4px; 28 | width: 30px; 29 | min-width: 0; 30 | height: 30px; 31 | padding: 0; 32 | color: @light-primary !important; 33 | fill: @light-primary !important; 34 | background-color: transparent !important; 35 | border-radius: 0; 36 | } 37 | } 38 | 39 | .menu__icon { 40 | position: absolute; 41 | top: 1px; 42 | left: 1px; 43 | width: 29px; 44 | height: 29px; 45 | 46 | body:not(.mobile) & { 47 | fill-opacity: 0.7; 48 | } 49 | 50 | body:not(.mobile) .btn:hover & { 51 | fill-opacity: 1; 52 | } 53 | } 54 | 55 | .menu__ava { 56 | position: absolute; 57 | top: 3px; 58 | left: 3px; 59 | display: none; 60 | width: 25px; 61 | height: 25px; 62 | background-color: @light-primary; 63 | background-repeat: no-repeat; 64 | background-position: 50% 50%; 65 | background-size: 25px; 66 | border-radius: 50%; 67 | transition: @duration @timing-function; 68 | transition-property: box-shadow; 69 | 70 | .menu__item--profile & { 71 | display: block; 72 | } 73 | 74 | &[data-sync="process"] { 75 | box-shadow: inset 0 0 0 25px fade(@sync-process, 50%); 76 | transition-delay: 5s; 77 | } 78 | 79 | &[data-sync="fail"] { 80 | box-shadow: inset 0 0 0 25px fade(@sync-fail, 75%); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/blocks/share/share.less: -------------------------------------------------------------------------------- 1 | // Share 2 | //------------------------------------------------------------------------------ 3 | 4 | .share { 5 | max-width: 280px; 6 | margin: 20px auto; 7 | 8 | @media (min-width: 768px) { 9 | max-width: none; 10 | } 11 | 12 | .ya-share2 { 13 | min-height: 34px; 14 | 15 | &__container { 16 | margin-left: -5px; 17 | margin-right: -5px; 18 | margin-bottom: -10px; 19 | text-align: center; 20 | } 21 | 22 | &__item { 23 | margin: 0 5px 10px !important; 24 | } 25 | 26 | &__link { 27 | position: relative; 28 | 29 | body:not(.mobile) &:hover { 30 | opacity: 0.9 !important; 31 | } 32 | 33 | body:not(.mobile) &:active { 34 | top: 1px; 35 | } 36 | } 37 | 38 | &__icon:active { 39 | box-shadow: none; 40 | } 41 | 42 | &__badge { 43 | padding: 5px; 44 | border-radius: (@radius * 4.25); 45 | } 46 | 47 | &__counter:before { 48 | top: 7px; 49 | bottom: 7px; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/blocks/sync/sync.js: -------------------------------------------------------------------------------- 1 | // Sync 2 | //------------------------------------------------------------------------------ 3 | 4 | // Задаем конфигурацию Firebase 5 | var firebaseConfig = { 6 | apiKey: 'AIzaSyBzWqGiMDErDxB_kUOO8-KYABo0_SYNap8', 7 | authDomain: 'mahoweek.com', 8 | databaseURL: 'https://mahoweek-8c3db.firebaseio.com' 9 | }; 10 | 11 | // Инициализируем Firebase 12 | firebase.initializeApp(firebaseConfig); 13 | 14 | 15 | // Работаем с идентификацией пользователя 16 | //------------------------------------------------------------------------------ 17 | 18 | (function($) { 19 | 20 | // Если пользователь авторизован 21 | if (localStorage.getItem('mwAuth')) { 22 | // Показываем панель пользователя 23 | $('.sync__auth').addClass('sync__auth--hidden'); 24 | $('.sync__user').addClass('sync__user--show'); 25 | 26 | // Показываем индикатор обновления 27 | $('.sync__ava, .menu__ava').attr('data-sync', 'process'); 28 | 29 | // Получаем данные о пользователе из Firebase 30 | firebase.auth().onAuthStateChanged(function(user) { 31 | // Если пользователь идентифицирован 32 | if (user) { 33 | // Вставляем аватар, имя и провайдера 34 | $('.sync__ava, .menu__ava').css('background-image', 'url(' + user.photoURL + ')'); 35 | $('.sync__name').text(user.displayName); 36 | $('.sync__ava').attr('data-provider', user.providerData[0].providerId); 37 | 38 | // Показываем пользователя в меню 39 | $('.menu__item--settings').addClass('menu__item--profile'); 40 | 41 | // Считаем кол-во синхронизаций изменений 42 | var syncCount = 0; 43 | 44 | // Подключаемся к БД и синхронизируем изменения 45 | firebase.database().ref('/users/' + user.uid + '/database').on('value', function(data) { 46 | syncCount++; 47 | 48 | // Если в БД дата обновления новее, чем в Хранилище 49 | if (data.val().settings.updatedTime > JSON.parse(localStorage.getItem('mwStorage')).settings.updatedTime) { 50 | // Генерируем данные для Хранилища из БД 51 | var storageData = { 52 | lists: data.val().lists !== undefined ? data.val().lists : [], 53 | tasks: data.val().tasks !== undefined ? data.val().tasks : [], 54 | settings: data.val().settings 55 | } 56 | 57 | // Обновляем Хранилище 58 | localStorage.setItem('mwStorage', JSON.stringify(storageData)); 59 | 60 | // Перезагружаем страницу 61 | window.location.reload(true); 62 | 63 | // Если это первая синхронизация 64 | } else if (syncCount == 1) { 65 | // Вставляем дату последнего изменения 66 | $('.sync__updated span').text(makeDate(data.val().settings.updatedTime, 'full')); 67 | 68 | // Показываем индикатор по-умолчанию 69 | $('.sync__ava, .menu__ava').attr('data-sync', 'default'); 70 | 71 | // Загружаем списки 72 | loadList(); 73 | 74 | // Загружаем дела 75 | loadTask(); 76 | 77 | // Слушаем состояние подключения к сети 78 | // firebase.database().ref('.info/connected').on('value', function(snap) { 79 | // // Если в сети 80 | // if (snap.val() === true) { 81 | // // Скрываем если было сообщение об ошибке 82 | // $('.sync__message').html('').hide(); 83 | 84 | // // Показываем индикатор по-умолчанию 85 | // $('.sync__ava, .menu__ava').attr('data-sync', 'default'); 86 | 87 | // // Если не в сети 88 | // } else { 89 | // // Показываем индикатор краха 90 | // $('.sync__ava, .menu__ava').attr('data-sync', 'fail'); 91 | 92 | // // Выводим ошибку в сообщении 93 | // $('.sync__message').html('Проблема с сетью').show(); 94 | // } 95 | // }); 96 | } 97 | }); 98 | 99 | // Если пользователь не идентифицирован 100 | } else { 101 | // Очищаем локальное хранилище полностью 102 | localStorage.clear(); 103 | 104 | // Добавляем хеш для вывода прощального сообщения 105 | window.location.replace('#bye'); 106 | 107 | // Перезагружаем страницу 108 | window.location.reload(true); 109 | } 110 | }); 111 | 112 | // Если пользователь не авторизован 113 | } else { 114 | // Загружаем списки 115 | loadList(); 116 | 117 | // Загружаем дела 118 | loadTask(); 119 | } 120 | 121 | }(jQuery)); 122 | 123 | 124 | // Работаем с аутентификацией пользователя 125 | //------------------------------------------------------------------------------ 126 | 127 | (function($) { 128 | 129 | // Логиним пользователя 130 | $('.js-login-sync').on('click', function() { 131 | var isThis = $(this); 132 | 133 | // Скрываем если было сообщение об ошибке 134 | $('.sync__message').html('').hide(); 135 | 136 | // Показываем процесс аутентификации 137 | $('.sync__auth').addClass('sync__auth--load'); 138 | 139 | // Если пользователь не идентифицирован 140 | if (!firebase.auth().currentUser) { 141 | // Определяем способы аутентификации 142 | if (isThis.attr('data-provider') == 'google') { 143 | var provider = new firebase.auth.GoogleAuthProvider(); 144 | provider.addScope('email'); 145 | } else if (isThis.attr('data-provider') == 'facebook') { 146 | var provider = new firebase.auth.FacebookAuthProvider(); 147 | provider.addScope('email'); 148 | } else if (isThis.attr('data-provider') == 'twitter') { 149 | var provider = new firebase.auth.TwitterAuthProvider(); 150 | } 151 | 152 | // Если вход выполняется не с мобильного устройства 153 | if (!MOBILE) { 154 | // Открываем отдельное окно с аутентификацией 155 | firebase.auth().signInWithPopup(provider).then(function(result) { 156 | // Проверяем пользователя 157 | checkUser(result.user.uid); 158 | }).catch(function(error) { 159 | // Скрываем процесс аутентификации 160 | $('.sync__auth').removeClass('sync__auth--load'); 161 | 162 | // Выводим ошибку в консоли и в сообщении 163 | console.error(error.code + ': ' + error.message); 164 | $('.sync__message').html(error.code + ': ' + error.message).show(); 165 | 166 | // Добавляем данные в Метрику 167 | yaCounter43856389.reachGoal('ya-firebase-fail'); 168 | }); 169 | 170 | // Если вход выполняется с мобильного устройства 171 | } else { 172 | // Открываем аутентификацию в текущем окне 173 | firebase.auth().signInWithRedirect(provider); 174 | } 175 | 176 | // Если вдруг пользователь был как-то криво авторизован 177 | } else { 178 | // Разогиниваем пользователя 179 | $('.js-logout-sync').trigger('click'); 180 | } 181 | }); 182 | 183 | // При редиректе после аутентификации 184 | firebase.auth().getRedirectResult().then(function(result) { 185 | // Если данные от провайдера получены 186 | if (result.credential) { 187 | // Проверяем пользователя 188 | checkUser(result.user.uid); 189 | } 190 | }).catch(function(error) { 191 | // Скрываем процесс аутентификации 192 | $('.sync__auth').removeClass('sync__auth--load'); 193 | 194 | // Выводим ошибку в консоли и в сообщении 195 | console.error(error.code + ': ' + error.message); 196 | $('.sync__message').html(error.code + ': ' + error.message).show(); 197 | 198 | // Добавляем данные в Метрику 199 | yaCounter43856389.reachGoal('ya-firebase-fail'); 200 | }); 201 | 202 | // Разогиниваем пользователя 203 | $('.js-logout-sync').on('click', function() { 204 | // Если пользователь идентифицирован 205 | if (firebase.auth().currentUser) { 206 | // Скрываем если было сообщение об ошибке 207 | $('.sync__message').html('').hide(); 208 | 209 | // Разлогиниваем 210 | firebase.auth().signOut().then(function() { 211 | // Очищаем локальное хранилище полностью 212 | localStorage.clear(); 213 | 214 | // Добавляем хеш для вывода прощального сообщения 215 | window.location.replace('#bye'); 216 | 217 | // Перезагружаем страницу 218 | window.location.reload(true); 219 | }).catch(function(error) { 220 | // Выводим ошибку в консоли и в сообщении 221 | console.error(error.code + ': ' + error.message); 222 | $('.sync__message').html(error.code + ': ' + error.message).show(); 223 | 224 | // Добавляем данные в Метрику 225 | yaCounter43856389.reachGoal('ya-firebase-fail'); 226 | }); 227 | } 228 | }); 229 | 230 | }(jQuery)); 231 | 232 | 233 | // Синхронизируем Хранилище и БД вручную 234 | //------------------------------------------------------------------------------ 235 | 236 | (function($) { 237 | 238 | $('.js-get-sync').on('click', function() { 239 | // Если пользователь идентифицирован 240 | // и есть метка о авторизации 241 | if (firebase.auth().currentUser && localStorage.getItem('mwAuth')) { 242 | // Скрываем если было сообщение об ошибке 243 | $('.sync__message').html('').hide(); 244 | 245 | // Показываем индикатор обновления 246 | $('.sync__ava, .menu__ava').attr('data-sync', 'process'); 247 | $('.sync__hand').addClass('sync__hand--spin'); 248 | 249 | // Подключаемся к БД единоразово 250 | return firebase.database().ref('/users/' + firebase.auth().currentUser.uid + '/database').once('value').then(function(data) { 251 | // Парсим Хранилище 252 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 253 | 254 | // Если в БД дата обновления новее или такая же как в Хранилище 255 | if (data.val().settings.updatedTime >= mahoweekStorage.settings.updatedTime) { 256 | // Генерируем данные для Хранилища из БД 257 | var storageData = { 258 | lists: data.val().lists !== undefined ? data.val().lists : [], 259 | tasks: data.val().tasks !== undefined ? data.val().tasks : [], 260 | settings: data.val().settings 261 | } 262 | 263 | // Обновляем Хранилище 264 | localStorage.setItem('mwStorage', JSON.stringify(storageData)); 265 | 266 | // Добавляем данные в Метрику 267 | yaCounter43856389.reachGoal('ya-manually-sync'); 268 | 269 | // Перезагружаем страницу 270 | window.location.reload(true); 271 | 272 | // Если в БД старая дата обновления 273 | } else { 274 | // Отправляем изменения в БД из Хранилища 275 | firebase.database().ref('users/' + firebase.auth().currentUser.uid + '/database').set({ 276 | "lists": mahoweekStorage.lists, 277 | "tasks": mahoweekStorage.tasks, 278 | "settings": mahoweekStorage.settings 279 | }).then(function() { 280 | // Показываем индикатор по-умолчанию 281 | $('.sync__ava, .menu__ava').attr('data-sync', 'default'); 282 | 283 | // Добавляем данные в Метрику 284 | yaCounter43856389.reachGoal('ya-manually-sync'); 285 | 286 | // Перезагружаем страницу 287 | window.location.reload(true); 288 | }).catch(function(error) { 289 | // Показываем индикатор краха 290 | $('.sync__ava, .menu__ava').attr('data-sync', 'fail'); 291 | $('.sync__hand').removeClass('sync__hand--spin'); 292 | 293 | // Выводим ошибку в консоли и в сообщении 294 | console.error(error.code + ': ' + error.message); 295 | $('.sync__message').html(error.code + ': ' + error.message).show(); 296 | 297 | // Добавляем данные в Метрику 298 | yaCounter43856389.reachGoal('ya-firebase-fail'); 299 | }); 300 | } 301 | }).catch(function(error) { 302 | // Показываем индикатор краха 303 | $('.sync__ava, .menu__ava').attr('data-sync', 'fail'); 304 | $('.sync__hand').removeClass('sync__hand--spin'); 305 | 306 | // Выводим ошибку в консоли и в сообщении 307 | console.error(error.code + ': ' + error.message); 308 | $('.sync__message').html(error.code + ': ' + error.message).show(); 309 | 310 | // Добавляем данные в Метрику 311 | yaCounter43856389.reachGoal('ya-firebase-fail'); 312 | }); 313 | 314 | // Если пользователь не идентифицирован 315 | } else { 316 | // Разогиниваем пользователя 317 | $('.js-logout-sync').trigger('click'); 318 | } 319 | }); 320 | 321 | }(jQuery)); 322 | 323 | 324 | // Работаем с авторизацией пользователя 325 | //------------------------------------------------------------------------------ 326 | 327 | function checkUser(uid) { 328 | 329 | // Подключаемся к БД единоразово 330 | return firebase.database().ref('/users/' + uid + '/database').once('value').then(function(data) { 331 | // Если пользователя в БД нет 332 | if (data.val() === null) { 333 | // Парсим Хранилище 334 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 335 | 336 | // Создаем пользователя в БД и заносим данные из Хранилища 337 | firebase.database().ref('users/' + uid + '/database').set({ 338 | "lists": mahoweekStorage.lists, 339 | "tasks": mahoweekStorage.tasks, 340 | "settings": mahoweekStorage.settings 341 | }).then(function() { 342 | // Ставим метку, что пользователь успешно авторизовался 343 | localStorage.setItem('mwAuth', true); 344 | 345 | // Перезагружаем страницу 346 | window.location.reload(true); 347 | }).catch(function(error) { 348 | // Выводим ошибку в консоли и в сообщении 349 | console.error(error.code + ': ' + error.message); 350 | $('.sync__message').html(error.code + ': ' + error.message).show(); 351 | 352 | // Добавляем данные в Метрику 353 | yaCounter43856389.reachGoal('ya-firebase-fail'); 354 | }); 355 | 356 | // Если пользователь в БД есть 357 | } else { 358 | // Генерируем данные для Хранилища из БД 359 | var storageData = { 360 | lists: data.val().lists !== undefined ? data.val().lists : [], 361 | tasks: data.val().tasks !== undefined ? data.val().tasks : [], 362 | settings: data.val().settings 363 | } 364 | 365 | // Обновляем Хранилище 366 | localStorage.setItem('mwStorage', JSON.stringify(storageData)); 367 | 368 | // Ставим метку, что пользователь успешно авторизовался 369 | localStorage.setItem('mwAuth', true); 370 | 371 | // Редиректим на главную 372 | window.history.replaceState(null, null, '/'); 373 | window.location.reload(true); 374 | } 375 | }).catch(function(error) { 376 | // Выводим ошибку в консоли и в сообщении 377 | console.error(error.code + ': ' + error.message); 378 | $('.sync__message').html(error.code + ': ' + error.message).show(); 379 | 380 | // Добавляем данные в Метрику 381 | yaCounter43856389.reachGoal('ya-firebase-fail'); 382 | }); 383 | 384 | } 385 | -------------------------------------------------------------------------------- /src/blocks/sync/sync.less: -------------------------------------------------------------------------------- 1 | // Sync 2 | //------------------------------------------------------------------------------ 3 | 4 | .sync { 5 | margin-top: 25px; 6 | margin-bottom: 30px; 7 | 8 | @media (min-width: 768px) { 9 | margin-top: 55px; 10 | margin-bottom: 60px; 11 | } 12 | } 13 | 14 | .sync__auth { 15 | position: relative; 16 | 17 | &--hidden { 18 | display: none; 19 | } 20 | 21 | &::before { 22 | content: ""; 23 | position: absolute; 24 | top: 50%; 25 | left: 50%; 26 | z-index: 2; 27 | width: 30px; 28 | height: 30px; 29 | margin: -50px 0 0 -15px; 30 | border: 2px solid @dark-primary; 31 | border-right-color: fade(@dark-primary, 30%); 32 | border-bottom-color: fade(@dark-primary, 30%); 33 | border-radius: 50%; 34 | visibility: hidden; 35 | visibility: visible; 36 | opacity: 0; 37 | animation-name: spin; 38 | animation-duration: (@duration * 3); 39 | animation-timing-function: linear; 40 | animation-iteration-count: infinite; 41 | animation-fill-mode: both; 42 | } 43 | 44 | &::after { 45 | content: ""; 46 | position: absolute; 47 | top: -39px; 48 | right: 0; 49 | bottom: 0; 50 | left: 0; 51 | z-index: 1; 52 | background-color: @light-primary; 53 | visibility: hidden; 54 | opacity: 0; 55 | transition: @duration @timing-function; 56 | transition-property: opacity, visibility; 57 | } 58 | 59 | &--load { 60 | &::before { 61 | visibility: visible; 62 | opacity: 1; 63 | } 64 | 65 | &::after { 66 | visibility: visible; 67 | opacity: 0.97; 68 | } 69 | } 70 | } 71 | 72 | .sync__btn { 73 | display: flex; 74 | flex-wrap: wrap; 75 | justify-content: center; 76 | 77 | .btn[data-provider="google"] { 78 | min-width: 44px; 79 | margin: 5px; 80 | color: @light-primary; 81 | fill: @light-primary; 82 | background-color: #E15B4D; 83 | 84 | body:not(.mobile) &:hover { 85 | background-color: darken(#E15B4D, 5%); 86 | } 87 | 88 | svg { 89 | width: 30px; 90 | height: 30px; 91 | margin: -4px -6px -9px -3px; 92 | } 93 | } 94 | 95 | .btn[data-provider="facebook"] { 96 | min-width: 44px; 97 | margin: 5px; 98 | color: @light-primary; 99 | fill: @light-primary; 100 | background-color: #3b5998; 101 | 102 | body:not(.mobile) &:hover { 103 | background-color: darken(#3b5998, 5%); 104 | } 105 | 106 | svg { 107 | width: 30px; 108 | height: 30px; 109 | margin: -4px -6px -9px -9px; 110 | } 111 | } 112 | 113 | .btn[data-provider="twitter"] { 114 | min-width: 44px; 115 | margin: 5px; 116 | color: @light-primary; 117 | fill: @light-primary; 118 | background-color: #1da1f3; 119 | 120 | body:not(.mobile) &:hover { 121 | background-color: darken(#1da1f3, 5%); 122 | } 123 | 124 | svg { 125 | width: 30px; 126 | height: 30px; 127 | margin: -4px -3px -9px -6px; 128 | } 129 | } 130 | } 131 | 132 | .sync__user { 133 | display: none; 134 | flex-wrap: wrap; 135 | justify-content: center; 136 | align-items: center; 137 | padding-top: 5px; 138 | text-align: center; 139 | 140 | &--show { 141 | display: flex; 142 | } 143 | 144 | .btn { 145 | min-width: 6px; 146 | } 147 | } 148 | 149 | .sync__hand { 150 | animation-name: none; 151 | animation-duration: (@duration * 3); 152 | animation-timing-function: linear; 153 | animation-iteration-count: infinite; 154 | animation-fill-mode: both; 155 | 156 | &--spin { 157 | animation-name: spin; 158 | } 159 | 160 | svg { 161 | display: block; 162 | width: 28px; 163 | height: 28px; 164 | margin: -2px -12px; 165 | } 166 | } 167 | 168 | .sync__logout svg { 169 | display: block; 170 | width: 25px; 171 | height: 25px; 172 | margin: -0.5px -11.5px -0.5px -9.5px; 173 | } 174 | 175 | .sync__ava { 176 | position: relative; 177 | width: 75px; 178 | height: 75px; 179 | margin: 0 25px; 180 | background-color: @light-secondary; 181 | background-repeat: no-repeat; 182 | background-position: 50% 50%; 183 | background-size: cover; 184 | border-radius: 50%; 185 | transition: @duration @timing-function; 186 | transition-property: box-shadow; 187 | 188 | &[data-sync="process"] { 189 | box-shadow: inset 0 0 0 75px fade(@sync-process, 50%); 190 | transition-delay: 5s; 191 | } 192 | 193 | &[data-sync="fail"] { 194 | box-shadow: inset 0 0 0 75px fade(@sync-fail, 75%); 195 | } 196 | } 197 | 198 | .sync__provider { 199 | position: absolute; 200 | right: -2px; 201 | bottom: -2px; 202 | display: none; 203 | box-sizing: content-box; 204 | width: 25px; 205 | height: 25px; 206 | overflow: hidden; 207 | fill: @light-primary; 208 | background-color: @light-secondary; 209 | border: 1px solid @light-primary; 210 | border-radius: 50%; 211 | 212 | &--google { 213 | background-color: #E15B4D; 214 | 215 | [data-provider="google.com"] & { 216 | display: block; 217 | } 218 | 219 | svg { 220 | margin-right: -5px; 221 | } 222 | } 223 | 224 | &--facebook { 225 | background-color: #3b5998; 226 | 227 | [data-provider="facebook.com"] & { 228 | display: block; 229 | } 230 | } 231 | 232 | &--twitter { 233 | background-color: #1da1f3; 234 | 235 | [data-provider="twitter.com"] & { 236 | display: block; 237 | } 238 | } 239 | 240 | svg { 241 | width: 25px; 242 | height: 25px; 243 | } 244 | } 245 | 246 | .sync__name { 247 | width: 100%; 248 | margin-top: 20px; 249 | font-weight: bold; 250 | } 251 | 252 | .sync__updated { 253 | .small; 254 | width: 100%; 255 | margin-top: 15px; 256 | color: @dark-secondary; 257 | } 258 | 259 | .sync__message { 260 | display: none; 261 | width: 100%; 262 | max-width: 390px; 263 | margin: 20px auto; 264 | padding: 15px; 265 | text-align: center; 266 | word-wrap: break-word; 267 | border-radius: @radius; 268 | 269 | &--fail { 270 | color: @sync-fail; 271 | background-color: lighten(@sync-fail, 30%); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/blocks/task/task.js: -------------------------------------------------------------------------------- 1 | // Task 2 | //------------------------------------------------------------------------------ 3 | 4 | // Выводим дела в списки 5 | //------------------------------------------------------------------------------ 6 | 7 | function loadTask() { 8 | 9 | // Вычисляем последний момент прошедшей недели 10 | var dayNumber = new Date().getDay(); 11 | 12 | if (dayNumber == 0) { 13 | dayNumber = 7; 14 | } 15 | 16 | var newDate = new Date(); 17 | newDate.setDate(newDate.getDate() - dayNumber); 18 | newDate.setHours(23, 59, 59, 999); 19 | 20 | // Итак, последний момент прошедшей недели 21 | var lastMomentLastWeek = newDate.getTime(); 22 | 23 | // Готовим новый массив для дел 24 | var tasksNew = []; 25 | 26 | // Парсим Хранилище 27 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 28 | 29 | // Пробегаемся по каждому делу 30 | for (var i = 0; i < mahoweekStorage.tasks.length; i ++) { 31 | // Если включено удаление выполненных дел, 32 | // а дело было реально выполнено и его дата выполнения совпадает с условием 33 | if (mahoweekStorage.settings.deleteCompletedTasks && mahoweekStorage.tasks[i].completed && mahoweekStorage.tasks[i].completedTime <= lastMomentLastWeek) { 34 | // Переходим к следующей итерации 35 | continue; 36 | } 37 | 38 | // Помещаем в новый массив дела, которые остались 39 | tasksNew.push(mahoweekStorage.tasks[i]); 40 | 41 | // Заносим дело в свой список 42 | LIST_BOARD.find('.list[data-id="' + mahoweekStorage.tasks[i].listId + '"] .list__tasks').append(makeTask(mahoweekStorage.tasks[i].id, mahoweekStorage.tasks[i].name, '', mahoweekStorage.tasks[i].completed, mahoweekStorage.tasks[i].markers)); 43 | 44 | // Находим это дело 45 | var task = LIST_BOARD.find('.task[data-id="' + mahoweekStorage.tasks[i].id + '"]'); 46 | 47 | // Изменяем стиль статуса дела 48 | changeStyleTaskStatus(task); 49 | } 50 | 51 | // Пробегаемся по каждому списку 52 | for (var i = 0; i < mahoweekStorage.lists.length; i ++) { 53 | // Рассчитываем прогресс выполнения списка 54 | makeProgress(mahoweekStorage.lists[i].id); 55 | 56 | // Включаем сортировку дел 57 | sortableTask(document.querySelectorAll('.list__tasks')[i]); 58 | } 59 | 60 | // Если массив дел изменился 61 | if (mahoweekStorage.tasks.length != tasksNew.length) { 62 | // Заменяем старый массив дел на новый 63 | mahoweekStorage.tasks = tasksNew; 64 | 65 | // Обновляем Хранилище 66 | updateStorage(mahoweekStorage); 67 | } 68 | 69 | // Меняем фавиконку 70 | changeFavicon(); 71 | 72 | // Показываем содержимое доски 73 | $('body').addClass('ready'); 74 | 75 | } 76 | 77 | 78 | // Фокусируем поле добавления дела 79 | //------------------------------------------------------------------------------ 80 | 81 | (function($) { 82 | 83 | // Если поле добавления в фокусе 84 | LIST_BOARD.on('focusin', '.js-add-task', function() { 85 | var isThis = $(this); 86 | 87 | // Скроллим список к началу 88 | // и запрещаем скроллиться 89 | isThis.parents('.list').scrollLeft(0).css({ 90 | 'overflow-x': 'hidden', 91 | '-webkit-overflow-scrolling': 'auto' 92 | }); 93 | }); 94 | 95 | // Если поле добавления не в фокусе 96 | LIST_BOARD.on('focusout', '.js-add-task', function() { 97 | var isThis = $(this); 98 | 99 | // Возвращаем списку скролл 100 | isThis.parents('.list').css({ 101 | 'overflow-x': '', 102 | '-webkit-overflow-scrolling': '' 103 | }); 104 | }); 105 | 106 | }(jQuery)); 107 | 108 | 109 | // Добавляем дело 110 | //------------------------------------------------------------------------------ 111 | 112 | (function($) { 113 | 114 | LIST_BOARD.on('input change', '.js-add-task', function(event) { 115 | let isThis = $(this); 116 | 117 | // Получаем текст дела 118 | let taskName = isThis.val(); 119 | 120 | // Ставим или убираем метку о заполнении поля 121 | if (taskName !== '') { 122 | isThis.parents('.task').addClass('task--filled'); 123 | } else { 124 | isThis.parents('.task').removeClass('task--filled'); 125 | } 126 | 127 | // Если был нажат Энтер и поле с делом не пустое 128 | // или изменились данные в поле при потере фокуса 129 | if (event.keyCode === 13 && taskName !== '' || event.type === 'change') { 130 | // Получаем список, хеш списка, хеш дела и метку времени 131 | let list = isThis.parents('.list'); 132 | let listId = list.attr('data-id'); 133 | let taskId = makeHash(); 134 | let taskCreatedTime = new Date().getTime(); 135 | 136 | // Парсим Хранилище 137 | let mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 138 | 139 | // Добавляем новое дело 140 | mahoweekStorage.tasks.push({ 141 | id: taskId, 142 | listId: listId, 143 | name: taskName, 144 | createdTime: taskCreatedTime 145 | }); 146 | 147 | // Обновляем Хранилище 148 | updateStorage(mahoweekStorage); 149 | 150 | // Стираем поле ввода добавления дела 151 | // и убираем метку о заполненности поля 152 | isThis.val(''); 153 | isThis.parents('.task').removeClass('task--filled'); 154 | 155 | // Выводим дело в списке 156 | isThis.parents('.list').find('.list__tasks').append(makeTask(taskId, taskName, 'task--new')); 157 | 158 | // Рассчитываем прогресс выполнения списка 159 | makeProgress(listId); 160 | 161 | // Находим созданное дело 162 | let taskNew = isThis.parents('.list').find('.list__tasks .task:last-child'); 163 | 164 | // Если дело добавлялось с меткой 165 | if (localStorage.getItem('mwTempNewTaskMarker')) { 166 | // Добавляем делу метку 167 | taskNew.find('.grid__date[data-date="' + localStorage.getItem('mwTempNewTaskMarker') + '"]').trigger('click'); 168 | 169 | // Удаляем метку для нового дела 170 | localStorage.removeItem('mwTempNewTaskMarker'); 171 | } 172 | 173 | // Удаляем метку о том что это новосозданное дело 174 | setTimeout(function() { 175 | taskNew.removeClass('task--new'); 176 | }, SPEED); 177 | 178 | // Берем данные окна 179 | let win = $(window); 180 | 181 | // Если созданное дело вытесняет за рамки экрана конец списка 182 | if (taskNew.offset().top > win.scrollTop() + win.height() - 30 - 41 - 40) { 183 | // Смещаем позицию прокрутки на высоту строки дела 184 | win.scrollTop(win.scrollTop() + taskNew.outerHeight(true)); 185 | } 186 | 187 | // Добавляем данные в Метрику 188 | yaCounter43856389.reachGoal('ya-add-task'); 189 | } 190 | }); 191 | 192 | }(jQuery)); 193 | 194 | 195 | // Изменяем статус дела 196 | //------------------------------------------------------------------------------ 197 | 198 | (function($) { 199 | 200 | LIST_BOARD.on('click', '.js-completed-task', function() { 201 | var isThis = $(this); 202 | var task = isThis.parents('.task'); 203 | 204 | // Получаем хеш списка, хеш дела, метку о выполнении и дату текущего дня 205 | var listId = task.parents('.list').attr('data-id'); 206 | var taskId = task.attr('data-id'); 207 | var taskCompleted = task.hasClass('task--completed'); 208 | var taskDateToday = task.find('.grid__date--today').attr('data-date'); 209 | 210 | // Парсим Хранилище 211 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 212 | 213 | // Получаем элемент дела в Хранилище 214 | var taskElement = mahoweekStorage.tasks.filter(function(value) { 215 | return value.id == taskId; 216 | }); 217 | 218 | // Получаем индекс дела в Хранилище 219 | var taskIndex = mahoweekStorage.tasks.indexOf(taskElement[0]); 220 | 221 | // Если дело невыполнено 222 | if (!taskCompleted) { 223 | // Переключаем метку выполнения в сетке дат 224 | task.find('.grid__date--today').toggleClass('grid__date--completed'); 225 | 226 | // Переключаем метку выполнения в хранилище 227 | if (task.find('.grid__date--today').hasClass('grid__date--completed')) { 228 | var markerAct = 'add'; 229 | } else { 230 | var markerAct = 'del'; 231 | } 232 | 233 | // Если дело не запланировано в будущем 234 | if (!task.find('.grid__date--today').nextAll('.grid__date--bull').length) { 235 | // Окончательно ставим метку выполнения в сетке дат и в хранилище 236 | task.find('.grid__date--today').addClass('grid__date--completed'); 237 | markerAct = 'add'; 238 | 239 | // Получаем метку времени 240 | var taskCompletedTime = new Date().getTime(); 241 | 242 | // Помечаем дело как выполненное 243 | mahoweekStorage.tasks[taskIndex].completed = 1; 244 | mahoweekStorage.tasks[taskIndex].completedTime = taskCompletedTime; 245 | 246 | // Обновляем дело в списке 247 | task.addClass('task--completed'); 248 | } 249 | 250 | // Если дело было выполнено 251 | } else { 252 | // Убираем метку выполнения из сетки дат 253 | task.find('.grid__date--today').removeClass('grid__date--completed'); 254 | 255 | // Убираем метку выполнения в хранилище 256 | var markerAct = 'del'; 257 | 258 | // Помечаем дело как невыполненное 259 | delete mahoweekStorage.tasks[taskIndex].completed; 260 | delete mahoweekStorage.tasks[taskIndex].completedTime; 261 | 262 | // Обновляем дело в списке 263 | task.removeClass('task--completed'); 264 | } 265 | 266 | // Заносим изменения в массив маркеров 267 | // и если массива маркеров не существовало 268 | if (markerAct == 'add' && !mahoweekStorage.tasks[taskIndex].markers) { 269 | // Создаем массив маркеров и заполняем 270 | mahoweekStorage.tasks[taskIndex].markers = [{ 271 | date: taskDateToday, 272 | completed: 1 273 | }]; 274 | 275 | // Если существовало 276 | } else { 277 | // Проверяем существовала ли уже метка на это число в хранилище 278 | var markerElement = mahoweekStorage.tasks[taskIndex].markers.filter(function(value) { 279 | return value.date == taskDateToday; 280 | }); 281 | 282 | // Если метка существовала 283 | if (markerElement != '') { 284 | // Получаем индекс метки 285 | var markerIndex = mahoweekStorage.tasks[taskIndex].markers.indexOf(markerElement[0]); 286 | 287 | // Если действие добавления метки 288 | if (markerAct == 'add') { 289 | // Добавляем информацию о выполнении 290 | mahoweekStorage.tasks[taskIndex].markers[markerIndex].completed = 1; 291 | 292 | // Если удаления метки 293 | } else { 294 | // Если была установлена метка, 295 | // то удаляем только информацию о выполнении 296 | if (mahoweekStorage.tasks[taskIndex].markers[markerIndex].label) { 297 | delete mahoweekStorage.tasks[taskIndex].markers[markerIndex].completed; 298 | 299 | // Иначе удаляем метку полностью 300 | } else { 301 | mahoweekStorage.tasks[taskIndex].markers.splice(markerIndex, 1); 302 | } 303 | } 304 | 305 | // Если метка не существовала 306 | } else { 307 | // Если действие добавления метки 308 | if (markerAct == 'add') { 309 | // Добавляем метку только с информацией о выполнении 310 | mahoweekStorage.tasks[taskIndex].markers.push({ 311 | date: taskDateToday, 312 | completed: 1 313 | }); 314 | } 315 | } 316 | } 317 | 318 | // Обновляем Хранилище 319 | updateStorage(mahoweekStorage); 320 | 321 | // Изменяем стиль статуса дела 322 | changeStyleTaskStatus(task); 323 | 324 | // Рассчитываем прогресс выполнения списка 325 | makeProgress(listId); 326 | 327 | // Меняем фавиконку 328 | changeFavicon(); 329 | }); 330 | 331 | }(jQuery)); 332 | 333 | 334 | // Сохраняем текст дела при изменении 335 | //------------------------------------------------------------------------------ 336 | 337 | (function($) { 338 | 339 | LIST_BOARD.on('input change', '.js-edit-task', function(event) { 340 | var isThis = $(this); 341 | 342 | // Если был нажат Enter или пропал фокус и были изменения 343 | if (event.keyCode == 13 || event.type == 'change') { 344 | // Получаем хеш и текст дела 345 | var taskId = isThis.parents('.task').attr('data-id'); 346 | var taskName = isThis.val(); 347 | 348 | // Парсим Хранилище 349 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 350 | 351 | // Получаем элемент дела в Хранилище 352 | var taskElement = mahoweekStorage.tasks.filter(function(value) { 353 | return value.id == taskId; 354 | }); 355 | 356 | // Получаем индекс дела в Хранилище 357 | var taskIndex = mahoweekStorage.tasks.indexOf(taskElement[0]); 358 | 359 | // Изменяем текст дела 360 | mahoweekStorage.tasks[taskIndex].name = taskName; 361 | 362 | // Обновляем Хранилище 363 | updateStorage(mahoweekStorage); 364 | 365 | // Убираем фокус с этого поля 366 | isThis.blur(); 367 | } 368 | }); 369 | 370 | }(jQuery)); 371 | 372 | 373 | // Удаляем дело 374 | //------------------------------------------------------------------------------ 375 | 376 | (function($) { 377 | 378 | LIST_BOARD.on('click', '.js-remove-task', function() { 379 | var isThis = $(this); 380 | 381 | // Получаем хеш списка, хеш дела и метку о выполнении 382 | var task = isThis.parents('.task'); 383 | var taskId = task.attr('data-id'); 384 | var taskCompleted = task.hasClass('task--completed'); 385 | var listId = isThis.parents('.list').attr('data-id'); 386 | 387 | // Если дело не выполнено 388 | if (!taskCompleted) { 389 | // Задаем вопрос 390 | var question = confirm('Дело ещё не выполнено, но будет удалено'); 391 | } 392 | 393 | // Если дело выполнено 394 | // или ответом на вопрос было «Да» 395 | if (taskCompleted || question) { 396 | // Парсим Хранилище 397 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 398 | 399 | // Получаем элемент дела в хранилище 400 | var taskElement = mahoweekStorage.tasks.filter(function(value) { 401 | return value.id == taskId; 402 | }); 403 | 404 | // Получаем индекс дела в Хранилище 405 | var taskIndex = mahoweekStorage.tasks.indexOf(taskElement[0]); 406 | 407 | // Удаляем дело 408 | mahoweekStorage.tasks.splice(taskIndex, 1); 409 | 410 | // Обновляем Хранилище 411 | updateStorage(mahoweekStorage); 412 | 413 | // Запускаем процесс удаления 414 | task.addClass('task--remove'); 415 | 416 | // Удаляем дело из списка 417 | setTimeout(function() { 418 | task.remove(); 419 | 420 | // Рассчитываем прогресс выполнения списка 421 | makeProgress(listId); 422 | 423 | // Меняем фавиконку 424 | changeFavicon(); 425 | }, SPEED); 426 | 427 | // Добавляем данные в Метрику 428 | yaCounter43856389.reachGoal('ya-remove-task'); 429 | } 430 | }); 431 | 432 | }(jQuery)); 433 | 434 | 435 | // Сортируем вручную дела 436 | //------------------------------------------------------------------------------ 437 | 438 | function sortableTask(element) { 439 | 440 | Sortable.create(element, { 441 | group: 'group', 442 | delay: SPEED, 443 | animation: (SPEED / 2), 444 | handle: '.task__name', 445 | filter: '.task__input', 446 | preventOnFilter: false, 447 | ghostClass: 'task--ghost', 448 | chosenClass: 'task--chosen', 449 | dragClass: 'task--drag', 450 | forceFallback: true, 451 | fallbackClass: 'task--fallback', 452 | fallbackOnBody: true, 453 | scrollSensitivity: 100, 454 | onChoose: function() { 455 | // Добавляем класс, что выполняется сортировка 456 | LIST_BOARD.addClass('board__lists--drag'); 457 | }, 458 | onEnd: function() { 459 | // Удаляем класс, что выполняется сортировка 460 | LIST_BOARD.removeClass('board__lists--drag'); 461 | }, 462 | onSort: function(event) { 463 | // Получаем хеш листа и хеш текущего дела 464 | var listId = event.item.parentElement.parentElement.attributes['data-id'].value; 465 | var taskId = event.item.attributes['data-id'].value; 466 | 467 | // Парсим Хранилище 468 | var mahoweekStorage = JSON.parse(localStorage.getItem('mwStorage')); 469 | 470 | // Получаем элемент дела в Хранилище 471 | var taskElement = mahoweekStorage.tasks.filter(function(value) { 472 | return value.id == taskId; 473 | }); 474 | 475 | // Получаем индекс дела в Хранилище 476 | var taskIndex = mahoweekStorage.tasks.indexOf(taskElement[0]); 477 | 478 | // Изменяем хеш листа дела 479 | mahoweekStorage.tasks[taskIndex].listId = listId; 480 | 481 | // Получаем удаленный элемент 482 | var taskRemove = mahoweekStorage.tasks.splice(taskIndex, 1)[0]; 483 | 484 | // Если дело помещено в самое начало списка 485 | if (event.newIndex === 0) { 486 | // Сортируем 487 | mahoweekStorage.tasks.splice(0, 0, taskRemove); 488 | 489 | // Если дело помещено в середину списка 490 | } else { 491 | // Получаем хеш дела выше текущего 492 | var taskPrevId = event.item.previousElementSibling.attributes['data-id'].value; 493 | 494 | // Получаем элемент дела в Хранилище 495 | var taskPrevElement = mahoweekStorage.tasks.filter(function(value) { 496 | return value.id == taskPrevId; 497 | }); 498 | 499 | // Получаем индекс дела в Хранилище 500 | var taskPrevIndex = mahoweekStorage.tasks.indexOf(taskPrevElement[0]); 501 | 502 | // Сортируем 503 | mahoweekStorage.tasks.splice((taskPrevIndex + 1), 0, taskRemove); 504 | } 505 | 506 | // Обновляем Хранилище 507 | updateStorage(mahoweekStorage); 508 | }, 509 | onAdd: function(event) { 510 | // Получаем хеш листа в который перенесли дело 511 | var listId = event.to.parentElement.attributes['data-id'].value; 512 | 513 | // Рассчитываем прогресс выполнения списка 514 | makeProgress(listId); 515 | }, 516 | onRemove: function(event) { 517 | // Получаем хеш листа из которого перенесли дело 518 | var listId = event.from.parentElement.attributes['data-id'].value; 519 | 520 | // Рассчитываем прогресс выполнения списка 521 | makeProgress(listId); 522 | } 523 | }); 524 | 525 | } 526 | 527 | 528 | // Работаем со стилем статуса дела 529 | //------------------------------------------------------------------------------ 530 | 531 | function changeStyleTaskStatus(task) { 532 | 533 | // Если у дела есть невыполненные метки за прошедшие дни 534 | // и если дело не было выполненным сегодня, 535 | // а так же если последняя метка точно о пропуске 536 | if (task.find('.grid__date.grid__date--past.grid__date--bull:not(.grid__date--completed)').length && !task.find('.grid__date.grid__date--today.grid__date--completed').length && task.find('.grid__date.grid__date--past.grid__date--bull:not(.grid__date--completed):last').index() > task.find('.grid__date.grid__date--past.grid__date--completed:last').index()) { 537 | // Помечаем дело как невыполненное 538 | task.addClass('task--past'); 539 | } else { 540 | // Размечаем дело как невыполненное 541 | task.removeClass('task--past'); 542 | } 543 | 544 | // Если у дела есть метка на любой будущий день 545 | if (task.find('.grid__date:not(.grid__date--past):not(.grid__date--completed).grid__date--bull').length) { 546 | // Помечаем дело как намеченное 547 | task.addClass('task--bull'); 548 | } else { 549 | // Размечаем дело как намеченное 550 | task.removeClass('task--bull'); 551 | } 552 | 553 | // Если у дела есть метка на сегодняшний день и она невыполнена 554 | if (task.find('.grid__date--today.grid__date--bull:not(.grid__date--completed)').length) { 555 | // Помечаем дело как сегодняшнее 556 | task.addClass('task--today'); 557 | } else { 558 | // Размечаем дело как сегодняшнее 559 | task.removeClass('task--today'); 560 | } 561 | 562 | } 563 | 564 | 565 | // Форматируем текст дела 566 | //------------------------------------------------------------------------------ 567 | 568 | function remakeTaskName(name) { 569 | 570 | let remakeName = name; 571 | 572 | // Работаем с УРЛ 573 | if (/(http(s)?:\/\/)/i.test(name)) { 574 | if (/https:\/\/mahoweek\.com\/#/i.test(name)) { 575 | remakeName = remakeName.replace(/(https:\/\/)(mahoweek\.com\/#)([\S]+[^ ,\.!])/ig, '$2$3'); 576 | } else { 577 | remakeName = remakeName.replace(/(http(s)?:\/\/(www\.)?)([\S]+[^ ,\.!])/ig, '$4'); 578 | } 579 | } 580 | 581 | // Работаем с временем 582 | if (/((2[0-3]|[0-1]\d)[:\.]([0-5]\d))/.test(name)) { 583 | remakeName = remakeName.replace(/(.+)?(\s)?((2[0-3]|[0-1]\d)[:\.]([0-5]\d))(\s)?(.+)?/i, '$4:$5 $1$7').trim(); 584 | 585 | remakeName = remakeName.replace(/(.+)?(\sв)(\s)?(!)?$/i, '$1$4'); 586 | } 587 | 588 | // Работаем с датой 589 | if (/сегодня/i.test(name)) { 590 | remakeName = remakeName.replace(/(.+)?(\s)?(сегодня)(\s)?(.+)?/ig, '$1$5').trim(); 591 | 592 | // Запоминаем метку для нового дела 593 | let date = new Date(); 594 | let time = date.getTime(); 595 | localStorage.setItem('mwTempNewTaskMarker', makeDate(time, 'grid')); 596 | } else if (/завтра/i.test(name)) { 597 | remakeName = remakeName.replace(/(.+)?(\s)?(завтра)(\s)?(.+)?/ig, '$1$5').trim(); 598 | 599 | // Запоминаем метку для нового дела 600 | let date = new Date(); 601 | let time = date.setDate(date.getDate() + 1); 602 | localStorage.setItem('mwTempNewTaskMarker', makeDate(time, 'grid')); 603 | } 604 | 605 | // Работаем с важностью 606 | if (/[!]{1,}/.test(name)) { 607 | remakeName = '' + remakeName + ''; 608 | } 609 | 610 | // Выводим отформатированный текст 611 | return remakeName; 612 | 613 | } 614 | 615 | 616 | // Генерируем дело 617 | //------------------------------------------------------------------------------ 618 | 619 | function makeTask(id, name, modifier, completed, markers) { 620 | 621 | modifier = modifier === undefined ? '' : modifier; 622 | 623 | // Определяем статус дела 624 | if (completed === 1) { 625 | completed = 'task--completed'; 626 | } else { 627 | completed = ''; 628 | } 629 | 630 | // Генерируем код 631 | return '' + 632 | '
' + 633 | '
' + 634 | '
' + 635 | '' + 636 | '
' + 637 | '
' + 638 | remakeTaskName(name) + 639 | '
' + 640 | '
' + 641 | '' + 646 | '
' + 647 | '
' + 648 | '
' + 649 | makeGrid('task', markers) + 650 | '
' + 651 | '
'; 652 | 653 | } 654 | -------------------------------------------------------------------------------- /src/blocks/task/task.less: -------------------------------------------------------------------------------- 1 | // Task 2 | //------------------------------------------------------------------------------ 3 | 4 | .task { 5 | position: relative; 6 | display: flex; 7 | height: 1px; 8 | margin-top: -1px; 9 | transition: @duration @timing-function; 10 | transition-property: font-size; 11 | will-change: font-size; 12 | 13 | &--add { 14 | height: 40px; 15 | font-size: @font-size-small; 16 | color: @dark-secondary; 17 | } 18 | 19 | &--new { 20 | animation-name: fadeInUp; 21 | animation-duration: @duration; 22 | animation-fill-mode: both; 23 | animation-timing-function: @timing-function; 24 | } 25 | 26 | &--remove { 27 | animation-name: fadeOut; 28 | animation-duration: @duration; 29 | animation-fill-mode: both; 30 | animation-timing-function: @timing-function; 31 | } 32 | 33 | &--completed { 34 | font-size: @font-size-small; 35 | } 36 | 37 | &--chosen { 38 | animation-name: chosen; 39 | animation-duration: @duration; 40 | animation-fill-mode: both; 41 | animation-timing-function: linear; 42 | } 43 | 44 | &--fallback { 45 | animation-name: none; 46 | } 47 | 48 | .focus-all &:not(&--add), 49 | .focus-today &--today:not(&--add), 50 | .focus-planned &--bull:not(&--add):not(&--today), 51 | .focus-someday &:not(&--add):not(&--bull):not(&--completed), 52 | .focus-completed &--completed:not(&--add) { 53 | height: 41px; 54 | } 55 | } 56 | 57 | .task__wrap { 58 | display: flex; 59 | flex: 1 0 0%; 60 | width: calc(~"100vw - 17px"); 61 | max-width: 370px; 62 | overflow: hidden; 63 | background-color: @task-bg; 64 | 65 | body:not(.mobile) .board__lists:not(.board__lists--drag) .task:hover & { 66 | background-color: @task-bg-hover; 67 | } 68 | 69 | @media (min-width: 768px) { 70 | max-width: 456px; 71 | } 72 | 73 | @media (min-width: 992px) { 74 | max-width: calc(~"100vw - 561px - 40px"); 75 | } 76 | 77 | @media (min-width: 1200px) { 78 | width: 559px; 79 | } 80 | 81 | .task--add & { 82 | border-radius: 0 0 0 (@radius * 2); 83 | } 84 | 85 | .task--chosen & { 86 | opacity: 0.5; 87 | transition: @duration @timing-function; 88 | transition-property: opacity; 89 | } 90 | 91 | .task--ghost & { 92 | opacity: 0; 93 | } 94 | } 95 | 96 | .task__status { 97 | position: relative; 98 | width: 39px; 99 | border-top: 1px solid transparent; 100 | border-bottom: 1px solid transparent; 101 | 102 | .task--add & { 103 | height: 40px; 104 | border-bottom-width: 0; 105 | } 106 | } 107 | 108 | .task__check { 109 | position: absolute; 110 | top: -1px; 111 | right: 0; 112 | bottom: 0; 113 | left: 0; 114 | width: 100%; 115 | background: none; 116 | border: 0; 117 | cursor: pointer; 118 | 119 | &::before { 120 | content: ""; 121 | position: absolute; 122 | z-index: 2; 123 | top: 13px; 124 | left: 14px; 125 | width: 15px; 126 | height: 15px; 127 | border: 2px solid; 128 | border-color: @task-pea; 129 | border-radius: 50%; 130 | transition: @duration @timing-function; 131 | transition-property: transform; 132 | 133 | .task--past & { 134 | border-color: @task-pea-past; 135 | } 136 | 137 | .task--bull & { 138 | background-color: @task-pea; 139 | border-color: @task-pea; 140 | } 141 | 142 | .task--today & { 143 | background-color: @task-pea-today; 144 | border-color: @task-pea-today; 145 | } 146 | 147 | .task--completed & { 148 | background-color: @task-pea-complete; 149 | border-color: @task-pea-complete; 150 | transform: scale3d(0.4666, 0.4666, 1); 151 | } 152 | } 153 | 154 | body:not(.mobile) &:hover::before { 155 | border-color: @task-pea-hover; 156 | } 157 | 158 | body:not(.mobile) .task--past &:hover::before { 159 | border-color: @task-pea-past-hover; 160 | } 161 | 162 | body:not(.mobile) .task--bull &:hover::before { 163 | background-color: @task-pea-hover; 164 | border-color: @task-pea-hover; 165 | } 166 | 167 | body:not(.mobile) .task--today &:hover::before { 168 | background-color: @task-pea-today-hover; 169 | border-color: @task-pea-today-hover; 170 | } 171 | 172 | body:not(.mobile) .task--completed &:hover::before { 173 | background-color: @task-pea-complete-hover; 174 | border-color: @task-pea-complete-hover; 175 | } 176 | } 177 | 178 | .task__plus { 179 | position: absolute; 180 | top: -1px; 181 | right: 0; 182 | bottom: 0; 183 | left: 0; 184 | fill: @dark-hint; 185 | 186 | svg { 187 | position: absolute; 188 | top: 8px; 189 | left: 9px; 190 | width: 25px; 191 | height: 25px; 192 | } 193 | } 194 | 195 | .task__name { 196 | flex: 1 0 0%; 197 | padding: 8px 0 7px; 198 | overflow: hidden; 199 | white-space: nowrap; 200 | text-overflow: ellipsis; 201 | border-top: 1px solid @task-border; 202 | border-bottom: 1px solid @task-border; 203 | 204 | .task--add & { 205 | border-bottom: 0; 206 | } 207 | 208 | .task--completed & { 209 | color: @dark-hint; 210 | 211 | strong { 212 | font-weight: normal; 213 | } 214 | 215 | a { 216 | color: @dark-hint; 217 | } 218 | } 219 | } 220 | 221 | .task__time { 222 | margin-right: 2px; 223 | font-weight: normal; 224 | color: @dark-hint; 225 | 226 | .task--today.task--now:not(.task--completed) & { 227 | color: @task-pea-today-hover; 228 | } 229 | } 230 | 231 | .task__input[type=text] { 232 | height: 39px; 233 | margin: -8px 0 -7px 0; 234 | padding: 8px 0 7px 0; 235 | 236 | .task--add & { 237 | padding-right: 8px; 238 | color: @dark-secondary; 239 | } 240 | 241 | .task--completed & { 242 | color: @dark-hint; 243 | } 244 | } 245 | 246 | .task__options { 247 | position: relative; 248 | width: 39px; 249 | border-top: 1px solid @task-border; 250 | border-bottom: 1px solid @task-border; 251 | } 252 | 253 | .task__trash { 254 | position: absolute; 255 | top: -1px; 256 | right: 0; 257 | bottom: 0; 258 | left: 0; 259 | width: 39px; 260 | padding: 0; 261 | fill: @task-trash; 262 | background: none; 263 | border: 0; 264 | transform: scale3d(0.8, 0.8, 1); 265 | visibility: hidden; 266 | cursor: pointer; 267 | opacity: 0; 268 | transition: @duration @timing-function; 269 | transition-property: visibility, opacity, transform; 270 | 271 | body:not(.mobile) &:hover { 272 | fill: @task-trash-hover; 273 | } 274 | 275 | body:not(.mobile) .task__wrap:hover & { 276 | transform: scale3d(1, 1, 1); 277 | visibility: visible; 278 | opacity: 1; 279 | transition-delay: (@duration * 2); 280 | } 281 | 282 | .mobile & { 283 | transform: scale3d(1, 1, 1); 284 | visibility: visible; 285 | opacity: 1; 286 | } 287 | 288 | .board__lists--drag & { 289 | transform: scale3d(0.8, 0.8, 1) !important; 290 | visibility: hidden; 291 | opacity: 0 !important; 292 | transition-delay: 0s !important; 293 | } 294 | 295 | svg { 296 | position: absolute; 297 | top: 8px; 298 | left: 8px; 299 | width: 25px; 300 | height: 25px; 301 | } 302 | } 303 | 304 | .task__grid { 305 | background-color: @grid-bg; 306 | 307 | body:not(.mobile) .board__lists:not(.board__lists--drag) .task:hover & { 308 | background-color: @grid-bg-hover; 309 | } 310 | 311 | .task--add & { 312 | border-radius: 0 0 (@radius * 2) 0; 313 | } 314 | 315 | .task--chosen & { 316 | transition: @duration @timing-function; 317 | transition-property: opacity; 318 | opacity: 0.5; 319 | } 320 | 321 | .task--ghost & { 322 | opacity: 0; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/blocks/tinycon/tinycon.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Tinycon - A small library for manipulating the Favicon 3 | * Tom Moor, http://tommoor.com 4 | * Copyright (c) 2015 Tom Moor 5 | * @license MIT Licensed 6 | */ 7 | 8 | (function(){ 9 | 10 | var Tinycon = {}; 11 | var currentFavicon = null; 12 | var originalFavicon = null; 13 | var faviconImage = null; 14 | var canvas = null; 15 | var options = {}; 16 | // Chrome browsers with nonstandard zoom report fractional devicePixelRatio. 17 | var r = Math.ceil(window.devicePixelRatio) || 1; 18 | var size = 16 * r; 19 | var defaults = { 20 | width: 16, 21 | height: 16, 22 | font: 11 * r + 'px Courier New', 23 | color: '#000', 24 | background: 'transparent', 25 | fallback: false, 26 | crossOrigin: true, 27 | abbreviate: true 28 | }; 29 | 30 | var ua = (function () { 31 | var agent = navigator.userAgent.toLowerCase(); 32 | // New function has access to 'agent' via closure 33 | return function (browser) { 34 | return agent.indexOf(browser) !== -1; 35 | }; 36 | }()); 37 | 38 | var browser = { 39 | ie: ua('trident'), 40 | chrome: ua('chrome'), 41 | webkit: ua('chrome') || ua('safari'), 42 | safari: ua('safari') && !ua('chrome'), 43 | mozilla: ua('mozilla') && !ua('chrome') && !ua('safari') 44 | }; 45 | 46 | // private methods 47 | var getFaviconTag = function(){ 48 | 49 | var links = document.getElementsByTagName('link'); 50 | 51 | for(var i=0, len=links.length; i < len; i++) { 52 | if ((links[i].getAttribute('rel') || '').match(/\bicon\b/i)) { 53 | return links[i]; 54 | } 55 | } 56 | 57 | return false; 58 | }; 59 | 60 | var removeFaviconTag = function(){ 61 | 62 | var links = document.getElementsByTagName('link'); 63 | 64 | for(var i=0, len=links.length; i < len; i++) { 65 | var exists = (typeof(links[i]) !== 'undefined'); 66 | if (exists && (links[i].getAttribute('rel') || '').match(/\bicon\b/i)) { 67 | links[i].parentNode.removeChild(links[i]); 68 | } 69 | } 70 | }; 71 | 72 | var getCurrentFavicon = function(){ 73 | 74 | if (!originalFavicon || !currentFavicon) { 75 | var tag = getFaviconTag(); 76 | currentFavicon = tag ? tag.getAttribute('href') : '/favicon.ico'; 77 | if (!originalFavicon) { 78 | originalFavicon = currentFavicon; 79 | } 80 | } 81 | 82 | return currentFavicon; 83 | }; 84 | 85 | var getCanvas = function (){ 86 | 87 | if (!canvas) { 88 | canvas = document.createElement("canvas"); 89 | canvas.width = size; 90 | canvas.height = size; 91 | } 92 | 93 | return canvas; 94 | }; 95 | 96 | var setFaviconTag = function(url){ 97 | if(url){ 98 | removeFaviconTag(); 99 | 100 | var link = document.createElement('link'); 101 | link.type = 'image/x-icon'; 102 | link.rel = 'icon'; 103 | link.href = url; 104 | document.getElementsByTagName('head')[0].appendChild(link); 105 | } 106 | }; 107 | 108 | var log = function(message){ 109 | if (window.console) window.console.log(message); 110 | }; 111 | 112 | var drawFavicon = function(label, color) { 113 | 114 | // fallback to updating the browser title if unsupported 115 | if (!getCanvas().getContext || browser.ie || browser.safari || options.fallback === 'force') { 116 | return updateTitle(label); 117 | } 118 | 119 | var context = getCanvas().getContext("2d"); 120 | var color = color || '#000000'; 121 | var src = getCurrentFavicon(); 122 | 123 | faviconImage = document.createElement('img'); 124 | faviconImage.onload = function() { 125 | 126 | // clear canvas 127 | context.clearRect(0, 0, size, size); 128 | 129 | // draw the favicon 130 | context.drawImage(faviconImage, 2, 2, faviconImage.width, faviconImage.height, 0, 0, size, size); 131 | 132 | // draw bubble over the top 133 | if ((label + '').length > 0) drawBubble(context, label, color); 134 | 135 | // refresh tag in page 136 | refreshFavicon(); 137 | }; 138 | 139 | // allow cross origin resource requests if the image is not a data:uri 140 | // as detailed here: https://github.com/mrdoob/three.js/issues/1305 141 | if (!src.match(/^data/) && options.crossOrigin) { 142 | faviconImage.crossOrigin = 'anonymous'; 143 | } 144 | 145 | faviconImage.src = src; 146 | }; 147 | 148 | var updateTitle = function(label) { 149 | 150 | if (options.fallback) { 151 | // Grab the current title that we can prefix with the label 152 | var originalTitle = document.title; 153 | 154 | // Strip out the old label if there is one 155 | if (originalTitle[0] === '(') { 156 | originalTitle = originalTitle.slice(originalTitle.indexOf(' ')); 157 | } 158 | 159 | if ((label + '').length > 0) { 160 | document.title = '(' + label + ') ' + originalTitle; 161 | } else { 162 | document.title = originalTitle; 163 | } 164 | } 165 | }; 166 | 167 | var drawBubble = function(context, label, color) { 168 | 169 | // automatic abbreviation for long (>2 digits) numbers 170 | if (typeof label == 'number' && label > 99 && options.abbreviate) { 171 | label = abbreviateNumber(label); 172 | } 173 | 174 | // bubble needs to be larger for double digits 175 | var len = (label + '').length-1; 176 | 177 | var width = options.width * r + (6 * r * len), 178 | height = options.height * r; 179 | 180 | var top = size - height, 181 | left = size - width - r, 182 | bottom = 16 * r, 183 | right = 16 * r, 184 | radius = 2 * r; 185 | 186 | // webkit seems to render fonts lighter than firefox 187 | context.font = (browser.webkit ? 'bold ' : '') + options.font; 188 | context.fillStyle = options.background; 189 | context.strokeStyle = options.background; 190 | context.lineWidth = r; 191 | 192 | // bubble 193 | context.beginPath(); 194 | context.moveTo(left + radius, top); 195 | context.quadraticCurveTo(left, top, left, top + radius); 196 | context.lineTo(left, bottom - radius); 197 | context.quadraticCurveTo(left, bottom, left + radius, bottom); 198 | context.lineTo(right - radius, bottom); 199 | context.quadraticCurveTo(right, bottom, right, bottom - radius); 200 | context.lineTo(right, top + radius); 201 | context.quadraticCurveTo(right, top, right - radius, top); 202 | context.closePath(); 203 | context.fill(); 204 | 205 | // bottom shadow 206 | // context.beginPath(); 207 | // context.strokeStyle = "rgba(0,0,0,0.3)"; 208 | // context.moveTo(left + radius / 2.0, bottom); 209 | // context.lineTo(right - radius / 2.0, bottom); 210 | // context.stroke(); 211 | 212 | // label pseudo stroke 213 | context.strokeStyle = "rgba(255, 255, 255, 0.9)"; 214 | context.lineWidth = 2; 215 | context.textAlign = "right"; 216 | context.textBaseline = "top"; 217 | 218 | // unfortunately webkit/mozilla are a pixel different in text positioning 219 | context.strokeText(label, r === 2 ? 32 : 16, browser.mozilla ? 7*r : 6*r); 220 | 221 | // label 222 | context.fillStyle = options.color; 223 | context.textAlign = "right"; 224 | context.textBaseline = "top"; 225 | 226 | // unfortunately webkit/mozilla are a pixel different in text positioning 227 | context.fillText(label, r === 2 ? 32 : 16, browser.mozilla ? 7*r : 6*r); 228 | }; 229 | 230 | var refreshFavicon = function(){ 231 | // check support 232 | if (!getCanvas().getContext) return; 233 | 234 | setFaviconTag(getCanvas().toDataURL()); 235 | }; 236 | 237 | var abbreviateNumber = function(label) { 238 | var metricPrefixes = [ 239 | ['G', 1000000000], 240 | ['M', 1000000], 241 | ['k', 1000] 242 | ]; 243 | 244 | for(var i = 0; i < metricPrefixes.length; ++i) { 245 | if (label >= metricPrefixes[i][1]) { 246 | label = round(label / metricPrefixes[i][1]) + metricPrefixes[i][0]; 247 | break; 248 | } 249 | } 250 | 251 | return label; 252 | }; 253 | 254 | var round = function (value, precision) { 255 | var number = new Number(value); 256 | return number.toFixed(precision); 257 | }; 258 | 259 | // public methods 260 | Tinycon.setOptions = function(custom){ 261 | options = {}; 262 | 263 | // account for deprecated UK English spelling 264 | if (custom.colour) { 265 | custom.color = custom.colour; 266 | } 267 | 268 | for(var key in defaults){ 269 | options[key] = custom.hasOwnProperty(key) ? custom[key] : defaults[key]; 270 | } 271 | return this; 272 | }; 273 | 274 | Tinycon.setImage = function(url){ 275 | currentFavicon = url; 276 | refreshFavicon(); 277 | return this; 278 | }; 279 | 280 | Tinycon.setBubble = function(label, color) { 281 | label = label || ''; 282 | drawFavicon(label, color); 283 | return this; 284 | }; 285 | 286 | Tinycon.reset = function(){ 287 | currentFavicon = originalFavicon; 288 | setFaviconTag(originalFavicon); 289 | }; 290 | 291 | Tinycon.setOptions(defaults); 292 | 293 | if(typeof define === 'function' && define.amd) { 294 | define(Tinycon); 295 | } else if (typeof module !== 'undefined') { 296 | module.exports = Tinycon; 297 | } else { 298 | window.Tinycon = Tinycon; 299 | } 300 | 301 | })(); 302 | -------------------------------------------------------------------------------- /src/blocks/view/view.less: -------------------------------------------------------------------------------- 1 | // View 2 | //------------------------------------------------------------------------------ 3 | 4 | .view { 5 | position: relative; 6 | margin-top: 20px; 7 | margin-bottom: 20px; 8 | background-color: @light-secondary; 9 | background-repeat: no-repeat; 10 | 11 | @media (min-width: 768px) { 12 | margin-top: 30px; 13 | margin-bottom: 30px; 14 | } 15 | 16 | &--browser { 17 | padding-bottom: 56.25%; 18 | background-position: 50% 0; 19 | background-size: cover; 20 | border-top: 10px solid @dark-divider; 21 | border-radius: @radius @radius 0 0; 22 | 23 | @media (min-width: 768px) { 24 | border-top-width: 21px; 25 | } 26 | 27 | &::before { 28 | content: ""; 29 | position: absolute; 30 | right: -7.5px; 31 | bottom: 0; 32 | left: -7.5px; 33 | height: 1px; 34 | background-color: @dark-divider; 35 | 36 | @media (min-width: 768px) { 37 | right: -15px; 38 | left: -15px; 39 | } 40 | } 41 | } 42 | 43 | &--details { 44 | width: 201px; 45 | padding-bottom: 199px; 46 | border: 1px solid @dark-divider; 47 | border-radius: 50%; 48 | } 49 | 50 | &--line { 51 | height: 41px; 52 | border: 1px solid @dark-divider; 53 | margin-top: 20px !important; 54 | margin-bottom: 20px !important; 55 | } 56 | } 57 | 58 | .view__window { 59 | position: absolute; 60 | top: -7px; 61 | left: 10px; 62 | width: 4px; 63 | height: 4px; 64 | background-color: @dark-hint; 65 | border-radius: 50%; 66 | 67 | @media (min-width: 768px) { 68 | top: -14px; 69 | left: 18px; 70 | width: 7px; 71 | height: 7px; 72 | } 73 | 74 | &::before, 75 | &::after { 76 | content: ""; 77 | position: absolute; 78 | top: 0; 79 | width: 100%; 80 | padding-bottom: 100%; 81 | background-color: @dark-hint; 82 | border-radius: 50%; 83 | } 84 | 85 | &::before { 86 | left: -150%; 87 | } 88 | 89 | &::after { 90 | right: -150%; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/favicon-32x32.png -------------------------------------------------------------------------------- /src/img/favicon-today.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/favicon-today.png -------------------------------------------------------------------------------- /src/img/interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/interface.png -------------------------------------------------------------------------------- /src/img/notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/notify.png -------------------------------------------------------------------------------- /src/img/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/settings.png -------------------------------------------------------------------------------- /src/img/themes/bush-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/bush-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/bush-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/bush-md.jpg -------------------------------------------------------------------------------- /src/img/themes/bush-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/bush-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/bush-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/bush-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/clouds-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/clouds-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/clouds-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/clouds-md.jpg -------------------------------------------------------------------------------- /src/img/themes/clouds-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/clouds-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/clouds-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/clouds-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/flow-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/flow-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/flow-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/flow-md.jpg -------------------------------------------------------------------------------- /src/img/themes/flow-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/flow-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/flow-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/flow-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/forest-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/forest-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/forest-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/forest-md.jpg -------------------------------------------------------------------------------- /src/img/themes/forest-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/forest-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/forest-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/forest-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/galaxy-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/galaxy-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/galaxy-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/galaxy-md.jpg -------------------------------------------------------------------------------- /src/img/themes/galaxy-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/galaxy-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/galaxy-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/galaxy-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/hill-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/hill-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/hill-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/hill-md.jpg -------------------------------------------------------------------------------- /src/img/themes/hill-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/hill-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/hill-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/hill-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/leaves-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/leaves-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/leaves-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/leaves-md.jpg -------------------------------------------------------------------------------- /src/img/themes/leaves-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/leaves-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/leaves-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/leaves-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/lighthouse-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/lighthouse-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/lighthouse-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/lighthouse-md.jpg -------------------------------------------------------------------------------- /src/img/themes/lighthouse-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/lighthouse-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/lighthouse-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/lighthouse-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/lighting-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/lighting-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/lighting-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/lighting-md.jpg -------------------------------------------------------------------------------- /src/img/themes/lighting-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/lighting-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/lighting-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/lighting-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/mountains-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/mountains-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/mountains-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/mountains-md.jpg -------------------------------------------------------------------------------- /src/img/themes/mountains-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/mountains-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/mountains-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/mountains-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/skyscrapers-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/skyscrapers-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/skyscrapers-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/skyscrapers-md.jpg -------------------------------------------------------------------------------- /src/img/themes/skyscrapers-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/skyscrapers-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/skyscrapers-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/skyscrapers-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/table-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/table-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/table-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/table-md.jpg -------------------------------------------------------------------------------- /src/img/themes/table-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/table-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/table-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/table-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/troposphere-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/troposphere-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/troposphere-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/troposphere-md.jpg -------------------------------------------------------------------------------- /src/img/themes/troposphere-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/troposphere-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/troposphere-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/troposphere-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/twigs-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/twigs-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/twigs-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/twigs-md.jpg -------------------------------------------------------------------------------- /src/img/themes/twigs-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/twigs-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/twigs-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/twigs-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/umbrella-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/umbrella-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/umbrella-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/umbrella-md.jpg -------------------------------------------------------------------------------- /src/img/themes/umbrella-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/umbrella-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/umbrella-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/umbrella-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/valley-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/valley-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/valley-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/valley-md.jpg -------------------------------------------------------------------------------- /src/img/themes/valley-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/valley-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/valley-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/valley-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/water-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/water-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/water-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/water-md.jpg -------------------------------------------------------------------------------- /src/img/themes/water-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/water-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/water-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/water-xs.jpg -------------------------------------------------------------------------------- /src/img/themes/waves-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/waves-lg.jpg -------------------------------------------------------------------------------- /src/img/themes/waves-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/waves-md.jpg -------------------------------------------------------------------------------- /src/img/themes/waves-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/waves-sm.jpg -------------------------------------------------------------------------------- /src/img/themes/waves-xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaxsof/mahoweek/a40bdb8a12d28205d0520b9e32fdfdc7dfae3628/src/img/themes/waves-xs.jpg -------------------------------------------------------------------------------- /src/inc/metrika.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/inc/popup-about.html: -------------------------------------------------------------------------------- 1 |
2 |

Mahoweek

3 | 4 |

Веб-приложение по ведению краткосрочного плана дел. Поможет просто организовать список задач и наметить на календарной сетке даты их выполнения на одну-две недели вперёд.

5 | 6 |
7 |

Как всё устроено

8 | 9 |

Mahoweek полностью бесплатен, не перенасыщен функционалом и одинаково хорошо выглядит как на ноутбуке, так и на телефоне.

10 | 11 |

Его можно добавить на домашний экран и использовать как мобильное приложение для Айос или Андроид.

12 | 13 |

Узнать чуть больше

14 |
15 | 16 |
17 |
18 |

Помощь и поддержка

19 | 20 |

Если вам понравился веб-сервис, вы можете оказать нам информационную поддержку.

21 | 22 |

Просто расскажите друзьям и знакомым о нас и этим вы серьёзно нам поможете.

23 | 24 | 28 |
29 | 30 |
31 | 66 |
67 |
68 | 69 |
70 |

Обратная связь

71 | 72 |

Любые вопросы, предложения, сомнения, критика или замечания помогают нам стать лучше, а вам — счастливее.

73 | 74 |

Пишите на электронную почту app@mahoweek.com.

75 | 76 |

Подписывайтесь, чтобы удобно следить за новостями и обновлениями проекта.

77 |
78 | 79 |

Сделано с любовью в Санкт-Петербурге

80 | 81 |

© 2017–2018

82 |
83 | -------------------------------------------------------------------------------- /src/inc/popup-bye.html: -------------------------------------------------------------------------------- 1 | До встречи! 2 | 3 |
4 |

👋 До встречи!

5 | 6 |

Вы отключили синхронизацию. Авторизуйтесь, чтобы все ваши дела и списки вновь появились на доске.

7 |
8 | -------------------------------------------------------------------------------- /src/inc/popup-hello.html: -------------------------------------------------------------------------------- 1 | Добро пожаловать! 2 | 3 |
4 |

Mahoweek

5 | 6 |

Веб-приложение по ведению краткосрочного плана дел. Поможет просто организовать список задач и наметить на календарной сетке даты их выполнения на одну-две недели вперёд.

7 | 8 |
9 |
10 |
11 | 12 |

Составляйте списки личных дел и проектов, организуйте план путешествия, работы и учёбы, планируйте встречи и поездки, ставьте цели и напоминания о событиях и ещё многое другое.

13 | 14 |

15 |
16 | -------------------------------------------------------------------------------- /src/inc/popup-privacy.html: -------------------------------------------------------------------------------- 1 |
2 |

Политика в отношении обработки персональных данных

3 | 4 |

1. Общие положения

5 |

Настоящая политика обработки персональных данных составлена в соответствии с требованиями Федерального закона от 27.07.2006. № 152-ФЗ «О персональных данных» (далее — Закон о персональных данных) и определяет порядок обработки персональных данных и меры по обеспечению безопасности персональных данных, предпринимаемые Mahoweek (далее — Оператор).

6 |

1.1. Оператор ставит своей важнейшей целью и условием осуществления своей деятельности соблюдение прав и свобод человека и гражданина при обработке его персональных данных, в том числе защиты прав на неприкосновенность частной жизни, личную и семейную тайну. 7 |

1.2. Настоящая политика Оператора в отношении обработки персональных данных (далее — Политика) применяется ко всей информации, которую Оператор может получить о посетителях веб-сайта https://mahoweek.com.

8 | 9 |

2. Основные понятия, используемые в Политике

10 |

2.1. Автоматизированная обработка персональных данных — обработка персональных данных с помощью средств вычислительной техники.

11 |

2.2. Блокирование персональных данных — временное прекращение обработки персональных данных (за исключением случаев, если обработка необходима для уточнения персональных данных).

12 |

2.3. Веб-сайт — совокупность графических и информационных материалов, а также программ для ЭВМ и баз данных, обеспечивающих их доступность в сети интернет по сетевому адресу https://mahoweek.com.

13 |

2.4. Информационная система персональных данных — совокупность содержащихся в базах данных персональных данных, и обеспечивающих их обработку информационных технологий и технических средств.

14 |

2.5. Обезличивание персональных данных — действия, в результате которых невозможно определить без использования дополнительной информации принадлежность персональных данных конкретному Пользователю или иному субъекту персональных данных.

15 |

2.6. Обработка персональных данных — любое действие (операция) или совокупность действий (операций), совершаемых с использованием средств автоматизации или без использования таких средств с персональными данными, включая сбор, запись, систематизацию, накопление, хранение, уточнение (обновление, изменение), извлечение, использование, передачу (распространение, предоставление, доступ), обезличивание, блокирование, удаление, уничтожение персональных данных.

16 |

2.7. Оператор — государственный орган, муниципальный орган, юридическое или физическое лицо, самостоятельно или совместно с другими лицами организующие и (или) осуществляющие обработку персональных данных, а также определяющие цели обработки персональных данных, состав персональных данных, подлежащих обработке, действия (операции), совершаемые с персональными данными.

17 |

2.8. Персональные данные — любая информация, относящаяся прямо или косвенно к определенному или определяемому Пользователю веб-сайта https://mahoweek.com.

18 |

2.9. Персональные данные, разрешенные субъектом персональных данных для распространения, — персональные данные, доступ неограниченного круга лиц к которым предоставлен субъектом персональных данных путем дачи согласия на обработку персональных данных, разрешенных субъектом персональных данных для распространения в порядке, предусмотренном Законом о персональных данных (далее — персональные данные, разрешенные для распространения).

19 |

2.10. Пользователь — любой посетитель веб-сайта https://mahoweek.com.

20 |

2.11. Предоставление персональных данных — действия, направленные на раскрытие персональных данных определенному лицу или определенному кругу лиц.

21 |

2.12. Распространение персональных данных — любые действия, направленные на раскрытие персональных данных неопределенному кругу лиц (передача персональных данных) или на ознакомление с персональными данными неограниченного круга лиц, в том числе обнародование персональных данных в средствах массовой информации, размещение в информационно-телекоммуникационных сетях или предоставление доступа к персональным данным каким-либо иным способом.

22 |

2.13. Трансграничная передача персональных данных — передача персональных данных на территорию иностранного государства органу власти иностранного государства, иностранному физическому или иностранному юридическому лицу.

23 |

2.14. Уничтожение персональных данных — любые действия, в результате которых персональные данные уничтожаются безвозвратно с невозможностью дальнейшего восстановления содержания персональных данных в информационной системе персональных данных и (или) уничтожаются материальные носители персональных данных.

24 | 25 |

3. Основные права и обязанности Оператора

26 |

3.1. Оператор имеет право:
27 | — получать от субъекта персональных данных достоверные информацию и/или документы, содержащие персональные данные;
28 | — в случае отзыва субъектом персональных данных согласия на обработку персональных данных Оператор вправе продолжить обработку персональных данных без согласия субъекта персональных данных при наличии оснований, указанных в Законе о персональных данных;
29 | — самостоятельно определять состав и перечень мер, необходимых и достаточных для обеспечения выполнения обязанностей, предусмотренных Законом о персональных данных и принятыми в соответствии с ним нормативными правовыми актами, если иное не предусмотрено Законом о персональных данных или другими федеральными законами.

30 |

3.2. Оператор обязан:
31 | — предоставлять субъекту персональных данных по его просьбе информацию, касающуюся обработки его персональных данных;
32 | — организовывать обработку персональных данных в порядке, установленном действующим законодательством РФ;
33 | — отвечать на обращения и запросы субъектов персональных данных и их законных представителей в соответствии с требованиями Закона о персональных данных;
34 | — сообщать в уполномоченный орган по защите прав субъектов персональных данных по запросу этого органа необходимую информацию в течение 30 дней с даты получения такого запроса;
35 | — публиковать или иным образом обеспечивать неограниченный доступ к настоящей Политике в отношении обработки персональных данных;
36 | — принимать правовые, организационные и технические меры для защиты персональных данных от неправомерного или случайного доступа к ним, уничтожения, изменения, блокирования, копирования, предоставления, распространения персональных данных, а также от иных неправомерных действий в отношении персональных данных;
37 | — прекратить передачу (распространение, предоставление, доступ) персональных данных, прекратить обработку и уничтожить персональные данные в порядке и случаях, предусмотренных Законом о персональных данных;
38 | — исполнять иные обязанности, предусмотренные Законом о персональных данных.

39 | 40 |

4. Основные права и обязанности субъектов персональных данных

41 |

4.1. Субъекты персональных данных имеют право:
42 | — получать информацию, касающуюся обработки его персональных данных, за исключением случаев, предусмотренных федеральными законами. Сведения предоставляются субъекту персональных данных Оператором в доступной форме, и в них не должны содержаться персональные данные, относящиеся к другим субъектам персональных данных, за исключением случаев, когда имеются законные основания для раскрытия таких персональных данных. Перечень информации и порядок ее получения установлен Законом о персональных данных;
43 | — требовать от оператора уточнения его персональных данных, их блокирования или уничтожения в случае, если персональные данные являются неполными, устаревшими, неточными, незаконно полученными или не являются необходимыми для заявленной цели обработки, а также принимать предусмотренные законом меры по защите своих прав;
44 | — выдвигать условие предварительного согласия при обработке персональных данных в целях продвижения на рынке товаров, работ и услуг;
45 | — на отзыв согласия на обработку персональных данных;
46 | — обжаловать в уполномоченный орган по защите прав субъектов персональных данных или в судебном порядке неправомерные действия или бездействие Оператора при обработке его персональных данных;
47 | — на осуществление иных прав, предусмотренных законодательством РФ.

48 |

4.2. Субъекты персональных данных обязаны:
49 | — предоставлять Оператору достоверные данные о себе;
50 | — сообщать Оператору об уточнении (обновлении, изменении) своих персональных данных.

51 |

4.3. Лица, передавшие Оператору недостоверные сведения о себе, либо сведения о другом субъекте персональных данных без согласия последнего, несут ответственность в соответствии с законодательством РФ.

52 | 53 |

5. Оператор может обрабатывать следующие персональные данные Пользователя

54 |

5.1. Фамилия, имя, отчество.

55 |

5.2. Электронный адрес.

56 |

5.3. Фотографии.

57 |

5.4. Также на сайте происходит сбор и обработка обезличенных данных о посетителях (в т. ч. файлов «cookie») с помощью сервисов интернет-статистики (Яндекс Метрика и Гугл Аналитика и других).

58 |

5.5. Вышеперечисленные данные далее по тексту Политики объединены общим понятием Персональные данные.

59 |

5.6. Обработка специальных категорий персональных данных, касающихся расовой, национальной принадлежности, политических взглядов, религиозных или философских убеждений, интимной жизни, Оператором не осуществляется.

60 |

5.7. Обработка персональных данных, разрешенных для распространения, из числа специальных категорий персональных данных, указанных в ч. 1 ст. 10 Закона о персональных данных, допускается, если соблюдаются запреты и условия, предусмотренные ст. 10.1 Закона о персональных данных.

61 |

5.8. Согласие Пользователя на обработку персональных данных, разрешенных для распространения, оформляется отдельно от других согласий на обработку его персональных данных. При этом соблюдаются условия, предусмотренные, в частности, ст. 10.1 Закона о персональных данных. Требования к содержанию такого согласия устанавливаются уполномоченным органом по защите прав субъектов персональных данных.

62 |

5.8.1 Согласие на обработку персональных данных, разрешенных для распространения, Пользователь предоставляет Оператору непосредственно.

63 |

5.8.2 Оператор обязан в срок не позднее трех рабочих дней с момента получения указанного согласия Пользователя опубликовать информацию об условиях обработки, о наличии запретов и условий на обработку неограниченным кругом лиц персональных данных, разрешенных для распространения.

64 |

5.8.3 Передача (распространение, предоставление, доступ) персональных данных, разрешенных субъектом персональных данных для распространения, должна быть прекращена в любое время по требованию субъекта персональных данных. Данное требование должно включать в себя фамилию, имя, отчество (при наличии), контактную информацию (номер телефона, адрес электронной почты или почтовый адрес) субъекта персональных данных, а также перечень персональных данных, обработка которых подлежит прекращению. Указанные в данном требовании персональные данные могут обрабатываться только Оператором, которому оно направлено.

65 |

5.8.4 Согласие на обработку персональных данных, разрешенных для распространения, прекращает свое действие с момента поступления Оператору требования, указанного в п. 5.8.3 настоящей Политики в отношении обработки персональных данных.

66 | 67 |

6. Принципы обработки персональных данных

68 |

6.1. Обработка персональных данных осуществляется на законной и справедливой основе.

69 |

6.2. Обработка персональных данных ограничивается достижением конкретных, заранее определенных и законных целей. Не допускается обработка персональных данных, несовместимая с целями сбора персональных данных.

70 |

6.3. Не допускается объединение баз данных, содержащих персональные данные, обработка которых осуществляется в целях, несовместимых между собой.

71 |

6.4. Обработке подлежат только персональные данные, которые отвечают целям их обработки.

72 |

6.5. Содержание и объем обрабатываемых персональных данных соответствуют заявленным целям обработки. Не допускается избыточность обрабатываемых персональных данных по отношению к заявленным целям их обработки.

73 |

6.6. При обработке персональных данных обеспечивается точность персональных данных, их достаточность, а в необходимых случаях и актуальность по отношению к целям обработки персональных данных. Оператор принимает необходимые меры и/или обеспечивает их принятие по удалению или уточнению неполных или неточных данных.

74 |

6.7. Хранение персональных данных осуществляется в форме, позволяющей определить субъекта персональных данных, не дольше, чем этого требуют цели обработки персональных данных, если срок хранения персональных данных не установлен федеральным законом, договором, стороной которого, выгодоприобретателем или поручителем по которому является субъект персональных данных. Обрабатываемые персональные данные уничтожаются либо обезличиваются по достижении целей обработки или в случае утраты необходимости в достижении этих целей, если иное не предусмотрено федеральным законом.

75 | 76 |

7. Цели обработки персональных данных

77 |

7.1. Цель обработки персональных данных Пользователя:
78 |  — предоставление доступа Пользователю к сервисам, информации и/или материалам, содержащимся на веб-сайте https://mahoweek.com.

79 |

7.2. Также Оператор имеет право направлять Пользователю уведомления о новых продуктах и услугах, специальных предложениях и различных событиях. Пользователь всегда может отказаться от получения информационных сообщений, направив Оператору письмо на адрес электронной почты app@mahoweek.com с пометкой «Отказ от уведомлений о новых продуктах и услугах и специальных предложениях».

80 |

7.3. Обезличенные данные Пользователей, собираемые с помощью сервисов интернет-статистики, служат для сбора информации о действиях Пользователей на сайте, улучшения качества сайта и его содержания.

81 | 82 |

8. Правовые основания обработки персональных данных

83 |

8.1. Правовыми основаниями обработки персональных данных Оператором являются:
84 | — уставные (учредительные) документы Оператора;
85 | — федеральные законы, иные нормативно-правовые акты в сфере защиты персональных данных;
86 | — согласия Пользователей на обработку их персональных данных, на обработку персональных данных, разрешенных для распространения.

87 |

8.2. Оператор обрабатывает персональные данные Пользователя только в случае их заполнения и/или отправки Пользователем самостоятельно через специальные формы, расположенные на сайте https://mahoweek.com или направленные Оператору посредством электронной почты. Заполняя соответствующие формы и/или отправляя свои персональные данные Оператору, Пользователь выражает свое согласие с данной Политикой.

88 |

8.3. Оператор обрабатывает обезличенные данные о Пользователе в случае, если это разрешено в настройках браузера Пользователя (включено сохранение файлов «cookie» и использование технологии JavaScript).

89 |

8.4. Субъект персональных данных самостоятельно принимает решение о предоставлении его персональных данных и дает согласие свободно, своей волей и в своем интересе.

90 | 91 |

9. Условия обработки персональных данных

92 |

9.1. Обработка персональных данных осуществляется с согласия субъекта персональных данных на обработку его персональных данных.

93 |

9.2. Обработка персональных данных необходима для достижения целей, предусмотренных международным договором Российской Федерации или законом, для осуществления возложенных законодательством Российской Федерации на оператора функций, полномочий и обязанностей.

94 |

9.3. Обработка персональных данных необходима для осуществления правосудия, исполнения судебного акта, акта другого органа или должностного лица, подлежащих исполнению в соответствии с законодательством Российской Федерации об исполнительном производстве.

95 |

9.4. Обработка персональных данных необходима для исполнения договора, стороной которого либо выгодоприобретателем или поручителем по которому является субъект персональных данных, а также для заключения договора по инициативе субъекта персональных данных или договора, по которому субъект персональных данных будет являться выгодоприобретателем или поручителем.

96 |

9.5. Обработка персональных данных необходима для осуществления прав и законных интересов оператора или третьих лиц либо для достижения общественно значимых целей при условии, что при этом не нарушаются права и свободы субъекта персональных данных.

97 |

9.6. Осуществляется обработка персональных данных, доступ неограниченного круга лиц к которым предоставлен субъектом персональных данных либо по его просьбе (далее — общедоступные персональные данные).

98 |

9.7. Осуществляется обработка персональных данных, подлежащих опубликованию или обязательному раскрытию в соответствии с федеральным законом.

99 | 100 |

10. Порядок сбора, хранения, передачи, удаления и других видов обработки персональных данных

101 |

Безопасность персональных данных, которые обрабатываются Оператором, обеспечивается путем реализации правовых, организационных и технических мер, необходимых для выполнения в полном объеме требований действующего законодательства в области защиты персональных данных.

102 |

10.1. Оператор обеспечивает сохранность персональных данных и принимает все возможные меры, исключающие доступ к персональным данным неуполномоченных лиц.

103 |

10.2. Персональные данные Пользователя никогда, ни при каких условиях не будут переданы третьим лицам, за исключением случаев, связанных с исполнением действующего законодательства либо в случае, если субъектом персональных данных дано согласие Оператору на передачу данных третьему лицу для исполнения обязательств по гражданско-правовому договору.

104 |

10.3. В случае выявления неточностей в персональных данных, Пользователь может актуализировать их самостоятельно, путем направления Оператору уведомление на адрес электронной почты Оператора app@mahoweek.com с пометкой «Актуализация персональных данных».

105 |

10.4. Срок обработки персональных данных определяется достижением целей, для которых были собраны персональные данные, если иной срок не предусмотрен договором или действующим законодательством. Пользователь может в любой момент отозвать свое согласие на обработку персональных данных, направив Оператору уведомление посредством электронной почты на электронный адрес Оператора app@mahoweek.com с пометкой «Отзыв согласия на обработку персональных данных».

106 |

10.5. Вся информация, которая собирается сторонними сервисами, в том числе платежными системами, средствами связи и другими поставщиками услуг, хранится и обрабатывается указанными лицами (Операторами) в соответствии с их Пользовательским соглашением и Политикой конфиденциальности. Субъект персональных данных и/или Пользователь обязан самостоятельно своевременно ознакомиться с указанными документами. Оператор не несет ответственность за действия третьих лиц, в том числе указанных в настоящем пункте поставщиков услуг.

107 |

10.6. Установленные субъектом персональных данных запреты на передачу (кроме предоставления доступа), а также на обработку или условия обработки (кроме получения доступа) персональных данных, разрешенных для распространения, не действуют в случаях обработки персональных данных в государственных, общественных и иных публичных интересах, определенных законодательством РФ.

108 |

10.7. Оператор при обработке персональных данных обеспечивает конфиденциальность персональных данных.

109 |

10.8. Оператор осуществляет хранение персональных данных в форме, позволяющей определить субъекта персональных данных, не дольше, чем этого требуют цели обработки персональных данных, если срок хранения персональных данных не установлен федеральным законом, договором, стороной которого, выгодоприобретателем или поручителем по которому является субъект персональных данных.

110 |

10.9. Условием прекращения обработки персональных данных может являться достижение целей обработки персональных данных, истечение срока действия согласия субъекта персональных данных или отзыв согласия субъектом персональных данных, а также выявление неправомерной обработки персональных данных.

111 |

10.10. Удаление всех персональных данных возможно по запросу Пользователя на адрес Оператора app@mahoweek.com в свободной форме только с того адреса электронного ящика по которому был зарегистрирован Пользователь.

112 | 113 |

11. Перечень действий, производимых Оператором с полученными персональными данными

114 |

11.1. Оператор осуществляет сбор, запись, систематизацию, накопление, хранение, уточнение (обновление, изменение), извлечение, использование, передачу (распространение, предоставление, доступ), обезличивание, блокирование, удаление и уничтожение персональных данных.

115 |

11.2. Оператор осуществляет автоматизированную обработку персональных данных с получением и/или передачей полученной информации по информационно-телекоммуникационным сетям или без таковой.

116 | 117 |

12. Трансграничная передача персональных данных

118 |

12.1. Оператор до начала осуществления трансграничной передачи персональных данных обязан убедиться в том, что иностранным государством, на территорию которого предполагается осуществлять передачу персональных данных, обеспечивается надежная защита прав субъектов персональных данных.

119 |

12.2. Трансграничная передача персональных данных на территории иностранных государств, не отвечающих вышеуказанным требованиям, может осуществляться только в случае наличия согласия в письменной форме субъекта персональных данных на трансграничную передачу его персональных данных и/или исполнения договора, стороной которого является субъект персональных данных.

120 | 121 |

13. Конфиденциальность персональных данных

122 |

Оператор и иные лица, получившие доступ к персональным данным, обязаны не раскрывать третьим лицам и не распространять персональные данные без согласия субъекта персональных данных, если иное не предусмотрено федеральным законом.

123 | 124 |

14. Заключительные положения

125 |

14.1. Пользователь может получить любые разъяснения по интересующим вопросам, касающимся обработки его персональных данных, обратившись к Оператору с помощью электронной почты app@mahoweek.com.

126 |

14.2. В данном документе будут отражены любые изменения политики обработки персональных данных Оператором. Политика действует бессрочно до замены ее новой версией.

127 |

14.3. Актуальная версия Политики в свободном доступе расположена в сети Интернет по адресу https://mahoweek.com/#privacy.

128 |
129 | -------------------------------------------------------------------------------- /src/inc/popup-terms.html: -------------------------------------------------------------------------------- 1 |
2 |

Правила пользования

3 | 4 |

5 |
6 | -------------------------------------------------------------------------------- /src/inc/popup-tour.html: -------------------------------------------------------------------------------- 1 |
2 |

Как всё устроено

3 | 4 |

Рабочая среда — это доска со списками, в которых живут ваши дела с размещёнными на календарной сетке метками.

5 | 6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 | 15 |
16 |

Статус

17 | 18 |

У каждого дела есть статус, по которому можно определить его текущее состояние.

19 | 20 |
    21 |
  • без даты выполнения
  • 22 |
  • с установленным сроком
  • 23 |
  • намечено на сегодня
  • 24 |
  • просрочено или невыполнено
  • 25 |
  • успешно завершено
  • 26 |
27 |
28 |
29 | 30 |
31 |

Календарная сетка

32 | 33 |

Календарная сетка совмещена со списком дел — это наглядно и позволяет мгновенно ориентироваться в сроках по каждой задаче и следить за своей продуктивностью на текущей неделе.

34 | 35 |

В нужное время календарь также покажет праздничные и выходные дни.

36 | 37 |
38 |
39 | 40 |
41 |
42 |
43 |
44 | 45 |
46 |

Метки

47 | 48 |

Метки — предполагаемые даты выполнения задач. Их может быть как одна, так и несколько, а может и не быть — всё зависит от дела.

49 | 50 |
    51 |
  • намечено на будущие дни
  • 52 |
  • намечено на сегодня
  • 53 |
  • просрочено или невыполнено
  • 54 |
  • успешно завершено
  • 55 |
56 |
57 |
58 | 59 |
60 |

Форматирование дела

61 | 62 |

При создании или редактировании задачи можно сформировать её внешний вид.

63 | 64 |
65 |
66 |

17:00
67 | Добавляет время выполнения

68 | 69 |
70 |
71 | 72 |
73 |

!
74 | Выделяет дело среди других

75 | 76 |
77 |
78 | 79 |
80 |

сегодня / завтра
81 | Добавляет соответствующую метку

82 | 83 |
84 |
85 |
86 |
87 | 88 |

Сортировка

89 | 90 |

Изменять порядок дел можно как внутри списка, так и переносить их между листами. Просто нажмите и ненадолго задержите кнопку мыши на задаче и можно передвигать. Та же схема работает и с целыми списками.

91 |
92 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mahoweek: краткосрочный план дел 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | @@include('inc/metrika.html') 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 |
90 | 91 |
92 | 93 |
94 | 97 | 98 | 123 | 124 |
125 | 129 | 130 | 134 | 135 | 139 | 140 | 144 | 145 | 149 |
150 |
151 | 152 |
153 |
154 | 155 | 170 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | // Core 5 | //------------------------------------------------------------------------------ 6 | 7 | @@include('global.js') 8 | 9 | 10 | // Blocks 11 | //------------------------------------------------------------------------------ 12 | 13 | @@include('../blocks/cartonbox/cartonbox.js') 14 | @@include('../blocks/tinycon/tinycon.js') 15 | @@include('../blocks/sync/sync.js') 16 | @@include('../blocks/form/form.js') 17 | @@include('../blocks/grid/grid.js') 18 | @@include('../blocks/list/list.js') 19 | @@include('../blocks/task/task.js') 20 | @@include('../blocks/focus/focus.js') 21 | -------------------------------------------------------------------------------- /src/less/global.less: -------------------------------------------------------------------------------- 1 | // General 2 | //------------------------------------------------------------------------------ 3 | 4 | html { 5 | box-sizing: border-box; 6 | overflow-x: hidden; 7 | overflow-y: scroll; 8 | cursor: default; 9 | -webkit-font-smoothing: antialiased; 10 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 11 | 12 | @media (min-width: 992px) { 13 | overflow-x: initial; 14 | } 15 | } 16 | 17 | *, 18 | ::after, 19 | ::before { 20 | box-sizing: inherit; 21 | } 22 | 23 | body { 24 | min-width: 320px; 25 | font-size: @font-size; 26 | line-height: @line-height; 27 | font-family: @font-family; 28 | font-display: swap; 29 | color: @dark-primary; 30 | fill: @dark-primary; 31 | background-color: @dark-primary; 32 | 33 | &.mobile { 34 | user-select: none; 35 | } 36 | } 37 | 38 | 39 | // Headers 40 | //------------------------------------------------------------------------------ 41 | 42 | h1 { 43 | position: relative; 44 | margin: 0; 45 | font-weight: normal; 46 | font-size: 18px; 47 | line-height: 30px; 48 | font-family: Verdana, @font-family; 49 | text-transform: uppercase; 50 | letter-spacing: 0.1em; 51 | } 52 | 53 | h2, 54 | .h2 { 55 | position: relative; 56 | margin-top: 0; 57 | margin-bottom: 15px; 58 | font-weight: normal; 59 | font-size: 24px; 60 | line-height: 30px; 61 | font-family: Verdana, @font-family; 62 | text-transform: uppercase; 63 | letter-spacing: 0.05em; 64 | 65 | @media (min-width: 768px) { 66 | margin-bottom: 25px !important; 67 | } 68 | } 69 | 70 | h3, 71 | .h3 { 72 | margin-top: 0; 73 | margin-bottom: 15px; 74 | font-weight: normal; 75 | font-size: 18px; 76 | line-height: 24px; 77 | font-family: Verdana, @font-family; 78 | text-transform: uppercase; 79 | letter-spacing: 0.05em; 80 | } 81 | 82 | 83 | // Links 84 | //------------------------------------------------------------------------------ 85 | 86 | a { 87 | color: @link-color; 88 | text-decoration: none; 89 | outline: none; 90 | 91 | body:not(.mobile) &:hover { 92 | color: @link-color-hover; 93 | } 94 | } 95 | 96 | 97 | // Buttons 98 | //------------------------------------------------------------------------------ 99 | 100 | .btn { 101 | position: relative; 102 | display: inline-block; 103 | min-width: 180px; 104 | padding: 10px 20px; 105 | line-height: @line-height; 106 | font-family: @font-family; 107 | text-align: center; 108 | text-decoration: none; 109 | white-space: nowrap; 110 | border: 0; 111 | border-radius: (@radius * 5.5); 112 | outline: none; 113 | cursor: pointer; 114 | 115 | body:not(.mobile) &:active { 116 | top: 1px; 117 | } 118 | 119 | &--default { 120 | padding: 9px 19px; 121 | color: @dark-primary !important; 122 | fill: @dark-primary !important; 123 | background-color: @btn-default-bg; 124 | border: 1px solid @dark-hint; 125 | 126 | body:not(.mobile) &:hover { 127 | background-color: @btn-default-bg-hover; 128 | } 129 | } 130 | 131 | &--primary { 132 | color: @light-primary !important; 133 | fill: @light-primary !important; 134 | background-color: @btn-primary-bg; 135 | 136 | body:not(.mobile) &:hover { 137 | background-color: @btn-primary-bg-hover; 138 | } 139 | } 140 | 141 | &--donate { 142 | color: @dark-primary !important; 143 | fill: @dark-primary !important; 144 | background-color: @btn-donate-bg; 145 | 146 | body:not(.mobile) &:hover { 147 | background-color: @btn-donate-bg-hover; 148 | } 149 | } 150 | 151 | &--danger { 152 | color: @light-primary !important; 153 | fill: @light-primary !important; 154 | background-color: @btn-danger-bg; 155 | 156 | body:not(.mobile) &:hover { 157 | background-color: @btn-danger-bg-hover; 158 | } 159 | } 160 | } 161 | 162 | 163 | // Forms 164 | //------------------------------------------------------------------------------ 165 | 166 | fieldset { 167 | margin: 0; 168 | padding: 0; 169 | border: 0; 170 | } 171 | 172 | legend { 173 | .h3; 174 | width: 100%; 175 | } 176 | 177 | input[type="text"] { 178 | width: 100%; 179 | padding: 0; 180 | line-height: @line-height; 181 | font-family: @font-family; 182 | color: @dark-primary; 183 | background-color: transparent; 184 | border: 0; 185 | outline: none; 186 | 187 | &::placeholder { 188 | color: @dark-hint; 189 | } 190 | 191 | &::-ms-clear { 192 | display: none; 193 | } 194 | } 195 | 196 | select { 197 | @icon: escape(""); 198 | 199 | margin: 0 -5px; 200 | padding: 0 18px 0 5px; 201 | line-height: @line-height; 202 | font-family: @font-family; 203 | color: @dark-primary; 204 | background-color: transparent; 205 | background-image: url("data:image/svg+xml, @{icon}") !important; 206 | background-repeat: no-repeat; 207 | background-position: calc(~"100% + 3px") calc(~"50% - 1px"); 208 | background-size: 25px 25px; 209 | border: 0; 210 | outline: none; 211 | cursor: pointer; 212 | appearance: none; 213 | } 214 | 215 | button, 216 | [role="button"] { 217 | font-family: @font-family; 218 | outline: none; 219 | cursor: pointer; 220 | } 221 | 222 | 223 | // Base 224 | //------------------------------------------------------------------------------ 225 | 226 | p { 227 | margin-top: 0; 228 | margin-bottom: 15px; 229 | } 230 | 231 | svg { 232 | transform: translateZ(0); 233 | } 234 | 235 | 236 | // Helpers 237 | //------------------------------------------------------------------------------ 238 | 239 | .hidden { 240 | display: none; 241 | } 242 | 243 | .text-center { 244 | text-align: center; 245 | } 246 | 247 | .small { 248 | font-size: @font-size-small; 249 | line-height: (@font-size * 1.25); 250 | } 251 | 252 | .center-block { 253 | display: block; 254 | margin-right: auto; 255 | margin-left: auto; 256 | } 257 | 258 | .gray { 259 | color: @dark-secondary; 260 | } 261 | 262 | .pt-sm-10 { 263 | @media (min-width: 768px) { 264 | padding-top: 10px; 265 | } 266 | } 267 | 268 | .list-custom { 269 | margin-bottom: 15px; 270 | padding-left: 23px; 271 | list-style: none; 272 | 273 | li { 274 | margin-bottom: 5px; 275 | 276 | &:last-child { 277 | margin-bottom: 0; 278 | } 279 | 280 | &::before { 281 | position: absolute; 282 | margin-left: -23px; 283 | opacity: 0.5; 284 | } 285 | } 286 | 287 | &--status li { 288 | &::before { 289 | content: ""; 290 | width: 15px; 291 | height: 15px; 292 | margin-top: 5px; 293 | border: 2px solid; 294 | border-color: @task-pea; 295 | border-radius: 50%; 296 | opacity: 1; 297 | } 298 | 299 | &:nth-child(2)::before { 300 | background-color: @task-pea; 301 | } 302 | 303 | &:nth-child(3)::before { 304 | background-color: @task-pea-today; 305 | border-color: @task-pea-today; 306 | } 307 | 308 | &:nth-child(4)::before { 309 | border-color: @task-pea-past; 310 | } 311 | 312 | &:nth-child(5)::before { 313 | background-color: @task-pea-complete; 314 | border-color: @task-pea-complete; 315 | transform: scale3d(0.4666, 0.4666, 1); 316 | } 317 | } 318 | 319 | &--markers li { 320 | &::before { 321 | content: ""; 322 | width: 11px; 323 | height: 11px; 324 | margin-top: 7px; 325 | background-color: @grid-pea; 326 | border-radius: 50%; 327 | opacity: 1; 328 | } 329 | 330 | &:nth-child(2)::before { 331 | background-color: @grid-pea-today; 332 | } 333 | 334 | &:nth-child(3)::before { 335 | background-color: @task-pea-past; 336 | transform: scale3d(0.6363, 0.6363, 1); 337 | } 338 | 339 | &:nth-child(4)::before { 340 | background-color: @grid-pea-complete; 341 | transform: scale3d(0.6363, 0.6363, 1); 342 | } 343 | } 344 | } 345 | 346 | ul.list-custom:not(.list-custom--status):not(.list-custom--markers) li::before { 347 | content: "—"; 348 | } 349 | 350 | ol.list-custom { 351 | counter-reset: code; 352 | 353 | li::before { 354 | content: counter(code)"."; 355 | counter-increment: code; 356 | } 357 | } 358 | 359 | 360 | // Animation 361 | //------------------------------------------------------------------------------ 362 | 363 | @keyframes fadeIn { 364 | 0% { 365 | opacity: 0; 366 | } 367 | 368 | 100% { 369 | opacity: 1; 370 | } 371 | } 372 | 373 | @keyframes fadeOut { 374 | 0% { 375 | opacity: 1; 376 | } 377 | 378 | 100% { 379 | opacity: 0; 380 | } 381 | } 382 | 383 | @keyframes fadeInDown { 384 | 0% { 385 | transform: translate3d(0, -10px, 0); 386 | opacity: 0; 387 | } 388 | 389 | 100% { 390 | transform: translate3d(0, 0, 0); 391 | opacity: 1; 392 | } 393 | } 394 | 395 | @keyframes fadeOutDown { 396 | 0% { 397 | transform: translate3d(0, 0, 0); 398 | opacity: 1; 399 | } 400 | 401 | 100% { 402 | transform: translate3d(0, 10px, 0); 403 | opacity: 0; 404 | } 405 | } 406 | 407 | @keyframes fadeInUp { 408 | 0% { 409 | transform: translate3d(0, 10px, 0); 410 | opacity: 0; 411 | } 412 | 413 | 100% { 414 | transform: translate3d(0, 0, 0); 415 | opacity: 1; 416 | } 417 | } 418 | 419 | @keyframes fadeOutUp { 420 | 0% { 421 | transform: translate3d(0, 0, 0); 422 | opacity: 1; 423 | } 424 | 425 | 100% { 426 | transform: translate3d(0, -10px, 0); 427 | opacity: 0; 428 | } 429 | } 430 | 431 | @keyframes fallIn { 432 | 0% { 433 | transform: scale3d(1.05, 1.05, 1); 434 | opacity: 0; 435 | } 436 | 437 | 100% { 438 | transform: scale3d(1, 1, 1); 439 | opacity: 1; 440 | } 441 | } 442 | 443 | @keyframes spin { 444 | 0% { 445 | transform: rotate(0deg); 446 | } 447 | 448 | 100% { 449 | transform: rotate(360deg); 450 | } 451 | } 452 | 453 | @keyframes chosen { 454 | 0% { 455 | transform: scale3d(1, 1, 1); 456 | } 457 | 458 | 50% { 459 | transform: scale3d(1.01, 1.01, 1); 460 | } 461 | 462 | 100% { 463 | transform: scale3d(1, 1, 1); 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /src/less/main.less: -------------------------------------------------------------------------------- 1 | // Core 2 | //------------------------------------------------------------------------------ 3 | 4 | @import "variables"; 5 | @import "global"; 6 | 7 | 8 | // Blocks 9 | //------------------------------------------------------------------------------ 10 | 11 | @import "../blocks/cartonbox/cartonbox"; 12 | @import "../blocks/board/board"; 13 | @import "../blocks/logo/logo"; 14 | @import "../blocks/menu/menu"; 15 | @import "../blocks/focus/focus"; 16 | @import "../blocks/list/list"; 17 | @import "../blocks/task/task"; 18 | @import "../blocks/grid/grid"; 19 | @import "../blocks/bricks/bricks"; 20 | @import "../blocks/sync/sync"; 21 | @import "../blocks/form/form"; 22 | @import "../blocks/chisel/chisel"; 23 | @import "../blocks/view/view"; 24 | @import "../blocks/share/share"; 25 | @import "../blocks/donate/donate"; 26 | -------------------------------------------------------------------------------- /src/less/variables.less: -------------------------------------------------------------------------------- 1 | // Core 2 | //------------------------------------------------------------------------------ 3 | 4 | @font-size: 16px; 5 | @font-size-small: 14px; 6 | @line-height: (@font-size * 1.5); 7 | @font-family: Arial, sans-serif, "Apple Color Emoji", "NotoColorEmoji", "Segoe UI Emoji", "Segoe UI Symbol"; 8 | 9 | @radius: 4px; 10 | 11 | @duration: 0.2s; 12 | @timing-function: cubic-bezier(0.39, 0.575, 0.565, 1); 13 | 14 | 15 | // Color 16 | //------------------------------------------------------------------------------ 17 | 18 | @dark-primary: lighten(#000, 13%); 19 | @dark-secondary: lighten(#000, 46%); 20 | @dark-hint: lighten(#000, 62%); 21 | @dark-divider: lighten(#000, 88%); 22 | 23 | @light-primary: #fff; 24 | @light-secondary: darken(#fff, 30%); 25 | @light-hint: darken(#fff, 50%); 26 | @light-divider: darken(#fff, 88%); 27 | 28 | @color-red: #F44336; 29 | @color-yellow: #FDD835; 30 | @color-green: #4cd964; 31 | @color-teal-blue: #5ac8fa; 32 | @color-blue: #1c7de6; 33 | 34 | 35 | // Logo 36 | //------------------------------------------------------------------------------ 37 | 38 | @logo-dark: @dark-primary; 39 | @logo-light: @light-primary; 40 | 41 | @logo-pea: @color-green; 42 | 43 | 44 | // Menu 45 | //------------------------------------------------------------------------------ 46 | 47 | @menu-link: fade(@dark-primary, 30%); 48 | @menu-link-hover: fade(@dark-primary, 40%); 49 | 50 | 51 | // Button 52 | //------------------------------------------------------------------------------ 53 | 54 | @btn-default-bg: @light-primary; 55 | @btn-default-bg-hover: darken(@btn-default-bg, 5%); 56 | 57 | @btn-primary-bg: darken(@task-pea-complete, 15%); 58 | @btn-primary-bg-hover: darken(@btn-primary-bg, 5%); 59 | 60 | @btn-donate-bg: @color-yellow; 61 | @btn-donate-bg-hover: darken(@btn-donate-bg, 5%); 62 | 63 | @btn-danger-bg: @color-red; 64 | @btn-danger-bg-hover: darken(@btn-danger-bg, 5%); 65 | 66 | 67 | // Checkbox 68 | //------------------------------------------------------------------------------ 69 | 70 | @checkbox-bg: @light-secondary; 71 | @checkbox-checked-bg: @logo-pea; 72 | 73 | 74 | // Link 75 | //------------------------------------------------------------------------------ 76 | 77 | @link-color: @color-blue; 78 | @link-color-hover: @color-red; 79 | 80 | 81 | // List 82 | //------------------------------------------------------------------------------ 83 | 84 | @list-bg: @task-bg; 85 | @list-bg-header: @light-primary; 86 | @list-border: @dark-divider; 87 | 88 | @list-progress-first: @color-teal-blue; 89 | @list-progress-second: @logo-pea; 90 | 91 | @list-trash: lighten(@color-red, 13%); 92 | @list-trash-hover: @color-red; 93 | 94 | 95 | // Task 96 | //------------------------------------------------------------------------------ 97 | 98 | @task-bg: darken(@light-primary, 3%); 99 | @task-bg-hover: @list-bg-header; 100 | @task-border: @list-border; 101 | 102 | @task-pea: darken(@list-border, 5%); 103 | @task-pea-hover: darken(@task-pea, 10%); 104 | @task-pea-past: @list-trash; 105 | @task-pea-past-hover: @list-trash-hover; 106 | @task-pea-today: @color-yellow; 107 | @task-pea-today-hover: darken(@grid-pea-today, 13%); 108 | @task-pea-complete: @logo-pea; 109 | @task-pea-complete-hover: darken(@task-pea-complete, 10%); 110 | 111 | @task-trash: @list-trash; 112 | @task-trash-hover: @list-trash-hover; 113 | 114 | 115 | // Grid 116 | //------------------------------------------------------------------------------ 117 | 118 | @grid-bg: darken(#fafafa, 3%); 119 | @grid-bg-hover: #fafafa; 120 | @grid-bg-header: @grid-bg-hover; 121 | @grid-border: @list-border; 122 | @grid-separator: darken(@grid-border, 10%); 123 | @grid-past-bg: darken(@grid-border, 5%); 124 | @grid-today-bg: #FFF8E1; 125 | @grid-today-border: darken(@grid-border, 7%); 126 | @grid-holiday-border: @list-trash; 127 | 128 | @grid-pea: @task-pea; 129 | @grid-pea-hover: @task-pea-hover; 130 | @grid-pea-today: @task-pea-today; 131 | @grid-pea-today-hover: @task-pea-today-hover; 132 | @grid-pea-complete: @logo-pea; 133 | @grid-pea-past: lighten(@color-red, 31%); 134 | @grid-pea-past-complete: lighten(@logo-pea, 23%); 135 | 136 | 137 | // Sync 138 | //------------------------------------------------------------------------------ 139 | 140 | @sync-process: lighten(@color-yellow, 3%); 141 | @sync-ok: lighten(@color-green, 3%); 142 | @sync-fail: lighten(@color-red, 5%); 143 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "ru", 3 | "dir": "ltr", 4 | "name": "Mahoweek: краткосрочный план дел", 5 | "short_name": "Mahoweek", 6 | "icons": [ 7 | { 8 | "src": "android-chrome-192x192.png?v=2", 9 | "sizes": "192x192", 10 | "type": "image/png" 11 | } 12 | ], 13 | "theme_color": "#212121", 14 | "background_color": "#212121", 15 | "start_url": "https://mahoweek.com", 16 | "orientation": "any", 17 | "display": "standalone", 18 | "scope": "/" 19 | } 20 | -------------------------------------------------------------------------------- /src/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Host: https://mahoweek.com 3 | Sitemap: https://mahoweek.com/sitemap.xml 4 | -------------------------------------------------------------------------------- /src/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | https://mahoweek.com/ 6 | 1 7 | always 8 | 9 | 10 | --------------------------------------------------------------------------------