diff --git a/.gitignore b/.gitignore index 5756bac73..bc2d203d1 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.* *.diff *.err *.orig @@ -12,21 +11,31 @@ # OS or Editor folders .DS_Store +.idea .cache .project .settings .tmproj -nbproject +.nvmrc Thumbs.db # NPM packages folder. node_modules/ -# Brunch folder for temporary files. +# TMP folder (anything) tmp/ -# Brunch output folder. -www/ +# contains v8 executable for linux-tick-processor (run from project root) +out/* + +# Generated content +www/* +app/stylesheets/sprites/* # Bower stuff. bower_components/ + +# Passwords and other secret stuff +modules/config/secret.js +modules/config/certs/* + diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 000000000..5f3a922bb --- /dev/null +++ b/.jshintrc @@ -0,0 +1,15 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "-W004": true, + "-W030": true // for yield* ... +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..0eb41820e --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +0.11 \ No newline at end of file diff --git a/README.md b/README.md index 83b578c01..c2d2bc254 100755 --- a/README.md +++ b/README.md @@ -1,46 +1,44 @@ -**Всем желающим предлагается поучаствовать в разработке новой версии сайта http://javascript.ru на Node.JS, Open Source on GitHub.** - -О проекте: - -* Это сайт по JavaScript и смежным технологиям (AJAX, COMET, Browser APIs...) -* Сайт достаточно большой и сложный. В новом проекте предусмотрены разделы: - * учебник (с генерацией PDF) - * вопрос-ответ - * тесты знаний - * онлайн-курсы - * справочник - * события - * работа -* Логин через соц. сети в том числе, личные сообщения и профиль. + +# Новый javascript.ru + +## powered by Node.js + +Всем привет! + +А это исходный код для нового движка сайта http://javascript.ru. + +## Что делаем? + +* Сайт по JavaScript и смежным технологиям (AJAX, COMET, Browser APIs...) * Сайт достаточно посещаемый: порядка 1-1.5 млн просмотров в месяц, и их станет больше при успешной реализации. -* Планируется перевод учебника на английский, после реализации на русском. -* Основная аудитория - разработчики, так что поддержка старых IE не нужна. Совсем. +* Сайт быстрый, при помощи Node.JS и правильного фронтенда он будет "летать". +* Сайт пока на русском, на английском сделаем потом. +* Сайт для разработчиков, да, кстати, они не пользуются старыми и страшными IE. + +С элементами SPA, но не SPA, так как должен хорошо индексироваться поисковиками, и вообще нафига козе баян. + +## Что в опен-сорсе? + +В опен-сорсе весь код, который будет заставлять двигаться эту штуку. +Многие модули из него можно взять и выделить в отдельные проекты, мы об этом подумаем, потом. + +Также в опен-сорсе - учебник JavaScript. Правда, это в другом репозитарии, здесь только код. + +## ♡ + +Пишите в issues, если есть о чём. + + -Так как сайт должен хорошо индексироваться поисковиками, он будет состоять из страниц с переходом между ними, не SPA. Хотя в различных интерфейсах элементы SPA приветствуются. -Мы будем стараться, чтобы сайт работал как можно быстрее. Это означает параллельные запросы к БД и кеширование на сервере и, по возможности, плавную инициализацию на клиенте. -Сейчас есть существенная часть дизайна и его вёрстка в HTML/SASS. -Общий стиль вы можете посмотреть здесь: https://www.dropbox.com/s/mo6yx0ct9rrzic4/Learn_Home.png. -RoadMap: -* Определиться с архитектурой проекта, технологиями. -* Реализовать профиль посетителя, логин через соц. сети, с заглушкой на title-page. -* Реализовать показ учебника и навигацию по нему, древовидные комментарии с оценками, подгрузкой. -* Сделать покупку PDF учебника (оформление, приём оплаты, почтовое уведомление, скачивание). -Это примерно соответствует текущему http://learn.javascript.ru. Когда закончим -- будет первый релиз, вместо старого learn.javascript.ru. -Далее или, если будет возможность, параллельно, реализуем вопрос-ответ, справочник, тесты знаний. -Обсуждение происходит в чате Node.JS (Skype), собрание сегодня 24.06.2014 в 11:00 **GMT+2**. -Если не можете войти - напишите мне в Skype, ник: "ilya.a.kantor". -Code Style: - * https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml - * `use strict` diff --git a/app.js b/app.js deleted file mode 100755 index 0c6865fac..000000000 --- a/app.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; - -const log = require('lib/log')(module); -const config = require('config'); - -const koa = require('koa'); - -require('models'); - -const app = koa(); - -require('setup/static')(app); - -require('setup/errors')(app); - -require('setup/logger')(app); -require('setup/bodyParser')(app); -require('setup/session')(app); -require('setup/render')(app); -require('setup/router')(app); - -require('./routes')(app); - -module.exports = app; \ No newline at end of file diff --git a/app/.jshintrc b/app/.jshintrc new file mode 100644 index 000000000..4b274c1ee --- /dev/null +++ b/app/.jshintrc @@ -0,0 +1,15 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": true, + "node": true, // for browserify require etc + "globals": ["$", "Prism"], + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "multistr": true, + "noyield": true, + "devel": true, + "-W004": true +} diff --git a/app/fonts/icons/icons.eot b/app/fonts/icons/icons.eot new file mode 100644 index 000000000..9a4a5a8c9 Binary files /dev/null and b/app/fonts/icons/icons.eot differ diff --git a/app/fonts/icons/icons.svg b/app/fonts/icons/icons.svg new file mode 100644 index 000000000..5e8c371de --- /dev/null +++ b/app/fonts/icons/icons.svg @@ -0,0 +1,47 @@ + + + +Copyright (C) 2014 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/fonts/icons/icons.ttf b/app/fonts/icons/icons.ttf new file mode 100644 index 000000000..5ef3ad0a4 Binary files /dev/null and b/app/fonts/icons/icons.ttf differ diff --git a/app/fonts/icons/icons.woff b/app/fonts/icons/icons.woff new file mode 100644 index 000000000..a4b2f9291 Binary files /dev/null and b/app/fonts/icons/icons.woff differ diff --git a/app/fonts/rur/alsrubl-verdana-regular.eot b/app/fonts/rur/alsrubl-verdana-regular.eot new file mode 100644 index 000000000..bea8a2db9 Binary files /dev/null and b/app/fonts/rur/alsrubl-verdana-regular.eot differ diff --git a/app/fonts/rur/alsrubl-verdana-regular.svg b/app/fonts/rur/alsrubl-verdana-regular.svg new file mode 100644 index 000000000..41756ecff --- /dev/null +++ b/app/fonts/rur/alsrubl-verdana-regular.svg @@ -0,0 +1,32 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright 2012 Adobe Systems Incorporated All rights reserved +Designer : +Foundry : PYRS Fontlab Ltd Made with FontLab + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/fonts/rur/alsrubl-verdana-regular.ttf b/app/fonts/rur/alsrubl-verdana-regular.ttf new file mode 100644 index 000000000..07696f01a Binary files /dev/null and b/app/fonts/rur/alsrubl-verdana-regular.ttf differ diff --git a/app/fonts/rur/alsrubl-verdana-regular.woff b/app/fonts/rur/alsrubl-verdana-regular.woff new file mode 100644 index 000000000..45fbe0d7b Binary files /dev/null and b/app/fonts/rur/alsrubl-verdana-regular.woff differ diff --git a/app/fonts/rur/stylesheet.css b/app/fonts/rur/stylesheet.css new file mode 100644 index 000000000..215f95fcc --- /dev/null +++ b/app/fonts/rur/stylesheet.css @@ -0,0 +1,43 @@ +/* Generated by Font Squirrel, (http://fontsquirrel.com/) */ +/* Compiled by Artem Polikarpov, Artem Gorbunov Design Bureau (http://artgorbunov.ru/) */ + +@font-face { + font-family: 'ALSRubl-Verdana'; + src: url('alsrubl-verdana-regular.eot'); + src: url('alsrubl-verdana-regular.eot?#iefix') format('embedded-opentype'),url('alsrubl-verdana-regular.woff') format('woff'), url('alsrubl-verdana-regular.ttf') format('truetype'), url('alsrubl-verdana-regular.svg#ALSRublRegular') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'ALSRubl-Verdana'; + src: url('alsrubl-verdana-italic.eot'); + src: url('alsrubl-verdana-italic.eot?#iefix') format('embedded-opentype'), url('alsrubl-verdana-italic.woff') format('woff'), url('alsrubl-verdana-italic.ttf') format('truetype'), url('alsrubl-verdana-italic.svg#ALSRublItalic') format('svg'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'ALSRubl-Verdana'; + src: url('alsrubl-verdana-bold.eot'); + src: url('alsrubl-verdana-bold.eot?#iefix') format('embedded-opentype'), url('alsrubl-verdana-bold.woff') format('woff'), url('alsrubl-verdana-bold.ttf') format('truetype'), url('alsrubl-verdana-bold.svg#ALSRublBold') format('svg'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'ALSRubl-Verdana'; + src: url('alsrubl-verdana-bolditalic.eot'); + src: url('alsrubl-verdana-bolditalic.eot?#iefix') format('embedded-opentype'), url('alsrubl-verdana-bolditalic.woff') format('woff'), url('alsrubl-verdana-bolditalic.ttf') format('truetype'), url('alsrubl-verdana-bolditalic.svg#ALSRublBoldItalic') format('svg'); + font-weight: bold; + font-style: italic; +} + +/* + ХТМЛ: 100 Р + Если вам нужен рубль в нескольких шрифтах, используйте .b-rub_verdana, вместо .b-rub +*/ +.b-rub, .b-rub_verdana { + font-family: 'ALSRubl-Verdana', Verdana, sans-serif; + line-height: normal; +} \ No newline at end of file diff --git a/app/fonts/rur/test.html b/app/fonts/rur/test.html new file mode 100644 index 000000000..6a74b01a4 --- /dev/null +++ b/app/fonts/rur/test.html @@ -0,0 +1,73 @@ + + + + Знак рубля Верданой + + + + + +

Знак рубля Верданой

+ +

Regular

+ +

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+ +
+

Italic

+ +
+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+
+ +
+

Bold

+ +
+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+
+ +
+

Bold Italic

+ +
+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+

100 500 Р

+
+ + + \ No newline at end of file diff --git a/app/img/ball.gif b/app/img/ball.gif new file mode 100644 index 000000000..0b2c177f1 Binary files /dev/null and b/app/img/ball.gif differ diff --git a/app/img/close-button.png b/app/img/close-button.png new file mode 100644 index 000000000..ef327e9e7 Binary files /dev/null and b/app/img/close-button.png differ diff --git a/app/img/content-sidebar-bg.png b/app/img/content-sidebar-bg.png new file mode 100644 index 000000000..121144413 Binary files /dev/null and b/app/img/content-sidebar-bg.png differ diff --git a/app/img/course-register__tail.gif b/app/img/course-register__tail.gif new file mode 100644 index 000000000..f8b57f947 Binary files /dev/null and b/app/img/course-register__tail.gif differ diff --git a/app/img/design-debug.png b/app/img/design-debug.png new file mode 100644 index 000000000..a1e6a23d7 Binary files /dev/null and b/app/img/design-debug.png differ diff --git a/app/img/flag-sb05ed3e5b2.png b/app/img/flag-sb05ed3e5b2.png new file mode 100644 index 000000000..fd77c2a3b Binary files /dev/null and b/app/img/flag-sb05ed3e5b2.png differ diff --git a/app/img/flag/flag_ru.png b/app/img/flag/flag_ru.png new file mode 100644 index 000000000..2cfb9fb28 Binary files /dev/null and b/app/img/flag/flag_ru.png differ diff --git a/app/img/flag/flag_ua.png b/app/img/flag/flag_ua.png new file mode 100644 index 000000000..320a01e9b Binary files /dev/null and b/app/img/flag/flag_ua.png differ diff --git a/app/img/highlights-icons/distance.png b/app/img/highlights-icons/distance.png new file mode 100644 index 000000000..0782ae328 Binary files /dev/null and b/app/img/highlights-icons/distance.png differ diff --git a/app/img/highlights-icons/quality.png b/app/img/highlights-icons/quality.png new file mode 100644 index 000000000..248f84905 Binary files /dev/null and b/app/img/highlights-icons/quality.png differ diff --git a/app/img/highlights-icons/result.png b/app/img/highlights-icons/result.png new file mode 100644 index 000000000..3ad5cdaa5 Binary files /dev/null and b/app/img/highlights-icons/result.png differ diff --git a/app/img/highlights-icons/support.png b/app/img/highlights-icons/support.png new file mode 100644 index 000000000..46afaf38d Binary files /dev/null and b/app/img/highlights-icons/support.png differ diff --git a/app/img/highlights-icons/warranty.png b/app/img/highlights-icons/warranty.png new file mode 100644 index 000000000..cd8afcbde Binary files /dev/null and b/app/img/highlights-icons/warranty.png differ diff --git a/app/img/important-icons-s32ff0be2d7.png b/app/img/important-icons-s32ff0be2d7.png new file mode 100644 index 000000000..c634c0cb6 Binary files /dev/null and b/app/img/important-icons-s32ff0be2d7.png differ diff --git a/app/img/important-icons/info.png b/app/img/important-icons/info.png new file mode 100644 index 000000000..17236c791 Binary files /dev/null and b/app/img/important-icons/info.png differ diff --git a/app/img/important-icons/ok.png b/app/img/important-icons/ok.png new file mode 100644 index 000000000..073ddb754 Binary files /dev/null and b/app/img/important-icons/ok.png differ diff --git a/app/img/important-icons/question.png b/app/img/important-icons/question.png new file mode 100644 index 000000000..c53409bd6 Binary files /dev/null and b/app/img/important-icons/question.png differ diff --git a/app/img/important-icons/warning.png b/app/img/important-icons/warning.png new file mode 100644 index 000000000..ce30b1f20 Binary files /dev/null and b/app/img/important-icons/warning.png differ diff --git a/app/img/interkassa.png b/app/img/interkassa.png new file mode 100644 index 000000000..1f0e4b91d Binary files /dev/null and b/app/img/interkassa.png differ diff --git a/app/img/learning-pyramide.jpg b/app/img/learning-pyramide.jpg new file mode 100644 index 000000000..f7a20588e Binary files /dev/null and b/app/img/learning-pyramide.jpg differ diff --git a/app/img/link-types/doc.png b/app/img/link-types/doc.png new file mode 100644 index 000000000..ad02369dc Binary files /dev/null and b/app/img/link-types/doc.png differ diff --git a/app/img/link-types/ecma.png b/app/img/link-types/ecma.png new file mode 100644 index 000000000..319fa6a12 Binary files /dev/null and b/app/img/link-types/ecma.png differ diff --git a/app/img/link-types/mailto.png b/app/img/link-types/mailto.png new file mode 100644 index 000000000..9cddd1d13 Binary files /dev/null and b/app/img/link-types/mailto.png differ diff --git a/app/img/link-types/mdn.png b/app/img/link-types/mdn.png new file mode 100644 index 000000000..e4d6c9f2c Binary files /dev/null and b/app/img/link-types/mdn.png differ diff --git a/app/img/link-types/msdn.png b/app/img/link-types/msdn.png new file mode 100644 index 000000000..27b86de7d Binary files /dev/null and b/app/img/link-types/msdn.png differ diff --git a/app/img/link-types/newwindow.png b/app/img/link-types/newwindow.png new file mode 100644 index 000000000..38521c4d9 Binary files /dev/null and b/app/img/link-types/newwindow.png differ diff --git a/app/img/link-types/pdf.png b/app/img/link-types/pdf.png new file mode 100644 index 000000000..f5be21a11 Binary files /dev/null and b/app/img/link-types/pdf.png differ diff --git a/app/img/link-types/sandbox.png b/app/img/link-types/sandbox.png new file mode 100644 index 000000000..095527198 Binary files /dev/null and b/app/img/link-types/sandbox.png differ diff --git a/app/img/link-types/w3c.png b/app/img/link-types/w3c.png new file mode 100644 index 000000000..dfcd3d10f Binary files /dev/null and b/app/img/link-types/w3c.png differ diff --git a/app/img/link-types/wiki.png b/app/img/link-types/wiki.png new file mode 100644 index 000000000..b9857a9cc Binary files /dev/null and b/app/img/link-types/wiki.png differ diff --git a/app/img/link-types/xls.png b/app/img/link-types/xls.png new file mode 100644 index 000000000..ef3c95a3c Binary files /dev/null and b/app/img/link-types/xls.png differ diff --git a/app/img/link-types/zip.png b/app/img/link-types/zip.png new file mode 100644 index 000000000..a53b7d7f0 Binary files /dev/null and b/app/img/link-types/zip.png differ diff --git a/app/img/linked-userpic.gif b/app/img/linked-userpic.gif new file mode 100644 index 000000000..4ac42541f Binary files /dev/null and b/app/img/linked-userpic.gif differ diff --git a/app/img/logo-light.png b/app/img/logo-light.png new file mode 100644 index 000000000..58cb6048a Binary files /dev/null and b/app/img/logo-light.png differ diff --git a/app/img/logo-small.png b/app/img/logo-small.png new file mode 100644 index 000000000..82a3aafc8 Binary files /dev/null and b/app/img/logo-small.png differ diff --git a/app/img/logo.png b/app/img/logo.png new file mode 100644 index 000000000..eae73179d Binary files /dev/null and b/app/img/logo.png differ diff --git a/app/img/navbar-bg.png b/app/img/navbar-bg.png new file mode 100644 index 000000000..e84f2460a Binary files /dev/null and b/app/img/navbar-bg.png differ diff --git a/app/img/noisy.png b/app/img/noisy.png new file mode 100644 index 000000000..e08522a18 Binary files /dev/null and b/app/img/noisy.png differ diff --git a/app/img/page-central-cloud.png b/app/img/page-central-cloud.png new file mode 100644 index 000000000..05d434a68 Binary files /dev/null and b/app/img/page-central-cloud.png differ diff --git a/app/img/page-footer.png b/app/img/page-footer.png new file mode 100644 index 000000000..4c165cc2d Binary files /dev/null and b/app/img/page-footer.png differ diff --git a/app/img/page-left-cloud.png b/app/img/page-left-cloud.png new file mode 100644 index 000000000..dbf0bc14c Binary files /dev/null and b/app/img/page-left-cloud.png differ diff --git a/app/img/page-right-cloud.png b/app/img/page-right-cloud.png new file mode 100644 index 000000000..c1c2d2586 Binary files /dev/null and b/app/img/page-right-cloud.png differ diff --git a/app/img/pager-scroll.png b/app/img/pager-scroll.png new file mode 100644 index 000000000..054e77b4c Binary files /dev/null and b/app/img/pager-scroll.png differ diff --git a/app/img/pay-method__bank-bill.png b/app/img/pay-method__bank-bill.png new file mode 100644 index 000000000..bb71ffc61 Binary files /dev/null and b/app/img/pay-method__bank-bill.png differ diff --git a/app/img/pay-method__interkassa.png b/app/img/pay-method__interkassa.png new file mode 100644 index 000000000..c2cd8f57d Binary files /dev/null and b/app/img/pay-method__interkassa.png differ diff --git a/app/img/pay-method__payanyway.png b/app/img/pay-method__payanyway.png new file mode 100644 index 000000000..78765b3c8 Binary files /dev/null and b/app/img/pay-method__payanyway.png differ diff --git a/app/img/pay-method__paypal.png b/app/img/pay-method__paypal.png new file mode 100644 index 000000000..a0210dcb6 Binary files /dev/null and b/app/img/pay-method__paypal.png differ diff --git a/app/img/pay-method__webmoney.png b/app/img/pay-method__webmoney.png new file mode 100644 index 000000000..98bd73539 Binary files /dev/null and b/app/img/pay-method__webmoney.png differ diff --git a/app/img/pay-method__yamoney.png b/app/img/pay-method__yamoney.png new file mode 100644 index 000000000..472491980 Binary files /dev/null and b/app/img/pay-method__yamoney.png differ diff --git a/app/img/payanyway.png b/app/img/payanyway.png new file mode 100644 index 000000000..81f6347da Binary files /dev/null and b/app/img/payanyway.png differ diff --git a/app/img/paypal.png b/app/img/paypal.png new file mode 100644 index 000000000..b65e5fa6c Binary files /dev/null and b/app/img/paypal.png differ diff --git a/app/img/presenter.jpg b/app/img/presenter.jpg new file mode 100644 index 000000000..63e6a68bc Binary files /dev/null and b/app/img/presenter.jpg differ diff --git a/app/img/profile-insert-tail.png b/app/img/profile-insert-tail.png new file mode 100644 index 000000000..3dbaa9a38 Binary files /dev/null and b/app/img/profile-insert-tail.png differ diff --git a/app/img/profile__confirmed.png b/app/img/profile__confirmed.png new file mode 100644 index 000000000..894f5b4a8 Binary files /dev/null and b/app/img/profile__confirmed.png differ diff --git a/app/img/receipts__bg.png b/app/img/receipts__bg.png new file mode 100644 index 000000000..7fb1914d2 Binary files /dev/null and b/app/img/receipts__bg.png differ diff --git a/app/img/receipts__separator.png b/app/img/receipts__separator.png new file mode 100644 index 000000000..247b6262d Binary files /dev/null and b/app/img/receipts__separator.png differ diff --git a/app/img/reviewer.jpg b/app/img/reviewer.jpg new file mode 100644 index 000000000..d476d0423 Binary files /dev/null and b/app/img/reviewer.jpg differ diff --git a/app/img/reviews-arrows-sea1675148f.png b/app/img/reviews-arrows-sea1675148f.png new file mode 100644 index 000000000..4be48f223 Binary files /dev/null and b/app/img/reviews-arrows-sea1675148f.png differ diff --git a/app/img/reviews-arrows/next.png b/app/img/reviews-arrows/next.png new file mode 100644 index 000000000..9acc16e8c Binary files /dev/null and b/app/img/reviews-arrows/next.png differ diff --git a/app/img/reviews-arrows/prev.png b/app/img/reviews-arrows/prev.png new file mode 100644 index 000000000..9d36c45be Binary files /dev/null and b/app/img/reviews-arrows/prev.png differ diff --git a/app/img/reviews-speech.gif b/app/img/reviews-speech.gif new file mode 100644 index 000000000..ad317dfe5 Binary files /dev/null and b/app/img/reviews-speech.gif differ diff --git a/app/img/sberbank.png b/app/img/sberbank.png new file mode 100644 index 000000000..b4409ee26 Binary files /dev/null and b/app/img/sberbank.png differ diff --git a/app/img/scales.png b/app/img/scales.png new file mode 100644 index 000000000..9e774bf4b Binary files /dev/null and b/app/img/scales.png differ diff --git a/app/img/select2-spinner.gif b/app/img/select2-spinner.gif new file mode 100644 index 000000000..5b33f7e54 Binary files /dev/null and b/app/img/select2-spinner.gif differ diff --git a/app/img/select2.png b/app/img/select2.png new file mode 100644 index 000000000..1d804ffb9 Binary files /dev/null and b/app/img/select2.png differ diff --git a/app/img/select2x2.png b/app/img/select2x2.png new file mode 100644 index 000000000..4bdd5c961 Binary files /dev/null and b/app/img/select2x2.png differ diff --git a/app/img/soc-icon-sa672b2e800.png b/app/img/soc-icon-sa672b2e800.png new file mode 100644 index 000000000..d2922f468 Binary files /dev/null and b/app/img/soc-icon-sa672b2e800.png differ diff --git a/app/img/soc-icon.png b/app/img/soc-icon.png new file mode 100644 index 000000000..e3686899c Binary files /dev/null and b/app/img/soc-icon.png differ diff --git a/app/img/soc-icon/facebook.png b/app/img/soc-icon/facebook.png new file mode 100644 index 000000000..dfb94bc83 Binary files /dev/null and b/app/img/soc-icon/facebook.png differ diff --git a/app/img/soc-icon/google.png b/app/img/soc-icon/google.png new file mode 100644 index 000000000..d4d94951d Binary files /dev/null and b/app/img/soc-icon/google.png differ diff --git a/app/img/soc-icon/twitter.png b/app/img/soc-icon/twitter.png new file mode 100644 index 000000000..bd610c8a3 Binary files /dev/null and b/app/img/soc-icon/twitter.png differ diff --git a/app/img/soc-icon/vk.png b/app/img/soc-icon/vk.png new file mode 100644 index 000000000..e8b827e34 Binary files /dev/null and b/app/img/soc-icon/vk.png differ diff --git a/app/img/social-link-icons-s2c10b6095d.png b/app/img/social-link-icons-s2c10b6095d.png new file mode 100644 index 000000000..874e28448 Binary files /dev/null and b/app/img/social-link-icons-s2c10b6095d.png differ diff --git a/app/img/social-link-icons/fb.png b/app/img/social-link-icons/fb.png new file mode 100644 index 000000000..2d2811d91 Binary files /dev/null and b/app/img/social-link-icons/fb.png differ diff --git a/app/img/social-link-icons/gh.png b/app/img/social-link-icons/gh.png new file mode 100644 index 000000000..2f24bf141 Binary files /dev/null and b/app/img/social-link-icons/gh.png differ diff --git a/app/img/social-link-icons/gp.png b/app/img/social-link-icons/gp.png new file mode 100644 index 000000000..cbbff2abd Binary files /dev/null and b/app/img/social-link-icons/gp.png differ diff --git a/app/img/social-link-icons/tw.png b/app/img/social-link-icons/tw.png new file mode 100644 index 000000000..9382557d8 Binary files /dev/null and b/app/img/social-link-icons/tw.png differ diff --git a/app/img/social-link-icons/vk.png b/app/img/social-link-icons/vk.png new file mode 100644 index 000000000..16eab6991 Binary files /dev/null and b/app/img/social-link-icons/vk.png differ diff --git a/app/img/social-login-icons-s8d483929bc.png b/app/img/social-login-icons-s8d483929bc.png new file mode 100644 index 000000000..be0e366cc Binary files /dev/null and b/app/img/social-login-icons-s8d483929bc.png differ diff --git a/app/img/social-login-icons/facebook.png b/app/img/social-login-icons/facebook.png new file mode 100644 index 000000000..1e641a7f8 Binary files /dev/null and b/app/img/social-login-icons/facebook.png differ diff --git a/app/img/social-login-icons/github.png b/app/img/social-login-icons/github.png new file mode 100644 index 000000000..9eaf537bb Binary files /dev/null and b/app/img/social-login-icons/github.png differ diff --git a/app/img/social-login-icons/google.png b/app/img/social-login-icons/google.png new file mode 100644 index 000000000..1d11af62c Binary files /dev/null and b/app/img/social-login-icons/google.png differ diff --git a/app/img/social-login-icons/twitter.png b/app/img/social-login-icons/twitter.png new file mode 100644 index 000000000..bb8c964d7 Binary files /dev/null and b/app/img/social-login-icons/twitter.png differ diff --git a/app/img/social-login-icons/vkontakte.png b/app/img/social-login-icons/vkontakte.png new file mode 100644 index 000000000..bdb1953df Binary files /dev/null and b/app/img/social-login-icons/vkontakte.png differ diff --git a/app/img/social-login-separator.png b/app/img/social-login-separator.png new file mode 100644 index 000000000..bbad31999 Binary files /dev/null and b/app/img/social-login-separator.png differ diff --git a/app/img/text-marked-icon-sf7bebd1e6f.png b/app/img/text-marked-icon-sf7bebd1e6f.png new file mode 100644 index 000000000..82fb09306 Binary files /dev/null and b/app/img/text-marked-icon-sf7bebd1e6f.png differ diff --git a/app/img/text-marked-icon/ok.png b/app/img/text-marked-icon/ok.png new file mode 100644 index 000000000..062cde286 Binary files /dev/null and b/app/img/text-marked-icon/ok.png differ diff --git a/app/img/text-marked-icon/unknown.png b/app/img/text-marked-icon/unknown.png new file mode 100644 index 000000000..9f59f78cd Binary files /dev/null and b/app/img/text-marked-icon/unknown.png differ diff --git a/app/img/top-part.png b/app/img/top-part.png new file mode 100644 index 000000000..b036c50fb Binary files /dev/null and b/app/img/top-part.png differ diff --git a/app/img/user.png b/app/img/user.png new file mode 100644 index 000000000..e25ea4e6f Binary files /dev/null and b/app/img/user.png differ diff --git a/app/img/userpic.gif b/app/img/userpic.gif new file mode 100644 index 000000000..e94d8de4d Binary files /dev/null and b/app/img/userpic.gif differ diff --git a/app/img/userpic.png b/app/img/userpic.png new file mode 100644 index 000000000..606505d2e Binary files /dev/null and b/app/img/userpic.png differ diff --git a/app/img/webmoney.png b/app/img/webmoney.png new file mode 100644 index 000000000..c8fc4828e Binary files /dev/null and b/app/img/webmoney.png differ diff --git a/app/img/x.gif b/app/img/x.gif new file mode 100644 index 000000000..7c8e9e98f Binary files /dev/null and b/app/img/x.gif differ diff --git a/app/img/yamoney.png b/app/img/yamoney.png new file mode 100644 index 000000000..d14429eb1 Binary files /dev/null and b/app/img/yamoney.png differ diff --git a/app/js/base.js b/app/js/base.js new file mode 100644 index 000000000..c9dcfc3dc --- /dev/null +++ b/app/js/base.js @@ -0,0 +1,1225 @@ +/** + * Функция возвращает окончание для множественного числа слова на основании числа и массива окончаний + * @param iNumber Integer Число на основе которого нужно сформировать окончание + * @param aEndings Array Массив слов или окончаний для чисел (1, 4, 5), + * например ['яблоко', 'яблока', 'яблок'] + * @return String + */ +function getNumEnding(iNumber, aEndings) +{ + var sEnding, i; + iNumber = iNumber % 100; + if (iNumber>=11 && iNumber<=19) { + sEnding=aEndings[2]; + } + else { + i = iNumber % 10; + switch (i) + { + case (1): sEnding = aEndings[0]; break; + case (2): + case (3): + case (4): sEnding = aEndings[1]; break; + default: sEnding = aEndings[2]; + } + } + return sEnding; +} + +// ====================== + +function getRandomIdentifier(prefix) { + return (prefix || '') + Math.random().toString(36).substr(2); +} + +// ====================== + +(function ($) { + $(document).ready(function () { + //////////////////////// + function positionDropdown() { + var root = $('.dropdown.open'); + var content = $('.dropdown-cloned'); + var offsetRoot = $('body'); + var style = {}; + if (root.hasClass('down-left')) { + style = { + position: 'absolute', + top: root.offset().top + root.outerHeight(), + right: $(window).width() - root.offset().left - root.outerWidth(), + "z-index": 9999 + } + } else if (root.hasClass('down-right')) { + style = { + position: 'absolute', + top: root.offset().top + root.outerHeight(), + left: root.offset().left, + "z-index": 9999 + } + } + if (root.hasClass('inherit-min-width')) { + style['min-width'] = root.outerWidth(); + } + content.css(style); + } + + function closeAllDropdowns() { + $('.dropdown-cloned').remove(); + $('.dropdown.open').removeClass('open'); + $(document).off('click', dropdownOuterClick); + $(document).off('mousemove', dropdownOuterMove); + $(document).off('touchstart', dropdownOuterClick); + $(document).off('touchstart', dropdownOuterMove); + $(window).off('resize', positionDropdown); + } + + function dropdownOuterClick(e) { + if ($(e.target).parents('.dropdown-cloned').length == 0 && !$(e.target).is('.dropdown-cloned')) { + closeAllDropdowns(); + } + } + + function dropdownOuterMove(e) { + if ( + $(e.target).parents('.dropdown-cloned.dropdown__content_open_hover').length == 0 + && !$(e.target).is('.dropdown-cloned.dropdown__content_open_hover') + && $(e.target).parents('.dropdown.dropdown_open_hover').length == 0 + ) { + closeAllDropdowns(); + } + } + + function initDropdowns() { + $('.dropdown:not(.dropdown_open_hover):not(.dropdown_inited) .dropdown__toggle').click(function () { + var root = $(this).parents('.dropdown'); + if (root.hasClass('open')) { closeAllDropdowns(); return; } + closeAllDropdowns(); + var content = root.find('.dropdown__content').clone() + .show() + .addClass('dropdown-cloned'); + $('body').append(content); + root.addClass('open'); + $(window).on('resize', positionDropdown); + positionDropdown(); + $(document).bind('click touchstart', dropdownOuterClick); + root.addClass('dropdown_inited'); + return false; + }); + + $('.dropdown.dropdown_open_hover:not(.dropdown_inited) .dropdown__toggle').mouseenter(function () { + closeAllDropdowns(); + var root = $(this).parents('.dropdown'); + var content = root.find('.dropdown__content').clone() + .show() + .addClass('dropdown-cloned dropdown__content_open_hover'); + $('body').append(content); + root.addClass('open'); + $(window).on('resize', positionDropdown); + positionDropdown(); + $(document).bind('mousemove touchstart', dropdownOuterMove); + root.addClass('dropdown_inited'); + return false; + }); + } + + initDropdowns(); + + //////////////////////// + $('.search > .dropdown__toggle').click(function () { + $('.dropdown-cloned.dropdown__content input[name="search_block_form"]').focus(); + }); + + //////////////////////// + + // prevent focus/select + $('.spoiler').on('click', '.spoiler__button', function(e) { + $(e.delegateTarget).toggleClass('closed'); + return false; + }); + + + // открытие окна с соц сетью при клике + $('.social__soc').click(function() { + var winHeight = 400, winWidth = 500; + var params = 'scrollbars=no,status=no,location=no,toolbar=no,' + + 'menubar=no,width=' + winWidth + ',height=' + winHeight + + ',left=' + (screen.availWidth / 2 - winWidth / 2) + + ',top=' + (screen.availHeight / 2 - winHeight / 2); + window.open($(this).attr('href'), 'share', params) + return false; + }); + + // sticky-соц плашка + /////////////////////// + function updateSharing() { + if ($('.social.aside.unfixed').offset().top - $(window).scrollTop() < 20) { + $('.social.aside.unfixed').addClass('invisible'); + $('.social.aside.fixed').removeClass('invisible'); + + if ($(document).scrollLeft() >= 0 && $(document).width() > $(window).width()) { + $('.social.aside.fixed').css('left', $('.social.aside.unfixed').offset().left + - parseInt($('.social.aside.unfixed').css('margin-left')) + - $(document).scrollLeft() + 'px'); + } else { + $('.social.aside.fixed').css('left', 'auto'); + } + } else { + $('.social.aside.fixed').addClass('invisible'); + $('.social.aside.unfixed').removeClass('invisible'); + } + } + + var asideSocial = $('.social.aside'); + if (!asideSocial.data('handler') && asideSocial.length > 0) { + asideSocial.addClass('unfixed').after(asideSocial.clone().removeClass('unfixed').addClass('fixed invisible')); + updateSharing(); + $(window).scroll(updateSharing); + $(window).resize(updateSharing); + asideSocial.data('handler', true); // prevent from setting handler multiple times + } + + + // навигация по текущему уроку, sticky + /////////////////////// + function fixNavigation() { + var sidebar = $('.sidebar'); + var fixedBlock = $('.sidebar .keep-visible.fixed'); + var unfixedBlock = $('.sidebar .keep-visible.unfixed'); + if ($(window).scrollTop() > ($('.sidebar__content').offset().top + $('.sidebar__content').height())) { + unfixedBlock.addClass('invisible'); + fixedBlock.removeClass('invisible'); + + if ($(document).scrollLeft() >= 0 && $(document).width() > $(window).width()) { + fixedBlock.css('left', unfixedBlock.offset().left - $(document).scrollLeft() + 'px'); + } else { + fixedBlock.css('left', 'auto'); + } + + // считаем видимую высоту сайдбара, вычитаем из нее смещение и высоту фиксированного блока + // если меньше нуля — не помещается + var diff = sidebar.offset().top + // + sidebarPaddingTop + + parseInt(sidebar.css('padding-top')) // подразумеваем пиксели + + sidebar.height() + - $(window).scrollTop() + - parseInt(fixedBlock.css('top')) // подразумеваем пиксели + - fixedBlock.outerHeight(); + if (diff < 0) { + fixedBlock.addClass('invisible'); + } else { + fixedBlock.removeClass('invisible'); + } + } else { + unfixedBlock.removeClass('invisible'); + fixedBlock.addClass('invisible'); + } + } + + if ($('.sidebar .keep-visible').length > 0) { + $('.sidebar').append($('.sidebar .keep-visible') + .addClass('unfixed') + .clone() + .removeClass('unfixed') + .addClass('fixed invisible')); + fixNavigation(); + $(window).on('scroll.sidebar', fixNavigation); + $(window).on('resize.sidebar', fixNavigation); + } else { + $(window).off('.sidebar'); + $(window).off('.sidebar'); // FIXME: why two times? + } + + // количество комментариев текущее + /////////////////////// + (function () { + var s = document.createElement('script'); + s.src = 'http://learnjavascriptru.disqus.com/count.js'; + (document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s); + }()); + + // навигация ctrl <- -> + /////////////////////// + $(document).keydown(function (e) { + var back = $('.prev-next .prev-next__prev .prev-next__link').eq(0).attr('href'); + var forward = $('.prev-next .prev-next__next .prev-next__link').eq(0).attr('href'); + if (e.ctrlKey || e.metaKey) { + switch (e.keyCode) { + case 37: + back && (document.location = back); + break; + case 39: + forward && (document.location = forward); + break; + } + } + }) + + // Для профиля карандашик справа от поля + ////////////////////////////////////////////////////// + function startInlineEdit(jQInlineEditable) { + jQInlineEditable.addClass('profile__inline-editable_editing'); + window.getSelection().removeAllRanges(); + // input[type='text'] is not enough, because 'text' is default type + // if not set at all. So text input can have no type. + jQInlineEditable.find('input:not(input[type]), input[type="text"], input[type="email"], select, textarea').first().focus(); + } + + function finishInlineEdit(jqInlineEditable) { + jqInlineEditable.removeClass('profile__inline-editable_editing'); + } + + ////////// Demo ////////// + $('.profile').on('click', '.profile__inline-edit-trigger', function() { + startInlineEdit($(this).parents('.profile__inline-editable')); + return false; + }) + + $('.profile').on('dblclick', '.profile__item', function() { + startInlineEdit($(this)); + return false; + }) + + $('.profile').on('click', '.profile__item-edit-cancel', function() { + finishInlineEdit($(this).parents('.profile__inline-editable')) + return false; + }) + + $('.profile').on('click', '.profile__submit', function() { + var _self = this + var form = $(_self).closest('form') + $.post(form.attr('action'), form.serialize()).done(function (data){ + finishInlineEdit($(_self).parents('.profile__inline-editable')); + $(_self).closest('tr').html(data) + }) + return false; + }) + ////////////////////////// + + $('.profile__upic-upload').change(function() { + $(this).parents('.profile__upic-change').submit(); + }) + + + // отзывы о курсах, слайдер с бендером + //////////////////// will it leak with turbolinks or not? + $('.slider').each(function(){ + var slider = $(this) + , items = slider.find('.slider__items') + , itemsList = items.find('.slider__item') + , itemsCount = itemsList.length + , animationDuration = 400 + , viewport + , frames + , frame1 + , frame2 + , frame3 + , currentItem; + items.addClass('slider__items_hidden'); + items.wrap('
'); + viewport = slider.find('.slider__viewport'); + viewport.append('
' + + '
' + + '
' + + '
' + + '
'); + frames = slider.find('.slider__frames'); + frame1 = frames.find('.slider__frame:nth-child(1)'); + frame2 = frames.find('.slider__frame:nth-child(2)'); + frame3 = frames.find('.slider__frame:nth-child(3)'); + + frames.css({ left: '-100%' }); + + currentItem = getCurrentItem(); + frame2.append(itemsList.eq(currentItem).clone()); + itemsList.eq(currentItem).addClass('slider__item_current'); + fixButtons(); + + slider.find('.slider__next').click(function(){ + slideTo(currentItem + 1); + }) + + slider.find('.slider__prev').click(function(){ + slideTo(currentItem - 1); + }) + + function slideTo(itemIndex) { + var newHeight + , animateToPosition + , tmpFramel + slider.find('.slider__prev, .slider__next').prop('disabled', true); + items.find('.slider__item_current').removeClass('slider__item_current'); + if (itemIndex > currentItem) { + viewport.css({height: frame2.find('.slider__item').height()}); // setting height to current height + tmpFrame = frame3; + animateToPosition = '-200%'; + } else if (itemIndex < currentItem) { + viewport.css({height: frame2.find('.slider__item').height()}); // setting height to current height + tmpFrame = frame1; + animateToPosition = '0%'; + } else { // itemIndex == currentItem + return; + } + + tmpFrame.append(itemsList.eq(itemIndex).clone()); + itemsList.eq(itemIndex).addClass('slider__item_current'); + + // Two next animations run in parallel but not in sync, + // it seems it shouldn't cause any serious problems + frames.animate({left: animateToPosition}, animationDuration, function() { + frame2.find('.slider__item').remove(); + frame2.append(tmpFrame.find('.slider__item')); + $(this).css({left: '-100%'}); + slider.find('.slider__prev, .slider__next').prop('disabled', false) + }); + viewport.animate({height: tmpFrame.find('.slider__item').height()}, animationDuration, function() { + $(this).height('auto'); + }); + + currentItem = getCurrentItem(); + fixButtons(); + } + + function getCurrentItem() { + var index = itemsList.index(items.find('.slider__item_current')); + if (index == -1) { + return 0; + } else { + return index; + } + } + + function fixButtons() { + slider.find('.slider__prev, .slider__next').show(); + if (currentItem == 0) { + slider.find('.slider__prev').hide(); + } + if (currentItem == itemsCount - 1) { + slider.find('.slider__next').hide(); + } + } + }); + + // универсальное решение для любых табов (навигационных или в блоке кода) + // .tabs is a container for all content and controls + // There are .tabs__tab elements inside .tabs, + // each of them should have .tabs__switch element inside. + // This element will be moved to .tabs__switches container + // that will be inserted before content inside .tabs + // The .tabs__switch-control element will listen to clicks + // and switch tabs. It can be placed inside .tabs__switch + // or just added to it as .tabs__switch.tabs__switch-control + // May be useful if only part of visible tab switch should + // listen to clicks (e. g. only text inside tab switch). + // No nested tabs allowed. + $('.tabs').each(function() { + var tabs = $(this) + , switchesContainer + , switchesList + , tabsList + , index; + tabs.prepend('
'); + switchesContainer = tabs.find('.tabs__switches'); + tabs.find('.tabs__switch').appendTo(switchesContainer) + switchesList = switchesContainer.find('.tabs__switch-control'); + tabsList = tabs.find('.tabs__tab'); + + if (tabs.find('.tabs__tab_current').length) { + index = tabsList.index(tabs.find('.tabs__tab_current')); + setCurrentTabSwitch(index); + } else { + tabsList.eq(0).addClass('tabs__tab_current'); + setCurrentTabSwitch(0); + } + + switchesContainer.on('click', '.tabs__switch-control', function() { + var clickedIndex = switchesList.index($(this)); + tabsList.removeClass('tabs__tab_current'); + tabsList.eq(clickedIndex).addClass('tabs__tab_current'); + setCurrentTabSwitch(clickedIndex); + }) + + function setCurrentTabSwitch(index) { + var clickedItem = switchesList.eq(index); + tabs.find('.tabs__switch_current').removeClass('tabs__switch_current'); + if (clickedItem.hasClass('tabs__switch')) { + clickedItem.addClass('tabs__switch_current') + } else { + clickedItem.parents('.tabs__switch').eq(0).addClass('tabs__switch_current'); + } + } + + tabs.addClass('tabs_inited'); + }); + + // тоже со страницы с бендером и отзывами + // Required elements: + // .accordion>.accordion__item>(.accordion__switch+.accordion__content) + // .accordion__item.accordion__item_open is open by default + // No nested accordions allowed. + $('.accordion').each(function() { + var accordion = $(this); + accordion.find('.accordion__content').wrapInner('
'); + if (accordion.find('.accordion__item_open').length > 0) { + accordion.find('.accordion__item:not(.accordion__item_open) .accordion__content').css({height: 0}); + } else { + accordion.find('.accordion__item:gt(0)').find('.accordion__content').css({height: 0}); + accordion.find('.accordion__item').eq(0).addClass('accordion__item_open'); + } + + accordion.on('click', '.accordion__switch', function() { + var currentItem = $(this) + , currentContent + , defaultHeight + , animationDuration = 400; + if (currentItem.parents('.accordion__item_open').length == 0) { // item is closed now + currentContent = currentItem.parents('.accordion__item').find('.accordion__content'); + defaultHeight = currentContent.css({height: 'auto'}).height(); + currentContent.css({height: 0}); + accordion.find('.accordion__item_open .accordion__content').animate({height: 0}, animationDuration, function () { + $(this).parents('.accordion__item_open').removeClass('accordion__item_open'); + }); + currentContent.animate({height: defaultHeight}, animationDuration, function () { + $(this).parents('.accordion__item').addClass('accordion__item_open'); + $(this).css({height: 'auto'}); + }); + } + }) + + accordion.addClass('accordion_inited'); + }); + + + + // placeholder? + // если будет нужен - стилизуемый placeholder + function initCompactLabels() { + $('.text-compact-label').not('.text-compact-label_inited').each(function() { + var textCompactLabel = $(this) + , label = textCompactLabel.find('.text-compact-label__label') + , input = textCompactLabel.find('.text-compact-label__input .text-input__control'); + + input.focus(function() { + label.hide(); + }) + + input.blur(function() { + if (input.val() == '') { + label.show(); + } else { + label.hide(); // if it is triggered to update dynamically added input + } + }) + + textCompactLabel.addClass('text-compact-label_inited'); + input.triggerHandler('blur'); + }); + }; + + initCompactLabels(); + + // блок [hide] + ///////////////////////////////////////////////// + $('.hide-link').click(function(e) { + $(this).parent().toggleClass('hide-closed hide-open'); + return false; + }); + + // для записи на курсы контрол для выбора количества участников с +- + // $('.number-input').on('valuechanged', function(e, newVal, oldVal) {console.log(newVal + ', ' + oldVal)}); + + // move state into an object and pass it to handlers + $('.number-input').each(function() { + var numberInput = $(this); + var text = numberInput.find('.number-input__input'); + var value = text.prop('value') != "" ? +text.prop('value') : undefined; + var min = numberInput.attr('data-min') != "" ? +numberInput.attr('data-min') : undefined; + var max = numberInput.attr('data-max') != "" ? +numberInput.attr('data-max') : undefined; + var step = +numberInput.attr('data-step') || 1; + + fixValue(); + + numberInput.on('click', '.number-input__dec', decValue) + .on('click', '.number-input__inc', incValue) + .on('keydown', '.number-input__input', processKey) + .on('blur', '.number-input__input', fixValue); + + function incValue() { + var newValue = (value || 0) + step; + if (isNaN(max) || newValue <= max) { + updateValue(newValue); + } + } + + function decValue() { + var newValue = (value || 0) - step; + if (isNaN(min) || newValue >= min) { + updateValue(newValue); + } + } + + function processKey(e) { + switch (e.which) { + case 38: incValue(); + return false; + case 40: decValue(); + return false; + } + } + + function fixValue() { + var currentValue = +text.prop('value'); + if (isNaN(currentValue) || (isFinite(min) && currentValue < min)) { + updateValue(min || 0); + return; + } + + if (isFinite(max) && currentValue > max) { + updateValue(max); + return; + } + + updateValue(currentValue); + } + + function updateValue(newVal) { + var oldVal = value; + value = newVal; + text.prop('value', newVal); + // value may be updated to the same when fixing incorrect input + // on blur in some cases + if (oldVal != newVal) { + numberInput.trigger('valuechanged', [newVal, oldVal]); + } + } + }); + + // выбор метода оплаты + /////////////////////////////////////////////////// + // pay-method block behaviour (just demo, block not finished yet) + $('.pay-method__radio').removeAttr('checked'); + $('.pay-method__insert').hide(); + $('.pay-method__radio_bank-bill').click(function() { + var root = $(this).parents('.pay-method').first(); + // root.find('.pay-method__insert').hide(); + root.find('.pay-method__insert_bank-bill').show(); + }); + + $('.pay-method__insert .form-insert__close').click(function() { + $(this).parents('.pay-method__insert').first().hide(); + $(this).parents('.pay-method').first().find('.pay-method__radio').removeAttr('checked'); + }); + + // код для формы курса + // количество мест, email'ы участников... + // There can be only one form, but just don't run the code if there is no form at all + $('.request-form').each(function() { + var participantsAmount = +$('.order-form__control_amount .number-input__input').prop('value'); + var emails = []; + var form = $(this).find('.complex-form__request-form'); + var price = +form.attr('data-price'); + var user = form.attr('data-useremail'); + var usdCourse = +form.attr('data-usd-course'); + var selfUser = form.find('#request-participant'); + var userIncluded = false; // we'll need to add user at first + var selfUserChecked = selfUser.prop('checked'); + var listTrigger = form.find('.order-form__participants-trigger'); + var particicpantsListWrap = form.find('.order-form__participants'); + var listVisible = false; // by default is hidden + + $('.order-form__control_amount').on('valuechanged', function(e, newVal, oldVal) { + participantsAmount = newVal; + if (newVal > oldVal) { + addEmail(newVal - oldVal); + } else { + removeEmail(oldVal - newVal); + } + updatePrice(); + fillAddresses(); + fixParticipantSwitch(); + fixParticipants(); + }); + + form.on('input change', '.order-form__email', updateEmails) + .on('input change', '.order-form__email', fixParticipantSwitch); + + selfUser.click(function(){ + if($(this).prop('checked')) { + addOwner(); + selfUserChecked = true; + } else { + removeOwner(); + selfUserChecked = false; + } + fixParticipants(); + }); + + updatePrice(); + + // sometimes it's cached after page refresh + selfUser.prop('checked', true).removeAttr('disabled'); + selfUserChecked = selfUser.prop('checked'); + + listTrigger.on('click', function() { + if (listVisible) { + removeList(); + } else { + addList(); + } + }); + + $('.order-form__close').on('click', removeList); + + form.attr('novalidate', 'nodalidate'); + + // + form parts // + + form.find('.request-form__step-contact, .request-form__step-payment, .request-form__step-confirm').hide(); + + form.find('#pay-form-contract').prop('checked', false).click(function() { + if($(this).prop('checked')) { + form.find('.pay-form__contract-info').show(); + } else { + form.find('.pay-form__contract-info').hide(); + } + }); + form.find('.pay-form__contract-info').hide(); + + form.find('.order-form__submit').click(function() { + if (validateEmails() == false) { + return false + } + form.find('.request-form__step-order').hide(); + form.find('.request-form__step-contact').show(); + $('.receipts__receipt_last').removeClass('receipts__receipt_last'); + $('.request-form__order').addClass('receipts__receipt_last').removeClass('receipts__receipt_pending'); + $('.request-form__next-contact').addClass('complex-form__next-item_finished'); + }); + form.find('.contact-form__submit').click(function() { + form.find('.request-form__step-contact').hide(); + form.find('.request-form__step-payment').show(); + $('.receipts__receipt_last').removeClass('receipts__receipt_last'); + $('.request-form__contact').addClass('receipts__receipt_last').removeClass('receipts__receipt_pending'); + $('.request-form__next-pay').addClass('complex-form__next-item_finished'); + }); + form.find('.pay-form__submit, .pay-form__later').click(function() { + form.find('.request-form__step-payment').hide(); + form.find('.request-form__step-confirm').show(); + $('.receipts__receipt_last').removeClass('receipts__receipt_last'); + $('.request-form__payment').addClass('receipts__receipt_last').removeClass('receipts__receipt_pending'); + $('.request-form__next-confirm').addClass('complex-form__next-item_finished'); + }); + + // - form parts // + + function fixParticipants() { + if (!selfUserChecked || participantsAmount > 1) { + particicpantsListWrap.removeClass('order-form__participants_hidden'); + } else { + particicpantsListWrap.addClass('order-form__participants_hidden'); + } + } + + function addList() { + $('.order-form__participants-addresses') + .removeClass('order-form__participants-addresses_hidden') + .append(''); + + addEmail(participantsAmount); + if (selfUserChecked && !userIncluded) { + addOwner(); + } else if (selfUserChecked) { + addOwner(false); + } else { + fillAddresses(); + } + listVisible = true; + } + + function removeList() { + $('.order-form__participants-list').remove(); + $('.order-form__participants-addresses').addClass('order-form__participants-addresses_hidden'); + listVisible = false; + } + + function addEmail(amount) { + amount = amount || 1; // let the function be called without argument + var emailsHtml = ''; + var startIndex = form.find('.order-form__participant').length + 1; + + for (var i = startIndex; i < startIndex + amount; i++) { + emailsHtml += getEmailItem(i); + } + + form.find('.order-form__participants-list').append(emailsHtml); + initCompactLabels(); + + function getEmailItem(itemNumber) { + var emaiString = '
  • ' + + '
    ' + + '' + + '
  • '; + + return emaiString; + } + } + + function removeEmail(amount) { + amount = amount || 1; // let the function be called without argument + form.find('.order-form__participant').slice(amount * -1).remove(); + } + + function updatePrice() { + form.find('#request-price').text(participantsAmount * price); + form.find('#request-usd').text(Math.ceil(participantsAmount * price / usdCourse)); + } + + function updateEmails() { + var self = $(this); + // we could parse id, but I don't want to rely on id format in case it changes + var index = form.find('.order-form__email').index(this); + + emails[index] = self.prop('value'); + } + + function fixParticipantSwitch() { + if (findGap() == -1 && !form.find('#request-participant').prop('checked')) { + form.find('#request-participant').attr('disabled', 'disabled'); + } else { + form.find('#request-participant').removeAttr('disabled'); + } + } + + function fillAddresses() { + form.find('.order-form__email').each(function(i) { + // 'blur' is triggered to hide hint on filled fields. Awful, I know + $(this).prop('value', emails[i] || '').triggerHandler('blur'); + }); + } + + function findGap() { + for (var i = 0; i < participantsAmount; i++) { + if ($.trim(emails[i]) == '' || emails[i] === undefined) { + return i; + } + } + return -1; + } + + function addOwner(completely) { + // if completely == false we just make the field inactive and change label + var gap; + if (completely === undefined) { completely = true } + if (completely) { + gap = findGap(); + if (gap > -1) { + emails.splice(gap, 1); + emails.unshift(user); + userIncluded = true; + } + } + fillAddresses(); + form.find('.order-form__email').first().attr('disabled', 'disabled'); + form.find('.order-form__participant-label').first().text('Участник 1 (вы):'); + } + + function removeOwner() { + emails.shift(); + form.find('.order-form__email').first().removeAttr('disabled'); + form.find('.order-form__participant-label').first().text('Участник 1:'); + fillAddresses(); + userIncluded = false; + } + + function isEmail(string) { + return /^[a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/.test($.trim(string)); + } + + function validateEmails() { + var isValid = true; + + form.find('.order-form__participant .text-input_invalid') + .removeClass('text-input_invalid'); + + form.find('.order-form__email').each(function(i) { + var value = $.trim($(this).prop('value')); + if (value !== '' && isEmail(value) == false) { + isValid = false; + $(this).parents('.order-form__email-wrap') + .addClass('text-input_invalid'); + } + }); + + return isValid; + } + }); + + // для формы заказа учебника (открывает следующий раздел формы, временно для вёрстки) + $('.order-book').each(function() { + var root = $(this); + var form = root.find('.complex-form__order-book'); + form.find('.order-book__step-confirm').hide(); + + // Demo, add logic here + // form.find('.pay-method__radio').click(function() { + // root.find('.receipts__receipt_pending') + // .removeClass('receipts__receipt_pending') + // .last().addClass('receipts__receipt_last'); + // root.find('.order-book__step-order').hide(); + // root.find('.order-book__step-confirm').show(); + // root.find('.order-book__next-contact').addClass('complex-form__next-item_finished'); + // }) + }); + + // ссылка для оплаты не из россии + $('.pay-hint').each(function() { + // at first we have to move the content to the top of the DOM to display correctly + var content = $(this).find('.pay-hint__content').addClass('pay-hint__content_detached').detach(); + content.prepend(''); + content.find('.pay-hint__close').click(function() { + $.modal.close(); + }) + $('body').prepend(content); + // then we use rel="modal:open" and href="#content-id" on a link + }); + + // для ввода телефона с кодом страны + // don't put addFlag into global scope + // подключается плагин select2 (он жирный, мб убрать?) + // только select2 умел показать флажки стран + // (и можно выбирать клавиатурой вверх-вниз, как в стандартном селекте) + $('.full-phone').each(function() { + function addFlag(code) { + var flag = $(code.element).data('flag'); + if (!code.id || flag === undefined) return code.text; + return '' + code.text; + } + + $(this).find('.full-phone__codes').select2({ + dropdownCssClass: 'select2-drop_nofilter', + formatResult: addFlag, + formatSelection: addFlag, + escapeMarkup: function(m) { return m; } + }); + }); + + // постраничная навигация в результатах поиска, со скроллом + // есть фантомный баг в Firefox: ширина полосы для бегунка вычисляется неверно, + // больше чем надо, и до конца список страниц невозможно прокрутить + ////////////////////////////////// + $('.pager').each(function(){ + var pager = $(this); + var pagerWidth = pager.width(); + var pagesNumber = pager.data('pages'); + var urlMask = pager.data('url'); + var pagesList = $('
    '); + var pagesListRow = pagesList.find('tr'); + var pageWidth = 40; + var pagesWidth = pageWidth * pagesNumber; + var currentPage = pager.data('current-page') || 1; + var scrollbar, scrollHandle, currentPageElem, currentElemOffset, url; + + function scrollPager(event) { + var start = scrollHandle.data('dragStart'); + var startPosition = scrollHandle.data('startPosition'); + var maxPosition = scrollbar.width() - scrollHandle.width(); + var position = Math.max(0, Math.min(event.pageX - start + startPosition, maxPosition)); + + scrollHandle.css('left', position); + pager.find('.pager__pages').css('left', -1 * (parseInt(scrollHandle.css('left')) * (pagesWidth / pagerWidth))); + } + + function disableScroll() { + $(document).off('mousemove', scrollPager) + .off('mouseup', disableScroll); + } + + function initPages() { + for (var i = 1; i < pagesNumber + 1; i++) { + url = urlMask.replace('{n}', i); + if (i == currentPage) { + pagesListRow.append('' + i + ''); + } else { + pagesListRow.append('' + i + ''); + } + } + + pagesList.find('.pager__pages').width(pagesWidth); + pager.empty(); + pager.append(pagesList); + } + + function initScroll() { + if (pagesWidth > pagerWidth) { + pager.append($('
    ')); + scrollbar = pager.find('.pager__scroll'); + scrollHandle = pager.find('.pager__scroll-handle'); + scrollHandle.width(pagerWidth * (scrollbar.width() / pagesWidth)); + scrollHandle.on('mousedown', function(event) { + var handle = $(this); + handle.data('dragStart', event.pageX).data('startPosition', parseInt(handle.css('left')) || 0); + $(document).on('mousemove', scrollPager) + .on('mouseup', disableScroll); + }); + } + } + + function positionCurrent() { + currentPageElem = pager.find('.pager__page-link_current'); + scrollbar = pager.find('.pager__scroll'); + scrollHandle = pager.find('.pager__scroll-handle'); + if (scrollbar.length != 0 && currentPageElem.length != 0) { + currentElemOffset = -1 * (currentPageElem.position().left - pagerWidth / 2 + currentPageElem.width() / 2); + currentElemOffset = Math.min(0, Math.max(currentElemOffset, -1 * (pagesWidth - pagerWidth))); + pager.find('.pager__pages').css('left', currentElemOffset); + scrollHandle.css('left', -1 * currentElemOffset * pagerWidth / pagesWidth); + } + } + + function addPageByPage() { + var pageByPage = $('
    '+ + '
    Ctrl +
    ' + + '
    ' + + '
    Ctrl +
    ' + + '
    '); + var next = null, prev = null; + + pageByPage.find('.pager__numberofpages').text(pagesNumber + ' ' + getNumEnding(pagesNumber, ['страница', 'страницы', 'страниц'])); + + if (currentPage == 1) { + $('Предыдущая страница') + .insertBefore(pageByPage.find('.pager__shortcut_prev kbd')); + } else { + prev = urlMask.replace('{n}', currentPage - 1); + $('Предыдущая страница') + .insertBefore(pageByPage.find('.pager__shortcut_prev kbd')); + } + + if (currentPage == pagesNumber) { + $('Следующая страница') + .insertBefore(pageByPage.find('.pager__shortcut_next kbd')); + } else { + next = urlMask.replace('{n}', currentPage + 1); + $('Следующая страница') + .insertBefore(pageByPage.find('.pager__shortcut_next kbd')); + } + + $(document).keydown(function (e) { + if (e.ctrlKey || e.metaKey) { + switch (e.keyCode) { + case 37: + prev && (document.location = prev); + break; + case 39: + next && (document.location = next); + break; + } + } + }); + + pager.append(pageByPage); + } + + initPages(); + initScroll(); + positionCurrent(); + addPageByPage(); + }); + + + // страница результатов поиска по сайту + // sticky результат поиска сверху + $('.search-query').each(function() { + var queryForm = $(this); + var mainContainer = $('.main'); // used for size and position calculations + var regularForm = queryForm.find('.search-query__wrap_regular'); + var fixedForm = queryForm.find('.search-query__wrap_fixed'); + var fixedFormTopPadding = parseInt(fixedForm.find('.search-query__input-wrap').css('paddingTop')); + var jqWindow = $(window); + + function updateFixedForm() { + fixedForm.css({ + 'left': mainContainer.offset().left - jqWindow.scrollLeft(), + 'width': mainContainer.width(), + 'padding-left': mainContainer.css('paddingLeft'), + 'padding-right': mainContainer.css('paddingRight') + }) + } + + function syncRegularToFixed() { + fixedForm.find('.search-query__input').val(regularForm.find('.search-query__input').val()); + } + + function syncFixedToRegular() { + regularForm.find('.search-query__input').val(fixedForm.find('.search-query__input').val()); + } + + if (fixedForm.length > 0) { + $('.main').append(fixedForm); + jqWindow.scroll(function() { + if ((jqWindow.scrollTop() - queryForm.offset().top) >= fixedFormTopPadding) { + if (fixedForm.hasClass('search-query__wrap_hidden')) { + syncRegularToFixed(); + fixedForm.removeClass('search-query__wrap_hidden'); + regularForm.css({'visibility': 'hidden'}); + jqWindow.on('resize', updateFixedForm); + jqWindow.on('scroll', updateFixedForm); + setTimeout(updateFixedForm, 0); + } + } else { + if (!fixedForm.hasClass('search-query__wrap_hidden')) { + fixedForm.addClass('search-query__wrap_hidden'); + regularForm.css({'visibility': 'visible'}); + jqWindow.off('resize', updateFixedForm); + jqWindow.off('scroll', updateFixedForm); + syncFixedToRegular(); + } + } + }); + } + }); + + // блок кода с табами на разные файлы + // недоделан + /////////////////////////////////////////////////////////////// + $('.complex-code.tabs_inited').each(function() { + var root = $(this); + var link = root.data('link'); + var uniqueClass = getRandomIdentifier('complex-code_'); + for ( ; $('.' + uniqueClass).length > 0 ; ) { + uniqueClass = getRandomIdentifier('complex-code_'); + } + + root.addClass(uniqueClass); + root.find('.tabs__switches') + .wrap('
    '); + root.find('.complex-code__tools-wrap') + .append([''].join('')); + root.find('.complex-code__dropdown').attr('data-parent', uniqueClass) + .find('.complex-code__dropdown-inner') + .append(root.find('.tabs__switch').clone()); + if (link) { + root.find('.complex-code__tools-wrap') + .append(''); + } + initDropdowns(); + }); + + $('.complex-code.tabs_inited').first().each(function() { + $(document).on('click.complex-code', '.complex-code__dropdown .tabs__switch-control', function() { + var jqComplexCodeDropdownItem = $(this); + var jqComplexCodeDropdown = jqComplexCodeDropdownItem.parents('.complex-code__dropdown'); + var jqComplexCode = $('.' + jqComplexCodeDropdown.data('parent')); + var index = jqComplexCodeDropdown.find('.tabs__switch-control').index(jqComplexCodeDropdownItem); + + jqComplexCode.find('.tabs__tab').removeClass('tabs__tab_current') + .eq(index) + .addClass('tabs__tab_current'); + jqComplexCode.find('.tabs__switch').removeClass('tabs__switch_current') + .eq(index) + .addClass('tabs__switch_current'); + closeAllDropdowns(); + }); + }); + + // подсветка текущего раздела в сайдбаре, при скролле + // run initialization once if there is at least one block, + // all blocks are inited at once + $('.page-contents').first().each(function() { + var fadeTimeout; + var headers = $('.main > h2 > a[href^=\'#\']'); + var currentHeader, jqCurrentHeader, i; + + $(window).off('.pageContents'); // turbolinks + $(window).on('scroll.pageContents', function() { + $('.page-contents.fixed:not(.invisible):not(.page-contents_fading):not(.page-contents_faded)').each(function() { + $(this).addClass('page-contents_fading'); + fadeTimeout = setTimeout(function() { + $('.page-contents.fixed.page-contents_fading').animate({ 'opacity': 0.35 }, 2000, 'linear', function() { + $(this).removeClass('page-contents_fading').addClass('page-contents_faded'); + }) + }, 10 * 1000); + }); + + $('.page-contents.fixed.invisible.page-contents_fading, .page-contents.fixed.invisible.page-contents_faded').each(function() { + clearTimeout(fadeTimeout); + $(this).removeClass('page-contents_fading page-contents_faded').css({ 'opacity': 1 }); + }); + }); + $(window).on('scroll.pageContents', function() { + for (i = headers.length - 1; i >=0 ; i--) { + if ( + $(window).scrollTop() >= headers.eq(i).offset().top + ) { + if (currentHeader == headers.eq(i).text()) { + break; + } + jqCurrentHeader = headers.eq(i) + currentHeader = jqCurrentHeader.text(); + + $('.page-contents .page-contents__link_active').removeClass('page-contents__link_active'); + $('.page-contents').find('a[href$="#' + jqCurrentHeader.attr('id') + '"]').addClass('page-contents__link_active'); + + break; + } + } + }); + + $('.sidebar').off('.pageContents'); // turbolinks + $('.sidebar').on('mouseenter.pageContents mouseleave.pageContents', '.page-contents.fixed.page-contents_faded', function(e) { + var fixedContents = $(this); + if (e.type == 'mouseenter') { + fixedContents.animate({ 'opacity': 1 }, 200); + } else { + fixedContents.animate({ 'opacity': .35 }, 600); + } + }); + }); + }); + +})(jQuery); + +function runDemo(node) { + var node = $(node); + var demoPre; + while(node.length) { + var demo = node.find('[data-demo]'); + if (demo.length) break; + node = node.parent(); + } + + demo = demo[0]; + if (!demo) return; + var code = demo.code; + eval(code); +} + +function isScrolledIntoView(elem) { + var docViewTop = $(window).scrollTop(); + var docViewBottom = docViewTop + $(window).height(); + + var elemTop = elem.offset().top; + var elemBottom = elemTop + elem.outerHeight(); + + var visibleHeight = 0; + if (elemTop <= docViewTop) { + visibleHeight = elemBottom - docViewTop; + } else if (elemBottom >= docViewBottom) { + visibleHeight = docViewBottom - elemTop; + } else { + visibleHeight = elemBottom - elemTop; + } + + return visibleHeight > 10; +} diff --git a/app/js/head.js b/app/js/head.js new file mode 100644 index 000000000..5019813e9 --- /dev/null +++ b/app/js/head.js @@ -0,0 +1,9 @@ +require('./polyfill'); + +var login = require('./login'); + +document.on('click', 'a.user__entrance', function(e) { + e.preventDefault(); + + login(); +}); diff --git a/app/js/login.js b/app/js/login.js new file mode 100644 index 000000000..a038cd50f --- /dev/null +++ b/app/js/login.js @@ -0,0 +1,52 @@ + +// Run like this: +// login() +// login({whyMessage:.. followLinkMessage:..}) +// login({whyMessage:.. followLinkMessage:..}, callback) +module.exports = function(options, callback) { + options = options || {}; + callback = callback || function() { }; + + var authWindow = document.createElement('div'); + authWindow.className = "auth-form"; + + authWindow.innerHTML = '
    '; + document.body.append(authWindow); + + var xhr = new XMLHttpRequest(); + xhr.open('GET', '/auth/form', true); + xhr.onloadend = function() { + if (this.status != 200 || !this.responseText) { + alert("Извините, ошибка на сервере"); + return; + } + authWindow.innerHTML = this.responseText; + addLoginFormEvents(authWindow.querySelector('.login-form'), callback); + }; + + xhr.send(); + +}; + +function addLoginFormEvents(form, callback) { + form.addEventListener('submit', function(event) { + event.preventDefault(); + + var email = form.elements.email; + var password = form.elements.password; + + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/auth/login/local', true); + xhr.onloadend = function() { + if (!this.status || this.status >= 500 || !this.responseText) { + alert("Извините, ошибка на сервере"); + return; + } + + alert(this.responseText); + }; + xhr.send(new FormData(form)); + + }); + +} diff --git a/app/js/main.js b/app/js/main.js new file mode 100644 index 000000000..fbd6d398d --- /dev/null +++ b/app/js/main.js @@ -0,0 +1,5 @@ + +require('jquery'); + +//window.$ = require('jquery'); +//require('./prism'); diff --git a/app/js/polyfill/dom4.js b/app/js/polyfill/dom4.js new file mode 100644 index 000000000..cddfcdbf8 --- /dev/null +++ b/app/js/polyfill/dom4.js @@ -0,0 +1,79 @@ + +function textNodeIfString(node) { + return typeof node === 'string' ? document.createTextNode(node) : node; + } + +function mutationMacro(nodes) { + if (nodes.length === 1) { + return textNodeIfString(nodes[0]); + } + var fragment = document.createDocumentFragment(); + var list = [].slice.call(nodes); + + for (var i = 0; i < list.length; i++) { + fragment.appendChild(textNodeIfString(list[i])); + } + return fragment; +} + +var methods = { + matches: Element.prototype.matchesSelector || Element.prototype.msMatchesSelector, + prepend: function() { + var node = mutationMacro(arguments); + this.insertBefore(node, this.firstChild); + }, + append: function() { + this.appendChild(mutationMacro(arguments)); + }, + before: function() { + var parentNode = this.parentNode; + if (parentNode) { + parentNode.insertBefore(mutationMacro(arguments), this); + } + }, + after: function() { + var parentNode = this.parentNode, + nextSibling = this.nextSibling, + node = mutationMacro(arguments); + if (parentNode) { + parentNode.insertBefore(node, nextSibling); + } + }, + replace: function() { + var parentNode = this.parentNode; + if (parentNode) { + parentNode.replaceChild(mutationMacro(arguments), this); + } + }, + remove: function() { + var parentNode = this.parentNode; + if (parentNode) { + parentNode.removeChild(this); + } + } +}; + +for (var methodName in methods) { + if (!Element.prototype[methodName]) { + Element.prototype[methodName] = methods[methodName]; + } +} + +try { + new CustomEvent("IE has CustomEvent, but doesn't support constructor"); +} catch(e) { + + window.CustomEvent = function(event, params) { + var evt; + params = params || { + bubbles: false, + cancelable: false, + detail: undefined + }; + evt = document.createEvent("CustomEvent"); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + }; + + CustomEvent.prototype = Object.create(window.Event.prototype); +} diff --git a/app/js/polyfill/index.js b/app/js/polyfill/index.js new file mode 100644 index 000000000..02bcba7e2 --- /dev/null +++ b/app/js/polyfill/index.js @@ -0,0 +1,2 @@ +require('./dom4'); +require('./on'); diff --git a/app/js/polyfill/on.js b/app/js/polyfill/on.js new file mode 100644 index 000000000..ab3dd67a1 --- /dev/null +++ b/app/js/polyfill/on.js @@ -0,0 +1,40 @@ +require('./dom4'); + +function findDelegateTarget(event, selector) { + var currentNode = event.target; + + while (currentNode) { + if (currentNode.matches(selector)) { + return currentNode; + } + + if (currentNode != event.currentTarget) { + currentNode = currentNode.parentElement; + } + } + return null; +} + +// IE doesn't have EventTarget, corresponding methods are in Node +var prototype = (window.EventTarget || Node).prototype; + +// currentTarget is top-level element! +prototype.on = function(eventName, selector, handler) { + this.addEventListener(eventName, function(event) { + var found = findDelegateTarget(event, selector); + + // currentTarget is read only, I can not fix it + // Object.create wrapper would break event.preventDefault() + // so, keep in mind: + // --> event.currentTarget is top-level element! + + event.delegateTarget = event.currentTarget; // for compat. with jQuery + if (found) { + handler.call(found, event); + } + }); +}; + +prototype.off = function() { + throw new Error("Not implemented (you need it? file an issue)"); +}; diff --git a/app/js/prism.js b/app/js/prism.js new file mode 100644 index 000000000..47398ef90 --- /dev/null +++ b/app/js/prism.js @@ -0,0 +1,129 @@ +require('prismjs/components/prism-core.js'); +require('prismjs/components/prism-markup.js'); +require('prismjs/components/prism-css.js'); +require('prismjs/components/prism-css-extras.js'); +require('prismjs/components/prism-clike.js'); +require('prismjs/components/prism-javascript.js'); +require('prismjs/components/prism-coffeescript.js'); +require('prismjs/components/prism-http.js'); +require('prismjs/components/prism-scss.js'); +require('prismjs/components/prism-sql.js'); +require('prismjs/components/prism-php.js'); +require('prismjs/components/prism-php-extras.js'); +require('prismjs/components/prism-python.js'); +require('prismjs/components/prism-ruby.js'); +require('prismjs/components/prism-java.js'); + +!function () { + document.removeEventListener('DOMContentLoaded', Prism.highlightAll); + + + function addLineNumbers(pre) { + + var linesNum = (1 + pre.innerHTML.split('\n').length); + var lineNumbersWrapper; + + var lines = new Array(linesNum); + lines = lines.join(''); + + lineNumbersWrapper = document.createElement('span'); + lineNumbersWrapper.className = 'line-numbers-rows'; + lineNumbersWrapper.innerHTML = lines; + + if (pre.hasAttribute('data-start')) { + pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1); + } + + pre.appendChild(lineNumbersWrapper); + } + + + function addBlockHighlight(pre) { + + var lines = $(pre).data('highlightBlock'); + + if (!lines) { + return; + } + + var ranges = lines.replace(/\s+/g, '').split(','); + + /*jshint -W084 */ + for (var i = 0, range; range = ranges[i++];) { + range = range.split('-'); + + var start = +range[0], + end = +range[1] || start; + + + var mask = $('
    ' + + new Array(start + 1).join('\n') + + '
    ' + new Array(end - start + 2).join('\n') + '
    '); + + $(pre).prepend(mask); + } + + } + + // fixme: require lodash.escape + function esc(str) { + return str + .replace(/&/g, '&') + .replace(//g, '>'); + } + + function unesc(str) { + return str + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>'); + } + + function addInlineHighlight(pre) { + var ranges = $(pre).data('highlightInline'); + var codeElem = $('code', pre); + + ranges = ranges ? ranges.split(",") : []; + + for (var i = 0; i < ranges.length; i++) { + var piece = ranges[i].split(':'); + var lineNum = +piece[0], strRange = piece[1].split('-'); + var start = +strRange[0], end = +strRange[1]; + var mask = $('
    ' + + new Array(lineNum + 1).join('\n') + + new Array(start + 1).join(' ') + + '' + new Array(end - start + 1).join(' ') + '
    '); + + codeElem.prepend(mask); + } + } + + + $(function() { + + // highlight inline + var codePre = $('pre[class*="language-"]'); + + codePre.each(function () { + this.code = unesc(this.innerHTML); + $(this).wrapInner(""); + + Prism.highlightElement(this.firstChild); + + addLineNumbers(this); + addBlockHighlight(this); + addInlineHighlight(this); + new CodeBox(this); + }); + + + }); + + $(function() { + $('iframe.result__iframe').each(function() { + new IframeBox(this); + }) + }); + +}(); diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl new file mode 100755 index 000000000..06862ef22 --- /dev/null +++ b/app/stylesheets/base.styl @@ -0,0 +1,147 @@ +// import all from nib except +// -flex +// -vendor +// -iconic +// -reset +@import 'nib/border' +@import 'nib/clearfix' +@import 'nib/color-image' +@import 'nib/gradients' +@import 'nib/image' +@import 'nib/overflow' +@import 'nib/positions' +@import 'nib/text' +@import 'nib/size' + +@require "sprites/*" +@require "blocks/variables/variables" +@require "blocks/reset/reset" +@require "blocks/placeholders/*" +@require "blocks/mixins/*" + +@require "blocks/login/login" +@require "blocks/progress/progress" +@require "blocks/body/body" +@require "blocks/top-part/top-part" +@require "blocks/logo/logo" +@require "blocks/page/page" +@require "blocks/links/links" +@require "blocks/navbar/navbar" +@require "blocks/nav-dropdown/nav-dropdown" +@require "blocks/main-content/main-content" +@require "blocks/main/main" +@require "blocks/sidebar/sidebar" +// @require "blocks/sidehelper/sidehelper" +// @require "blocks/page-contents/page-contents" +@require "blocks/breadcrumbs/breadcrumbs" +@require "blocks/important/important" + +@require "blocks/summary/summary" +@require "blocks/shortcut/shortcut" +@require "blocks/spoiler/spoiler" + +@require "blocks/dropdown/dropdown" + +@require "blocks/toolbar/toolbar" +@require "blocks/codebox/codebox" +@require "blocks/result/result" +@require "blocks/code-example/code-example" +// @require "blocks/complex-code/complex-code" +@require "blocks/balance/balance" +// @require "blocks/rating/rating" +@require "blocks/comments/comments" +// @require "blocks/submit-button/submit-button" +// @require "blocks/secondary-button/secondary-button" +@require "blocks/page-footer/page-footer" +@require "blocks/soc-icon/soc-icon" +@require "blocks/social/social" +// @require "blocks/user/user" +// @require "blocks/user-menu/user-menu" +// @require "blocks/primary-tabs/primary-tabs" +// @require "blocks/pager/pager" +// @require "blocks/disqus/disqus" +@require "blocks/standard-table/standard-table" +@require "blocks/prev-next/prev-next" +@require "blocks/corrector/corrector" +// @require "blocks/course-type/course-type" +// @require "blocks/currency/currency" +// @require "blocks/lessons/lessons" +// @require "blocks/category-foot/category-foot" +// @require "blocks/text-input/text-input" +// @require "blocks/text-marked/text-marked" +// @require "blocks/input-select/input-select" +// @require "blocks/textarea-input/textarea-input" +// @require "blocks/modal-form/modal-form" +// @require "blocks/social-login/social-login" +// @require "blocks/switch-input/switch-input" +// @require "blocks/profile/profile" +// @require "blocks/profile-insert/profile-insert" +// @require "blocks/select2/select2" +// @require "blocks/full-phone/full-phone" +// @require "blocks/confirm/confirm" +// @require "blocks/social-link/social-link" +// @require "blocks/text-compact-label/text-compact-label" +// @require "blocks/course-search/course-search" +// @require "blocks/header-note/header-note" +// @require "blocks/columns/columns" +// @require "blocks/h2/h2" +// @require "blocks/nowrap/nowrap" +// @require "blocks/highlight/highlight" +// @require "blocks/strong/strong" +// @require "blocks/bricks/bricks" +// @require "blocks/nodejs-link/nodejs-link" +// @require "blocks/highlights/highlights" +// @require "blocks/presenter-programs/presenter-programs" +// @require "blocks/presenter/presenter" +// @require "blocks/large-list/large-list" +// @require "blocks/slider/slider" +// @require "blocks/reviews/reviews" +// @require "blocks/tabs/tabs" +// @require "blocks/large-tabs/large-tabs" +// @require "blocks/accordion/accordion" +// @require "blocks/large-accordion/large-accordion" +// @require "blocks/sidebyside/sidebyside" +// @require "blocks/group-list/group-list" +// @require "blocks/group/group" +// @require "blocks/special-links-list/special-links-list" +// @require "blocks/flag/flag" +// @require "blocks/number-input/number-input" +// @require "blocks/close-button/close-button" +// @require "blocks/price/price" +// @require "blocks/extract/extract" +// @require "blocks/material/material" +// @require "blocks/form-insert/form-insert" +// @require "blocks/order-confirm/order-confirm" +// @require "blocks/grayed-list/grayed-list" +// @require "blocks/complex-form/complex-form" +// @require "blocks/order-form/order-form" +// @require "blocks/contact-form/contact-form" +// @require "blocks/pay-form/pay-form" +// @require "blocks/pay-method/pay-method" +// @require "blocks/book-form/book-form" +// @require "blocks/receipts/receipts" +// @require "blocks/pay-hint/pay-hint" +// @require "blocks/ol/ol" +// @require "blocks/ul/ul" +// @require "blocks/course-start-notify/course-start-notify" +// @require "blocks/course-start-subscribe/course-start-subscribe" +@require "blocks/hide/hide" +// @require "blocks/error/error" +// @require "blocks/redirect/redirect" +// @require "blocks/search-query/search-query" +// @require "blocks/path/path" +// @require "blocks/search-results/search-results" +// @require "blocks/search-notfound/search-notfound" +// @require "blocks/tests-list/tests-list" +// @require "blocks/task/task" + +// @require "blocks/mibbit-irc/mibbit-irc" +@require "blocks/prism/prism" +@require "blocks/prism/prism-line-highlight" +@require "blocks/prism/prism-line-numbers" +@require "blocks/prism/my-prism" +// @require "blocks/fancybox/jquery.fancybox" + + +// @require "jquery-modal/jquery.modal" + diff --git a/app/stylesheets/blocks/balance/balance.styl b/app/stylesheets/blocks/balance/balance.styl new file mode 100644 index 000000000..ea9b0cd7a --- /dev/null +++ b/app/stylesheets/blocks/balance/balance.styl @@ -0,0 +1,125 @@ + +.balance + position relative + margin 38px -1*content_horizontal_padding + + &__content + display: table; + width: 100%; + + &__title + margin 0 0 20px + text-align center + + & &__list + padding-left 25px + + // convert to class .balance__list-item + & &__list > li + margin 1em 0 1em + + & &__list > li::before + position absolute + margin-left -25px + + &__pluses, + &__minuses + display table-cell + width 50% + + &__pluses + padding 20px 35px 20px 40px + background: #D7F1B9; + + &__pluses &__title + color #004010 + + & &__pluses ul&__list > li::before + @extend $font-pros + color #238C00 + + &__minuses + padding 20px 40px 20px 35px + background #FFCFBF + + &__minuses &__title + color #401000 + + & &__minuses ul&__list > li::before + @extend $font-cons; + color #B20000 + + &_single + margin 20px -1*content_horizontal_padding 30px + + &_single &__content + width 100% + + &_single &__title + text-align left + + &_single &__pluses, + &_single &__minuses + padding 20px content_horizontal_padding + width auto + + &::before + size-of ('scales.png') + background url('/img/scales.png') + margin-left (image-width('scales.png') / -2) + content "" + position absolute + top -3px + left 50% + + &_single::before + display none + + @media (min-width: media_step_1) + margin: 38px -1*(content_horizontal_padding + 10px) + + &__pluses + padding 20px 35px 20px (content_horizontal_padding + 10px) + + &__minuses + padding 20px (content_horizontal_padding + 10px) 20px 35px + + &_single + margin 20px -1*(content_horizontal_padding + 10px) 30px + + &_single &__pluses, + &_single &__minuses + padding 20px (content_horizontal_padding + 10px) + + @media (min-width: media_step_2) + margin 38px -1*(content_horizontal_padding + 20px) + + &__pluses + padding 20px 35px 20px (content_horizontal_padding + 20px) + + &__minuses + padding 20px (content_horizontal_padding + 20px) 20px 35px + + &_single + margin 20px -1*(content_horizontal_padding + 20px) 30px + + &_single &__pluses, + &_single &__minuses + padding: 20px (content_horizontal_padding + 20px) + + + @media (min-width: media_step_3) + margin 38px -1*(content_horizontal_padding + 30px) + + &__pluses + padding 20px 35px 20px (content_horizontal_padding + 30px) + + &__minuses + padding 20px (content_horizontal_padding + 30px) 20px 35px + + &_single + margin 20px -1*(content_horizontal_padding + 30px) 30px + + &_single &__pluses, + &_single &__minuses + padding: 20px (content_horizontal_padding + 30px) diff --git a/app/stylesheets/blocks/balance/scales.png b/app/stylesheets/blocks/balance/scales.png new file mode 100644 index 000000000..9e774bf4b Binary files /dev/null and b/app/stylesheets/blocks/balance/scales.png differ diff --git a/app/stylesheets/blocks/body/body.styl b/app/stylesheets/blocks/body/body.styl new file mode 100644 index 000000000..5f248488d --- /dev/null +++ b/app/stylesheets/blocks/body/body.styl @@ -0,0 +1,27 @@ +body + @extend $center + @extend $min-max-width + + font size/lineheight font + -webkit-text-size-adjust none + padding 0 60px + color color + background page_background + +.body_redirect + background #fff + + +@media (min-width: media_step_1) + body + padding 0 70px + +@media (min-width: media_step_2) + body + padding 0 80px + + +@media (min-width: media_step_3) + body + padding 0 90px + diff --git a/app/stylesheets/blocks/breadcrumbs/breadcrumbs.styl b/app/stylesheets/blocks/breadcrumbs/breadcrumbs.styl new file mode 100644 index 000000000..d3720f667 --- /dev/null +++ b/app/stylesheets/blocks/breadcrumbs/breadcrumbs.styl @@ -0,0 +1,23 @@ +.breadcrumbs + list-style none + font-size 13px + margin 0 + padding 0 + color #777 + + & &__item + display inline-block + margin 0 5px 0 0 + + &::after + content " →" + color #666 + + &:last-child::after + display none + + &__link + color link_color + + &__link:hover + color link_hover_color diff --git a/app/stylesheets/blocks/code-example/code-example.styl b/app/stylesheets/blocks/code-example/code-example.styl new file mode 100644 index 000000000..6e38bf570 --- /dev/null +++ b/app/stylesheets/blocks/code-example/code-example.styl @@ -0,0 +1,10 @@ +.code-example + margin 1.5em 0 + + &__codebox, + &__codebox .codebox, + &__codebox pre[class*="language-"] + margin-bottom 0 + + &__result + margin-top 0 diff --git a/app/stylesheets/blocks/codebox/codebox.styl b/app/stylesheets/blocks/codebox/codebox.styl new file mode 100644 index 000000000..b77dc19c8 --- /dev/null +++ b/app/stylesheets/blocks/codebox/codebox.styl @@ -0,0 +1,14 @@ +.codebox + position relative + margin 1.5em 0 + overflow auto + +.codebox__toolbar + position absolute + top 0 + right 0 + z-index 9 + opacity 0.8 + +.codebox__code + width 100% diff --git a/app/stylesheets/blocks/comments/comments.styl b/app/stylesheets/blocks/comments/comments.styl new file mode 100644 index 000000000..5f8177299 --- /dev/null +++ b/app/stylesheets/blocks/comments/comments.styl @@ -0,0 +1,210 @@ +comment-reply-color = #00A3D9 +.comments + margin-top 15px + + & &__header-title + border-bottom 0 + display inline + margin-right 16px + color color + font 30px/36px secondary_font + + &__header-title::before + @extend $font-comment + font-size 80% + color #eee + margin-right 9px + + &__header-number + color #aaa + + &__header-number::before + content "(" + + &__header-number::after + content ")" + + &__header-write:link + @extend $pseudo + color #666 + font-size 12px + vertical-align .4em + position relative + display inline-block + line-height 1 + + &__header-write::after + content "↓" + position absolute + margin-left 3px + vertical-align middle + +// .comments__items { +// overflow-x: hidden; +// margin: 25px -#{$content-horizontal-padding} 0; +// padding: 0 $content-horizontal-padding; +// border-bottom: 1px solid $separator-color; +// } + +// .comments__comment { +// font-size: 92%; +// line-height: 130%; +// position: relative; +// padding-top: 24px; +// } + +// .comments__comment::before { +// content: ""; +// position: absolute; +// top: 0; +// left: -200px; +// width: 1500px; +// height: 0; +// border-bottom: 1px solid $separator-color; +// } + +// .comments__comment .comments__comment { +// padding-left: 20px; +// } + +// // Don't increase indentation after 4th level +// .comments__comment +// .comments__comment +// .comments__comment +// .comments__comment +// .comments__comment { +// padding-left: 0; +// } + +// .comments__comment-header { +// @extend %clearfix; +// border-bottom: 0; +// padding-bottom: 0; +// margin-bottom: $lineheight*.65; +// } + +// .comments .comments__username { +// color: #444; +// } + +// .comments .comments__username:hover, +// .comments .comments__anchor:hover { +// color: $link-hover-color; +// } + +// .comments__userpic { +// float: left; +// } + +// .comments .comments__userpic { +// margin: 0; +// } + +// .comments__rating { +// float: right; +// } + +// .comments__info { +// padding: 0 12px; +// overflow: auto; +// } + +// .comments__date::before { +// display: none; +// } + +// .comments__date, +// .comments .comments__anchor { +// color: #888; +// font: 11px Arial, Helvetica, sans-serif; +// margin-right: 5px; +// } + +// .comments__star::before { +// @extend %font-star; +// color: #bbb; +// } + +// .comments__comment pre { +// font: 12px $fixed-width-font; +// color: #400000; +// } + +// .comments__footer { +// margin: 12px 0 22px; +// } + +// .comments .comments__reply { +// @extend %button-reset; + +// color: $comment-reply-color; +// cursor: pointer; +// text-decoration: none; // Тут может быть и ссылка, если будет якорь, или кнопка — если инлайн-форма +// border-bottom: 1px dashed transparent; +// font-size: 13px; +// } + +// .comments__reply:hover { +// color: $link-hover-color; +// border-bottom: 1px dashed transparentize($link-hover-color, .7); +// } + +// .comments__form { +// margin: 33px 0; +// } + +// .comments__form-header { +// @extend %clearfix; +// margin-bottom: 25px; +// } + +// .comments .comments__form-title { +// color: #888; +// margin: 0; +// float: left; +// } +// .comments__form-title::before { +// @extend %font-comment; +// color: #ccc; +// margin-right: 15px; +// } +// .comments .comments__form-formatting { +// @extend %pseudo; +// float: right; +// font-size: 12px; +// line-height: 1; +// margin-top: 8px; +// color: #656565; +// } +// .comments .comments__form-formatting:hover { +// color: $link-hover-color; +// } +// .comment-form__text { + +// width: 100%; +// height: 144px; +// margin: 16px 0; +// display: block; +// clear: both; +// } + +// @media (min-width: $media-step-1) { +// .comments__items { +// margin: 23px -#{$content-horizontal-padding + 10px}; +// padding: 0 ($content-horizontal-padding + 10px); +// } +// } + +// @media (min-width: $media-step-2) { +// .comments__items { +// margin: 23px -#{$content-horizontal-padding + 20px}; +// padding: 0 ($content-horizontal-padding + 20px); +// } +// } + +// @media (min-width: $media-step-3) { +// .comments__items { +// margin: 23px -#{$content-horizontal-padding + 30px}; +// padding: 0 ($content-horizontal-padding + 30px); +// } +// } \ No newline at end of file diff --git a/app/stylesheets/blocks/corrector/corrector.styl b/app/stylesheets/blocks/corrector/corrector.styl new file mode 100644 index 000000000..38bc3b5a3 --- /dev/null +++ b/app/stylesheets/blocks/corrector/corrector.styl @@ -0,0 +1,4 @@ +.corrector + padding 18px 0 + color #999 + font-style italic \ No newline at end of file diff --git a/app/stylesheets/blocks/dropdown/dropdown.styl b/app/stylesheets/blocks/dropdown/dropdown.styl new file mode 100644 index 000000000..de065a1a9 --- /dev/null +++ b/app/stylesheets/blocks/dropdown/dropdown.styl @@ -0,0 +1,2 @@ +.dropdown .dropdown__content + display none diff --git a/app/stylesheets/blocks/hide/hide.styl b/app/stylesheets/blocks/hide/hide.styl new file mode 100644 index 000000000..40734c21d --- /dev/null +++ b/app/stylesheets/blocks/hide/hide.styl @@ -0,0 +1,29 @@ +.hide-open, +.hide-closed + margin round(lineheight*.65) 0 + +.hide-closed .hide-content + display none + +.hide-open .hide-link, +.hide-closed .hide-link, +.hide-open .hide-link u, +.hide-closed .hide-link u + text-decoration none + color $link-color + +.hide-link:hover u + border-bottom 1px dashed + +.hide-link code + font inherit + color inherit + +.hide-link::after + margin-left 8px + +.hide-closed .hide-link::after + @extend $font-open + +.hide-open .hide-link::after + @extend $font-close \ No newline at end of file diff --git a/app/stylesheets/blocks/important/important.sprite/info.png b/app/stylesheets/blocks/important/important.sprite/info.png new file mode 100644 index 000000000..17236c791 Binary files /dev/null and b/app/stylesheets/blocks/important/important.sprite/info.png differ diff --git a/app/stylesheets/blocks/important/important.sprite/ok.png b/app/stylesheets/blocks/important/important.sprite/ok.png new file mode 100644 index 000000000..073ddb754 Binary files /dev/null and b/app/stylesheets/blocks/important/important.sprite/ok.png differ diff --git a/app/stylesheets/blocks/important/important.sprite/question.png b/app/stylesheets/blocks/important/important.sprite/question.png new file mode 100644 index 000000000..c53409bd6 Binary files /dev/null and b/app/stylesheets/blocks/important/important.sprite/question.png differ diff --git a/app/stylesheets/blocks/important/important.sprite/warning.png b/app/stylesheets/blocks/important/important.sprite/warning.png new file mode 100644 index 000000000..ce30b1f20 Binary files /dev/null and b/app/stylesheets/blocks/important/important.sprite/warning.png differ diff --git a/app/stylesheets/blocks/important/important.styl b/app/stylesheets/blocks/important/important.styl new file mode 100644 index 000000000..62bdd8048 --- /dev/null +++ b/app/stylesheets/blocks/important/important.styl @@ -0,0 +1,87 @@ +// $important-icon: sprite-map("important-icons/*.png"); + +.important + margin 18px 0 + border 3px solid #F5F2F0 + + &__header + margin 0 + padding 21px 25px 0 55px + border none + + &__type + font-weight: 700; + font-size: 17px; + + &__type::before + content "" + spriteImage $important-ok + position absolute + margin-left -30px + margin-top 1px + + &__title + display inline + margin 0 + + a, a u + color inherit + text-decoration none + + a:hover u + text-decoration underline + + & &__task-link + @extend $plain-link + + &__task-link + margin-right 40px + + &__task-link_empty + margin-right 0 + + &__task-link::after + @extend $font-external + position absolute + margin-left 5px + font-size 14px + margin-top 2px + color #005DB3 + font-weight 400 + + &__importance + font-size 11px + color #84836d + + &__task-link_empty + &__importance + margin-left 40px + + &__content + margin 12px 25px 22px + + & > .spoiler + border 0 + background none + margin 15px 0 + padding 10px 20px 10px 0 + + & > .spoiler + .spoiler + margin-top -15px + + & .spoiler__button + color #747361 + + &_warn &__type::before + spriteElem $important-warning + + &_smart &__type::before + spriteElem $important-info + + &_ponder &__type::before + spriteElem $important-question + + &_ok &__type::before + spriteElem $important-ok + + @media print + page-break-inside avoid diff --git a/app/stylesheets/blocks/links/link-types/doc.png b/app/stylesheets/blocks/links/link-types/doc.png new file mode 100644 index 000000000..ad02369dc Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/doc.png differ diff --git a/app/stylesheets/blocks/links/link-types/ecma.png b/app/stylesheets/blocks/links/link-types/ecma.png new file mode 100644 index 000000000..319fa6a12 Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/ecma.png differ diff --git a/app/stylesheets/blocks/links/link-types/mailto.png b/app/stylesheets/blocks/links/link-types/mailto.png new file mode 100644 index 000000000..9cddd1d13 Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/mailto.png differ diff --git a/app/stylesheets/blocks/links/link-types/mdn.png b/app/stylesheets/blocks/links/link-types/mdn.png new file mode 100644 index 000000000..e4d6c9f2c Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/mdn.png differ diff --git a/app/stylesheets/blocks/links/link-types/msdn.png b/app/stylesheets/blocks/links/link-types/msdn.png new file mode 100644 index 000000000..27b86de7d Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/msdn.png differ diff --git a/app/stylesheets/blocks/links/link-types/newwindow.png b/app/stylesheets/blocks/links/link-types/newwindow.png new file mode 100644 index 000000000..38521c4d9 Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/newwindow.png differ diff --git a/app/stylesheets/blocks/links/link-types/pdf.png b/app/stylesheets/blocks/links/link-types/pdf.png new file mode 100644 index 000000000..f5be21a11 Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/pdf.png differ diff --git a/app/stylesheets/blocks/links/link-types/sandbox.png b/app/stylesheets/blocks/links/link-types/sandbox.png new file mode 100644 index 000000000..095527198 Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/sandbox.png differ diff --git a/app/stylesheets/blocks/links/link-types/w3c.png b/app/stylesheets/blocks/links/link-types/w3c.png new file mode 100644 index 000000000..dfcd3d10f Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/w3c.png differ diff --git a/app/stylesheets/blocks/links/link-types/wiki.png b/app/stylesheets/blocks/links/link-types/wiki.png new file mode 100644 index 000000000..b9857a9cc Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/wiki.png differ diff --git a/app/stylesheets/blocks/links/link-types/xls.png b/app/stylesheets/blocks/links/link-types/xls.png new file mode 100644 index 000000000..ef3c95a3c Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/xls.png differ diff --git a/app/stylesheets/blocks/links/link-types/zip.png b/app/stylesheets/blocks/links/link-types/zip.png new file mode 100644 index 000000000..a53b7d7f0 Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/zip.png differ diff --git a/app/stylesheets/blocks/links/links.styl b/app/stylesheets/blocks/links/links.styl new file mode 100644 index 000000000..d191a1e73 --- /dev/null +++ b/app/stylesheets/blocks/links/links.styl @@ -0,0 +1,143 @@ +link-type(type) + padding-right image-width('link-types/' + type + '.png') + 3 + background-image url('/img/link-types/' + type + '.png') + +$pseudo + text-decoration none + border-bottom 1px dashed transparent + &:hover + border-bottom 1px dashed + text-decoration none + +$link-type + background-position 100% 60% + background-repeat no-repeat + +// .pseudo is for any tag but link that should look like pseudo-link +.pseudo + @extend $pseudo + cursor pointer + color link_color + +.pseudo:hover + color link_hover_color + +:link + color link_color + +:visited + color link_visited_color + +@media (print) + a:visited + color link_color + +:link, +:visited + text-decoration none + +a:hover, +a:active + color link_hover_color + text-decoration underline + +.anchor, +.pseudo + @extend $pseudo + +.link-ref + vertical-align super + font-size 90% + +// -- +.main [href^="mailto:"], +a.mailto + @extend $link-type + link-type 'mailto' + +// -- +.main [target="_blank"], +a.external + @extend $link-type + link-type 'newwindow' + +// -- +.main [href^='/play/'], +.main [href^='http://plnkr.co/'], +a.sandbox + @extend $link-type + link-type 'sandbox' + +// -- +.main [href$=".doc"], +.main [href$=".docx"], +a.doc + @extend $link-type + link-type 'doc' + +// -- +.main [href$=".zip"], +a.zip + @extend $link-type + link-type 'zip' + +// -- +.main [href$=".xls"], +.main [href$=".xlsx"], +a.xls + @extend $link-type + link-type 'xls' + +// -- +.main [href$=".pdf"], +a.pdf + @extend $link-type + link-type 'pdf' + +// -- +.main [href^='http://developer.mozilla.org'], +.main [href^='https://developer.mozilla.org'], +a.mdn + @extend $link-type + link-type 'mdn' + +// -- +.main [href^='http://msdn.microsoft.com'], +.main [href^='https://msdn.microsoft.com'], +a.msdn + @extend $link-type + link-type 'msdn' + +// -- +.main [href^='http://wikipedia.org'], +.main [href*='wikipedia.org'], +a.wiki + @extend $link-type + link-type 'wiki' + +// -- +.main [href^='http://w3.org'], +.main [href^='http://dev.w3.org'], +.main [href^='http://www.w3.org'], +.main [href^='https://www.w3.org'], +.main [href^='https://w3.org'], +a.w3c + @extend $link-type + link-type 'w3c' + +// -- +.main [href^='http://es5.github.com'], +a.ecma + @extend $link-type + link-type 'ecma' +// -- + +// placeholder selector, can't be moved to the top +// because order matters +// decared as placeholder to be extended easier +$plain-link + padding 0 + background-image none + +.main .plain + @extend $plain-link diff --git a/app/stylesheets/blocks/login/login.styl b/app/stylesheets/blocks/login/login.styl new file mode 100644 index 000000000..f964e3c67 --- /dev/null +++ b/app/stylesheets/blocks/login/login.styl @@ -0,0 +1,15 @@ + +.auth-form + fixed top 50% left 50% + width 400px + height 400px + margin-left -(@width / 2) + margin-top -(@height / 2) + background white + border 1px solid black + z-index 10000 + +.auth-form .progress + margin: 100px auto 0 auto + + diff --git a/app/stylesheets/blocks/logo/logo-light.png b/app/stylesheets/blocks/logo/logo-light.png new file mode 100644 index 000000000..58cb6048a Binary files /dev/null and b/app/stylesheets/blocks/logo/logo-light.png differ diff --git a/app/stylesheets/blocks/logo/logo.png b/app/stylesheets/blocks/logo/logo.png new file mode 100644 index 000000000..eae73179d Binary files /dev/null and b/app/stylesheets/blocks/logo/logo.png differ diff --git a/app/stylesheets/blocks/logo/logo.styl b/app/stylesheets/blocks/logo/logo.styl new file mode 100644 index 000000000..d76222fcc --- /dev/null +++ b/app/stylesheets/blocks/logo/logo.styl @@ -0,0 +1,18 @@ +////////// Блок из костылей и подпорок, все равно его не обновить ////////// +////////// простой заменой картинки, при редизайне - переделывать ////////// +.logo + width 500px + height image-height('logo-light.png') - 2px + background url('/img/logo-light.png') 25px 50% no-repeat + position relative + z-index 99 + padding 1px + + a, img + image-size('logo.png') + @extend $center + display block + margin 19px 0 0 100px + + a img + margin 0 diff --git a/app/stylesheets/blocks/main-content/main-content.styl b/app/stylesheets/blocks/main-content/main-content.styl new file mode 100644 index 000000000..c330275db --- /dev/null +++ b/app/stylesheets/blocks/main-content/main-content.styl @@ -0,0 +1,4 @@ +.main-content + display table + width 100% + table-layout fixed diff --git a/app/stylesheets/blocks/main/main.styl b/app/stylesheets/blocks/main/main.styl new file mode 100644 index 000000000..6b9ccab8b --- /dev/null +++ b/app/stylesheets/blocks/main/main.styl @@ -0,0 +1,287 @@ +// we use placeholder selector to make it easier to rename the block +// we can't extend selectors with &, so we don't use them for extends +$main-loud + font-size 15px + font-weight 700 + line-height 21px + margin 1em 0 + +.main + display table-cell + // position relative + padding 0 content_horizontal_padding 35px + + .breadcrumbs + margin 34px 0 15px + padding 0 + + &__header + padding 0 0 12px + border-bottom 2px solid #f5f2f0 + + &__header_center + border 0 + + &__header-title, + .breadcrumbs + text-align center + + &__header_search + border-bottom 0 + margin-top 33px + + & &__header-title + border-bottom 2px solid #f5f2f0 + font 700 40px/40px secondary_font + margin 18px 0 12px + padding 0 0 14px + + &__header-title_noseparator + border-bottom 0 + + &__header_search &__header-title + margin-bottom 0 + padding-bottom 10px + + &__username + font-style inherit + font-weight inherit + color secondary_color + + &__header-date, + &__header-comments a + color #666 + + &__header-comments a + text-decoration underline + + &__header-comments:hover + color link_hover_color + + &__header-date + margin-right 23px + + &__header-date::before, + &__header-comments::before + color secondary_color + + &__header-date::before + @extend $font-time + margin-right 3px + + &__header-comments::before + @extend $font-comment + margin-right 3px + + &__header-nav + float right + + &__lesson-nav + display table + float right + + &__lesson-nav-prev, + &__lesson-nav-next + display table-cell + + &__lesson-nav-prev + padding-right 15px + border-right 2px solid #f5f2f0 + + &__lesson-nav-next + padding-left 15px + + &__lesson-nav-prev:last-child + border none + padding-right 0 + + &__lesson-nav kbd + font-size 11px + color #999 + font: inherit + + &__lesson-nav-arr + position relative + top -.1em + + &__lesson-nav-link + color #656565 + text-decoration underline + + ul, + ol + padding-left 25px + margin 1em 0 + + > li + margin: .5em 0 + + ul > li::before + @extend $font-bullet + position absolute + margin-left -20px + color #B2C1C1 + + // TODO: h1 вне блока .main__header ??! Проверить, встречается ли + h1 + margin-bottom .5em + /* FIXME пока некрасиво с этим: page-break-before: always; */ + + h2 + padding-bottom 11px + margin 36px 0 6px + font-size 34px + line-height 40px + font-family secondary_font + + h3, + h4 + margin 24px 0 8px + + h3 + font-size 128% + line-height 22px + + h4 + font-size 114% + line-height 20px + + h1 a, + h1 code, + h2 a, + h2 code, + h3 a, + h3 code, + h4 a, + h4 code + font inherit + color inherit + text-decoration none + + // .format_error - old code support? + .format_error + color red + + div.format_error + border 1px solid red + padding 5px + + .admin_link + float right + color gray + + h2 a[href^='#']:focus, + h3 a[href^='#']:focus, + h4 a[href^='#']:focus + outline none + + code + color code_color + font-family fixed_width_font + + /* for regexps */ + code.pattern + border-bottom 1px solid red + + code.subject + border-bottom 1px solid blue + + code.match + border-bottom 1px solid green + + span.shortcut + white-space nowrap + + span.shortcut code + border 1px solid rgb(51,51,51) + padding 0 1px + display inline-block + margin 1px 0 + + p + margin round(lineheight*.65) 0 + + // Is it really used? + dl + margin 1em 0 + + dt + font-weight 700 + + dd + margin 0 0 round(lineheight*.8) 40px + + figure, + img + margin 30px 0 + + figure img + margin 0 + + &__loud + @extend $main-loud + + &__strong + @extend $main-loud + font-style italic + color #8C0000 + + &__footer + @extend $clearfix + background #F5F5F2 + margin 30px -40px 27px + padding 16px 40px 9px + + .rating + margin-right 22px + + .social + float right + + &__footer-date, + &__footer-author, + &__footer-author a + color #444 + font 13px Arial, Helvetica, sans-serif + + &__footer-date + margin-right 15px + + &__footer-author + margin-right 10px + + &__footer-star + position absolute + font-size 18px + color #BBB + margin 0 10px + + &__footer-star::before + @extend $font-star + + @media (min-width: media_step_1) + padding 0 (content_horizontal_padding + 10px) 35px + line-height 19px + + &__footer + margin 30px -1*(content_horizontal_padding + 10px) 27px + padding 16px (content_horizontal_padding + 10px) 9px + + @media (min-width: media_step_2) + padding 0 (content_horizontal_padding + 20px) 35px + line-height 20px + + &__footer + margin 30px -1*(content_horizontal_padding + 20px) 27px + padding 16px (content_horizontal_padding + 20px) 9px + + @media (min-width: media_step_3) + padding 0 (content_horizontal_padding + 30px) 35px + line-height 22px + + &__footer + margin 30px -1*(content_horizontal_padding + 30px) 27px + padding 16px (content_horizontal_padding + 30px) 9px + + @media (min-width: largescreen) + font-size 16px diff --git a/app/stylesheets/blocks/mixins/imagesize.styl b/app/stylesheets/blocks/mixins/imagesize.styl new file mode 100644 index 000000000..5a9b77d90 --- /dev/null +++ b/app/stylesheets/blocks/mixins/imagesize.styl @@ -0,0 +1,9 @@ +image-width(img) + return image-size(img)[0] + +image-height(img) + return image-size(img)[1] + +size-of(img) + width image-width(img) + height image-height(img) \ No newline at end of file diff --git a/app/stylesheets/blocks/mixins/sprite.styl b/app/stylesheets/blocks/mixins/sprite.styl new file mode 100644 index 000000000..13ead9f1a --- /dev/null +++ b/app/stylesheets/blocks/mixins/sprite.styl @@ -0,0 +1,27 @@ +spriteWidth($sprite) + width $sprite[4] + +spriteHeight($sprite) + height $sprite[5] + +spritePosition($sprite) + background-position $sprite[2] $sprite[3] + +spriteImage($sprite) + if length($sprite) == 3 + background-image url($sprite[2]) + else + background-image url($sprite[8]) + +sprite($sprite) + if !match('hover', selector()) && !match('active', selector()) + spriteImage($sprite) + spritePosition($sprite) + spriteWidth($sprite) + spriteHeight($sprite) + + +spriteElem($sprite) + spritePosition($sprite) + spriteWidth($sprite) + spriteHeight($sprite) \ No newline at end of file diff --git a/app/stylesheets/blocks/nav-dropdown/nav-dropdown.styl b/app/stylesheets/blocks/nav-dropdown/nav-dropdown.styl new file mode 100644 index 000000000..6a97ae5bf --- /dev/null +++ b/app/stylesheets/blocks/nav-dropdown/nav-dropdown.styl @@ -0,0 +1,27 @@ +.dropdown_nav + position relative + +.dropdown__toggle_nav::after + @extend $font-open + margin-left 5px + color #b0b2b3 + line-height 0 + +.navbar__sections-item.open .dropdown__toggle_nav::after, +.navbar__sections-item_active .dropdown__toggle_nav::after + color secondary_color + +.dropdown__content_nav + background #e9e9dd + padding 4px 0 12px + + & > li + margin 0 15px 4px 40px + font-size 12px + + a + color #0063b9 + + a:hover + color link_hover_color + text-decoration none diff --git a/app/stylesheets/blocks/navbar/navbar.styl b/app/stylesheets/blocks/navbar/navbar.styl new file mode 100644 index 000000000..e28a1f7b1 --- /dev/null +++ b/app/stylesheets/blocks/navbar/navbar.styl @@ -0,0 +1,121 @@ +.navbar + background #e9e9dd + border-top-left-radius 4px + border-top-right-radius 4px + font 700 15px secondary_font + +.navbar__sections + line-height 30px + display table + width 100% + +.navbar__sections-item + display table-cell + vertical-align top + text-align center + border-right 1px solid #384144 + text-transform uppercase + white-space nowrap + color secondary_color + +.navbar__sections-item_active, +.navbar__sections-link + padding 4px 15px 7px 10px + +.navbar__sections-item_active .navbar__sections-link + margin -4px -15px -7px -10px + +.navbar__sections-item_active .navbar__sections-link:link, +.navbar__sections-item_active .navbar__sections-link:visited, +.navbar__sections-item_active .navbar__sections-link:hover, +.navbar__sections-item_active .navbar__sections-link:active, +.navbar__sections-item.open .navbar__sections-link + background none + color secondary_color + +.navbar__sections-link + display block + background #22292b + +.navbar__sections-link + &:hover + background #373E3F + &:link + &:visited + &:hover + &:active + color #fff + text-decoration none + +.navbar__icon + font-weight 400 + padding-right 10px + line-height 0 + color #caf3ff + +.navbar__sections-item_active .navbar__icon, +.navbar__sections-item.open .navbar__icon + color secondary_color + +.navbar__sections-link::before + color #caf3ff + +.navbar__icon_study::before + @extend $font-study + +.navbar__icon_blogs::before + @extend $font-blogs + +.navbar__icon_qa::before + @extend $font-qa + +.navbar__icon_reference::before + @extend $font-reference + +.navbar__icon_events::before + @extend $font-events + +.navbar__icon_job::before + @extend $font-job + +.navbar__icon_sandbox::before + @extend $font-code + +.navbar__icon_chat::before + @extend $font-chat + +.navbar__icon_search + padding-right 0 + color #fff + +.navbar__icon_search::before + @extend $font-search + +.navbar__sections-item_search + padding 0 + width 40px + text-align center + background #22292b + + .dropdown__toggle + @extend $button-reset + font-size 16px + width 40px + height 40px + display block + + + &.open + background #B81800 + +.navbar__sections-item:first-child, +.navbar__sections-item:first-child .navbar__sections-link + border-top-left-radius 4px + + +.navbar__sections-item:nth-last-child(2).navbar__sections-item_active + border-right 1px solid transparent + +.navbar__sections-item:last-child + border-right 0 + border-top-right-radius 4px diff --git a/app/stylesheets/blocks/page-footer/page-footer.png b/app/stylesheets/blocks/page-footer/page-footer.png new file mode 100644 index 000000000..4c165cc2d Binary files /dev/null and b/app/stylesheets/blocks/page-footer/page-footer.png differ diff --git a/app/stylesheets/blocks/page-footer/page-footer.styl b/app/stylesheets/blocks/page-footer/page-footer.styl new file mode 100644 index 000000000..59c893dba --- /dev/null +++ b/app/stylesheets/blocks/page-footer/page-footer.styl @@ -0,0 +1,69 @@ +.page-footer + display table + width 100% + background #666 + + &__contents, + &__copy + display table-cell + vertical-align top + + &__title, + a + color #fff + font-size 15px + + a + text-decoration underline + + &__contents + padding 35px 40px 60px + background url('/img/page-footer.png') 97.5% 35px no-repeat + + &__list + font-weight 700 + + &__list-item + margin-bottom 6px + + &__title + margin-bottom 8px + + &__list.small &__list-item + margin-bottom 3px + a + font-weight 400 + font-size 13px + + &__col + float left + width 20.6% + margin 0 3% 0 0 + + &__copy + width sidebar_width + padding 35px sidebar_padding_right 60px sidebar_padding_left + color #fff + font-style normal + a + font-size 13px + + &__line + display block + margin-bottom 3px + + &__line.author + margin-bottom 6px + + @media (min-width: media_step_1) + &__contents + padding-left content_horizontal_padding + 10px + + + @media (min-width: media_step_2) + &__contents + padding-left content_horizontal_padding + 20px + + @media (min-width: media_step_3) + &__contents + padding-left content_horizontal_padding + 30px diff --git a/app/stylesheets/blocks/page/page-central-cloud.png b/app/stylesheets/blocks/page/page-central-cloud.png new file mode 100644 index 000000000..05d434a68 Binary files /dev/null and b/app/stylesheets/blocks/page/page-central-cloud.png differ diff --git a/app/stylesheets/blocks/page/page-left-cloud.png b/app/stylesheets/blocks/page/page-left-cloud.png new file mode 100644 index 000000000..dbf0bc14c Binary files /dev/null and b/app/stylesheets/blocks/page/page-left-cloud.png differ diff --git a/app/stylesheets/blocks/page/page-right-cloud.png b/app/stylesheets/blocks/page/page-right-cloud.png new file mode 100644 index 000000000..c1c2d2586 Binary files /dev/null and b/app/stylesheets/blocks/page/page-right-cloud.png differ diff --git a/app/stylesheets/blocks/page/page.styl b/app/stylesheets/blocks/page/page.styl new file mode 100644 index 000000000..5fab892fb --- /dev/null +++ b/app/stylesheets/blocks/page/page.styl @@ -0,0 +1,22 @@ +.page + @extend $min-max-width + @extend $center + + box-shadow: 0 0 3px 3px rgba(128, 128, 128, 0.3) + position: relative + z-index: 0 + &::before + content: "" + display: block + position: absolute + top: -120px + left: -20px + right: -8px + height: 182px + z-index: -1 + background: url('/img/page-left-cloud.png') 0 35px no-repeat, + url('/img/page-central-cloud.png') 50% 35px no-repeat, + url('/img/page-right-cloud.png') 100% 0 no-repeat + + .page__inner + background: background diff --git a/app/stylesheets/blocks/placeholders/button-reset.styl b/app/stylesheets/blocks/placeholders/button-reset.styl new file mode 100644 index 000000000..36187b5c5 --- /dev/null +++ b/app/stylesheets/blocks/placeholders/button-reset.styl @@ -0,0 +1,14 @@ +$button-reset + border 0 + background none + display inline + margin 0 + padding 0 + cursor pointer + + &::-moz-focus-inner + border none + padding 0 + + &:active + position relative diff --git a/app/stylesheets/blocks/placeholders/center.styl b/app/stylesheets/blocks/placeholders/center.styl new file mode 100644 index 000000000..a35116746 --- /dev/null +++ b/app/stylesheets/blocks/placeholders/center.styl @@ -0,0 +1,2 @@ +$center + margin auto \ No newline at end of file diff --git a/app/stylesheets/blocks/placeholders/clearfix.styl b/app/stylesheets/blocks/placeholders/clearfix.styl new file mode 100644 index 000000000..4f20bdede --- /dev/null +++ b/app/stylesheets/blocks/placeholders/clearfix.styl @@ -0,0 +1,6 @@ +$clearfix + &::after + content "" + display block + overflow hidden + clear both \ No newline at end of file diff --git a/app/stylesheets/blocks/placeholders/font-icons.styl b/app/stylesheets/blocks/placeholders/font-icons.styl new file mode 100644 index 000000000..ebb7fd724 --- /dev/null +++ b/app/stylesheets/blocks/placeholders/font-icons.styl @@ -0,0 +1,153 @@ +@font-face + font-family 'FontIcons' + src url('/fonts/icons/icons.eot') + src local('☺'), url('/fonts/icons/icons.woff') format('woff'), url('/fonts/icons/icons.ttf') format('truetype'), url('/fonts/icons/icons.svg') format('svg') + font-weight normal + font-style normal + +$font-icon + font-family 'FontIcons' + +$font-close + @extend $font-icon + content '\25b4' + +$font-open + @extend $font-icon + content '\25be' + +$font-star + @extend $font-icon + content '\2605' + +$font-star-empty + @extend $font-icon + content '\2606' + +$font-warning + @extend $font-icon + content '\26a0' + +$font-mail + @extend $font-icon + content '\2709' + +$font-edit + @extend $font-icon + content '\270d' + +$font-ok + @extend $font-icon + content '\2714' + +$font-qa + @extend $font-icon + content '\2753' + +$font-pros + @extend $font-icon + content '\e059' + +$font-cons + @extend $font-icon + content '\e075' + +$font-help + @extend $font-icon + content '\e704' + +$font-info + @extend $font-icon + content '\e705' + +$font-code + @extend $font-icon + content '\e714' + +$font-external + @extend $font-icon + content '\e715' + +$font-download + @extend $font-icon + content '\e805' + +$font-print + @extend $font-icon + content '\e716' + +$font-chat + @extend $font-icon + content '\e720' + +$font-bullet + @extend $font-icon + content '\e75e' + +$font-run + @extend $font-icon + content '\f00f' + +$font-copy + @extend $font-icon + content '\f0c5' + +$font-comment + @extend $font-icon + content '\f4ac' + +$font-search + @extend $font-icon + content '\f50d' + +$font-study + @extend $font-icon + content '\1f393' + +$font-user + @extend $font-icon + content '\1f464' + +$font-job + @extend $font-icon + content '\1f4bc' + +$font-blogs + @extend $font-icon + content '\1f4c4' + +$font-events + @extend $font-icon + content '\1f4c5' + +$font-reference + @extend $font-icon + content '\1f4d5' + +$font-time + @extend $font-icon + content '\1f554' + +$font-video + @extend $font-icon + content '\e800' + +$font-mobile + @extend $font-icon + content '\e801' + +$font-printable + @extend $font-icon + content '\e802' + +$font-pencil + @extend $font-icon + content '\e803' + +$font-trash + @extend $font-icon + content '\e804' + +$font-sitemap + @extend $font-icon + content '\e806' diff --git a/app/stylesheets/blocks/placeholders/min-max-width.styl b/app/stylesheets/blocks/placeholders/min-max-width.styl new file mode 100644 index 000000000..7e515bc85 --- /dev/null +++ b/app/stylesheets/blocks/placeholders/min-max-width.styl @@ -0,0 +1,4 @@ +$min-max-width + min-width min_width + max-width max_width + \ No newline at end of file diff --git a/app/stylesheets/blocks/prev-next/prev-next.styl b/app/stylesheets/blocks/prev-next/prev-next.styl new file mode 100644 index 000000000..434938e1e --- /dev/null +++ b/app/stylesheets/blocks/prev-next/prev-next.styl @@ -0,0 +1,53 @@ +.prev-next + display table + table-layout fixed + + &__prev, + &__next + display table-cell + + &__prev + text-align left + padding-right 10px + + &__next + text-align right + padding-left 10px + + &__shortcut + color #999 + font inherit + + &__arr + position relative + top -.1em + + &_top &__link + color #656565 + text-decoration underline + margin 0 .5ex + + &_top &__prev + border-right 2px solid #f5f2f0 + + &_top &__prev:last-child + text-align right + border 0 + padding-right 0 + + &_top &__next:first-child + padding-left 0 + + &_top &__shortcut + font-size 11px + + &_bottom + width 100% + padding 12px 0 + border-bottom 1px solid #ddd + + &_bottom &__shortcut + margin-left .5ex + + &_bottom &__shortcut-wrap + margin-bottom 5px diff --git a/app/stylesheets/blocks/prism/my-prism.styl b/app/stylesheets/blocks/prism/my-prism.styl new file mode 100644 index 000000000..c47905c06 --- /dev/null +++ b/app/stylesheets/blocks/prism/my-prism.styl @@ -0,0 +1,36 @@ +pre[class*="language-"], +code[class*="language-"] + font 14px/17px fixed_width_font + z-index 0 + text-shadow none + margin 0 + +pre[class*="language-"] + position relative + code + color inherit + position relative + +pre.line-numbers + padding-left 3.2em + +// span with line numbers is moved from to the outer
    ,
    +// because we need to handle many ... inside single 
    +// (this we need for highlighting *!*...* /!* inline
    +.line-numbers .line-numbers-rows
    +	left 0
    +	top 0
    +	padding 1em 0
    +	border 0
    +	background #e7e5e3
    +	width auto
    +
    +.line-numbers-rows > span:before
    +	padding 0 .7em 0 .8em
    +	text-shadow none
    +
    +@media (min-width: largescreen)
    +	pre[class*="language-"],
    +	code[class*="language-"]
    +		font-size 16px
    +		line-height 19px
    diff --git a/app/stylesheets/blocks/prism/prism-line-highlight.styl b/app/stylesheets/blocks/prism/prism-line-highlight.styl
    new file mode 100644
    index 000000000..5e053e0d9
    --- /dev/null
    +++ b/app/stylesheets/blocks/prism/prism-line-highlight.styl
    @@ -0,0 +1,31 @@
    +.inline-highlight
    +	position absolute
    +	pointer-events none
    +	line-height inherit
    +	white-space pre
    +	left 0
    +	top -1px
    +	z-index -1
    +
    +	.mask 
    +		background #F5E7C6
    +		outline 2px solid #F5E7C6
    +
    +.block-highlight
    +	position absolute
    +	left 0
    +	right 0
    +	top -1px
    +	padding inherit 0
    +	margin-top 1em /* Same as .prism’s padding-top */
    +
    +	pointer-events none
    +
    +	line-height inherit
    +	white-space pre
    +	.mask
    +		background #F5E7C6
    +		outline 2px solid #F5E7C6
    +		left 0
    +		right 0
    +		position absolute
    \ No newline at end of file
    diff --git a/app/stylesheets/blocks/prism/prism-line-numbers.styl b/app/stylesheets/blocks/prism/prism-line-numbers.styl
    new file mode 100644
    index 000000000..7f4a87511
    --- /dev/null
    +++ b/app/stylesheets/blocks/prism/prism-line-numbers.styl
    @@ -0,0 +1,31 @@
    +pre.line-numbers
    +	position relative
    +	padding-left 3.8em
    +	counter-reset linenumber
    +
    +.line-numbers .line-numbers-rows
    +	position absolute
    +	pointer-events none
    +	top 0
    +	font-size 100%
    +	left -3.8em
    +	width 3em /* works for line-numbers below 1000 lines */
    +	letter-spacing -1px
    +	border-right 1px solid #999
    +
    +	-webkit-user-select none
    +	-moz-user-select none
    +	-ms-user-select none
    +	user-select none
    +
    +.line-numbers-rows > span
    +	pointer-events none
    +	display block
    +	counter-increment linenumber
    +
    +.line-numbers-rows > span:before
    +	content counter(linenumber)
    +	color #999
    +	display block
    +	padding-right 0.8em
    +	text-align right
    \ No newline at end of file
    diff --git a/app/stylesheets/blocks/prism/prism.styl b/app/stylesheets/blocks/prism/prism.styl
    new file mode 100644
    index 000000000..3a42ad81d
    --- /dev/null
    +++ b/app/stylesheets/blocks/prism/prism.styl
    @@ -0,0 +1,101 @@
    +/**
    + * prism.js default theme for JavaScript, CSS and HTML
    + * Based on dabblet (http://dabblet.com)
    + * @author Lea Verou
    + */
    +
    +code[class*="language-"],
    +pre[class*="language-"]
    +	color black
    +	text-shadow 0 1px white
    +	font-family Consolas, Monaco, 'Andale Mono', monospace
    +	direction ltr
    +	text-align left
    +	white-space pre
    +	word-spacing normal
    +
    +	-moz-tab-size 4
    +	-o-tab-size 4
    +	tab-size 4
    +
    +	-webkit-hyphens none
    +	-moz-hyphens none
    +	-ms-hyphens none
    +	hyphens none
    +
    +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
    +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection
    +	text-shadow none
    +	background #b3d4fc
    +
    +pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
    +code[class*="language-"]::selection, code[class*="language-"] ::selection
    +	text-shadow none
    +	background #b3d4fc
    +
    +@media print
    +	code[class*="language-"],
    +	pre[class*="language-"]
    +		text-shadow: none
    +
    +/* Code blocks */
    +pre[class*="language-"]
    +	padding 1em
    +	margin 1.5em 0
    +	overflow auto
    +
    +:not(pre) > code[class*="language-"],
    +pre[class*="language-"]
    +	background: background_blocks
    +
    +/* Inline code */
    +:not(pre) > code[class*="language-"]
    +	padding .1em
    +	border-radius .3em
    +
    +.token.comment,
    +.token.prolog,
    +.token.doctype,
    +.token.cdata
    +	color slategray
    +
    +.token.punctuation
    +	color #999
    +
    +.namespace
    +	opacity .7
    +
    +.token.property,
    +.token.tag,
    +.token.boolean,
    +.token.number
    +	color #905
    +
    +.token.selector,
    +.token.attr-name,
    +.token.string
    +	color #690
    +
    +.token.operator,
    +.token.entity,
    +.token.url,
    +.language-css .token.string,
    +.style .token.string
    +	color #a67f59
    +	/* background: hsla(0,0%,100%,.5); */
    +
    +.token.atrule,
    +.token.attr-value,
    +.token.keyword
    +	color #07a
    +
    +
    +.token.regex,
    +.token.important
    +	color #e90
    +
    +.token.important
    +	font-weight bold
    +
    +.token.entity 
    +	cursor help
    diff --git a/app/stylesheets/blocks/progress/progress.styl b/app/stylesheets/blocks/progress/progress.styl
    new file mode 100644
    index 000000000..1ce520f07
    --- /dev/null
    +++ b/app/stylesheets/blocks/progress/progress.styl
    @@ -0,0 +1,69 @@
    +/**
    + * (C)Leanest CSS spinner ever
    + * From http://lea.verou.me/2013/11/cleanest-css-spinner-ever/
    + *
    + * Usage: 
    Loading…
    + +
    Loading…
    + +
    Loading…
    + */ + +@keyframes spin { + to { transform: rotate(1turn); } +} + +.progress { + position: relative; +// display: inline-block; + width: 5em; + height: 5em; + margin: 0 .5em; + font-size: 12px; + text-indent: 999em; + overflow: hidden; + animation: spin 1s infinite steps(8); +} + +.small.progress { + font-size: 6px; +} + +.large.progress { + font-size: 24px; +} + +.progress:before, +.progress:after, +.progress > div:before, +.progress > div:after { + content: ''; + position: absolute; + top: 0; + left: 2.25em; /* (container width - part width)/2 */ + width: .5em; + height: 1.5em; + border-radius: .2em; + background: #eee; + box-shadow: 0 3.5em #eee; /* container height - part height */ + transform-origin: 50% 2.5em; /* container height / 2 */ +} + +.progress:before { + background: #555; +} + +.progress:after { + transform: rotate(-45deg); + background: #777; +} + +.progress > div:before { + transform: rotate(-90deg); + background: #999; +} + +.progress > div:after { + transform: rotate(-135deg); + background: #bbb; +} diff --git a/app/stylesheets/blocks/reset/reset.styl b/app/stylesheets/blocks/reset/reset.styl new file mode 100644 index 000000000..b101ae61d --- /dev/null +++ b/app/stylesheets/blocks/reset/reset.styl @@ -0,0 +1,28 @@ +body, +h1, h2, h3, h4, h5, h6, +p, blockquote, pre, address, +dl, dd, ul, ol, +fieldset, form, legend, +th, td, +figure, figcaption + margin 0 + padding 0 + +section, article, aside, +figure, figcaption, +header, hgroup, footer, +nav + display block + +a img + border 0 + +ul + list-style none + +p + margin $lineheight 0 + +.invisible + visibility hidden + \ No newline at end of file diff --git a/app/stylesheets/blocks/result/result.styl b/app/stylesheets/blocks/result/result.styl new file mode 100644 index 000000000..55cf73c5f --- /dev/null +++ b/app/stylesheets/blocks/result/result.styl @@ -0,0 +1,15 @@ +.result + position relative + border 1px solid #e7e5e3 + margin 1.5em 0 + + &__toolbar + position absolute + top 0 + right 0 + + &__iframe + display block + border 0 + width 100% + height 100px diff --git a/app/stylesheets/blocks/shortcut/shortcut.styl b/app/stylesheets/blocks/shortcut/shortcut.styl new file mode 100644 index 000000000..c8f62e089 --- /dev/null +++ b/app/stylesheets/blocks/shortcut/shortcut.styl @@ -0,0 +1,11 @@ +.shortcut + background #f6f4f2 + border 1px solid #e8e6e5 + border-radius 3px + padding 1px 2px 0 + font-family fixed_width_font + line-height inherit + word-spacing -.4ex + + &__plus + color #b8b7b7 diff --git a/app/stylesheets/blocks/sidebar/sidebar-bg.png b/app/stylesheets/blocks/sidebar/sidebar-bg.png new file mode 100644 index 000000000..121144413 Binary files /dev/null and b/app/stylesheets/blocks/sidebar/sidebar-bg.png differ diff --git a/app/stylesheets/blocks/sidebar/sidebar.styl b/app/stylesheets/blocks/sidebar/sidebar.styl new file mode 100644 index 000000000..634a1cb60 --- /dev/null +++ b/app/stylesheets/blocks/sidebar/sidebar.styl @@ -0,0 +1,87 @@ +.sidebar + box-shadow inset 10px 0 10px -10px #ddd + display table-cell + vertical-align top + width sidebar_width + min-width sidebar_width + padding: 0 sidebar_padding_right 35px sidebar_padding_left; + background url('/img/sidebar-bg.png') #F1F1F1 + +// .sidebar__section { +// margin: 25px 0 35px; +// } + +// .sidebar__list ul { +// margin: 8px 0 0 10px; +// } + +// .sidebar__list-item { +// margin: 0 0 6px; +// list-style: none; +// text-overflow: ellipsis; +// overflow: hidden; +// line-height: normal; + +// > .menu { +// margin: 10px 0 0 10px; +// } +// } + +// .sidebar__title { +// display: block; +// text-decoration: none; +// margin: 25px 0 12px; +// padding: 0 0 12px; +// font-size: 16px; +// font-weight: 700; +// color: #666; +// border-bottom: 1px solid #ccc; +// } + +// .sidebar__title_small { +// font-size: 12px; +// padding: 0 0 8px; +// } + +// .sidebar__complex-title { +// margin: 35px 0 10px; +// } + +// .sidebar__subtitle { +// font-size: 12px; +// font-weight: 400; +// } + +// .sidebar__subtitle + .sidebar__title { +// margin: -3px 0 10px; +// } + +// a { +// font-size: 13px; +// color: $link-color; +// } + +// a:hover { +// color: $link-hover-color; +// } + +// .sidebar__list-item.active > a { +// color: #B20000; +// font-weight: 700; +// } + +// .keep-visible.cloned { +// width: $sidebar-width; +// } + +// .fixed { +// position: fixed; +// top: 10px; +// margin: 0; +// width: $sidebar-width; +// .sidebar__title, +// .sidebar__complex-title { +// margin-top: 0; +// } +// } +// } \ No newline at end of file diff --git a/app/stylesheets/blocks/soc-icon/soc-icon.styl b/app/stylesheets/blocks/soc-icon/soc-icon.styl new file mode 100644 index 000000000..b8bb3fc51 --- /dev/null +++ b/app/stylesheets/blocks/soc-icon/soc-icon.styl @@ -0,0 +1,14 @@ +.soc-icon + spriteImage $soc_icon + + &.google + spriteElem $soc_icon-google + + &.facebook + spriteElem $soc_icon-facebook + + &.twitter + spriteElem $soc_icon-twitter + + &.vk + spriteElem $soc_icon-vk diff --git a/app/stylesheets/blocks/soc-icon/soc_icon.sprite/facebook.png b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/facebook.png new file mode 100644 index 000000000..dfb94bc83 Binary files /dev/null and b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/facebook.png differ diff --git a/app/stylesheets/blocks/soc-icon/soc_icon.sprite/google.png b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/google.png new file mode 100644 index 000000000..d4d94951d Binary files /dev/null and b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/google.png differ diff --git a/app/stylesheets/blocks/soc-icon/soc_icon.sprite/twitter.png b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/twitter.png new file mode 100644 index 000000000..bd610c8a3 Binary files /dev/null and b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/twitter.png differ diff --git a/app/stylesheets/blocks/soc-icon/soc_icon.sprite/vk.png b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/vk.png new file mode 100644 index 000000000..e8b827e34 Binary files /dev/null and b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/vk.png differ diff --git a/app/stylesheets/blocks/social/social.styl b/app/stylesheets/blocks/social/social.styl new file mode 100644 index 000000000..830b325b6 --- /dev/null +++ b/app/stylesheets/blocks/social/social.styl @@ -0,0 +1,74 @@ +.social + & &__anchor-up + @extend $pseudo; + font-size 11px + color #868686 + border-bottom 1px dashed + line-height 1 + + & &__anchor-up:hover + color link_hover_color + + & &__soc + line-height 0 + img + margin 0 + opacity .5 + + body & &__soc + @extend $plain-link + + &__soc:hover + img + opacity 1 + + &.aside + box-shadow 0 0 1px 1px rgba(128, 128, 128, 0.3) + float left + margin-left -57px + max-width 45px + padding 5px 7px 14px + background #ECF3F3 + border-radius 3px + + &.aside &__anchor-up + display inline-block + margin-bottom 8px + + &.aside &__soc + @extend $center + display block + width 24px + margin-top 4px + img + display block + + &.aside.fixed + position fixed + top 20px + + &.inline + font-size 0 + line-height 0 + display inline-block + & &__soc + font-size: size + display inline-block + width 24px + margin-left 5px + + @media (min-width: media_step_1) + &.aside + margin-left -62px + + @media (min-width: media_step_2) + &.aside + margin-left -66px + + @media (min-width: media_step_3) + &.aside + margin-left -68px + + @media screen and (max-device-width: 760px) + &.aside + display none \ No newline at end of file diff --git a/app/stylesheets/blocks/spoiler/spoiler.styl b/app/stylesheets/blocks/spoiler/spoiler.styl new file mode 100644 index 000000000..9564b427e --- /dev/null +++ b/app/stylesheets/blocks/spoiler/spoiler.styl @@ -0,0 +1,38 @@ +.spoiler + background background_blocks + border-top 1px solid #D9D7D6 + margin 20px 0 + padding 15px 20px 20px + + &__button + @extend $button-reset + color link_color + position relative + font size/lineheight font + padding 1px 1px 1px 0 + text-decoration none + outline none + u + text-decoration none + + + &__button:hover + color link_hover_color + u + @extend $pseudo + + &__button::after + margin-left 1ex + @extend $font-close + + &.closed &__button::after + @extend $font-open + + &__content + margin-top 20px + + & + & + margin-top -20px + + &.closed &__content + display none diff --git a/app/stylesheets/blocks/standard-table/standard-table.styl b/app/stylesheets/blocks/standard-table/standard-table.styl new file mode 100644 index 000000000..a15827e78 --- /dev/null +++ b/app/stylesheets/blocks/standard-table/standard-table.styl @@ -0,0 +1,41 @@ +.main table + width 100% + border-collapse collapse + font-size 13px + margin 30px 0 + + tbody + border 0 + + tr + border-bottom 1px solid #ccc + + tr:first-child th + border-bottom 3px solid #CCC + vertical-align bottom + + th + text-align left + + th, + td + padding 2px 1em 2px 5px + + tr:nth-child(even) + background #f9f9f9 + + code + font-weight inherit + +.main table.bordered + td, + th + border solid #ccc + border-width 0 1px + + th + border-width 0 0 1px + +@media (min-width: largescreen) + .main table + font-size: 15px; diff --git a/app/stylesheets/blocks/summary/summary.styl b/app/stylesheets/blocks/summary/summary.styl new file mode 100644 index 000000000..35ea3c092 --- /dev/null +++ b/app/stylesheets/blocks/summary/summary.styl @@ -0,0 +1,15 @@ +.summary + background background_blocks + margin round(lineheight*.65) 0 + border 1px solid separator_color + + &__content + margin 22px + + &_noborder + border 0 + padding 1px 0 + border-radius 4px + + @media print + page-break-inside: avoid; diff --git a/app/stylesheets/blocks/toolbar/toolbar.styl b/app/stylesheets/blocks/toolbar/toolbar.styl new file mode 100644 index 000000000..deea71802 --- /dev/null +++ b/app/stylesheets/blocks/toolbar/toolbar.styl @@ -0,0 +1,40 @@ +.toolbar + display table-row + +.toolbar__tool + display table-cell + padding-left 1px + +.toolbar__button + display inline-block + vertical-align bottom + width 30px + height 30px + background #c4c2c0 + text-align center + line-height 30px + font-size 16px + @extend $plain-link + &:link, + &:visited, + &:hover, + &:active + color #fff + text-decoration none + &:hover + background darken(@background, 5%) + +.toolbar__button_run::before + @extend $font-run + +.toolbar__button_external::before + @extend $font-external + +.toolbar__button_download::before + @extend $font-download + +.toolbar__button_edit::before + @extend $font-edit + +.toolbar__button-text + display none diff --git a/app/stylesheets/blocks/top-part/top-part.png b/app/stylesheets/blocks/top-part/top-part.png new file mode 100644 index 000000000..b036c50fb Binary files /dev/null and b/app/stylesheets/blocks/top-part/top-part.png differ diff --git a/app/stylesheets/blocks/top-part/top-part.styl b/app/stylesheets/blocks/top-part/top-part.styl new file mode 100644 index 000000000..02d173d22 --- /dev/null +++ b/app/stylesheets/blocks/top-part/top-part.styl @@ -0,0 +1,13 @@ +.top-part + @extend $center + @extend $min-max-width + position relative + background url('/img/top-part.png') 50% 0 no-repeat + .user + position absolute + z-index 999 + bottom 36px + right 46px + + .logo + @extend $center diff --git a/app/stylesheets/blocks/variables/variables.styl b/app/stylesheets/blocks/variables/variables.styl new file mode 100644 index 000000000..fbfdf1348 --- /dev/null +++ b/app/stylesheets/blocks/variables/variables.styl @@ -0,0 +1,33 @@ +link_color = #0059B2; +alternate_link_color = #2974BB; // there are some a bit lighter links that need separate color +link_hover_color = #BA1000; +link_visited_color = #990099; + +background = #fff; +color = #333; +page_background = #D1E9EC; +separator_color = #DFDFD0; +secondary_color = #B20600; +code_color = #8C6900; +background_blocks = #F5F2F0; + +font = 'Open Sans', Verdana, Arial, Helvetica, sans-serif; +size = 14px; +lineheight = 18px; + +secondary_font = 'Open Sans Condensed', Arial, Helvetica, sans-serif; +fixed_width_font = 'Consolas', 'Monaco', 'Menlo', 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace; + +content_horizontal_padding = 40px; + +sidebar_width = 205px; +sidebar_padding_left = 23px; +sidebar_padding_right = 30px; + +min_width = 960px; +max_width = 1200px; + +media_step_1 = 1120px; +media_step_2 = 1170px; +media_step_3 = 1220px; +largescreen = 1420px; \ No newline at end of file diff --git a/app/stylesheets/main.styl b/app/stylesheets/main.styl deleted file mode 100755 index 6eec76731..000000000 --- a/app/stylesheets/main.styl +++ /dev/null @@ -1,4 +0,0 @@ -/* :-) */ -h1 - font-style italic - diff --git a/app/stylesheets/sprites/.gitkeep b/app/stylesheets/sprites/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/bin/www b/bin/www index 103dff3c5..f9616793f 100755 --- a/bin/www +++ b/bin/www @@ -1,10 +1,15 @@ #!/usr/bin/env node 'use strict'; -var app = require('app'); -var log = require('lib/log')(module); -var config = require('config'); +const log = require('js-log')(); +const config = require('config'); +const co = require('co'); +const app = require('app'); -app.listen(config.port, config.host, function() { - log.info('App listen %s:%d', config.host, config.port); -}); \ No newline at end of file +co(function*() { + + yield* app.run(); + +})(function(err) { + if (err) throw err; +}); diff --git a/brunch-config.coffee b/brunch-config.coffee deleted file mode 100755 index 34f086d1f..000000000 --- a/brunch-config.coffee +++ /dev/null @@ -1,32 +0,0 @@ -exports.config = -# See https://github.com/brunch/brunch/blob/master/docs/config.md for documentation. - paths: - public: 'www' - files: - javascripts: - joinTo: - 'javascripts/app.js': /^app/ - 'javascripts/vendor.js': /^(vendor|bower_components)/ - order: - before: [] - - stylesheets: - joinTo: - 'stylesheets/app.css': /^app/ - order: - before: [] - after: [] - - templates: - joinTo: 'javascripts/app.js' - - - plugins: - jade: - pretty: yes # Adds pretty-indentation whitespaces to output (false by default) - stylus: - linenos: true - includeCss: true - - conventions: - assets: /^assets/ \ No newline at end of file diff --git a/config/base.js b/config/base.js deleted file mode 100755 index 5e59fe976..000000000 --- a/config/base.js +++ /dev/null @@ -1,27 +0,0 @@ -module.exports = function() { - return { - "port": process.env.PORT || 3000, - "host": process.env.HOST || '0.0.0.0', - "mongoose": { - "uri": "mongodb://localhost/javascript", - "options": { - "server": { - "socketOptions": { - "keepAlive": 1 - }, - "poolSize": 5 - } - } - }, - "session": { - "keys": ["KillerIsJim"] - }, - template: { - path: process.cwd() + '/views', - options: { - 'default': 'jade', - 'cache': true - } - } - } -}; \ No newline at end of file diff --git a/config/env/development.js b/config/env/development.js deleted file mode 100755 index 188b92380..000000000 --- a/config/env/development.js +++ /dev/null @@ -1,5 +0,0 @@ -var _ = require('lodash'); - -module.exports = function(config) { - return config; -}; \ No newline at end of file diff --git a/config/env/production.js b/config/env/production.js deleted file mode 100755 index 188b92380..000000000 --- a/config/env/production.js +++ /dev/null @@ -1,5 +0,0 @@ -var _ = require('lodash'); - -module.exports = function(config) { - return config; -}; \ No newline at end of file diff --git a/config/env/test.js b/config/env/test.js deleted file mode 100755 index 10dfe4268..000000000 --- a/config/env/test.js +++ /dev/null @@ -1,9 +0,0 @@ -var _ = require('lodash'); - -module.exports = function(config) { - return _.merge(config, { - "mongoose": { - "uri": "mongodb://localhost/javascript_test" - } - }); -}; diff --git a/config/index.js b/config/index.js deleted file mode 100755 index defd7abe4..000000000 --- a/config/index.js +++ /dev/null @@ -1,10 +0,0 @@ - -if (!process.env.NODE_ENV) { - throw new Error("NODE_ENV environment variable is required"); -} - - -var base = require('./base')(); -var env = require('./env/' + process.env.NODE_ENV)(base); - -module.exports = env; diff --git a/db b/db new file mode 100755 index 000000000..b514d4487 --- /dev/null +++ b/db @@ -0,0 +1,3 @@ +#!/bin/bash +echo Loading the "default" site fixture. +./gulp loaddb --db fixture/db.js diff --git a/docs/payment.sketch/Data b/docs/payment.sketch/Data new file mode 100644 index 000000000..3ec90aed0 Binary files /dev/null and b/docs/payment.sketch/Data differ diff --git a/docs/payment.sketch/metadata b/docs/payment.sketch/metadata new file mode 100644 index 000000000..e0754f9f3 --- /dev/null +++ b/docs/payment.sketch/metadata @@ -0,0 +1,21 @@ + + + + + app + com.bohemiancoding.sketch3 + build + 7891 + commit + debc570766a4cc5a2e31258967910f7e5776f485 + fonts + + Helvetica + Helvetica-Bold + + length + 240489 + version + 37 + + diff --git a/docs/payment.sketch/version b/docs/payment.sketch/version new file mode 100644 index 000000000..7c091989d --- /dev/null +++ b/docs/payment.sketch/version @@ -0,0 +1 @@ +37 \ No newline at end of file diff --git a/error/httpError.js b/error/httpError.js deleted file mode 100755 index cd5a3dd78..000000000 --- a/error/httpError.js +++ /dev/null @@ -1,19 +0,0 @@ -var util = require('util'); -var http = require('http'); - -// ошибки для выдачи посетителю -function HttpError(status, message) { - Error.apply(this, arguments); - Error.captureStackTrace(this, HttpError); - - this.status = status; - this.message = message || http.STATUS_CODES[status] || "Error"; -} - -util.inherits(HttpError, Error); -module.exports = HttpError; - -HttpError.prototype.name = 'HttpError'; - - - diff --git a/error/index.js b/error/index.js deleted file mode 100755 index 3522cd29c..000000000 --- a/error/index.js +++ /dev/null @@ -1,9 +0,0 @@ -var fs = require('fs'); -var path = require('path'); - -var files = fs.readdirSync(__dirname); -files.forEach(function(file) { - if (file == 'index.js') return; - var errorClass = require(path.join(__dirname, file)); - module.exports[errorClass.prototype.name] = errorClass; -}); diff --git a/files/Readme.md b/files/Readme.md new file mode 100644 index 000000000..484c968f8 --- /dev/null +++ b/files/Readme.md @@ -0,0 +1,3 @@ +Restricted files, not directly accessible from outside. + +A user can download these using ExpiringDownloadLink or by other non-direct means. diff --git a/fixture/db.js b/fixture/db.js new file mode 100644 index 000000000..5d00fe295 --- /dev/null +++ b/fixture/db.js @@ -0,0 +1,31 @@ +const mongoose = require('mongoose'); + +var OrderTemplate = require('payments').OrderTemplate; +var User = require('auth/models/user'); + +exports.OrderTemplate = [ + { + title: "Основы JavaScript", + description: "500 стр, 10мб", + slug: "jsbasics", + amount: 1 + }, + { + title: "JS-DOM", + description: "400 стр, 8мб", + slug: "dom", + amount: 1 + }, + { + title: "Две книги сразу", + description: "500 стр, 8мб", + slug: "api", + amount: 1 + } +]; + +exports.User = [{ + email: "iliakan@gmail.com", + username: "Ilya Kantor", + password: "123456" +}]; diff --git a/gulp b/gulp new file mode 100755 index 000000000..cae819387 --- /dev/null +++ b/gulp @@ -0,0 +1,2 @@ +#!/bin/bash +NODE_ENV=development node --harmony `which gulp` $* diff --git a/gulp-debug b/gulp-debug new file mode 100644 index 000000000..51118e6c0 --- /dev/null +++ b/gulp-debug @@ -0,0 +1,2 @@ +#!/bin/bash +NODE_ENV=development node --debug-brk --harmony `which gulp` $* diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 000000000..e43b33a12 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,122 @@ +/** + * NB: All tasks are initialized lazily, even plugins are required lazily, + * running 1 task does not require all tasks' files + */ + +const gulp = require('gulp'); +const path = require('path'); +const fs = require('fs'); +const assert = require('assert'); + +const development = (process.env.NODE_ENV == 'development'); + +const serverSources = [ + 'hmvc/**/*.js', 'modules/**/*.js', 'tasks/**/*.js', '*.js' +]; + +function lazyRequireTask(name) { + var args = [].slice.call(arguments, 1); + + return function(callback) { + var task = require('./tasks/' + name).apply(this, args); + + return task(callback); + }; +} + +function wrapWatch(watch, task) { + return function(callback) { + if (process.env.NODE_ENV == 'development') { + gulp.watch(watch, [task]); + } else { + callback(); + } + }; +} + +gulp.task('lint-once', lazyRequireTask('lintOnce', { src: serverSources })); + +gulp.task('lint-or-die', lazyRequireTask('lintOnce', { src: serverSources, dieOnError: true })); + +gulp.task('lint', ['lint-once'], lazyRequireTask('lint', {src: serverSources})); + +// usage: gulp loaddb --db fixture/db +gulp.task('loaddb', lazyRequireTask('loadDb')); + +gulp.task("supervisor", ['link-modules'], lazyRequireTask('supervisor', { cmd: "./bin/www", watch: ["hmvc", "modules"] })); + +gulp.task("app:livereload", lazyRequireTask("livereload", { watch: "www/**/*.*" })); + +gulp.task('link-modules', lazyRequireTask('linkModules', { src: ['modules/*', 'hmvc/*'] })); + + +gulp.task("app:sync-resources", lazyRequireTask('syncResources', { + 'app/fonts' : 'www/fonts', + 'app/img': 'www/img' +})); + +gulp.task("app:sync-css-images-once", lazyRequireTask('syncCssImages', { + src: 'app/stylesheets/**/*.{png,svg,gif,jpg}', + dst: 'www/i' +})); + +gulp.task('app:sync-css-images', ['app:sync-css-images-once'], + wrapWatch('app/stylesheets/**/*.{png,svg,gif,jpg}', 'app:sync-css-images-once') +); + + +gulp.task('app:sprite-once', lazyRequireTask('sprite', { + spritesSearchFsRoot: 'app', + spritesWebRoot: '/i', + spritesFsDir: 'www/i', + styleFsDir: 'app/stylesheets/sprites' +})); + +gulp.task('app:sprite', ['app:sprite-once'], wrapWatch("app/**/*.sprite/**", 'sprite')); + +gulp.task('app:clean-compiled-css', function(callback) { + fs.unlink('./www/stylesheets/base.css', function(err) { + if (err && err.code == 'ENOENT') err = null; + callback(err); + }); +}); + +// Show errors if encountered +gulp.task('app:compile-css-once', + ['app:clean-compiled-css'], + lazyRequireTask('compileCss', { + src: './app/stylesheets/base.styl', + dst: './www/stylesheets' + }) +); + + + +gulp.task('app:compile-css', ['app:compile-css-once'], wrapWatch("app/**/*.styl", "app:compile-css-once")); + + +gulp.task("app:browserify:clean", lazyRequireTask('browserifyClean', { dst: './www/js'} )); + + +gulp.task("app:browserify", ['app:browserify:clean'], lazyRequireTask('browserify')); + + +// compile-css and sprites are independant tasks +// run both or run *-once separately +gulp.task('run', ['supervisor', 'app:livereload', "app:sync-resources", 'app:compile-css', 'app:sprite', 'app:browserify', 'app:sync-css-images']); + + +// TODO: refactor me out! +gulp.task('import', function(callback) { + const mongoose = require('config/mongoose'); + const taskImport = require('tutorial/tasks/import'); + + taskImport({ + root: path.join(path.dirname(__dirname), 'javascript-tutorial'), + updateFiles: true // skip same size files + //minify: true // takes time(!) + })(function() { + mongoose.disconnect(); + callback.apply(null, arguments); + }); +}); diff --git a/hmvc/activities/Readme.md b/hmvc/activities/Readme.md new file mode 100644 index 000000000..d1330f8ce --- /dev/null +++ b/hmvc/activities/Readme.md @@ -0,0 +1 @@ +Курсы. TODO. diff --git a/hmvc/auth/controller/form.js b/hmvc/auth/controller/form.js new file mode 100644 index 000000000..1f93c3baf --- /dev/null +++ b/hmvc/auth/controller/form.js @@ -0,0 +1,6 @@ + +exports.get = function *get (next) { + this.render(__dirname, 'form'); +}; + + diff --git a/hmvc/auth/controller/login/local.js b/hmvc/auth/controller/login/local.js new file mode 100644 index 000000000..afb018313 --- /dev/null +++ b/hmvc/auth/controller/login/local.js @@ -0,0 +1,18 @@ +var passport = require('koa-passport'); + +exports.post = function*(next) { + var ctx = this; + yield passport.authenticate('local', function*(err, user, info) { + // missing credentials ?!? +// console.log("HERE 2", err, user, info); + if (err) throw err; + if (user === false) { + ctx.status = 401; + ctx.body = { success: false }; + } else { + yield ctx.login(user); + ctx.body = { success: true }; + } + }).call(this, next); +}; + diff --git a/hmvc/auth/controller/logout.js b/hmvc/auth/controller/logout.js new file mode 100644 index 000000000..4032eb244 --- /dev/null +++ b/hmvc/auth/controller/logout.js @@ -0,0 +1,6 @@ + +exports.post = function*(next) { + this.logout(); + this.redirect('/'); +}; + diff --git a/hmvc/auth/controller/user.js b/hmvc/auth/controller/user.js new file mode 100644 index 000000000..8b8b2acdb --- /dev/null +++ b/hmvc/auth/controller/user.js @@ -0,0 +1,13 @@ + +exports.get = function *get (next) { + + this.body = { + username: this.req.user.username, + email: this.req.user.email, + created: this.req.user.created + }; + + +}; + + diff --git a/hmvc/auth/index.js b/hmvc/auth/index.js new file mode 100644 index 000000000..5a299106e --- /dev/null +++ b/hmvc/auth/index.js @@ -0,0 +1,5 @@ +const router = require('./router'); + +require('./lib/passport'); + +exports.middleware = router.middleware(); diff --git a/hmvc/auth/lib/hash.js b/hmvc/auth/lib/hash.js new file mode 100644 index 000000000..ab2803a03 --- /dev/null +++ b/hmvc/auth/lib/hash.js @@ -0,0 +1,11 @@ +const crypto = require('crypto'); +const config = require('config'); + +// warning, takes time, about ~70ms for length=128, iterations=12000 +exports.createHashSlow = function(password, salt) { + return crypto.pbkdf2Sync(password, salt, config.crypto.hash.iterations, config.crypto.hash.length); +}; + +exports.createSalt = function() { + return crypto.randomBytes(config.crypto.hash.length).toString('base64'); +}; diff --git a/hmvc/auth/lib/passport.js b/hmvc/auth/lib/passport.js new file mode 100644 index 000000000..02bf0b384 --- /dev/null +++ b/hmvc/auth/lib/passport.js @@ -0,0 +1,32 @@ +const passport = require('koa-passport'); +const LocalStrategy = require('passport-local').Strategy; +const User = require('../models/user'); + +// setup auth strategy +passport.serializeUser(function(user, done) { + done(null, user.id); +}); + +passport.deserializeUser(function(id, done) { + User.findById(id, function (err, user) { + done(err, user); + }); +}); + +passport.use(new LocalStrategy({ + usernameField: 'email', + passwordField: 'password' +}, function(email, password, done) { + + if (!email) return done(null, false, { message: 'Please provide email.' }); + if (!password) return done(null, false, { message: 'Please provide password.' }); + User.findOne({email: email}, function(err, user) { + console.log(email, password, err, user); + + if (err) return done(err); + if (!user) return done(null, false, { message: 'Non-registered email.' }); + return user.checkPassword(password) + ? done(null, user) + : done(null, false, { message: 'Incorrect password.' }); + }); +})); diff --git a/hmvc/auth/models/user.js b/hmvc/auth/models/user.js new file mode 100644 index 000000000..65c1d9a71 --- /dev/null +++ b/hmvc/auth/models/user.js @@ -0,0 +1,57 @@ +var mongoose = require('mongoose'); +var hash = require('../lib/hash'); + +var Schema = mongoose.Schema; + +var schema = new Schema({ + username: { + type: String, + required: true + }, + email: { + type: String, + unique: true, + required: true, + index: true + }, + salt: { + type: String, + required: true + }, + passwordHash: { + type: String, + required: true + }, + created: { + type: Date, + default: Date.now + }, + avatar: { + type: String + } +}); + +schema.virtual('password') + .set(function(password) { + this._plainPassword = password; + this.salt = hash.createSalt(); + this.passwordHash = hash.createHashSlow(password, this.salt); + }) + .get(function() { + return this._plainPassword; + }); + +schema.methods.checkPassword = function(password) { + return hash.createHashSlow(password, this.salt) == this.passwordHash; +}; + +schema.path('email').validate(function(value) { + // wrap in new RegExp instead of /.../, to evade WebStorm validation errors (buggy webstorm) + return new RegExp('^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,12}$').test(value); +}, 'Укажите, пожалуйста, корретный email.'); + +// all references using mongoose.model for safe recreation +// when I recreate model (for tests) => I can reload it from mongoose.model (single source of truth) +// exports are less convenient to update +module.exports = mongoose.model('User', schema); + diff --git a/hmvc/auth/router.js b/hmvc/auth/router.js new file mode 100644 index 000000000..dc44f0a68 --- /dev/null +++ b/hmvc/auth/router.js @@ -0,0 +1,14 @@ +var Router = require('koa-router'); +var form = require('./controller/form'); +var user = require('./controller/user'); +var local = require('./controller/login/local'); +var logout = require('./controller/logout'); + +var router = module.exports = new Router(); + +router.get('/form', form.get); +router.get('/user', user.get); + +router.post('/login/local', local.post); + +router.post('/logout', logout.post); diff --git a/hmvc/auth/templates/form.jade b/hmvc/auth/templates/form.jade new file mode 100644 index 000000000..a2d69fc73 --- /dev/null +++ b/hmvc/auth/templates/form.jade @@ -0,0 +1,20 @@ +form.login-form + h2 Вход в систему + + div + label Email + input(name="email" type="email") + + div + label Пароль + input(name="password" type="password") + + div + input(type="submit" value="Войти") + + +form.register-form(style="display:none") + h2 Регистрация + + p ... + diff --git a/hmvc/auth/test/.jshintrc b/hmvc/auth/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/hmvc/auth/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/hmvc/auth/test/auth.js b/hmvc/auth/test/auth.js new file mode 100644 index 000000000..66610fe41 --- /dev/null +++ b/hmvc/auth/test/auth.js @@ -0,0 +1,71 @@ +/* globals describe, it, before */ + +const db = require('lib/dataUtil'); +const mongoose = require('mongoose'); +const path = require('path'); +const request = require('supertest'); +const fixtures = require(path.join(__dirname, './fixtures/db')); +const app = require('app'); +const assert = require('better-assert'); + +describe('Authorization', function() { + + var agent; + before(function * () { + yield db.loadDb(path.join(__dirname, './fixtures/db')); + yield app.run(); + + agent = request.agent(app); + + }); + + describe('login', function() { + + it('should log me in', function(done) { + agent + .post('/auth/login/local') + .send({ + email: fixtures.User[0].email, + password: fixtures.User[0].password + }) + .expect(200) + .end(function(err, res) { +// console.log(res.headers); + // sessionId = res.headers['set-cookie'][0]; + done(err); + }); + }); + + it('should return current user info', function(done) { + agent + .get('/auth/user') + .expect(200) + .end(function(err, res) { + res.body.email.should.be.eql(fixtures.User[0].email); + done(err); + }); + }); + + }); + + 0 && describe('logout', function() { + it('should log me out', function(done) { + request(app) + .get('/api/logout') + .set('Cookie', sessionId) + .expect(200) + .end(function(err, res) { + done(err); + }); + }); + it('should return error because session is incorrected', function(done) { + request(app) + .get('/api/auth/user') + .set('Cookie', sessionId) + .expect(401) + .end(function(err, res) { + done(err); + }); + }); + }); +}); diff --git a/hmvc/auth/test/fixtures/db.js b/hmvc/auth/test/fixtures/db.js new file mode 100644 index 000000000..fe8fba922 --- /dev/null +++ b/hmvc/auth/test/fixtures/db.js @@ -0,0 +1,20 @@ +exports.User = [ + { "_id": "000000000000000000000001", + "created": new Date(2014,0,1), + "username": "ilya kantor", + "email": "iliakan@gmail.com", + "password": "123" + }, + { "_id": "000000000000000000000002", + "created": new Date(2014,0,1), + "username": "tester", + "email": "tester@mail.com", + "password": "123" + }, + { "_id": "000000000000000000000003", + "created": new Date(2014,0,1), + "username": "vasya", + "email": "vasya@mail.com", + "password": "123" + } +]; \ No newline at end of file diff --git a/hmvc/auth/test/unit/model/user.js b/hmvc/auth/test/unit/model/user.js new file mode 100644 index 000000000..4de1a3391 --- /dev/null +++ b/hmvc/auth/test/unit/model/user.js @@ -0,0 +1,82 @@ +var app = require('app'); +var mongoose = require('config/mongoose'); + +var dataUtil = require('lib/dataUtil'); + +describe('User', function() { + + var User = require('../../../models/user'); + + before(function* () { + yield dataUtil.createEmptyDb; + }); + + it('given bad email errors on save', function*() { + var user = new User({ + email: "BAD", + username: "John", + password: "123" + }); + + user.persist()(function(err) { + err.name.should.equal('ValidationError'); + err.errors.email.value.should.equal(user.get('email')); + }); + + }); + + it('requires password & email & username', function*() { + [ + { + email: "my@gmail.com", + username: "John" + }, + { + email: "my@gmail.com", + password: "John" + }, + { + username: "John", + password: "****" + } + ].map(function(data) { + var user = new User(data); + user.persist()(function(err) { + err.name.should.equal('ValidationError'); + }); + }); + + }); + + it('autogenerates salt and hash', function* () { + + var user = new User({ + email: "a@b.ru", + username: "John", + password: "pass" + }); + + user.get('salt').should.not.be.empty; + user.get('passwordHash').should.not.be.empty; + user.checkPassword("pass").should.be.true; + + }); + + it('requires unique email', function* () { + + var data = { + username: "nonunique", + email: "nonunique@b.ru", + password: "pass" + }; + + yield new User(data).persist(); + try { + yield new User(data).persist(); + throw new Error("Same email is saved twice!"); + } catch(err) { + err.code.should.equal(11000); // unique index is checked by mongo + } + + }); +}); diff --git a/controllers/frontpage.js b/hmvc/frontpage/controller/frontpage.js similarity index 67% rename from controllers/frontpage.js rename to hmvc/frontpage/controller/frontpage.js index 8782b0332..f6f0296b6 100644 --- a/controllers/frontpage.js +++ b/hmvc/frontpage/controller/frontpage.js @@ -1,6 +1,6 @@ exports.get = function *get (next) { - yield this.render('index', { + this.render(__dirname, 'index', { title: 'Hello, world' }); }; diff --git a/hmvc/frontpage/index.js b/hmvc/frontpage/index.js new file mode 100644 index 000000000..cdfa18f19 --- /dev/null +++ b/hmvc/frontpage/index.js @@ -0,0 +1,4 @@ + +var router = require('./router'); + +exports.middleware = router.middleware(); diff --git a/hmvc/frontpage/router.js b/hmvc/frontpage/router.js new file mode 100644 index 000000000..a5799db4d --- /dev/null +++ b/hmvc/frontpage/router.js @@ -0,0 +1,8 @@ +var Router = require('koa-router'); + +var frontpage = require('./controller/frontpage'); + +var router = module.exports = new Router(); + +router.get('/', frontpage.get); + diff --git a/views/index.jade b/hmvc/frontpage/templates/index.jade old mode 100755 new mode 100644 similarity index 70% rename from views/index.jade rename to hmvc/frontpage/templates/index.jade index 128334f63..382d24d71 --- a/views/index.jade +++ b/hmvc/frontpage/templates/index.jade @@ -1,9 +1,5 @@ -extends layout +extends layouts/base block content h1= title p Welcome to #{title} - - - - diff --git a/hmvc/getpdf/controller/checkout.js b/hmvc/getpdf/controller/checkout.js new file mode 100644 index 000000000..5d91e5223 --- /dev/null +++ b/hmvc/getpdf/controller/checkout.js @@ -0,0 +1,63 @@ +var mongoose = require('mongoose'); +var log = require('js-log')(); +var payments = require('payments'); +var Order = payments.Order; +var OrderTemplate = payments.OrderTemplate; +var methods = require('../paymentMethods').methods; + +log.debugOn(); + +exports.post = function*(next) { + + yield* this.loadOrder(); + var method = methods[this.request.body.paymentMethod]; + if (!method) { + this.throw(403, "Unsupported payment method"); + } + + if (this.order) { + log.debug("order exists", this.order.number); + yield* updateOrderFromBody(this.request.body, this.order); + } else { + // if we don't have the order in our database, then make a new one + // (use the incoming order post for that, but don't trust it) + + console.log(this.request.body.orderTemplate); + + var orderTemplate = yield OrderTemplate.findOne({ + slug: this.request.body.orderTemplate + }).exec(); + + if (!orderTemplate) { + this.throw(404); + } + + console.log("GOT TEMPLATE"); + // create order from template, don't trust the incoming post + this.order = Order.createFromTemplate(orderTemplate, { + module: 'getpdf', + email: this.request.body.email + }); + + yield* updateOrderFromBody(this.request.body, this.order); + + log.debug("order created", this.order.number); + + if (!this.session.orders) { + this.session.orders = []; + } + this.session.orders.push(this.order.number); + } + + var form = yield* payments.createTransactionForm(this.order, method.name); + + this.body = form; + +}; + +function* updateOrderFromBody(body, order) { + order.email = body.email; + order.markModified('data'); + + yield order.persist(); +} diff --git a/hmvc/getpdf/controller/orders.js b/hmvc/getpdf/controller/orders.js new file mode 100644 index 000000000..e545bd855 --- /dev/null +++ b/hmvc/getpdf/controller/orders.js @@ -0,0 +1,38 @@ +const payments = require('payments'); +var Order = payments.Order; +var OrderTemplate = payments.OrderTemplate; +var Transaction = payments.Transaction; + +exports.get = function*(next) { + this.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + + if (this.params.orderNumber) { + yield* this.loadOrder(); + } else { + + var orderTemplate = yield OrderTemplate.findOne({ + slug: this.params.orderTemplate + }).exec(); + + if (!orderTemplate) { + this.throw(404); + } + + this.locals.orderTemplate = this.params.orderTemplate; + + // this order is not saved anywhere, + // it's only used to initially fill the form + // order.isNew = true! + this.order = Order.createFromTemplate(orderTemplate, { + module: 'getpdf', + email: Math.round(Math.random()*1e6).toString(36) + '@gmail.com' + }); + + } + + this.locals.order = this.order; + + this.locals.paymentMethods = require('../paymentMethods').methods; + + this.render(__dirname, 'main'); +}; diff --git a/hmvc/getpdf/controller/payResult.js b/hmvc/getpdf/controller/payResult.js new file mode 100644 index 000000000..dc0b8d50a --- /dev/null +++ b/hmvc/getpdf/controller/payResult.js @@ -0,0 +1,79 @@ +const payments = require('payments'); +var Order = payments.Order; +var OrderTemplate = payments.OrderTemplate; +var Transaction = payments.Transaction; +const escapeHtml = require('escape-html'); + +/** + * 3 kinds of response + * 1) { status, successHtml } - if order success + * 2) { status, statusMessage(optional) } - if another status + * 3) "" - empty string if no information + * @param next + */ +exports.get = function*(next) { + this.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + + yield* this.loadOrder(); + + + if (this.order.status == Transaction.STATUS_SUCCESS) { + this.body = { + status: Transaction.STATUS_SUCCESS, + html: 'Спасибо за покупку! Вот ваши ништяки.' + }; + return; + } + + + var lastTransaction = yield Transaction.findOne({ order: this.order._id }).sort({created: -1}).exec(); + + // no payment at all?!? strange + if (!lastTransaction) { + this.body = { + status: Transaction.STATUS_FAIL, + html: 'Оплаты не было.' + }; + return; + } + + // the order is not yet successful, but the last transaction is not, + // that's possible if order.onSuccess hook has not yet finished + // let's wait a little bit + if (lastTransaction.status == Transaction.STATUS_SUCCESS) { + this.body = ''; + return; + } + + // transaction status unknown + // -> it means we're awaiting a response from the payment system + if (!lastTransaction.status) { + this.body = ''; + return; + } + + if (lastTransaction.status == Transaction.STATUS_FAIL) { + this.body = { + status: lastTransaction.status, + html: 'Оплата не прошла.' + }; + + if (lastTransaction.statusMessage) { + this.body.html += '
    ' + escapeHtml(lastTransaction.statusMessage) + '
    '; + } + return; + } + + + if (lastTransaction.status == Transaction.STATUS_PENDING) { + this.body = { + status: lastTransaction.status, + html: 'Оплата ожидается.' + }; + + if (lastTransaction.statusMessage) { + this.body.html += '
    ' + escapeHtml(lastTransaction.statusMessage) + '
    '; + } + } + +}; diff --git a/hmvc/getpdf/index.js b/hmvc/getpdf/index.js new file mode 100644 index 000000000..362d503c3 --- /dev/null +++ b/hmvc/getpdf/index.js @@ -0,0 +1,6 @@ + +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.onSuccess = require('./onSuccess'); diff --git a/hmvc/getpdf/onSuccess.js b/hmvc/getpdf/onSuccess.js new file mode 100644 index 000000000..3eb9a7db3 --- /dev/null +++ b/hmvc/getpdf/onSuccess.js @@ -0,0 +1,33 @@ +const Transaction = require('payments').Transaction; +const expiringDownload = require('expiring-download'); + +const ExpiringDownloadLink = expiringDownload.ExpiringDownloadLink; +const nodemailer = require('nodemailer'); +const ses = require('nodemailer-ses-transport'); + +module.exports = function* (order) { + + yield order.persist({ + status: Transaction.STATUS_SUCCESS + }); + + // CREATE DOWNLOAD LINK + + // EMAIL IT TO USER (move nodemailer to a separate site-wide "mail" module) + /* + var transporter = nodemailer.createTransport(ses({ + accessKeyId: 'AWSACCESSKEY', + secretAccessKey: 'AWS/Secret/key' + })); + transporter.sendMail({ + from: 'sender@address', + to: 'receiver@address', + subject: 'hello', + text: 'hello world!' + }); +*/ + + // ... + + console.log("Order success: " + order.number); +}; diff --git a/hmvc/getpdf/paymentMethods.js b/hmvc/getpdf/paymentMethods.js new file mode 100644 index 000000000..27067fc7b --- /dev/null +++ b/hmvc/getpdf/paymentMethods.js @@ -0,0 +1,6 @@ +exports.methods = { + 'yandexmoney': {name: "yandexmoney", title: "Яндекс.Деньги"}, + 'webmoney': {name: "webmoney", title: "Webmoney"}, + 'payanyway': {name: "payanyway", title: "PayAnyWay"}, + 'paypal': {name: "paypal", title: "Paypal"} +}; diff --git a/hmvc/getpdf/router.js b/hmvc/getpdf/router.js new file mode 100644 index 000000000..c956568c7 --- /dev/null +++ b/hmvc/getpdf/router.js @@ -0,0 +1,14 @@ +var Router = require('koa-router'); + +var router = module.exports = new Router(); + +var orders = require('./controller/orders'); +var payResult = require('./controller/payResult'); +var checkout = require('./controller/checkout'); + +router.get('/:orderTemplate', orders.get); +router.get('/orders/:orderNumber(\\d+)', orders.get); + +router.get('/pay-result/:orderNumber(\\d+)', payResult.get); + +router.post('/checkout', checkout.post); diff --git a/hmvc/getpdf/templates/main.jade b/hmvc/getpdf/templates/main.jade new file mode 100644 index 000000000..bc9e33ea0 --- /dev/null +++ b/hmvc/getpdf/templates/main.jade @@ -0,0 +1,225 @@ +p. + TODO: доделать форму оплаты, как частный случай более сложных курсов + TODO: при выборе способа оплаты "позже" не надо перезагружать страницу, нужно менять состояние формы на ней + + +style. + + /* выбор оплаты - только для новых заказов или для неудавшейся оплаты */ + .pay-chooser { + display: none; + } + form.order-form[data-new="1"] .pay-chooser { + display: block; + } + form.order-form[data-payment-state="fail"] .pay-chooser { + display: block; + } + + /* индикатор загрузки - только для существующих заказов без статуса оплаты */ + .pay-loading { + display: none; + } + + form.order-form[data-new="0"]:not([data-payment-state]) .pay-loading { + display: block; + } + + /* информация о проблеме с оплатой оплаты (над .pay-chooser) - только когда она есть */ + .pay-fail { + display: none; + } + + form.order-form[data-payment-state="fail"] .pay-fail { + display: block; + } + + /* результат заказа оплаченного */ + .order-result { + display: none; + } + form.order-form[data-payment-state="success"] .order-result, + form.order-form[data-payment-state="pending"] .order-result { + display: block; + } + +script var csrf = "#{csrf}"; +script var orderNumber = #{order.isNew ? order.number : 'null'}; +script var orderTemplate = "#{orderTemplate}"; + // TODO + + +div + + form.order-form(data-new=(order.isNew ? "1" : "0")) + + fieldset + legend Описание книги и стоимость + h2= order.title + div= order.description + div #{order.amount}р. + + fieldset + legend Укажите свой email (если не авторизован) + + input(name="email" value=order.email placeholder="E-mail" disabled=!order.isNew) + + fieldset + legend Оплата или результат оплаты + + .pay-loading Загружаем информацию об оплате... + + .pay-fail + + .pay-chooser + p Выберите способ оплаты: + select(name="paymentMethod") + each paymentMethod in paymentMethods + option(value=paymentMethod.name) #{paymentMethod.title} + input(type="submit" value="Оплатить") + + + .pay-result + legend Результат заказа при успешной оплате + + .content + + +script(src="http://code.jquery.com/jquery-2.1.1.js") + +script. + var orderForm = $('.order-form'); + var payResult = $('.pay-result'); + + if (orderForm.data('new') == 0) { + var requestPayResultStart = new Date(); + requestPayResult(); + } + + orderForm.on('submit', onPayFormSubmit); + + function requestPayResult() { + if (new Date() - requestPayResultStart > 120e3) { // 2 mins + orderForm.attr('data-payment-state', 'timeout'); + payResult.html("Таймаут ответа от платёжной системы. Попробуйте обновить эту страницу позже или обратиться в поддержку."); + return; + } + + $.ajax({ + method: 'GET', + url: '/getpdf/pay-result/' + orderForm[0].elements.orderNumber.value, + data: { + orderNumber: orderForm[0].elements.orderNumber.value + } + }) + .done(function(result) { + if (!result) { + setTimeout(requestPayResult, 1000); + return; + } + showPayResult(result); + }) + .fail(function(err) { + setTimeout(requestPayResult, 1000); + }); + } + + function showPayResult(result) { + orderForm.attr('data-payment-state', result.status); + switch(result.status) { + case 'success': + orderForm.find('.order-success').html(result.successHtml); + break; + case 'pending': + payResult.html('Оплата ожидается, о результате мы напишем по email'); + break; + case 'fail': + $('.pay-fail').html(result.html); + break; + default: + throw new Error("Unknown payment status: " + result.status); + } + } + + function onPayFormSubmit(e) { + e.preventDefault(); + $.ajax({ + method: 'POST', + url: '/getpdf/checkout', + data: { + _csrf: csrf, + orderNumber: this.elements.orderNumber.value, + orderTemplate: this.elements.orderTemplate.value, + email: this.elements.email.value, + paymentMethod: this.elements.paymentMethod.value + } + }) + .fail(function(err) { + alert("Ошибка на сервере"); + }) + .done(function(htmlForm) { + $(htmlForm).submit(); + }); + } + "mailto:help@javascript.ru\">поддержку."); + return; + } + + $.ajax({ + method: 'GET', + url: '/getpdf/pay-result/' + payForm[0].elements.orderNumber.value, + data: { + orderNumber: payForm[0].elements.orderNumber.value + } + }) + .done(function(result) { + if (!result) { + setTimeout(requestPayResult, 1000); + return; + } + showPayResult(result); + }) + .fail(function(err) { + setTimeout(requestPayResult, 1000); + }); + } + + function showPayResult(result) { + payForm.attr('data-payment-state', result.status); + switch(result.status) { + case 'success': + payForm.find('.order-success').html(result.successHtml); + break; + case 'pending': + payResult.html('Оплата ожидается, о результате мы напишем по email'); + break; + case 'fail': + var message = 'Оплата не прошла'; + if (result.statusMessage) message += '
    Ошибка: ' + result.statusMessage + '
    '; + payResult.html(message); + break; + default: + throw new Error("Unknown payment status: " + result.status); + } + } + + function onPayFormSubmit(e) { + e.preventDefault(); + $.ajax({ + method: 'POST', + url: '/getpdf/checkout', + data: { + _csrf: csrf, + orderNumber: this.elements.orderNumber.value, + orderTemplate: this.elements.orderTemplate.value, + email: this.elements.email.value, + paymentMethod: this.elements.paymentMethod.value + } + }) + .fail(function(err) { + alert("Ошибка на сервере"); + }) + .done(function(htmlForm) { + $(htmlForm).submit(); + }); + } diff --git a/hmvc/markup/controller/markup.js b/hmvc/markup/controller/markup.js new file mode 100644 index 000000000..b63a5c833 --- /dev/null +++ b/hmvc/markup/controller/markup.js @@ -0,0 +1,8 @@ +var join = require('path').join; + +exports.get = function *get(next) { + + var path = this.params[0]; + this.render(__dirname, path); +}; + diff --git a/hmvc/markup/index.js b/hmvc/markup/index.js new file mode 100644 index 000000000..cdfa18f19 --- /dev/null +++ b/hmvc/markup/index.js @@ -0,0 +1,4 @@ + +var router = require('./router'); + +exports.middleware = router.middleware(); diff --git a/hmvc/markup/router.js b/hmvc/markup/router.js new file mode 100644 index 000000000..98798a4d0 --- /dev/null +++ b/hmvc/markup/router.js @@ -0,0 +1,8 @@ +var Router = require('koa-router'); + +var markup = require('./controller/markup'); + +var router = module.exports = new Router(); + +router.get(/^\/([^.]*)$/, markup.get); + diff --git a/hmvc/markup/templates/blocks/article-foot.jade b/hmvc/markup/templates/blocks/article-foot.jade new file mode 100644 index 000000000..6726fec7c --- /dev/null +++ b/hmvc/markup/templates/blocks/article-foot.jade @@ -0,0 +1,7 @@ +footer.main__footer + time.main__footer-date(datetime="2010-12-03") 12.03.2010 + span.main__footer-author + a(href="http://ikantor.moikrug.ru") Илья Кантор + span.main__footer-star + + include ../blocks/social-inline \ No newline at end of file diff --git a/hmvc/markup/templates/blocks/breadcrumbs.jade b/hmvc/markup/templates/blocks/breadcrumbs.jade new file mode 100644 index 000000000..a94baeaf3 --- /dev/null +++ b/hmvc/markup/templates/blocks/breadcrumbs.jade @@ -0,0 +1,9 @@ +ol.breadcrumbs + li.breadcrumbs__item + a.breadcrumbs__link(href='/') Главная + li.breadcrumbs__item + a.breadcrumbs__link(href='/tutorial') Учебник + li.breadcrumbs__item + a.breadcrumbs__link(href='/getting-started') Общая информация + li.breadcrumbs__item + | Введение в JavaScript \ No newline at end of file diff --git a/hmvc/markup/templates/blocks/comments.jade b/hmvc/markup/templates/blocks/comments.jade new file mode 100644 index 000000000..613799d41 --- /dev/null +++ b/hmvc/markup/templates/blocks/comments.jade @@ -0,0 +1,30 @@ +.comments#comments + .comments__header + h2.comments__header-title + | Комментарии + span.comments__header-number 5 + a.comments__header-write(href="#write-comment") Написать + ul + li Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них. + li Если ваш комментарий касается задачи — откройте её в отдельном окне и напишите там. + li + | Для кода внутри строки используйте тег + <code> + | , для блока кода — тег + <pre> + | , если больше 10 строк — ссылку на + песочницу + | . + li Если что-то непонятно — пишите, что именно и с какого места. +//-
    +//-
    +//-

    Комментарии 5

    +//- Написать +//-
      +//-
    • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
    • +//-
    • Если ваш комментарий касается задачи — откройте её в отдельном окне и напишите там.
    • +//-
    • Для кода внутри строки используйте тег <code>, для блока кода — тег <pre>, если больше 10 строк — ссылку на песочницу.
    • +//-
    • Если что-то непонятно — пишите, что именно и с какого места.
    • +//-
    +//-
    +//-
    \ No newline at end of file diff --git a/hmvc/markup/templates/blocks/corrector.jade b/hmvc/markup/templates/blocks/corrector.jade new file mode 100644 index 000000000..d97edf608 --- /dev/null +++ b/hmvc/markup/templates/blocks/corrector.jade @@ -0,0 +1,2 @@ +.corrector + Нашли опечатку на сайте? Что-то кажется странным? Выделите соответствующий текст и нажмите Ctrl+Enter \ No newline at end of file diff --git a/hmvc/markup/templates/blocks/head.jade b/hmvc/markup/templates/blocks/head.jade new file mode 100644 index 000000000..49afee86d --- /dev/null +++ b/hmvc/markup/templates/blocks/head.jade @@ -0,0 +1,8 @@ +doctype html +html + meta(charset='UTF-8') + title= self.title + link(href='http://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700|Open+Sans+Condensed:700&subset=latin,latin-ext,cyrillic,cyrillic-ext' rel='stylesheet' type='text/css') + link(href='/stylesheets/base.css' rel='stylesheet') + //if lte IE 9 + //- link(href='../app/assets/stylesheets/base.ie.css' rel='stylesheet') \ No newline at end of file diff --git a/hmvc/markup/templates/blocks/lesson.jade b/hmvc/markup/templates/blocks/lesson.jade new file mode 100644 index 000000000..e69de29bb diff --git a/hmvc/markup/templates/blocks/lessons.jade b/hmvc/markup/templates/blocks/lessons.jade new file mode 100644 index 000000000..cddee8ad3 --- /dev/null +++ b/hmvc/markup/templates/blocks/lessons.jade @@ -0,0 +1,8 @@ +- var lessons = []; +- lessons.push({title: 'Переменные', }) +- lessons.push({title: 'Переменные'}) + +.lessons + for [1..10] + .lessons__lesson-wrap + include lesson diff --git a/hmvc/markup/templates/blocks/navbar.jade b/hmvc/markup/templates/blocks/navbar.jade new file mode 100644 index 000000000..ff055bd0a --- /dev/null +++ b/hmvc/markup/templates/blocks/navbar.jade @@ -0,0 +1,64 @@ +nav.navbar + ul.navbar__sections + li.navbar__sections-item.dropdown.dropdown_nav.down-right.inherit-min-width.dropdown_open_hover + a.navbar__sections-link.dropdown__toggle.dropdown__toggle_nav(href='/study') + span.navbar__icon.navbar__icon_study + Обучение + ul.dropdown__content.dropdown__content_nav + li + a(href='/courses') Курсы + li + a(href='/mk') Мастерклассы + li + a(href='/articles') Статьи + li.navbar__sections-item + a.navbar__sections-link(href='/blogs') + span.navbar__icon.navbar__icon_blogs + Блоги + li.navbar__sections-item + a.navbar__sections-link(href='/qa') + span.navbar__icon.navbar__icon_qa + Вопрос / ответ + li.navbar__sections-item.navbar__sections-item_active.dropdown.dropdown_nav.down-right.inherit-min-width.dropdown_open_hover + a.navbar__sections-link.dropdown__toggle.dropdown__toggle_nav(href='http://javascript.ru/manual') + span.navbar__icon.navbar__icon_reference + Справочник + ul.dropdown__content.dropdown__content_nav + li + a(href='http://es5.javascript.ru/') Стандарт ES5 + li + a(href='/referencd') Справочник JS + li + a(href='/php') PHP-функции + li + a(href='/book') Книги + li.navbar__sections-item + a.navbar__sections-link(href='/event') + span.navbar__icon.navbar__icon_events + События + li.navbar__sections-item + a.navbar__sections-link(href='/job') + span.navbar__icon.navbar__icon_job + Работа + li.navbar__sections-item + a.navbar__sections-link(href='/play') + span.navbar__icon.navbar__icon_sandbox + Песочница + li.navbar__sections-item + a.navbar__sections-link(href='/chat') + span.navbar__icon.navbar__icon_chat + Чат + li.navbar__sections-item.navbar__sections-item_search.dropdown.down-left + button.dropdown__toggle + span.navbar__icon.navbar__icon_search + .dropdown__content.dropdown-search + .search-form + form#search-block-form(action='/intro', method='post', accept-charset='UTF-8') + // TODO: проверить, точно ли нужно? Не ошибка? + div + .search-form__query + input#edit-search-block-form-1.form-text(type='text', maxlength='128', name='search_block_form', value='') + button(type='submit') Найти + input#w4vSP09jvn-orNjyHpBepTXLSn6qFxyehtYKuZbWWv4(type='hidden', name='form_build_id', value='w4vSP09jvn-orNjyHpBepTXLSn6qFxyehtYKuZbWWv4') + input#edit-search-block-form-form-token(type='hidden', name='form_token', value='O9WnzduxDr7Fwfy6eG4XL06_OilwEwBIsJDKhduRG0w') + input#edit-search-block-form(type='hidden', name='form_id', value='search_block_form') diff --git a/hmvc/markup/templates/blocks/page-footer.jade b/hmvc/markup/templates/blocks/page-footer.jade new file mode 100644 index 000000000..ca09bbfa1 --- /dev/null +++ b/hmvc/markup/templates/blocks/page-footer.jade @@ -0,0 +1,35 @@ +.page-footer + .page-footer__contents + nav.page-footer__col + h3.page-footer__title Обучение + ul.page-footer__list.small + li.page-footer__list-item: a(href="#a1") Учебник + li.page-footer__list-item: a(href="#a2") Тесты знаний + li.page-footer__list-item: a(href="#a3") Курсы Javascript + nav.page-footer__col + h3.page-footer__title Справочник + ul.page-footer__list.small + li.page-footer__list-item: a(href="#es5") Стандарт ES5 + li.page-footer__list-item: a(href="#ref") Справочник + li.page-footer__list-item: a(href="#php") PHP-функции + li.page-footer__list-item: a(href="#books") Книги + nav.page-footer__col + ul.page-footer__list + li.page-footer__list-item: a(href="#blogs") Блоги + li.page-footer__list-item: a(href="#qa") Вопрос/Ответ + li.page-footer__list-item: a(href="#events") События + li.page-footer__list-item: a(href="#job") Работа + nav.page-footer__col + ul.page-footer__list + li.page-footer__list-item: a(href="#chat") Чат + li.page-footer__list-item: a(href="#sandbox") Песочница + address.page-footer__copy + //- spaces between span tags are inserted explicitly + span.page-footer__line.author + ©2007—2012 Илья Кантор + | + span.page-footer__line: a(href="/feedback/") Обратная связь + | + span.page-footer__line + a(href="/about/") О проекте + | diff --git a/hmvc/markup/templates/blocks/prev-next-bottom.jade b/hmvc/markup/templates/blocks/prev-next-bottom.jade new file mode 100644 index 000000000..bcdcf07f3 --- /dev/null +++ b/hmvc/markup/templates/blocks/prev-next-bottom.jade @@ -0,0 +1,17 @@ +.prev-next.prev-next_bottom + .prev-next__prev + .prev-next__shortcut-wrap + | Предыдущий урок + kbd.prev-next__shortcut + | (Ctrl +  + span.prev-next__arr ← + | ) + a.prev-next__link(href="#prev") Введение в JavaScript + .prev-next__next + .prev-next__shortcut-wrap + | Следующий урок + kbd.prev-next__shortcut + | (Ctrl +  + span.prev-next__arr → + | ) + a.prev-next__link(href="#next") Книги по JS, HTML/CSS и не только diff --git a/hmvc/markup/templates/blocks/prev-next-top.jade b/hmvc/markup/templates/blocks/prev-next-top.jade new file mode 100644 index 000000000..034e8147c --- /dev/null +++ b/hmvc/markup/templates/blocks/prev-next-top.jade @@ -0,0 +1,13 @@ +.prev-next.prev-next_top.main__header-nav + .prev-next__prev + kbd.prev-next__shortcut + | (Ctrl+ + span.prev-next__arr ← + | ) + a.prev-next__link(href="#prev") Предыдущий урок + .prev-next__next + a.prev-next__link(href="#next") Следующий урок + kbd.prev-next__shortcut + | (Ctrl+ + span.prev-next__arr → + | ) diff --git a/hmvc/markup/templates/blocks/scripts.jade b/hmvc/markup/templates/blocks/scripts.jade new file mode 100644 index 000000000..88c28b05f --- /dev/null +++ b/hmvc/markup/templates/blocks/scripts.jade @@ -0,0 +1,26 @@ +//- TODO: Почему бы не использовать абсолютные пути? +script(src='http://code.jquery.com/jquery-1.10.1.min.js') +script(src='http://code.jquery.com/jquery-migrate-1.2.1.min.js') +script(src='/js/prism-core.js') +script(src='/js/prism-markup.js') +script(src='/js/prism-css.js') +script(src='/js/prism-css-extras.js') +script(src='/js/prism-clike.js') +script(src='/js/prism-javascript.js') +script(src='/js/prism-coffeescript.js') +script(src='/js/prism-http.js') +script(src='/js/prism-scss.js') +script(src='/js/prism-sql.js') +script(src='/js/prism-php.js') +script(src='/js/prism-php-extras.js') +script(src='/js/prism-python.js') +script(src='/js/prism-ruby.js') +script(src='/js/prism-java.js') +script(src='/js/codebox.js') +script(src='/js/iframebox.js') +script(src='/js/prism-my.js') +script(src='/js/select2.js') +script(src='/js/select2_locale_ru.js') +script(src='/js/base.js') +script(src='/js/iframe-resize.js') +script(src='/js/jquery.modal.js') diff --git a/hmvc/markup/templates/blocks/sidebar.jade b/hmvc/markup/templates/blocks/sidebar.jade new file mode 100644 index 000000000..bf512d4a7 --- /dev/null +++ b/hmvc/markup/templates/blocks/sidebar.jade @@ -0,0 +1,2 @@ +.sidebar + | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Molestiae at consequuntur consectetur ad ipsam itaque enim hic, labore eveniet quaerat praesentium fuga amet sequi explicabo excepturi ut distinctio dolores officiis. \ No newline at end of file diff --git a/hmvc/markup/templates/blocks/social-aside.jade b/hmvc/markup/templates/blocks/social-aside.jade new file mode 100644 index 000000000..e6bd981b9 --- /dev/null +++ b/hmvc/markup/templates/blocks/social-aside.jade @@ -0,0 +1,10 @@ +.social.aside + a.social__anchor-up(href='#page') Наверх + a.social__soc(href='https://plus.google.com/share?url=http://design.javascript.ru/intro', target='_blank') + img.soc-icon.google(src='/img/x.gif', width='24', height='24', alt='g+', title='Поделиться в гугл+') + a.social__soc(href='http://www.facebook.com/sharer/sharer.php?s=100&p[url]=http://design.javascript.ru/intro', target='_blank') + img.soc-icon.facebook(src='/img/x.gif', width='24', height='24', alt='f', title='Поделиться в фейсбуке') + a.social__soc(href='https://twitter.com/share?url=http://design.javascript.ru/intro', target='_blank') + img.soc-icon.twitter(src='/img/x.gif', width='24', height='24', alt='t', title='Поделиться в твитере') + a.social__soc(href='http://vkontakte.ru/share.php?url=http://design.javascript.ru/intro', target='_blank') + img.soc-icon.vk(src='/img/x.gif', width='24', height='24', alt='v', title='Поделиться во вконтакте') \ No newline at end of file diff --git a/hmvc/markup/templates/blocks/social-inline.jade b/hmvc/markup/templates/blocks/social-inline.jade new file mode 100644 index 000000000..8b44e7656 --- /dev/null +++ b/hmvc/markup/templates/blocks/social-inline.jade @@ -0,0 +1,2 @@ +.social.inline + include ../blocks/social \ No newline at end of file diff --git a/hmvc/markup/templates/blocks/social.jade b/hmvc/markup/templates/blocks/social.jade new file mode 100644 index 000000000..3b340f205 --- /dev/null +++ b/hmvc/markup/templates/blocks/social.jade @@ -0,0 +1,8 @@ +a.social__soc(target="_blank", href="https://plus.google.com/share?url=http://design.javascript.ru/intro") + img.soc-icon.google(src="/img/x.gif", width="24", height="24", title="Поделиться в гугл+", alt="g+") +a.social__soc(target="_blank", href="http://www.facebook.com/sharer/sharer.php?s=100&p[url]=http://design.javascript.ru/intro") + img.soc-icon.facebook(src="/img/x.gif", width="24", height="24", title="Поделиться в фейсбуке", alt="fb") +a.social__soc(target="_blank" href="https://twitter.com/share?url=http://design.javascript.ru/intro") + img.soc-icon.twitter(src="/img/x.gif", width="24", height="24", title="Поделиться в твитере", alt="tw") +a.social__soc(target="_blank", href="http://vkontakte.ru/share.php?url=http://design.javascript.ru/intro") + img.soc-icon.vk(src="/img/x.gif", width="24", height="24", title="Поделиться во вконтакте", alt="vk") \ No newline at end of file diff --git a/hmvc/markup/templates/blocks/task-block.jade b/hmvc/markup/templates/blocks/task-block.jade new file mode 100644 index 000000000..7903b43ab --- /dev/null +++ b/hmvc/markup/templates/blocks/task-block.jade @@ -0,0 +1,25 @@ +-var url = "/task/vyzov-na-meste" +-var title = "Вызов «на месте»" +-var importance = 4 +-var content = "

    Content

    Content

    " +-var solution = [{title:"",content:"

    Текст

    "}] + +section.important.important_ok + header.important__header + span.important__type Задание: + h3.important__title + a.important__task-link(href=url) + u!=title + span.important__importance(title="Насколько эта задача важна для освоения материала, от 1 до 5") + | Важность: + = ' ' + importance + + .important__content + != content + each part in solution + .spoiler.closed + button.spoiler__button + u!= part.title || "Решение" + .spoiler__content + u!= part.content + diff --git a/hmvc/markup/templates/blocks/top-parts.jade b/hmvc/markup/templates/blocks/top-parts.jade new file mode 100644 index 000000000..299c88205 --- /dev/null +++ b/hmvc/markup/templates/blocks/top-parts.jade @@ -0,0 +1,6 @@ +.top-part + .logo + a(href='http://javascript.ru/') + img(src='/img/logo.png', alt='Javascript.ru') + .user.dropdown.down-left.inherit-min-width + a.user__entrance(href='/user') Вход на сайт diff --git a/hmvc/markup/templates/example/user.jade b/hmvc/markup/templates/example/user.jade new file mode 100644 index 000000000..3970dae38 --- /dev/null +++ b/hmvc/markup/templates/example/user.jade @@ -0,0 +1,11 @@ +//- Статическое объявление переменных шаблона +//- с описаниями, что есть что и зачем :) +-var name = "Vasya" + +doctype html + +html + head + title my jade template + body + h1 Hello #{name} diff --git a/hmvc/markup/templates/layouts/base.jade b/hmvc/markup/templates/layouts/base.jade new file mode 100644 index 000000000..846013168 --- /dev/null +++ b/hmvc/markup/templates/layouts/base.jade @@ -0,0 +1,24 @@ +block variables +include ../blocks/head +body(id='#{self.bodyId}') + include ../blocks/top-parts + .page + .page__inner + include ../blocks/navbar + include ../blocks/social-aside + .main-content + article.main + header.main__header + include ../blocks/breadcrumbs + h1.main__header-title= self.title + span.main__header-comments + a(href='http://learn.javascript.ru/intro#disqus_thread') Комментариев #{self.comments.length} + include ../blocks/prev-next-top + block content + include ../blocks/article-foot + include ../blocks/prev-next-bottom + include ../blocks/corrector + include ../blocks/comments + include ../blocks/sidebar + include ../blocks/page-footer + include ../blocks/scripts \ No newline at end of file diff --git a/hmvc/markup/templates/pages/article.html b/hmvc/markup/templates/pages/article.html new file mode 100644 index 000000000..17c245627 --- /dev/null +++ b/hmvc/markup/templates/pages/article.html @@ -0,0 +1,476 @@ +

    Давайте посмотрим, что такого особенного в JavaScript, почему именно он, и какие еще технологии существуют, кроме JavaScript.

    + +

    Что такое JavaScript?

    + +

    JavaScript изначально создавался для того, чтобы сделать web-странички «живыми».
    +Программы на этом языке называются скриптами. Они подключаются напрямую к HTML и, как только загружается страничка — тут же выполняются.

    +

    ECMAScript +намылить новая вкладка песочница песочница plnkr.co документ архив таблица pdf-документ справка в MDN справка microsoft документация w3c спецификация ECMA

    +

    ECMAScript +намылить новая вкладка песочница песочница plnkr.co документ архив таблица pdf-документ справка в MDN справка microsoft документация w3c спецификация ECMA

    +

    ECMAScript +намылить новая вкладка песочница песочница plnkr.co документ архив таблица pdf-документ справка в MDN справка microsoft документация w3c спецификация ECMA

    +

    Программы на JavaScript — обычный текст. Они не требуют какой-то специальной подготовки.

    +

    А здесь мы приведем пример сочетания клавиш Cmd + R прямо в тексте урока.

    +

    Примеры кода и результата, которые должны создаваться динамически:

    + +
    <div class="code-example">
    +    <div class="codebox code-example__codebox">
    +        <div class="toolbar codebox__toolbar">
    +            <div class="toolbar__tool">
    +                <a href="/play/abcdef" class="toolbar__button toolbar__button_run" title="выполнить">
    +                    <span class="toolbar__button-text">выполнить</span>
    +                </a>
    +            </div>
    +            <div class="toolbar__tool">
    +                <a href="/files/file.doc" class="toolbar__button toolbar__button_edit" title="редактировать">
    +                    <span class="toolbar__button-text">редактировать</span>
    +                </a>
    +            </div>
    +        </div>
    +<pre class="language-javascript line-numbers">function sayHi(name) {
    +  var phrase = "Привет, " + name;
    +  alert(phrase);
    +}
    +
    +sayHi('Вася');</pre>
    +    </div>
    +
    +    <div class="result code-example__result">
    +        <div class="toolbar result__toolbar">
    +            <div class="toolbar__tool">
    +                <a href="/files/file.doc" class="toolbar__button toolbar__button_edit" title="редактировать">
    +                    <span class="toolbar__button-text">редактировать</span>
    +                </a>
    +            </div>
    +        </div>
    +        <iframe class="result__iframe" src="http://sass-lang.com/documentation/Sass/Script/Functions"></iframe>
    +    </div>
    +</div>
    + +

    Пример кода

    + +
    function sayHi(name) {
    +  var phrase = "Привет, " + name;
    +  alert(phrase);
    +}
    +
    +function HelloWorld(world) {
    +    alert('Hello, ' + world);
    +}
    +
    +HelloWorld('World');
    +
    +sayHi('Вася');
    + +

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

    +
    <div class="code-example">
    +    <div class="result code-example__result">
    +        <div class="toolbar result__toolbar">
    +            <div class="toolbar__tool">
    +                <a href="/files/file.doc" class="toolbar__button toolbar__button_edit" title="редактировать">
    +                    <span class="toolbar__button-text">редактировать</span>
    +                </a>
    +            </div>
    +        </div>
    +        <iframe class="result__iframe" src="http://sass-lang.com/documentation/Sass/Script/Functions"></iframe>
    +    </div>
    +</div>
    + +

    В этом плане JavaScript сильно отличается от другого языка, который называется Java.

    + +

    Выделенный блок с информацией.

    + +
    +
    + +

    Почему JavaScript?

    +
    +
    +

    Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным.

    +

    Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения.

    +

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

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    XMLHttpRequestIFRAMESCRIPTEventSourceWebSocket
    Кросс-доменностьда, кроме IE<10x1да, сложности в IE<8i1дадада
    МетодыЛюбыеGET / POSTGETGETСвой протокол
    COMETДлинные опросыx2Непрерывное соединениеДлинные опросыНепрерывное соединениеНепрерывное соединение в обе стороны
    ПоддержкаВсе браузеры, ограничения в IE<10x3Все браузерыВсе браузерыКроме IEIE 10, FF11, Chrome 16, Safari 6, Opera 12.5w1
    + +

    Выделенный блок c предупреждением

    + +
    +
    + Важно: +

    Почему JavaScript?

    +
    +
    +

    Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным.

    +

    Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения.

    +

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

    +
    +
    + +

    Выделенный блок c вопросом

    + +
    +
    + Вопрос: +

    Почему JavaScript?

    +
    +
    +

    Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным.

    +

    Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения.

    +

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

    +
    +
    + +

    Выделенный блок c ответом

    + +
    +
    + Задание + + Важность: 4 +
    +
    +

    Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным.

    +

    Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения.

    +

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

    +
    +
    + +

    Чтобы читать и выполнять текст на JavaScript, нужна специальная программа — интерпретатор. Процесс выполнения скрипта называют «интерпретацией».

    + +
    +
    + На заметку: +

    Компиляция и интерпретация, для программистов

    +
    +
    +

    Строго говоря, для выполнения программ существуют «компиляторы» и «интерпретаторы».

    +

    Когда-то между ними была большая разница. Компиляторы преобразовывали программу в машинный код, который потом можно выполнять. А интерпретаторы — просто выполняли.

    +

    Сейчас разница гораздо меньше. Современные интерпретаторы перед выполнением преобразуют JavaScript в машинный код (или близко к нему), чтобы выполнялся он быстрее. То есть, по сути, компилируют, а затем запускают.

    +
    +
    + +

    Во все основные браузеры встроен интерпретатор JavaScript, именно поэтому они могут выполнять скрипты на странице.

    + +

    Но, разумеется, этим возможности JavaScript не ограничены. Это полноценный язык, программы на котором можно запускать и на сервере, и даже в стиральной машинке, если в ней установлен соответствующий интерпретатор.

    + +

    Что умеет JavaScript?

    + +

    Современный JavaScript — это «безопасный» язык программирования общего назначения. Он не предоставляет низкоуровневых средств работы с памятью, процессором, так как изначально был ориентирован на браузеры, в которых это не требуется.

    +

    В браузере JavaScript умеет делать все, что относится к манипуляции со страницей, взаимодействию с посетителем и, в какой-то мере, с сервером:

    +
      +
    • Создавать новые HTML-теги, удалять существующие, менять стили элементов, прятать, показывать элементы и т.п.
    • +
    • Реагировать на действия посетителя, обрабатывать клики мыши, перемещение курсора, нажатие на клавиатуру и т.п.
    • +
    • Посылать запросы на сервер и загружать данные без перезагрузки страницы(эта технология называется "AJAX").
    • +
    • Получать и устанавливать cookie, запрашивать данные, выводить сообщения…
    • +
    • …и многое, многое другое!
    • +
    + +

    Что НЕ умеет JavaScript?

    +

    JavaScript — быстрый и мощный язык, но браузер накладывает на его исполнение некоторые ограничения.

    +

    Это сделано для безопасности пользователей, чтобы злоумышленник не мог с помощью JavaScript получить личные данные или как-то навредить компьютеру пользователя. В браузере Firefox существует способ «подписи» скриптов с целью обхода части ограничений, но он нестандартный и не кросс-браузерный.

    + +
    +
    + Задание: +

    Вызов «на месте»

    + Важность: 4 +
    +
    +

    Каков будет результат выполнения кода? Почему?

    +
    +
    var a = 5
    +
    +(function() {
    +alert(a)
    +})()
    +
    + +

    P.S. Подумайте хорошо! Здесь все ошибаются!
    + P.P.S. Внимание, здесь подводный камень! Ок, вы предупреждены.

    + +
    + +
    +

    Результат - ошибка. Попробуйте:

    +
    +
    var a = 5
    +
    +(function() {
    +alert(a)
    +})()
    +
    +

    Дело в том, что после var a = 5 нет точки с запятой.

    +

    JavaScript воспринимает этот код как если бы перевода строки не было:

    + +
    +
    var a = 5(function() {
    +alert(a)
    +})()
    +
    +

    То есть, он пытается вызвать функцию 5, что и приводит к ошибке.

    +

    Если точку с запятой поставить, все будет хорошо:

    +
    +
    var a = 5;
    +
    +(function() {
    +alert(a)
    +})()
    +
    +

    Это один из наиболее частых и опасных подводных камней, приводящих к ошибкам тех, кто не ставит точки с запятой.

    +
    +
    +
    + +
    +

    К одной задаче могут быть добавлены одно или несколько решений. Решения, идущие подряд, «стыкуются» без промежутков

    +
    +
    +
    +
    + +

    Спойлеры вне блока .important

    + +
    + +
    +

    Результат - ошибка. Попробуйте:

    +
    +
    var a = 5
    +
    +(function() {
    +alert(a)
    +})()
    +
    +

    Дело в том, что после var a = 5 нет точки с запятой.

    +

    JavaScript воспринимает этот код как если бы перевода строки не было:

    + +
    +
    var a = 5(function() {
    +alert(a)
    +})()
    +
    +

    То есть, он пытается вызвать функцию 5, что и приводит к ошибке.

    +

    Если точку с запятой поставить, все будет хорошо:

    +
    +
    var a = 5;
    +
    +(function() {
    +alert(a)
    +})()
    +
    +

    Это один из наиболее частых и опасных подводных камней, приводящих к ошибкам тех, кто не ставит точки с запятой.

    +
    +
    +
    + +
    +

    К одной задаче могут быть добавлены одно или несколько решений. Решения, идущие подряд, «стыкуются» без промежутков

    +
    +
    + +

    Этих ограничений нет там, где JavaScript используется вне браузера, например на сервере.

    +

    Большинство возможностей JavaScript в браузере ограничено текущим окном и страницей.

    +

    +
      +
    • +

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

      +

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

    • +
    • +

      JavaScript, работающий в одной вкладке, почти не может общаться с другими вкладками и окнами. За исключением случая, когда он сам открыл это окно или несколько вкладок из одного источника (одинаковый домен, порт, протокол).

      +

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

      +
    • +
    • +

      Из JavaScript можно легко посылать запросы на сервер, с которого пришла страничка. Запрос на другой домен тоже возможен, но менее удобен, т.к. и здесь есть ограничения безопасности.

      +
    • +
    + +

    В чем уникальность JavaScript?

    +

    Есть как минимум три замечательных особенности JavaScript:

    +
    +
    +
    + +
      +
    • Полная интеграция с HTML/CSS. function f (x, y, z) { return x + y + z; }

    • +
    • Простые вещи делаются просто. +

      function sayHi(name) {
      +  var phrase = "Привет, " + name;
      +  alert(phrase);
      +}
      +
      +sayHi('Вася');

    • +
    • Поддерживается всеми распространенными браузерами и включен по умолчанию.

    • +
    +
    +
    +
    + +

    Вариант блока с достоинствами и недостатками

    + +
    +
    +
    +

    Достоинства

    +
      +
    • Полная интеграция с HTML/CSS.

    • +
    • Простые вещи делаются просто.

    • +
    • Поддерживается всеми распространенными браузерами и включен по умолчанию.

    • +
    +
    +
    +

    Недостатки

    +
      +
    • Полная интеграция с HTML/CSS.

    • +
    • Простые вещи делаются просто.

    • +
    • Поддерживается всеми распространенными браузерами и включен по умолчанию.

    • +
    +
    +
    +
    + +

    Этих трех вещей одновременно нет больше ни в одной браузерной технологии. Поэтому JavaScript и является самым распространенным средством создания браузерных интерфейсов.

    +

    Пример блока "balance" с узким контентом, который может вызвать ошибку

    + +
    +
    +
    +

    Достоинства

    +
      +
    • +

      Простота реализации.

      +
    • +
    +
    +
    +

    Недостатки

    +
      +
    • +

      Задержки между событием и уведомлением.

      +
    • +
    • +

      Лишний трафик и запросы на сервер.

      +
    • +
    +
    +
    +
    + +
    + Показать простой вариант compareNumeric +
    +

    Функция должна возвращать положительное число, если a > b, отрицательное, если наоборот, и, например, 0, если числа равны.

    +

    Всем этим требованиям удовлетворяет функция.

    +

    А примера кода не будет.

    +
    +
    + +

    Тенденции развития.

    +

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

    + +

    HTML 5

    +

    HTML 5 — эволюция стандарта HTML, добавляющая новые теги и, что более важно, ряд новых возможностей браузерам.

    +
    + + +
    +

    Здесь может быть что угодно

    +
    +
    +

    Вот несколько примеров:

    +
      +
    • Чтение/запись файлов на диск (в специальной «песочнице», то есть не любые).
    • +
    • Встроенная в браузер база данных, которая позволяет хранить данные на компьютере пользователя.
    • +
    • Многозадачность с одновременным использованием нескольких ядер процессора.
    • +
    • Проигрывание видео/аудио, без Flash.
    • +
    • 2d и 3d-рисование с аппаратной поддержкой, как в современных играх.
    • +
    +

    Многие возможности HTML5 все еще в разработке, но браузеры постепенно начинают их поддерживать.

    +
    +

    Тенденция: JavaScript становится все более и более мощным и возможности браузера растут в сторону десктопных приложений.

    +
    + +

    EcmaScript

    +

    Сам язык JavaScript улучшается. Современный стандарт EcmaScript 5 включает в себя новые возможности для разработки.

    +

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

    +
    +
    +

    Тенденция: JavaScript становится все быстрее и стабильнее.

    +
    +
    +

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

    +

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

    +

    …Однако, со временем стандарт меняется и браузерам приходится подстраиваться к нему, что может привести к ошибкам в уже написанном (старом) коде. Поэтому следует дважды подумать перед тем, как применять на практике такие «супер-новые» решения.

    +

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

    +
    +
    +

    Тенденция: все идет к полной совместимости со стандартом.

    +
    +
    + +

    Недостатки JavaScript

    +

    Зачастую, недостатки подходов и технологий — это обратная сторона их полезности. Стоит ли упрекать молоток в том, что он — тяжелый? Да, неудобно, зато гвозди забиваются лучше.

    + +

    Заголовок третьего уровня

    +

    В JavaScript, однако, есть вполне объективные недоработки, связанные с тем, что язык, по выражению его автора (Brendan Eich) делался «за 10 бессонных дней и ночей». Поэтому некоторые моменты продуманы плохо, есть и откровенные ошибки (которые признает тот же Brendan).

    +

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

    + +

    Заголовок четвертого уровня

    +

    Пока что нам важно знать, что некоторые «странности» языка не являются чем-то очень умным, а просто не были достаточно хорошо продуманы в своё время. В этом учебнике мы будем обращать особое внимание на основные недоработки и «грабли». Ничего критичного в них нет, если знаешь — не наступишь.

    +

    В новых версиях JavaScript (ECMAScript) эти недостатки постепенно убирают. Процесс внедрения небыстрый, в первую очередь из-за старых версий IE, но они постепенно вымирают. Современный IE в этом отношении несравнимо лучше.

    diff --git a/hmvc/markup/templates/pages/article.jade b/hmvc/markup/templates/pages/article.jade new file mode 100644 index 000000000..2ce0bd653 --- /dev/null +++ b/hmvc/markup/templates/pages/article.jade @@ -0,0 +1,12 @@ +extends ../layouts/base + +// - http://stackoverflow.com/questions/12646451/how-to-pass-variables-between-jade-templates +block variables + - var self = {}; + - self.bodyId = 'page'; + - self.title = 'Учебник — Javascript.ru'; + - self.comments = {} // хм? + - self.comments.lenght = 5; + +block content + include ./article.html diff --git a/hmvc/markup/templates/pages/form.jade b/hmvc/markup/templates/pages/form.jade new file mode 100644 index 000000000..588d99efd --- /dev/null +++ b/hmvc/markup/templates/pages/form.jade @@ -0,0 +1,4 @@ +form(method="POST",action="/test") + input(type="hidden",name="name",value="value") + input(type="submit",value="submit") + diff --git a/hmvc/markup/templates/pages/my.html b/hmvc/markup/templates/pages/my.html new file mode 100644 index 000000000..9db1c64de --- /dev/null +++ b/hmvc/markup/templates/pages/my.html @@ -0,0 +1 @@ +Hello diff --git a/hmvc/payments/bank-simple/index.js b/hmvc/payments/bank-simple/index.js new file mode 100644 index 000000000..ad9f729eb --- /dev/null +++ b/hmvc/payments/bank-simple/index.js @@ -0,0 +1,4 @@ + +// TODO + +exports.renderForm = require('./renderForm'); diff --git a/hmvc/payments/bank-simple/renderForm.js b/hmvc/payments/bank-simple/renderForm.js new file mode 100644 index 000000000..0a6f6d526 --- /dev/null +++ b/hmvc/payments/bank-simple/renderForm.js @@ -0,0 +1,15 @@ +const jade = require('jade'); +const config = require('config'); +const path = require('path'); + +module.exports = function* (transaction) { + + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + amount: transaction.amount, + number: transaction.number, + webmoney: config.payments.modules.webmoney + }); + +}; + + diff --git a/hmvc/payments/bank-simple/templates/form.jade b/hmvc/payments/bank-simple/templates/form.jade new file mode 100644 index 000000000..8ad6d67ce --- /dev/null +++ b/hmvc/payments/bank-simple/templates/form.jade @@ -0,0 +1,7 @@ +form(method="POST",action="https://merchant.webmoney.ru/lmi/payment.asp") + input(type="hidden",name="LMI_PAYMENT_AMOUNT",value=amount) + input(type="hidden",name="LMI_PAYMENT_DESC_BASE64",value=new Buffer('оплата по счету ' + number).toString('base64')) + input(type="hidden",name="LMI_PAYMENT_NO",value=number) + input(type="hidden",name="LMI_PAYEE_PURSE",value=webmoney.purse) + input(type="hidden",name="LMI_SIM_MODE",value=(isTest ? 1 : 0)) + input(type="submit",value="Оплатить") diff --git a/hmvc/payments/bank-simple/test/.jshintrc b/hmvc/payments/bank-simple/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/hmvc/payments/bank-simple/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/hmvc/payments/index.js b/hmvc/payments/index.js new file mode 100644 index 000000000..8907bb8c4 --- /dev/null +++ b/hmvc/payments/index.js @@ -0,0 +1,64 @@ +var mount = require('koa-mount'); +var config = require('config'); +var compose = require('koa-compose'); + +// Interaction with payment systems only. + +exports.loadOrder = require('./lib/loadOrder'); +exports.loadTransaction = require('./lib/loadTransaction'); + +var Order = exports.Order = require('./models/order'); +var OrderTemplate = exports.OrderTemplate = require('./models/orderTemplate'); +var Transaction = exports.Transaction = require('./models/transaction'); +var TransactionLog = exports.TransactionLog = require('./models/transactionLog'); + +// all submodules +var paymentModules = {}; +for(var name in config.payments.modules) { + paymentModules[name] = require('./' + name); +} + +// mount('/webmoney', webmoney.middleware()) +var paymentMounts = []; +for(var name in paymentModules) { + paymentMounts.push(mount('/' + name, paymentModules[name].middleware)); +} + +// delegate all HTTP calls to payment modules +exports.middleware = compose(paymentMounts); + + +exports.populateContextMiddleware = function*(next) { + this.redirectToOrder = function(order) { + order = order || this.order; + this.redirect('/' + order.module + '/orders/' + order.number); + }; + this.loadOrder = exports.loadOrder; + this.loadTransaction = exports.loadTransaction; + yield* next; +}; + +// creates transaction and returns the form to submit for its payment +// delegates form to the method +exports.createTransactionForm = function* (order, method) { + + var transaction = new Transaction({ + order: order._id, + amount: order.amount, + module: method + }); + + yield transaction.persist(); + + console.log(transaction); + + var form = yield* paymentModules[method].renderForm(transaction); + + yield transaction.log('form', form); + + return form; + +}; + + + diff --git a/hmvc/payments/lib/loadOrder.js b/hmvc/payments/lib/loadOrder.js new file mode 100644 index 000000000..f71e937ca --- /dev/null +++ b/hmvc/payments/lib/loadOrder.js @@ -0,0 +1,32 @@ +var mongoose = require('mongoose'); +var Order = require('../models/order'); +var assert = require('assert'); + +// Populates this.order with the order by "orderNumber" parameter +module.exports = function* (field) { + + if (!field) field = 'orderNumber'; + + var orderNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; + + if (!orderNumber) { + return; + } + + var order = yield Order.findOne({number: orderNumber}).populate('order').exec(); + + if (!order) { + this.throw(404, 'Нет такого заказа'); + } + + // todo: add belongs to check (with auth) + if (!this.session.orders || this.session.orders.indexOf(order.number) == -1) { + this.throw(403, 'Заказ в сессии не найден'); + } + + + assert(!this.order, "this.order is already set (by loadTransaction?)"); + + this.order = order; + +}; diff --git a/hmvc/payments/lib/loadTransaction.js b/hmvc/payments/lib/loadTransaction.js new file mode 100644 index 000000000..2603a7c34 --- /dev/null +++ b/hmvc/payments/lib/loadTransaction.js @@ -0,0 +1,38 @@ +var mongoose = require('mongoose'); +var Transaction = require('../models/transaction'); +var log = require('js-log')(); +var assert = require('assert'); + +// Populates this.transaction with the transaction by "transactionNumber" parameter +// options.skipOwnerCheck is for signed submissions, set to true allows anyone to load transaction +module.exports = function* (field, options) { + options = options || {}; + if (!field) field = 'transactionNumber'; + + var transactionNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; + + log.debug('tx number: ' + transactionNumber); + if (!transactionNumber) { + return; + } + + var transaction = yield Transaction.findOne({number: transactionNumber}).populate('order').exec(); + + if (!transaction) { + this.throw(404, 'Нет такой транзакции'); + } + + if (!options.skipOwnerCheck) { + // todo: add belongs to check (with auth) + if (!this.session.orders || this.session.orders.indexOf(transaction.order.number) == -1) { + this.throw(403, 'Не найден заказ в сессии для этой транзакции'); + } + } + + assert(!this.transaction, "this.transaction is already set"); + assert(!this.order, "this.order is already set"); + + this.transaction = transaction; + this.order = transaction.order; + +}; diff --git a/hmvc/payments/models/order.js b/hmvc/payments/models/order.js new file mode 100644 index 000000000..0618939c2 --- /dev/null +++ b/hmvc/payments/models/order.js @@ -0,0 +1,60 @@ +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; +var autoIncrement = require('mongoose-auto-increment'); +var OrderTemplate = require('./orderTemplate'); +var _ = require('lodash'); + +var schema = new Schema({ + amount: { + type: Number, + required: true + }, + module: { // module so that transaction handler knows where to go back e.g. 'getpdf' + type: String, + required: true + }, + title: { + type: String, + required: true + }, + description: { + type: String + }, + status: { + type: String + }, + + // order can be bound to either an email or a user + email: { + type: String + }, + user: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + + data: Schema.Types.Mixed, + created: { + type: Date, + default: Date.now + } +}); + +schema.statics.createFromTemplate = function(orderTemplate, body) { + var Order = this; + + var data = _.assign({ + title: orderTemplate.title, + description: orderTemplate.description, + amount: orderTemplate.amount, + data: {} + }, body || {}); + + return new Order(data); + +}; + +schema.plugin(autoIncrement.plugin, {model: 'Order', field: 'number', startAt: 1}); + +module.exports = mongoose.model('Order', schema); + diff --git a/hmvc/payments/models/orderTemplate.js b/hmvc/payments/models/orderTemplate.js new file mode 100644 index 000000000..04626e66a --- /dev/null +++ b/hmvc/payments/models/orderTemplate.js @@ -0,0 +1,37 @@ +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +/** + * In other words, "Store items" + * New orders do *not* reference the items, because store items may change + * Instead new orders contain full information about themselves. + * + * OrderTemplate can be deleted, but the order is self-contained. + * @type {Schema} + */ +var schema = new Schema({ + title: { + type: String, + required: true + }, + description: { + type: String + }, + // when a user visits /order/slug, the new order is created from this template + slug: { + type: String, + required: true, + index: true + }, + amount: { + type: Number, + required: true + }, + created: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model('OrderTemplate', schema); + diff --git a/hmvc/payments/models/transaction.js b/hmvc/payments/models/transaction.js new file mode 100644 index 000000000..98ae34f11 --- /dev/null +++ b/hmvc/payments/models/transaction.js @@ -0,0 +1,132 @@ +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; +var autoIncrement = require('mongoose-auto-increment'); +var Order = require('./order'); +var TransactionLog = require('./transactionLog'); + +/** + * Transaction is an actual payment attempt (successful or not) for something + * - Order may exist without any transactions (pay later) + * - Transaction has it's own separate number (payment attempt) + * - Transaction amount can be different from order amount (partial payment) + * - Every transaction save generates a log record + * @type {Schema} + */ +var schema = new Schema({ + order: { + type: Schema.Types.ObjectId, + ref: 'Order' + }, + amount: { + type: Number, + required: true + }, + module: { + type: String, + required: true + }, + created: { + type: Date, + default: Date.now + }, + status: { + type: String + }, + statusMessage: { + type: String + } +}); + +schema.plugin(autoIncrement.plugin, {model: 'Transaction', field: 'number', startAt: 1}); + +schema.statics.STATUS_SUCCESS = 'success'; +schema.statics.STATUS_PENDING = 'pending'; +schema.statics.STATUS_FAIL = 'fail'; + +/* +// DEPRECATED: orderModule.onSuccess updates its status +// autoupdate order to SUCCESS when succeeded +schema.pre('save', function(next) { + if (this.status == Transaction.STATUS_SUCCESS) { + var orderId = this.order._id || this.order; + Order.findByIdAndUpdate(orderId, {status: Transaction.STATUS_SUCCESS}, next); + } else { + next(); + } +}); +*/ + +// autolog all changes +schema.pre('save', function(next) { + + var log = new TransactionLog({ + transaction: this._id, + event: 'save', + data: { + status: this.status, + statusMessage: this.statusMessage, + amount: this.amount + } + }); + + log.save(function(err, doc) { + next(err); + }); +}); +/* +schema.methods.getStatusDescription = function() { + if (this.status == Transaction.STATUS_SUCCESS) { + return 'Оплата прошла успешно.'; + } + if (this.status == Transaction.STATUS_PENDING) { + return 'Оплата ожидается, о её успешномо окончании вы будете извещены по e-mail.'; + } + + if (this.status == Transaction.STATUS_FAIL) { + var result = 'Оплата не прошла'; + if (this.statusMessage) result += ': ' + this.statusMessage; + return result + '.'; + } + + if (!this.status) { + return 'нет информации об оплате'; + } + + throw new Error("неподдерживаемый статус транзакции"); +}; +*/ + +schema.methods.logRequest = function*(event, request) { + yield this.log(event, {url: request.originalUrl, body: request.body}); +}; + +// log anything related to the transaction +schema.methods.log = function*(event, data) { + + if (typeof event != "string") { + throw new Error("event name must be a string"); + } + + var options = { + transaction: this._id, + event: event, + data: data + }; + + // for complex objects -> prior to logging make them simple (must be jsonable) + // e.g for HTTP response (HTTP.IncomingMessage) + if (options.data && typeof options.data == 'object') { + // object keys may not contain "." in mongodb, so I may not store arbitrary objects + // only json can help + options.data = JSON.stringify(options.data); + } + +// console.log(options); + + var log = new TransactionLog(options); + yield log.persist(); +}; + +/* jshint -W003 */ +var Transaction = module.exports = mongoose.model('Transaction', schema); + diff --git a/hmvc/payments/models/transactionLog.js b/hmvc/payments/models/transactionLog.js new file mode 100644 index 000000000..0ef24a5a8 --- /dev/null +++ b/hmvc/payments/models/transactionLog.js @@ -0,0 +1,24 @@ +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +var schema = new Schema({ + + transaction: { + type: Schema.Types.ObjectId, + ref: 'Transaction', + index: true + }, + event: { + type: String, + index: true + }, + data: Schema.Types.Mixed, + + created: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model('TransactionLog', schema); + diff --git a/hmvc/payments/payanyway/controller/callback.js b/hmvc/payments/payanyway/controller/callback.js new file mode 100644 index 000000000..c0d80df54 --- /dev/null +++ b/hmvc/payments/payanyway/controller/callback.js @@ -0,0 +1,75 @@ +const config = require('config'); +//require('config/mongoose'); +const payanywayConfig = config.payments.modules.payanyway; +const Order = require('../../models/order'); +const Transaction = require('../../models/transaction'); +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + +exports.post = function* (next) { + + checkSignature(this.request.body); + this.body = 'SUCCESS'; + return; + + yield* this.loadTransaction('MNT_TRANSACTION_ID', {skipOwnerCheck : true}); + + + yield this.transaction.logRequest('callback unverified', this.request); + + if (!checkSignature(this.request.body)) { + log.debug("wrong signature"); + this.throw(403, "wrong signature"); + } + + yield this.transaction.logRequest('callback', this.request); + + // signature is valid, so everything MUST be fine + if (this.transaction.amount != parseFloat(this.request.body.MNT_AMOUNT) || + this.request.body.MNT_ID != payanywayConfig.id) { + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL, + statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой" + }); + this.throw(404, "transaction data doesn't match the POST body"); + } + + yield this.transaction.persist({ + status: Transaction.STATUS_SUCCESS + }); + + var order = this.order; + log.debug("will call order onSuccess module=" + order.module); + yield* require(order.module).onSuccess(order); + + this.body = 'SUCCESS'; +}; + +function checkSignature(body) { + + var signature = body.MNT_ID + body.MNT_TRANSACTION_ID + body.MNT_AMOUNT + + body.MNT_CURRENCY_CODE + (body.MNT_SUBSCRIBER_ID || '') + (+body.MNT_TEST_MODE ? '1' : '0') + payanywayConfig.secret; + + console.log(signature); + signature = md5(signature); + + console.log(signature); + return signature == body.MNT_SIGNATURE; +} + +/* +var body ={ MNT_ID: '31873866', + MNT_TRANSACTION_ID: '12', + MNT_OPERATION_ID: '55923826', + MNT_AMOUNT: '1.00', + MNT_CURRENCY_CODE: 'RUB', + MNT_TEST_MODE: '0', + MNT_SIGNATURE: 'ebf8d4b9fa10301b858cc314b356cc41', + 'paymentSystem.unitId': '822360', + MNT_CORRACCOUNT: '15598507', + qiwiphone: '9035419441' } + +console.log(+checkSignature(body)); +*/ diff --git a/hmvc/payments/payanyway/controller/cancel.js b/hmvc/payments/payanyway/controller/cancel.js new file mode 100644 index 000000000..9c366a4fa --- /dev/null +++ b/hmvc/payments/payanyway/controller/cancel.js @@ -0,0 +1,17 @@ +const Transaction = require('../../models/transaction'); +const log = require('js-log')(); + + +exports.get = function* (next) { + + yield* this.loadTransaction('MNT_TRANSACTION_ID'); + + this.transaction.persist({ + status: Transaction.STATUS_FAIL, + statusMessage: 'отказ от оплаты' + }); + + this.redirectToOrder(); +}; + + diff --git a/hmvc/payments/payanyway/controller/fail.js b/hmvc/payments/payanyway/controller/fail.js new file mode 100644 index 000000000..af3f6b83f --- /dev/null +++ b/hmvc/payments/payanyway/controller/fail.js @@ -0,0 +1,14 @@ +const Transaction = require('../../models/transaction'); + +exports.get = function* (next) { + + yield* this.loadTransaction('MNT_TRANSACTION_ID'); + + this.transaction.persist({ + status: Transaction.STATUS_FAIL + }); + + this.redirectToOrder(); +}; + + diff --git a/hmvc/payments/payanyway/controller/inprogress.js b/hmvc/payments/payanyway/controller/inprogress.js new file mode 100644 index 000000000..f5d43d90f --- /dev/null +++ b/hmvc/payments/payanyway/controller/inprogress.js @@ -0,0 +1,11 @@ +const Transaction = require('../../models/transaction'); + + +exports.get = function* (next) { + + yield* this.loadTransaction('MNT_TRANSACTION_ID'); + + this.redirectToOrder(); +}; + + diff --git a/hmvc/payments/payanyway/controller/success.js b/hmvc/payments/payanyway/controller/success.js new file mode 100644 index 000000000..b80caa6f7 --- /dev/null +++ b/hmvc/payments/payanyway/controller/success.js @@ -0,0 +1,7 @@ +const Transaction = require('../../models/transaction'); + +exports.get = function* (next) { + yield* this.loadTransaction('MNT_TRANSACTION_ID'); + + this.redirectToOrder(); +}; diff --git a/hmvc/payments/payanyway/index.js b/hmvc/payments/payanyway/index.js new file mode 100644 index 000000000..6a6df2fb3 --- /dev/null +++ b/hmvc/payments/payanyway/index.js @@ -0,0 +1,7 @@ + +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.renderForm = require('./renderForm'); + diff --git a/hmvc/payments/payanyway/renderForm.js b/hmvc/payments/payanyway/renderForm.js new file mode 100644 index 000000000..922e3a11c --- /dev/null +++ b/hmvc/payments/payanyway/renderForm.js @@ -0,0 +1,17 @@ +const jade = require('jade'); +const config = require('config'); +const path = require('path'); + +module.exports = function* (transaction) { + + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + amount: transaction.amount, + number: transaction.number, + currency: config.payments.currency, + id: config.payments.modules.payanyway.id, + limitIds: process.env.NODE_ENV == 'development' ? '' : '843858,248362,822360,545234,1028,499669' + }); + +}; + + diff --git a/hmvc/payments/payanyway/router.js b/hmvc/payments/payanyway/router.js new file mode 100644 index 000000000..aaaf0ed6b --- /dev/null +++ b/hmvc/payments/payanyway/router.js @@ -0,0 +1,18 @@ +var Router = require('koa-router'); + +var router = module.exports = new Router(); + +var callback = require('./controller/callback'); + +var success = require('./controller/success'); +var inprogress = require('./controller/inprogress'); +var cancel = require('./controller/cancel'); + +router.post('/callback', callback.post); + +router.get('/success', success.get); +router.get('/inprogress', inprogress.get); + +router.get('/cancel', cancel.get); + + diff --git a/hmvc/payments/payanyway/templates/form.jade b/hmvc/payments/payanyway/templates/form.jade new file mode 100644 index 000000000..afe7fdb6c --- /dev/null +++ b/hmvc/payments/payanyway/templates/form.jade @@ -0,0 +1,10 @@ + +form(method="POST" action="https://www.moneta.ru/assistant.htm" accept-charset="UTF-8") + input(type="hidden",name="MNT_ID",value=id) + input(type="hidden",name="MNT_TRANSACTION_ID",value=number) + input(type="hidden",name="MNT_CURRENCY_CODE",value=currency) + input(type="hidden",name="MNT_AMOUNT",value=amount) + if limitIds + input(type="hidden",name="paymentSystem.limitIds",value=limitIds) + input(type="submit",value="Оплатить") + diff --git a/hmvc/payments/paypal/controller/callback.js b/hmvc/payments/paypal/controller/callback.js new file mode 100644 index 000000000..8cc387785 --- /dev/null +++ b/hmvc/payments/paypal/controller/callback.js @@ -0,0 +1,127 @@ +const config = require('config') +const paypalConfig = config.payments.modules.paypal; +const Order = require('../../models/order'); +const Transaction = require('../../models/transaction'); +const TransactionLog = require('../../models/transactionLog'); +const log = require('js-log')(); +const request = require('koa-request'); + +// docs: +// +// https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNIntro/ + +log.debugOn(); + +/* jshint -W106 */ +exports.post = function* (next) { + + yield* this.loadTransaction('invoice', {skipOwnerCheck: true}); + + yield this.transaction.logRequest('ipn-request unverified', this.request); + + var qs = { + 'cmd': '_notify-validate' + }; + + for (var field in this.request.body) { + qs[field] = this.request.body[field]; + } + + // request oauth token + var options = { + method: 'GET', + qs: qs, + url: 'https://www.paypal.com/cgi-bin/webscr' + }; + + + yield this.transaction.log('request ipn verify', options); + + var response; + try { + response = yield request(options); + } catch(e) { + yield this.transaction.log('request ipn verify failed', e.message); + this.throw(403, "Couldn't verify ipn"); + } + + if (response.body != "VERIFIED") { + this.throw(403, "Invalid IPN"); + } + + // ipn is verified now! But we check if it's data matches the transaction (as recommended in docs) + if (this.transaction.amount != parseFloat(this.request.body.mc_gross) || + this.request.body.receiver_email != paypalConfig.email || + this.request.body.mc_currency != config.payments.currency) { + + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL, + statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой" + }); + this.throw(404, "transaction data doesn't match the POST body"); + } + + // match agains latest ipn in logs as recommended: + // if there was an IPN about the same transaction, and it's state is the same + // => then the current one is a duplicate + + var previousIpn = yield TransactionLog.findOne({ + event: "ipn", + transaction: this.transaction._id + }).sort({created: -1}).exec(); + + if (previousIpn && previousIpn.data.payment_status == this.request.body.payment_status) { + yield this.transaction.log("ipn duplicate", this.request.body); + // ignore duplicate + this.body = ''; + return; + } + + // now we have a valid non-duplicate IPN, let's update the transaction + + // log it right now to evade conflicts with duplicates + yield this.transaction.log("ipn", this.request.body); + + // Do not perform any processing on WPS transactions here that do not have + // transaction IDs, indicating they are non-payment IPNs such as those used + // for subscription signup requests. + if (!this.request.body.txn_id) { + yield this.transaction.log("ipn without txn_id", this.request.body); + this.body = ''; + return; + } + + switch(this.request.body.payment_status) { + case 'Failed': + case 'Voided': + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL + }); + this.body = ''; + return; + case 'Pending': + yield this.transaction.persist({ + status: Transaction.STATUS_PENDING, + statusMessage: this.request.body.pending_reason + }); + this.body = ''; + return; + case 'Completed': + yield this.transaction.persist({ + status: Transaction.STATUS_SUCCESS + }); + + yield* require(this.order.module).onSuccess(this.order); + this.body = ''; + return; + default: + // Refunded ... + yield this.transaction.log("ipn payment_status unknown", this.request.body); + + this.body = ''; + return; + } + + + +}; diff --git a/hmvc/payments/paypal/controller/cancel.js b/hmvc/payments/paypal/controller/cancel.js new file mode 100644 index 000000000..fcef4fd87 --- /dev/null +++ b/hmvc/payments/paypal/controller/cancel.js @@ -0,0 +1,16 @@ +const Transaction = require('../../models/transaction'); +const log = require('js-log')(); + + +exports.get = function* (next) { + + yield* this.loadTransaction(); + + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL, + statusMessage: 'отказ от оплаты' + }); + + this.redirectToOrder(); +}; + diff --git a/hmvc/payments/paypal/controller/success.js b/hmvc/payments/paypal/controller/success.js new file mode 100644 index 000000000..fecd136ef --- /dev/null +++ b/hmvc/payments/paypal/controller/success.js @@ -0,0 +1,6 @@ + +exports.post = function* (next) { + yield* this.loadTransaction(); + + this.redirectToOrder(); +}; diff --git a/hmvc/payments/paypal/index.js b/hmvc/payments/paypal/index.js new file mode 100644 index 000000000..fb208bb72 --- /dev/null +++ b/hmvc/payments/paypal/index.js @@ -0,0 +1,7 @@ +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.renderForm = require('./renderForm'); + + diff --git a/hmvc/payments/paypal/renderForm.js b/hmvc/payments/paypal/renderForm.js new file mode 100644 index 000000000..43c3c29ab --- /dev/null +++ b/hmvc/payments/paypal/renderForm.js @@ -0,0 +1,46 @@ +const jade = require('jade'); +const config = require('config'); +const paypalConfig = config.payments.modules.paypal; +const path = require('path'); +const signCart = require('./signCart')(paypalConfig.myCertPath, paypalConfig.myKeyPath, paypalConfig.paypalCertPath); + +module.exports = function* (transaction) { + + /* jshint -W106 */ + var cart = { + cert_id: paypalConfig.certId, + business: paypalConfig.email, + invoice: transaction.number, + amount: transaction.amount, + item_name: "Оплата по счёту " + transaction.number, + cmd: "_xclick", // Buy now button (buying a single item, works with Encrypted Website Payments, not sure if _cart works too) + charset: "utf-8", + no_note: 1, + no_shipping: 1, + rm: 2, + currency_code: config.payments.currency, + lc: "RU" + }; + + yield transaction.log("cart", cart); + + var cartFormatted = []; + for(var key in cart) { + cartFormatted.push(key + '=' + cart[key]); + } + cartFormatted = cartFormatted.join("\n"); + + var cartEncrypted = yield signCart(cartFormatted); + + var form = jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + encrypted: cartEncrypted, + notifyUrl: paypalConfig.callbackUrl + '?transactionNumber=' + transaction.number, + cancelUrl: paypalConfig.cancelUrl + '?transactionNumber=' + transaction.number, + returnUrl: paypalConfig.successUrl + '?transactionNumber=' + transaction.number + }); + + return form; + +}; + + diff --git a/hmvc/payments/paypal/router.js b/hmvc/payments/paypal/router.js new file mode 100644 index 000000000..ec7df27a6 --- /dev/null +++ b/hmvc/payments/paypal/router.js @@ -0,0 +1,17 @@ +var Router = require('koa-router'); + +var router = module.exports = new Router(); + +var callback = require('./controller/callback'); +var success = require('./controller/success'); +var cancel = require('./controller/cancel'); + +// webmoney server posts here (in background) +router.post('/callback', callback.post); + +// webmoney server redirects here if payment successful +router.post('/success', success.post); + +router.get('/cancel', cancel.get); + + diff --git a/hmvc/payments/paypal/signCart.js b/hmvc/payments/paypal/signCart.js new file mode 100644 index 000000000..a5b8e0542 --- /dev/null +++ b/hmvc/payments/paypal/signCart.js @@ -0,0 +1,76 @@ +/* + # Signing the cart for paypal WPS + + ## Why OpenSSL Cli ? + Node crypto can't sign pkcs7 + node-forge (not sure) couldn't find the way + so I'm using openssl CLI + + ## OpenSSL params origins + Thanks to from http://www.stellarwebsolutions.com/en/articles/paypal_button_encryption_php.php + + ## Preparation + First, make certs: + openssl genrsa -out app_key.pem 1024 + openssl req -new -key app_key.pem -x509 -days 365 -out app_cert.pem + + (for java optional) openssl pkcs12 -export -inkey app_key.pem -in app_cert.pem -out app_cert.p12 + (for java optional) p12 is a format which groups key and cert and signs, so password is required + + ## Alternative way to sign: + EWP Soft: + https://www.paypal.com/us/cgi-bin/webscr?cmd=p/xcl/rec/ewp-code + to compile java, get http://www.bouncycastle.org/archive/124/crypto-124.tar.gz + put to parent dir and run the compiler like this (modified build_app.sh): + === + export CRYPTO_HOME="/js/javascript-nodejs/tmp/crypto" + + CLASSPATH="." + CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bcmail-jdk15-124.jar" + CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bcpg-jdk15-124.jar" + CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bcprov-jdk15-124.jar" + CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bctest-jdk15-124.jar" + CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/jce-jdk13-124.jar" + export CLASSPATH + + javac -g -classpath "$CLASSPATH" ButtonEncryption.java com/paypal/crypto/sample/*.java + + Then: + (where Kah1voo8 is p12 password, 7BXVJJ3YFS3HQ is cert id from paypal (upload app_cert.pem to paypal to see it) + java ButtonEncryption app_cert.pem app_cert.p12 paypal_cert.pem Kah1voo8 "cert_id=7BXVJJ3YFS3HQ,business=iliakan@gmail.com,notify_url=http://stage.javascript.ru/payments/paypal/callback?transactionNumber=123,cancel_return=http://stage.javascript.ru/payments/paypal/cancel?transactionNumber=123,notify_url=http://stage.javascript.ru/payments/paypal/callback?transactionNumber=123,return=http://stage.javascript.ru/payments/paypal/success?transactionNumber=123,invoice=123,amount=2,item_name=Оплата по счету 123,cmd=_xclick,charset=utf-8,no_note=1,no_shipping=1,rm=2,currency_code=RUB,lc=RU,secret=blabla" out.html + ===== +*/ + +/* jshint -W021 */ + +var exec = require('child_process').exec; +var fs = require('fs'); +var assert = require('assert'); +var thunkify = require('thunkify'); + +function signCart(myCertPath, myKeyPath, paypalCertPath, message, callback) { + + var cmd = 'openssl smime -sign -signer ' + myCertPath + ' -inkey ' + myKeyPath + ' -outform der -nodetach -binary | openssl smime -encrypt -des3 -binary -outform pem ' + paypalCertPath; + + var child = exec( + cmd, + function(err, stdout, stderr) { + if (err) return callback(err); + return callback(null, stdout); + } + ); + + child.stdin.end(message); +} + +module.exports = function(myCertPath, myKeyPath, paypalCertPath) { + assert(fs.existsSync(myCertPath)); + assert(fs.existsSync(myKeyPath)); + assert(fs.existsSync(paypalCertPath)); + + return thunkify(signCart.bind(null, myCertPath, myKeyPath, paypalCertPath)); +}; + + +//openssl smime -sign -signer $MY_CERT_FILE -inkey $MY_KEY_FILE -in content -outform der -nodetach -binary +// | openssl smime -encrypt -des3 -binary -outform pem $PAYPAL_CERT_FILE diff --git a/hmvc/payments/paypal/templates/form.jade b/hmvc/payments/paypal/templates/form.jade new file mode 100644 index 000000000..a195ad254 --- /dev/null +++ b/hmvc/payments/paypal/templates/form.jade @@ -0,0 +1,8 @@ + +form(method="post" action="https://www.paypal.com/cgi-bin/webscr") + input(type="hidden",name="cmd",value="_s-xclick") + input(type="submit" value="submit") + input(type="hidden",name="encrypted",value=encrypted) + input(type="hidden",name="notify_url",value=notifyUrl) + input(type="hidden",name="cancel_return",value=cancelUrl) + input(type="hidden",name="return",value=returnUrl) diff --git a/hmvc/payments/paypal/test/.jshintrc b/hmvc/payments/paypal/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/hmvc/payments/paypal/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/hmvc/payments/paypal/test/unit/signCart.js b/hmvc/payments/paypal/test/unit/signCart.js new file mode 100644 index 000000000..784f51f66 --- /dev/null +++ b/hmvc/payments/paypal/test/unit/signCart.js @@ -0,0 +1,17 @@ +var signCart = require('../../signCart'); +var paypalConfig = require('config').payments.modules.paypal; + +describe("signCart", function() { + + it("signs a message", function*() { + + var signed = yield signCart("cart content", + paypalConfig.myCertPath, paypalConfig.myKeyPath, paypalConfig.paypalCertPath); + + var header = '-----BEGIN PKCS7-----'; + + signed.slice(0, header.length).should.eql(header); + + }); + +}); diff --git a/hmvc/payments/webmoney/controller/callback.js b/hmvc/payments/webmoney/controller/callback.js new file mode 100644 index 000000000..84d4545fb --- /dev/null +++ b/hmvc/payments/webmoney/controller/callback.js @@ -0,0 +1,71 @@ +const webmoneyConfig = require('config').payments.modules.webmoney; +const mongoose = require('mongoose'); +const Order = require('../../models/order'); +const Transaction = require('../../models/transaction'); +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + +// ONLY ACCESSED from WEBMONEY SERVER +exports.prerequest = function* (next) { + yield* this.loadTransaction('LMI_PAYMENT_NO', {skipOwnerCheck : true}); + + log.debug("prerequest"); + + yield this.transaction.logRequest('prerequest', this.request); + + if (this.transaction.status == Transaction.STATUS_SUCCESS || + this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || + this.request.body.LMI_PAYEE_PURSE != webmoneyConfig.purse + ) { + log.debug("no pending transaction " + this.request.body.LMI_PAYMENT_NO); + this.throw(404, 'unfinished transaction with given params not found'); + } + + this.body = 'YES'; + +}; + +exports.post = function* (next) { + + yield* this.loadTransaction('LMI_PAYMENT_NO', {skipOwnerCheck : true}); + + if (!checkSignature(this.request.body)) { + log.debug("wrong signature"); + this.throw(403, "wrong signature"); + } + + yield this.transaction.logRequest('callback', this.request); + + if (this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || + this.request.body.LMI_PAYEE_PURSE != webmoneyConfig.purse) { + // STRANGE, signature is correct + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL, + statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой" + }); + this.throw(404, "transaction data doesn't match the POST body"); + } + + if (!this.request.body.LMI_SIM_MODE || this.request.body.LMI_SIM_MODE == '0') { + this.transaction.status = Transaction.STATUS_SUCCESS; + yield this.transaction.persist(); + } + + var order = this.transaction.order; + log.debug("will call order onSuccess module=" + order.module); + yield* require(order.module).onSuccess(order); + + this.body = 'OK'; + +}; + +function checkSignature(body) { + + var signature = md5(body.LMI_PAYEE_PURSE + body.LMI_PAYMENT_AMOUNT + body.LMI_PAYMENT_NO + + body.LMI_MODE + body.LMI_SYS_INVS_NO + body.LMI_SYS_TRANS_NO + body.LMI_SYS_TRANS_DATE + + webmoneyConfig.secretKey + body.LMI_PAYER_PURSE + body.LMI_PAYER_WM).toUpperCase(); + + return signature == body.LMI_HASH; +} diff --git a/hmvc/payments/webmoney/controller/fail.js b/hmvc/payments/webmoney/controller/fail.js new file mode 100644 index 000000000..0a50f9bb6 --- /dev/null +++ b/hmvc/payments/webmoney/controller/fail.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose'); +const Transaction = require('../../models/transaction'); +const log = require('js-log')(); + +exports.post = function* (next) { + + yield* this.loadTransaction('LMI_PAYMENT_NO'); + + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL + }); + + this.redirectToOrder(); + +}; diff --git a/hmvc/payments/webmoney/controller/success.js b/hmvc/payments/webmoney/controller/success.js new file mode 100644 index 000000000..6b4d94e5f --- /dev/null +++ b/hmvc/payments/webmoney/controller/success.js @@ -0,0 +1,9 @@ +const config = require('config'); +const mongoose = require('mongoose'); +const log = require('js-log')(); + +exports.post = function* (next) { + yield* this.loadTransaction('LMI_PAYMENT_NO'); + + this.redirectToOrder(); +}; diff --git a/hmvc/payments/webmoney/index.js b/hmvc/payments/webmoney/index.js new file mode 100644 index 000000000..1a9c21a42 --- /dev/null +++ b/hmvc/payments/webmoney/index.js @@ -0,0 +1,5 @@ +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.renderForm = require('./renderForm'); diff --git a/hmvc/payments/webmoney/renderForm.js b/hmvc/payments/webmoney/renderForm.js new file mode 100644 index 000000000..0a6f6d526 --- /dev/null +++ b/hmvc/payments/webmoney/renderForm.js @@ -0,0 +1,15 @@ +const jade = require('jade'); +const config = require('config'); +const path = require('path'); + +module.exports = function* (transaction) { + + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + amount: transaction.amount, + number: transaction.number, + webmoney: config.payments.modules.webmoney + }); + +}; + + diff --git a/hmvc/payments/webmoney/router.js b/hmvc/payments/webmoney/router.js new file mode 100644 index 000000000..adcb5361e --- /dev/null +++ b/hmvc/payments/webmoney/router.js @@ -0,0 +1,25 @@ +var Router = require('koa-router'); + +var router = module.exports = new Router(); + +var callback = require('./controller/callback'); +var success = require('./controller/success'); +var fail = require('./controller/fail'); + + +// webmoney server posts here (in background) +router.post('/callback', function* (next) { + if (this.request.body.LMI_PREREQUEST == '1') { + yield* callback.prerequest.call(this, next); + } else { + yield* callback.post.call(this, next); + } +}); + +// webmoney server redirects here if payment successful +router.post('/success', success.post); + +// webmoney server redirects here if payment failed +router.post('/fail', fail.post); + + diff --git a/hmvc/payments/webmoney/templates/form.jade b/hmvc/payments/webmoney/templates/form.jade new file mode 100644 index 000000000..8ad6d67ce --- /dev/null +++ b/hmvc/payments/webmoney/templates/form.jade @@ -0,0 +1,7 @@ +form(method="POST",action="https://merchant.webmoney.ru/lmi/payment.asp") + input(type="hidden",name="LMI_PAYMENT_AMOUNT",value=amount) + input(type="hidden",name="LMI_PAYMENT_DESC_BASE64",value=new Buffer('оплата по счету ' + number).toString('base64')) + input(type="hidden",name="LMI_PAYMENT_NO",value=number) + input(type="hidden",name="LMI_PAYEE_PURSE",value=webmoney.purse) + input(type="hidden",name="LMI_SIM_MODE",value=(isTest ? 1 : 0)) + input(type="submit",value="Оплатить") diff --git a/hmvc/payments/webmoney/test/.jshintrc b/hmvc/payments/webmoney/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/hmvc/payments/webmoney/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/hmvc/payments/yandexmoney/controller/back.js b/hmvc/payments/yandexmoney/controller/back.js new file mode 100644 index 000000000..794f8b121 --- /dev/null +++ b/hmvc/payments/yandexmoney/controller/back.js @@ -0,0 +1,198 @@ +const config = require('config'); +const Order = require('../../models/order'); +const Transaction = require('../../models/transaction'); +const log = require('js-log')(); +const request = require('koa-request'); + +log.debugOn(); + +/* jshint -W106 */ +exports.get = function* (next) { + + var self = this; + + yield* this.loadTransaction(); + + yield this.transaction.logRequest('back', this.request); + + if (!this.query.code) { + yield* fail(this.query.error_description || this.query.error); + return; + } + + + try { + var oauthTokenResponse = yield* requestOauthToken(this.query.code); + + var oauthToken = oauthTokenResponse.access_token; + if (!oauthToken) { + throwResponseError(oauthTokenResponse); + } + + var requestPaymentResponse = yield* requestPayment(oauthToken); + + if (requestPaymentResponse.status != "success") { + + if (requestPaymentResponse.error == 'ext_action_required') { + self.redirect(requestPaymentResponse.ext_action_uri); + return; + } + + throwResponseError(requestPaymentResponse); + } + + var requestId = requestPaymentResponse.request_id; + + var startTime = new Date(); + + while_in_progress: + while (true) { + if (new Date() - startTime > 5 * 60 * 1e3) { // 5 minutes wait max + yield* fail("timeout"); + return; + } + var processPaymentResponse = yield* processPayment(oauthToken, requestId); + + switch (processPaymentResponse.status) { + case 'success': + break while_in_progress; + case 'refused': + yield* fail(processPaymentResponse.error); + return; + case 'ext_auth_required': + yield* fail("необходимо подтвердить авторизацию по технологии 3D-Secure"); + return; + case 'in_progress': + yield delay(processPaymentResponse.next_retry); + break; + default: + yield delay(1000); + } + + } + + + // success! + var orderModule = require(this.order.module); + yield* orderModule.onSuccess(this.order); + + + self.redirect(self.getOrderSuccessUrl()); + + } catch (e) { + if (e instanceof URIError) { + yield* fail(e.message); + return; + } else if (e instanceof SyntaxError) { + yield* fail("некорректный ответ платёжной системы"); + return; + } else { + throw e; + } + } + + /* jshint -W106 */ + function* fail(reason) { + self.transaction.status = Transaction.STATUS_FAIL; + self.transaction.statusMessage = reason; + + yield self.transaction.persist(); + + self.redirectToOrder(); + } + + + function* requestOauthToken(code) { + + // request oauth token + var options = { + method: 'POST', + form: { + code: code, + client_id: config.payments.modules.yandexmoney.clientId, + grant_type: 'authorization_code', + redirect_uri: config.payments.modules.yandexmoney.redirectUri + '?transactionNumber=' + self.transaction.number, + client_secret: config.payments.modules.yandexmoney.clientSecret + }, + url: 'https://sp-money.yandex.ru/oauth/token' + }; + + + yield self.transaction.log('request oauth/token', options); + + var response = yield request(options); + + yield self.transaction.log('response oauth/token', response.body); + + return JSON.parse(response.body); + } + + // request payment + // return return request_id + function* requestPayment(oauthToken) { + var options = { + method: 'POST', + form: { + pattern_id: 'p2p', + to: config.payments.modules.yandexmoney.purse, + amount: self.transaction.amount, + comment: 'оплата по счету ' + self.transaction.number, + message: 'оплата по счету ' + self.transaction.number, + identifier_type: 'account' + }, + headers: { + 'Authorization': 'Bearer ' + oauthToken + }, + url: 'https://money.yandex.ru/api/request-payment' + }; + + yield self.transaction.log('request api/request-payment', options); + + var response = yield request(options); + yield self.transaction.log('response api/request-payment', response.body); + + return JSON.parse(response.body); + } + + function* processPayment(oauthToken, requestId) { + var options = { + method: 'POST', + form: { + request_id: requestId + }, + headers: { + 'Authorization': 'Bearer ' + oauthToken + }, + url: 'https://money.yandex.ru/api/process-payment' + }; + + yield self.transaction.log('request api/process-payment', options); + + var response = yield request(options); + yield self.transaction.log('response api/process-payment', response.body); + + return JSON.parse(response.body); + } + + +}; + +function throwResponseError(response) { + var message; + + if (response.error && response.error_description) { + message = '[' + response.error + '] ' + response.error_description; + } else if (response.error) { + message = response.error; + } else { + message = "детали ошибки не указаны"; + } + + throw new URIError(message); +} + +function delay(ms) { + return function(callback) { + setTimeout(callback, ms); + }; +} diff --git a/hmvc/payments/yandexmoney/index.js b/hmvc/payments/yandexmoney/index.js new file mode 100644 index 000000000..741de07fe --- /dev/null +++ b/hmvc/payments/yandexmoney/index.js @@ -0,0 +1,9 @@ +const config = require('config'); +const jade = require('jade'); +const path = require('path'); + +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.renderForm = require('./renderForm'); diff --git a/hmvc/payments/yandexmoney/renderForm.js b/hmvc/payments/yandexmoney/renderForm.js new file mode 100644 index 000000000..868baa511 --- /dev/null +++ b/hmvc/payments/yandexmoney/renderForm.js @@ -0,0 +1,18 @@ +const jade = require('jade'); +const config = require('config'); +const path = require('path'); + +module.exports = function* (transaction) { + + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + clientId: config.payments.modules.yandexmoney.clientId, + redirectUri: config.payments.modules.yandexmoney.redirectUri, + purse: config.payments.modules.yandexmoney.purse, + transactionNumber: transaction.number, + amount: transaction.amount + }); + +}; + + + diff --git a/hmvc/payments/yandexmoney/router.js b/hmvc/payments/yandexmoney/router.js new file mode 100644 index 000000000..bb51a3220 --- /dev/null +++ b/hmvc/payments/yandexmoney/router.js @@ -0,0 +1,9 @@ +var Router = require('koa-router'); + +var router = module.exports = new Router(); + +var back = require('./controller/back'); + +router.get('/back', back.get); + + diff --git a/hmvc/payments/yandexmoney/templates/form.jade b/hmvc/payments/yandexmoney/templates/form.jade new file mode 100644 index 000000000..5612a281c --- /dev/null +++ b/hmvc/payments/yandexmoney/templates/form.jade @@ -0,0 +1,7 @@ + +form(method="POST",action="https://sp-money.yandex.ru/oauth/authorize") + input(type="hidden",name="client_id",value=clientId) + input(type="hidden",name="response_type",value="code") + input(type="hidden",name="redirect_uri",value=(redirectUri + '?transactionNumber=' + transactionNumber)) + input(type="hidden",name="scope",value=('payment.to-account("' + purse + '").limit(,' + amount + ')')) + input(type="submit",value="Оплатить") diff --git a/hmvc/tutorial/controller/article.js b/hmvc/tutorial/controller/article.js new file mode 100644 index 000000000..1578b6a99 --- /dev/null +++ b/hmvc/tutorial/controller/article.js @@ -0,0 +1,48 @@ +const mongoose = require('mongoose'); +const Article = require('../models/article'); +const ArticleRenderer = require('../renderer/articleRenderer').ArticleRenderer; +const treeUtil = require('lib/treeUtil'); +const jade = require('jade'); +const _ = require('lodash'); + +exports.get = function *get(next) { + + const article = yield Article.findOne({ slug: this.params.slug }).exec(); + if (!article) { + yield next; + return; + } + + const renderer = new ArticleRenderer(); + const articleBody = yield renderer.render(article); + + const tree = yield Article.findTree(); + + var prevNext = treeUtil.findPrevNextById(tree, article._id); + + var prev = prevNext.prev; + if (prev) { + prev.url = Article.getUrlBySlug(prev.slug); + } + + var next = prevNext.next; + if (next) { + next.url = Article.getUrlBySlug(next.slug); + } + + var locals = { + title: article.title, + content: articleBody, + modified: article.modified, + prev: prev, + next: next + }; + _.assign(this.locals, locals); + + this.render(__dirname, "article", locals); + + //yield this.render("/hmvc/tutorial/template/article", ); + + +}; + diff --git a/hmvc/tutorial/controller/task.js b/hmvc/tutorial/controller/task.js new file mode 100644 index 000000000..182a261da --- /dev/null +++ b/hmvc/tutorial/controller/task.js @@ -0,0 +1,20 @@ +const mongoose = require('mongoose'); +const Task = require('../models/task'); +const TaskRenderer = require('../renderer/taskRenderer').TaskRenderer; + +exports.get = function *get(next) { + const task = yield Task.findOne({ + slug: this.params.slug + }).exec(); + + if (!task) { + yield next; + return; + } + + const renderer = new TaskRenderer(); + // todo: implement this + this.body = yield renderer.render(task); + +}; + diff --git a/hmvc/tutorial/index.js b/hmvc/tutorial/index.js new file mode 100644 index 000000000..05c3ee659 --- /dev/null +++ b/hmvc/tutorial/index.js @@ -0,0 +1,7 @@ +//var requireTree = require('require-tree'); +// +//requireTree('./model'); + +var router = require('./router'); + +exports.middleware = router.middleware(); diff --git a/hmvc/tutorial/models/article.js b/hmvc/tutorial/models/article.js new file mode 100644 index 000000000..b8b9cdced --- /dev/null +++ b/hmvc/tutorial/models/article.js @@ -0,0 +1,139 @@ +const mongoose = require('mongoose'); +const troop = require('mongoose-troop'); +const ObjectId = mongoose.Schema.Types.ObjectId; +const Schema = mongoose.Schema; +const config = require('config'); +const path = require('path'); +const Reference = require('./reference'); + +const schema = new Schema({ + title: { + type: String, + required: true + }, + + slug: { + type: String, + unique: true, + required: true, + index: true + }, + + content: { + type: String, + required: true + }, + + parent: { + type: ObjectId, + ref: 'Article', + index: true + }, + + weight: { + type: Number, + required: true + }, + + isFolder: { + type: Boolean, + required: true + } + +}); + +// all resources are here +schema.statics.resourceFsRoot = path.join(config.publicPath, 'article'); + +schema.statics.getResourceFsRootBySlug = function(slug) { + return path.join(schema.statics.resourceFsRoot, slug); +}; + +schema.statics.getResourceWebRootBySlug = function(slug) { + return '/article/' + slug; +}; + +schema.statics.getUrlBySlug = function(slug) { + return '/' + slug; +}; + +schema.methods.getResourceFsRoot = function() { + return schema.statics.getResourceFsRootBySlug(this.get('slug')); +}; + +schema.methods.getResourceWebRoot = function() { + return schema.statics.getResourceWebRootBySlug(this.get('slug')); +}; + +schema.methods.getUrl = function() { + return schema.statics.getUrlBySlug(this.get('slug')); +}; + +/** + * Returns {children: [whole article tree]} with nested children + * @returns {{children: Array}} + */ +schema.statics.findTree = function* () { + const Article = this; + var articles = yield Article.find({}).sort({weight: 1}).select('parent slug title weight isFolder').lean().exec(); + + // arrange by ids + var articlesById = {}; + for (var i=0; i anchor +var schema = new Schema({ + + article: { + type: Schema.Types.ObjectId, + ref: 'Article' + }, + + anchor: { + type: String, + index: true, + unique: true, + required: true + } + +}); + +schema.methods.getUrl = function() { + return this.article.getUrl() + '#' + this.anchor; +}; + +module.exports = mongoose.model('Reference', schema); diff --git a/hmvc/tutorial/models/task.js b/hmvc/tutorial/models/task.js new file mode 100644 index 000000000..3e9471e3b --- /dev/null +++ b/hmvc/tutorial/models/task.js @@ -0,0 +1,75 @@ +var mongoose = require('mongoose'); +var troop = require('mongoose-troop'); +var ObjectId = mongoose.Schema.Types.ObjectId; +var Schema = mongoose.Schema; +const config = require('config'); +const path = require('path'); + +var schema = new Schema({ + title: { + type: String, + required: true + }, + + importance: { + type: Number + }, + + slug: { + type: String, + unique: true, + required: true, + index: true + }, + + content: { + type: String, + required: true + }, + + solution: { + type: String, + required: true + }, + + parent: { + type: ObjectId, + ref: 'Article', + required: true, + index: true + } +}); + +// all resources are here +schema.statics.resourceFsRoot = path.join(config.publicPath, 'task'); + +schema.statics.getResourceFsRootBySlug = function(slug) { + return path.join(schema.statics.resourceFsRoot, slug); +}; + +schema.statics.getResourceWebRootBySlug = function(slug) { + return '/task/' + slug; +}; + +schema.statics.getUrlBySlug = function(slug) { + return '/' + slug; +}; + +schema.methods.getResourceFsRoot = function() { + return schema.statics.getResourceFsRootBySlug(this.get('slug')); +}; + +schema.methods.getResourceWebRoot = function() { + return schema.statics.getResourceWebRootBySlug(this.get('slug')); +}; + +schema.methods.getUrl = function() { + return schema.statics.getUrlBySlug(this.get('slug')); +}; + + +schema.plugin(troop.timestamp); + +module.exports = mongoose.model('Task', schema); + + diff --git a/hmvc/tutorial/renderer/articleRenderer.js b/hmvc/tutorial/renderer/articleRenderer.js new file mode 100644 index 000000000..7165c7bd5 --- /dev/null +++ b/hmvc/tutorial/renderer/articleRenderer.js @@ -0,0 +1,39 @@ +const HtmlTransformer = require('javascript-parser').HtmlTransformer; +const ReferenceResolver = require('./referenceResolver').ReferenceResolver; +const TaskResolver = require('./taskResolver').TaskResolver; +const BodyParser = require('javascript-parser').BodyParser; + +/** + * Can render many articles, keeping metadata + * @constructor + */ +function ArticleRenderer() { + this.metadata = {}; +} + +ArticleRenderer.prototype.render = function* (article) { + + const options = { + resourceFsRoot: article.getResourceFsRoot(), + resourceWebRoot: article.getResourceWebRoot(), + metadata: this.metadata, + trusted: true + }; + + // shift off the title header + const articleNode = yield new BodyParser(article.get('content'), options).parseAndWrap(); + articleNode.removeChild(articleNode.getChild(0)); + + const referenceResolver = new ReferenceResolver(articleNode); + yield referenceResolver.run(); + + const taskResolver = new TaskResolver(articleNode); + yield taskResolver.run(); + + const transformer = new HtmlTransformer(articleNode, options); + const content = yield transformer.run(); + return content; +}; + + +exports.ArticleRenderer = ArticleRenderer; \ No newline at end of file diff --git a/hmvc/tutorial/renderer/referenceResolver.js b/hmvc/tutorial/renderer/referenceResolver.js new file mode 100644 index 000000000..17481966f --- /dev/null +++ b/hmvc/tutorial/renderer/referenceResolver.js @@ -0,0 +1,62 @@ +const util = require('util'); +const TreeWalker = require('javascript-parser').TreeWalker; +const ReferenceNode = require('javascript-parser').ReferenceNode; +const CompositeTag = require('javascript-parser').CompositeTag; +const TextNode = require('javascript-parser').TextNode; +const ErrorTag = require('javascript-parser').ErrorTag; +const mongoose = require('mongoose'); +const Reference = require('../models/reference'); +const Article = require('../models/article'); +const Task = require('../models/task'); + +function ReferenceResolver(root) { + this.root = root; +} + + +ReferenceResolver.prototype.run = function* () { + + var treeWalker = new TreeWalker(this.root); + + var self = this; + yield treeWalker.walk(function*(node) { + if (!(node instanceof ReferenceNode)) return; + + // need + const referenceObj = yield self.resolve(node.ref); + + if (!referenceObj) { + return new ErrorTag('span', 'No such reference: ' + node.ref); + } + + var newNode = new CompositeTag('a', node.getChildren(), {href: referenceObj.url}); + + if (newNode.getChildren().length === 0) { + if (node.ref[0] == '#') { + newNode.appendChild(new TextNode(node.ref.slice(1))); + } else { + newNode.appendChild(new TextNode(referenceObj.title)); + } + } + + return newNode; + + }); +}; + +ReferenceResolver.prototype.resolve = function* (value) { + if (value[0] == '#') { + var ref = yield Reference.findOne({anchor: value.slice(1)}).populate('article', 'slug title').exec(); + return ref && { title: ref.article.title, url: ref.getUrl() }; + } + + if (value.indexOf('/task/') === 0) { + var task = yield Task.findOne({slug: value.slice('/task/'.length)}, 'slug title').exec(); + return task && {title: task.title, url: task.getUrl()}; + } + + var article = yield Article.findOne({slug: value.slice(1)}, 'slug title').exec(); + return article && {title: article.title, url: article.getUrl()}; +}; + +exports.ReferenceResolver = ReferenceResolver; diff --git a/hmvc/tutorial/renderer/taskRenderer.js b/hmvc/tutorial/renderer/taskRenderer.js new file mode 100644 index 000000000..ab40cec33 --- /dev/null +++ b/hmvc/tutorial/renderer/taskRenderer.js @@ -0,0 +1,86 @@ +const TreeWalker = require('javascript-parser').TreeWalker; +const HtmlTransformer = require('javascript-parser').HtmlTransformer; +const ReferenceResolver = require('./referenceResolver').ReferenceResolver; +const BodyParser = require('javascript-parser').BodyParser; +const TaskNode = require('javascript-parser').TaskNode; +const HeaderTag = require('javascript-parser').HeaderTag; +const TextNode = require('javascript-parser').TextNode; +const CompositeTag = require('javascript-parser').CompositeTag; + +/** + * Can render many articles, keeping metadata + * @constructor + */ +function TaskRenderer() { + this.metadata = {}; +} + +TaskRenderer.prototype.renderContent = function* (task) { + + const options = { + resourceFsRoot: task.getResourceFsRoot(), + resourceWebRoot: task.getResourceWebRoot(), + metadata: this.metadata, + trusted: true + }; + + // shift off the title header + const taskNode = yield new BodyParser(task.content, options).parseAndWrap(); + taskNode.removeChild(taskNode.getChild(0)); + + const referenceResolver = new ReferenceResolver(taskNode); + yield referenceResolver.run(); + + const transformer = new HtmlTransformer(taskNode, options); + const content = yield transformer.run(); + return content; +}; + + +TaskRenderer.prototype.renderSolution = function* (task) { + + const options = { + resourceFsRoot: task.getResourceFsRoot(), + resourceWebRoot: task.getResourceWebRoot(), + metadata: this.metadata, + trusted: true + }; + + // shift off the title header + const solutionNode = yield new BodyParser(task.solution, options).parseAndWrap(); + + const referenceResolver = new ReferenceResolver(solutionNode); + yield referenceResolver.run(); + + const newChildren = new CompositeTag(); + + var children = solutionNode.getChildren(); + + const solutionParts = []; + if (children[0] instanceof HeaderTag) { + // split into parts + var currentPart; + for (var i = 0; i < children.length; i++) { + var child = children[i]; + if (child instanceof HeaderTag) { + currentPart = { title: yield new HtmlTransformer(child, options).run(), content: [] }; + solutionParts.push(currentPart); + continue; + } + + currentPart.content.push(child); + } + } else { + solutionParts.push({title: "", content: children}); + } + + for (var i = 0; i < solutionParts.length; i++) { + var part = solutionParts[i]; + part.content = yield new HtmlTransformer(part.content, options).run(); + } + + return solutionParts; +}; + + +exports.TaskRenderer = TaskRenderer; diff --git a/hmvc/tutorial/renderer/taskResolver.js b/hmvc/tutorial/renderer/taskResolver.js new file mode 100644 index 000000000..35fb78d94 --- /dev/null +++ b/hmvc/tutorial/renderer/taskResolver.js @@ -0,0 +1,32 @@ +const util = require('util'); +const TreeWalker = require('javascript-parser').TreeWalker; +const TaskNode = require('javascript-parser').TaskNode; +const CompositeTag = require('javascript-parser').CompositeTag; +const TextNode = require('javascript-parser').TextNode; +const ErrorTag = require('javascript-parser').ErrorTag; +const mongoose = require('mongoose'); +const Reference = require('../models/reference'); +const Article = require('../models/article'); +const Task = require('../models/task'); + + +function TaskResolver(root) { + this.root = root; +} + +TaskResolver.prototype.run = function* () { + + var treeWalker = new TreeWalker(this.root); + + var self = this; + yield treeWalker.walk(function*(node) { + if (!(node instanceof TaskNode)) return; + + + + return new TextNode("
    [task " + node.slug + "]
    "); + + }); +}; + +exports.TaskResolver = TaskResolver; diff --git a/hmvc/tutorial/router.js b/hmvc/tutorial/router.js new file mode 100644 index 000000000..d3f854a22 --- /dev/null +++ b/hmvc/tutorial/router.js @@ -0,0 +1,10 @@ +var Router = require('koa-router'); + +var task = require('./controller/task'); +var article = require('./controller/article'); + +var router = module.exports = new Router(); + +router.get('/task/:slug', task.get); +router.get('/:slug', article.get); + diff --git a/hmvc/tutorial/tasks/import.js b/hmvc/tutorial/tasks/import.js new file mode 100644 index 000000000..7a4d441a7 --- /dev/null +++ b/hmvc/tutorial/tasks/import.js @@ -0,0 +1,401 @@ +const co = require('co'); +const fs = require('fs'); +const fse = require('fs-extra'); +const path = require('path'); +const config = require('config'); +const log = require('js-log')(); +const mongoose = require('mongoose'); +const Article = require('../models/article'); +const Reference = require('../models/reference'); +const Task = require('../models/task'); +const BodyParser = require('javascript-parser').BodyParser; +const HtmlTransformer = require('javascript-parser').HtmlTransformer; +const TreeWalker = require('javascript-parser').TreeWalker; +const HeaderTag = require('javascript-parser').HeaderTag; +const Imagemin = require('imagemin'); +const pngcrush = require('imagemin-pngcrush'); +const gm = require('gm'); +const svgo = require('imagemin-svgo'); + +// TODO: use htmlhint/jslint for html/js examples + + +/** + * + * @param options + * options.minify => true to enable minification + * options.root => the root to import from + * @returns {Function} + */ +module.exports = function(options) { + const root = options.root; + + return function(callback) { + + co(function* () { + + yield Article.destroy({}); + yield Task.destroy({}); + yield Reference.destroy({}); + + if (!options.updateFiles) { + fse.removeSync(Article.resourceFsRoot); + fse.removeSync(Task.resourceFsRoot); + + fs.mkdirSync(Article.resourceFsRoot); + fs.mkdirSync(Task.resourceFsRoot); + } + + + const subPaths = fs.readdirSync(root); + + for (var i = 0; i < subPaths.length; i++) { + var subPath = path.join(root, subPaths[i]); + if (fs.existsSync(path.join(subPath, 'index.md'))) { + yield importFolder(subPath, null); + } + } + + mongoose.disconnect(); + + })(callback); + }; + + function stripTags(str) { + return str.replace(/<\/?[a-z].*?>/gim, ''); + } + + function htmlTransform(roots, options) { + return new HtmlTransformer(roots, options).run(); + } + + function* importFolder(sourceFolderPath, parent) { + log.info("importFolder", sourceFolderPath); + + const contentPath = path.join(sourceFolderPath, 'index.md'); + const content = fs.readFileSync(contentPath, 'utf-8').trim(); + + const folderFileName = path.basename(sourceFolderPath); + + const data = { + isFolder: true, + content: content + }; + + if (parent) { + data.parent = parent._id; + } + + data.weight = parseInt(folderFileName); + data.slug = folderFileName.slice(3); + + const options = { + resourceFsRoot: sourceFolderPath, + resourceWebRoot: Article.getResourceWebRootBySlug(data.slug), + metadata: {}, + trusted: true + }; + + const parsed = yield new BodyParser(content, options).parse(); + + checkIfErrorsInParsed(parsed); + + const titleHeader = parsed[0]; + if (parsed[0].getType() != 'HeaderTag') { + throw new Error(contentPath + ": must start with a #Header"); + } + + data.title = stripTags(yield htmlTransform(titleHeader, options)); + + + const folder = new Article(data); + yield folder.persist(); + + const subPaths = fs.readdirSync(sourceFolderPath); + + for (var i = 0; i < subPaths.length; i++) { + if (subPaths[i] == 'index.md') continue; + + var subPath = path.join(sourceFolderPath, subPaths[i]); + + if (fs.existsSync(path.join(subPath, 'index.md'))) { + yield importFolder(subPath, folder); + } else if (fs.existsSync(path.join(subPath, 'article.md'))) { + yield importArticle(subPath, folder); + } else { + yield importResource(subPath, folder.getResourceFsRoot()); + } + } + + } + + // todo with incremental import: move to separate task? + function checkIfErrorsInParsed(parsed) { + const walker = new TreeWalker(parsed); + const errors = []; + + walker.walk(function*(node) { + if (node.getType() == 'ErrorTag') { + errors.push(node.text); + } + }); + if (errors.length) { + throw new Error("Errors: " + errors.join()); + } + } + + function* importArticle(articlePath, parent) { + log.info("importArticle", articlePath); + + const contentPath = path.join(articlePath, 'article.md'); + const content = fs.readFileSync(contentPath, 'utf-8').trim(); + + const articlePathName = path.basename(articlePath); + + const data = { + content: content, + isFolder: false + }; + + if (parent) { + data.parent = parent._id; + } + + data.weight = parseInt(articlePathName); + data.slug = articlePathName.slice(3); + + const options = { + resourceFsRoot: articlePath, + resourceWebRoot: Article.getResourceWebRootBySlug(data.slug), + metadata: { }, + trusted: true + }; + + const parsed = yield new BodyParser(content, options).parse(); + + checkIfErrorsInParsed(parsed); + + const titleHeader = parsed[0]; + if (parsed[0].getType() != 'HeaderTag') { + throw new Error(contentPath + ": must start with a #Header"); + } + + data.title = stripTags(yield htmlTransform(titleHeader, options)); + + // todo: updating: + // first check if references are unique, + // -> fail w/o deleting old article if not unique, + // delete old article & insert the new one & insert refs + const article = new Article(data); + yield article.persist(); + + + const refs = options.metadata.refs.toArray(); + const refThunks = refs.map(function(anchor) { + return new Reference({anchor: anchor, article: article._id}).persist(); + }); + + try { + // save all references in parallel + yield refThunks; + } catch (e) { + // something went wrong => we don't need an unfinished article + yield article.destroy(); // will kill it's refs too + throw e; + } + + + const subPaths = fs.readdirSync(articlePath); + + for (var i = 0; i < subPaths.length; i++) { + if (subPaths[i] == 'article.md') continue; + + var subPath = path.join(articlePath, subPaths[i]); + if (fs.existsSync(path.join(subPath, 'task.md'))) { + yield importTask(subPath, article); + } else { + // resources + yield importResource(subPath, article.getResourceFsRoot()); + } + } + + + } + + function* importResource(sourcePath, destDir) { + fse.ensureDirSync(destDir); + + log.info("importResource", sourcePath, destDir); + + const stat = fs.statSync(sourcePath); + const ext = getFileExt(sourcePath); + const destPath = path.join(destDir, path.basename(sourcePath)); + + if (stat.isFile()) { + if (ext == 'png' || ext == 'jpg' || ext == 'gif' || ext == 'svg') { + yield importImage(sourcePath, destDir); + return; + } + copySync(sourcePath, destPath); + } else if (stat.isDirectory()) { + fse.ensureDirSync(destPath); + const subPathNames = fs.readdirSync(sourcePath); + for (var i = 0; i < subPathNames.length; i++) { + var subPath = path.join(sourcePath, subPathNames[i]); + yield importResource(subPath, destPath); + } + + } else { + throw new Error("Unsupported file type at " + sourcePath); + } + + } + + function getFileExt(filePath) { + var ext = filePath.match(/\.([^.]+)$/); + return ext && ext[1]; + } + + function* importImage(srcPath, dstDir) { + log.info("importImage", srcPath, "to", dstDir); + const filename = path.basename(srcPath); + const dstPath = path.join(dstDir, filename); + + if (checkSameSizeFiles(srcPath, dstPath)) { + return; + } + + copySync(srcPath, dstPath); + yield minifyImage(dstPath); + + + var isRetina = /@2x\.[^.]+$/.test(srcPath); + + + if (isRetina) { + var normalResolutionFilename = filename.replace(/@2x(?=\.[^.]+$)/, ''); + var normalResolutionPath = path.join(dstDir, normalResolutionFilename); + + yield function(callback) { + gm(srcPath).resize("50%").write(normalResolutionPath, callback); + }; + yield minifyImage(normalResolutionPath); + } + + } + + function copySync(srcPath, dstPath) { + if (options.updateFiles && checkSameSizeFiles(srcPath, dstPath)) { + return; + } + + fse.copySync(srcPath, dstPath); + + } + + function* minifyImage(imagePath) { + if (!options.minify) return; + log.info("minifyImage", imagePath); + + var plugin; + switch (getFileExt(imagePath)) { + case 'jpg': + plugin = Imagemin.jpegtran({ progressive: true }); + break; + case 'gif': + plugin = Imagemin.gifsicle({ interlaced: true }); + break; + case 'png': + plugin = pngcrush({ reduce: true }); + break; + case 'svg': + plugin = svgo({}); + break; + } + + var imagemin = new Imagemin() + .src(imagePath) + .dest(imagePath) + .use(plugin); + + yield function(callback) { + imagemin.optimize(callback); + }; + + } + + function* importTask(taskPath, parent) { + log.info("importTask", taskPath); + + const contentPath = path.join(taskPath, 'task.md'); + const content = fs.readFileSync(contentPath, 'utf-8').trim(); + + const taskPathName = path.basename(taskPath); + + const data = { + content: content, + parent: parent._id, + slug: taskPathName + }; + + const options = { + resourceFsRoot: taskPath, + resourceWebRoot: Task.getResourceWebRootBySlug(data.slug), + metadata: {}, + trusted: true + }; + + const parsed = yield new BodyParser(content, options).parse(); + + checkIfErrorsInParsed(parsed); + + const titleHeader = parsed[0]; + if (parsed[0].getType() != 'HeaderTag') { + throw new Error(contentPath + ": must start with a #Header"); + } + + data.title = stripTags(yield htmlTransform(titleHeader, options)); + + data.importance = options.metadata.importance; + + const solutionPath = path.join(taskPath, 'solution.md'); + const solution = fs.readFileSync(solutionPath, 'utf-8').trim(); + data.solution = solution; + + + const parsedSolution = yield new BodyParser(solution, options).parse(); + checkIfErrorsInParsed(parsedSolution); + + const task = new Task(data); + yield task.persist(); + + const subPaths = fs.readdirSync(taskPath); + + for (var i = 0; i < subPaths.length; i++) { + if (subPaths[i] == 'task.md' || subPaths[i] == 'solution.md') continue; + + var subPath = path.join(taskPath, subPaths[i]); + yield importResource(subPath, task.getResourceFsRoot()); + } + + } + +}; + + +function checkSameSizeFiles(filePath1, filePath2) { + if (!fs.existsSync(filePath2)) return false; + + const stat1 = fs.statSync(filePath1); + if (!stat1.isFile()) { + throw new Error("not a file: " + filePath1); + } + + const stat2 = fs.statSync(filePath2); + if (!stat2.isFile()) { + throw new Error("not a file: " + filePath2); + } + + return stat1.size == stat2.size; + +} diff --git a/hmvc/tutorial/templates/article.jade b/hmvc/tutorial/templates/article.jade new file mode 100644 index 000000000..d41002e3c --- /dev/null +++ b/hmvc/tutorial/templates/article.jade @@ -0,0 +1,16 @@ +extends layouts/base + +block content + article.main + header.main__header.main__header_center + include blocks/breadcrumbs + h1.main__header-title!= title + span.main__header-comments + a(href='http://learn.javascript.ru/intro#disqus_thread') + include blocks/tutorial/top-navigation + != content + include blocks/tutorial/footer + include blocks/tutorial/bottom-navigation + .corrector Нашли опечатку на сайте? Что-то кажется странным? Выделите соответствующий текст и нажмите Ctrl+Enter + .sidebar + | TODO: SIDEBAR diff --git a/hmvc/tutorial/test/.jshintrc b/hmvc/tutorial/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/hmvc/tutorial/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/hmvc/tutorial/test/data/article.js b/hmvc/tutorial/test/data/article.js new file mode 100644 index 000000000..2d1f9b25d --- /dev/null +++ b/hmvc/tutorial/test/data/article.js @@ -0,0 +1,65 @@ +const mongoose = require('mongoose'); + +var ids = []; +for(var i=0; i<10; i++) ids[i] = new mongoose.Types.ObjectId(); + +exports.Article = [ + { + _id: ids[0], + title: "Article 1", + slug: "article-1", + content: "Content 1", + modified: new Date(2014, 0, 1), + isFolder: true, + weight: 0 + }, + { + _id: ids[1], + title: "Article 2", + slug: "article-2", + content: "Content 2", + modified: new Date(2014, 0, 2), + isFolder: true, + weight: 1 + }, + { + _id: ids[2], + title: "Article 1.1", + slug: "article-1.1", + content: "Content 1.1", + modified: new Date(2014, 0, 2), + isFolder: false, + weight: 0, + parent: ids[0] + }, + { + _id: ids[3], + title: "Article 1.2", + slug: "article-1.2", + content: "Content 1.2", + modified: new Date(2014, 0, 3), + isFolder: false, + weight: 1, + parent: ids[0] + }, + { + _id: ids[4], + title: "Article 2.1", + slug: "article-2.1", + content: "Content 2.1", + modified: new Date(2014, 0, 4), + isFolder: false, + weight: 0, + parent: ids[1] + }, + { + _id: ids[5], + title: "Article 2.2", + slug: "article-2.2", + content: "Content 2.2", + modified: new Date(2014, 0, 5), + isFolder: false, + weight: 1, + parent: ids[1] + } +]; diff --git a/hmvc/tutorial/test/model/article.js b/hmvc/tutorial/test/model/article.js new file mode 100644 index 000000000..0f296ebe4 --- /dev/null +++ b/hmvc/tutorial/test/model/article.js @@ -0,0 +1,46 @@ +require('app'); + +var dataUtil = require('lib/dataUtil'); +var mongoose = require('mongoose'); +var assert = require('assert'); +var path = require('path'); +var treeUtil = require('lib/treeUtil'); +var Article = require('../../models/article'); + +describe('Article', function() { + + before(function* () { + yield dataUtil.loadDb(path.join(__dirname, '../data/article')); + + }); + + + it('sets created & modified', function*() { + var date = new Date(); + + var article = new Article({ + title: "Title", + slug: 'slug', + content: "Content", + isFolder: false, + weight: 0 + }); + + yield article.persist(); + + assert(article.modified >= date); + + yield article.destroy(); + }); + + describe('findTree', function() { + + it("works", function* () { + var tree = yield Article.findTree(); + console.log(treeUtil.flattenArray(tree)); +// console.log(util.inspect(tree, {depth:100})); + }); + + }); + +}); diff --git a/hmvc/tutorial/test/renderer/articleRenderer.js b/hmvc/tutorial/test/renderer/articleRenderer.js new file mode 100644 index 000000000..d0cd9b835 --- /dev/null +++ b/hmvc/tutorial/test/renderer/articleRenderer.js @@ -0,0 +1,55 @@ +const app = require('app'); + +const ArticleRenderer = require('../../renderer/articleRenderer').ArticleRenderer; +const mongoose = require('config/mongoose'); +const Article = require('../../models/article'); + +describe("ArticleRenderer", function() { + + beforeEach(function* () { + yield Article.destroy(); + }); + + it("appens -2, -3... to header with same title", function* () { + + const article = new Article({ + title: "Title", + slug: "test", + content: "# Title\n\n## Title\n\nMy html *string*." + }); + const renderer = new ArticleRenderer(); + + const result = yield renderer.render(article); + result.replace(/\n/g, '').should.be.eql( + '

    Title

    My html string.

    ' + ); + + }); + + it("resolves references to article", function* () { + + yield new Article({ + title: "Title", + slug: "test", + weight: 0, + isFolder: false, + content: "# Title\n\n## Title\n\nMy html *string*." + }).persist(); + + const article = new Article({ + title: "Title", + slug: "test", + weight: 0, + isFolder: false, + content: "# Title\n\n[](/test)" + }); + + const renderer = new ArticleRenderer(); + + const result = yield renderer.render(article); + result.replace(/\n/g, '').should.be.eql( + '

    Title

    ' + ); + + }); +}); diff --git a/hmvc/tutorial/test/renderer/taskRenderer.js b/hmvc/tutorial/test/renderer/taskRenderer.js new file mode 100644 index 000000000..867110cb3 --- /dev/null +++ b/hmvc/tutorial/test/renderer/taskRenderer.js @@ -0,0 +1,57 @@ +const app = require('app'); + +const TaskRenderer = require('../../renderer/taskRenderer').TaskRenderer; +const mongoose = require('config/mongoose'); +const Task = require('../../models/task'); + +describe("TaskRenderer", function() { + + beforeEach(function* () { + yield Task.destroy(); + }); + + it("renderContent", function* () { + + const task = new Task({ + "content": "# Title\n\nContent", + "slug": "margin-between-pairs", + "title": "Title", + "importance": 4, + "solution": "..." + }); + const renderer = new TaskRenderer(); + + const result = yield renderer.renderContent(task); + + result.replace(/\n/g, '').should.be.eql('

    Content

    '); + }); + + + it("renderSolution", function* () { + + const task = new Task({ + "content": "# Title\n\nContent", + "slug": "margin-between-pairs", + "title": "Title", + "importance": 4, + "solution": "# Part 1\n\nContent 1\n\n# Part 2\n\nContent 2" + }); + const renderer = new TaskRenderer(); + + const result = yield renderer.renderSolution(task); +/* + result.replace(/\n/g, '').should.be.eql('
    +

    +

    Content 1

    +
    +
    +
    +

    +

    Content 2

    +
    ') + */ + console.log(result); + + + }); +}); diff --git a/lib/error/httpError.js b/lib/error/httpError.js deleted file mode 100755 index cd5a3dd78..000000000 --- a/lib/error/httpError.js +++ /dev/null @@ -1,19 +0,0 @@ -var util = require('util'); -var http = require('http'); - -// ошибки для выдачи посетителю -function HttpError(status, message) { - Error.apply(this, arguments); - Error.captureStackTrace(this, HttpError); - - this.status = status; - this.message = message || http.STATUS_CODES[status] || "Error"; -} - -util.inherits(HttpError, Error); -module.exports = HttpError; - -HttpError.prototype.name = 'HttpError'; - - - diff --git a/lib/error/index.js b/lib/error/index.js deleted file mode 100755 index 3522cd29c..000000000 --- a/lib/error/index.js +++ /dev/null @@ -1,9 +0,0 @@ -var fs = require('fs'); -var path = require('path'); - -var files = fs.readdirSync(__dirname); -files.forEach(function(file) { - if (file == 'index.js') return; - var errorClass = require(path.join(__dirname, file)); - module.exports[errorClass.prototype.name] = errorClass; -}); diff --git a/lib/hash.js b/lib/hash.js deleted file mode 100755 index 70dfb8177..000000000 --- a/lib/hash.js +++ /dev/null @@ -1,16 +0,0 @@ -var crypto = require('crypto'); - -var LEN = 128; - -/** - * Iterations. ~300ms - */ -var ITERATIONS = 12000; - -exports.createHash = function(password, salt) { - return crypto.pbkdf2Sync(password, salt, ITERATIONS, LEN); -}; - -exports.createSalt = function() { - return crypto.randomBytes(LEN).toString('base64'); -}; diff --git a/lib/log.js b/lib/log.js deleted file mode 100755 index 9ab59ca4a..000000000 --- a/lib/log.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Flexible logging wrapper around windston - * - * usage: - * var log = require('lib/log')(module) - * log.debug("Winston %s", "syntax"); - * - * enabling debug directly in the code: - * log.debugOn(); - * enabling debug for paths from CLI: - * DEBUG=path node app - * where path is calculated from the project root (where package.json resides) - * example: - * DEBUG=models/* node app - * DEBUG=models/*,lib/* node app - * exclusion: - * DEBUG=-models/user,models/* node app (to log all models except user) - */ - -var winston = require('winston'); -var path = require('path'); -var fs = require('fs'); - -var names = [], skips = []; - -(process.env.DEBUG || '') - .split(/[\s,]+/) - .forEach(function(name) { - name = name.replace('*', '.*?'); - if (name[0] === '-') { - skips.push(new RegExp('^' + name.substr(1) + '$')); - } else { - names.push(new RegExp('^' + name + '$')); - } - }); - -function findProjectRoot() { - - var dir = __dirname; - while (!fs.existsSync(path.join(dir, 'package.json'))) { - dir = path.dirname(dir); - } - - return path.normalize(dir); -} - -var projectRoot = findProjectRoot(); - -function getLogLevel(module) { - - var modulePath = module.filename.slice(projectRoot.length + 1); // models/user.js - modulePath = modulePath.replace(/\.js$/, ''); // models.user - - var logLevel = 'info'; - - var isSkipped = skips.some(function(re) { - return re.test(modulePath); - }); - - if (!isSkipped) { - var isIncluded = names.some(function(re) { - return re.test(modulePath); - }); - - if (isIncluded) logLevel = 'debug'; - } - - return logLevel; -} - -function getLogger(module) { - - var showPath = module.filename.split('/').slice(-2).join('/'); - - var logLevel = getLogLevel(module); - var logger = new winston.Logger({ - transports: [ - new winston.transports.Console({ - colorize: true, - level: logLevel, - label: showPath - }) - ] - }); - - logger.debugOn = function() { - Object.keys(this.transports).forEach(function(key) { - logger.transports[key].level = 'debug'; - }, this); - }; - - return logger; -} - -module.exports = getLogger; \ No newline at end of file diff --git a/lib/middleware/auth.js b/lib/middleware/auth.js deleted file mode 100755 index 44d2a9225..000000000 --- a/lib/middleware/auth.js +++ /dev/null @@ -1,26 +0,0 @@ -var HttpError = require('lib/error').HttpError; - -exports.mustBeAuthenticated = function(req, res, next) { - if (req.isAuthenticated()) { - next(); - } else { - return next(403); - } -}; - -exports.userIdMustBeCurrentUser = function(req, res, next) { - if (req.params.userId != req.user.id) { - next(403); - } else { - next(); - } -}; - -exports.mustBeAnonymous = function(req, res, next) { - if (req.isAuthenticated()) { - res.redirect('/'); - } else { - next(); - } - -}; \ No newline at end of file diff --git a/lib/middleware/sendHttpError.js b/lib/middleware/sendHttpError.js deleted file mode 100755 index ee8c6ebec..000000000 --- a/lib/middleware/sendHttpError.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = function(req, res, next) { - - res.sendHttpError = function(error) { - - res.status(error.status); - - var preferredType = res.req.accepts('html, json'); - if (preferredType == 'json') { - res.json(error); - } else { - res.render("error", {error: error}); - } - }; - - next(); - -}; \ No newline at end of file diff --git a/lib/mongoose.js b/lib/mongoose.js deleted file mode 100755 index 03b6b9f6b..000000000 --- a/lib/mongoose.js +++ /dev/null @@ -1,9 +0,0 @@ -var mongoose = require('mongoose'); -var config = require('config'); - -mongoose.connect(config.mongoose.uri, config.mongoose.options); - -module.exports = mongoose; - -// require('models'); - diff --git a/lib/passport.js b/lib/passport.js deleted file mode 100755 index 912f974cf..000000000 --- a/lib/passport.js +++ /dev/null @@ -1,42 +0,0 @@ -var passport = require("passport"); -var mongoose = require('mongoose'); -var LocalStrategy = require('passport-local').Strategy; - -var User = mongoose.models.User; - -passport.serializeUser(function(user, done) { - done(null, user.id); -}); - -passport.deserializeUser(function(id, done) { - User.findOne({ _id: id }, function(err, user) { - done(err, user); - }); -}); - -passport.use(new LocalStrategy({ - usernameField: 'email', - passwordField: 'password' - }, - function(email, password, done) { - if (!email) return done(null, false, { message: 'Please provide email.' }); - if (!password) return done(null, false, { message: 'Please provide password.' }); - - User.findOne({email: email}, function(err, user) { - if (err) return done(err); - if (!user) return done(null, false, { message: 'Non-registered email.' }); - - if (user.checkPassword(password)) { - return done(null, user); - } else { - done(null, false, { - message: 'Incorrect password.' - }); - } - }); - } -)); - - - -module.exports = passport; diff --git a/lib/sessionStore.js b/lib/sessionStore.js deleted file mode 100755 index 011e6a1c9..000000000 --- a/lib/sessionStore.js +++ /dev/null @@ -1,7 +0,0 @@ -var mongoose = require('mongoose'); -var session = require('express-session'); -var MongoStore = require('connect-mongo')(session); - -var sessionStore = new MongoStore({mongoose_connection: mongoose.connection}); - -module.exports = sessionStore; \ No newline at end of file diff --git a/middleware/auth.js b/middleware/auth.js deleted file mode 100755 index 44d2a9225..000000000 --- a/middleware/auth.js +++ /dev/null @@ -1,26 +0,0 @@ -var HttpError = require('lib/error').HttpError; - -exports.mustBeAuthenticated = function(req, res, next) { - if (req.isAuthenticated()) { - next(); - } else { - return next(403); - } -}; - -exports.userIdMustBeCurrentUser = function(req, res, next) { - if (req.params.userId != req.user.id) { - next(403); - } else { - next(); - } -}; - -exports.mustBeAnonymous = function(req, res, next) { - if (req.isAuthenticated()) { - res.redirect('/'); - } else { - next(); - } - -}; \ No newline at end of file diff --git a/middleware/sendHttpError.js b/middleware/sendHttpError.js deleted file mode 100755 index ee8c6ebec..000000000 --- a/middleware/sendHttpError.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = function(req, res, next) { - - res.sendHttpError = function(error) { - - res.status(error.status); - - var preferredType = res.req.accepts('html, json'); - if (preferredType == 'json') { - res.json(error); - } else { - res.render("error", {error: error}); - } - }; - - next(); - -}; \ No newline at end of file diff --git a/middleware/templateHelpers.js b/middleware/templateHelpers.js deleted file mode 100755 index cee101805..000000000 --- a/middleware/templateHelpers.js +++ /dev/null @@ -1,53 +0,0 @@ -var util = require('util'); - -function Block(name) { - this.members = []; -} - -Block.prototype.unshift = function(name) { - if (name instanceof Array) { - [].unshift.apply(this.members, name); - } else { - this.members.unshift(name); - } -}; - -Block.prototype.push = function(name) { - if (name instanceof Array) { - [].push.apply(this.members, name); - } else { - this.members.push(name); - } -}; - -function Script() { - Block.apply(this, arguments); -} -util.inherits(Script, Block); -Script.prototype.toString = function() { - var result = ''; - this.members.forEach(function(script) { - result += '\n'; - }); - return result; -}; - -function Style() { - Block.apply(this, arguments); -} -util.inherits(Style, Block); - -Style.prototype.toString = function() { - var result = ''; - this.members.forEach(function(style) { - result += '\n'; - }); - return result; -}; - - -module.exports = function(req, res, next) { - res.locals.script = new Script; - res.locals.style = new Style; - next(); -}; \ No newline at end of file diff --git a/mocha b/mocha new file mode 100644 index 000000000..7b01318e1 --- /dev/null +++ b/mocha @@ -0,0 +1,13 @@ +#!/bin/bash + +# this script is used to run all or standalone mocha scripts +# like this: +# ./mocha.sh +# OR +# ./mocha.sh test/unit/model/user.js + +# tried also gulp-mocha and node --harmony `which gulp` test, +# but it hangs after tests, not sure why, mocha.sh works fine so leave it as is +NODE_ENV=development ./gulp link-modules +NODE_ENV=test mocha --harmony $* + diff --git a/mocha.sh b/mocha.sh deleted file mode 100755 index 1f9e10e2b..000000000 --- a/mocha.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -NODE_ENV=test NODE_PATH=. mocha --timeout 10000 --require should --require test/env $* - diff --git a/models/index.js b/models/index.js deleted file mode 100644 index c484a6f15..000000000 --- a/models/index.js +++ /dev/null @@ -1,6 +0,0 @@ -var fs = require("fs"); - -fs.readdirSync(__dirname).forEach(function(file) { - if (file == 'index.js') return; - require("./" + file); -}); diff --git a/models/user.js b/models/user.js deleted file mode 100755 index 20f9db57c..000000000 --- a/models/user.js +++ /dev/null @@ -1,78 +0,0 @@ -var mongoose = require('lib/mongoose'); -var hash = require('lib/hash'); -var Schema = mongoose.Schema; - -var schema = Schema({ - username: { - type: String, - required: true, - unique: true - }, - email: { - type: String, - unique: true, - required: true, - index: true - }, - salt: String, - hash: String, - created: { - type: Date, - default: Date.now - }, - avatar: { - type: String, - default: '1.jpg' - } -}); - -schema.methods.toPublicObject = function() { - return { - username: this.get('username'), - email: this.get('email'), - avatar: this.get('avatar'), - following: this.get('following'), - created: this.get('created'), - messagesCount: this.get('messagesCount'), - id: this.id - }; -}; - -schema.virtual('password') - .set(function(password) { - this._plainPassword = password; - this.salt = hash.createSalt(); - this.hash = hash.createHash(password, this.salt); - }) - .get(function() { - return this._plainPassword; - }); - - -schema.statics.signup = function(username, email, password, done) { - var User = this; - - User.create({ - username: username, - email: email, - password: password, - avatar: (Math.random()*42^0) + '.jpg' - }, done); - -}; - -schema.methods.checkPassword = function(password) { - return hash.createHash(password, this.salt) == this.hash; -}; - -schema.path('email').validate(function(value) { - // wrap in new RegExp instead of /.../, to evade WebStorm validation errors (buggy webstorm) - return new RegExp('^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,5}$').test(value); -}, 'Please provide the correct e-mail.'); - -// all references using mongoose.model for safe recreation -// when I recreate model (for tests) => I can reload it from mongoose.model (single source of truth) -// exports are less convenient to update -mongoose.model('User', schema); - - diff --git a/modules/app.js b/modules/app.js new file mode 100644 index 000000000..74aead9c7 --- /dev/null +++ b/modules/app.js @@ -0,0 +1,96 @@ +"use strict"; + +require('lib/debug'); +const koa = require('koa'); +const log = require('js-log')(); +const config = require('config'); +const mongoose = require('config/mongoose'); +const app = koa(); + +// trust all headers from proxy +// X-Forwarded-Host +// X-Forwarded-Proto +// X-Forwarded-For -> ip +app.proxy = true; + +function requireSetup(path) { + // if debug is on => will log the middleware travel chain + if (process.env.NODE_ENV == 'development') { + app.use(function *(next) { + log.debug("-> setup " + path); + yield next; + log.debug("<- setup " + path); + }); + } + require(path)(app); +} + +// usually nginx will handle this before node +// that's why we put it at the top +requireSetup('setup/static'); + +// this middleware adds this.render method +// it is *before errorHandler*, because errors need this.render +requireSetup('setup/render'); + +// errors wrap everything +requireSetup('setup/errorHandler'); + +// this logger only logs HTTP status and URL +// before everything to make sure it log all +requireSetup('setup/accessLogger'); + +// before anything that may deal with body +requireSetup('setup/httpPostParser'); + +// right after parsing body, make sure we logged for development +requireSetup('setup/verboseLogger'); + +if (process.env.NODE_ENV == 'development') { +// app.verboseLogger.addPath('/:any*'); +} + +requireSetup('setup/session'); + +requireSetup('setup/passport'); + +requireSetup('setup/csrf'); + +requireSetup('setup/payments'); + +requireSetup('setup/router'); + +// wait for full app load and all associated warm-ups to finish +app.waitBoot = function* () { + yield function(callback) { + mongoose.waitConnect(callback); + }; +}; + + +// adding middlewares only possible before app.run +app.run = function*() { + yield* app.waitBoot(); + + // every test may use app.run() + // app will only start the 1st time + if (!app.isListening) { + yield function(callback) { + app.listen(config.port, config.host, function() { + log.info('App listen %s:%d', config.host, config.port); + callback(); + }); + }; + app.isListening = true; + } +}; + +// for supertest(app), it wants app.address().port +app.address = function() { + return { + port: config.port + }; +}; + +module.exports = app; + diff --git a/modules/config/index.js b/modules/config/index.js new file mode 100644 index 000000000..b13637cab --- /dev/null +++ b/modules/config/index.js @@ -0,0 +1,50 @@ +if (!process.env.NODE_ENV) { + process.env.NODE_ENV = 'development'; +} + +if (process.env.NODE_ENV == 'development' && process.env.DEV_TRACE) { + // @see https://github.com/AndreasMadsen/trace + require('trace'); // active long stack trace + require('clarify'); // Exclude node internal calls from the stack +} + + +var path = require('path'); +var fs = require('fs'); + +var secretPath = fs.existsSync(path.join(__dirname, 'secret.js')) ? './secret' : './secret.template'; +var secret = require(secretPath); + +module.exports = { + "port": process.env.PORT || 3000, + "host": process.env.HOST || '0.0.0.0', + "domain": "stage.javascript.ru", + "mongoose": { + "uri": "mongodb://localhost/" + (process.env.NODE_ENV == 'test' ? "js_test" : "js"), + "options": { + "server": { + "socketOptions": { + "keepAlive": 1 + }, + "poolSize": 5 + } + } + }, + session: { + keys: [secret.sessionKey] + }, + payments: secret.payments, + template: { + options: { + 'cache': process.env.NODE_ENV != 'development' + } + }, + crypto: { + hash: { + length: 128, + // may be slow(!): iterations = 12000 take ~60ms to generate strong password + iterations: process.env.NODE_ENV == 'prod' ? 12000 : 1 + } + }, + publicPath: path.join(process.cwd(), 'www') +}; diff --git a/modules/config/mongoose.js b/modules/config/mongoose.js new file mode 100644 index 000000000..567a3aa14 --- /dev/null +++ b/modules/config/mongoose.js @@ -0,0 +1,106 @@ +/** + * This file must be required at least ONCE. + * After it's done, one can use require('mongoose') + * + * In web-app: this is done at init phase + * In tests: in mocha.opts + * In gulpfile: in beginning + */ + +var mongoose = require('mongoose'); +var path = require('path'); +var fs = require('fs'); +var log = require('js-log')(); +var autoIncrement = require('mongoose-auto-increment'); + +//mongoose.set('debug', true); + +var config = require('config'); +var _ = require('lodash'); + +mongoose.connect(config.mongoose.uri, config.mongoose.options); + +autoIncrement.initialize(mongoose.connection); + +// bind context now for thunkify without bind +_.bindAll(mongoose.connection); +_.bindAll(mongoose.connection.db); + +// plugin from https://github.com/LearnBoost/mongoose/issues/1859 +// yield.. .persist() or .destroy() for generators instead of save/remove +// mongoose 3.10 will not need that (!) +mongoose.plugin(function(schema) { + schema.methods.persist = function(body) { + var model = this; + + return function(callback) { + if (body) model.set(body); + model.save(callback); + }; + }; + schema.methods.destroy = function() { + var model = this; + + return function(callback) { + model.remove(callback); + }; + }; + + schema.statics.destroy = function(query) { + return function(callback) { + this.remove(query, callback); + }.bind(this); + }; +}); + +mongoose.waitConnect = function(callback) { + if (mongoose.connection.readyState == 1) { + setImmediate(callback); + } else { + // we wait either for an error + // OR + // for a successful connection + mongoose.connection.on("connected", onConnected); + mongoose.connection.on("error", onError); + } + + function onConnected() { + log.debug("Mongoose has just connected"); + cleanUp(); + callback(); + } + + function onError(err) { + log.debug('Failed to connect to DB', err); + cleanUp(); + callback(err); + } + + function cleanUp() { + mongoose.connection.removeListener("connected", onConnected); + mongoose.connection.removeListener("error", onError); + } +}; + +module.exports = mongoose; + +/* +// models may want lib/mongoose that's why we require them AFTER module.exports = mongoose + + +// read ALL models NOW, so that any hmvc app may require a model of another app +requireModels(); + +function requireModels() { + var root = process.cwd(); + + requireTree(path.join(root, 'model')); + + var hmvcApps = fs.readdirSync(path.join(root, 'hmvc')); + hmvcApps.forEach(function(hmvcDir) { + var modelPath = path.join(path.join(root), 'hmvc', hmvcDir, 'model'); + if (fs.existsSync(modelPath)) requireTree(modelPath); + }); +} + +*/ diff --git a/modules/config/secret.template.js b/modules/config/secret.template.js new file mode 100644 index 000000000..9674cb1db --- /dev/null +++ b/modules/config/secret.template.js @@ -0,0 +1,8 @@ +// this file contains all passwords etc, +// should not be in repo + +exports.SESSION_KEY = "KillerIsJim"; + +exports.payments = { + modules: {} +}; \ No newline at end of file diff --git a/modules/expiring-download/index.js b/modules/expiring-download/index.js new file mode 100644 index 000000000..418e8b804 --- /dev/null +++ b/modules/expiring-download/index.js @@ -0,0 +1 @@ +exports.ExpiringDownloadLink = require('./models/expiringDownloadLink'); diff --git a/modules/expiring-download/models/expiringDownloadLink.js b/modules/expiring-download/models/expiringDownloadLink.js new file mode 100644 index 000000000..8047f0c61 --- /dev/null +++ b/modules/expiring-download/models/expiringDownloadLink.js @@ -0,0 +1,21 @@ +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +// files use /files/ dir +var schema = new Schema({ + // use _id as unique token + + // path from /files/ + relativePath: { + type: String, + required: true + }, + created: { + type: Date, + default: Date.now, + expires: '3d' // link must die in 3 days + } +}); + +module.exports = mongoose.model('ExpiringDownloadLink', schema); + diff --git a/modules/httpPostParser/index.js b/modules/httpPostParser/index.js new file mode 100644 index 000000000..dfb61dc3e --- /dev/null +++ b/modules/httpPostParser/index.js @@ -0,0 +1,75 @@ +'use strict'; + +const koaFormidable = require('koa-formidable'); +const _ = require('lodash'); +const pathToRegexp = require('path-to-regexp'); +const log = require('js-log')(); + +/** + * Wrapper around koa-bodyparser + * allows to set per-path options which are used in middleware + * usage: + * + * app.httpPostParser = new HttpPostParser + * app.use(app.bodyParser.middleware()) + * ... + * app.httpPostParser.addPathOptions('/upload/path', {bytesExpected: 1e10}); + * @constructor + */ +function HttpPostParser() { + this.pathOptions = []; +} + +// options should be an object { path: string|regexp, options } +HttpPostParser.prototype.addPathOptions = function(path, options) { + if (path instanceof RegExp) { + this.pathOptions.push({path: path, options: options}); + } else if (typeof path == 'string') { + this.pathOptions.push({path: pathToRegexp(path), options: options}); + } else { + throw new Error("unsupported path type: " + path); + } +}; + +HttpPostParser.prototype.middleware = function() { + + var self = this; + var optionsDefault = { bytesExpected: 1e7 }; + + return function* (next) { + var options = Object.create(optionsDefault); + + for (var i = 0; i < self.pathOptions.length; i++) { + var path = self.pathOptions[i].path; + log.debug("test " + this.req.url + " against " + path); + if (path.test(this.req.url)) { + log.debug("found options", self.pathOptions[i].options); + _.assign(options, self.pathOptions[i].options); + break; + } + } + + // if request file too big, don't start accepting it + // in normal situation, large uploads have the header and get stopped here + if (this.get('content-length')) { + var bytesExpected = parseInt(this.get('content-length'), 10); + + if (bytesExpected > options.bytesExpected) { + this.status = 413; + this.body = 'Request entity too large: ' + bytesExpected + ' > ' + options.bytesExpected; + return; + } + } + + // safety: + // even if a bad person did not supply content-length, + // formidable will not read more than options.bytesExpected + + + yield* koaFormidable(options).call(this, next); + + }; +}; + + +module.exports = HttpPostParser; diff --git a/modules/httpPostParser/test/post.js b/modules/httpPostParser/test/post.js new file mode 100644 index 000000000..b58572fcb --- /dev/null +++ b/modules/httpPostParser/test/post.js @@ -0,0 +1,45 @@ +const app = require('app'); +const supertest = require('supertest'); +const should = require('should'); + +describe("HttpPostParser", function() { + + before(function* () { + + // if app.isListening, then we can't add our middleware + should.not.exist(app.isListening); + + app.use(function* echoBody(next) { + if ('/test/http-post-parser' != this.path) return yield next; + this.body = this.request.body; + }); + + yield app.run(); + + }); + + it("parses body", function(done) { + + var message = { name: 'Manny', species: 'cat' }; + supertest(app) + .post('/test/http-post-parser') + .send(message) + .end(function(error, res) { + res.body.should.be.eql(message); + done(error); + }); + + }); + + it("dies when the file is too big", function(done) { + + // fixme: superagent console.warns: double callback! + // seems like a bug in superagent: https://github.com/visionmedia/superagent/issues/351 + supertest(app) + .post('/test/http-post-parser') + .send({big: new Array(1e7).join(' ')}) + .expect(413, done); + + }); + +}); diff --git a/modules/lib/csrf.js b/modules/lib/csrf.js new file mode 100644 index 000000000..dfc7c0447 --- /dev/null +++ b/modules/lib/csrf.js @@ -0,0 +1,55 @@ +const pathToRegexp = require('path-to-regexp'); +const log = require('js-log')(); + +function Csrf() { + this.ignorePaths = []; +} + + +// csrf.addIgnore adds a path into "disabled csrf" list +Csrf.prototype.addIgnorePath = function(path) { + if (path instanceof RegExp) { + this.ignorePaths.push(path); + } else if (typeof path == 'string') { + this.ignorePaths.push(pathToRegexp(path)); + } else { + throw new Error("unsupported path type: " + path); + } +}; + +Csrf.prototype.middleware = function() { + var self = this; + + return function*(next) { + // skip these methods + if (this.method === 'GET' || this.method === 'HEAD' || this.method === 'OPTIONS') { + return yield* next; + } + + var checkCsrf = true; + for (var i = 0; i < self.ignorePaths.length; i++) { + var path = self.ignorePaths[i]; + log.debug("test " + this.req.url + " against " + path); + if (path.test(this.req.url)) { + log.debug("match found, disable csrf check"); + checkCsrf = false; + break; + } + } + + // If test check CSRF only when "X-Test-Csrf" header is set + if (process.env.NODE_ENV == 'test') { + if (!this.get('X-Test-Csrf')) { + checkCsrf = false; + } + } + + if (checkCsrf) { + this.assertCSRF(this.request.body); + } + + yield* next; + }; +}; + +exports.Csrf = Csrf; diff --git a/modules/lib/dataUtil.js b/modules/lib/dataUtil.js new file mode 100644 index 000000000..3af7ba54f --- /dev/null +++ b/modules/lib/dataUtil.js @@ -0,0 +1,120 @@ +"use strict"; + +var mongoose = require('mongoose'); +var log = require('js-log')(); +var co = require('co'); +var thunk = require('thunkify'); +var glob = require('glob'); + +var db; +//log.debugOn(); + +function *createEmptyDb() { + + function *open() { + + if (mongoose.connection.readyState == 1) { // connected + return mongoose.connection.db; + } + + yield thunk(mongoose.connection.on)('open'); + + return mongoose.connection.db; + } + + function *clearDatabase() { + + var collections = yield thunk(db.collectionNames)(); + + var collectionNames = collections + .map(function(collection) { + var collectionName = collection.name.slice(db.databaseName.length + 1); + if (collectionName.indexOf('system.') === 0) { + return null; + } + return collectionName; + }) + .filter(function(name) { + return name; + }); + + yield collectionNames.map(function(name) { + log.debug("drop ", name); + return thunk(db.dropCollection)(name); + }); + + } + + // wait till indexes are complete, especially unique + // required to throw errors + function *ensureIndexes() { + + yield mongoose.modelNames().map(function(modelName) { + var model = mongoose.models[modelName]; + return thunk(model.ensureIndexes.bind(model))(); + }); + + } + + // ensure that capped collections are actually capped + function *ensureCapped() { + + yield mongoose.modelNames().map(function(modelName) { + var model = mongoose.models[modelName]; + var schema = model.schema; + if (!schema.options.capped) return; + + return thunk(db.command)({convertToCapped: model.collection.name, size: schema.options.capped}); + }); + } + + log.debug("co"); + + db = yield open(); + log.debug("open"); + + yield clearDatabase(); + log.debug("clear"); + + yield ensureIndexes(); + log.debug('indexes'); + + yield ensureCapped(); + log.debug('capped'); + +} + +// not using pow-mongoose-fixtures, becuae it fails with capped collections +// it calls remove() on them => everything dies +function *loadDb(dataFile) { + yield* createEmptyDb(); + var modelsData = require(dataFile); + + yield Object.keys(modelsData).map(function(modelName) { + return loadModel(modelName, modelsData[modelName]); + }); +} + +// fixture file must make sure that the model is loaded! +function *loadModel(name, data) { + + var Model = mongoose.models[name]; + + yield data.map(function(itemData) { + var model = new Model(itemData); + log.debug("save", itemData); + return model.persist(); + }); + + log.debug("loadModel is done"); +} + +exports.loadDb = loadDb; +exports.createEmptyDb = createEmptyDb; + +/* + Usage: + co(loadDb('sampleDb'))(function(err) { + if (err) throw err; + mongoose.connection.close(); + });*/ diff --git a/modules/lib/debug.js b/modules/lib/debug.js new file mode 100644 index 000000000..9601c077c --- /dev/null +++ b/modules/lib/debug.js @@ -0,0 +1,18 @@ + + +/* + some crap to log & isolate steps for stackless errors + p() will print next number + */ +if (process.env.NODE_ENV == 'development') { + + global.p = function() { + var stack = new Error().stack.split("\n")[2].trim(); + console.log("----> " + global.p.counter++ + " at " + stack); + }; + global.p.counter = 1; +} else { + global.p = function() { + + }; +} diff --git a/modules/lib/treeUtil.js b/modules/lib/treeUtil.js new file mode 100644 index 000000000..46ebf9661 --- /dev/null +++ b/modules/lib/treeUtil.js @@ -0,0 +1,71 @@ +function walkArray(node, visitor) { + + for (var i = 0; i < node.children.length; i++) { + var treeNode = node.children[i]; + visitor(treeNode); + if (treeNode.children) { + walkArray(treeNode, visitor); + } + } + +} + +function flattenArray(root) { + + const flatten = []; + + walkArray(root, function(node) { + flatten.push(node); + }); + + return flatten; + +} + +exports.walkArray = walkArray; + + +exports.flattenArray = flattenArray; + +exports.findPrevNextById = function(root, id) { + + var flatten = flattenArray(root); + + var node, i; + + for (i = 0; i < flatten.length; i++) { + node = flatten[i]; + if (node._id.toString() == id.toString()) break; + } + + if (node._id.toString() != id.toString()) { + throw new Error("Id is not in the tree: " + id); + } + + var nextNum = i + 1, next; + while(true) { + if (!flatten[nextNum]) break; // array finished, no next, sorry + if (!flatten[nextNum].parent) break; // next item is a root of another tree, search finished + if (flatten[nextNum].isFolder) { // next item is a folder, go down + nextNum++; + continue; + } + next = flatten[nextNum]; + break; + } + + var prevNum = i - 1, prev; + while(true) { + if (!flatten[prevNum]) break; + if (!flatten[prevNum].parent) break; + if (flatten[prevNum].isFolder) { + prevNum--; + continue; + } + prev = flatten[prevNum]; + break; + } + + + return { next: next, prev: prev }; +}; diff --git a/modules/lib/verboseLogger.js b/modules/lib/verboseLogger.js new file mode 100644 index 000000000..5272e9c11 --- /dev/null +++ b/modules/lib/verboseLogger.js @@ -0,0 +1,56 @@ +const pathToRegexp = require('path-to-regexp'); +const log = require('js-log')(); + +function VerboseLogger() { + this.logPaths = []; +} + + +// csrf.addIgnore adds a path into "disabled csrf" list +VerboseLogger.prototype.addPath = function(path) { + if (path instanceof RegExp) { + this.logPaths.push(path); + } else if (typeof path == 'string') { + this.logPaths.push(pathToRegexp(path)); + } else { + throw new Error("unsupported path type: " + path); + } +}; + +VerboseLogger.prototype.middleware = function() { + var self = this; + + return function*(next) { + + var shouldLog = false; + for (var i = 0; i < self.logPaths.length; i++) { + var path = self.logPaths[i]; + log.debug("test " + this.req.url + " against " + path); + if (path.test(this.req.url)) { + log.debug("match found, will log all"); + shouldLog = true; + break; + } + } + + if (shouldLog) { + self.log(this); + } + + yield* next; + }; +}; + +VerboseLogger.prototype.log = function(context) { + + for (var name in context.req.headers) { + console.log(name + ": " + context.req.headers[name]); + } + + if (context.request.body) { + console.log(context.request.body); + } + +}; + +exports.VerboseLogger = VerboseLogger; diff --git a/setup/logger.js b/modules/setup/accessLogger.js old mode 100755 new mode 100644 similarity index 96% rename from setup/logger.js rename to modules/setup/accessLogger.js index 1674e7e1c..09d0c8c9d --- a/setup/logger.js +++ b/modules/setup/accessLogger.js @@ -3,4 +3,4 @@ const logger = require('koa-logger'); module.exports = function(app) { app.use(logger()); -}; \ No newline at end of file +}; diff --git a/modules/setup/csrf.js b/modules/setup/csrf.js new file mode 100644 index 000000000..01c817737 --- /dev/null +++ b/modules/setup/csrf.js @@ -0,0 +1,12 @@ +const csrf = require('koa-csrf'); +const Csrf = require('lib/csrf').Csrf; + +// every request gets different this._csrf to use in POST +// but ALL tokens are valid +module.exports = function(app) { + csrf(app); + + app.csrf = new Csrf(); + + app.use(app.csrf.middleware()); +}; diff --git a/modules/setup/errorHandler.js b/modules/setup/errorHandler.js new file mode 100644 index 000000000..7e41690b7 --- /dev/null +++ b/modules/setup/errorHandler.js @@ -0,0 +1,94 @@ +'use strict'; + +const config = require('config'); +const log = require('js-log')(); +const escapeHtml = require('escape-html'); + +function renderError(error) { + /*jshint -W040 */ + this.status = error.status; + + var preferredType = this.accepts('html', 'json'); + + if (preferredType == 'json') { + this.body = error; + } else { + this.render("error", {error: error}); + } +} + +function renderDevError(error) { + /*jshint -W040 */ + this.status = 500; + + var preferredType = this.accepts('html', 'json'); + + if (preferredType == 'json') { + this.body = error; + } else { + var stack = (error.stack || '') + .split('\n').slice(1) + .map(function(v){ return '
  • ' + escapeHtml(v).replace(/ /g, '  ') + '
  • '; }).join(''); + + this.type = 'text/html; charset=utf-8'; + this.body = "

    " + error.message + "

      "+stack+ "
    "; + } + +} + +module.exports = function(app) { + + app.use(function*(next) { + this.renderError = renderError; + this.renderDevError = renderDevError; + + try { + yield* next; + } catch (err) { + + if (err.status) { + // user-level error + if (process.env.NODE_ENV == 'development') { + console.log(err); + } + this.renderError(err); + } else { + + // if error is "call stack too long", then log.error(err) is not verbose + // so I cast it to string + log.error(err.toString()); + log.error(err.stack); + + this.set('X-Content-Type-Options', 'nosniff'); + + if (process.env.NODE_ENV == 'development') { + this.renderDevError(err); + } else { + this.renderError({status: 500, message: "Internal Error"}); + } + } + } + + }); + + // this middleware handles error BEFORE ^^^ + // rewrite mongoose wrong mongoose parameter -> 400 (not 500) + app.use(function* rewriteCastError(next) { + + try { + yield next; + } catch (err) { + + if (err.name == 'CastError') { + // malformed or absent mongoose params + if (process.env.NODE_ENV != 'development') { + this.throw(400); + } + } + + throw err; + } + + }); + +}; diff --git a/modules/setup/httpPostParser.js b/modules/setup/httpPostParser.js new file mode 100644 index 000000000..d83007ed6 --- /dev/null +++ b/modules/setup/httpPostParser.js @@ -0,0 +1,11 @@ +'use strict'; + +const HttpPostParser = require('httpPostParser'); +const _ = require('lodash'); + +module.exports = function(app) { + + app.httpPostParser = new HttpPostParser(); + app.use(app.httpPostParser.middleware()); + +}; diff --git a/modules/setup/passport.js b/modules/setup/passport.js new file mode 100644 index 000000000..bee347347 --- /dev/null +++ b/modules/setup/passport.js @@ -0,0 +1,11 @@ +const mongoose = require('mongoose'); +const passport = require('koa-passport'); +const config = require('config'); + + +module.exports = function(app) { + + app.use(passport.initialize()); + app.use(passport.session()); + +}; diff --git a/modules/setup/payments.js b/modules/setup/payments.js new file mode 100644 index 000000000..16d4d9fbe --- /dev/null +++ b/modules/setup/payments.js @@ -0,0 +1,7 @@ +const mongoose = require('mongoose'); +const payments = require('payments'); +const config = require('config'); + +module.exports = function(app) { + app.use(payments.populateContextMiddleware); +}; diff --git a/modules/setup/render.js b/modules/setup/render.js new file mode 100644 index 000000000..076e22829 --- /dev/null +++ b/modules/setup/render.js @@ -0,0 +1,125 @@ +'use strict'; + +const moment = require('moment'); +const Parser = require('jade').Parser; +const util = require('util'); +const path = require('path'); +const config = require('config'); +const fs = require('fs'); +const log = require('js-log')(); +const jade = require('jade'); +const _ = require('lodash'); +const assert = require('assert'); + +//log.debugOn(); + +module.exports = function render(app) { + app.use(function *(next) { + var ctx = this; + + this.locals = _.assign({}, config.template.options); + this.locals.moment = moment; + + // warning! + // _.assign does NOT copy defineProperty + Object.defineProperty(this.locals, "csrf", { + get: function() { + var csrf = ctx.csrf; + assert(csrf); + return csrf; + } + }); + + // this.locals.debug causes jade to dump function + /* jshint -W087 */ + this.locals.deb = function() { + debugger; + }; + + // render(__dirname, 'article', {}) -- 3 args + // render(__dirname, 'article') -- 2 args + // render('article', {}) -- 2 args + // render('article') + this.render = function(templateDir, templatePath, locals) { + if (arguments.length == 2) { + if (typeof templatePath == 'object') { + templatePath = arguments[0]; + templateDir = process.cwd(); + locals = arguments[1]; + } else { + locals = {}; + } + } + + if (arguments.length == 1) { + locals = {}; + templateDir = process.cwd(); + } + + // this jade parser knows templateDir through closure + function JadeParser(str, filename, options) { + Parser.apply(this, arguments); + } + + util.inherits(JadeParser, Parser); + + JadeParser.prototype.resolvePath = function(templatePath, purpose) { + + if (templatePath[0] == '.') { + if (!this.filename) { + throw new Error('the "filename" option is required to use "' + purpose + '" with "relative" paths'); + } + + // files like article.html are included in a special way (Filter) + templatePath = path.join(path.dirname(this.filename), templatePath) + (path.extname(templatePath) ? '' : '.jade'); + //console.log("Resolve to ", path); + return templatePath; + } + + log.debug("resolvePathUp " + templateDir + " " + templatePath); + + return resolvePathUp(templateDir, templatePath + '.jade'); + }; + + var loc = Object.create(this.locals); + + var parseLocals = { + parser: JadeParser + }; + + _.assign(loc, parseLocals, locals); + +// console.log(loc); + var file = resolvePathUp(templateDir, templatePath + '.jade'); + if (!file) { + throw new Error("Template file not found: " + templatePath + " (in dir " + templateDir + ") "); + } + log.debug("render file " + file); + this.body = jade.renderFile(file, loc); + }; + + + yield next; + }); + +}; + +function resolvePathUp(templateDir, templateName) { + + templateDir = path.resolve(templateDir); + var top = path.resolve(process.cwd()); + + while (templateDir != path.dirname(top)) { + var template = path.join(templateDir, 'templates', templateName); + log.debug("-- try path", template); + if (fs.existsSync(template)) { + log.debug("-- found"); + return template; + } + log.debug("-- skipped"); + templateDir = path.dirname(templateDir); + } + + log.debug("-- failed", templateDir, top); + return null; +} diff --git a/modules/setup/router.js b/modules/setup/router.js new file mode 100644 index 000000000..33ec2f3af --- /dev/null +++ b/modules/setup/router.js @@ -0,0 +1,55 @@ +'use strict'; + +var mount = require('koa-mount'); +var compose = require('koa-compose'); + +module.exports = function(app) { + + + app.use(mount('/', require('frontpage').middleware)); + + if (process.env.NODE_ENV == 'development') { + app.use(mount('/markup', require('markup').middleware)); + } + + app.use(mount('/auth', require('auth').middleware)); + app.csrf.addIgnorePath('/auth/login/:any*'); + + app.use(mount('/getpdf', require('getpdf').middleware)); + + app.use(mount('/payments', require('payments').middleware)); + app.csrf.addIgnorePath('/payments/:any*'); + app.verboseLogger.addPath('/payments/:any*'); + + /* + app.use(mount('/webmoney', compose([payment.middleware, require('webmoney').middleware]))); + app.csrf.addIgnorePath('/webmoney/:any*'); + app.verboseLogger.addPath('/webmoney/:any*'); + + app.use(mount('/yandexmoney', compose([payment.middleware, require('yandexmoney').middleware]))); + app.csrf.addIgnorePath('/yandexmoney/:any*'); + app.verboseLogger.addPath('/yandexmoney/:any*'); + + app.use(mount('/payanyway', compose([payment.middleware, require('payanyway').middleware]))); + app.csrf.addIgnorePath('/payanyway/:any*'); + app.verboseLogger.addPath('/payanyway/:any*'); + + app.use(mount('/paypal', compose([payment.middleware, require('paypal').middleware]))); + app.csrf.addIgnorePath('/paypal/:any*'); + app.verboseLogger.addPath('/paypal/:any*'); +*/ + + // stick to bottom to detect any not-yet-processed /:slug + app.use(mount('/', require('tutorial').middleware)); + + // by default if the router didn't find anything => it yields to next middleware + // so I throw error here manually + app.use(function* (next) { + yield* next; + + if (this.status == 404) { + this.throw(404); // still not found? pass to default errorHandler + } + }); + +}; diff --git a/setup/session.js b/modules/setup/session.js old mode 100755 new mode 100644 similarity index 76% rename from setup/session.js rename to modules/setup/session.js index 518e915e9..ec8b7e6b7 --- a/setup/session.js +++ b/modules/setup/session.js @@ -1,5 +1,5 @@ -const mongoose = require('lib/mongoose'); -const session = require('koa-sess'); +const mongoose = require('mongoose'); +const session = require('koa-generic-session'); const mongooseStore = require('koa-session-mongoose'); const config = require('config'); @@ -14,4 +14,4 @@ module.exports = function(app) { app.keys = config.session.keys; // needed for cookie-signing -}; \ No newline at end of file +}; diff --git a/modules/setup/static.js b/modules/setup/static.js new file mode 100644 index 000000000..d69adc7fe --- /dev/null +++ b/modules/setup/static.js @@ -0,0 +1,36 @@ +'use strict'; + + +const favicon = require('koa-favicon'); +const send = require('koa-send'); +const path = require('path'); + + +/** + * koa-static is a thin wrapper around koa-send + * Here we statically send all paths with extension. + * + * ...And if we fail, there is no big-nice-error-screen which is slow to render + * just a simple default error message + * @param app + */ +module.exports = function(app) { + + app.use(favicon()); + + app.use(function*(next) { + var opts = { + root: 'www', + index: 'index.html' + }; + + if (this.idempotent && path.extname(this.path) !== '') { + yield send(this, this.path, opts); + return; + } + + yield* next; + + }); + +}; diff --git a/modules/setup/verboseLogger.js b/modules/setup/verboseLogger.js new file mode 100644 index 000000000..fe35f10ec --- /dev/null +++ b/modules/setup/verboseLogger.js @@ -0,0 +1,9 @@ +const VerboseLogger = require('lib/verboseLogger').VerboseLogger; + + +module.exports = function(app) { + + app.verboseLogger = new VerboseLogger(); + app.use(app.verboseLogger.middleware()); + +}; diff --git a/out/native/d8 b/out/native/d8 new file mode 100644 index 000000000..21166c891 Binary files /dev/null and b/out/native/d8 differ diff --git a/package.json b/package.json index 76bedfc03..098f65036 100755 --- a/package.json +++ b/package.json @@ -3,46 +3,119 @@ "version": "0.0.1", "private": true, "scripts": { - "start": "NODE_ENV=production NODE_PATH=. node --harmony --harmony_generators ./bin/www", - "dev": "NODE_ENV=development DEBUG=server:* NODE_PATH=. supervisor --harmony --debug --ignore node_modules ./bin/www", - "test": "NODE_ENV=test DEBUG=server:* NODE_PATH=. supervisor --harmony --debug --ignore node_modules ./bin/www" + "prod": "NODE_ENV=production node --harmony ./bin/www", + "dev": "NODE_ENV=development supervisor --harmony --debug --ignore node_modules ./bin/www", + "debug": "NODE_ENV=development supervisor --harmony --debug-brk --ignore node_modules ./bin/www", + "test": "NODE_ENV=test supervisor --harmony --debug --ignore node_modules ./bin/www", + "fixperms": "sudo chown -R `id -u` . ~/.npm* ~/.node-gyp" }, + "precommit": "NODE_ENV=development node --harmony `which gulp` pre-commit", "dependencies": { + "MD5": "*", "body-parser": "*", + "brfs": "*", "co": "*", - "koa-views": "*", + "escape-html": "*", + "event-stream": "*", + "factor-bundle": "*", + "fs-extra": "*", + "glob": "*", + "gm": "*", + "gulp-cache": "*", + "gulp-concat": "*", + "gulp-debug": "*", + "gulp-dir-sync": "*", + "gulp-if": "*", + "gulp-ignore": "*", + "gulp-jshint-cache": "*", + "gulp-load-plugins": "*", + "gulp-newer": "*", + "gulp-notify": "*", + "gulp-plumber": "*", + "gulp-rimraf": "*", + "gulp-stylus": "*", + "gulp-util": "*", + "gulp.spritesmith": "*", + "imagemin": "*", + "imagemin-pngcrush": "*", + "imagemin-svgo": "*", "jade": "*", + "javascript-gulp-task-lint": "*", + "javascript-parser": "*", + "jquery": "*", + "js-log": "*", "koa": "*", "koa-bodyparser": "*", + "koa-compose": "*", + "koa-csrf": "*", "koa-favicon": "*", + "koa-formidable": "*", + "koa-generic-session": "*", "koa-logger": "*", + "koa-mount": "*", + "koa-passport": "*", + "koa-request": "*", "koa-router": "*", - "koa-sess": "*", + "koa-send": "*", "koa-session-mongoose": "*", "koa-static": "*", + "koa-views": "*", "lodash": "*", - "mongoose": "*", + "map-stream": "*", + "moment": "*", + "mongoose": "3.8", + "mongoose-auto-increment": "*", + "mongoose-troop": "git://github.com/iliakan/mongoose-troop", + "nib": "*", + "nodemailer": "*", + "nodemailer-ses-transport": "*", "passport": "*", - "winston": "*" + "passport-local": "*", + "path-to-regexp": "*", + "prismjs": "git://github.com/LeaVerou/prism#gh-pages", + "stylus": "*", + "svgutils": "*", + "through2": "*", + "thunkify": "*", + "vinyl-fs": "*", + "vinyl-source-stream": "*", + "winston": "*", + "yargs": "*" }, "devDependencies": { - "javascript-brunch": "*", - "css-brunch": "*", - "uglify-js-brunch": "*", - "clean-css-brunch": "*", - "stylus-brunch": "*", - "autoprefixer-brunch": "*", - "supervisor": "*" + "better-assert": "^1.0.1", + "browserify": "*", + "clarify": "*", + "co-mocha": "*", + "gulp": "*", + "gulp-autoprefixer": "*", + "gulp-flatten": "*", + "gulp-jshint": "*", + "gulp-livereload": "*", + "gulp-mocha": "*", + "gulp-sourcemaps": "*", + "gulp-stylus-sprite": "*", + "gulp-supervisor": "*", + "lazypipe": "*", + "mocha": "*", + "node-notifier": "*", + "should": "*", + "sinon": "*", + "superagent": "*", + "supertest": "*", + "supervisor": "*", + "trace": "*", + "watchify": "*" }, "engines": { - "node" : "0.11.12" + "node": ">=0.11.13" }, "engineStrict": true, "repository": { "type": "git", "url": "https://github.com/iliakan/javascript-nodejs.git" }, - "author": "Ilya Kantor and other contributors listed in git repo", + "author": "Ilya Kantor and other contributors in git repo", "license": "CC BY-NC-SA 3.0", "bugs": { "url": "https://github.com/iliakan/javascript-nodejs/issues" diff --git a/routes/index.js b/routes/index.js deleted file mode 100755 index a4f0a24bc..000000000 --- a/routes/index.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -var log = require('lib/log')(module); -var config = require('config'); - -module.exports = function(app) { - app.get('/', require('controllers/frontpage').get); -}; \ No newline at end of file diff --git a/setup/bodyParser.js b/setup/bodyParser.js deleted file mode 100755 index a935f5285..000000000 --- a/setup/bodyParser.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -const bodyParser = require('koa-bodyparser'); - -module.exports = function(app) { - app.use(bodyParser()); -}; \ No newline at end of file diff --git a/setup/errors.js b/setup/errors.js deleted file mode 100755 index 36d9633ba..000000000 --- a/setup/errors.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -var config = require('config'); -var log = require('lib/log')(module); -var HttpError = require('error').HttpError; - -module.exports = function(app) { - - /* TODO: rewrite this express-style error handling in koa -@see https://github.com/koajs/koa/wiki/Error-Handling - - this.use(function(req, res, next) { - next(404); - }); - - this.use(function(err, req, res, next) { - if (typeof err == 'number') { - err = new HttpError(err); - } - - if (err.name == 'CastError') { - // malformed or absent mongoose params - if (process.env.NODE_ENV == 'development') { - log.error(err); - } - res.sendHttpError(new HttpError(400)); - return; - } - - if (err instanceof HttpError) { - res.sendHttpError(err); - } else { - // if error is "call stack too long", then log.error(err) is not verbose - // so I cast it to string - log.error(err.toString()); - - if (process.env.NODE_ENV == 'development') { - errorhandler()(err, req, res, next); - } else { - res.sendHttpError(new HttpError(500)); - } - } - }); -*/ -}; \ No newline at end of file diff --git a/setup/render.js b/setup/render.js deleted file mode 100644 index 8e3a82133..000000000 --- a/setup/render.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -var views = require('koa-views'); -var config = require('config'); - -module.exports = function render(app) { - app.use(views(config.template.path, config.template.options)); -}; - diff --git a/setup/router.js b/setup/router.js deleted file mode 100755 index 07707cede..000000000 --- a/setup/router.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -const router = require('koa-router'); -module.exports = function(app) { - - app.use(router(app)); - -}; \ No newline at end of file diff --git a/setup/static.js b/setup/static.js deleted file mode 100755 index 34b1ed198..000000000 --- a/setup/static.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -const serve = require('koa-static'); -const favicon = require('koa-favicon'); - -module.exports = function(app) { - app.use(serve('www')); - app.use(favicon()); -}; \ No newline at end of file diff --git a/tasks/browserify.js b/tasks/browserify.js new file mode 100644 index 000000000..7269b1941 --- /dev/null +++ b/tasks/browserify.js @@ -0,0 +1,85 @@ +const gp = require('gulp-load-plugins')(); +const gulp = require('gulp'); +const source = require('vinyl-source-stream'); +const gutil = require('gulp-util'); +const watchify = require('watchify'); +const browserify = require('browserify'); +var Notification = require('node-notifier'); +var assert = require('assert'); +var _ = require('lodash'); +var path = require('path'); + +// TODO: add uglify if not development +function makeBundler(options) { + + // dst has same name as (single) src + var opts = _.assign({}, options, { + debug: (process.env.NODE_ENV === 'development'), + cache: {}, + packageCache: {}, + fullPaths: true + }); + + var bundler = browserify(opts); + bundler.rebundle = function() { + this.bundle() + .on('error', function(e) { + gutil.log(e.message); + new Notification().notify({ + message: e + }); + }) + .pipe(source(path.basename(this._options.dst))) + .pipe(gulp.dest(path.dirname(this._options.dst))); + }; + bundler.on('update', bundler.rebundle); + bundler.on('log', function(msg) { + gutil.log("browserify: " + msg); + }); + + if (options.externals) { + for (var i = 0; i < options.externals.length; i++) { + var external = options.externals[i]; + bundler.external(external); + } + } + + if (process.env.NODE_ENV == 'development') { + bundler = watchify(bundler); + } + + return bundler; +} + +module.exports = function() { + + return function(callback) { + + var vendor = ['jquery']; + + var bundler = makeBundler({ + entries: [], + dst: './www/js/vendor.js', + require: vendor + }); + + bundler.rebundle(); + + var bundler = makeBundler({ + entries: './app/js/head.js', + dst: './www/js/head.js' + }); + + bundler.rebundle(); + + + var bundler = makeBundler({ + entries: './app/js/main.js', + dst: './www/js/main.js', + externals: vendor + }); + + + bundler.rebundle(); + }; +}; diff --git a/tasks/browserifyClean.js b/tasks/browserifyClean.js new file mode 100644 index 000000000..f27e68a69 --- /dev/null +++ b/tasks/browserifyClean.js @@ -0,0 +1,11 @@ +const fse = require('fs-extra'); + +module.exports = function(options) { + + return function(callback) { + fse.removeSync(options.dst); + fse.mkdirsSync(options.dst); + callback(); + }; +}; + diff --git a/tasks/compileCss.js b/tasks/compileCss.js new file mode 100644 index 000000000..d09c5aeb9 --- /dev/null +++ b/tasks/compileCss.js @@ -0,0 +1,19 @@ +const gulp = require('gulp'); +const gp = require('gulp-load-plugins')(); + +module.exports = function(options) { + + return function() { + + return gulp.src(options.src) + // without plumber if stylus emits PluginError, it will disappear at the next step + // plumber propagates it down the chain + .pipe(gp.plumber({errorHandler: gp.notify.onError("<%= error.message %>")})) + .pipe(gp.stylus({use: [require('nib')()]})) + .pipe(gp.autoprefixer("last 1 version")) + .pipe(gulp.dest(options.dst)); + }; + + +}; + diff --git a/tasks/linkModules.js b/tasks/linkModules.js new file mode 100644 index 000000000..2d7f3aacf --- /dev/null +++ b/tasks/linkModules.js @@ -0,0 +1,55 @@ +var fs = require('fs'); +var glob = require('glob'); +var path = require('path'); +var gutil = require('gulp-util'); + +// Ensures the existance of a symlink linkDst -> linkSrc +// returns true if link was created +// returns false if link exists alread (and is correct) +// throws error if conflict (another file or link by that name) +function ensureSymlinkSync(linkSrc, linkDst) { + var lstat; + try { + lstat = fs.lstatSync(linkDst); + } catch (e) { + } + + if (lstat) { + if (!lstat.isSymbolicLink()) { + throw new Error("Conflict: path exist and is not a link: " + linkDst); + } + + var oldDst = fs.readlinkSync(linkDst); + if (oldDst == linkSrc) { + return false; // already exists + } + // kill old link! + fs.unlinkSync(linkDst); + } + + fs.symlinkSync(linkSrc, linkDst); + return true; +} + +module.exports = function(options) { + + return function(callback) { + var modules = []; + options.src.forEach(function(pattern) { + modules = modules.concat(glob.sync(pattern)); + }); + + for (var i = 0; i < modules.length; i++) { + var moduleToLinkRelPath = modules[i]; // hmvc/auth + var moduleToLinkName = path.basename(moduleToLinkRelPath); // auth + var linkSrc = path.join('..', moduleToLinkRelPath); + var linkDst = path.join('node_modules', moduleToLinkName); + if (ensureSymlinkSync(linkSrc, linkDst)) { + gutil.log(linkSrc + " -> " + linkDst); + } + } + callback(); + }; + +}; + diff --git a/tasks/lint.js b/tasks/lint.js new file mode 100644 index 000000000..56f200236 --- /dev/null +++ b/tasks/lint.js @@ -0,0 +1,12 @@ + +const gulp = require('gulp'); + +module.exports = function(options) { + return function(callback) { + if (process.env.NODE_ENV == 'development') { + gulp.watch(options.src, ['lint']); + } else { + callback(); + } + }; +}; diff --git a/tasks/lintOnce.js b/tasks/lintOnce.js new file mode 100644 index 000000000..768fb5ac7 --- /dev/null +++ b/tasks/lintOnce.js @@ -0,0 +1,8 @@ +const gp = require('gulp-load-plugins')(); + +module.exports = function(options) { + + return function(callback) { + return gp.jshintCache(options).apply(this, arguments); + }; +}; diff --git a/tasks/livereload.js b/tasks/livereload.js new file mode 100644 index 000000000..5defc1b5f --- /dev/null +++ b/tasks/livereload.js @@ -0,0 +1,20 @@ +var livereload = require('gulp-livereload'); +var gulp = require('gulp'); +var gutil = require('gulp-util'); + +// options.watch must NOT be www/**, because that breaks (why?!?) supervisor reloading +// www/**/*.* is fine +module.exports = function(options) { + + // listen to changes after 7 secs, to let initial jobs finish + // no one is going to livereload right now anyway + return function(callback) { + livereload.listen(); + setTimeout(function() { + gutil.log("livereload: deferred listen on change " + options.watch); + gulp.watch(options.watch).on('change', livereload.changed); + }, 7000); + }; +}; + + diff --git a/tasks/loadDb.js b/tasks/loadDb.js new file mode 100644 index 000000000..51a8361bd --- /dev/null +++ b/tasks/loadDb.js @@ -0,0 +1,33 @@ +var fs = require('fs'); +var co = require('co'); +var path = require('path'); +var gutil = require('gulp-util'); +var dataUtil = require('lib/dataUtil'); +var mongoose = require('config/mongoose'); + +module.exports = function() { + return function(callback) { + + var args = require('yargs') + .usage("Path to DB is required.") + .demand(['db']) + .argv; + + var dbPath = path.join(process.cwd(), args.db); + + gutil.log("loading db " + dbPath); + + co(function*() { + + yield* dataUtil.loadDb(dbPath); + + gutil.log("loaded db " + dbPath); + })(function(err) { + if (err) throw err; + mongoose.disconnect(); + callback(); + }); + + }; +}; + diff --git a/tasks/sprite.js b/tasks/sprite.js new file mode 100644 index 000000000..549adf4af --- /dev/null +++ b/tasks/sprite.js @@ -0,0 +1,9 @@ +const gp = require('gulp-load-plugins')(); + +module.exports = function(options) { + + return function(callback) { + return gp.stylusSprite(options).apply(this, arguments); + }; +}; + diff --git a/tasks/supervisor.js b/tasks/supervisor.js new file mode 100644 index 000000000..e1c4c50f4 --- /dev/null +++ b/tasks/supervisor.js @@ -0,0 +1,16 @@ + +const gp = require('gulp-load-plugins')(); + +module.exports = function(options) { + + return function(callback) { + gp.supervisor(options.cmd, { + args: [], + watch: options.watch, + pollInterval: 100, + extensions: [ "js" ], + debug: true, + harmony: true + }); + }; +}; diff --git a/tasks/syncCssImages.js b/tasks/syncCssImages.js new file mode 100644 index 000000000..fd8e06697 --- /dev/null +++ b/tasks/syncCssImages.js @@ -0,0 +1,17 @@ +const fse = require('fs-extra'); +const gp = require('gulp-load-plugins')(); +const gulp = require('gulp'); + +module.exports = function(options) { + + return function(callback) { + + fse.ensureDirSync(options.dst); + + return gulp.src(options.src) + .pipe(gp.flatten()) + .pipe(gp.newer(options.dst)) + .pipe(gulp.dest(options.dst)); + + }; +}; diff --git a/tasks/syncResources.js b/tasks/syncResources.js new file mode 100644 index 000000000..7e21a24e7 --- /dev/null +++ b/tasks/syncResources.js @@ -0,0 +1,23 @@ +const fse = require('fs-extra'); +const gp = require('gulp-load-plugins')(); + +module.exports = function(resources) { + + return function(callback) { + + for (var src in resources) { + var dst = resources[src]; + + fse.removeSync(dst); + + if (process.env.NODE_ENV == 'development') { + fse.mkdirsSync(dst); + gp.dirSync(src, dst); + } else { + fse.copySync(src, dst); + } + } + + if (process.env.NODE_ENV != 'development') callback(); + }; +}; diff --git a/templates/blocks/breadcrumbs.jade b/templates/blocks/breadcrumbs.jade new file mode 100644 index 000000000..eae4df9c5 --- /dev/null +++ b/templates/blocks/breadcrumbs.jade @@ -0,0 +1,9 @@ +ol.breadcrumbs + li.breadcrumbs__item + a(href='/') Главная + li.breadcrumbs__item + a(href='/tutorial') Учебник + li.breadcrumbs__item + a(href='/getting-started') Общая информация + li.breadcrumbs__item + | Введение в JavaScript diff --git a/templates/blocks/footer.jade b/templates/blocks/footer.jade new file mode 100644 index 000000000..4ed5a30c2 --- /dev/null +++ b/templates/blocks/footer.jade @@ -0,0 +1 @@ +.page-footer Hello from footer \ No newline at end of file diff --git a/templates/blocks/head.jade b/templates/blocks/head.jade new file mode 100644 index 000000000..52240e5db --- /dev/null +++ b/templates/blocks/head.jade @@ -0,0 +1,10 @@ +doctype html +html + meta(charset='UTF-8') + title= title + link(href='http://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700|Open+Sans+Condensed:700&subset=latin,latin-ext,cyrillic,cyrillic-ext' rel='stylesheet' type='text/css') + link(href='/stylesheets/base.css',rel='stylesheet') + script(src='/js/head.js') + //- link(href='../app/assets/stylesheets/base.css' rel='stylesheet') + //if lte IE 9 + //- link(href='../app/assets/stylesheets/base.ie.css' rel='stylesheet') diff --git a/templates/blocks/lesson.jade b/templates/blocks/lesson.jade new file mode 100644 index 000000000..e69de29bb diff --git a/templates/blocks/lessons.jade b/templates/blocks/lessons.jade new file mode 100644 index 000000000..cddee8ad3 --- /dev/null +++ b/templates/blocks/lessons.jade @@ -0,0 +1,8 @@ +- var lessons = []; +- lessons.push({title: 'Переменные', }) +- lessons.push({title: 'Переменные'}) + +.lessons + for [1..10] + .lessons__lesson-wrap + include lesson diff --git a/templates/blocks/navbar.jade b/templates/blocks/navbar.jade new file mode 100644 index 000000000..69f41f265 --- /dev/null +++ b/templates/blocks/navbar.jade @@ -0,0 +1,40 @@ +nav.navbar + ul.navbar__sections + li.navbar__sections-item.active.study + a(href='/study') Обучение + li.navbar__sections-item.blogs + a(href='/blogs') Блоги + li.navbar__sections-item.qa + a(href='/qa') Вопрос / ответ + li.navbar__sections-item.dropdown.dropdown_nav.down-right.inherit-min-width.dropdown_open_hover.reference + a.dropdown__toggle.dropdown__toggle_nav(href='http://javascript.ru/manual') Справочник + ul.dropdown__content.dropdown__content_nav + li + a(href='http://es5.javascript.ru/') Стандарт ES5 + li + a(href='/referencd') Справочник JS + li + a(href='/php') PHP-функции + li + a(href='/book') Книги + li.navbar__sections-item.events + a(href='/event') События + li.navbar__sections-item.job + a(href='/job') Работа + li.navbar__sections-item.sandbox + a(href='/play') Песочница + li.navbar__sections-item.chat + a(href='/chat') Чат + li.navbar__sections-item.search.dropdown.down-left + button.dropdown__toggle + .dropdown__content.dropdown-search + .search-form + form#search-block-form(action='/intro', method='post', accept-charset='UTF-8') + // TODO: проверить, точно ли нужно? Не ошибка? + div + .search-form__query + input#edit-search-block-form-1.form-text(type='text', maxlength='128', name='search_block_form', value='') + button(type='submit') Найти + input#w4vSP09jvn-orNjyHpBepTXLSn6qFxyehtYKuZbWWv4(type='hidden', name='form_build_id', value='w4vSP09jvn-orNjyHpBepTXLSn6qFxyehtYKuZbWWv4') + input#edit-search-block-form-form-token(type='hidden', name='form_token', value='O9WnzduxDr7Fwfy6eG4XL06_OilwEwBIsJDKhduRG0w') + input#edit-search-block-form(type='hidden', name='form_id', value='search_block_form') diff --git a/templates/blocks/scripts.jade b/templates/blocks/scripts.jade new file mode 100644 index 000000000..05b76cf1a --- /dev/null +++ b/templates/blocks/scripts.jade @@ -0,0 +1,2 @@ +script(src='/js/vendor.js') +script(src='/js/main.js') diff --git a/templates/blocks/social-aside.jade b/templates/blocks/social-aside.jade new file mode 100644 index 000000000..e6bd981b9 --- /dev/null +++ b/templates/blocks/social-aside.jade @@ -0,0 +1,10 @@ +.social.aside + a.social__anchor-up(href='#page') Наверх + a.social__soc(href='https://plus.google.com/share?url=http://design.javascript.ru/intro', target='_blank') + img.soc-icon.google(src='/img/x.gif', width='24', height='24', alt='g+', title='Поделиться в гугл+') + a.social__soc(href='http://www.facebook.com/sharer/sharer.php?s=100&p[url]=http://design.javascript.ru/intro', target='_blank') + img.soc-icon.facebook(src='/img/x.gif', width='24', height='24', alt='f', title='Поделиться в фейсбуке') + a.social__soc(href='https://twitter.com/share?url=http://design.javascript.ru/intro', target='_blank') + img.soc-icon.twitter(src='/img/x.gif', width='24', height='24', alt='t', title='Поделиться в твитере') + a.social__soc(href='http://vkontakte.ru/share.php?url=http://design.javascript.ru/intro', target='_blank') + img.soc-icon.vk(src='/img/x.gif', width='24', height='24', alt='v', title='Поделиться во вконтакте') \ No newline at end of file diff --git a/templates/blocks/task-block.jade b/templates/blocks/task-block.jade new file mode 100644 index 000000000..7903b43ab --- /dev/null +++ b/templates/blocks/task-block.jade @@ -0,0 +1,25 @@ +-var url = "/task/vyzov-na-meste" +-var title = "Вызов «на месте»" +-var importance = 4 +-var content = "

    Content

    Content

    " +-var solution = [{title:"",content:"

    Текст

    "}] + +section.important.important_ok + header.important__header + span.important__type Задание: + h3.important__title + a.important__task-link(href=url) + u!=title + span.important__importance(title="Насколько эта задача важна для освоения материала, от 1 до 5") + | Важность: + = ' ' + importance + + .important__content + != content + each part in solution + .spoiler.closed + button.spoiler__button + u!= part.title || "Решение" + .spoiler__content + u!= part.content + diff --git a/templates/blocks/top-parts.jade b/templates/blocks/top-parts.jade new file mode 100644 index 000000000..f7b471ba3 --- /dev/null +++ b/templates/blocks/top-parts.jade @@ -0,0 +1,9 @@ + +.top-part + .logo + a(href='http://javascript.ru/') + img(src='/img/logo.png', alt='Javascript.ru') + .user.dropdown.down-left.inherit-min-width + a.user__entrance(href='/user') Вход на сайт + + diff --git a/templates/blocks/tutorial/bottom-navigation.jade b/templates/blocks/tutorial/bottom-navigation.jade new file mode 100644 index 000000000..55883defa --- /dev/null +++ b/templates/blocks/tutorial/bottom-navigation.jade @@ -0,0 +1,18 @@ +.book-navigation + .page-links.clearfix + if prev + .book-navigation__prev + | Предыдущий урок + kbd + | (Ctrl +  + span.book-navigation__arr ← + | ) + a.page-previous(href=prev.url,title="На предыдущую страницу") #{prev.title} + if next + .book-navigation__next + | Следующий урок + kbd + | (Ctrl +  + span.book-navigation__arr → + | ) + a.page-next(href=next.url,title="На следующую страницу") #{next.title} diff --git a/templates/blocks/tutorial/footer.jade b/templates/blocks/tutorial/footer.jade new file mode 100644 index 000000000..52493c0f3 --- /dev/null +++ b/templates/blocks/tutorial/footer.jade @@ -0,0 +1,3 @@ +footer.main__footer + time.main__footer-date(datetime=modified.toJSON()) + != moment(modified).format('DD.MM.YYYY') diff --git a/templates/blocks/tutorial/top-navigation.jade b/templates/blocks/tutorial/top-navigation.jade new file mode 100644 index 000000000..a9ec63a47 --- /dev/null +++ b/templates/blocks/tutorial/top-navigation.jade @@ -0,0 +1,15 @@ +.main__lesson-nav + if prev + .main__lesson-nav-prev + kbd + | (Ctrl +  + span.main__lesson-nav-arr ← + | ) + a.main__lesson-nav-link(href=prev.url) Предыдущий урок + if next + .main__lesson-nav-next + a.main__lesson-nav-link(href=next.url) Следующий урок + kbd + | (Ctrl +  + span.main__lesson-nav-arr → + | ) diff --git a/templates/error.jade b/templates/error.jade new file mode 100644 index 000000000..8ef0aa284 --- /dev/null +++ b/templates/error.jade @@ -0,0 +1,6 @@ +extends layouts/base + +block content + h1 HTTP Error (user-land report) + h2= error.message + pre #{error.stack} diff --git a/templates/layouts/base.jade b/templates/layouts/base.jade new file mode 100644 index 000000000..f3ce7f9cd --- /dev/null +++ b/templates/layouts/base.jade @@ -0,0 +1,12 @@ +block variables +include ../blocks/head +body + include ../blocks/top-parts + .page + .page__inner + include ../blocks/navbar + include ../blocks/social-aside + .main-content + block content + include ../blocks/footer + include ../blocks/scripts \ No newline at end of file diff --git a/test.sh b/test.sh deleted file mode 100755 index 20305d22e..000000000 --- a/test.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -DEBUG=server:* NODE_ENV=test NODE_PATH=. mocha --debug -R list diff --git a/test/.jshintrc b/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/test/data/index.js b/test/data/index.js deleted file mode 100755 index a10bb4724..000000000 --- a/test/data/index.js +++ /dev/null @@ -1,95 +0,0 @@ -var mongoose = require('lib/mongoose'); -var async = require('async'); -var log = require('lib/log')(module); - -var db = mongoose.connection.db; - -function createEmptyDb(callback) { - - async.series([ - function open(callback) { - if (mongoose.connection.readyState == 1) { // already connected - return callback(); - } else { - mongoose.connection.on('open', callback); - } - }, - function clearDatabase(callback) { - - // db.dropDatabase reallocates file and is slow - // that's why we drop collections one-by-one - - db.collectionNames(function(err, collections) { - - async.each(collections, function(collection, callback) { - var collectionName = collection.name.slice(db.databaseName.length + 1); - if (collectionName.indexOf('system.') === 0) { - return callback(); - } - log.debug("drop ", collectionName); - db.dropCollection(collectionName, callback); - }, callback); - - }); - - }, - function ensureIndexes(callback) { - // models must be recreated - // but that's problematic - // so we just make sure all necessary features are in place - async.each(mongoose.modelNames(), function(modelName, callback) { - mongoose.models[modelName].ensureIndexes(callback); - }, callback); - }, - function ensureCapped(callback) { - // models must be recreated - // but that's problematic - // so we just make sure all necessary features are in place - async.each(mongoose.modelNames(), function(modelName, callback) { - var schema = mongoose.models[modelName].schema; - if (!schema.options.capped) return callback(); - db.command({convertToCapped: mongoose.models[modelName].collection.name, size: schema.options.capped}, callback); - }, callback); - } - ], function(err) { - callback(err); - }); -} - -exports.createEmptyDb = createEmptyDb; - -/** - * Clear database, - * require models & wait until indexes are created, - * then load data from test/data/dataFile.json & callback - * @param dataFile - * @param callback - */ -exports.loadDb = function(dataFile, callback) { - // warning: pow-mongoose-fixtures fails to work with capped collections - // it calls remove() on them => everything dies - async.series([ - createEmptyDb, - function fillDb(callback) { - var modelsData = require('test/data/' + dataFile); - - async.each(Object.keys(modelsData), function(modelName, callback) { - loadModel(modelName, modelsData[modelName], callback); - }, callback); - } - ], function(err) { - callback(err); - }); - -}; - -function loadModel(name, data, callback) { - - var Model = mongoose.models[name]; - - async.each(data, function(itemData, callback) { - var model = new Model(itemData); - model.save(callback); - }, callback); - -} \ No newline at end of file diff --git a/test/data/sampleDb.js b/test/data/sampleDb.js deleted file mode 100755 index d385dcf37..000000000 --- a/test/data/sampleDb.js +++ /dev/null @@ -1,65 +0,0 @@ -exports.User = [ - { "_id": "000000000000000000000001", - "created": new Date(2014,0,1), - "username": "ilya kantor", - "email": "iliakan@gmail.com", - "password": "123", - "avatar": "1.jpg", - "following": [] - }, - { "_id": "000000000000000000000002", - "created": new Date(2014,0,1), - "username": "tester", - "email": "tester@mail.com", - "password": "123", - "avatar": "2.jpg", - "following": ["000000000000000000000001"] - }, - { "_id": "000000000000000000000003", - "created": new Date(2014,0,1), - "username": "vasya", - "email": "vasya@mail.com", - "password": "123", - "avatar": "3.jpg", - "following": ["000000000000000000000001", "000000000000000000000002"] - } -]; - -exports.Message = [ - { "_id": "100000000000000000000001", - "content": "Message 1", - "created": new Date(2014, 0, 1), - "user": "000000000000000000000001" - }, - { "_id": "100000000000000000000002", - "content": "Message 2", - "created": new Date(2014, 0, 2), - "user": "000000000000000000000002" - }, - { "_id": "100000000000000000000003", - "content": "Message 3", - "created": new Date(2014, 0, 3), - "user": "000000000000000000000002" - }, - { "_id": "100000000000000000000004", - "content": "Message 4", - "created": new Date(2014, 0, 4), - "user": "000000000000000000000003" - }, - { "_id": "100000000000000000000005", - "content": "Message 5", - "created": new Date(2014, 0, 5), - "user": "000000000000000000000003" - }, - { "_id": "100000000000000000000006", - "content": "Message 6", - "created": new Date(2014, 0, 6), - "user": "000000000000000000000003" - } -]; - -exports.Session = [ - { "_id" : "200000000000000000000001", "session" : "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"passport\":{\"user\":\"000000000000000000000001\"}}", "expires" : new Date("2015-01-24T18:52:24.306Z") }, - { "_id" : "200000000000000000000002", "session" : "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"passport\":{\"user\":\"000000000000000000000002\"}}", "expires" : ("2015-01-24T18:52:24.306Z") }, - { "_id" : "200000000000000000000003", "session" : "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"passport\":{\"user\":\"000000000000000000000003\"}}", "expires" : ("2015-01-24T18:52:24.306Z") } -]; \ No newline at end of file diff --git a/test/env.js b/test/env.js deleted file mode 100755 index f7534b31d..000000000 --- a/test/env.js +++ /dev/null @@ -1 +0,0 @@ -process.env.NODE_ENV = 'test'; diff --git a/test/mocha.opts b/test/mocha.opts index 7f3ce0129..790728212 100755 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,5 +1,9 @@ --reporter spec --colors ---timeout 10000 +--timeout 10000 +--require config/mongoose --require should ---require test/env $* \ No newline at end of file +--require co +--require co-mocha +--recursive +--ui bdd diff --git a/test/unit/lib/treeUtil.js b/test/unit/lib/treeUtil.js new file mode 100644 index 000000000..a9a386a7f --- /dev/null +++ b/test/unit/lib/treeUtil.js @@ -0,0 +1,66 @@ +const treeUtil = require('lib/treeUtil'); +const should = require('should'); + +describe('treeUtil', function() { + + const tree = {children: [ + { _id: '1', + title: 'Article 1', + isFolder: true, + weight: 0, + children: [ + { _id: '2', + title: 'Article 1.1', + weight: 0, + parent: '1', + isFolder: true, + children: [ + { _id: '3', + title: 'Article 1.1.1', + weight: 0, + isFolder: false, + parent: '2' }, + { _id: '4', + title: 'Article 1.1.2', + isFolder: false, + weight: 1, + parent: '2' } + ] + }, + { _id: '5', + title: 'Article 1.2', + weight: 1, + isFolder: false, + parent: '1' } + ] }, + { _id: '6', + title: 'Article 2', + weight: 1, + isFolder: true, + children: [ + { _id: '7', + title: 'Article 2.1', + weight: 0, + isFolder: false, + parent: '6' }, + { _id: '8', + title: 'Article 2.2', + weight: 1, + isFolder: false, + parent: '6' } + ] } + ]}; + + it('finds next', function() { + treeUtil.findPrevNextById(tree, 3).next._id.should.be.eql(4); + treeUtil.findPrevNextById(tree, 4).next._id.should.be.eql(5); + should.not.exist(treeUtil.findPrevNextById(tree, 5).next); + }); + + it('finds prev', function() { + should.not.exist(treeUtil.findPrevNextById(tree, 3).prev); + treeUtil.findPrevNextById(tree, 4).prev._id.should.be.eql(3); + treeUtil.findPrevNextById(tree, 5).prev._id.should.be.eql(4); + }); + +}); diff --git a/test/unit/model/user.js b/test/unit/model/user.js deleted file mode 100755 index 7218bd095..000000000 --- a/test/unit/model/user.js +++ /dev/null @@ -1,74 +0,0 @@ -var data = require('test/data'); -var mongoose = require('mongoose'); -var async = require('async'); - -describe('User', function() { - before(function(done) { - data.createEmptyDb(done) - }); - - var User = mongoose.models.User; - - it('errors on save for bad email', function(done) { - - var user = new User({ - email: "BAD", - password: "123" - }); - - user.save(function(err) { - err.name.should.equal('ValidationError'); - err.errors.email.value.should.equal(user.get('email')); - done(); - }); - - }); - - it('errors on save without password', function(done) { - - var user = new User({ - email: "BAD" - }); - - user.save(function(err) { - done(err ? undefined : new Error("Password must be set")); - }); - - }); - - it('autogenerates salt and hash', function(done) { - - var user = new User({ - email: "a@b.ru", - password: "pass" - }); - - user.get('salt').should.not.be.empty; - user.get('hash').should.not.be.empty; - user.checkPassword("pass").should.be.true; - done(); - - }); - - it('requires unique email', function(done) { - - function create(callback) { - new User({ - username: "nonunique", - email: "nonunique@b.ru", - password: "pass" - }).save(callback); - } - - async.series([ - create, - create - ], function(err) { - if (!err) return done(new Error("Same email is saved twice!")); - err.code.should.equal(11000); // unique index is checked by mongo - done(); - }); - - }); - -}); diff --git a/test/unit/subscribe/socket.js b/test/unit/subscribe/socket.js deleted file mode 100755 index ad1d21617..000000000 --- a/test/unit/subscribe/socket.js +++ /dev/null @@ -1,262 +0,0 @@ -var models = require('mongoose').models; -var async = require('async'); -var config = require('config'); -var io = require('socket.io-client'); -var querystring = require('querystring'); -var should = require('should'); - -var socketOptions ={ - transports: ['xhr-polling'], - 'force new connection': true -}; - -var url = 'http://localhost:' + config.get('port'); - -function openSessionSocket(sid) { - var clientOptions = {}; - for(var key in socketOptions) { - clientOptions[key] = socketOptions[key]; - } - var query = {}; - query[config.get('session:key')] = sid; - - clientOptions.query = querystring.stringify(query); - return io.connect('http://localhost:' + config.get('port'), clientOptions); -} - -describe("Socket",function() { - - before(function(done) { - var testData = require('test/data'); - testData.loadDb('sampleDb', done); - }); - - var anon; - var ilya, tester, vasya; - var ilyaId = '000000000000000000000001'; - var testerId = '000000000000000000000002'; - var vasyaId = '000000000000000000000003'; - - it("can open connect with invalid session", function(done) { - anon = openSessionSocket('FFFFFFFFFFFFFF0000000011'); - anon.on('connect', function() { - done(); - }) - }); - - it("anon user may not create a message", function(done) { - anon.emit('message:create', {content: "ANON MESSAGE"}, function(err) { - err.status.should.equal(403); - done(); - }); - }); - - describe("two users", function() { - - before(function(done) { // connect two clients before tests - async.parallel([ - function(callback) { - ilya = openSessionSocket('200000000000000000000001'); - ilya.on('connect', callback); - }, - function(callback) { - tester = openSessionSocket('200000000000000000000002'); - tester.on('connect', callback); - }, - function(callback) { - vasya = openSessionSocket('200000000000000000000003'); - vasya.on('connect', callback); - } - ], done); - }); - - it('should be able to create a message', function(done){ - ilya.emit('message:create', {content: "NEW ILYA MESSAGE"}, function(err, message) { - should.equal(err, null); - message.content.should.equal("NEW ILYA MESSAGE"); - done(); - }); - }); - - var receivedMessages = []; - var subscriptions = {}; - - it('tester subscribes to vasya', function(done) { - async.waterfall([ - function(callback) { - tester.emit('message:subscribe', {userId: vasyaId}, callback); - }, - function getOldMessages(id, callback) { - subscriptions.vasya = id; - - tester.on('message:create', function(message) { - receivedMessages.push(message); - if (receivedMessages.length == 3) callback(); - }); - - }, - function(callback) { - setTimeout(callback, 20); - }, - function(callback) { - // only 3 messages, no more! - receivedMessages.length.should.equal(3); - - vasya.emit('message:create', {content: "NEW VASYA MESSAGE"}, function(err, message) { - should.equal(err, null); - message.content.should.equal("NEW VASYA MESSAGE"); - // timeout for subscriber to react - setTimeout(callback, 30); - }); - }, - function(callback) { - receivedMessages.length.should.equal(4); - tester.removeAllListeners('message:create'); - callback(); - } - - ], done); - }); - - // NOW tester is subscribed to vasya - - it('tester subscribes to ilya', function(done) { - async.waterfall([ - function(callback) { - tester.emit('message:subscribe', {userId: ilyaId}, callback); - }, - function(id, callback) { - subscriptions.ilya = id; - - tester.on('message:create', function(message) { - receivedMessages.push(message); - if (receivedMessages.length == 6) callback(); - }); - - }, - function(callback) { - setTimeout(callback, 20); - }, - function(callback) { - receivedMessages.length.should.equal(6); - - ilya.emit('message:create', {content: "NEW ILYA MESSAGE 2"}, function(err, message) { - should.equal(err, null); - message.content.should.equal("NEW ILYA MESSAGE 2"); - // timeout for subscriber to react - setTimeout(callback, 300); - }); - }, - function(callback) { - receivedMessages.length.should.equal(7); - callback(); - } - - ], done); - }); - - // NOW tester is subscribed to ilya AND vasya - - it('tester unsubscribes from vasya', function(done) { - var oldReceivedCount = receivedMessages.length; - - async.waterfall([ - function(callback) { - tester.emit('message:unsubscribe', subscriptions.vasya, callback); - }, - - function(id, callback) { - - should.equal(id, subscriptions.vasya); - delete subscriptions.vasya; - - vasya.emit('message:create', {content: "NEW VASYA MESSAGE"}, function(err, message) { - should.equal(err, null); - message.content.should.equal("NEW VASYA MESSAGE"); - // timeout for subscriber to react - setTimeout(callback, 30); - }); - }, - - function(callback) { - // no new messages from vasya (unsubscribed) - oldReceivedCount.should.equal(receivedMessages.length); - - ilya.emit('message:create', {content: "NEW ILYA MESSAGE 3"}, function(err, message) { - should.equal(err, null); - message.content.should.equal("NEW ILYA MESSAGE 3"); - // timeout for subscriber to react - setTimeout(callback, 100); - }); - }, - function(callback) { - receivedMessages.length.should.be.equal(oldReceivedCount + 1); - tester.removeAllListeners('message:create'); - tester.emit('message:unsubscribe', subscriptions.ilya, callback); - delete subscriptions.ilya; - } - ], done); - }); - - // NOW tester is not subscribed to anything - - it('tester subscribes to vasya & following', function(done) { - var oldReceivedCount = receivedMessages.length; - async.waterfall([ - function(callback) { - tester.emit('message:subscribe', {userId: vasyaId, following: true}, callback); - }, - function(id, callback) { - subscriptions.vasya = id; - // skip old messages - setTimeout(callback, 60); - }, - function(callback) { - - tester.on('message:create', function(message) { - receivedMessages.push(message); - }); - - async.series([ - function(callback) { - vasya.emit('message:create', {content: "NEW VASYA MESSAGE"}, callback); - }, - function(callback) { - ilya.emit('message:create', {content: "NEW ILYA MESSAGE"}, callback); - } - ], function(err) { - callback(err); - }); - }, - function(callback) { - // skip old messages - setTimeout(callback, 60); - }, - function(callback) { - receivedMessages.length.should.equal(oldReceivedCount + 2); - tester.removeAllListeners('message:create'); - callback(); - } - - ], done); - - }); - - // NOW tester is subscribed to vasya & following - - }); - - - - after(function(done) { - async.each([anon, ilya, vasya, tester], function(user, callback) { - if (!user) return callback(); // if error while opening the socket - user.on('disconnect', callback); - user.disconnect(); - }, function() { - // disconnect sends the packet and triggers event synchronously, - // we use timeout 10 to let the packet actually leave the client process - setTimeout(done, 10) }); - }); - -}); \ No newline at end of file diff --git a/test/unit/subscribe/subscription.js b/test/unit/subscribe/subscription.js deleted file mode 100755 index cffc40ca3..000000000 --- a/test/unit/subscribe/subscription.js +++ /dev/null @@ -1,105 +0,0 @@ -var async = require('async'); -var config = require('config'); -var models = require('lib/mongoose').models; -var subscriptionStore = require('lib/subscriptionStore').store; - -describe("Subscription",function() { - - this.timeout(5000); - - before(function(done) { - var testData = require('test/data'); - testData.loadDb('sampleDb', done); - }); - - var subscription, subscription2; - var receivedMessages = []; - it("gets old messages", function(done) { - subscription = subscriptionStore.createSubscription(models.Message.find({user:'000000000000000000000002'})); - subscription.on('data', function(message) { - receivedMessages.push(message); - }); - - setTimeout(function() { - receivedMessages.length.should.equal(2); - done(); - }, 100); - }); - - it("gets new messages", function(done) { - - async.series([ - function(callback) { - new models.Message({ - user: '000000000000000000000002', - content: 'MESSAGE FROM QUERY' - }).save(callback); - }, - function(callback) { - new models.Message({ - user: '000000000000000000000002', - content: 'MESSAGE FROM QUERY' - }).save(callback); - }, - function(callback) { - new models.Message({ - user: '000000000000000000000001', - content: 'MESSAGE NOT IN QUERY' - }).save(callback); - }, - function(callback) { - setTimeout(callback, 50); - }, - function(callback) { - receivedMessages.length.should.equal(4); - callback(); - } - ], done); - - }); - - it("add one more", function(done) { - subscription2 = subscriptionStore.createSubscription(models.Message.find({user:'000000000000000000000003'})); - subscription2.on('data', function(message) { - receivedMessages.push(message); - }); - - setTimeout(function() { - receivedMessages.length.should.equal(7); - done(); - }, 100); - }); - - - it("receives from both", function(done) { - async.series([ - function(callback) { - new models.Message({ - user: '000000000000000000000002', - content: 'MESSAGE FROM QUERY' - }).save(callback); - }, - function(callback) { - new models.Message({ - user: '000000000000000000000003', - content: 'MESSAGE FROM QUERY' - }).save(callback); - }, - function(callback) { - new models.Message({ - user: '000000000000000000000001', - content: 'MESSAGE NOT IN QUERY' - }).save(callback); - }, - function(callback) { - setTimeout(callback, 100); - }, - function(callback) { - receivedMessages.length.should.equal(9); - callback(); - } - ], done); - }); - - -}); \ No newline at end of file diff --git a/util/log.js b/util/log.js deleted file mode 100755 index 465533d13..000000000 --- a/util/log.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Flexible logging wrapper around windston - * - * usage: - * var log = require('lib/log')(module) - * log.debug("Winston %s", "syntax"); - * - * enabling debugdirectly from code: - * log.debugOn(); - * enabling from CLI: - * DEBUG=path node app - * where path is calculated from the project root (where package.json resides) - * example: - * DEBUG=models/* node app - * or - * DEBUG=-models/user,models/* node app (to log all models except user) - */ - -var winston = require('winston'); -var path = require('path'); -var fs = require('fs'); - -var names = [], skips = []; - -(process.env.DEBUG || '') - .split(/[\s,]+/) - .forEach(function(name) { - name = name.replace('*', '.*?'); - if (name[0] === '-') { - skips.push(new RegExp('^' + name.substr(1) + '$')); - } else { - names.push(new RegExp('^' + name + '$')); - } - }); - -function findProjectRoot() { - - var dir = __dirname; - while (!fs.existsSync(path.join(dir, 'package.json'))) { - dir = path.dirname(dir); - } - - return path.normalize(dir); -} - -var projectRoot = findProjectRoot(); - -function getLogLevel(module) { - - var modulePath = module.filename.slice(projectRoot.length + 1); // models/user.js - modulePath = modulePath.replace(/\.js$/, ''); // models.user - - var logLevel = 'info'; - - var isSkipped = skips.some(function(re) { - return re.test(modulePath); - }); - - if (!isSkipped) { - var isIncluded = names.some(function(re) { - return re.test(modulePath); - }); - - if (isIncluded) logLevel = 'debug'; - } - - return logLevel; -} - -function getLogger(module) { - - var showPath = module.filename.split('/').slice(-2).join('/'); - - var logLevel = getLogLevel(module); - var logger = new winston.Logger({ - transports: [ - new winston.transports.Console({ - colorize: true, - level: logLevel, - label: showPath - }) - ] - }); - - logger.debugOn = function() { - Object.keys(this.transports).forEach(function(key) { - logger.transports[key].level = 'debug'; - }, this); - }; - - return logger; -} - -module.exports = getLogger; \ No newline at end of file diff --git a/views/error.jade b/views/error.jade deleted file mode 100755 index 51ec12c6a..000000000 --- a/views/error.jade +++ /dev/null @@ -1,6 +0,0 @@ -extends layout - -block content - h1= message - h2= error.status - pre #{error.stack} diff --git a/views/layout.jade b/views/layout.jade deleted file mode 100755 index 474914e79..000000000 --- a/views/layout.jade +++ /dev/null @@ -1,7 +0,0 @@ -doctype html -html - head - title= title - link(rel='stylesheet', href='/stylesheets/app.css') - body - block content