├── .eslintignore ├── .eslintrc ├── .gitignore ├── client └── assets │ ├── js │ ├── bundle.js │ └── src │ │ └── index.js │ └── sass │ ├── _common.sass │ ├── _variables.sass │ ├── components │ ├── _feed.sass │ ├── _gallery.sass │ ├── _login.sass │ └── _sidebar.sass │ ├── style.sass │ └── vendor │ ├── _nprogress.sass │ ├── _sweetalert.sass │ └── _swiper.sass ├── package.json ├── readme.md ├── rollup.config.js ├── server ├── api-router.js ├── app.js ├── feed-websockets.js ├── index.js ├── json-data │ ├── feed.json │ ├── gallery.json │ ├── index.json │ └── login.json ├── middlewares │ └── sass.js └── views │ └── base.ejs └── shared ├── components ├── app.tag ├── layout │ ├── sidebar.tag │ └── user-status.tag ├── mixins.js └── pages │ ├── feed.tag │ ├── gallery.tag │ ├── index.tag │ └── login.tag ├── config.js ├── gateways ├── FeedGateway.js ├── GalleryGateway.js ├── IndexGateway.js └── LoginGateway.js ├── helpers └── index.js ├── models ├── GatewayModel.js └── User.js └── routes.js /.eslintignore: -------------------------------------------------------------------------------- 1 | public/components/lib -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | env: 2 | 3 | ########################################################################### 4 | # # 5 | # ENVIRONMENT: if you write code that will be executed in one of the # 6 | # following environments, the value for that environment should be # 7 | # set to true. # 8 | # # 9 | ########################################################################### 10 | 11 | browser: true 12 | es6: true 13 | node: true 14 | 15 | parserOptions: 16 | sourceType: "module" 17 | ecmaFeatures: 18 | destructuring: true 19 | spread: true 20 | arrowFunctions: true 21 | blockBindings: true 22 | 23 | globals: 24 | riot: true 25 | socket: true 26 | io: true 27 | IS_CLIENT: true 28 | IS_SERVER: true 29 | Velocity: true 30 | swal: true 31 | Swiper: true 32 | 33 | ########################################################################### 34 | # # 35 | # GLOBALS: ESLint will assume the following variables are declared # 36 | # globally; other variables will require explicit declaration. # 37 | # # 38 | ########################################################################### 39 | 40 | require: true 41 | 42 | rules: 43 | 44 | ########################################################################### 45 | # # 46 | # POSSIBLE ERRORS: these rules point out areas where you might have # 47 | # made mistakes. # 48 | # # 49 | ########################################################################### 50 | 51 | comma-dangle: 1 # disallow trailing commas in object literals 52 | no-cond-assign: 0 # disallow assignment in conditional expressions 53 | no-console: 0 # disallow use of console 54 | no-constant-condition: 2 # disallow use of constant expressions in conditions 55 | no-control-regex: 2 # disallow control characters in regular expressions 56 | no-debugger: 2 # disallow use of debugger 57 | no-dupe-keys: 2 # disallow duplicate keys when creating object literals 58 | no-empty: 2 # disallow empty statements 59 | no-empty-character-class: 2 # disallow the use of empty character classes in regular expressions 60 | no-ex-assign: 2 # disallow assigning to the exception in a catch block 61 | no-extra-boolean-cast: 2 # disallow double-negation boolean casts in a boolean context 62 | no-extra-semi: 0 # disallow unnecessary semicolons 63 | no-func-assign: 0 # disallow overwriting functions written as function declarations 64 | no-inner-declarations: 1 # disallow function or variable declarations in nested blocks 65 | no-invalid-regexp: 2 # disallow invalid regular expression strings in the RegExp 66 | # constructor 67 | no-irregular-whitespace: 2 # disallow irregular whitespace outside of strings and comments 68 | no-negated-in-lhs: 2 # disallow negation of the left operand of an in expression 69 | no-obj-calls: 2 # disallow the use of object properties of the global object (Math 70 | # and JSON) as functions 71 | no-regex-spaces: 1 # disallow multiple spaces in a regular expression literal 72 | no-reserved-keys: 0 # disallow reserved words being used as object literal keys 73 | no-sparse-arrays: 2 # disallow sparse arrays 74 | no-unreachable: 2 # disallow unreachable statements after a return, throw, continue, 75 | # or break statement 76 | use-isnan: 2 # disallow comparisons with the value NaN 77 | valid-jsdoc: # ensure JSDoc comments are valid 78 | [1, { "prefer": { "return": "returns" }, "requireReturn": false }] 79 | valid-typeof: 2 # ensure that the results of typeof are compared against a 80 | # valid string 81 | 82 | ########################################################################### 83 | # # 84 | # BEST PRACTICES: these rules are designed to prevent you from making # 85 | # mistakes. They either prescribe a better way of doing something or # 86 | # help you avoid pitfalls. # 87 | # # 88 | ########################################################################### 89 | 90 | block-scoped-var: 0 # treat var statements as if they were block scoped 91 | complexity: [1, 250] # specify the maximum cyclomatic complexity allowed in a program 92 | consistent-return: 0 # require return statements to either always or never specify values 93 | curly: 0 # specify curly brace conventions for all control 94 | # statements 95 | default-case: 2 # require default case in switch statements 96 | dot-notation: 1 # encourages use of dot notation whenever possible 97 | eqeqeq: 0 # require the use of === and !== 98 | guard-for-in: 1 # make sure for-in loops have an if statement 99 | no-alert: 0 # disallow the use of alert, confirm, and prompt 100 | no-caller: 2 # disallow use of arguments.caller or arguments.callee 101 | no-div-regex: 0 # disallow division operators explicitly at beginning of regular 102 | # expression 103 | no-else-return: 0 # disallow else after a return in an if 104 | no-eq-null: 0 # disallow comparisons to null without a type-checking operator 105 | no-eval: 2 # disallow use of eval() 106 | no-extend-native: 2 # disallow adding to native types 107 | no-extra-bind: 2 # disallow unnecessary function binding 108 | no-fallthrough: 2 # disallow fallthrough of case statements 109 | no-floating-decimal: 2 # disallow the use of leading or trailing decimal points in numeric 110 | # literals 111 | no-implied-eval: 2 # disallow use of eval()-like methods 112 | no-iterator: 2 # disallow usage of __iterator__ property 113 | no-labels: 2 # disallow use of labeled statements 114 | no-lone-blocks: 2 # disallow unnecessary nested blocks 115 | no-loop-func: 0 # disallow creation of functions within loops 116 | no-multi-spaces: 0 # disallow use of multiple spaces 117 | no-multi-str: 2 # disallow use of multiline strings 118 | no-native-reassign: 2 # disallow reassignments of native objects 119 | no-new: 2 # disallow use of new operator when not part of the assignment or 120 | # comparison 121 | no-new-func: 0 # disallow use of new operator for Function object 122 | no-new-wrappers: 2 # disallows creating new instances of String,Number, and Boolean 123 | no-octal: 0 # disallow use of octal literals 124 | no-octal-escape: 0 # disallow use of octal escape sequences in string literals, such as 125 | # `var foo = "Copyright \251"` 126 | no-process-env: 0 # disallow use of process.env 127 | no-proto: 2 # disallow usage of __proto__ property 128 | no-redeclare: 1 # disallow declaring the same variable more then once 129 | no-return-assign: 0 # disallow use of assignment in return statement 130 | no-script-url: 2 # disallow use of javascript urls. 131 | no-self-compare: 2 # disallow comparisons where both sides are exactly the same 132 | no-sequences: 2 # disallow use of comma operator 133 | no-unused-expressions: 0 # disallow usage of expressions in statement position 134 | no-void: 2 # disallow use of void operator 135 | no-warning-comments: 0 # disallow usage of configurable warning terms in comments - e.g. 136 | no-with: 2 # disallow use of the with statement 137 | radix: 2 # require use of the second argument for parseInt() 138 | vars-on-top: 0 # requires to declare all vars on top of their containing scope 139 | wrap-iife: [2, "inside"] # require immediate function invocation to be wrapped in parentheses 140 | 141 | ########################################################################### 142 | # # 143 | # STRICT MODE: these rules relate to using strict mode. # 144 | # # 145 | ########################################################################### 146 | 147 | strict: [2, "never"] # require that all functions are run in strict mode 148 | 149 | ########################################################################### 150 | # # 151 | # VARIABLES: these rules have to do with variable declarations. # 152 | # # 153 | ########################################################################### 154 | 155 | no-catch-shadow: 2 # disallow the catch clause parameter name being the same as a 156 | # variable in the outer scope 157 | no-delete-var: 2 # disallow deletion of variables 158 | no-label-var: 2 # disallow labels that share a name with a variable 159 | no-shadow: 0 # disallow declaration of variables already declared in the 160 | # outer scope 161 | no-shadow-restricted-names: 2 # disallow shadowing of names such as arguments 162 | no-undef: 2 # disallow use of undeclared variables unless mentioned in a 163 | # /*global */ block 164 | no-undef-init: 2 # disallow use of undefined when initializing variables 165 | no-undefined: 0 # disallow use of undefined variable 166 | no-unused-vars: 0 # disallow declaration of variables that are not used in the code 167 | no-use-before-define: 0 # disallow use of variables before they are defined 168 | 169 | ########################################################################### 170 | # # 171 | # NODE: these rules relate to functionality provided in Node.js. # 172 | # # 173 | ########################################################################### 174 | 175 | handle-callback-err: 0 # enforces error handling in callbacks 176 | no-mixed-requires: 0 # disallow mixing regular variable and require declarations 177 | no-new-require: 2 # disallow use of new operator with the require function 178 | no-path-concat: 2 # disallow string concatenation with __dirname and __filename 179 | no-process-exit: 0 # disallow process.exit() 180 | no-restricted-modules: 0 # restrict usage of specified node modules 181 | no-sync: 0 # disallow use of synchronous methods 182 | 183 | ########################################################################### 184 | # # 185 | # STYLISTIC ISSUES: these rules are purely matters of style and, # 186 | # while valueable to enforce consistently across a project, are # 187 | # quite subjective. # 188 | # # 189 | ########################################################################### 190 | 191 | indent: [2, 2] # Set a specific tab width 192 | brace-style: 0 # enforce one true brace style 193 | camelcase: 0 # require camel case names 194 | comma-spacing: 2 # enforce spacing before and after comma 195 | comma-style: 2 # enforce one true comma style 196 | consistent-this: 0 # enforces consistent naming when capturing the current execution context 197 | eol-last: 0 # enforce newline at the end of file, with no multiple empty lines 198 | func-names: 0 # require function expressions to have a name 199 | func-style: 0 # enforces use of function declarations or expressions 200 | key-spacing: 2 # enforces spacing between keys and values in object literal properties 201 | max-nested-callbacks: [2, 4] # specify the maximum depth callbacks can be nested 202 | new-cap: 0 # require a capital letter for constructors 203 | new-parens: 2 # disallow the omission of parentheses when invoking a constructor with no arguments 204 | no-array-constructor: 2 # disallow use of the Array constructor 205 | no-lonely-if: 0 # disallow if as the only statement in an else block 206 | no-mixed-spaces-and-tabs: 2 # disallow mixed spaces and tabs for indentation 207 | no-nested-ternary: 0 # disallow nested ternary expressions 208 | no-new-object: 1 # disallow use of the Object constructor 209 | no-space-before-semi: 0 # disallow space before semicolon 210 | no-spaced-func: 2 # disallow space between function identifier and application 211 | no-ternary: 0 # disallow the use of ternary operators 212 | 213 | no-trailing-spaces: 2 # disallow trailing whitespace at the end of lines 214 | no-multiple-empty-lines: 0 # disallow multiple empty lines 215 | no-underscore-dangle: 0 # disallow dangling underscores in identifiers 216 | no-extra-parens: 2 # disallow wrapping of non-IIFE statements in parens 217 | one-var: 0 # allow just one var statement per function 218 | padded-blocks: 0 # enforce padding within blocks 219 | quotes: # specify whether double or single quotes should be used 220 | [1, "single", "avoid-escape"] 221 | quote-props: 0 # require quotes around object literal property names 222 | semi: [2, "never"] # require or disallow use of semicolons instead of ASI 223 | semi-spacing: 0 224 | sort-vars: 0 # sort variables within the same declaration block 225 | keyword-spacing: 2 # require a space after certain keywords 226 | space-before-blocks: 2 # require or disallow space before blocks 227 | space-in-brackets: 0 # require or disallow spaces inside brackets 228 | space-in-parens: 0 # require or disallow spaces inside parentheses 229 | space-infix-ops: 0 # require spaces around operators 230 | spaced-line-comment: 0 # require or disallow a space immediately following 231 | # the // in a line comment 232 | wrap-regex: 0 # require regex literals to be wrapped in parentheses 233 | 234 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | 13 | # OS or Editor folders 14 | .DS_Store 15 | Thumbs.db 16 | .cache 17 | .project 18 | .settings 19 | .tmproj 20 | *.esproj 21 | nbproject 22 | 23 | # Dreamweaver added files 24 | _notes 25 | dwsync.xml 26 | 27 | # Komodo 28 | *.komodoproject 29 | .komodotools 30 | 31 | # Folders to ignore 32 | .hg 33 | .svn 34 | .CVS 35 | intermediate 36 | publish 37 | .idea 38 | node_modules 39 | .sass-cache 40 | 41 | # ignore the distribution folders 42 | lib 43 | .tmp 44 | 45 | # the style.css will be generated 46 | app/assets/css/style.css -------------------------------------------------------------------------------- /client/assets/js/src/index.js: -------------------------------------------------------------------------------- 1 | import Velocity from 'velocity-animate' 2 | import 'velocity-animate/velocity.ui' 3 | import '../../../../shared/components/app.tag' 4 | import '../../../../shared/components/mixins' 5 | import { mount } from 'riot' 6 | import route from 'riot-route' 7 | import User from '../../../../shared/models/User' 8 | import routes from '../../../../shared/routes' 9 | import NProgress from 'nprogress' 10 | 11 | var app, 12 | initialData = JSON.parse(window.initialData) 13 | 14 | window.IS_SERVER = false 15 | window.IS_CLIENT = true 16 | 17 | route.base('/') 18 | // start the progress bar 19 | NProgress.start() 20 | 21 | Object.keys(routes).forEach(function(path) { 22 | route(path, function(...args) { 23 | var gateway = routes[path](args) 24 | if (!app) { 25 | 26 | // extend the gateway using the initial data 27 | Object.assign(gateway, initialData.gateway) 28 | // store the initial data 29 | gateway._data = initialData 30 | 31 | initialData.gateway = gateway 32 | initialData.user = new User() 33 | app = mount('app', initialData)[0] 34 | NProgress.done() 35 | } else { 36 | if (!gateway.wasFetched) NProgress.start() 37 | 38 | gateway.fetch() 39 | .then(function(data) { 40 | data.gateway = gateway 41 | app.mountSubview(data) 42 | NProgress.done() 43 | }) 44 | 45 | } 46 | }) 47 | }) 48 | 49 | route.start(true) 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /client/assets/sass/_common.sass: -------------------------------------------------------------------------------- 1 | body 2 | background: $body-background-color 3 | color: $font-color 4 | font-family: $font-family 5 | font-size: 1.2rem 6 | line-height: 1.5rem 7 | 8 | a 9 | color: $links-color 10 | text-decoration: none 11 | 12 | main 13 | opacity: 0 14 | padding: 2rem 15 | box-sizing: border-box 16 | width: calc(100% - #{$sidebar-width}) 17 | 18 | h1 19 | margin-top: 0 20 | 21 | h1, 22 | h2 23 | font-weight: 200 24 | color: $headings-color 25 | 26 | -------------------------------------------------------------------------------- /client/assets/sass/_variables.sass: -------------------------------------------------------------------------------- 1 | // colors 2 | $body-background-color: #fff 3 | $font-color: #515151 4 | $primary-color: #FF0044 5 | $headings-color: #777 6 | $links-color: #408bc2 7 | $sidebar-background: $primary-color 8 | 9 | // other stuff 10 | $sidebar-width: 8rem 11 | $font-family: 'Lato', sans-serif -------------------------------------------------------------------------------- /client/assets/sass/components/_feed.sass: -------------------------------------------------------------------------------- 1 | [data-is=feed] 2 | ul 3 | margin: 0 4 | padding: 0 5 | list-style: none 6 | 7 | li 8 | background: white 9 | padding: 1rem 0 10 | margin-bottom: 1rem -------------------------------------------------------------------------------- /client/assets/sass/components/_gallery.sass: -------------------------------------------------------------------------------- 1 | [data-is=gallery] 2 | .swiper-slide 3 | min-height: 500px -------------------------------------------------------------------------------- /client/assets/sass/components/_login.sass: -------------------------------------------------------------------------------- 1 | [data-is=login] 2 | label 3 | width: 100% 4 | float: left 5 | margin-bottom: 1rem 6 | 7 | input 8 | font-family: $font-family 9 | font-size: 1.2rem 10 | width: 100% 11 | border: 1px solid $links-color 12 | border-radius: 5px 13 | box-sizing: border-box 14 | padding: 1rem 0.8rem 15 | margin: 0.4rem 0 0 16 | 17 | button 18 | background-color: $links-color 19 | font-family: $font-family 20 | color: white 21 | border: none 22 | box-shadow: none 23 | font-size: 1.2rem 24 | border-radius: 5px 25 | padding: 1rem 2rem 26 | margin: 2rem 0 0 27 | cursor: pointer -------------------------------------------------------------------------------- /client/assets/sass/components/_sidebar.sass: -------------------------------------------------------------------------------- 1 | sidebar 2 | position: fixed 3 | right: 0 4 | top: 0 5 | height: 100% 6 | background: $sidebar-background 7 | width: $sidebar-width 8 | padding: 1.8rem 0 9 | color: white 10 | box-sizing: border-box 11 | transition: 0.3s 12 | font-size: 1.2rem 13 | line-height: 2rem 14 | 15 | ul 16 | padding: 0 17 | margin: 0 18 | 19 | li 20 | list-style: none 21 | margin-bottom: 1px 22 | 23 | &:hover, 24 | &:active 25 | a 26 | background: darken($sidebar-background, 10%) 27 | 28 | a 29 | display: inline-block 30 | width: 100% 31 | color: white 32 | padding: 0.5rem 1rem 33 | 34 | &.active 35 | a 36 | background: $links-color 37 | 38 | footer 39 | position: absolute 40 | bottom: 2rem 41 | left: 0 42 | display: inline-block 43 | color: white 44 | width: 100% 45 | line-height: 1rem 46 | text-align: center 47 | 48 | small 49 | font-size: 0.8rem -------------------------------------------------------------------------------- /client/assets/sass/style.sass: -------------------------------------------------------------------------------- 1 | @import 'variables' 2 | 3 | // third party stuff 4 | @import 'vendor/nprogress' 5 | @import 'vendor/sweetalert' 6 | @import 'vendor/swiper' 7 | 8 | @import 'common' 9 | @import 'components/sidebar' 10 | @import 'components/feed' 11 | @import 'components/login' 12 | @import 'components/gallery' 13 | 14 | -------------------------------------------------------------------------------- /client/assets/sass/vendor/_nprogress.sass: -------------------------------------------------------------------------------- 1 | #nprogress 2 | pointer-events: none 3 | 4 | .bar 5 | background: $links-color 6 | position: fixed 7 | z-index: 1031 8 | top: 0 9 | left: 0 10 | width: 100% 11 | height: 2px 12 | 13 | .peg 14 | display: block 15 | position: absolute 16 | right: 0px 17 | width: 100px 18 | height: 100% 19 | box-shadow: 0 0 10px $links-color, 0 0 5px $links-color 20 | opacity: 1 21 | -webkit-transform: rotate(3deg) translate(0px, -4px) 22 | -ms-transform: rotate(3deg) translate(0px, -4px) 23 | transform: rotate(3deg) translate(0px, -4px) 24 | 25 | .spinner 26 | display: block 27 | position: fixed 28 | z-index: 1031 29 | top: 15px 30 | left: 15px 31 | 32 | .spinner-icon 33 | width: 18px 34 | height: 18px 35 | box-sizing: border-box 36 | border: solid 2px transparent 37 | border-top-color: $links-color 38 | border-left-color: $links-color 39 | border-radius: 50% 40 | -webkit-animation: nprogress-spinner 400ms linear infinite 41 | animation: nprogress-spinner 400ms linear infinite 42 | 43 | .nprogress-custom-parent 44 | overflow: hidden 45 | position: relative 46 | 47 | #nprogress 48 | .spinner, .bar 49 | position: absolute 50 | 51 | @-webkit-keyframes nprogress-spinner 52 | 0% 53 | -webkit-transform: rotate(0deg) 54 | 55 | 100% 56 | -webkit-transform: rotate(360deg) 57 | 58 | @keyframes nprogress-spinner 59 | 0% 60 | transform: rotate(0deg) 61 | 62 | 100% 63 | transform: rotate(360deg) -------------------------------------------------------------------------------- /client/assets/sass/vendor/_sweetalert.sass: -------------------------------------------------------------------------------- 1 | // SweetAlert 2 | // 2014-2015 (c) - Tristan Edwards 3 | // github.com/t4t5/sweetalert 4 | 5 | body.stop-scrolling 6 | height: 100% 7 | overflow: hidden 8 | 9 | .sweet-overlay 10 | background-color: rgb(0, 0, 0) 11 | background-color: rgba(black, 0.4) 12 | position: fixed 13 | left: 0 14 | right: 0 15 | top: 0 16 | bottom: 0 17 | display: none 18 | z-index: 10000 19 | 20 | .sweet-alert 21 | $width: 478px 22 | $padding: 17px 23 | 24 | background-color: white 25 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif 26 | width: $width 27 | padding: $padding 28 | border-radius: 5px 29 | text-align: center 30 | position: fixed 31 | left: 50% 32 | top: 50% 33 | margin-left: -($width / 2 + $padding) 34 | margin-top: -200px 35 | overflow: hidden 36 | display: none 37 | z-index: 99999 38 | 39 | @media all and (max-width: 540px) 40 | width: auto 41 | margin-left: 0 42 | margin-right: 0 43 | left: 15px 44 | right: 15px 45 | 46 | h2 47 | color: #575757 48 | font-size: 30px 49 | text-align: center 50 | font-weight: 600 51 | text-transform: none 52 | position: relative 53 | margin: 25px 0 54 | padding: 0 55 | line-height: 40px 56 | display: block 57 | 58 | p 59 | color: #797979 60 | font-size: 16px 61 | text-align: center 62 | font-weight: 300 63 | position: relative 64 | text-align: inherit 65 | float: none 66 | margin: 0 67 | padding: 0 68 | line-height: normal 69 | 70 | fieldset 71 | border: none 72 | position: relative 73 | 74 | .sa-error-container 75 | background-color: #f1f1f1 76 | margin-left: -17px 77 | margin-right: -17px 78 | overflow: hidden 79 | padding: 0 10px 80 | max-height: 0 81 | webkit-transition: padding 0.15s, max-height 0.15s 82 | transition: padding 0.15s, max-height 0.15s 83 | 84 | &.show 85 | padding: 10px 0 86 | max-height: 100px 87 | webkit-transition: padding 0.2s, max-height 0.2s 88 | transition: padding 0.25s, max-height 0.25s 89 | 90 | .icon 91 | display: inline-block 92 | width: 24px 93 | height: 24px 94 | border-radius: 50% 95 | background-color: rgb(234, 125, 125) 96 | color: white 97 | line-height: 24px 98 | text-align: center 99 | margin-right: 3px 100 | 101 | p 102 | display: inline-block 103 | 104 | .sa-input-error 105 | position: absolute 106 | top: 29px 107 | right: 26px 108 | width: 20px 109 | height: 20px 110 | opacity: 0 111 | -webkit-transform: scale(0.5) 112 | transform: scale(0.5) 113 | -webkit-transform-origin: 50% 50% 114 | transform-origin: 50% 50% 115 | -webkit-transition: all 0.1s 116 | transition: all 0.1s 117 | 118 | &::before, &::after 119 | content: "" 120 | width: 20px 121 | height: 6px 122 | background-color: #f06e57 123 | border-radius: 3px 124 | position: absolute 125 | top: 50% 126 | margin-top: -4px 127 | left: 50% 128 | margin-left: -9px 129 | 130 | &::before 131 | -webkit-transform: rotate(-45deg) 132 | transform: rotate(-45deg) 133 | 134 | &::after 135 | -webkit-transform: rotate(45deg) 136 | transform: rotate(45deg) 137 | 138 | &.show 139 | opacity: 1 140 | -webkit-transform: scale(1) 141 | transform: scale(1) 142 | 143 | input 144 | width: 100% 145 | box-sizing: border-box 146 | border-radius: 3px 147 | border: 1px solid rgb(215, 215, 215) 148 | height: 43px 149 | margin-top: 10px 150 | margin-bottom: 17px 151 | font-size: 18px 152 | box-shadow: inset 0px 1px 1px rgba(black, 0.06) 153 | padding: 0 12px 154 | display: none 155 | -webkit-transition: all 0.3s 156 | transition: all 0.3s 157 | 158 | &:focus 159 | outline: none 160 | box-shadow: 0px 0px 3px rgb(196, 230, 245) 161 | border: 1px solid rgb(180, 219, 237) 162 | 163 | &::-moz-placeholder 164 | transition: opacity 0.3s 0.03s ease 165 | opacity: 0.5 166 | 167 | &:-ms-input-placeholder 168 | transition: opacity 0.3s 0.03s ease 169 | opacity: 0.5 170 | 171 | &::-webkit-input-placeholder 172 | transition: opacity 0.3s 0.03s ease 173 | opacity: 0.5 174 | 175 | &::-moz-placeholder 176 | color: lighten(#575757, 40) 177 | 178 | &:-ms-input-placeholder 179 | color: lighten(#575757, 40) 180 | 181 | &::-webkit-input-placeholder 182 | color: lighten(#575757, 40) 183 | 184 | &.show-input input 185 | display: block 186 | 187 | .sa-confirm-button-container 188 | display: inline-block 189 | position: relative 190 | 191 | .la-ball-fall 192 | position: absolute 193 | left: 50% 194 | top: 50% 195 | margin-left: -27px 196 | margin-top: 4px 197 | opacity: 0 198 | visibility: hidden 199 | 200 | button 201 | $btnBlue: $links-color 202 | $btnGray: #c1c1c1 203 | 204 | background-color: $btnBlue 205 | color: white 206 | border: none 207 | box-shadow: none 208 | font-size: 17px 209 | font-weight: 500 210 | -webkit-border-radius: 4px 211 | border-radius: 5px 212 | padding: 10px 32px 213 | margin: 26px 5px 0 5px 214 | cursor: pointer 215 | 216 | &:focus 217 | outline: none 218 | box-shadow: 0 0 2px rgba(128, 179, 235, 0.5), inset 0 0 0 1px rgba(0, 0, 0, 0.05) 219 | 220 | &:hover 221 | background-color: darken($btnBlue, 3%) 222 | 223 | &:active 224 | background-color: darken($btnBlue, 10%) 225 | 226 | &.cancel 227 | background-color: $btnGray 228 | 229 | &:hover 230 | background-color: darken($btnGray, 3%) 231 | 232 | &:active 233 | background-color: darken($btnGray, 10%) 234 | 235 | // Cancel button should keep the same style 236 | &:focus 237 | box-shadow: rgba(197, 205, 211, 0.8) 0px 0px 2px, rgba(0, 0, 0, 0.04706) 0px 0px 0px 1px inset !important 238 | 239 | &[disabled] 240 | opacity: 0.6 241 | cursor: default 242 | 243 | &.confirm[disabled] 244 | color: transparent 245 | 246 | ~ .la-ball-fall 247 | opacity: 1 248 | visibility: visible 249 | transition-delay: 0s 250 | 251 | // Removes selection outline in Firefox 252 | &::-moz-focus-inner 253 | border: 0 254 | 255 | // Only show focus-style when there is multiple choice of actions 256 | &[data-has-cancel-button=false] button 257 | box-shadow: none !important 258 | 259 | &[data-has-confirm-button=false][data-has-cancel-button=false] 260 | padding-bottom: 40px 261 | 262 | .sa-icon 263 | $red: #f27474 264 | $orange: #f8bb86 265 | $blue: #c9dae1 266 | $green: #a5dc86 267 | 268 | width: 80px 269 | height: 80px 270 | border: 4px solid gray 271 | -webkit-border-radius: 40px 272 | border-radius: 40px 273 | border-radius: 50% 274 | margin: 20px auto 275 | padding: 0 276 | position: relative 277 | box-sizing: content-box 278 | 279 | &.sa-error 280 | border-color: $red 281 | 282 | .sa-x-mark 283 | position: relative 284 | display: block 285 | 286 | .sa-line 287 | position: absolute 288 | height: 5px 289 | width: 47px 290 | background-color: $red 291 | display: block 292 | top: 37px 293 | border-radius: 2px 294 | 295 | &.sa-left 296 | -webkit-transform: rotate(45deg) 297 | transform: rotate(45deg) 298 | left: 17px 299 | 300 | &.sa-right 301 | -webkit-transform: rotate(-45deg) 302 | transform: rotate(-45deg) 303 | right: 16px 304 | 305 | &.sa-warning 306 | border-color: $orange 307 | 308 | .sa-body 309 | // Exclamation mark body 310 | position: absolute 311 | width: 5px 312 | height: 47px 313 | left: 50% 314 | top: 10px 315 | -webkit-border-radius: 2px 316 | border-radius: 2px 317 | margin-left: -2px 318 | background-color: $orange 319 | 320 | .sa-dot 321 | // Exclamation mark dot 322 | position: absolute 323 | width: 7px 324 | height: 7px 325 | -webkit-border-radius: 50% 326 | border-radius: 50% 327 | margin-left: -3px 328 | left: 50% 329 | bottom: 10px 330 | background-color: $orange 331 | 332 | &.sa-info 333 | border-color: $blue 334 | 335 | &::before 336 | // i-letter body 337 | content: "" 338 | position: absolute 339 | width: 5px 340 | height: 29px 341 | left: 50% 342 | bottom: 17px 343 | border-radius: 2px 344 | margin-left: -2px 345 | background-color: $blue 346 | 347 | &::after 348 | // i-letter dot 349 | content: "" 350 | position: absolute 351 | width: 7px 352 | height: 7px 353 | border-radius: 50% 354 | margin-left: -3px 355 | top: 19px 356 | background-color: $blue 357 | 358 | &.sa-success 359 | border-color: $green 360 | 361 | &::before, &::after 362 | // Emulate moving circular line 363 | content: "" 364 | -webkit-border-radius: 40px 365 | border-radius: 40px 366 | border-radius: 50% 367 | position: absolute 368 | width: 60px 369 | height: 120px 370 | background: white 371 | -webkit-transform: rotate(45deg) 372 | transform: rotate(45deg) 373 | 374 | &::before 375 | -webkit-border-radius: 120px 0 0 120px 376 | border-radius: 120px 0 0 120px 377 | top: -7px 378 | left: -33px 379 | -webkit-transform: rotate(-45deg) 380 | transform: rotate(-45deg) 381 | -webkit-transform-origin: 60px 60px 382 | transform-origin: 60px 60px 383 | 384 | &::after 385 | -webkit-border-radius: 0 120px 120px 0 386 | border-radius: 0 120px 120px 0 387 | top: -11px 388 | left: 30px 389 | -webkit-transform: rotate(-45deg) 390 | transform: rotate(-45deg) 391 | -webkit-transform-origin: 0px 60px 392 | transform-origin: 0px 60px 393 | 394 | .sa-placeholder 395 | // Ring 396 | width: 80px 397 | height: 80px 398 | border: 4px solid rgba($green, 0.2) 399 | -webkit-border-radius: 40px 400 | border-radius: 40px 401 | border-radius: 50% 402 | box-sizing: content-box 403 | position: absolute 404 | left: -4px 405 | top: -4px 406 | z-index: 2 407 | 408 | .sa-fix 409 | // Hide corners left from animation 410 | width: 5px 411 | height: 90px 412 | background-color: white 413 | position: absolute 414 | left: 28px 415 | top: 8px 416 | z-index: 1 417 | -webkit-transform: rotate(-45deg) 418 | transform: rotate(-45deg) 419 | 420 | .sa-line 421 | height: 5px 422 | background-color: $green 423 | display: block 424 | border-radius: 2px 425 | position: absolute 426 | z-index: 2 427 | 428 | &.sa-tip 429 | width: 25px 430 | left: 14px 431 | top: 46px 432 | -webkit-transform: rotate(45deg) 433 | transform: rotate(45deg) 434 | 435 | &.sa-long 436 | width: 47px 437 | right: 8px 438 | top: 38px 439 | -webkit-transform: rotate(-45deg) 440 | transform: rotate(-45deg) 441 | 442 | &.sa-custom 443 | background-size: contain 444 | border-radius: 0 445 | border: none 446 | background-position: center center 447 | background-repeat: no-repeat 448 | 449 | =keyframes($animation-name) 450 | @-webkit-keyframes #{$animation-name} 451 | @content 452 | 453 | @keyframes #{$animation-name} 454 | @content 455 | 456 | =animation($str) 457 | -webkit-animation: #{$str} 458 | animation: #{$str} 459 | 460 | // Modal animation 461 | 462 | +keyframes(showSweetAlert) 463 | 0% 464 | transform: scale(0.7) 465 | -webkit-transform: scale(0.7) 466 | 467 | 45% 468 | transform: scale(1.05) 469 | -webkit-transform: scale(1.05) 470 | 471 | 80% 472 | transform: scale(0.95) 473 | -webkit-transform: scale(0.95) 474 | 475 | 100% 476 | transform: scale(1) 477 | -webkit-transform: scale(1) 478 | 479 | 480 | +keyframes(hideSweetAlert) 481 | 0% 482 | transform: scale(1) 483 | -webkit-transform: scale(1) 484 | 485 | 100% 486 | transform: scale(0.5) 487 | -webkit-transform: scale(0.5) 488 | 489 | 490 | +keyframes(slideFromTop) 491 | 0% 492 | top: 0% 493 | 494 | 100% 495 | top: 50% 496 | 497 | 498 | +keyframes(slideToTop) 499 | 0% 500 | top: 50% 501 | 502 | 100% 503 | top: 0% 504 | 505 | 506 | +keyframes(slideFromBottom) 507 | 0% 508 | top: 70% 509 | 510 | 100% 511 | top: 50% 512 | 513 | 514 | +keyframes(slideToBottom) 515 | 0% 516 | top: 50% 517 | 518 | 100% 519 | top: 70% 520 | 521 | 522 | .showSweetAlert 523 | &[data-animation=pop] 524 | +animation("showSweetAlert 0.3s") 525 | 526 | &[data-animation=none] 527 | +animation("none") 528 | 529 | &[data-animation=slide-from-top] 530 | +animation("slideFromTop 0.3s") 531 | 532 | &[data-animation=slide-from-bottom] 533 | +animation("slideFromBottom 0.3s") 534 | 535 | .hideSweetAlert 536 | &[data-animation=pop] 537 | +animation("hideSweetAlert 0.2s") 538 | 539 | &[data-animation=none] 540 | +animation("none") 541 | 542 | &[data-animation=slide-from-top] 543 | +animation("slideToTop 0.4s") 544 | 545 | &[data-animation=slide-from-bottom] 546 | +animation("slideToBottom 0.3s") 547 | 548 | // Success icon animation 549 | 550 | +keyframes(animateSuccessTip) 551 | 0% 552 | width: 0 553 | left: 1px 554 | top: 19px 555 | 556 | 54% 557 | width: 0 558 | left: 1px 559 | top: 19px 560 | 561 | 70% 562 | width: 50px 563 | left: -8px 564 | top: 37px 565 | 566 | 84% 567 | width: 17px 568 | left: 21px 569 | top: 48px 570 | 571 | 100% 572 | width: 25px 573 | left: 14px 574 | top: 45px 575 | 576 | 577 | +keyframes(animateSuccessLong) 578 | 0% 579 | width: 0 580 | right: 46px 581 | top: 54px 582 | 583 | 65% 584 | width: 0 585 | right: 46px 586 | top: 54px 587 | 588 | 84% 589 | width: 55px 590 | right: 0px 591 | top: 35px 592 | 593 | 100% 594 | width: 47px 595 | right: 8px 596 | top: 38px 597 | 598 | 599 | +keyframes(rotatePlaceholder) 600 | 0% 601 | transform: rotate(-45deg) 602 | -webkit-transform: rotate(-45deg) 603 | 604 | 5% 605 | transform: rotate(-45deg) 606 | -webkit-transform: rotate(-45deg) 607 | 608 | 12% 609 | transform: rotate(-405deg) 610 | -webkit-transform: rotate(-405deg) 611 | 612 | 100% 613 | transform: rotate(-405deg) 614 | -webkit-transform: rotate(-405deg) 615 | 616 | 617 | .animateSuccessTip 618 | +animation("animateSuccessTip 0.75s") 619 | 620 | .animateSuccessLong 621 | +animation("animateSuccessLong 0.75s") 622 | 623 | .sa-icon.sa-success.animate::after 624 | +animation("rotatePlaceholder 4.25s ease-in") 625 | 626 | // Error icon animation 627 | 628 | +keyframes(animateErrorIcon) 629 | 0% 630 | transform: rotateX(100deg) 631 | -webkit-transform: rotateX(100deg) 632 | opacity: 0 633 | 634 | 100% 635 | transform: rotateX(0deg) 636 | -webkit-transform: rotateX(0deg) 637 | opacity: 1 638 | 639 | 640 | .animateErrorIcon 641 | +animation("animateErrorIcon 0.5s") 642 | 643 | +keyframes(animateXMark) 644 | 0% 645 | transform: scale(0.4) 646 | -webkit-transform: scale(0.4) 647 | margin-top: 26px 648 | opacity: 0 649 | 650 | 50% 651 | transform: scale(0.4) 652 | -webkit-transform: scale(0.4) 653 | margin-top: 26px 654 | opacity: 0 655 | 656 | 80% 657 | transform: scale(1.15) 658 | -webkit-transform: scale(1.15) 659 | margin-top: -6px 660 | 661 | 100% 662 | transform: scale(1) 663 | -webkit-transform: scale(1) 664 | margin-top: 0 665 | opacity: 1 666 | 667 | 668 | .animateXMark 669 | +animation("animateXMark 0.5s") 670 | 671 | +keyframes(pulseWarning) 672 | 0% 673 | border-color: #F8D486 674 | 675 | 100% 676 | border-color: #F8BB86 677 | 678 | 679 | .pulseWarning 680 | +animation("pulseWarning 0.75s infinite alternate") 681 | 682 | +keyframes(pulseWarningIns) 683 | 0% 684 | background-color: #F8D486 685 | 686 | 100% 687 | background-color: #F8BB86 688 | 689 | 690 | .pulseWarningIns 691 | +animation("pulseWarningIns 0.75s infinite alternate") 692 | 693 | +keyframes(rotate-loading) 694 | 0% 695 | transform: rotate(0deg) 696 | 697 | 100% 698 | transform: rotate(360deg) -------------------------------------------------------------------------------- /client/assets/sass/vendor/_swiper.sass: -------------------------------------------------------------------------------- 1 | /* 2 | * Swiper 3.2.7 3 | * Most modern mobile touch slider and framework with hardware accelerated transitions 4 | * 5 | * http://www.idangero.us/swiper/ 6 | * 7 | * Copyright 2015, Vladimir Kharlampidi 8 | * The iDangero.us 9 | * http://www.idangero.us/ 10 | * 11 | * Licensed under MIT 12 | * 13 | * Released on: December 7, 2015 14 | */ 15 | 16 | .swiper-container 17 | margin: 0 auto 18 | position: relative 19 | overflow: hidden 20 | z-index: 1 21 | 22 | .swiper-container-no-flexbox .swiper-slide 23 | float: left 24 | 25 | .swiper-container-vertical > .swiper-wrapper 26 | -webkit-box-orient: vertical 27 | -moz-box-orient: vertical 28 | -ms-flex-direction: column 29 | -webkit-flex-direction: column 30 | flex-direction: column 31 | 32 | .swiper-wrapper 33 | position: relative 34 | width: 100% 35 | height: 100% 36 | z-index: 1 37 | display: -webkit-box 38 | display: -moz-box 39 | display: -ms-flexbox 40 | display: -webkit-flex 41 | display: flex 42 | -webkit-transition-property: -webkit-transform 43 | -moz-transition-property: -moz-transform 44 | -o-transition-property: -o-transform 45 | -ms-transition-property: -ms-transform 46 | transition-property: transform 47 | -webkit-box-sizing: content-box 48 | -moz-box-sizing: content-box 49 | box-sizing: content-box 50 | 51 | .swiper-container-android .swiper-slide, .swiper-wrapper 52 | -webkit-transform: translate3d(0px, 0, 0) 53 | -moz-transform: translate3d(0px, 0, 0) 54 | -o-transform: translate(0px, 0px) 55 | -ms-transform: translate3d(0px, 0, 0) 56 | transform: translate3d(0px, 0, 0) 57 | 58 | .swiper-container-multirow > .swiper-wrapper 59 | -webkit-box-lines: multiple 60 | -moz-box-lines: multiple 61 | -ms-flex-wrap: wrap 62 | -webkit-flex-wrap: wrap 63 | flex-wrap: wrap 64 | 65 | .swiper-container-free-mode > .swiper-wrapper 66 | -webkit-transition-timing-function: ease-out 67 | -moz-transition-timing-function: ease-out 68 | -ms-transition-timing-function: ease-out 69 | -o-transition-timing-function: ease-out 70 | transition-timing-function: ease-out 71 | margin: 0 auto 72 | 73 | .swiper-slide 74 | -webkit-flex-shrink: 0 75 | -ms-flex: 0 0 auto 76 | flex-shrink: 0 77 | width: 100% 78 | height: 100% 79 | position: relative 80 | 81 | // Auto Height 82 | 83 | .swiper-container-autoheight 84 | height: auto 85 | .swiper-slide 86 | height: auto 87 | .swiper-wrapper 88 | -webkit-box-align: start 89 | -ms-flex-align: start 90 | -webkit-align-items: flex-start 91 | align-items: flex-start 92 | -webkit-transition-property: -webkit-transform, height 93 | -moz-transition-property: -moz-transform 94 | -o-transition-property: -o-transform 95 | -ms-transition-property: -ms-transform 96 | transition-property: transform, height 97 | 98 | // a11y 99 | 100 | .swiper-container .swiper-notification 101 | position: absolute 102 | left: 0 103 | top: 0 104 | pointer-events: none 105 | opacity: 0 106 | z-index: -1000 107 | 108 | // IE10 Windows Phone 8 Fixes 109 | 110 | .swiper-wp8-horizontal 111 | -ms-touch-action: pan-y 112 | touch-action: pan-y 113 | 114 | .swiper-wp8-vertical 115 | -ms-touch-action: pan-x 116 | touch-action: pan-x 117 | 118 | // Arrows 119 | 120 | .swiper-button-prev, .swiper-button-next 121 | position: absolute 122 | top: 50% 123 | width: 27px 124 | height: 44px 125 | margin-top: -22px 126 | z-index: 10 127 | cursor: pointer 128 | -moz-background-size: 27px 44px 129 | -webkit-background-size: 27px 44px 130 | background-size: 27px 44px 131 | background-position: center 132 | background-repeat: no-repeat 133 | 134 | .swiper-button-prev.swiper-button-disabled, .swiper-button-next.swiper-button-disabled 135 | opacity: 0.35 136 | cursor: auto 137 | pointer-events: none 138 | 139 | .swiper-button-prev, .swiper-container-rtl .swiper-button-next 140 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23408bc2'%2F%3E%3C%2Fsvg%3E") 141 | left: 10px 142 | right: auto 143 | 144 | .swiper-button-prev.swiper-button-black, .swiper-container-rtl .swiper-button-next.swiper-button-black 145 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23000000'%2F%3E%3C%2Fsvg%3E") 146 | 147 | .swiper-button-prev.swiper-button-white, .swiper-container-rtl .swiper-button-next.swiper-button-white 148 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23ffffff'%2F%3E%3C%2Fsvg%3E") 149 | 150 | .swiper-button-next, .swiper-container-rtl .swiper-button-prev 151 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23408bc2'%2F%3E%3C%2Fsvg%3E") 152 | right: 10px 153 | left: auto 154 | 155 | .swiper-button-next.swiper-button-black, .swiper-container-rtl .swiper-button-prev.swiper-button-black 156 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23000000'%2F%3E%3C%2Fsvg%3E") 157 | 158 | .swiper-button-next.swiper-button-white, .swiper-container-rtl .swiper-button-prev.swiper-button-white 159 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23ffffff'%2F%3E%3C%2Fsvg%3E") 160 | 161 | // Pagination Styles 162 | 163 | .swiper-pagination 164 | position: absolute 165 | text-align: center 166 | -webkit-transition: 300ms 167 | -moz-transition: 300ms 168 | -o-transition: 300ms 169 | transition: 300ms 170 | -webkit-transform: translate3d(0, 0, 0) 171 | -ms-transform: translate3d(0, 0, 0) 172 | -o-transform: translate3d(0, 0, 0) 173 | transform: translate3d(0, 0, 0) 174 | z-index: 10 175 | &.swiper-pagination-hidden 176 | opacity: 0 177 | 178 | .swiper-pagination-bullet 179 | width: 8px 180 | height: 8px 181 | display: inline-block 182 | border-radius: 100% 183 | background: #000 184 | opacity: 0.2 185 | 186 | button.swiper-pagination-bullet 187 | border: none 188 | margin: 0 189 | padding: 0 190 | box-shadow: none 191 | -moz-appearance: none 192 | -ms-appearance: none 193 | -webkit-appearance: none 194 | appearance: none 195 | 196 | .swiper-pagination-clickable .swiper-pagination-bullet 197 | cursor: pointer 198 | 199 | .swiper-pagination-white .swiper-pagination-bullet 200 | background: #fff 201 | 202 | .swiper-pagination-bullet-active 203 | opacity: 1 204 | background: #408bc2 205 | 206 | .swiper-pagination-white .swiper-pagination-bullet-active 207 | background: #fff 208 | 209 | .swiper-pagination-black .swiper-pagination-bullet-active 210 | background: #000 211 | 212 | .swiper-container-vertical > .swiper-pagination 213 | right: 10px 214 | top: 50% 215 | -webkit-transform: translate3d(0px, -50%, 0) 216 | -moz-transform: translate3d(0px, -50%, 0) 217 | -o-transform: translate(0px, -50%) 218 | -ms-transform: translate3d(0px, -50%, 0) 219 | transform: translate3d(0px, -50%, 0) 220 | .swiper-pagination-bullet 221 | margin: 5px 0 222 | display: block 223 | 224 | .swiper-container-horizontal > .swiper-pagination 225 | bottom: 10px 226 | left: 0 227 | width: 100% 228 | .swiper-pagination-bullet 229 | margin: 0 5px 230 | 231 | // 3D Container 232 | 233 | .swiper-container-3d 234 | -webkit-perspective: 1200px 235 | -moz-perspective: 1200px 236 | -o-perspective: 1200px 237 | perspective: 1200px 238 | .swiper-wrapper, .swiper-slide, .swiper-slide-shadow-left, .swiper-slide-shadow-right, .swiper-slide-shadow-top, .swiper-slide-shadow-bottom, .swiper-cube-shadow 239 | -webkit-transform-style: preserve-3d 240 | -moz-transform-style: preserve-3d 241 | -ms-transform-style: preserve-3d 242 | transform-style: preserve-3d 243 | .swiper-slide-shadow-left, .swiper-slide-shadow-right, .swiper-slide-shadow-top, .swiper-slide-shadow-bottom 244 | position: absolute 245 | left: 0 246 | top: 0 247 | width: 100% 248 | height: 100% 249 | pointer-events: none 250 | z-index: 10 251 | .swiper-slide-shadow-left 252 | background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0))) 253 | // Safari 4+, Chrome 254 | background-image: -webkit-linear-gradient(right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 255 | // Chrome 10+, Safari 5.1+, iOS 5+ 256 | background-image: -moz-linear-gradient(right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 257 | // Firefox 3.6-15 258 | background-image: -o-linear-gradient(right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 259 | // Opera 11.10-12.00 260 | background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 261 | // Firefox 16+, IE10, Opera 12.50+ 262 | .swiper-slide-shadow-right 263 | background-image: -webkit-gradient(linear, right top, left top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0))) 264 | // Safari 4+, Chrome 265 | background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 266 | // Chrome 10+, Safari 5.1+, iOS 5+ 267 | background-image: -moz-linear-gradient(left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 268 | // Firefox 3.6-15 269 | background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 270 | // Opera 11.10-12.00 271 | background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 272 | // Firefox 16+, IE10, Opera 12.50+ 273 | .swiper-slide-shadow-top 274 | background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0))) 275 | // Safari 4+, Chrome 276 | background-image: -webkit-linear-gradient(bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 277 | // Chrome 10+, Safari 5.1+, iOS 5+ 278 | background-image: -moz-linear-gradient(bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 279 | // Firefox 3.6-15 280 | background-image: -o-linear-gradient(bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 281 | // Opera 11.10-12.00 282 | background-image: linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 283 | // Firefox 16+, IE10, Opera 12.50+ 284 | .swiper-slide-shadow-bottom 285 | background-image: -webkit-gradient(linear, left bottom, left top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0))) 286 | // Safari 4+, Chrome 287 | background-image: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 288 | // Chrome 10+, Safari 5.1+, iOS 5+ 289 | background-image: -moz-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 290 | // Firefox 3.6-15 291 | background-image: -o-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 292 | // Opera 11.10-12.00 293 | background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 294 | // Firefox 16+, IE10, Opera 12.50+ 295 | 296 | // Coverflow 297 | 298 | .swiper-container-coverflow .swiper-wrapper 299 | // Windows 8 IE 10 fix 300 | -ms-perspective: 1200px 301 | 302 | // Fade 303 | 304 | .swiper-container-fade 305 | &.swiper-container-free-mode .swiper-slide 306 | -webkit-transition-timing-function: ease-out 307 | -moz-transition-timing-function: ease-out 308 | -ms-transition-timing-function: ease-out 309 | -o-transition-timing-function: ease-out 310 | transition-timing-function: ease-out 311 | .swiper-slide 312 | pointer-events: none 313 | .swiper-slide 314 | pointer-events: none 315 | .swiper-slide-active 316 | pointer-events: auto 317 | .swiper-slide-active 318 | pointer-events: auto 319 | 320 | // Cube 321 | 322 | .swiper-container-cube 323 | overflow: visible 324 | .swiper-slide 325 | pointer-events: none 326 | visibility: hidden 327 | -webkit-transform-origin: 0 0 328 | -moz-transform-origin: 0 0 329 | -ms-transform-origin: 0 0 330 | transform-origin: 0 0 331 | -webkit-backface-visibility: hidden 332 | -moz-backface-visibility: hidden 333 | -ms-backface-visibility: hidden 334 | backface-visibility: hidden 335 | width: 100% 336 | height: 100% 337 | z-index: 1 338 | &.swiper-container-rtl .swiper-slide 339 | -webkit-transform-origin: 100% 0 340 | -moz-transform-origin: 100% 0 341 | -ms-transform-origin: 100% 0 342 | transform-origin: 100% 0 343 | .swiper-slide-active, .swiper-slide-next, .swiper-slide-prev, .swiper-slide-next + .swiper-slide 344 | pointer-events: auto 345 | visibility: visible 346 | .swiper-slide-shadow-top, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left, .swiper-slide-shadow-right 347 | z-index: 0 348 | -webkit-backface-visibility: hidden 349 | -moz-backface-visibility: hidden 350 | -ms-backface-visibility: hidden 351 | backface-visibility: hidden 352 | .swiper-cube-shadow 353 | position: absolute 354 | left: 0 355 | bottom: 0px 356 | width: 100% 357 | height: 100% 358 | background: #000 359 | opacity: 0.6 360 | -webkit-filter: blur(50px) 361 | filter: blur(50px) 362 | z-index: 0 363 | 364 | // Scrollbar 365 | 366 | .swiper-scrollbar 367 | border-radius: 10px 368 | position: relative 369 | -ms-touch-action: none 370 | background: rgba(0, 0, 0, 0.1) 371 | 372 | .swiper-container-horizontal > .swiper-scrollbar 373 | position: absolute 374 | left: 1% 375 | bottom: 3px 376 | z-index: 50 377 | height: 5px 378 | width: 98% 379 | 380 | .swiper-container-vertical > .swiper-scrollbar 381 | position: absolute 382 | right: 3px 383 | top: 1% 384 | z-index: 50 385 | width: 5px 386 | height: 98% 387 | 388 | .swiper-scrollbar-drag 389 | height: 100% 390 | width: 100% 391 | position: relative 392 | background: rgba(0, 0, 0, 0.5) 393 | border-radius: 10px 394 | left: 0 395 | top: 0 396 | 397 | .swiper-scrollbar-cursor-drag 398 | cursor: move 399 | 400 | // Preloader 401 | 402 | .swiper-lazy-preloader 403 | width: 42px 404 | height: 42px 405 | position: absolute 406 | left: 50% 407 | top: 50% 408 | margin-left: -21px 409 | margin-top: -21px 410 | z-index: 10 411 | -webkit-transform-origin: 50% 412 | -moz-transform-origin: 50% 413 | transform-origin: 50% 414 | -webkit-animation: swiper-preloader-spin 1s steps(12, end) infinite 415 | -moz-animation: swiper-preloader-spin 1s steps(12, end) infinite 416 | animation: swiper-preloader-spin 1s steps(12, end) infinite 417 | &:after 418 | display: block 419 | content: "" 420 | width: 100% 421 | height: 100% 422 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20viewBox%3D'0%200%20120%20120'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20xmlns%3Axlink%3D'http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink'%3E%3Cdefs%3E%3Cline%20id%3D'l'%20x1%3D'60'%20x2%3D'60'%20y1%3D'7'%20y2%3D'27'%20stroke%3D'%236c6c6c'%20stroke-width%3D'11'%20stroke-linecap%3D'round'%2F%3E%3C%2Fdefs%3E%3Cg%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(30%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(60%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(90%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(120%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(150%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.37'%20transform%3D'rotate(180%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.46'%20transform%3D'rotate(210%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.56'%20transform%3D'rotate(240%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.66'%20transform%3D'rotate(270%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.75'%20transform%3D'rotate(300%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.85'%20transform%3D'rotate(330%2060%2C60)'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") 423 | background-position: 50% 424 | -webkit-background-size: 100% 425 | background-size: 100% 426 | background-repeat: no-repeat 427 | 428 | .swiper-lazy-preloader-white:after 429 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20viewBox%3D'0%200%20120%20120'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20xmlns%3Axlink%3D'http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink'%3E%3Cdefs%3E%3Cline%20id%3D'l'%20x1%3D'60'%20x2%3D'60'%20y1%3D'7'%20y2%3D'27'%20stroke%3D'%23fff'%20stroke-width%3D'11'%20stroke-linecap%3D'round'%2F%3E%3C%2Fdefs%3E%3Cg%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(30%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(60%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(90%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(120%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(150%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.37'%20transform%3D'rotate(180%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.46'%20transform%3D'rotate(210%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.56'%20transform%3D'rotate(240%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.66'%20transform%3D'rotate(270%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.75'%20transform%3D'rotate(300%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.85'%20transform%3D'rotate(330%2060%2C60)'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") 430 | 431 | @-webkit-keyframes swiper-preloader-spin 432 | 100% 433 | -webkit-transform: rotate(360deg) 434 | 435 | @keyframes swiper-preloader-spin 436 | 100% 437 | transform: rotate(360deg) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riot-app-example", 3 | "version": "1.0.0", 4 | "description": "riot demo app", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "test", 8 | "build": "rollup -c rollup.config.js", 9 | "start": "node server/index.js", 10 | "watch": "nodemon -e js,tag --watch shared/components/src --watch client/assets/js/src -x 'npm run build'", 11 | "watch-server": "nodemon -e js,tag --watch server --watch shared/components/src -x 'npm run start'" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/GianlucaGuarini/riot-app-example.git" 16 | }, 17 | "keywords": [ 18 | "riot", 19 | "app", 20 | "example", 21 | "demo" 22 | ], 23 | "author": "Gianluca Guarini (http://gianlucaguarini.com/)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/GianlucaGuarini/riot-app-example/issues" 27 | }, 28 | "homepage": "https://github.com/GianlucaGuarini/riot-app-example#readme", 29 | "dependencies": { 30 | "nprogress": "^0.2.0", 31 | "riot": "^3.0.5", 32 | "riot-route": "^3.0.2", 33 | "sweetalert": "^1.1.3", 34 | "swiper": "^3.4.1", 35 | "velocity-animate": "^1.4.0" 36 | }, 37 | "devDependencies": { 38 | "autoprefixer": "^6.6.0", 39 | "casual": "^1.5.8", 40 | "ejs": "^2.5.5", 41 | "express": "^4.14.0", 42 | "node-sass": "^4.1.1", 43 | "nodemon": "^1.11.0", 44 | "postcss": "^5.2.6", 45 | "reify": "^0.4.4", 46 | "rollup": "^0.38.0", 47 | "rollup-plugin-buble": "^0.15.0", 48 | "rollup-plugin-commonjs": "^6.0.1", 49 | "rollup-plugin-inject": "^2.0.0", 50 | "rollup-plugin-node-resolve": "^2.0.0", 51 | "rollup-plugin-riot": "^1.1.0", 52 | "socket.io": "^1.7.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Simple isomorphic riot app 2 | 3 | ## Philosophy 4 | 5 | Use mostly your mind and only the tools your app really needs: riot allows you doing both things! 6 | 7 | ## Info 8 | 9 | This app is really simple and it may not be perfect because I have coded it in a couple of days but it's enough to answer to the following FAQ: 10 | 11 | - How can I render riot components on the server? 12 | - How can I dispatch events across several riot components? 13 | - How can I store/manage my data state? 14 | - How can I share the same router paths on the client and on the server? 15 | - How can I create a bundle file building my riot tags for my production app? 16 | - How can I use es6 with my riot tags? 17 | 18 | ## Setup 19 | 20 | ```bash 21 | $ npm install 22 | ``` 23 | 24 | ## Run 25 | 26 | ```bash 27 | $ npm start 28 | ``` 29 | 30 | ## Develop 31 | 32 | ```bash 33 | $ npm run watch # watch the client js files 34 | $ npm run watch-server # watch the server js files 35 | ``` 36 | 37 | ## TODO 38 | 39 | - Simplify the app logic that at moment is a bit too complicate for what it does 40 | - Use always immutable data avoiding manipulating objects 41 | - Implement redux like events api 42 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import buble from 'rollup-plugin-buble' 4 | import riot from 'rollup-plugin-riot' 5 | import inject from 'rollup-plugin-inject' 6 | 7 | export default { 8 | entry: 'client/assets/js/src/index.js', 9 | dest: 'client/assets/js/bundle.js', 10 | format: 'umd', 11 | plugins: [ 12 | riot(), 13 | buble(), 14 | commonjs(), 15 | nodeResolve({ 16 | jsnext: true, 17 | main: true 18 | }), 19 | inject({ 20 | swal: 'sweetalert', 21 | Swiper: 'swiper', 22 | Velocity: 'velocity-animate' 23 | }) 24 | ] 25 | } -------------------------------------------------------------------------------- /server/api-router.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | const router = express.Router() 4 | 5 | // fake json api 6 | // these data are supposed to come from db queries... 7 | 8 | router.get('/index', function (req, res) { 9 | res.send(require('./json-data/index.json')) 10 | }) 11 | 12 | router.get('/feed', function (req, res) { 13 | res.send(require('./json-data/feed.json')) 14 | }) 15 | 16 | router.get('/login', function (req, res) { 17 | res.send(require('./json-data/login.json')) 18 | }) 19 | 20 | router.get('/gallery', function (req, res) { 21 | res.send(require('./json-data/gallery.json')) 22 | }) 23 | 24 | export default router -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import routes from '../shared/routes' 3 | import apiRouter from './api-router' 4 | import sassMiddleware from './middlewares/sass' 5 | import FeedWebsockets from './feed-websockets' 6 | import User from '../shared/models/User' 7 | 8 | import { join } from 'path' 9 | import { 10 | HOST_NAME, 11 | PORT, 12 | STATIC_FOLDER, 13 | TAGS_FOLDER, 14 | VIEWS_FOLDER, 15 | VIEWS_ENGINE 16 | } from '../shared/config' 17 | 18 | import { render } from 'riot' 19 | import '../shared/components/app.tag' 20 | 21 | const app = express(), 22 | BASE = __dirname 23 | 24 | var server, feedWebsockets 25 | 26 | // static template engine 27 | app.set('views', join(BASE, VIEWS_FOLDER)) 28 | app.set('view engine', VIEWS_ENGINE) 29 | 30 | // auto compile the sass files on any request 31 | app.use(/\.sass$/, sassMiddleware(join(BASE, STATIC_FOLDER))) 32 | 33 | // set the app routes receiving the view to render and its data 34 | Object.keys(routes).forEach(function(route) { 35 | app.get(route, function (req, res) { 36 | // pass all the url variables as arguments 37 | var gateway = routes[route](...Object.keys(req.params).map((k) => req.params[k])) 38 | 39 | gateway 40 | .fetch() 41 | .then(function(data) { 42 | data.gateway = gateway 43 | data.user = new User() 44 | res.render('base', { 45 | initialData: JSON.stringify(data), 46 | body: render('app', data) 47 | }) 48 | }) 49 | }) 50 | }) 51 | 52 | // set the api router 53 | app.use('/api', apiRouter) 54 | 55 | // set the folder containing the static files 56 | app.use(express.static(join(BASE, STATIC_FOLDER))) 57 | 58 | // start the server 59 | server = app.listen(PORT, HOST_NAME, function() { 60 | console.log('Example app listening at http://%s:%s', HOST_NAME, PORT) 61 | }) 62 | 63 | // add some fancy websockets to this server 64 | feedWebsockets = new FeedWebsockets(server) -------------------------------------------------------------------------------- /server/feed-websockets.js: -------------------------------------------------------------------------------- 1 | import socketIO from 'socket.io' 2 | import casual from 'casual' 3 | 4 | export default function(server) { 5 | 6 | var io = socketIO.listen(server) 7 | 8 | io.on('connection', function(socket) { 9 | 10 | // start emitting fake data 11 | var pollingTimer 12 | pollingTimer = setInterval(() => { 13 | socket.emit('news', { 14 | title: casual.title, 15 | description: casual.description, 16 | image: `https://placeimg.com/720/280/any?${ new Date().getMilliseconds() }` 17 | }) 18 | }, 3000) 19 | 20 | socket.on('disconnect', () => clearTimeout(pollingTimer)) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | require('reify/repl') // enablr the es6 modules also in node 2 | global.IS_SERVER = true 3 | global.IS_CLIENT = false 4 | require('./app') -------------------------------------------------------------------------------- /server/json-data/feed.json: -------------------------------------------------------------------------------- 1 | { 2 | "view": "feed", 3 | "data": { 4 | "title": "Welcome to the live news feed", 5 | "message": "I got some awesome news today for you, ( it is using websockets )", 6 | "news": [] 7 | } 8 | } -------------------------------------------------------------------------------- /server/json-data/gallery.json: -------------------------------------------------------------------------------- 1 | { 2 | "view": "gallery", 3 | "data": { 4 | "title": "Welcome to image gallery", 5 | "message": "Look at these beautiful pictures, note that the image current index will be preserved across the navigation", 6 | "images": [ 7 | "https://placeimg.com/1024/520/any?1", 8 | "https://placeimg.com/1024/520/any?2", 9 | "https://placeimg.com/1024/520/any?3", 10 | "https://placeimg.com/1024/520/any?4", 11 | "https://placeimg.com/1024/520/any?5", 12 | "https://placeimg.com/1024/520/any?6", 13 | "https://placeimg.com/1024/520/any?7", 14 | "https://placeimg.com/1024/520/any?8", 15 | "https://placeimg.com/1024/520/any?9" 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /server/json-data/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "view": "index", 3 | "data": { 4 | "title": "This is the homepage", 5 | "message": "Simple polymorphic demo app to show you how to preserve your app state without using any fancy framework" 6 | } 7 | } -------------------------------------------------------------------------------- /server/json-data/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "view": "login", 3 | "data": { 4 | "title": "Fake User Login", 5 | "message": "Here you can simulate a fake use login screen dispatching global events" 6 | } 7 | } -------------------------------------------------------------------------------- /server/middlewares/sass.js: -------------------------------------------------------------------------------- 1 | import sass from 'node-sass' 2 | import { join } from 'path' 3 | import postcss from 'postcss' 4 | import autoprefixer from 'autoprefixer' 5 | 6 | var prefixer = postcss([autoprefixer]) 7 | 8 | // auto compile the sass files on any request 9 | export default function(STATIC_FOLDER) { 10 | return function (req, res, next) { 11 | sass.render({ 12 | file: join(STATIC_FOLDER, req.originalUrl), 13 | indentedSyntax: false, 14 | omitSourceMapUrl: true 15 | }, function(err, result) { 16 | if (!err) 17 | prefixer 18 | .process(result.css.toString()) 19 | .then(function (prefixed) { 20 | res.setHeader('content-type', 'text/css') 21 | res.send(prefixed.css) 22 | next() 23 | }) 24 | else 25 | throw new Error(err) 26 | }) 27 | } 28 | } -------------------------------------------------------------------------------- /server/views/base.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Riot app example 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | <%- body %> 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /shared/components/app.tag: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | 7 | 8 | 72 |
-------------------------------------------------------------------------------- /shared/components/layout/sidebar.tag: -------------------------------------------------------------------------------- 1 | 2 | 18 |
19 | 20 |
21 | 22 | 26 |
-------------------------------------------------------------------------------- /shared/components/layout/user-status.tag: -------------------------------------------------------------------------------- 1 | 2 | 3 | User: { user.name } 4 | 5 | 6 | 7 | You are not
logged! 8 |
9 | 10 | 16 |
-------------------------------------------------------------------------------- /shared/components/mixins.js: -------------------------------------------------------------------------------- 1 | import { mixin } from 'riot' 2 | 3 | // add special animation features to the current tag instance 4 | mixin('animation-features', { 5 | features: { Velocity }, 6 | defaultTransitions: { 7 | in: 'transition.slideUpIn', 8 | out: 'transition.slideUpOut' 9 | }, 10 | moveIn: function (el) { 11 | return Velocity(el, this.defaultTransitions.in) 12 | }, 13 | moveOut: function (el) { 14 | return Velocity(el, this.defaultTransitions.out) 15 | } 16 | }) 17 | 18 | -------------------------------------------------------------------------------- /shared/components/pages/feed.tag: -------------------------------------------------------------------------------- 1 | 2 |

{ opts.data.title }

3 |

{ opts.data.message }

4 |

Loading the news... ( new news any 3 seconds )

5 | 12 | 29 |
-------------------------------------------------------------------------------- /shared/components/pages/gallery.tag: -------------------------------------------------------------------------------- 1 | 2 |

{ opts.data.title }

3 |

{ opts.data.message }

4 | 5 |
6 | 7 |
8 | 9 |
10 | 11 |
12 |
13 |
14 | 15 |
16 | 17 |
18 |
19 |
20 | 62 |
-------------------------------------------------------------------------------- /shared/components/pages/index.tag: -------------------------------------------------------------------------------- 1 | 2 |

{ opts.data.title }

3 |

{ opts.data.message }

4 |
-------------------------------------------------------------------------------- /shared/components/pages/login.tag: -------------------------------------------------------------------------------- 1 | 2 |

{ opts.data.title }

3 |

{ opts.data.message }

4 |
5 | 9 | 13 | 16 |
17 | 18 |

19 | Welcome dear { user.name } 20 |

21 | 22 | 51 |
-------------------------------------------------------------------------------- /shared/config.js: -------------------------------------------------------------------------------- 1 | // App configuration constants 2 | 3 | export const PORT = 3000 4 | export const HOST_NAME = 'localhost' 5 | export const STATIC_FOLDER = '../client/assets' 6 | export const VIEWS_FOLDER = '../server/views' 7 | export const VIEWS_ENGINE = 'ejs' -------------------------------------------------------------------------------- /shared/gateways/FeedGateway.js: -------------------------------------------------------------------------------- 1 | import Gateway from '../models/GatewayModel' 2 | import { getBase } from '../helpers' 3 | 4 | export default class extends Gateway { 5 | listen() { 6 | // no need to run this on the server 7 | if (IS_SERVER) return 8 | this.socket = io.connect(getBase(), { forceNew: true }) 9 | this.socket.on('news', (news) => { 10 | this.trigger('news::published', news) 11 | }) 12 | } 13 | } -------------------------------------------------------------------------------- /shared/gateways/GalleryGateway.js: -------------------------------------------------------------------------------- 1 | import Gateway from '../models/GatewayModel' 2 | 3 | export default class extends Gateway { 4 | set slideId(id) { 5 | if (id == this._slideId) return 6 | this._slideId = +id // typecast to number 7 | this.trigger('slide::changed', this._slideId) 8 | } 9 | get slideId() { 10 | return this._slideId || 1 11 | } 12 | } -------------------------------------------------------------------------------- /shared/gateways/IndexGateway.js: -------------------------------------------------------------------------------- 1 | import Gateway from '../models/GatewayModel' 2 | 3 | export default class extends Gateway { 4 | } -------------------------------------------------------------------------------- /shared/gateways/LoginGateway.js: -------------------------------------------------------------------------------- 1 | import Gateway from '../models/GatewayModel' 2 | 3 | export default class extends Gateway { 4 | } -------------------------------------------------------------------------------- /shared/helpers/index.js: -------------------------------------------------------------------------------- 1 | import { HOST_NAME, PORT } from '../config' 2 | import { get } from 'http' 3 | 4 | /** 5 | * Private methods 6 | */ 7 | 8 | function _ajaxRequest(url) { 9 | return new Promise(function(resolve, reject) { 10 | var request = new XMLHttpRequest() 11 | request.open('GET', url, true) 12 | request.onload = function() { 13 | if (request.status >= 200 && request.status < 400) 14 | resolve(request.responseText) 15 | else 16 | reject(request) 17 | 18 | } 19 | request.onerror = reject 20 | request.send() 21 | }) 22 | } 23 | 24 | function _nodeRequest(url, hostname, port) { 25 | 26 | var options = { 27 | host: HOST_NAME, 28 | port: PORT, 29 | path: url 30 | } 31 | return new Promise(function(resolve, reject) { 32 | get(options, function(res, body) { 33 | res.on('data', function (chunk) { 34 | resolve(chunk) 35 | }) 36 | }).on('error', function(e) { 37 | reject(e) 38 | }) 39 | }) 40 | } 41 | 42 | /** 43 | * Public methods 44 | */ 45 | 46 | /** 47 | * Get the base url of the project reading the config variables 48 | * @param { String } hostname - to override the hostname 49 | * @param { Number } port - to override the port 50 | * @returns { String } return the base path of the app 51 | */ 52 | export function getBase(hostname, port) { 53 | return `http://${ hostname || HOST_NAME }:${ port || PORT }` 54 | } 55 | 56 | /** 57 | * Detect if we are running the code on a node environment 58 | * @returns {Boolean} - either true or false 59 | */ 60 | function isNode() { 61 | return typeof window === 'undefined' 62 | } 63 | 64 | /** 65 | * Cheap fetch polyfill that works also on node because whatwg-fetch sucks! 66 | * @param { String } url - url to call 67 | * @param { String } hostname - optional hostname 68 | * @param { Number } port - optional port 69 | * @returns { Promise } hoping to get this promise resolved.. 70 | */ 71 | export function fetch(url, hostname, port) { 72 | if (isNode()) { 73 | return _nodeRequest(url, hostname, port) 74 | } else { 75 | return _ajaxRequest(`${ getBase(hostname, port) }${ url }`) 76 | } 77 | } -------------------------------------------------------------------------------- /shared/models/GatewayModel.js: -------------------------------------------------------------------------------- 1 | import { fetch } from '../helpers' 2 | import { observable } from 'riot' 3 | 4 | // cached data 5 | export default class { 6 | constructor(opts) { 7 | observable(this) 8 | this.url = opts.url 9 | this.wasFetched = false 10 | this._data = null 11 | } 12 | // fetch new data from the api caching the result 13 | fetch() { 14 | // was it already fetched 15 | if (!this.wasFetched) { 16 | this.trigger('fetching') 17 | return fetch(this.url) 18 | .then((res) => { 19 | var data = JSON.parse(res) 20 | this._data = Object.assign({}, data) // clone the original data 21 | this.wasFetched = true 22 | this.trigger('fetched', data) 23 | return data 24 | }) 25 | } else 26 | return Promise.resolve(this._data) 27 | } 28 | } -------------------------------------------------------------------------------- /shared/models/User.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor(name = 'Jack') { 3 | this.name = name 4 | this.isLogged = false 5 | } 6 | // fake authentication 7 | authenticate(email, password) { 8 | if (password == 'foo') { 9 | return this.isLogged = true 10 | } 11 | else 12 | return 'The password was wrong try with "foo"' 13 | } 14 | } -------------------------------------------------------------------------------- /shared/routes.js: -------------------------------------------------------------------------------- 1 | import IndexGateway from './gateways/IndexGateway' 2 | import FeedGateway from './gateways/FeedGateway' 3 | import LoginGateway from './gateways/LoginGateway' 4 | import GalleryGateway from './gateways/GalleryGateway' 5 | 6 | // cache the gateways instances 7 | var gateways = {}, 8 | // cache the gateways avoiding to fetch the same data twice 9 | getGateway = function(name, Gateway, opts) { 10 | if (gateways[name] && IS_CLIENT) 11 | return gateways[name] 12 | else { 13 | // cache the gateway 14 | gateways[name] = new Gateway(opts) 15 | return gateways[name] 16 | } 17 | } 18 | 19 | export default { 20 | '/': function() { 21 | return getGateway('index', IndexGateway, { url: '/api/index' }) 22 | }, 23 | '/feed': function() { 24 | return getGateway('feed', FeedGateway, { url: '/api/feed' }) 25 | }, 26 | '/login': function() { 27 | return getGateway('login', LoginGateway, { url: '/api/login' }) 28 | }, 29 | '/gallery': function() { 30 | return getGateway('gallery', GalleryGateway, { url: '/api/gallery' }) 31 | }, 32 | '/gallery/*': function(id) { 33 | var galleryGateway = getGateway('gallery', GalleryGateway, { url: '/api/gallery' }) 34 | galleryGateway.slideId = id // set the slide id 35 | return galleryGateway 36 | } 37 | } --------------------------------------------------------------------------------