├── .gitignore ├── README.md ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── test.exs └── webpack.config.js ├── karma.conf.js ├── lib ├── mix │ └── tasks │ │ └── digest.ex ├── phoenixReact.ex └── phoenixReact │ ├── endpoint.ex │ ├── guardian_hooks.ex │ ├── guardian_serializer.ex │ └── repo.ex ├── mix.exs ├── mix.lock ├── package.json ├── priv ├── repo │ ├── migrations │ │ └── 20150907192802_create_user.exs │ └── seeds.exs └── static │ ├── css │ ├── app-7c1659d1c5aa51ffb74a4ea907ca3c5e.css │ ├── app-7c1659d1c5aa51ffb74a4ea907ca3c5e.css.gz │ ├── app.css │ └── app.css.gz │ ├── favicon-a8ca4e3a2bb8fea46a9ee9e102e7d3eb.ico │ ├── favicon.ico │ ├── images │ ├── phoenix-5bd99a0d17dd41bc9d9bf6840abcc089.png │ └── phoenix.png │ ├── js │ ├── app-d41d8cd98f00b204e9800998ecf8427e.js │ ├── app-d41d8cd98f00b204e9800998ecf8427e.js.gz │ ├── app.js │ ├── app.js.gz │ ├── bundle-ad70171bde621994bc6077e834531309.js │ ├── bundle-ad70171bde621994bc6077e834531309.js.gz │ ├── bundle.js │ ├── bundle.js.gz │ ├── phoenix-a68af163b7e7f033ce9bf3c75afaeb28.js │ ├── phoenix-a68af163b7e7f033ce9bf3c75afaeb28.js.gz │ ├── phoenix.js │ └── phoenix.js.gz │ ├── manifest.json │ ├── robots-067185ba27a5d9139b10a759679045bf.txt │ ├── robots-067185ba27a5d9139b10a759679045bf.txt.gz │ ├── robots.txt │ └── robots.txt.gz ├── specs_support ├── spec_helper.js └── utils.js ├── test ├── controllers │ └── page_controller_test.exs ├── models │ └── user_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── model_case.ex ├── test_helper.exs └── views │ ├── error_view_test.exs │ ├── layout_view_test.exs │ └── page_view_test.exs ├── web ├── channels │ ├── room_channel.ex │ └── user_socket.ex ├── controllers │ ├── api │ │ ├── registrations_controller.ex │ │ └── session_controller.ex │ └── page_controller.ex ├── models │ ├── queries.ex │ └── user.ex ├── npm-debug.log ├── router.ex ├── static │ └── js │ │ ├── app.jsx │ │ ├── app │ │ ├── actions │ │ │ ├── dashboard.js │ │ │ ├── form.js │ │ │ ├── messages.js │ │ │ ├── settings.js │ │ │ └── user.js │ │ ├── components │ │ │ ├── base_component.jsx │ │ │ ├── common │ │ │ │ ├── message.jsx │ │ │ │ └── messages.jsx │ │ │ ├── defines.js │ │ │ ├── index.jsx │ │ │ ├── index.spec.js │ │ │ ├── layout │ │ │ │ └── header.jsx │ │ │ ├── main │ │ │ │ ├── about.jsx │ │ │ │ ├── dashboard.jsx │ │ │ │ ├── home.jsx │ │ │ │ └── home.spec.js │ │ │ ├── mixins │ │ │ │ └── store_keeper.js │ │ │ ├── not_found.jsx │ │ │ └── users │ │ │ │ ├── login.jsx │ │ │ │ ├── login.spec.js │ │ │ │ ├── logout.jsx │ │ │ │ └── register.jsx │ │ ├── stores │ │ │ ├── dashboard.js │ │ │ ├── form.js │ │ │ ├── messages.js │ │ │ ├── messages.spec.js │ │ │ └── user.js │ │ └── themes │ │ │ └── mui_theme.jsx │ │ ├── app_routes.jsx │ │ └── common │ │ ├── actions │ │ ├── api.js │ │ └── socket.js │ │ ├── constants.js │ │ ├── dispatcher.js │ │ ├── stores │ │ ├── settings.js │ │ └── store_common.js │ │ └── utils │ │ ├── query_string.js │ │ └── utils.js ├── templates │ ├── layout │ │ └── app.html.eex │ └── page │ │ └── index.html.eex ├── views │ ├── error_view.ex │ ├── layout_view.ex │ └── page_view.ex └── web.ex ├── webpack.hot.js └── webpack.tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | /node_modules 7 | /npm-debug.log 8 | 9 | # Generate on crash by the VM 10 | erl_crash.dump 11 | 12 | # The config/prod.secret.exs file by default contains sensitive 13 | # data and you should not commit it into version control. 14 | # 15 | # Alternatively, you may comment the line below and commit the 16 | # secrets file as long as you replace its contents by environment 17 | # variables. 18 | /config/prod.secret.exs 19 | 20 | # Mac 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhoenixReact 2 | 3 | To start your Phoenix app: 4 | 5 | 1. Install dependencies with `mix deps.get` 6 | 2. Create and migrate your database with `mix ecto.create && mix ecto.migrate` 7 | 3. Start Phoenix endpoint with `mix phoenix.server` 8 | 9 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 10 | 11 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment). 12 | 13 | ## Learn more 14 | 15 | * Official website: http://www.phoenixframework.org/ 16 | * Guides: http://phoenixframework.org/docs/overview 17 | * Docs: http://hexdocs.pm/phoenix 18 | * Mailing list: http://groups.google.com/group/phoenix-talk 19 | * Source: https://github.com/phoenixframework/phoenix 20 | 21 | ##Debugging 22 | 23 | 1. Require IEx in the script you wish to debug: `Require IEx ` 24 | 2. Run server using: `IEx -S mix phoenix.server ` 25 | 3. Use `IEx.pry ` to inspect inline. 26 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # Configures the endpoint 9 | config :phoenixReact, PhoenixReact.Endpoint, 10 | url: [host: "localhost"], 11 | root: Path.dirname(__DIR__), 12 | secret_key_base: "AZ1tmpuCvvXGT4M6Mo3iXOg+YTLk3ntiyyKal5KpFWGPAX7dO3bbNE8rSlCBtArR", 13 | render_errors: [default_format: "html"], 14 | pubsub: [name: PhoenixReact.PubSub, 15 | adapter: Phoenix.PubSub.PG2] 16 | 17 | # Configures Elixir's Logger 18 | config :logger, :console, 19 | format: "$time $metadata[$level] $message\n", 20 | metadata: [:request_id] 21 | 22 | config :joken, config_module: Guardian.JWT 23 | 24 | config :guardian, Guardian, 25 | issuer: "PhoenixReact", 26 | ttl: { 30, :days }, 27 | verify_issuer: true, 28 | secret_key: "g14687Nu6sAcp(IQ03I/kL;qLNO7q[", 29 | serializer: PhoenixReact.GuardianSerializer, 30 | hooks: PhoenixReact.GuardianHooks, 31 | permissions: %{ 32 | default: [:read_profile, :write_profile] 33 | } 34 | # Import environment specific config. This must remain at the bottom 35 | # of this file so it overrides the configuration defined above. 36 | import_config "#{Mix.env}.exs" 37 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | #"node_modules/webpack/bin/webpack.js" 10 | # Disabled brunch in favor of webpack. 11 | config :phoenixReact, PhoenixReact.Endpoint, 12 | http: [port: 4000], 13 | debug_errors: true, 14 | code_reloader: true, 15 | cache_static_lookup: false 16 | # watchers: [{Path.expand("webpack.devserver.js"), ["--watch", "--colors", "--progress"]}] 17 | 18 | # Watch static and templates for browser reloading. 19 | # Disabled live reload of static folder. Webpack will handle this for us. 20 | config :phoenixReact, PhoenixReact.Endpoint, 21 | live_reload: [ 22 | patterns: [ 23 | # ~r{priv/static/.*(js|css|png|jpeg|jpg|gif)$}, 24 | ~r{web/views/.*(ex)$}, 25 | ~r{web/templates/.*(eex)$} 26 | ] 27 | ] 28 | 29 | # Do not include metadata nor timestamps in development logs 30 | config :logger, :console, format: "[$level] $message\n" 31 | 32 | # Configure your database 33 | config :phoenixReact, PhoenixReact.Repo, 34 | adapter: Ecto.Adapters.Postgres, 35 | username: "jaden", 36 | password: "", 37 | database: "phoenixreact_dev", 38 | size: 10 # The amount of database connections in the pool 39 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we configure the host to read the PORT 4 | # from the system environment. Therefore, you will need 5 | # to set PORT=80 before running your server. 6 | # 7 | # You should also configure the url host to something 8 | # meaningful, we use this information when generating URLs. 9 | # 10 | # Finally, we also include the path to a manifest 11 | # containing the digested version of static files. This 12 | # manifest is generated by the mix phoenix.digest task 13 | # which you typically run after static files are built. 14 | config :phoenixReact, PhoenixReact.Endpoint, 15 | http: [port: {:system, "PORT"}], 16 | url: [host: "example.com", port: 80], 17 | cache_static_manifest: "priv/static/manifest.json" 18 | 19 | # Do not print debug messages in production 20 | config :logger, level: :info 21 | 22 | # ## SSL Support 23 | # 24 | # To get SSL working, you will need to add the `https` key 25 | # to the previous section and set your `:url` port to 443: 26 | # 27 | # config :phoenixReact, PhoenixReact.Endpoint, 28 | # ... 29 | # url: [host: "example.com", port: 443], 30 | # https: [port: 443, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 33 | # 34 | # Where those two env variables return an absolute path to 35 | # the key and cert in disk or a relative path inside priv, 36 | # for example "priv/ssl/server.key". 37 | # 38 | # We also recommend setting `force_ssl`, ensuring no data is 39 | # ever sent via http, always redirecting to https: 40 | # 41 | # config :phoenixReact, PhoenixReact.Endpoint, 42 | # force_ssl: [hsts: true] 43 | # 44 | # Check `Plug.SSL` for all available options in `force_ssl`. 45 | 46 | # ## Using releases 47 | # 48 | # If you are doing OTP releases, you need to instruct Phoenix 49 | # to start the server for all endpoints: 50 | # 51 | # config :phoenix, :serve_endpoints, true 52 | # 53 | # Alternatively, you can configure exactly which server to 54 | # start per endpoint: 55 | # 56 | # config :phoenixReact, PhoenixReact.Endpoint, server: true 57 | # 58 | 59 | # Finally import the config/prod.secret.exs 60 | # which should be versioned separately. 61 | import_config "prod.secret.exs" 62 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :phoenixReact, PhoenixReact.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :phoenixReact, PhoenixReact.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "phoenixreact_test", 18 | pool: Ecto.Adapters.SQL.Sandbox 19 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var path = require('path') 3 | var ChunkManifestPlugin = require('chunk-manifest-webpack-plugin'); 4 | 5 | const APP_PORT = 3000; 6 | const PHOENIX_PORT = 4000; 7 | // const GRAPHQL_PORT = 8080; #Relay 8 | 9 | var publicPath = 'http://localhost:' + APP_PORT + '/' 10 | 11 | var env = process.env.MIX_ENV || 'dev' 12 | var release = env === 'prod' 13 | 14 | var entry = './web/static/js/app.jsx' 15 | 16 | //############### LOADERS ########## 17 | var autoprefix = '{browsers:["Android 2.3", "Android >= 4", "Chrome >= 20", "Firefox >= 24", "Explorer >= 8", "iOS >= 6", "Opera >= 12", "Safari >= 6"]}'; 18 | 19 | var jsLoaders = ["babel-loader?stage=0&optional=runtime"]; // include the runtime 20 | 21 | var cssLoaders = ['style-loader', 'css-loader', 'autoprefixer-loader?' + autoprefix]; 22 | 23 | var scssLoaders = cssLoaders.slice(0); 24 | scssLoaders.push('sass-loader?outputStyle=expanded&includePaths[]=' + (path.resolve(__dirname, './node_modules/bootstrap-sass'))); 25 | 26 | var lessLoaders = cssLoaders.slice(0); 27 | lessLoaders.push("less-loader"); 28 | 29 | //############### LOADERS ########## 30 | 31 | if (!release) { 32 | console.log("Enable React Hot Loader") 33 | jsLoaders.unshift("react-hot-loader"); 34 | } 35 | 36 | module.exports = { 37 | phoenix_port: PHOENIX_PORT, 38 | app_port: APP_PORT, 39 | // graphql_port: GRAPHQL_PORT, #Relay 40 | entry: release ? entry : [ 41 | 'webpack-dev-server/client?' + publicPath, 42 | 'webpack/hot/only-dev-server', 43 | entry 44 | ], 45 | output: { 46 | path: path.join(__dirname, './priv/static/js'), 47 | filename: 'bundle.js', 48 | publicPath: publicPath 49 | }, 50 | resolve: { 51 | extensions: ['', '.js', '.json', '.jsx'], 52 | modulesDirectories: ["node_modules", "vendor"] 53 | }, 54 | cache: true, 55 | quiet: false, 56 | noInfo: false, 57 | debug: false, 58 | outputPathinfo: !release, 59 | devtool: release ? false : "eval", // http://webpack.github.io/docs/configuration.html#devtool 60 | plugins: release ? [ 61 | new webpack.DefinePlugin({'process.env.NODE_ENV': '"releaseuction"'}), 62 | new webpack.optimize.DedupePlugin(), 63 | new webpack.optimize.UglifyJsPlugin(), 64 | new webpack.optimize.OccurenceOrderPlugin(), 65 | new webpack.optimize.AggressiveMergingPlugin(), 66 | new ChunkManifestPlugin({ 67 | filename: 'webpack-common-manifest.json', 68 | manfiestVariable: 'webpackBundleManifest' 69 | }) 70 | //new ExtractTextPlugin("[name]_web_pack_bundle.css"), 71 | //new webpack.optimize.CommonsChunkPlugin('init.js') // Use to extract common code from multiple entry points into a single init.js 72 | ] : [ 73 | //new ExtractTextPlugin("[name]_web_pack_bundle.css"), 74 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"', '__DEV__': true }), 75 | new webpack.HotModuleReplacementPlugin(), 76 | new webpack.NoErrorsPlugin() 77 | ], 78 | module: { 79 | loaders: [ 80 | { test: /\.js$/, loaders: jsLoaders, exclude: /(node_modules|socket)/ }, 81 | { test: /\.jsx?$/, loaders: jsLoaders, exclude: /(node_modules|deps)/ }, 82 | { test: /\.scss$/, loader: scssLoaders.join('!') }, 83 | { test: /\.css$/ , loader: cssLoaders.join('!') }, 84 | { test: /\.less$/ , loader: lessLoaders.join('!') }, 85 | { test: /\.(png|woff|woff2|eot|ttf|svg)($|\?)/, loader: 'url-loader' } 86 | ] 87 | }, 88 | stats: { 89 | colors: true 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // karma config info: http://karma-runner.github.io/0.12/config/configuration-file.html 2 | module.exports = function(config) { 3 | 4 | function isCoverage(argument) { 5 | return argument === '--coverage'; 6 | } 7 | 8 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 9 | var reporters = ['spec']; 10 | 11 | if(process.argv.some(isCoverage)){ 12 | reporters.push('coverage'); 13 | } 14 | 15 | var testConfig = { 16 | 17 | // If browser does not capture in given timeout [ms], kill it 18 | captureTimeout: 60000, 19 | 20 | // How long will Karma wait for a message from a browser before disconnecting from it (in ms) 21 | browserNoActivityTimeout: 60000, 22 | 23 | // enable / disable colors in the output (reporters and logs) 24 | colors: true, 25 | 26 | // web server port 27 | port: 9876, 28 | 29 | files: [ 30 | './specs_support/spec_helper.js', 31 | //'./js/**/*.spec.js' // Use webpack to build each test individually. If changed here, match the change in preprocessors 32 | './webpack.tests.js' // More performant but tests cannot be run individually 33 | ], 34 | 35 | // Transpile tests with the karma-webpack plugin 36 | preprocessors: { 37 | //'./js/**/*.spec.js': ['webpack', 'sourcemap'] // Use webpack to build each test individually. If changed here, match the change in files 38 | './webpack.tests.js': ['webpack', 'sourcemap'] // More performant but tests cannot be run individually 39 | }, 40 | 41 | // Run the tests using any of the following browsers 42 | // - Chrome npm install --save-dev karma-chrome-launcher 43 | // - ChromeCanary 44 | // - Firefox npm install --save-dev karma-firefox-launcher 45 | // - Opera npm install --save-dev karma-opera-launcher 46 | // - Safari npm install --save-dev karma-safari-launcher (only Mac) 47 | // - PhantomJS npm install --save-dev karma-phantomjs-launcher 48 | // - IE npm install karma-ie-launcher (only Windows) 49 | browsers: ['Chrome'], 50 | 51 | // Exit the test runner as well when the test suite returns. 52 | singleRun: false, 53 | 54 | // enable / disable watching file and executing tests whenever any file changes 55 | autoWatch: true, 56 | 57 | // Use jasmine as the test framework 58 | frameworks: ['jasmine-ajax', 'jasmine'], 59 | 60 | reporters: reporters, 61 | 62 | // karma-webpack configuration. Load and transpile js and jsx files. 63 | // Use istanbul-transformer post loader to generate code coverage report. 64 | webpack: { 65 | devtool: 'eval', 66 | module: { 67 | loaders: [ 68 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader?stage=0" }, 69 | { test: /\.jsx?$/, exclude: /node_modules/, loader: "babel-loader?stage=0" } 70 | ] 71 | }, 72 | resolve: { 73 | extensions: ['', '.js', '.jsx'], 74 | modulesDirectories: ["node_modules", "web_modules", "vendor"] 75 | } 76 | }, 77 | 78 | // Reduce the noise to the console 79 | webpackMiddleware: { 80 | noInfo: true, 81 | stats: { 82 | colors: true 83 | } 84 | } 85 | 86 | }; 87 | 88 | 89 | // Generate code coverage report if --coverage is specified 90 | if(process.argv.some(isCoverage)) { 91 | // Generate a code coverage report using `lcov` format. Result will be output to coverage/lcov.info 92 | // run using `npm coveralls` 93 | testConfig['webpack']['module']['postLoaders'] = [{ 94 | test: /\.jsx?$/, 95 | exclude: /(test|node_modules)\//, 96 | loader: 'istanbul-instrumenter' 97 | }]; 98 | 99 | testConfig['coverageReporter'] = { 100 | dir: 'coverage/', 101 | reporters: [ 102 | { type: 'lcovonly', subdir: '.', file: 'lcov.info' }, 103 | { type: 'html', subdir: 'html' } 104 | ] 105 | }; 106 | } 107 | 108 | config.set(testConfig); 109 | }; 110 | -------------------------------------------------------------------------------- /lib/mix/tasks/digest.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.PhoenixReact.Digest do 2 | use Mix.Task 3 | 4 | def run(args) do 5 | Mix.Shell.IO.cmd "./node_modules/webpack/bin/webpack.js" 6 | :ok = Mix.Tasks.Phoenix.Digest.run(args) 7 | end 8 | end -------------------------------------------------------------------------------- /lib/phoenixReact.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | children = [ 10 | # Start the endpoint when the application starts 11 | supervisor(PhoenixReact.Endpoint, []), 12 | # Start the Ecto repository 13 | worker(PhoenixReact.Repo, []), 14 | # Here you could define other workers and supervisors as children 15 | # worker(PhoenixReact.Worker, [arg1, arg2, arg3]), 16 | ] 17 | 18 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 19 | # for other strategies and supported options 20 | opts = [strategy: :one_for_one, name: PhoenixReact.Supervisor] 21 | Supervisor.start_link(children, opts) 22 | end 23 | 24 | # Tell Phoenix to update the endpoint configuration 25 | # whenever the application is updated. 26 | def config_change(changed, _new, removed) do 27 | PhoenixReact.Endpoint.config_change(changed, removed) 28 | :ok 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/phoenixReact/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :phoenixReact 3 | 4 | socket "/socket", PhoenixReact.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", from: :phoenixReact, gzip: false, 12 | only: ~w(css fonts images js favicon.ico robots.txt) 13 | 14 | # Code reloading can be explicitly enabled under the 15 | # :code_reloader configuration of your endpoint. 16 | if code_reloading? do 17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 18 | plug Phoenix.LiveReloader 19 | plug Phoenix.CodeReloader 20 | end 21 | 22 | plug Plug.RequestId 23 | plug Plug.Logger 24 | 25 | plug Plug.Parsers, 26 | parsers: [:urlencoded, :multipart, :json], 27 | pass: ["*/*"], 28 | json_decoder: Poison 29 | 30 | plug Plug.MethodOverride 31 | plug Plug.Head 32 | 33 | plug Plug.Session, 34 | store: :cookie, 35 | key: "_phoenixReact_key", 36 | signing_salt: "IizaBh7t" 37 | 38 | plug PhoenixReact.Router 39 | end 40 | -------------------------------------------------------------------------------- /lib/phoenixReact/guardian_hooks.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.GuardianHooks do 2 | use Guardian.Hooks 3 | 4 | def before_mint(resource, type, claims) do 5 | IO.puts("GOING TO MINT: #{inspect(resource)} WITH TYPE #{inspect(type)} AND CLAIMS #{inspect(claims)}") 6 | { :ok, { resource, type, claims } } 7 | end 8 | 9 | def after_sign_in(conn, location) do 10 | user = Guardian.Plug.current_resource(conn, location) 11 | IO.puts("SIGNED INTO LOCATION WITH: #{user.email}") 12 | conn 13 | end 14 | 15 | def before_sign_out(conn, nil), do: before_sign_out(conn, :default) 16 | 17 | def before_sign_out(conn, :all) do 18 | IO.puts("SIGNING OUT ALL THE PEOPLE") 19 | conn 20 | end 21 | 22 | def before_sign_out(conn, location) do 23 | user = Guardian.Plug.current_resource(conn, location) 24 | IO.puts("SIGNING OUT: #{user.email}") 25 | conn 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/phoenixReact/guardian_serializer.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.GuardianSerializer do 2 | @behaviour Guardian.Serializer 3 | 4 | alias PhoenixReact.Repo 5 | alias PhoenixReact.User 6 | 7 | def for_token(user = %User{}), do: { :ok, "User:#{user.id}" } 8 | def for_token(_), do: { :error, "Unknown resource type" } 9 | 10 | def from_token("User:" <> id), do: { :ok, Repo.get(User, String.to_integer(id)) } 11 | def from_token(thing), do: { :error, "Unknown resource type" } 12 | end 13 | -------------------------------------------------------------------------------- /lib/phoenixReact/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.Repo do 2 | use Ecto.Repo, otp_app: :phoenixReact 3 | end 4 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :phoenixReact, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | deps: deps, 13 | aliases: ["phoenix.digest": "PhoenixReact.digest"]] 14 | end 15 | 16 | # Configuration for the OTP application 17 | # 18 | # Type `mix help compile.app` for more information 19 | def application do 20 | [mod: {PhoenixReact, []}, 21 | applications: [:phoenix, :phoenix_html, :cowboy, :logger, 22 | :phoenix_ecto, :postgrex, :comeonin]] 23 | end 24 | 25 | # Specifies which paths to compile per environment 26 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"] 27 | defp elixirc_paths(_), do: ["lib", "web"] 28 | 29 | # Specifies your project dependencies 30 | # 31 | # Type `mix help deps` for examples and options 32 | defp deps do 33 | [{:phoenix, "~> 1.0.3"}, 34 | {:phoenix_ecto, "~> 0.9"}, 35 | {:postgrex, ">= 0.0.0"}, 36 | {:phoenix_html, "~> 2.0"}, 37 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 38 | {:cowboy, "~> 1.0"}, 39 | {:guardian, "~> 0.6.2"}, 40 | {:comeonin, "~> 1.1"}] 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"comeonin": {:hex, :comeonin, "1.1.3"}, 2 | "cowboy": {:hex, :cowboy, "1.0.4"}, 3 | "cowlib": {:hex, :cowlib, "1.0.2"}, 4 | "decimal": {:hex, :decimal, "1.1.0"}, 5 | "ecto": {:hex, :ecto, "0.16.0"}, 6 | "fs": {:hex, :fs, "0.9.2"}, 7 | "guardian": {:hex, :guardian, "0.6.2"}, 8 | "joken": {:hex, :joken, "0.15.0"}, 9 | "phoenix": {:hex, :phoenix, "1.0.4"}, 10 | "phoenix_ecto": {:hex, :phoenix_ecto, "0.9.0"}, 11 | "phoenix_html": {:hex, :phoenix_html, "2.2.0"}, 12 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.1"}, 13 | "plug": {:hex, :plug, "1.0.3"}, 14 | "poison": {:hex, :poison, "1.5.0"}, 15 | "poolboy": {:hex, :poolboy, "1.5.1"}, 16 | "postgrex": {:hex, :postgrex, "0.9.1"}, 17 | "ranch": {:hex, :ranch, "1.2.0"}, 18 | "uuid": {:hex, :uuid, "1.0.1"}} 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phoenixreact", 3 | "version": "1.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/JadenH/PhoenixReact.git" 7 | }, 8 | "description": "Phoenix/Reactjs boiler plate.", 9 | "main": "index.js", 10 | "directories": { 11 | "test": "test" 12 | }, 13 | "dependencies": { 14 | "atomic-form": "^0.1.4", 15 | "babel": "5.8.21", 16 | "babel-runtime": "^5.6.20", 17 | "classnames": "^2.1.3", 18 | "flux": "^2.1.1", 19 | "jasmine-core": "^2.3.4", 20 | "lodash": "^3.10.1", 21 | "material-ui": "^0.13.4", 22 | "mdi": "^1.2.65", 23 | "object-assign": "^4.0.1", 24 | "react": "^0.14.0", 25 | "react-dom": "^0.14.0", 26 | "react-hot-loader": "^1.2.8", 27 | "react-inline-grid": "^0.5.2", 28 | "react-router": "^1.0.3", 29 | "style-loader": "^0.13.0", 30 | "superagent": "^1.3.0", 31 | "url-loader": "^0.5.6", 32 | "validator": "^4.0.5", 33 | "websocket": "^1.0.21", 34 | "react-tap-event-plugin": "^0.2.0", 35 | "file-loader": "*", 36 | "history": "^1.17.0" 37 | }, 38 | "scripts": { 39 | "test": "karma start", 40 | "coveralls": "cat coverage/lcov.info | coveralls", 41 | "start": "babel-node webpack.hot.js", 42 | "hot": "babel-node webpack.hot.js" 43 | }, 44 | "author": "Jaden", 45 | "license": "MIT", 46 | "devDependencies": { 47 | "async": "^1.4.2", 48 | "babel-core": "^5.6.20", 49 | "babel-loader": "^5.3.2", 50 | "chunk-manifest-webpack-plugin": "0.0.1", 51 | "css-loader": "^0.21.0", 52 | "karma": "^0.13.3", 53 | "karma-chrome-launcher": "^0.2.0", 54 | "karma-cli": "0.0.4", 55 | "karma-coverage": "^0.3.1", 56 | "karma-firefox-launcher": "^0.1.4", 57 | "karma-jasmine": "^0.3.6", 58 | "karma-jasmine-ajax": "^0.1.12", 59 | "karma-jasmine-jquery": "^0.1.1", 60 | "karma-phantomjs-launcher": "^0.1.4", 61 | "karma-sourcemap-loader": "^0.3.4", 62 | "karma-spec-reporter": "0.0.19", 63 | "karma-webpack": "^1.7.0", 64 | "node-sass": "^3.3.3", 65 | "sass-loader": "^3.0.0", 66 | "webpack": "^1.12.2", 67 | "webpack-dev-server": "^1.10.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /priv/repo/migrations/20150907192802_create_user.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.Repo.Migrations.CreateUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users) do 6 | add :name, :string 7 | add :email, :string 8 | add :encrypted_password, :string 9 | add :password, :string 10 | 11 | timestamps 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # PhoenixReact.Repo.insert!(%SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /priv/static/css/app-7c1659d1c5aa51ffb74a4ea907ca3c5e.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/css/app-7c1659d1c5aa51ffb74a4ea907ca3c5e.css.gz -------------------------------------------------------------------------------- /priv/static/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: white; 3 | background-color: #1d1e22; 4 | font-family: 'Fira Sans', sans-serif; 5 | margin: 0; 6 | } 7 | 8 | a { 9 | color: #6CA547; 10 | text-decoration: none; 11 | text-shadow: 0px 0px 0px #6CA547; 12 | cursor: pointer; 13 | cursor: hand; 14 | } 15 | 16 | a:hover { 17 | color: #8CD85C; 18 | text-decoration: none; 19 | } 20 | a:focus { 21 | text-decoration: none; 22 | } -------------------------------------------------------------------------------- /priv/static/css/app.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/css/app.css.gz -------------------------------------------------------------------------------- /priv/static/favicon-a8ca4e3a2bb8fea46a9ee9e102e7d3eb.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/favicon-a8ca4e3a2bb8fea46a9ee9e102e7d3eb.ico -------------------------------------------------------------------------------- /priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/favicon.ico -------------------------------------------------------------------------------- /priv/static/images/phoenix-5bd99a0d17dd41bc9d9bf6840abcc089.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/images/phoenix-5bd99a0d17dd41bc9d9bf6840abcc089.png -------------------------------------------------------------------------------- /priv/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/images/phoenix.png -------------------------------------------------------------------------------- /priv/static/js/app-d41d8cd98f00b204e9800998ecf8427e.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/js/app-d41d8cd98f00b204e9800998ecf8427e.js -------------------------------------------------------------------------------- /priv/static/js/app-d41d8cd98f00b204e9800998ecf8427e.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/js/app-d41d8cd98f00b204e9800998ecf8427e.js.gz -------------------------------------------------------------------------------- /priv/static/js/app.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/js/app.js -------------------------------------------------------------------------------- /priv/static/js/app.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/js/app.js.gz -------------------------------------------------------------------------------- /priv/static/js/bundle-ad70171bde621994bc6077e834531309.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/js/bundle-ad70171bde621994bc6077e834531309.js.gz -------------------------------------------------------------------------------- /priv/static/js/bundle.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/js/bundle.js.gz -------------------------------------------------------------------------------- /priv/static/js/phoenix-a68af163b7e7f033ce9bf3c75afaeb28.js: -------------------------------------------------------------------------------- 1 | (function(/*! Brunch !*/) { 2 | 'use strict'; 3 | 4 | var globals = typeof window !== 'undefined' ? window : global; 5 | if (typeof globals.require === 'function') return; 6 | 7 | var modules = {}; 8 | var cache = {}; 9 | 10 | var has = function(object, name) { 11 | return ({}).hasOwnProperty.call(object, name); 12 | }; 13 | 14 | var expand = function(root, name) { 15 | var results = [], parts, part; 16 | if (/^\.\.?(\/|$)/.test(name)) { 17 | parts = [root, name].join('/').split('/'); 18 | } else { 19 | parts = name.split('/'); 20 | } 21 | for (var i = 0, length = parts.length; i < length; i++) { 22 | part = parts[i]; 23 | if (part === '..') { 24 | results.pop(); 25 | } else if (part !== '.' && part !== '') { 26 | results.push(part); 27 | } 28 | } 29 | return results.join('/'); 30 | }; 31 | 32 | var dirname = function(path) { 33 | return path.split('/').slice(0, -1).join('/'); 34 | }; 35 | 36 | var localRequire = function(path) { 37 | return function(name) { 38 | var dir = dirname(path); 39 | var absolute = expand(dir, name); 40 | return globals.require(absolute, path); 41 | }; 42 | }; 43 | 44 | var initModule = function(name, definition) { 45 | var module = {id: name, exports: {}}; 46 | cache[name] = module; 47 | definition(module.exports, localRequire(name), module); 48 | return module.exports; 49 | }; 50 | 51 | var require = function(name, loaderPath) { 52 | var path = expand(name, '.'); 53 | if (loaderPath == null) loaderPath = '/'; 54 | 55 | if (has(cache, path)) return cache[path].exports; 56 | if (has(modules, path)) return initModule(path, modules[path]); 57 | 58 | var dirIndex = expand(path, './index'); 59 | if (has(cache, dirIndex)) return cache[dirIndex].exports; 60 | if (has(modules, dirIndex)) return initModule(dirIndex, modules[dirIndex]); 61 | 62 | throw new Error('Cannot find module "' + name + '" from '+ '"' + loaderPath + '"'); 63 | }; 64 | 65 | var define = function(bundle, fn) { 66 | if (typeof bundle === 'object') { 67 | for (var key in bundle) { 68 | if (has(bundle, key)) { 69 | modules[key] = bundle[key]; 70 | } 71 | } 72 | } else { 73 | modules[bundle] = fn; 74 | } 75 | }; 76 | 77 | var list = function() { 78 | var result = []; 79 | for (var item in modules) { 80 | if (has(modules, item)) { 81 | result.push(item); 82 | } 83 | } 84 | return result; 85 | }; 86 | 87 | globals.require = require; 88 | globals.require.define = define; 89 | globals.require.register = define; 90 | globals.require.list = list; 91 | globals.require.brunch = true; 92 | })(); 93 | require.define({'phoenix': function(exports, require, module){ "use strict"; 94 | 95 | var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; 96 | 97 | var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; 98 | 99 | // Phoenix Channels JavaScript client 100 | // 101 | // ## Socket Connection 102 | // 103 | // A single connection is established to the server and 104 | // channels are mulitplexed over the connection. 105 | // Connect to the server using the `Socket` class: 106 | // 107 | // let socket = new Socket("/ws") 108 | // socket.connect({userToken: "123"}) 109 | // 110 | // The `Socket` constructor takes the mount point of the socket 111 | // as well as options that can be found in the Socket docs, 112 | // such as configuring the `LongPoll` transport, and heartbeat. 113 | // Socket params can also be passed as an object literal to `connect`. 114 | // 115 | // ## Channels 116 | // 117 | // Channels are isolated, concurrent processes on the server that 118 | // subscribe to topics and broker events between the client and server. 119 | // To join a channel, you must provide the topic, and channel params for 120 | // authorization. Here's an example chat room example where `"new_msg"` 121 | // events are listened for, messages are pushed to the server, and 122 | // the channel is joined with ok/error matches, and `after` hook: 123 | // 124 | // let channel = socket.channel("rooms:123", {token: roomToken}) 125 | // channel.on("new_msg", msg => console.log("Got message", msg) ) 126 | // $input.onEnter( e => { 127 | // channel.push("new_msg", {body: e.target.val}) 128 | // .receive("ok", (msg) => console.log("created message", msg) ) 129 | // .receive("error", (reasons) => console.log("create failed", reasons) ) 130 | // .after(10000, () => console.log("Networking issue. Still waiting...") ) 131 | // }) 132 | // channel.join() 133 | // .receive("ok", ({messages}) => console.log("catching up", messages) ) 134 | // .receive("error", ({reason}) => console.log("failed join", reason) ) 135 | // .after(10000, () => console.log("Networking issue. Still waiting...") ) 136 | // 137 | // 138 | // ## Joining 139 | // 140 | // Joining a channel with `channel.join(topic, params)`, binds the params to 141 | // `channel.params`. Subsequent rejoins will send up the modified params for 142 | // updating authorization params, or passing up last_message_id information. 143 | // Successful joins receive an "ok" status, while unsuccessful joins 144 | // receive "error". 145 | // 146 | // 147 | // ## Pushing Messages 148 | // 149 | // From the previous example, we can see that pushing messages to the server 150 | // can be done with `channel.push(eventName, payload)` and we can optionally 151 | // receive responses from the push. Additionally, we can use 152 | // `after(millsec, callback)` to abort waiting for our `receive` hooks and 153 | // take action after some period of waiting. 154 | // 155 | // 156 | // ## Socket Hooks 157 | // 158 | // Lifecycle events of the multiplexed connection can be hooked into via 159 | // `socket.onError()` and `socket.onClose()` events, ie: 160 | // 161 | // socket.onError( () => console.log("there was an error with the connection!") ) 162 | // socket.onClose( () => console.log("the connection dropped") ) 163 | // 164 | // 165 | // ## Channel Hooks 166 | // 167 | // For each joined channel, you can bind to `onError` and `onClose` events 168 | // to monitor the channel lifecycle, ie: 169 | // 170 | // channel.onError( () => console.log("there was an error!") ) 171 | // channel.onClose( () => console.log("the channel has gone away gracefully") ) 172 | // 173 | // ### onError hooks 174 | // 175 | // `onError` hooks are invoked if the socket connection drops, or the channel 176 | // crashes on the server. In either case, a channel rejoin is attemtped 177 | // automatically in an exponential backoff manner. 178 | // 179 | // ### onClose hooks 180 | // 181 | // `onClose` hooks are invoked only in two cases. 1) the channel explicitly 182 | // closed on the server, or 2). The client explicitly closed, by calling 183 | // `channel.leave()` 184 | // 185 | 186 | var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 }; 187 | var CHANNEL_STATES = { 188 | closed: "closed", 189 | errored: "errored", 190 | joined: "joined", 191 | joining: "joining" }; 192 | var CHANNEL_EVENTS = { 193 | close: "phx_close", 194 | error: "phx_error", 195 | join: "phx_join", 196 | reply: "phx_reply", 197 | leave: "phx_leave" 198 | }; 199 | var TRANSPORTS = { 200 | longpoll: "longpoll", 201 | websocket: "websocket" 202 | }; 203 | 204 | var Push = (function () { 205 | 206 | // Initializes the Push 207 | // 208 | // channel - The Channelnel 209 | // event - The event, for example `"phx_join"` 210 | // payload - The payload, for example `{user_id: 123}` 211 | // 212 | 213 | function Push(channel, event, payload) { 214 | _classCallCheck(this, Push); 215 | 216 | this.channel = channel; 217 | this.event = event; 218 | this.payload = payload || {}; 219 | this.receivedResp = null; 220 | this.afterHook = null; 221 | this.recHooks = []; 222 | this.sent = false; 223 | } 224 | 225 | _prototypeProperties(Push, null, { 226 | send: { 227 | value: function send() { 228 | var _this = this; 229 | 230 | var ref = this.channel.socket.makeRef(); 231 | this.refEvent = this.channel.replyEventName(ref); 232 | this.receivedResp = null; 233 | this.sent = false; 234 | 235 | this.channel.on(this.refEvent, function (payload) { 236 | _this.receivedResp = payload; 237 | _this.matchReceive(payload); 238 | _this.cancelRefEvent(); 239 | _this.cancelAfter(); 240 | }); 241 | 242 | this.startAfter(); 243 | this.sent = true; 244 | this.channel.socket.push({ 245 | topic: this.channel.topic, 246 | event: this.event, 247 | payload: this.payload, 248 | ref: ref 249 | }); 250 | }, 251 | writable: true, 252 | configurable: true 253 | }, 254 | receive: { 255 | value: function receive(status, callback) { 256 | if (this.receivedResp && this.receivedResp.status === status) { 257 | callback(this.receivedResp.response); 258 | } 259 | 260 | this.recHooks.push({ status: status, callback: callback }); 261 | return this; 262 | }, 263 | writable: true, 264 | configurable: true 265 | }, 266 | after: { 267 | value: function after(ms, callback) { 268 | if (this.afterHook) { 269 | throw "only a single after hook can be applied to a push"; 270 | } 271 | var timer = null; 272 | if (this.sent) { 273 | timer = setTimeout(callback, ms); 274 | } 275 | this.afterHook = { ms: ms, callback: callback, timer: timer }; 276 | return this; 277 | }, 278 | writable: true, 279 | configurable: true 280 | }, 281 | matchReceive: { 282 | 283 | // private 284 | 285 | value: function matchReceive(_ref) { 286 | var status = _ref.status; 287 | var response = _ref.response; 288 | var ref = _ref.ref; 289 | 290 | this.recHooks.filter(function (h) { 291 | return h.status === status; 292 | }).forEach(function (h) { 293 | return h.callback(response); 294 | }); 295 | }, 296 | writable: true, 297 | configurable: true 298 | }, 299 | cancelRefEvent: { 300 | value: function cancelRefEvent() { 301 | this.channel.off(this.refEvent); 302 | }, 303 | writable: true, 304 | configurable: true 305 | }, 306 | cancelAfter: { 307 | value: function cancelAfter() { 308 | if (!this.afterHook) { 309 | return; 310 | } 311 | clearTimeout(this.afterHook.timer); 312 | this.afterHook.timer = null; 313 | }, 314 | writable: true, 315 | configurable: true 316 | }, 317 | startAfter: { 318 | value: function startAfter() { 319 | var _this = this; 320 | 321 | if (!this.afterHook) { 322 | return; 323 | } 324 | var callback = function () { 325 | _this.cancelRefEvent(); 326 | _this.afterHook.callback(); 327 | }; 328 | this.afterHook.timer = setTimeout(callback, this.afterHook.ms); 329 | }, 330 | writable: true, 331 | configurable: true 332 | } 333 | }); 334 | 335 | return Push; 336 | })(); 337 | 338 | var Channel = exports.Channel = (function () { 339 | function Channel(topic, params, socket) { 340 | var _this = this; 341 | 342 | _classCallCheck(this, Channel); 343 | 344 | this.state = CHANNEL_STATES.closed; 345 | this.topic = topic; 346 | this.params = params || {}; 347 | this.socket = socket; 348 | this.bindings = []; 349 | this.joinedOnce = false; 350 | this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params); 351 | this.pushBuffer = []; 352 | this.rejoinTimer = new Timer(function () { 353 | return _this.rejoinUntilConnected(); 354 | }, this.socket.reconnectAfterMs); 355 | this.joinPush.receive("ok", function () { 356 | _this.state = CHANNEL_STATES.joined; 357 | _this.rejoinTimer.reset(); 358 | }); 359 | this.onClose(function () { 360 | _this.socket.log("channel", "close " + _this.topic); 361 | _this.state = CHANNEL_STATES.closed; 362 | _this.socket.remove(_this); 363 | }); 364 | this.onError(function (reason) { 365 | _this.socket.log("channel", "error " + _this.topic, reason); 366 | _this.state = CHANNEL_STATES.errored; 367 | _this.rejoinTimer.setTimeout(); 368 | }); 369 | this.on(CHANNEL_EVENTS.reply, function (payload, ref) { 370 | _this.trigger(_this.replyEventName(ref), payload); 371 | }); 372 | } 373 | 374 | _prototypeProperties(Channel, null, { 375 | rejoinUntilConnected: { 376 | value: function rejoinUntilConnected() { 377 | this.rejoinTimer.setTimeout(); 378 | if (this.socket.isConnected()) { 379 | this.rejoin(); 380 | } 381 | }, 382 | writable: true, 383 | configurable: true 384 | }, 385 | join: { 386 | value: function join() { 387 | if (this.joinedOnce) { 388 | throw "tried to join multiple times. 'join' can only be called a single time per channel instance"; 389 | } else { 390 | this.joinedOnce = true; 391 | } 392 | this.sendJoin(); 393 | return this.joinPush; 394 | }, 395 | writable: true, 396 | configurable: true 397 | }, 398 | onClose: { 399 | value: function onClose(callback) { 400 | this.on(CHANNEL_EVENTS.close, callback); 401 | }, 402 | writable: true, 403 | configurable: true 404 | }, 405 | onError: { 406 | value: function onError(callback) { 407 | this.on(CHANNEL_EVENTS.error, function (reason) { 408 | return callback(reason); 409 | }); 410 | }, 411 | writable: true, 412 | configurable: true 413 | }, 414 | on: { 415 | value: function on(event, callback) { 416 | this.bindings.push({ event: event, callback: callback }); 417 | }, 418 | writable: true, 419 | configurable: true 420 | }, 421 | off: { 422 | value: function off(event) { 423 | this.bindings = this.bindings.filter(function (bind) { 424 | return bind.event !== event; 425 | }); 426 | }, 427 | writable: true, 428 | configurable: true 429 | }, 430 | canPush: { 431 | value: function canPush() { 432 | return this.socket.isConnected() && this.state === CHANNEL_STATES.joined; 433 | }, 434 | writable: true, 435 | configurable: true 436 | }, 437 | push: { 438 | value: function push(event, payload) { 439 | if (!this.joinedOnce) { 440 | throw "tried to push '" + event + "' to '" + this.topic + "' before joining. Use channel.join() before pushing events"; 441 | } 442 | var pushEvent = new Push(this, event, payload); 443 | if (this.canPush()) { 444 | pushEvent.send(); 445 | } else { 446 | this.pushBuffer.push(pushEvent); 447 | } 448 | 449 | return pushEvent; 450 | }, 451 | writable: true, 452 | configurable: true 453 | }, 454 | leave: { 455 | 456 | // Leaves the channel 457 | // 458 | // Unsubscribes from server events, and 459 | // instructs channel to terminate on server 460 | // 461 | // Triggers onClose() hooks 462 | // 463 | // To receive leave acknowledgements, use the a `receive` 464 | // hook to bind to the server ack, ie: 465 | // 466 | // channel.leave().receive("ok", () => alert("left!") ) 467 | // 468 | 469 | value: function leave() { 470 | var _this = this; 471 | 472 | return this.push(CHANNEL_EVENTS.leave).receive("ok", function () { 473 | _this.socket.log("channel", "leave " + _this.topic); 474 | _this.trigger(CHANNEL_EVENTS.close, "leave"); 475 | }); 476 | }, 477 | writable: true, 478 | configurable: true 479 | }, 480 | onMessage: { 481 | 482 | // Overridable message hook 483 | // 484 | // Receives all events for specialized message handling 485 | 486 | value: function onMessage(event, payload, ref) {}, 487 | writable: true, 488 | configurable: true 489 | }, 490 | isMember: { 491 | 492 | // private 493 | 494 | value: function isMember(topic) { 495 | return this.topic === topic; 496 | }, 497 | writable: true, 498 | configurable: true 499 | }, 500 | sendJoin: { 501 | value: function sendJoin() { 502 | this.state = CHANNEL_STATES.joining; 503 | this.joinPush.send(); 504 | }, 505 | writable: true, 506 | configurable: true 507 | }, 508 | rejoin: { 509 | value: function rejoin() { 510 | this.sendJoin(); 511 | this.pushBuffer.forEach(function (pushEvent) { 512 | return pushEvent.send(); 513 | }); 514 | this.pushBuffer = []; 515 | }, 516 | writable: true, 517 | configurable: true 518 | }, 519 | trigger: { 520 | value: function trigger(triggerEvent, payload, ref) { 521 | this.onMessage(triggerEvent, payload, ref); 522 | this.bindings.filter(function (bind) { 523 | return bind.event === triggerEvent; 524 | }).map(function (bind) { 525 | return bind.callback(payload, ref); 526 | }); 527 | }, 528 | writable: true, 529 | configurable: true 530 | }, 531 | replyEventName: { 532 | value: function replyEventName(ref) { 533 | return "chan_reply_" + ref; 534 | }, 535 | writable: true, 536 | configurable: true 537 | } 538 | }); 539 | 540 | return Channel; 541 | })(); 542 | 543 | var Socket = exports.Socket = (function () { 544 | 545 | // Initializes the Socket 546 | // 547 | // endPoint - The string WebSocket endpoint, ie, "ws://example.com/ws", 548 | // "wss://example.com" 549 | // "/ws" (inherited host & protocol) 550 | // opts - Optional configuration 551 | // transport - The Websocket Transport, for example WebSocket or Phoenix.LongPoll. 552 | // Defaults to WebSocket with automatic LongPoll fallback. 553 | // heartbeatIntervalMs - The millisec interval to send a heartbeat message 554 | // reconnectAfterMs - The optional function that returns the millsec 555 | // reconnect interval. Defaults to stepped backoff of: 556 | // 557 | // function(tries){ 558 | // return [1000, 5000, 10000][tries - 1] || 10000 559 | // } 560 | // 561 | // logger - The optional function for specialized logging, ie: 562 | // `logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) } 563 | // 564 | // longpollerTimeout - The maximum timeout of a long poll AJAX request. 565 | // Defaults to 20s (double the server long poll timer). 566 | // 567 | // For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim) 568 | // 569 | 570 | function Socket(endPoint) { 571 | var _this = this; 572 | 573 | var opts = arguments[1] === undefined ? {} : arguments[1]; 574 | 575 | _classCallCheck(this, Socket); 576 | 577 | this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] }; 578 | this.channels = []; 579 | this.sendBuffer = []; 580 | this.ref = 0; 581 | this.transport = opts.transport || window.WebSocket || LongPoll; 582 | this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000; 583 | this.reconnectAfterMs = opts.reconnectAfterMs || function (tries) { 584 | return [1000, 5000, 10000][tries - 1] || 10000; 585 | }; 586 | this.logger = opts.logger || function () {}; // noop 587 | this.longpollerTimeout = opts.longpollerTimeout || 20000; 588 | this.params = {}; 589 | this.reconnectTimer = new Timer(function () { 590 | return _this.connect(_this.params); 591 | }, this.reconnectAfterMs); 592 | this.endPoint = "" + endPoint + "/" + TRANSPORTS.websocket; 593 | } 594 | 595 | _prototypeProperties(Socket, null, { 596 | protocol: { 597 | value: function protocol() { 598 | return location.protocol.match(/^https/) ? "wss" : "ws"; 599 | }, 600 | writable: true, 601 | configurable: true 602 | }, 603 | endPointURL: { 604 | value: function endPointURL() { 605 | var uri = Ajax.appendParams(this.endPoint, this.params); 606 | if (uri.charAt(0) !== "/") { 607 | return uri; 608 | } 609 | if (uri.charAt(1) === "/") { 610 | return "" + this.protocol() + ":" + uri; 611 | } 612 | 613 | return "" + this.protocol() + "://" + location.host + "" + uri; 614 | }, 615 | writable: true, 616 | configurable: true 617 | }, 618 | disconnect: { 619 | value: function disconnect(callback, code, reason) { 620 | if (this.conn) { 621 | this.conn.onclose = function () {}; // noop 622 | if (code) { 623 | this.conn.close(code, reason || ""); 624 | } else { 625 | this.conn.close(); 626 | } 627 | this.conn = null; 628 | } 629 | callback && callback(); 630 | }, 631 | writable: true, 632 | configurable: true 633 | }, 634 | connect: { 635 | 636 | // params - The params to send when connecting, for example `{user_id: userToken}` 637 | 638 | value: function connect() { 639 | var _this = this; 640 | 641 | var params = arguments[0] === undefined ? {} : arguments[0]; 642 | this.params = params; 643 | this.disconnect(function () { 644 | _this.conn = new _this.transport(_this.endPointURL()); 645 | _this.conn.timeout = _this.longpollerTimeout; 646 | _this.conn.onopen = function () { 647 | return _this.onConnOpen(); 648 | }; 649 | _this.conn.onerror = function (error) { 650 | return _this.onConnError(error); 651 | }; 652 | _this.conn.onmessage = function (event) { 653 | return _this.onConnMessage(event); 654 | }; 655 | _this.conn.onclose = function (event) { 656 | return _this.onConnClose(event); 657 | }; 658 | }); 659 | }, 660 | writable: true, 661 | configurable: true 662 | }, 663 | log: { 664 | 665 | // Logs the message. Override `this.logger` for specialized logging. noops by default 666 | 667 | value: function log(kind, msg, data) { 668 | this.logger(kind, msg, data); 669 | }, 670 | writable: true, 671 | configurable: true 672 | }, 673 | onOpen: { 674 | 675 | // Registers callbacks for connection state change events 676 | // 677 | // Examples 678 | // 679 | // socket.onError(function(error){ alert("An error occurred") }) 680 | // 681 | 682 | value: function onOpen(callback) { 683 | this.stateChangeCallbacks.open.push(callback); 684 | }, 685 | writable: true, 686 | configurable: true 687 | }, 688 | onClose: { 689 | value: function onClose(callback) { 690 | this.stateChangeCallbacks.close.push(callback); 691 | }, 692 | writable: true, 693 | configurable: true 694 | }, 695 | onError: { 696 | value: function onError(callback) { 697 | this.stateChangeCallbacks.error.push(callback); 698 | }, 699 | writable: true, 700 | configurable: true 701 | }, 702 | onMessage: { 703 | value: function onMessage(callback) { 704 | this.stateChangeCallbacks.message.push(callback); 705 | }, 706 | writable: true, 707 | configurable: true 708 | }, 709 | onConnOpen: { 710 | value: function onConnOpen() { 711 | var _this = this; 712 | 713 | this.log("transport", "connected to " + this.endPointURL(), this.transport.prototype); 714 | this.flushSendBuffer(); 715 | this.reconnectTimer.reset(); 716 | if (!this.conn.skipHeartbeat) { 717 | clearInterval(this.heartbeatTimer); 718 | this.heartbeatTimer = setInterval(function () { 719 | return _this.sendHeartbeat(); 720 | }, this.heartbeatIntervalMs); 721 | } 722 | this.stateChangeCallbacks.open.forEach(function (callback) { 723 | return callback(); 724 | }); 725 | }, 726 | writable: true, 727 | configurable: true 728 | }, 729 | onConnClose: { 730 | value: function onConnClose(event) { 731 | this.log("transport", "close", event); 732 | this.triggerChanError(); 733 | clearInterval(this.heartbeatTimer); 734 | this.reconnectTimer.setTimeout(); 735 | this.stateChangeCallbacks.close.forEach(function (callback) { 736 | return callback(event); 737 | }); 738 | }, 739 | writable: true, 740 | configurable: true 741 | }, 742 | onConnError: { 743 | value: function onConnError(error) { 744 | this.log("transport", error); 745 | this.triggerChanError(); 746 | this.stateChangeCallbacks.error.forEach(function (callback) { 747 | return callback(error); 748 | }); 749 | }, 750 | writable: true, 751 | configurable: true 752 | }, 753 | triggerChanError: { 754 | value: function triggerChanError() { 755 | this.channels.forEach(function (channel) { 756 | return channel.trigger(CHANNEL_EVENTS.error); 757 | }); 758 | }, 759 | writable: true, 760 | configurable: true 761 | }, 762 | connectionState: { 763 | value: function connectionState() { 764 | switch (this.conn && this.conn.readyState) { 765 | case SOCKET_STATES.connecting: 766 | return "connecting"; 767 | case SOCKET_STATES.open: 768 | return "open"; 769 | case SOCKET_STATES.closing: 770 | return "closing"; 771 | default: 772 | return "closed"; 773 | } 774 | }, 775 | writable: true, 776 | configurable: true 777 | }, 778 | isConnected: { 779 | value: function isConnected() { 780 | return this.connectionState() === "open"; 781 | }, 782 | writable: true, 783 | configurable: true 784 | }, 785 | remove: { 786 | value: function remove(channel) { 787 | this.channels = this.channels.filter(function (c) { 788 | return !c.isMember(channel.topic); 789 | }); 790 | }, 791 | writable: true, 792 | configurable: true 793 | }, 794 | channel: { 795 | value: function channel(topic) { 796 | var chanParams = arguments[1] === undefined ? {} : arguments[1]; 797 | 798 | var channel = new Channel(topic, chanParams, this); 799 | this.channels.push(channel); 800 | return channel; 801 | }, 802 | writable: true, 803 | configurable: true 804 | }, 805 | push: { 806 | value: function push(data) { 807 | var _this = this; 808 | 809 | var topic = data.topic; 810 | var event = data.event; 811 | var payload = data.payload; 812 | var ref = data.ref; 813 | 814 | var callback = function () { 815 | return _this.conn.send(JSON.stringify(data)); 816 | }; 817 | this.log("push", "" + topic + " " + event + " (" + ref + ")", payload); 818 | if (this.isConnected()) { 819 | callback(); 820 | } else { 821 | this.sendBuffer.push(callback); 822 | } 823 | }, 824 | writable: true, 825 | configurable: true 826 | }, 827 | makeRef: { 828 | 829 | // Return the next message ref, accounting for overflows 830 | 831 | value: function makeRef() { 832 | var newRef = this.ref + 1; 833 | if (newRef === this.ref) { 834 | this.ref = 0; 835 | } else { 836 | this.ref = newRef; 837 | } 838 | 839 | return this.ref.toString(); 840 | }, 841 | writable: true, 842 | configurable: true 843 | }, 844 | sendHeartbeat: { 845 | value: function sendHeartbeat() { 846 | this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref: this.makeRef() }); 847 | }, 848 | writable: true, 849 | configurable: true 850 | }, 851 | flushSendBuffer: { 852 | value: function flushSendBuffer() { 853 | if (this.isConnected() && this.sendBuffer.length > 0) { 854 | this.sendBuffer.forEach(function (callback) { 855 | return callback(); 856 | }); 857 | this.sendBuffer = []; 858 | } 859 | }, 860 | writable: true, 861 | configurable: true 862 | }, 863 | onConnMessage: { 864 | value: function onConnMessage(rawMessage) { 865 | var msg = JSON.parse(rawMessage.data); 866 | var topic = msg.topic; 867 | var event = msg.event; 868 | var payload = msg.payload; 869 | var ref = msg.ref; 870 | 871 | this.log("receive", "" + (payload.status || "") + " " + topic + " " + event + " " + (ref && "(" + ref + ")" || ""), payload); 872 | this.channels.filter(function (channel) { 873 | return channel.isMember(topic); 874 | }).forEach(function (channel) { 875 | return channel.trigger(event, payload, ref); 876 | }); 877 | this.stateChangeCallbacks.message.forEach(function (callback) { 878 | return callback(msg); 879 | }); 880 | }, 881 | writable: true, 882 | configurable: true 883 | } 884 | }); 885 | 886 | return Socket; 887 | })(); 888 | 889 | var LongPoll = exports.LongPoll = (function () { 890 | function LongPoll(endPoint) { 891 | _classCallCheck(this, LongPoll); 892 | 893 | this.endPoint = null; 894 | this.token = null; 895 | this.skipHeartbeat = true; 896 | this.onopen = function () {}; // noop 897 | this.onerror = function () {}; // noop 898 | this.onmessage = function () {}; // noop 899 | this.onclose = function () {}; // noop 900 | this.pollEndpoint = this.normalizeEndpoint(endPoint); 901 | this.readyState = SOCKET_STATES.connecting; 902 | 903 | this.poll(); 904 | } 905 | 906 | _prototypeProperties(LongPoll, null, { 907 | normalizeEndpoint: { 908 | value: function normalizeEndpoint(endPoint) { 909 | return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll); 910 | }, 911 | writable: true, 912 | configurable: true 913 | }, 914 | endpointURL: { 915 | value: function endpointURL() { 916 | return Ajax.appendParams(this.pollEndpoint, { 917 | token: this.token, 918 | format: "json" 919 | }); 920 | }, 921 | writable: true, 922 | configurable: true 923 | }, 924 | closeAndRetry: { 925 | value: function closeAndRetry() { 926 | this.close(); 927 | this.readyState = SOCKET_STATES.connecting; 928 | }, 929 | writable: true, 930 | configurable: true 931 | }, 932 | ontimeout: { 933 | value: function ontimeout() { 934 | this.onerror("timeout"); 935 | this.closeAndRetry(); 936 | }, 937 | writable: true, 938 | configurable: true 939 | }, 940 | poll: { 941 | value: function poll() { 942 | var _this = this; 943 | 944 | if (!(this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting)) { 945 | return; 946 | } 947 | 948 | Ajax.request("GET", this.endpointURL(), "application/json", null, this.timeout, this.ontimeout.bind(this), function (resp) { 949 | if (resp) { 950 | var status = resp.status; 951 | var token = resp.token; 952 | var messages = resp.messages; 953 | 954 | _this.token = token; 955 | } else { 956 | var status = 0; 957 | } 958 | 959 | switch (status) { 960 | case 200: 961 | messages.forEach(function (msg) { 962 | return _this.onmessage({ data: JSON.stringify(msg) }); 963 | }); 964 | _this.poll(); 965 | break; 966 | case 204: 967 | _this.poll(); 968 | break; 969 | case 410: 970 | _this.readyState = SOCKET_STATES.open; 971 | _this.onopen(); 972 | _this.poll(); 973 | break; 974 | case 0: 975 | case 500: 976 | _this.onerror(); 977 | _this.closeAndRetry(); 978 | break; 979 | default: 980 | throw "unhandled poll status " + status; 981 | } 982 | }); 983 | }, 984 | writable: true, 985 | configurable: true 986 | }, 987 | send: { 988 | value: function send(body) { 989 | var _this = this; 990 | 991 | Ajax.request("POST", this.endpointURL(), "application/json", body, this.timeout, this.onerror.bind(this, "timeout"), function (resp) { 992 | if (!resp || resp.status !== 200) { 993 | _this.onerror(status); 994 | _this.closeAndRetry(); 995 | } 996 | }); 997 | }, 998 | writable: true, 999 | configurable: true 1000 | }, 1001 | close: { 1002 | value: function close(code, reason) { 1003 | this.readyState = SOCKET_STATES.closed; 1004 | this.onclose(); 1005 | }, 1006 | writable: true, 1007 | configurable: true 1008 | } 1009 | }); 1010 | 1011 | return LongPoll; 1012 | })(); 1013 | 1014 | var Ajax = exports.Ajax = (function () { 1015 | function Ajax() { 1016 | _classCallCheck(this, Ajax); 1017 | } 1018 | 1019 | _prototypeProperties(Ajax, { 1020 | request: { 1021 | value: function request(method, endPoint, accept, body, timeout, ontimeout, callback) { 1022 | if (window.XDomainRequest) { 1023 | var req = new XDomainRequest(); // IE8, IE9 1024 | this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback); 1025 | } else { 1026 | var req = window.XMLHttpRequest ? new XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari 1027 | new ActiveXObject("Microsoft.XMLHTTP"); // IE6, IE5 1028 | this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback); 1029 | } 1030 | }, 1031 | writable: true, 1032 | configurable: true 1033 | }, 1034 | xdomainRequest: { 1035 | value: function xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) { 1036 | var _this = this; 1037 | 1038 | req.timeout = timeout; 1039 | req.open(method, endPoint); 1040 | req.onload = function () { 1041 | var response = _this.parseJSON(req.responseText); 1042 | callback && callback(response); 1043 | }; 1044 | if (ontimeout) { 1045 | req.ontimeout = ontimeout; 1046 | } 1047 | 1048 | // Work around bug in IE9 that requires an attached onprogress handler 1049 | req.onprogress = function () {}; 1050 | 1051 | req.send(body); 1052 | }, 1053 | writable: true, 1054 | configurable: true 1055 | }, 1056 | xhrRequest: { 1057 | value: function xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback) { 1058 | var _this = this; 1059 | 1060 | req.timeout = timeout; 1061 | req.open(method, endPoint, true); 1062 | req.setRequestHeader("Content-Type", accept); 1063 | req.onerror = function () { 1064 | callback && callback(null); 1065 | }; 1066 | req.onreadystatechange = function () { 1067 | if (req.readyState === _this.states.complete && callback) { 1068 | var response = _this.parseJSON(req.responseText); 1069 | callback(response); 1070 | } 1071 | }; 1072 | if (ontimeout) { 1073 | req.ontimeout = ontimeout; 1074 | } 1075 | 1076 | req.send(body); 1077 | }, 1078 | writable: true, 1079 | configurable: true 1080 | }, 1081 | parseJSON: { 1082 | value: function parseJSON(resp) { 1083 | return resp && resp !== "" ? JSON.parse(resp) : null; 1084 | }, 1085 | writable: true, 1086 | configurable: true 1087 | }, 1088 | serialize: { 1089 | value: function serialize(obj, parentKey) { 1090 | var queryStr = []; 1091 | for (var key in obj) { 1092 | if (!obj.hasOwnProperty(key)) { 1093 | continue; 1094 | } 1095 | var paramKey = parentKey ? "" + parentKey + "[" + key + "]" : key; 1096 | var paramVal = obj[key]; 1097 | if (typeof paramVal === "object") { 1098 | queryStr.push(this.serialize(paramVal, paramKey)); 1099 | } else { 1100 | queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal)); 1101 | } 1102 | } 1103 | return queryStr.join("&"); 1104 | }, 1105 | writable: true, 1106 | configurable: true 1107 | }, 1108 | appendParams: { 1109 | value: function appendParams(url, params) { 1110 | if (Object.keys(params).length === 0) { 1111 | return url; 1112 | } 1113 | 1114 | var prefix = url.match(/\?/) ? "&" : "?"; 1115 | return "" + url + "" + prefix + "" + this.serialize(params); 1116 | }, 1117 | writable: true, 1118 | configurable: true 1119 | } 1120 | }); 1121 | 1122 | return Ajax; 1123 | })(); 1124 | 1125 | Ajax.states = { complete: 4 }; 1126 | 1127 | // Creates a timer that accepts a `timerCalc` function to perform 1128 | // calculated timeout retries, such as exponential backoff. 1129 | // 1130 | // ## Examples 1131 | // 1132 | // let reconnectTimer = new Timer(() => this.connect(), function(tries){ 1133 | // return [1000, 5000, 10000][tries - 1] || 10000 1134 | // }) 1135 | // reconnectTimer.setTimeout() // fires after 1000 1136 | // reconnectTimer.setTimeout() // fires after 5000 1137 | // reconnectTimer.reset() 1138 | // reconnectTimer.setTimeout() // fires after 1000 1139 | // 1140 | 1141 | var Timer = (function () { 1142 | function Timer(callback, timerCalc) { 1143 | _classCallCheck(this, Timer); 1144 | 1145 | this.callback = callback; 1146 | this.timerCalc = timerCalc; 1147 | this.timer = null; 1148 | this.tries = 0; 1149 | } 1150 | 1151 | _prototypeProperties(Timer, null, { 1152 | reset: { 1153 | value: function reset() { 1154 | this.tries = 0; 1155 | clearTimeout(this.timer); 1156 | }, 1157 | writable: true, 1158 | configurable: true 1159 | }, 1160 | setTimeout: { 1161 | 1162 | // Cancels any previous setTimeout and schedules callback 1163 | 1164 | value: (function (_setTimeout) { 1165 | var _setTimeoutWrapper = function setTimeout() { 1166 | return _setTimeout.apply(this, arguments); 1167 | }; 1168 | 1169 | _setTimeoutWrapper.toString = function () { 1170 | return _setTimeout.toString(); 1171 | }; 1172 | 1173 | return _setTimeoutWrapper; 1174 | })(function () { 1175 | var _this = this; 1176 | 1177 | clearTimeout(this.timer); 1178 | 1179 | this.timer = setTimeout(function () { 1180 | _this.tries = _this.tries + 1; 1181 | _this.callback(); 1182 | }, this.timerCalc(this.tries + 1)); 1183 | }), 1184 | writable: true, 1185 | configurable: true 1186 | } 1187 | }); 1188 | 1189 | return Timer; 1190 | })(); 1191 | 1192 | Object.defineProperty(exports, "__esModule", { 1193 | value: true 1194 | }); 1195 | }}); 1196 | if(typeof(window) === 'object' && !window.Phoenix){ window.Phoenix = require('phoenix') }; -------------------------------------------------------------------------------- /priv/static/js/phoenix-a68af163b7e7f033ce9bf3c75afaeb28.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/js/phoenix-a68af163b7e7f033ce9bf3c75afaeb28.js.gz -------------------------------------------------------------------------------- /priv/static/js/phoenix.js: -------------------------------------------------------------------------------- 1 | (function(/*! Brunch !*/) { 2 | 'use strict'; 3 | 4 | var globals = typeof window !== 'undefined' ? window : global; 5 | if (typeof globals.require === 'function') return; 6 | 7 | var modules = {}; 8 | var cache = {}; 9 | 10 | var has = function(object, name) { 11 | return ({}).hasOwnProperty.call(object, name); 12 | }; 13 | 14 | var expand = function(root, name) { 15 | var results = [], parts, part; 16 | if (/^\.\.?(\/|$)/.test(name)) { 17 | parts = [root, name].join('/').split('/'); 18 | } else { 19 | parts = name.split('/'); 20 | } 21 | for (var i = 0, length = parts.length; i < length; i++) { 22 | part = parts[i]; 23 | if (part === '..') { 24 | results.pop(); 25 | } else if (part !== '.' && part !== '') { 26 | results.push(part); 27 | } 28 | } 29 | return results.join('/'); 30 | }; 31 | 32 | var dirname = function(path) { 33 | return path.split('/').slice(0, -1).join('/'); 34 | }; 35 | 36 | var localRequire = function(path) { 37 | return function(name) { 38 | var dir = dirname(path); 39 | var absolute = expand(dir, name); 40 | return globals.require(absolute, path); 41 | }; 42 | }; 43 | 44 | var initModule = function(name, definition) { 45 | var module = {id: name, exports: {}}; 46 | cache[name] = module; 47 | definition(module.exports, localRequire(name), module); 48 | return module.exports; 49 | }; 50 | 51 | var require = function(name, loaderPath) { 52 | var path = expand(name, '.'); 53 | if (loaderPath == null) loaderPath = '/'; 54 | 55 | if (has(cache, path)) return cache[path].exports; 56 | if (has(modules, path)) return initModule(path, modules[path]); 57 | 58 | var dirIndex = expand(path, './index'); 59 | if (has(cache, dirIndex)) return cache[dirIndex].exports; 60 | if (has(modules, dirIndex)) return initModule(dirIndex, modules[dirIndex]); 61 | 62 | throw new Error('Cannot find module "' + name + '" from '+ '"' + loaderPath + '"'); 63 | }; 64 | 65 | var define = function(bundle, fn) { 66 | if (typeof bundle === 'object') { 67 | for (var key in bundle) { 68 | if (has(bundle, key)) { 69 | modules[key] = bundle[key]; 70 | } 71 | } 72 | } else { 73 | modules[bundle] = fn; 74 | } 75 | }; 76 | 77 | var list = function() { 78 | var result = []; 79 | for (var item in modules) { 80 | if (has(modules, item)) { 81 | result.push(item); 82 | } 83 | } 84 | return result; 85 | }; 86 | 87 | globals.require = require; 88 | globals.require.define = define; 89 | globals.require.register = define; 90 | globals.require.list = list; 91 | globals.require.brunch = true; 92 | })(); 93 | require.define({'phoenix': function(exports, require, module){ "use strict"; 94 | 95 | var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; 96 | 97 | var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; 98 | 99 | // Phoenix Channels JavaScript client 100 | // 101 | // ## Socket Connection 102 | // 103 | // A single connection is established to the server and 104 | // channels are mulitplexed over the connection. 105 | // Connect to the server using the `Socket` class: 106 | // 107 | // let socket = new Socket("/ws") 108 | // socket.connect({userToken: "123"}) 109 | // 110 | // The `Socket` constructor takes the mount point of the socket 111 | // as well as options that can be found in the Socket docs, 112 | // such as configuring the `LongPoll` transport, and heartbeat. 113 | // Socket params can also be passed as an object literal to `connect`. 114 | // 115 | // ## Channels 116 | // 117 | // Channels are isolated, concurrent processes on the server that 118 | // subscribe to topics and broker events between the client and server. 119 | // To join a channel, you must provide the topic, and channel params for 120 | // authorization. Here's an example chat room example where `"new_msg"` 121 | // events are listened for, messages are pushed to the server, and 122 | // the channel is joined with ok/error matches, and `after` hook: 123 | // 124 | // let channel = socket.channel("rooms:123", {token: roomToken}) 125 | // channel.on("new_msg", msg => console.log("Got message", msg) ) 126 | // $input.onEnter( e => { 127 | // channel.push("new_msg", {body: e.target.val}) 128 | // .receive("ok", (msg) => console.log("created message", msg) ) 129 | // .receive("error", (reasons) => console.log("create failed", reasons) ) 130 | // .after(10000, () => console.log("Networking issue. Still waiting...") ) 131 | // }) 132 | // channel.join() 133 | // .receive("ok", ({messages}) => console.log("catching up", messages) ) 134 | // .receive("error", ({reason}) => console.log("failed join", reason) ) 135 | // .after(10000, () => console.log("Networking issue. Still waiting...") ) 136 | // 137 | // 138 | // ## Joining 139 | // 140 | // Joining a channel with `channel.join(topic, params)`, binds the params to 141 | // `channel.params`. Subsequent rejoins will send up the modified params for 142 | // updating authorization params, or passing up last_message_id information. 143 | // Successful joins receive an "ok" status, while unsuccessful joins 144 | // receive "error". 145 | // 146 | // 147 | // ## Pushing Messages 148 | // 149 | // From the previous example, we can see that pushing messages to the server 150 | // can be done with `channel.push(eventName, payload)` and we can optionally 151 | // receive responses from the push. Additionally, we can use 152 | // `after(millsec, callback)` to abort waiting for our `receive` hooks and 153 | // take action after some period of waiting. 154 | // 155 | // 156 | // ## Socket Hooks 157 | // 158 | // Lifecycle events of the multiplexed connection can be hooked into via 159 | // `socket.onError()` and `socket.onClose()` events, ie: 160 | // 161 | // socket.onError( () => console.log("there was an error with the connection!") ) 162 | // socket.onClose( () => console.log("the connection dropped") ) 163 | // 164 | // 165 | // ## Channel Hooks 166 | // 167 | // For each joined channel, you can bind to `onError` and `onClose` events 168 | // to monitor the channel lifecycle, ie: 169 | // 170 | // channel.onError( () => console.log("there was an error!") ) 171 | // channel.onClose( () => console.log("the channel has gone away gracefully") ) 172 | // 173 | // ### onError hooks 174 | // 175 | // `onError` hooks are invoked if the socket connection drops, or the channel 176 | // crashes on the server. In either case, a channel rejoin is attemtped 177 | // automatically in an exponential backoff manner. 178 | // 179 | // ### onClose hooks 180 | // 181 | // `onClose` hooks are invoked only in two cases. 1) the channel explicitly 182 | // closed on the server, or 2). The client explicitly closed, by calling 183 | // `channel.leave()` 184 | // 185 | 186 | var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 }; 187 | var CHANNEL_STATES = { 188 | closed: "closed", 189 | errored: "errored", 190 | joined: "joined", 191 | joining: "joining" }; 192 | var CHANNEL_EVENTS = { 193 | close: "phx_close", 194 | error: "phx_error", 195 | join: "phx_join", 196 | reply: "phx_reply", 197 | leave: "phx_leave" 198 | }; 199 | var TRANSPORTS = { 200 | longpoll: "longpoll", 201 | websocket: "websocket" 202 | }; 203 | 204 | var Push = (function () { 205 | 206 | // Initializes the Push 207 | // 208 | // channel - The Channelnel 209 | // event - The event, for example `"phx_join"` 210 | // payload - The payload, for example `{user_id: 123}` 211 | // 212 | 213 | function Push(channel, event, payload) { 214 | _classCallCheck(this, Push); 215 | 216 | this.channel = channel; 217 | this.event = event; 218 | this.payload = payload || {}; 219 | this.receivedResp = null; 220 | this.afterHook = null; 221 | this.recHooks = []; 222 | this.sent = false; 223 | } 224 | 225 | _prototypeProperties(Push, null, { 226 | send: { 227 | value: function send() { 228 | var _this = this; 229 | 230 | var ref = this.channel.socket.makeRef(); 231 | this.refEvent = this.channel.replyEventName(ref); 232 | this.receivedResp = null; 233 | this.sent = false; 234 | 235 | this.channel.on(this.refEvent, function (payload) { 236 | _this.receivedResp = payload; 237 | _this.matchReceive(payload); 238 | _this.cancelRefEvent(); 239 | _this.cancelAfter(); 240 | }); 241 | 242 | this.startAfter(); 243 | this.sent = true; 244 | this.channel.socket.push({ 245 | topic: this.channel.topic, 246 | event: this.event, 247 | payload: this.payload, 248 | ref: ref 249 | }); 250 | }, 251 | writable: true, 252 | configurable: true 253 | }, 254 | receive: { 255 | value: function receive(status, callback) { 256 | if (this.receivedResp && this.receivedResp.status === status) { 257 | callback(this.receivedResp.response); 258 | } 259 | 260 | this.recHooks.push({ status: status, callback: callback }); 261 | return this; 262 | }, 263 | writable: true, 264 | configurable: true 265 | }, 266 | after: { 267 | value: function after(ms, callback) { 268 | if (this.afterHook) { 269 | throw "only a single after hook can be applied to a push"; 270 | } 271 | var timer = null; 272 | if (this.sent) { 273 | timer = setTimeout(callback, ms); 274 | } 275 | this.afterHook = { ms: ms, callback: callback, timer: timer }; 276 | return this; 277 | }, 278 | writable: true, 279 | configurable: true 280 | }, 281 | matchReceive: { 282 | 283 | // private 284 | 285 | value: function matchReceive(_ref) { 286 | var status = _ref.status; 287 | var response = _ref.response; 288 | var ref = _ref.ref; 289 | 290 | this.recHooks.filter(function (h) { 291 | return h.status === status; 292 | }).forEach(function (h) { 293 | return h.callback(response); 294 | }); 295 | }, 296 | writable: true, 297 | configurable: true 298 | }, 299 | cancelRefEvent: { 300 | value: function cancelRefEvent() { 301 | this.channel.off(this.refEvent); 302 | }, 303 | writable: true, 304 | configurable: true 305 | }, 306 | cancelAfter: { 307 | value: function cancelAfter() { 308 | if (!this.afterHook) { 309 | return; 310 | } 311 | clearTimeout(this.afterHook.timer); 312 | this.afterHook.timer = null; 313 | }, 314 | writable: true, 315 | configurable: true 316 | }, 317 | startAfter: { 318 | value: function startAfter() { 319 | var _this = this; 320 | 321 | if (!this.afterHook) { 322 | return; 323 | } 324 | var callback = function () { 325 | _this.cancelRefEvent(); 326 | _this.afterHook.callback(); 327 | }; 328 | this.afterHook.timer = setTimeout(callback, this.afterHook.ms); 329 | }, 330 | writable: true, 331 | configurable: true 332 | } 333 | }); 334 | 335 | return Push; 336 | })(); 337 | 338 | var Channel = exports.Channel = (function () { 339 | function Channel(topic, params, socket) { 340 | var _this = this; 341 | 342 | _classCallCheck(this, Channel); 343 | 344 | this.state = CHANNEL_STATES.closed; 345 | this.topic = topic; 346 | this.params = params || {}; 347 | this.socket = socket; 348 | this.bindings = []; 349 | this.joinedOnce = false; 350 | this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params); 351 | this.pushBuffer = []; 352 | this.rejoinTimer = new Timer(function () { 353 | return _this.rejoinUntilConnected(); 354 | }, this.socket.reconnectAfterMs); 355 | this.joinPush.receive("ok", function () { 356 | _this.state = CHANNEL_STATES.joined; 357 | _this.rejoinTimer.reset(); 358 | }); 359 | this.onClose(function () { 360 | _this.socket.log("channel", "close " + _this.topic); 361 | _this.state = CHANNEL_STATES.closed; 362 | _this.socket.remove(_this); 363 | }); 364 | this.onError(function (reason) { 365 | _this.socket.log("channel", "error " + _this.topic, reason); 366 | _this.state = CHANNEL_STATES.errored; 367 | _this.rejoinTimer.setTimeout(); 368 | }); 369 | this.on(CHANNEL_EVENTS.reply, function (payload, ref) { 370 | _this.trigger(_this.replyEventName(ref), payload); 371 | }); 372 | } 373 | 374 | _prototypeProperties(Channel, null, { 375 | rejoinUntilConnected: { 376 | value: function rejoinUntilConnected() { 377 | this.rejoinTimer.setTimeout(); 378 | if (this.socket.isConnected()) { 379 | this.rejoin(); 380 | } 381 | }, 382 | writable: true, 383 | configurable: true 384 | }, 385 | join: { 386 | value: function join() { 387 | if (this.joinedOnce) { 388 | throw "tried to join multiple times. 'join' can only be called a single time per channel instance"; 389 | } else { 390 | this.joinedOnce = true; 391 | } 392 | this.sendJoin(); 393 | return this.joinPush; 394 | }, 395 | writable: true, 396 | configurable: true 397 | }, 398 | onClose: { 399 | value: function onClose(callback) { 400 | this.on(CHANNEL_EVENTS.close, callback); 401 | }, 402 | writable: true, 403 | configurable: true 404 | }, 405 | onError: { 406 | value: function onError(callback) { 407 | this.on(CHANNEL_EVENTS.error, function (reason) { 408 | return callback(reason); 409 | }); 410 | }, 411 | writable: true, 412 | configurable: true 413 | }, 414 | on: { 415 | value: function on(event, callback) { 416 | this.bindings.push({ event: event, callback: callback }); 417 | }, 418 | writable: true, 419 | configurable: true 420 | }, 421 | off: { 422 | value: function off(event) { 423 | this.bindings = this.bindings.filter(function (bind) { 424 | return bind.event !== event; 425 | }); 426 | }, 427 | writable: true, 428 | configurable: true 429 | }, 430 | canPush: { 431 | value: function canPush() { 432 | return this.socket.isConnected() && this.state === CHANNEL_STATES.joined; 433 | }, 434 | writable: true, 435 | configurable: true 436 | }, 437 | push: { 438 | value: function push(event, payload) { 439 | if (!this.joinedOnce) { 440 | throw "tried to push '" + event + "' to '" + this.topic + "' before joining. Use channel.join() before pushing events"; 441 | } 442 | var pushEvent = new Push(this, event, payload); 443 | if (this.canPush()) { 444 | pushEvent.send(); 445 | } else { 446 | this.pushBuffer.push(pushEvent); 447 | } 448 | 449 | return pushEvent; 450 | }, 451 | writable: true, 452 | configurable: true 453 | }, 454 | leave: { 455 | 456 | // Leaves the channel 457 | // 458 | // Unsubscribes from server events, and 459 | // instructs channel to terminate on server 460 | // 461 | // Triggers onClose() hooks 462 | // 463 | // To receive leave acknowledgements, use the a `receive` 464 | // hook to bind to the server ack, ie: 465 | // 466 | // channel.leave().receive("ok", () => alert("left!") ) 467 | // 468 | 469 | value: function leave() { 470 | var _this = this; 471 | 472 | return this.push(CHANNEL_EVENTS.leave).receive("ok", function () { 473 | _this.socket.log("channel", "leave " + _this.topic); 474 | _this.trigger(CHANNEL_EVENTS.close, "leave"); 475 | }); 476 | }, 477 | writable: true, 478 | configurable: true 479 | }, 480 | onMessage: { 481 | 482 | // Overridable message hook 483 | // 484 | // Receives all events for specialized message handling 485 | 486 | value: function onMessage(event, payload, ref) {}, 487 | writable: true, 488 | configurable: true 489 | }, 490 | isMember: { 491 | 492 | // private 493 | 494 | value: function isMember(topic) { 495 | return this.topic === topic; 496 | }, 497 | writable: true, 498 | configurable: true 499 | }, 500 | sendJoin: { 501 | value: function sendJoin() { 502 | this.state = CHANNEL_STATES.joining; 503 | this.joinPush.send(); 504 | }, 505 | writable: true, 506 | configurable: true 507 | }, 508 | rejoin: { 509 | value: function rejoin() { 510 | this.sendJoin(); 511 | this.pushBuffer.forEach(function (pushEvent) { 512 | return pushEvent.send(); 513 | }); 514 | this.pushBuffer = []; 515 | }, 516 | writable: true, 517 | configurable: true 518 | }, 519 | trigger: { 520 | value: function trigger(triggerEvent, payload, ref) { 521 | this.onMessage(triggerEvent, payload, ref); 522 | this.bindings.filter(function (bind) { 523 | return bind.event === triggerEvent; 524 | }).map(function (bind) { 525 | return bind.callback(payload, ref); 526 | }); 527 | }, 528 | writable: true, 529 | configurable: true 530 | }, 531 | replyEventName: { 532 | value: function replyEventName(ref) { 533 | return "chan_reply_" + ref; 534 | }, 535 | writable: true, 536 | configurable: true 537 | } 538 | }); 539 | 540 | return Channel; 541 | })(); 542 | 543 | var Socket = exports.Socket = (function () { 544 | 545 | // Initializes the Socket 546 | // 547 | // endPoint - The string WebSocket endpoint, ie, "ws://example.com/ws", 548 | // "wss://example.com" 549 | // "/ws" (inherited host & protocol) 550 | // opts - Optional configuration 551 | // transport - The Websocket Transport, for example WebSocket or Phoenix.LongPoll. 552 | // Defaults to WebSocket with automatic LongPoll fallback. 553 | // heartbeatIntervalMs - The millisec interval to send a heartbeat message 554 | // reconnectAfterMs - The optional function that returns the millsec 555 | // reconnect interval. Defaults to stepped backoff of: 556 | // 557 | // function(tries){ 558 | // return [1000, 5000, 10000][tries - 1] || 10000 559 | // } 560 | // 561 | // logger - The optional function for specialized logging, ie: 562 | // `logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) } 563 | // 564 | // longpollerTimeout - The maximum timeout of a long poll AJAX request. 565 | // Defaults to 20s (double the server long poll timer). 566 | // 567 | // For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim) 568 | // 569 | 570 | function Socket(endPoint) { 571 | var _this = this; 572 | 573 | var opts = arguments[1] === undefined ? {} : arguments[1]; 574 | 575 | _classCallCheck(this, Socket); 576 | 577 | this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] }; 578 | this.channels = []; 579 | this.sendBuffer = []; 580 | this.ref = 0; 581 | this.transport = opts.transport || window.WebSocket || LongPoll; 582 | this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000; 583 | this.reconnectAfterMs = opts.reconnectAfterMs || function (tries) { 584 | return [1000, 5000, 10000][tries - 1] || 10000; 585 | }; 586 | this.logger = opts.logger || function () {}; // noop 587 | this.longpollerTimeout = opts.longpollerTimeout || 20000; 588 | this.params = {}; 589 | this.reconnectTimer = new Timer(function () { 590 | return _this.connect(_this.params); 591 | }, this.reconnectAfterMs); 592 | this.endPoint = "" + endPoint + "/" + TRANSPORTS.websocket; 593 | } 594 | 595 | _prototypeProperties(Socket, null, { 596 | protocol: { 597 | value: function protocol() { 598 | return location.protocol.match(/^https/) ? "wss" : "ws"; 599 | }, 600 | writable: true, 601 | configurable: true 602 | }, 603 | endPointURL: { 604 | value: function endPointURL() { 605 | var uri = Ajax.appendParams(this.endPoint, this.params); 606 | if (uri.charAt(0) !== "/") { 607 | return uri; 608 | } 609 | if (uri.charAt(1) === "/") { 610 | return "" + this.protocol() + ":" + uri; 611 | } 612 | 613 | return "" + this.protocol() + "://" + location.host + "" + uri; 614 | }, 615 | writable: true, 616 | configurable: true 617 | }, 618 | disconnect: { 619 | value: function disconnect(callback, code, reason) { 620 | if (this.conn) { 621 | this.conn.onclose = function () {}; // noop 622 | if (code) { 623 | this.conn.close(code, reason || ""); 624 | } else { 625 | this.conn.close(); 626 | } 627 | this.conn = null; 628 | } 629 | callback && callback(); 630 | }, 631 | writable: true, 632 | configurable: true 633 | }, 634 | connect: { 635 | 636 | // params - The params to send when connecting, for example `{user_id: userToken}` 637 | 638 | value: function connect() { 639 | var _this = this; 640 | 641 | var params = arguments[0] === undefined ? {} : arguments[0]; 642 | this.params = params; 643 | this.disconnect(function () { 644 | _this.conn = new _this.transport(_this.endPointURL()); 645 | _this.conn.timeout = _this.longpollerTimeout; 646 | _this.conn.onopen = function () { 647 | return _this.onConnOpen(); 648 | }; 649 | _this.conn.onerror = function (error) { 650 | return _this.onConnError(error); 651 | }; 652 | _this.conn.onmessage = function (event) { 653 | return _this.onConnMessage(event); 654 | }; 655 | _this.conn.onclose = function (event) { 656 | return _this.onConnClose(event); 657 | }; 658 | }); 659 | }, 660 | writable: true, 661 | configurable: true 662 | }, 663 | log: { 664 | 665 | // Logs the message. Override `this.logger` for specialized logging. noops by default 666 | 667 | value: function log(kind, msg, data) { 668 | this.logger(kind, msg, data); 669 | }, 670 | writable: true, 671 | configurable: true 672 | }, 673 | onOpen: { 674 | 675 | // Registers callbacks for connection state change events 676 | // 677 | // Examples 678 | // 679 | // socket.onError(function(error){ alert("An error occurred") }) 680 | // 681 | 682 | value: function onOpen(callback) { 683 | this.stateChangeCallbacks.open.push(callback); 684 | }, 685 | writable: true, 686 | configurable: true 687 | }, 688 | onClose: { 689 | value: function onClose(callback) { 690 | this.stateChangeCallbacks.close.push(callback); 691 | }, 692 | writable: true, 693 | configurable: true 694 | }, 695 | onError: { 696 | value: function onError(callback) { 697 | this.stateChangeCallbacks.error.push(callback); 698 | }, 699 | writable: true, 700 | configurable: true 701 | }, 702 | onMessage: { 703 | value: function onMessage(callback) { 704 | this.stateChangeCallbacks.message.push(callback); 705 | }, 706 | writable: true, 707 | configurable: true 708 | }, 709 | onConnOpen: { 710 | value: function onConnOpen() { 711 | var _this = this; 712 | 713 | this.log("transport", "connected to " + this.endPointURL(), this.transport.prototype); 714 | this.flushSendBuffer(); 715 | this.reconnectTimer.reset(); 716 | if (!this.conn.skipHeartbeat) { 717 | clearInterval(this.heartbeatTimer); 718 | this.heartbeatTimer = setInterval(function () { 719 | return _this.sendHeartbeat(); 720 | }, this.heartbeatIntervalMs); 721 | } 722 | this.stateChangeCallbacks.open.forEach(function (callback) { 723 | return callback(); 724 | }); 725 | }, 726 | writable: true, 727 | configurable: true 728 | }, 729 | onConnClose: { 730 | value: function onConnClose(event) { 731 | this.log("transport", "close", event); 732 | this.triggerChanError(); 733 | clearInterval(this.heartbeatTimer); 734 | this.reconnectTimer.setTimeout(); 735 | this.stateChangeCallbacks.close.forEach(function (callback) { 736 | return callback(event); 737 | }); 738 | }, 739 | writable: true, 740 | configurable: true 741 | }, 742 | onConnError: { 743 | value: function onConnError(error) { 744 | this.log("transport", error); 745 | this.triggerChanError(); 746 | this.stateChangeCallbacks.error.forEach(function (callback) { 747 | return callback(error); 748 | }); 749 | }, 750 | writable: true, 751 | configurable: true 752 | }, 753 | triggerChanError: { 754 | value: function triggerChanError() { 755 | this.channels.forEach(function (channel) { 756 | return channel.trigger(CHANNEL_EVENTS.error); 757 | }); 758 | }, 759 | writable: true, 760 | configurable: true 761 | }, 762 | connectionState: { 763 | value: function connectionState() { 764 | switch (this.conn && this.conn.readyState) { 765 | case SOCKET_STATES.connecting: 766 | return "connecting"; 767 | case SOCKET_STATES.open: 768 | return "open"; 769 | case SOCKET_STATES.closing: 770 | return "closing"; 771 | default: 772 | return "closed"; 773 | } 774 | }, 775 | writable: true, 776 | configurable: true 777 | }, 778 | isConnected: { 779 | value: function isConnected() { 780 | return this.connectionState() === "open"; 781 | }, 782 | writable: true, 783 | configurable: true 784 | }, 785 | remove: { 786 | value: function remove(channel) { 787 | this.channels = this.channels.filter(function (c) { 788 | return !c.isMember(channel.topic); 789 | }); 790 | }, 791 | writable: true, 792 | configurable: true 793 | }, 794 | channel: { 795 | value: function channel(topic) { 796 | var chanParams = arguments[1] === undefined ? {} : arguments[1]; 797 | 798 | var channel = new Channel(topic, chanParams, this); 799 | this.channels.push(channel); 800 | return channel; 801 | }, 802 | writable: true, 803 | configurable: true 804 | }, 805 | push: { 806 | value: function push(data) { 807 | var _this = this; 808 | 809 | var topic = data.topic; 810 | var event = data.event; 811 | var payload = data.payload; 812 | var ref = data.ref; 813 | 814 | var callback = function () { 815 | return _this.conn.send(JSON.stringify(data)); 816 | }; 817 | this.log("push", "" + topic + " " + event + " (" + ref + ")", payload); 818 | if (this.isConnected()) { 819 | callback(); 820 | } else { 821 | this.sendBuffer.push(callback); 822 | } 823 | }, 824 | writable: true, 825 | configurable: true 826 | }, 827 | makeRef: { 828 | 829 | // Return the next message ref, accounting for overflows 830 | 831 | value: function makeRef() { 832 | var newRef = this.ref + 1; 833 | if (newRef === this.ref) { 834 | this.ref = 0; 835 | } else { 836 | this.ref = newRef; 837 | } 838 | 839 | return this.ref.toString(); 840 | }, 841 | writable: true, 842 | configurable: true 843 | }, 844 | sendHeartbeat: { 845 | value: function sendHeartbeat() { 846 | this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref: this.makeRef() }); 847 | }, 848 | writable: true, 849 | configurable: true 850 | }, 851 | flushSendBuffer: { 852 | value: function flushSendBuffer() { 853 | if (this.isConnected() && this.sendBuffer.length > 0) { 854 | this.sendBuffer.forEach(function (callback) { 855 | return callback(); 856 | }); 857 | this.sendBuffer = []; 858 | } 859 | }, 860 | writable: true, 861 | configurable: true 862 | }, 863 | onConnMessage: { 864 | value: function onConnMessage(rawMessage) { 865 | var msg = JSON.parse(rawMessage.data); 866 | var topic = msg.topic; 867 | var event = msg.event; 868 | var payload = msg.payload; 869 | var ref = msg.ref; 870 | 871 | this.log("receive", "" + (payload.status || "") + " " + topic + " " + event + " " + (ref && "(" + ref + ")" || ""), payload); 872 | this.channels.filter(function (channel) { 873 | return channel.isMember(topic); 874 | }).forEach(function (channel) { 875 | return channel.trigger(event, payload, ref); 876 | }); 877 | this.stateChangeCallbacks.message.forEach(function (callback) { 878 | return callback(msg); 879 | }); 880 | }, 881 | writable: true, 882 | configurable: true 883 | } 884 | }); 885 | 886 | return Socket; 887 | })(); 888 | 889 | var LongPoll = exports.LongPoll = (function () { 890 | function LongPoll(endPoint) { 891 | _classCallCheck(this, LongPoll); 892 | 893 | this.endPoint = null; 894 | this.token = null; 895 | this.skipHeartbeat = true; 896 | this.onopen = function () {}; // noop 897 | this.onerror = function () {}; // noop 898 | this.onmessage = function () {}; // noop 899 | this.onclose = function () {}; // noop 900 | this.pollEndpoint = this.normalizeEndpoint(endPoint); 901 | this.readyState = SOCKET_STATES.connecting; 902 | 903 | this.poll(); 904 | } 905 | 906 | _prototypeProperties(LongPoll, null, { 907 | normalizeEndpoint: { 908 | value: function normalizeEndpoint(endPoint) { 909 | return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll); 910 | }, 911 | writable: true, 912 | configurable: true 913 | }, 914 | endpointURL: { 915 | value: function endpointURL() { 916 | return Ajax.appendParams(this.pollEndpoint, { 917 | token: this.token, 918 | format: "json" 919 | }); 920 | }, 921 | writable: true, 922 | configurable: true 923 | }, 924 | closeAndRetry: { 925 | value: function closeAndRetry() { 926 | this.close(); 927 | this.readyState = SOCKET_STATES.connecting; 928 | }, 929 | writable: true, 930 | configurable: true 931 | }, 932 | ontimeout: { 933 | value: function ontimeout() { 934 | this.onerror("timeout"); 935 | this.closeAndRetry(); 936 | }, 937 | writable: true, 938 | configurable: true 939 | }, 940 | poll: { 941 | value: function poll() { 942 | var _this = this; 943 | 944 | if (!(this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting)) { 945 | return; 946 | } 947 | 948 | Ajax.request("GET", this.endpointURL(), "application/json", null, this.timeout, this.ontimeout.bind(this), function (resp) { 949 | if (resp) { 950 | var status = resp.status; 951 | var token = resp.token; 952 | var messages = resp.messages; 953 | 954 | _this.token = token; 955 | } else { 956 | var status = 0; 957 | } 958 | 959 | switch (status) { 960 | case 200: 961 | messages.forEach(function (msg) { 962 | return _this.onmessage({ data: JSON.stringify(msg) }); 963 | }); 964 | _this.poll(); 965 | break; 966 | case 204: 967 | _this.poll(); 968 | break; 969 | case 410: 970 | _this.readyState = SOCKET_STATES.open; 971 | _this.onopen(); 972 | _this.poll(); 973 | break; 974 | case 0: 975 | case 500: 976 | _this.onerror(); 977 | _this.closeAndRetry(); 978 | break; 979 | default: 980 | throw "unhandled poll status " + status; 981 | } 982 | }); 983 | }, 984 | writable: true, 985 | configurable: true 986 | }, 987 | send: { 988 | value: function send(body) { 989 | var _this = this; 990 | 991 | Ajax.request("POST", this.endpointURL(), "application/json", body, this.timeout, this.onerror.bind(this, "timeout"), function (resp) { 992 | if (!resp || resp.status !== 200) { 993 | _this.onerror(status); 994 | _this.closeAndRetry(); 995 | } 996 | }); 997 | }, 998 | writable: true, 999 | configurable: true 1000 | }, 1001 | close: { 1002 | value: function close(code, reason) { 1003 | this.readyState = SOCKET_STATES.closed; 1004 | this.onclose(); 1005 | }, 1006 | writable: true, 1007 | configurable: true 1008 | } 1009 | }); 1010 | 1011 | return LongPoll; 1012 | })(); 1013 | 1014 | var Ajax = exports.Ajax = (function () { 1015 | function Ajax() { 1016 | _classCallCheck(this, Ajax); 1017 | } 1018 | 1019 | _prototypeProperties(Ajax, { 1020 | request: { 1021 | value: function request(method, endPoint, accept, body, timeout, ontimeout, callback) { 1022 | if (window.XDomainRequest) { 1023 | var req = new XDomainRequest(); // IE8, IE9 1024 | this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback); 1025 | } else { 1026 | var req = window.XMLHttpRequest ? new XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari 1027 | new ActiveXObject("Microsoft.XMLHTTP"); // IE6, IE5 1028 | this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback); 1029 | } 1030 | }, 1031 | writable: true, 1032 | configurable: true 1033 | }, 1034 | xdomainRequest: { 1035 | value: function xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) { 1036 | var _this = this; 1037 | 1038 | req.timeout = timeout; 1039 | req.open(method, endPoint); 1040 | req.onload = function () { 1041 | var response = _this.parseJSON(req.responseText); 1042 | callback && callback(response); 1043 | }; 1044 | if (ontimeout) { 1045 | req.ontimeout = ontimeout; 1046 | } 1047 | 1048 | // Work around bug in IE9 that requires an attached onprogress handler 1049 | req.onprogress = function () {}; 1050 | 1051 | req.send(body); 1052 | }, 1053 | writable: true, 1054 | configurable: true 1055 | }, 1056 | xhrRequest: { 1057 | value: function xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback) { 1058 | var _this = this; 1059 | 1060 | req.timeout = timeout; 1061 | req.open(method, endPoint, true); 1062 | req.setRequestHeader("Content-Type", accept); 1063 | req.onerror = function () { 1064 | callback && callback(null); 1065 | }; 1066 | req.onreadystatechange = function () { 1067 | if (req.readyState === _this.states.complete && callback) { 1068 | var response = _this.parseJSON(req.responseText); 1069 | callback(response); 1070 | } 1071 | }; 1072 | if (ontimeout) { 1073 | req.ontimeout = ontimeout; 1074 | } 1075 | 1076 | req.send(body); 1077 | }, 1078 | writable: true, 1079 | configurable: true 1080 | }, 1081 | parseJSON: { 1082 | value: function parseJSON(resp) { 1083 | return resp && resp !== "" ? JSON.parse(resp) : null; 1084 | }, 1085 | writable: true, 1086 | configurable: true 1087 | }, 1088 | serialize: { 1089 | value: function serialize(obj, parentKey) { 1090 | var queryStr = []; 1091 | for (var key in obj) { 1092 | if (!obj.hasOwnProperty(key)) { 1093 | continue; 1094 | } 1095 | var paramKey = parentKey ? "" + parentKey + "[" + key + "]" : key; 1096 | var paramVal = obj[key]; 1097 | if (typeof paramVal === "object") { 1098 | queryStr.push(this.serialize(paramVal, paramKey)); 1099 | } else { 1100 | queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal)); 1101 | } 1102 | } 1103 | return queryStr.join("&"); 1104 | }, 1105 | writable: true, 1106 | configurable: true 1107 | }, 1108 | appendParams: { 1109 | value: function appendParams(url, params) { 1110 | if (Object.keys(params).length === 0) { 1111 | return url; 1112 | } 1113 | 1114 | var prefix = url.match(/\?/) ? "&" : "?"; 1115 | return "" + url + "" + prefix + "" + this.serialize(params); 1116 | }, 1117 | writable: true, 1118 | configurable: true 1119 | } 1120 | }); 1121 | 1122 | return Ajax; 1123 | })(); 1124 | 1125 | Ajax.states = { complete: 4 }; 1126 | 1127 | // Creates a timer that accepts a `timerCalc` function to perform 1128 | // calculated timeout retries, such as exponential backoff. 1129 | // 1130 | // ## Examples 1131 | // 1132 | // let reconnectTimer = new Timer(() => this.connect(), function(tries){ 1133 | // return [1000, 5000, 10000][tries - 1] || 10000 1134 | // }) 1135 | // reconnectTimer.setTimeout() // fires after 1000 1136 | // reconnectTimer.setTimeout() // fires after 5000 1137 | // reconnectTimer.reset() 1138 | // reconnectTimer.setTimeout() // fires after 1000 1139 | // 1140 | 1141 | var Timer = (function () { 1142 | function Timer(callback, timerCalc) { 1143 | _classCallCheck(this, Timer); 1144 | 1145 | this.callback = callback; 1146 | this.timerCalc = timerCalc; 1147 | this.timer = null; 1148 | this.tries = 0; 1149 | } 1150 | 1151 | _prototypeProperties(Timer, null, { 1152 | reset: { 1153 | value: function reset() { 1154 | this.tries = 0; 1155 | clearTimeout(this.timer); 1156 | }, 1157 | writable: true, 1158 | configurable: true 1159 | }, 1160 | setTimeout: { 1161 | 1162 | // Cancels any previous setTimeout and schedules callback 1163 | 1164 | value: (function (_setTimeout) { 1165 | var _setTimeoutWrapper = function setTimeout() { 1166 | return _setTimeout.apply(this, arguments); 1167 | }; 1168 | 1169 | _setTimeoutWrapper.toString = function () { 1170 | return _setTimeout.toString(); 1171 | }; 1172 | 1173 | return _setTimeoutWrapper; 1174 | })(function () { 1175 | var _this = this; 1176 | 1177 | clearTimeout(this.timer); 1178 | 1179 | this.timer = setTimeout(function () { 1180 | _this.tries = _this.tries + 1; 1181 | _this.callback(); 1182 | }, this.timerCalc(this.tries + 1)); 1183 | }), 1184 | writable: true, 1185 | configurable: true 1186 | } 1187 | }); 1188 | 1189 | return Timer; 1190 | })(); 1191 | 1192 | Object.defineProperty(exports, "__esModule", { 1193 | value: true 1194 | }); 1195 | }}); 1196 | if(typeof(window) === 'object' && !window.Phoenix){ window.Phoenix = require('phoenix') }; -------------------------------------------------------------------------------- /priv/static/js/phoenix.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/js/phoenix.js.gz -------------------------------------------------------------------------------- /priv/static/manifest.json: -------------------------------------------------------------------------------- 1 | {"robots.txt":"robots-067185ba27a5d9139b10a759679045bf.txt","js/phoenix.js":"js/phoenix-a68af163b7e7f033ce9bf3c75afaeb28.js","js/bundle.js":"js/bundle-ad70171bde621994bc6077e834531309.js","js/app.js":"js/app-d41d8cd98f00b204e9800998ecf8427e.js","images/phoenix.png":"images/phoenix-5bd99a0d17dd41bc9d9bf6840abcc089.png","favicon.ico":"favicon-a8ca4e3a2bb8fea46a9ee9e102e7d3eb.ico","css/app.css":"css/app-7c1659d1c5aa51ffb74a4ea907ca3c5e.css"} -------------------------------------------------------------------------------- /priv/static/robots-067185ba27a5d9139b10a759679045bf.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /priv/static/robots-067185ba27a5d9139b10a759679045bf.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/robots-067185ba27a5d9139b10a759679045bf.txt.gz -------------------------------------------------------------------------------- /priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /priv/static/robots.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JadenH/PhoenixReact/ce90a3df59cac0a41178e7d2508533cf7d359380/priv/static/robots.txt.gz -------------------------------------------------------------------------------- /specs_support/spec_helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Test helper. Load libraries and polyfills 4 | 5 | // Function.prototype.bind polyfill used by PhantomJS 6 | if (typeof Function.prototype.bind != 'function') { 7 | Function.prototype.bind = function bind(obj) { 8 | var args = Array.prototype.slice.call(arguments, 1), 9 | self = this, 10 | nop = function() { 11 | }, 12 | bound = function() { 13 | return self.apply( 14 | this instanceof nop ? this : (obj || {}), args.concat( 15 | Array.prototype.slice.call(arguments) 16 | ) 17 | ); 18 | }; 19 | nop.prototype = this.prototype || {}; 20 | bound.prototype = new nop(); 21 | return bound; 22 | }; 23 | } 24 | 25 | function helpStubAjax(SettingsActions){ 26 | 27 | helpLoadSettings(SettingsActions); 28 | 29 | beforeEach(function(){ 30 | jasmine.Ajax.install(); 31 | 32 | // Stub request to load problems 33 | var accounts_payload = JSON.stringify([{ 34 | "id":1, 35 | "name":"Canvas Starter App", 36 | "domain":"bfcoderServer.ngrok.io", 37 | "lti_key":"canvasstarterapp", 38 | "lti_secret":"d52ca2", 39 | "canvas_uri":"https://canvas.instructure.com", 40 | "code":"bfcoderServer" 41 | }]); 42 | 43 | jasmine.Ajax.stubRequest( 44 | RegExp('.*/api/accounts/') 45 | ).andReturn({ 46 | "status": 200, 47 | "contentType": "json", 48 | "statusText": "OK", 49 | "responseText": accounts_payload 50 | }); 51 | }); 52 | 53 | afterEach(function(){ 54 | jasmine.Ajax.uninstall(); 55 | }); 56 | } 57 | 58 | var helpDefaultSettings = { 59 | apiUrl: "http://www.example.com/api" 60 | }; 61 | 62 | function helpLoadSettings(SettingsActions){ 63 | 64 | helpMockClock(); 65 | 66 | beforeEach(function(){ 67 | SettingsActions.load(helpDefaultSettings); 68 | jasmine.clock().tick(); // Advance the clock to the next tick 69 | }); 70 | 71 | } 72 | 73 | function helpMockClock(){ 74 | 75 | beforeEach(function(){ 76 | jasmine.clock().install(); // Mock out the built in timers 77 | }); 78 | 79 | afterEach(function(){ 80 | jasmine.clock().uninstall(); 81 | }); 82 | 83 | } 84 | -------------------------------------------------------------------------------- /specs_support/utils.js: -------------------------------------------------------------------------------- 1 | import TestUtils from 'react/lib/ReactTestUtils'; 2 | import _ from "lodash"; 3 | 4 | export default { 5 | 6 | findTextField(textFields, labelText){ 7 | return _.find(textFields, function(field){ 8 | var label = TestUtils.findRenderedDOMComponentWithTag(field, 'label'); 9 | return label.getDOMNode().textContent.toLowerCase() == labelText; 10 | }); 11 | } 12 | 13 | }; -------------------------------------------------------------------------------- /test/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.PageControllerTest do 2 | use PhoenixReact.ConnCase 3 | 4 | test "GET /" do 5 | conn = get conn(), "/" 6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/models/user_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.UserTest do 2 | use PhoenixReact.ModelCase 3 | 4 | alias PhoenixReact.User 5 | 6 | @valid_attrs %{email: "some content", encrypted_password: "some content", name: "some content", password: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = User.changeset(%User{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = User.changeset(%User{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | alias PhoenixReact.Repo 24 | import Ecto.Model 25 | import Ecto.Query, only: [from: 2] 26 | 27 | 28 | # The default endpoint for testing 29 | @endpoint PhoenixReact.Endpoint 30 | end 31 | end 32 | 33 | setup tags do 34 | unless tags[:async] do 35 | Ecto.Adapters.SQL.restart_test_transaction(PhoenixReact.Repo, []) 36 | end 37 | 38 | :ok 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | 23 | alias PhoenixReact.Repo 24 | import Ecto.Model 25 | import Ecto.Query, only: [from: 2] 26 | 27 | import PhoenixReact.Router.Helpers 28 | 29 | # The default endpoint for testing 30 | @endpoint PhoenixReact.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | unless tags[:async] do 36 | Ecto.Adapters.SQL.restart_test_transaction(PhoenixReact.Repo, []) 37 | end 38 | 39 | :ok 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | 6 | You may define functions here to be used as helpers in 7 | your model tests. See `errors_on/2`'s definition as reference. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias PhoenixReact.Repo 20 | import Ecto.Model 21 | import Ecto.Query, only: [from: 2] 22 | import PhoenixReact.ModelCase 23 | end 24 | end 25 | 26 | setup tags do 27 | unless tags[:async] do 28 | Ecto.Adapters.SQL.restart_test_transaction(PhoenixReact.Repo, []) 29 | end 30 | 31 | :ok 32 | end 33 | 34 | @doc """ 35 | Helper for returning list of errors in model when passed certain data. 36 | 37 | ## Examples 38 | 39 | Given a User model that has validation for the presence of a value for the 40 | `:name` field and validation that `:password` is "safe": 41 | 42 | iex> errors_on(%User{}, password: "password") 43 | [{:password, "is unsafe"}, {:name, "is blank"}] 44 | 45 | You would then write your assertion like: 46 | 47 | assert {:password, "is unsafe"} in errors_on(%User{}, password: "password") 48 | """ 49 | def errors_on(model, data) do 50 | model.__struct__.changeset(model, data).errors 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | Mix.Task.run "ecto.create", ["--quiet"] 4 | Mix.Task.run "ecto.migrate", ["--quiet"] 5 | Ecto.Adapters.SQL.begin_test_transaction(PhoenixReact.Repo) 6 | 7 | -------------------------------------------------------------------------------- /test/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.ErrorViewTest do 2 | use PhoenixReact.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(PhoenixReact.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(PhoenixReact.ErrorView, "500.html", []) == 14 | "Server internal error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(PhoenixReact.ErrorView, "505.html", []) == 19 | "Server internal error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.LayoutViewTest do 2 | use PhoenixReact.ConnCase, async: true 3 | end -------------------------------------------------------------------------------- /test/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.PageViewTest do 2 | use PhoenixReact.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /web/channels/room_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.ChatChannel do 2 | use Phoenix.Channel 3 | 4 | def join("chat:lobby", auth_msg, socket) do 5 | {:ok, socket} 6 | end 7 | 8 | def join("chat:" <> _private_room_id, _auth_msg, socket) do 9 | {:error, %{reason: "unauthorized"}} 10 | end 11 | 12 | def handle_in("new_msg", %{"body" => body}, socket) do 13 | broadcast! socket, "new_msg", %{body: body, user: socket.assigns[:name]} 14 | {:noreply, socket} 15 | end 16 | 17 | def handle_out("new_msg", payload, socket) do 18 | push socket, "new_msg", payload 19 | {:noreply, socket} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | channel "chat:*", PhoenixReact.ChatChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | 23 | def connect(params, socket) do 24 | if (params["jwt"]) do 25 | case Guardian.decode_and_verify(params["jwt"]) do 26 | {:ok, claims} -> 27 | case Guardian.serializer.from_token(claims["sub"]) do 28 | {:ok, user} -> 29 | socket = assign(socket, :name, user.name) 30 | {:ok, assign(socket, :user_id, user.id)} 31 | {:error, reason} -> 32 | {:error, reason} 33 | end 34 | {:error, reason} -> 35 | {:error, reason} 36 | end 37 | else 38 | :error 39 | end 40 | end 41 | 42 | # Socket id's are topics that allow you to identify all sockets for a given user: 43 | # 44 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}" 45 | # 46 | # Would allow you to broadcast a "disconnect" event and terminate 47 | # all active sockets and channels for a given user: 48 | # 49 | # PhoenixReact.Endpoint.broadcast("users_socket:" <> user.id, "disconnect", %{}) 50 | # 51 | # Returning `nil` makes this socket anonymous. 52 | def id(socket), do: "users_socket:" <> to_string(socket.assigns.user_id) 53 | 54 | end -------------------------------------------------------------------------------- /web/controllers/api/registrations_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.Api.RegistrationsController do 2 | use PhoenixReact.Web, :controller 3 | 4 | alias PhoenixReact.User 5 | 6 | plug :scrub_params, "user" when action in [:create, :update] 7 | 8 | def update(conn, %{"jwt" => jwt, "user" => user_params}) do 9 | case Guardian.decode_and_verify(jwt) do 10 | { :ok, claims } -> 11 | case Guardian.serializer.from_token(claims["sub"]) do 12 | { :ok, user } -> 13 | changeset = User.update_changeset(user, user_params) 14 | if changeset.valid? do 15 | user = elem(Repo.update(changeset), 1) 16 | json(conn, %{user: %{jwt: jwt, email: user.email, name: user.name}}) 17 | else 18 | conn 19 | |> put_status(400) 20 | |> json(%{user: changeset}) 21 | end 22 | { :error, reason } -> 23 | conn 24 | |> put_status(403) 25 | |> json(%{ error: :forbidden }) 26 | end 27 | { :error, reason } -> 28 | conn 29 | |> put_status(403) 30 | |> json(%{ error: :forbidden }) 31 | end 32 | end 33 | 34 | def create(conn, %{"user" => user_params}) do 35 | changeset = User.create_changeset(%User{}, user_params) 36 | if changeset.valid? do 37 | user = elem(Repo.insert(changeset), 1) 38 | jwt = elem(Guardian.encode_and_sign(user, :token),1) 39 | # perms: %{ default: Guardian.Permissions.max } 40 | json(conn, %{user: %{jwt: jwt, email: user.email, name: user.name}}) 41 | else 42 | conn 43 | |> put_status(400) 44 | |> json(%{user: changeset}) 45 | end 46 | end 47 | 48 | 49 | # def index(conn, _params) do 50 | # users = Repo.all(User) 51 | # json(conn, %{ data: users, current_user: Guardian.Plug.current_resource(conn) }) 52 | # end 53 | 54 | # def new(conn, _params) do 55 | # changeset = User.create_changeset(%User{}) 56 | # render(conn, "new.html", changeset: changeset) 57 | # end 58 | end 59 | -------------------------------------------------------------------------------- /web/controllers/api/session_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.Api.SessionController do 2 | use PhoenixReact.Web, :controller 3 | 4 | alias PhoenixReact.User 5 | alias PhoenixReact.UserQuery 6 | 7 | plug :scrub_params, "user" when action in [:create] 8 | 9 | # def new(conn, params) do 10 | # changeset = User.login_changeset(%User{}) 11 | # render(conn, PhoenixReact.SessionView, "new.html", changeset: changeset) 12 | # end 13 | 14 | def create(conn, params = %{}) do 15 | user = Repo.one(UserQuery.by_email(params["user"]["email"] || "")) 16 | if user do 17 | changeset = User.login_changeset(user, params["user"]) 18 | if changeset.valid? do 19 | jwt = elem(Guardian.encode_and_sign(user, :token),1) 20 | # perms: %{ default: Guardian.Permissions.max } 21 | json(conn, %{user: %{jwt: jwt, email: user.email, name: user.name}}) 22 | else 23 | conn 24 | |> put_status(400) 25 | |> json(%{formError: "Invalid username or password."}) 26 | 27 | end 28 | else 29 | conn 30 | |> put_status(400) 31 | |> json(%{formError: "Invalid username or password."}) 32 | end 33 | end 34 | 35 | def delete(conn, _params) do 36 | json(conn, %{info: "Logged out successfully."}) 37 | end 38 | 39 | def unauthenticated_api(conn, _params) do 40 | the_conn = put_status(conn, 401) 41 | case Guardian.Plug.claims(conn) do 42 | { :error, :no_session } -> json(the_conn, %{ error: "Login required" }) 43 | { :error, reason } -> json(the_conn, %{ error: reason }) 44 | _ -> json(the_conn, %{ error: "Login required" }) 45 | end 46 | end 47 | 48 | def forbidden_api(conn, _) do 49 | conn 50 | |> put_status(403) 51 | |> json(%{ error: :forbidden }) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.PageController do 2 | use PhoenixReact.Web, :controller 3 | 4 | def index(conn, _params) do 5 | render conn, "index.html" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /web/models/queries.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.UserQuery do 2 | import Ecto.Query 3 | alias PhoenixReact.User 4 | 5 | def by_email(email) do 6 | from u in User, where: u.email == ^email 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /web/models/user.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.User do 2 | use PhoenixReact.Web, :model 3 | 4 | alias PhoenixReact.Repo 5 | 6 | schema "users" do 7 | field :name, :string 8 | field :email, :string 9 | field :encrypted_password, :string 10 | field :password, :string, virtual: true 11 | 12 | timestamps 13 | end 14 | 15 | @required_register_fields ~w(name email password) 16 | @optional_register_fields ~w() 17 | 18 | @required_update_fields ~w(password) 19 | @optional_update_fields ~w(name, email) 20 | @password_min_length 5 21 | 22 | before_insert :maybe_update_password 23 | before_update :maybe_update_password 24 | 25 | def from_email(nil), do: { :error, :not_found } 26 | def from_email(email) do 27 | Repo.one(User, email: email) 28 | end 29 | 30 | def create_changeset(model, params \\ :empty) do 31 | model 32 | |> cast(params, @required_register_fields, @optional_register_fields) 33 | |> unique_constraint(:email, on: PhoenixReact.Repo, downcase: true) 34 | |> validate_length(:password, min: @password_min_length) 35 | end 36 | 37 | def update_changeset(model, params \\ :empty) do 38 | model 39 | |> cast(params, @required_update_fields, @optional_update_fields) 40 | |> unique_constraint(:email, on: PhoenixReact.Repo, downcase: true) 41 | |> validate_length(:password, min: @password_min_length) 42 | end 43 | 44 | def login_changeset(model), do: model |> cast(%{}, ~w(), ~w(email password)) 45 | 46 | def login_changeset(model, params) do 47 | model 48 | |> cast(params, ~w(email password), ~w()) 49 | |> validate_password 50 | end 51 | 52 | def valid_password?(nil, _), do: false 53 | def valid_password?(_, nil), do: false 54 | def valid_password?(password, crypted), do: Comeonin.Bcrypt.checkpw(password, crypted) 55 | 56 | defp maybe_update_password(changeset) do 57 | case Ecto.Changeset.fetch_change(changeset, :password) do 58 | { :ok, password } -> 59 | changeset 60 | |> Ecto.Changeset.put_change(:encrypted_password, Comeonin.Bcrypt.hashpwsalt(password)) 61 | :error -> changeset 62 | end 63 | end 64 | 65 | defp validate_password(changeset) do 66 | case Ecto.Changeset.get_field(changeset, :encrypted_password) do 67 | nil -> password_incorrect_error(changeset) 68 | crypted -> validate_password(changeset, crypted) 69 | end 70 | end 71 | 72 | defp validate_password(changeset, crypted) do 73 | password = Ecto.Changeset.get_change(changeset, :password) 74 | if valid_password?(password, crypted), do: changeset, else: password_incorrect_error(changeset) 75 | end 76 | 77 | defp password_incorrect_error(changeset), do: Ecto.Changeset.add_error(changeset, :password, "is incorrect") 78 | end -------------------------------------------------------------------------------- /web/npm-debug.log: -------------------------------------------------------------------------------- 1 | 0 info it worked if it ends with ok 2 | 1 verbose cli [ 'node', '/usr/local/bin/npm', 'start' ] 3 | 2 info using npm@2.12.1 4 | 3 info using node@v0.12.7 5 | 4 verbose node symlink /usr/local/bin/node 6 | 5 verbose run-script [ 'prestart', 'start', 'poststart' ] 7 | 6 info prestart phoenixreact@1.0.0 8 | 7 info start phoenixreact@1.0.0 9 | 8 verbose unsafe-perm in lifecycle true 10 | 9 info phoenixreact@1.0.0 Failed to exec start script 11 | 10 verbose stack Error: phoenixreact@1.0.0 start: `babel-node webpack.hot.js` 12 | 10 verbose stack spawn ENOENT 13 | 10 verbose stack at ChildProcess. (/usr/local/lib/node_modules/npm/lib/utils/spawn.js:17:16) 14 | 10 verbose stack at ChildProcess.emit (events.js:110:17) 15 | 10 verbose stack at maybeClose (child_process.js:1015:16) 16 | 10 verbose stack at Process.ChildProcess._handle.onexit (child_process.js:1087:5) 17 | 11 verbose pkgid phoenixreact@1.0.0 18 | 12 verbose cwd /Users/jaden/projects/PhoenixReact/web 19 | 13 error Darwin 14.5.0 20 | 14 error argv "node" "/usr/local/bin/npm" "start" 21 | 15 error node v0.12.7 22 | 16 error npm v2.12.1 23 | 17 error file sh 24 | 18 error code ELIFECYCLE 25 | 19 error errno ENOENT 26 | 20 error syscall spawn 27 | 21 error phoenixreact@1.0.0 start: `babel-node webpack.hot.js` 28 | 21 error spawn ENOENT 29 | 22 error Failed at the phoenixreact@1.0.0 start script 'babel-node webpack.hot.js'. 30 | 22 error This is most likely a problem with the phoenixreact package, 31 | 22 error not with npm itself. 32 | 22 error Tell the author that this fails on your system: 33 | 22 error babel-node webpack.hot.js 34 | 22 error You can get their info via: 35 | 22 error npm owner ls phoenixreact 36 | 22 error There is likely additional logging output above. 37 | 23 verbose exit [ 1, true ] 38 | -------------------------------------------------------------------------------- /web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.Router do 2 | use PhoenixReact.Web, :router 3 | require IEx 4 | 5 | pipeline :browser do 6 | plug :accepts, ["html"] 7 | plug :fetch_session 8 | plug :fetch_flash 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | plug :default_settings 12 | end 13 | 14 | pipeline :browser_session do 15 | # plug Guardian.Plug.VerifySession 16 | # plug Guardian.Plug.LoadResource 17 | end 18 | 19 | pipeline :api do 20 | plug :accepts, ["json"] 21 | plug :fetch_session 22 | plug :protect_from_forgery 23 | plug Guardian.Plug.VerifyHeader 24 | plug Guardian.Plug.LoadResource 25 | end 26 | 27 | scope "/", PhoenixReact do 28 | pipe_through [:browser, :browser_session] # Use the default browser stack 29 | get "/", PageController, :index 30 | end 31 | 32 | scope "/api", PhoenixReact.Api do 33 | pipe_through :api 34 | 35 | post "/sign_in", SessionController, :create, as: :login 36 | delete "/sign_out", SessionController, :delete, as: :logout 37 | post "/sign_up", RegistrationsController, :create, as: :register 38 | post "/account_update", RegistrationsController, :update, as: :account_update 39 | 40 | end 41 | 42 | # ------------------------------------------------------------------------------ 43 | 44 | defp default_settings(conn, _) do 45 | # "http://" <> Application.get_env(:phoenixReact, PhoenixReact.Endpoint)[:url][:host] <> 46 | default_settings = %{ 47 | api_url: "/api", 48 | csrfToken: get_csrf_token() 49 | } 50 | assign(conn, :default_settings, Poison.encode!(default_settings)) 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /web/static/js/app.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from 'react'; 4 | import Router from 'react-router'; 5 | import Routes from './app_routes'; 6 | import SettingsAction from './app/actions/settings'; 7 | import ReactDOM from 'react-dom'; 8 | 9 | import ThemeManager from 'material-ui/lib/styles/theme-manager'; 10 | 11 | 12 | //Change from using react-tab-event-plugin. See https://github.com/callemall/material-ui/issues/1030 13 | import EventPluginHub from 'react/lib/EventPluginHub'; 14 | import TapEventPlugin from 'react/lib/TapEventPlugin'; 15 | EventPluginHub.injection.injectEventPluginsByName({ TapEventPlugin }); 16 | 17 | 18 | // Set a device type based on window width, so that we can write media queries in javascript 19 | // by calling if (this.props.deviceType === "mobile") 20 | var deviceType; 21 | 22 | if (window.matchMedia("(max-width: 639px)").matches){ 23 | deviceType = "mobile"; 24 | } else if (window.matchMedia("(max-width: 768px)").matches){ 25 | deviceType = "tablet"; 26 | } else { 27 | deviceType = "desktop"; 28 | } 29 | 30 | // Initialize store singletons 31 | SettingsAction.load(window.DEFAULT_SETTINGS); 32 | 33 | // Old Router Code 34 | // Router.run(AppRoutes.Routes(), (Handler) => { 35 | // return React.render(, document.body); 36 | // }); 37 | 38 | import "!style!css!sass!mdi/scss/materialdesignicons.scss"; 39 | 40 | var App = new Routes; 41 | ReactDOM.render({App.Routes()}, document.getElementById('app')) 42 | 43 | 44 | // Router.run(routes, (Handler) => { 45 | // return React.render(, document.body); 46 | // }); 47 | 48 | // Use the HTML5 history API for cleaner URLs: 49 | // Router.run(routes, Router.HistoryLocation, (Handler) => { 50 | // return React.render(, document.body); 51 | // }); 52 | -------------------------------------------------------------------------------- /web/static/js/app/actions/dashboard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Constants from "../../common/constants"; 4 | import Dispatcher from "../../common/dispatcher"; 5 | import Socket from "../../common/actions/socket"; 6 | 7 | export default { 8 | 9 | sendChat(payload){ 10 | Socket.sendMessage("chat:lobby", "new_msg", {body: payload}); 11 | }, 12 | 13 | }; -------------------------------------------------------------------------------- /web/static/js/app/actions/form.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Constants from "../../common/constants"; 4 | import Dispatcher from "../../common/dispatcher"; 5 | 6 | export default { 7 | 8 | clearFormErrors(){ 9 | Dispatcher.dispatch({ 10 | action: Constants.CLEAR_FORM_ERRORS 11 | }); 12 | }, 13 | 14 | }; -------------------------------------------------------------------------------- /web/static/js/app/actions/messages.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Constants from "../../common/constants"; 4 | import Dispatcher from "../../common/dispatcher"; 5 | 6 | export default { 7 | 8 | addMessage(message){ 9 | Dispatcher.dispatch({ 10 | action: Constants.ADD_MESSAGE, 11 | data: message 12 | }); 13 | }, 14 | 15 | clearMessages(){ 16 | Dispatcher.dispatch({ 17 | action: Constants.CLEAR_MESSAGES 18 | }); 19 | }, 20 | 21 | removeMessage(messageID) { 22 | Dispatcher.dispatch({ 23 | action: Constants.REMOVE_MESSAGE, 24 | data: messageID 25 | }); 26 | } 27 | 28 | }; -------------------------------------------------------------------------------- /web/static/js/app/actions/settings.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Constants from "../../common/constants"; 4 | import Dispatcher from "../../common/dispatcher"; 5 | 6 | export default { 7 | 8 | load(defaultSettings){ 9 | Dispatcher.dispatch({ action: Constants.SETTINGS_LOAD, data: defaultSettings }); 10 | } 11 | 12 | }; -------------------------------------------------------------------------------- /web/static/js/app/actions/user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Constants from "../../common/constants"; 4 | import Dispatcher from "../../common/dispatcher"; 5 | 6 | import Api from "../../common/actions/api"; 7 | import Socket from "../../common/actions/socket"; 8 | 9 | export default { 10 | 11 | login(payload){ 12 | Dispatcher.dispatch({ action: Constants.LOGIN_PENDING }); 13 | Api.post(Constants.LOGIN, "sign_in", payload); 14 | }, 15 | 16 | register(payload) { 17 | Dispatcher.dispatch({ action: Constants.REGISTER_PENDING }); 18 | Api.post(Constants.REGISTER, "sign_up", payload); 19 | }, 20 | 21 | logout(){ 22 | Dispatcher.dispatch({ action: Constants.LOGOUT_PENDING }); 23 | Api.del(Constants.LOGOUT, "sign_out"); 24 | } 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /web/static/js/app/components/base_component.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Socket from "../../common/actions/socket"; 3 | 4 | // Note: If you override componentDidMount or componentWillUnmount you will need to 5 | // call super.componentDidMount() or super.componentWillUnmount() or call 6 | // watchStores() and unWatchStores() directly. 7 | export default class BaseComponent extends React.Component { 8 | 9 | constructor(props, context) { 10 | super(props, context); 11 | this._bind("storeChanged"); 12 | } 13 | 14 | _bind(...methods) { 15 | methods.forEach( (method) => this[method] = this[method].bind(this) ); 16 | } 17 | 18 | // Method to update state based upon store changes 19 | storeChanged(){ 20 | this.setState(this.getState(this.props, this.context)); 21 | } 22 | 23 | componentDidMount(){ 24 | this.watchStores(); 25 | this.connectChannels(); 26 | } 27 | 28 | componentWillUnmount(){ 29 | this.unWatchStores(); 30 | this.disconnectChannels(); 31 | } 32 | 33 | connectChannels() { 34 | _.forEach(this.channels, (channel) => { 35 | Socket.connectChannel(channel); 36 | }) 37 | } 38 | 39 | disconnectChannels() { 40 | _.forEach(this.channels, (channel) => { 41 | Socket.disconnectChannel(channel); 42 | }) 43 | } 44 | 45 | // Listen for changes in the stores 46 | watchStores(){ 47 | _.each(this.stores, function(store){ 48 | store.addChangeListener(this.storeChanged); 49 | }.bind(this)); 50 | } 51 | 52 | // Remove change listers from stores 53 | unWatchStores(){ 54 | _.each(this.stores, function(store){ 55 | store.removeChangeListener(this.storeChanged); 56 | }.bind(this)); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /web/static/js/app/components/common/message.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from 'react'; 4 | import MessageAction from "../../actions/messages"; 5 | import BaseComponent from "../base_component"; 6 | 7 | export default class Message extends BaseComponent { 8 | constructor(props, context) { 9 | super(props, context); 10 | this._bind('removeMessage', 'removeMessageFade', 'mouseOut', 'mouseOver'); 11 | this.state = { 12 | opacity: 1, 13 | cancel: false, 14 | allowCancel: true, 15 | timer: setTimeout(this.removeMessageFade, this.props.displayTime, this.props.id) 16 | } 17 | } 18 | 19 | getState() { 20 | return { 21 | } 22 | } 23 | 24 | removeMessage(id) { 25 | if (!this.state.cancel) { 26 | MessageAction.removeMessage(id); 27 | } 28 | } 29 | 30 | removeMessageFade(allowCancel) { 31 | this.setState({opacity: 0, allowCancel: allowCancel}); 32 | setTimeout(this.removeMessage, this.props.fadeTime, this.props.id); 33 | } 34 | 35 | mouseOver() { 36 | if(this.props.mouseOver){ 37 | this.props.mouseOver(); 38 | } else { 39 | if (this.state.allowCancel) { 40 | window.clearInterval(this.state.timer); 41 | this.setState({cancel: true, timer: null}); 42 | } 43 | } 44 | } 45 | 46 | mouseOut(id) { 47 | if(this.props.mouseOut){ 48 | this.props.mouseOut(); 49 | } else { 50 | if (this.state.allowCancel) { 51 | this.setState({cancel: false, opacity: 1, timer: setTimeout(this.removeMessageFade, this.props.displayTime, this.props.id)}); 52 | } 53 | } 54 | } 55 | 56 | getStyles() { 57 | return { 58 | opacity: this.state.opacity 59 | } 60 | } 61 | 62 | render() { 63 | var styles = this.getStyles(); 64 | return ( 65 |
{ this.mouseOver()} } onMouseOut={ (e) => { this.mouseOut()} }> 66 | {this.props.children} 67 | { this.removeMessageFade(false)} }>× 68 |
69 | ); 70 | } 71 | 72 | } 73 | 74 | Message.propTypes = { children: React.PropTypes.string }; 75 | Message.defaultProps = { 76 | displayTime: 4000, 77 | fadeTime: 1600 78 | }; -------------------------------------------------------------------------------- /web/static/js/app/components/common/messages.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from "react"; 4 | import MessagesStore from "../../stores/messages"; 5 | import Message from "./message"; 6 | import { Toolbar } from "material-ui"; 7 | import BaseComponent from "../base_component"; 8 | import _ from "lodash"; 9 | 10 | 11 | export default class Messages extends BaseComponent{ 12 | 13 | constructor(){ 14 | super(); 15 | this.stores = [MessagesStore]; 16 | this.state = this.getState(); 17 | } 18 | 19 | getState(){ 20 | return { 21 | messages: MessagesStore.current(), 22 | hasMessages: MessagesStore.hasMessages() 23 | } 24 | } 25 | 26 | render() { 27 | 28 | if(!this.state.hasMessages){ 29 | return null; 30 | } 31 | 32 | var messages = _.map(this.state.messages, function(message, i){ 33 | return {message}; 34 | }); 35 | 36 | return ( 37 | 38 |
    39 | {messages} 40 |
41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /web/static/js/app/components/defines.js: -------------------------------------------------------------------------------- 1 | export default { 2 | colors: { 3 | grey : "#616161", 4 | darkGrey : "#444444", 5 | lightGrey : "#eeeeee", 6 | snow : "#fafafa", 7 | teal : "#009698", 8 | white : "#ffffff", 9 | black : "#000000" 10 | } 11 | }; -------------------------------------------------------------------------------- /web/static/js/app/components/index.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { Grid, Row, Cell } from 'react-inline-grid'; 3 | 4 | import React from "react"; 5 | import Messages from "./common/messages"; 6 | import UsersStore from "../stores/user"; 7 | import mui from "material-ui"; 8 | import muiTheme from "../themes/mui_theme"; 9 | import BaseComponent from "./base_component"; 10 | import SettingsStore from "../../common/stores/settings"; 11 | import Header from "./layout/header"; 12 | 13 | var Colors = mui.Styles.Colors; 14 | var Typography = mui.Styles.Typography; 15 | var ThemeManager = mui.Styles.ThemeManager; 16 | 17 | var { AppCanvas } = mui; 18 | 19 | class Index extends BaseComponent { 20 | constructor(props, context) { 21 | super(props, context); 22 | this.stores = [SettingsStore] 23 | } 24 | 25 | storeChanged(){ 26 | if (!SettingsStore.loggedIn()) { 27 | this.context.history.pushState(null, '/login'); 28 | } 29 | } 30 | 31 | getChildContext() { 32 | return { 33 | muiTheme: ThemeManager.getMuiTheme(muiTheme) 34 | } 35 | } 36 | 37 | render(){ 38 | return ( 39 | 40 |
41 | {this.props.children} 42 |
47 | 48 | 49 | 50 |
51 | Built by Jaden Holladay 52 |
53 |
54 |
55 |
56 |
57 | 58 | ); 59 | } 60 | 61 | } 62 | 63 | Index.contextTypes = { 64 | history: React.PropTypes.object.isRequired, 65 | muiTheme: React.PropTypes.object 66 | }; 67 | 68 | Index.childContextTypes = { 69 | muiTheme: React.PropTypes.object 70 | } 71 | 72 | module.exports = Index; 73 | -------------------------------------------------------------------------------- /web/static/js/app/components/index.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from 'react'; 4 | import TestUtils from 'react/lib/ReactTestUtils'; 5 | import Index from './index'; 6 | import StubContext from '../../specs_support/stub_context'; 7 | 8 | describe('index', function() { 9 | var Subject; 10 | var result; 11 | 12 | beforeEach(()=>{ 13 | Subject = StubContext(Index, {}); 14 | result = TestUtils.renderIntoDocument(); 15 | }); 16 | 17 | it('renders the index', function() { 18 | expect(React.findDOMNode(result)).toBeDefined(); 19 | }); 20 | 21 | afterEach(()=>{ 22 | React.unmountComponentAtNode(React.findDOMNode(result).parentNode) 23 | }); 24 | }); -------------------------------------------------------------------------------- /web/static/js/app/components/layout/header.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { Grid, Row, Cell } from 'react-inline-grid'; 3 | import { Link } from "react-router"; 4 | import { Paper, TextField, FlatButton, RaisedButton, FontIcon, IconMenu, IconButton } from "material-ui"; 5 | import MenuItem from 'material-ui/lib/menus/menu-item'; 6 | 7 | import React from "react"; 8 | import BaseComponent from "../base_component"; 9 | import SettingsStore from "../../../common/stores/settings"; 10 | 11 | class Header extends BaseComponent { 12 | constructor(props, context) { 13 | super(props, context); 14 | this.stores = [SettingsStore] 15 | } 16 | 17 | getState() { 18 | return {} 19 | } 20 | 21 | styles() { 22 | return { 23 | header: { 24 | height: "45px", 25 | backgroundColor: "#2e2f33" 26 | }, 27 | logo: { 28 | verticalAlign: "middle", 29 | color: "#ffffff", 30 | }, 31 | userIcon: { 32 | float: "right", 33 | marginTop: "0px", 34 | marginRight: "3px", 35 | textTransform: "uppercase", 36 | color: "#7f8082" 37 | }, 38 | docs: { 39 | float: "right" 40 | }, 41 | docsIcon: { 42 | verticalAlign: "middle", 43 | color: "#ffffff", 44 | } 45 | } 46 | } 47 | 48 | accountMenu(e, value) { 49 | if (value == "logout") { 50 | this.context.history.pushState(null, '/logout'); 51 | } 52 | } 53 | 54 | accountDropDown() { 55 | var styles = this.styles(); 56 | if (SettingsStore.loggedIn()) { 57 | return( 58 | 59 | this.accountMenu(e, value)} iconButtonElement={ 60 | 61 | 62 | 63 | }> 64 | 65 | 66 | 67 | ) 68 | } else { 69 | return null 70 | } 71 | } 72 | 73 | render(){ 74 | var styles = this.styles(); 75 | return ( 76 |
77 | 78 | 79 | 80 | 81 | 82 | {this.accountDropDown()} 83 | 84 | 85 |
86 | ); 87 | } 88 | 89 | } 90 | 91 | Header.contextTypes = { 92 | history: React.PropTypes.object.isRequired, 93 | }; 94 | 95 | module.exports = Header; 96 | -------------------------------------------------------------------------------- /web/static/js/app/components/main/about.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from 'react'; 4 | 5 | export default class About extends React.Component{ 6 | render(){ 7 | return

About

; 8 | } 9 | }; 10 | module.export = About; -------------------------------------------------------------------------------- /web/static/js/app/components/main/dashboard.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { Link } from "react-router"; 3 | import { Paper, TextField, FlatButton, RaisedButton, FontIcon } from "material-ui"; 4 | import { Grid, Row, Cell } from 'react-inline-grid'; 5 | 6 | import React from "react"; 7 | import DashboardActions from "../../actions/dashboard"; 8 | import AtomicForm from "atomic-form"; 9 | import DashboardStore from "../../stores/dashboard"; 10 | import BaseComponent from "../base_component"; 11 | 12 | export default class Dashboard extends BaseComponent{ 13 | constructor(props, context) { 14 | super(props, context); 15 | this.stores = [DashboardStore] 16 | this.channels = ["chat:lobby"]; 17 | this.state = this.getState(); 18 | } 19 | 20 | getState() { 21 | return { 22 | messages: DashboardStore.messages() 23 | } 24 | } 25 | 26 | sendChat(formData) { 27 | DashboardActions.sendChat(formData.message); 28 | } 29 | 30 | collectFormData(refs) { 31 | var formData = {}; 32 | _.forEach(refs, (val, ref) => { 33 | formData[ref] = refs[ref].getValue(); 34 | }.bind(this)); 35 | return formData; 36 | } 37 | 38 | submitChatBox(e) { 39 | this.refs.chatBox.handleSubmit(e); 40 | } 41 | 42 | messages(messages) { 43 | var messages = _.map(messages, (message, index) => { 44 | return
45 | {message.user}: 46 | {message.message} 47 |
48 | }) 49 | return messages.reverse() 50 | } 51 | 52 | render(){ 53 | return ( 54 | 55 | 56 | 57 | this.submitChatBox}/> 58 | 59 | {this.messages(this.state.messages)} 60 | 61 | 62 | ); 63 | } 64 | }; 65 | module.export = Dashboard; 66 | 67 | // 68 | // 69 | // 70 | //
{this.messages(this.state.messages)}
-------------------------------------------------------------------------------- /web/static/js/app/components/main/home.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from 'react'; 4 | import { Link, History, Navigation } from "react-router"; 5 | 6 | export default class Home extends React.Component{ 7 | render(){ 8 | return ( 9 |
10 |

Home

11 | Login 12 |
13 | ); 14 | } 15 | }; 16 | module.export = Home; 17 | -------------------------------------------------------------------------------- /web/static/js/app/components/main/home.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from 'react'; 4 | import TestUtils from 'react/lib/ReactTestUtils'; 5 | import Home from './home'; 6 | 7 | describe('Home', ()=> { 8 | it('renders the home heading', ()=> { 9 | 10 | var result = TestUtils.renderIntoDocument(); 11 | expect(React.findDOMNode(result).textContent).toEqual('Home'); 12 | 13 | }); 14 | }); -------------------------------------------------------------------------------- /web/static/js/app/components/mixins/store_keeper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import _ from "lodash"; 4 | 5 | export default { 6 | 7 | getInitialState(){ 8 | return this.constructor.getState(); 9 | }, 10 | 11 | // Method to update state based upon store changes 12 | storeChanged(){ 13 | this.setState(this.constructor.getState()); 14 | }, 15 | 16 | // Listen for changes in the stores 17 | componentDidMount(){ 18 | _.each(this.constructor.stores, function(store){ 19 | store.addChangeListener(this.storeChanged); 20 | }.bind(this)); 21 | }, 22 | 23 | // Remove change listers from stores 24 | componentWillUnmount(){ 25 | _.each(this.constructor.stores, function(store){ 26 | store.removeChangeListener(this.storeChanged); 27 | }.bind(this)); 28 | } 29 | 30 | }; -------------------------------------------------------------------------------- /web/static/js/app/components/not_found.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from 'react'; 4 | 5 | export default class NotFound extends React.Component{ 6 | render(){ 7 | return

Not Found

; 8 | } 9 | }; 10 | module.export = NotFound; 11 | -------------------------------------------------------------------------------- /web/static/js/app/components/users/login.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Link, History, Navigation } from "react-router"; 4 | import { Paper, TextField, FlatButton, RaisedButton, FontIcon } from "material-ui"; 5 | import { Grid, Row, Cell } from 'react-inline-grid'; 6 | 7 | import React from "react"; 8 | import Validator from "validator"; 9 | import UserActions from "../../actions/user"; 10 | import FormActions from "../../actions/form"; 11 | import _ from "lodash"; 12 | import assign from "object-assign"; 13 | import UserStore from "../../stores/user"; 14 | import FormStore from "../../stores/form"; 15 | import AtomicForm from "atomic-form"; 16 | import BaseComponent from "../base_component"; 17 | import SettingsStore from "../../../common/stores/settings"; 18 | 19 | class Login extends BaseComponent { 20 | constructor(props, context){ 21 | super(props, context); 22 | 23 | this.stores = [UserStore, SettingsStore, FormStore]; 24 | this.state = this.getState(); 25 | this._bind('validationMessage', 'onInputChange', 'afterValidation') 26 | } 27 | 28 | componentWillReceiveProps(nextProps) { 29 | this.props = nextProps; 30 | this.setState(this.getState()); 31 | } 32 | 33 | getState() { 34 | var nextPathname = this.props.location.state && this.props.location.state.nextPathname 35 | return { 36 | loggedIn: SettingsStore.loggedIn(), 37 | nextPathname: nextPathname || '/dashboard', 38 | submitDisabled: true, 39 | formError: FormStore.formMessages() 40 | }; 41 | } 42 | 43 | // Method to update state based upon store changes 44 | storeChanged(){ 45 | this.setState(this.getState()); 46 | } 47 | 48 | componentWillMount(){ 49 | if(this.state.loggedIn) { 50 | this.context.history.pushState(null, this.state.nextPathname); 51 | } 52 | } 53 | 54 | componentDidUpdate(){ 55 | if(this.state.loggedIn) { 56 | this.context.history.pushState(null, this.state.nextPathname); 57 | } 58 | } 59 | 60 | handleSubmit(formData){ 61 | UserActions.login({ 62 | user: { 63 | email: formData.email, 64 | password: formData.password 65 | } 66 | }); 67 | } 68 | 69 | collectFormData(refs) { 70 | var formData = {}; 71 | _.forEach(refs, (val, ref) => { 72 | formData[ref] = refs[ref].getValue(); 73 | }.bind(this)); 74 | return formData; 75 | } 76 | 77 | afterValidation(formValidations) { 78 | //Callback after validation fails. 79 | this.setState({validations: formValidations}); 80 | } 81 | 82 | onInputChange(e, ref) { 83 | var formData = this.refs.MainForm.formData(); 84 | var formValidations = this.refs.MainForm.validateForm(formData); 85 | var validations = this.state.validations || {}; 86 | validations[ref] = formValidations[ref]; 87 | var submitDisabled = !this.refs.MainForm.allValid(formValidations); 88 | this.setState({validations: validations, submitDisabled:submitDisabled}); 89 | } 90 | 91 | validationMessage(field) { 92 | var res = {}; 93 | if (this.state.validations && this.state.validations[field]) { 94 | if (!this.state.validations[field].isValid) { 95 | res = this.state.validations[field].message[0]; 96 | return res; 97 | } 98 | } 99 | return null; 100 | } 101 | 102 | getStyles() { 103 | return { 104 | socialButtons: { 105 | height: "100%", 106 | verticalAlign: "middle", 107 | float: "left", 108 | paddingLeft: "12px", 109 | lineHeight: "36px", 110 | color: "#295266" 111 | }, 112 | 113 | }; 114 | } 115 | 116 | render(){ 117 | var styles = this.getStyles(); 118 | return ( 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 |
127 |
{this.state.formError}
128 | this.onInputChange(e, 'email')} validate={[ 129 | { 130 | message: "Must be a valid Email.", 131 | validate: "isEmail", 132 | } 133 | ]}/> 134 | this.onInputChange(e, 'password')} validate={[ 135 | { 136 | message: "Password can't be blank.", 137 | validate: "isLength", 138 | args: [1] 139 | } 140 | ]}/> 141 | 142 |

143 | Need an account? Sign Up 144 |

145 |
146 |
147 |
148 |
149 |
150 |
151 |
); 152 | } 153 | 154 | } 155 | 156 | Login.contextTypes = { 157 | history: React.PropTypes.object.isRequired, 158 | muiTheme: React.PropTypes.object 159 | }; 160 | 161 | module.exports = Login; 162 | 163 | 164 | // 165 | // 166 | // 167 | // 168 | // 169 | // 170 | // 171 | // 172 | // 173 | // 174 | // 175 | // 176 | // 177 | // 178 | // 179 | // 180 | // -------------------------------------------------------------------------------- /web/static/js/app/components/users/login.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from 'react'; 4 | import TestUtils from 'react/lib/ReactTestUtils'; 5 | import Login from './login'; 6 | import Utils from '../../../specs_support/utils'; 7 | import StubContext from '../../../specs_support/stub_context'; 8 | 9 | describe ('Login', function(){ 10 | var login; 11 | var loginDOM; 12 | var labels; 13 | var form; 14 | var Subject; 15 | var result; 16 | 17 | beforeEach(function(){ 18 | Subject = StubContext(Login); 19 | result = TestUtils.renderIntoDocument(); 20 | login = result.originalComponent(); 21 | loginDOM = React.findDOMNode(result); 22 | labels = TestUtils.scryRenderedDOMComponentsWithTag(result, 'label'); 23 | form = TestUtils.findRenderedDOMComponentWithTag(result, 'form'); 24 | }); 25 | 26 | it('Renders the login component', function(){ 27 | expect(loginDOM).toBeDefined(); 28 | 29 | var email = Utils.findTextField(labels, 'email'); 30 | expect(email).toBeDefined(); 31 | 32 | var password = Utils.findTextField(labels, 'password'); 33 | expect(password).toBeDefined(); 34 | }); 35 | 36 | it('outputs a validation error if no email is provided', function(){ 37 | TestUtils.Simulate.submit(form); 38 | expect(form.getDOMNode().textContent).toContain('Invalid email'); 39 | }); 40 | 41 | it('It calls the handleLogin method when the form is submitted', function(){ //The submit has to be called twice, for reasons unknown 42 | spyOn(login, 'handleLogin'); 43 | TestUtils.Simulate.submit(form); 44 | expect(login.handleLogin).toHaveBeenCalled(); 45 | }); 46 | 47 | it('It calls the handleLogin method with an email address in the form', function(){ //Trying to figure out double submit issue 48 | spyOn(login, 'handleLogin'); 49 | var email = Utils.findTextField(labels, 'email'); 50 | email.getDOMNode().value = 'johndoe@example.com'; 51 | TestUtils.Simulate.submit(form); 52 | expect(login.handleLogin).toHaveBeenCalled(); 53 | }); 54 | 55 | it('It calls the validateEmail when the form is submitted', function(){ 56 | spyOn(login, 'validateEmail'); 57 | TestUtils.Simulate.submit(form); 58 | expect(login.validateEmail).toHaveBeenCalled(); 59 | }); 60 | 61 | it('It calls the validateEmail method with an email address in the form', function(){ 62 | spyOn(login, 'validateEmail'); 63 | var email = Utils.findTextField(labels, 'email'); 64 | email.getDOMNode().value = 'johndoe@example.com'; 65 | TestUtils.Simulate.submit(form); 66 | expect(login.validateEmail).toHaveBeenCalled(); 67 | }); 68 | 69 | afterEach(()=>{ 70 | React.unmountComponentAtNode(React.findDOMNode(result).parentNode) 71 | }); 72 | }); 73 | 74 | -------------------------------------------------------------------------------- /web/static/js/app/components/users/logout.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from 'react'; 4 | import {Link} from 'react-router'; 5 | import BaseComponent from "../base_component"; 6 | import UserActions from "../../actions/user"; 7 | import SettingsStore from "../../../common/stores/settings"; 8 | 9 | class Logout extends BaseComponent { 10 | 11 | constructor(){ 12 | super(); 13 | UserActions.logout(); 14 | } 15 | 16 | logout() { 17 | UserActions.logout(); 18 | } 19 | 20 | render(){ 21 | return (
Ooops, an error while logging out may have occured. logout()}>Click here to try Again.
) 22 | } 23 | } 24 | 25 | module.exports = Logout; 26 | -------------------------------------------------------------------------------- /web/static/js/app/components/users/register.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { Link } from 'react-router'; 3 | import { Grid, Row, Cell } from 'react-inline-grid'; 4 | import { Paper, TextField, FlatButton, RaisedButton, FontIcon } from "material-ui"; 5 | 6 | import React from 'react'; 7 | import Validator from "validator"; 8 | import UserStore from "../../stores/user"; 9 | import UserActions from "../../actions/user"; 10 | import _ from "lodash"; 11 | import assign from "object-assign"; 12 | import AtomicForm from "atomic-form"; 13 | import BaseComponent from "../base_component"; 14 | import Defines from "../defines"; 15 | import SettingsStore from "../../../common/stores/settings"; 16 | 17 | class Register extends BaseComponent { 18 | 19 | constructor(props, context){ 20 | super(props, context) 21 | this.stores = [UserStore] 22 | this._bind('onInputChange', 'validationMessage', 'onInputChange', 'afterValidation') 23 | this.state = this.getState(); 24 | } 25 | 26 | componentWillReceiveProps(nextProps) { 27 | this.props = nextProps; 28 | this.setState(this.getState()); 29 | } 30 | 31 | getState() { 32 | var nextPathname = this.props.location.state && this.props.location.state.nextPathname 33 | return { 34 | loggedIn: SettingsStore.loggedIn(), 35 | nextPathname: nextPathname || '/dashboard', 36 | submitDisabled: true 37 | }; 38 | } 39 | 40 | storeChanged() { 41 | this.setState(this.getState()); 42 | } 43 | 44 | componentWillMount(){ 45 | if(this.state.loggedIn) { 46 | this.context.history.pushState(null, this.state.nextPathname); 47 | } 48 | } 49 | 50 | componentDidUpdate(){ 51 | if(this.state.loggedIn) { 52 | this.context.history.pushState(null, this.state.nextPathname); 53 | } 54 | } 55 | 56 | handleRegister(formData){ 57 | UserActions.register({ 58 | user: { 59 | email: formData.email, 60 | password: formData.password, 61 | name: formData.name 62 | } 63 | }); 64 | } 65 | 66 | onInputChange(e, ref) { 67 | var formData = this.refs.MainForm.formData(); 68 | var formValidations = this.refs.MainForm.validateForm(formData); 69 | var validations = this.state.validations || {}; 70 | _.forEach(ref, (ref) => { 71 | validations[ref] = formValidations[ref]; 72 | }) 73 | var submitDisabled = !this.refs.MainForm.allValid(formValidations); 74 | this.setState({validations: validations, submitDisabled: submitDisabled}); 75 | } 76 | 77 | getStyles(){ 78 | return { 79 | signUpButton: { 80 | marginTop: "20px", 81 | backgroundColor: Defines.colors.teal 82 | } 83 | }; 84 | } 85 | 86 | afterValidation(formValidations) { 87 | //Callback after validation fails. 88 | this.setState({validations: formValidations}); 89 | } 90 | 91 | collectFormData(refs) { 92 | var formData = {}; 93 | _.forEach(refs, (val, ref) => { 94 | formData[ref] = refs[ref].getValue(); 95 | }.bind(this)); 96 | return formData; 97 | } 98 | 99 | validationMessage(field) { 100 | var res = {}; 101 | if (this.state.validations && this.state.validations[field]) { 102 | if (!this.state.validations[field].isValid) { 103 | res = this.state.validations[field].message[0]; 104 | return res; 105 | } 106 | } 107 | return null; 108 | } 109 | 110 | 111 | render(){ 112 | var styles = this.getStyles(); 113 | return ( 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |

Create An Account

122 | this.onInputChange(e, ['email'])} validate={[ 123 | { 124 | message: "Must be a valid Email.", 125 | validate: "isEmail", 126 | } 127 | ]}/> 128 | this.onInputChange(e, ['name'])} validate={[ 129 | { 130 | message: "Cannot contain spaces.", 131 | validate: (val) => { return !/\s/.test(val)} 132 | }, 133 | { 134 | message: "Name must be at least 3 characters.", 135 | validate: "isLength", 136 | args: [3] 137 | } 138 | ]}/> 139 | this.onInputChange(e, ['password', 'confirmPassword'])} onChange={(e) => this.onInputChange(e, ['password', 'confirmPassword'])} validate={[ 140 | { 141 | message: "Password must be at least 5 characters.", 142 | validate: "isLength", 143 | args: [5] 144 | } 145 | ]}/> 146 | this.onInputChange(e, ['password', 'confirmPassword'])} validate={[ 147 | { 148 | message: "Passwords don't match.", 149 | validate: (val, formData) => { return val == formData.password} 150 | } 151 | ]}/> 152 | 153 |

154 | Already have an account? Sign in 155 |

156 |
157 |
158 |
159 |
160 |
161 |
162 |
); 163 | } 164 | } 165 | 166 | Register.contextTypes = { 167 | history: React.PropTypes.object.isRequired 168 | }; 169 | 170 | 171 | module.exports = Register; 172 | 173 | 174 | // register: { 175 | // width: "400px", 176 | // margin: "5% auto", 177 | // backgroundColor: Defines.colors.white, 178 | // } -------------------------------------------------------------------------------- /web/static/js/app/stores/dashboard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Constants from "../../common/constants"; 4 | import Dispatcher from "../../common/dispatcher"; 5 | import StoreCommon from "../../common/stores/store_common"; 6 | import assign from "object-assign"; 7 | 8 | var _messages = []; 9 | 10 | function receiveMessage(payload){ 11 | _messages.push({message: payload.data.body, user: payload.data.user}); 12 | } 13 | 14 | // Extend User Store with EventEmitter to add eventing capabilities 15 | var DashboardStore = assign({}, StoreCommon, { 16 | 17 | // Return current user 18 | messages(){ 19 | return _messages 20 | }, 21 | 22 | }); 23 | 24 | // Register callback with Dispatcher 25 | Dispatcher.register((payload) => { 26 | var action = payload.action; 27 | 28 | switch(action){ 29 | 30 | // Respond to REGISTER action 31 | case Constants.NEW_MSG: 32 | receiveMessage(payload); 33 | break; 34 | 35 | default: 36 | return true; 37 | } 38 | 39 | // If action was responded to, emit change event 40 | DashboardStore.emitChange(); 41 | 42 | return true; 43 | 44 | }); 45 | 46 | export default DashboardStore; 47 | 48 | -------------------------------------------------------------------------------- /web/static/js/app/stores/form.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Constants from "../../common/constants"; 4 | import Dispatcher from "../../common/dispatcher"; 5 | import StoreCommon from "../../common/stores/store_common"; 6 | import assign from "object-assign"; 7 | 8 | var _formMessages = []; 9 | 10 | // Extend User Store with EventEmitter to add eventing capabilities 11 | var FormStore = assign({}, StoreCommon, { 12 | 13 | formMessages() { 14 | if (!_.isEmpty(_formMessages)) { 15 | return _formMessages.pop(); 16 | } else { 17 | return null; 18 | } 19 | } 20 | 21 | }); 22 | 23 | // Register callback with Dispatcher 24 | Dispatcher.register((payload) => { 25 | var action = payload.action; 26 | 27 | switch(action){ 28 | 29 | case Constants.ERROR: 30 | if (payload.data.body.formError) { 31 | _formMessages = []; 32 | _formMessages.push(payload.data.body.formError); 33 | } 34 | break; 35 | 36 | case Constants.CLEAR_FORM_ERRORS: 37 | _formMessages = []; 38 | break; 39 | 40 | case Constants.LOGOUT: 41 | _formMessages = []; 42 | break; 43 | 44 | default: 45 | return true; 46 | } 47 | 48 | // If action was responded to, emit change event 49 | FormStore.emitChange(); 50 | 51 | return true; 52 | 53 | }); 54 | 55 | export default FormStore; 56 | 57 | -------------------------------------------------------------------------------- /web/static/js/app/stores/messages.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Constants from "../../common/constants"; 4 | import Dispatcher from "../../common/dispatcher"; 5 | import StoreCommon from "../../common/stores/store_common"; 6 | import assign from "object-assign"; 7 | import _ from "lodash"; 8 | 9 | const MessageTimeout = 5000; 10 | 11 | var _messages = {}; 12 | var messageCount = 0; 13 | 14 | // Extend Message Store with EventEmitter to add eventing capabilities 15 | var MessagesStore = assign({}, StoreCommon, { 16 | 17 | // Return current messages 18 | current(){ 19 | return _messages; 20 | }, 21 | 22 | hasMessages(){ 23 | return _.keys(_messages).length > 0; 24 | } 25 | 26 | }); 27 | 28 | // Register callback with Dispatcher 29 | Dispatcher.register(function(payload) { 30 | 31 | switch(payload.action){ 32 | 33 | // Respond to TIMEOUT action 34 | case Constants.TIMEOUT: 35 | addMessage("Request timed out. Reponse was: " + payload.data); 36 | break; 37 | 38 | // Respond to NOT_AUTHORIZED action 39 | case Constants.NOT_AUTHORIZED: 40 | addServerMessage(payload.data); 41 | break; 42 | 43 | // Respond to ERROR action 44 | case Constants.ERROR: 45 | addServerMessage(payload.data); 46 | break; 47 | 48 | // Respond to ADD_MESSAGE action 49 | case Constants.ADD_MESSAGE: 50 | addMessage(payload.data); 51 | break; 52 | 53 | // Respond to REMOVE_MESSAGE action 54 | case Constants.REMOVE_MESSAGE: 55 | removeMessage(payload.messageId); 56 | break; 57 | 58 | // Respond to CLEAR_MESSAGES action 59 | case Constants.CLEAR_MESSAGES: 60 | clearMessages(); 61 | break; 62 | 63 | default: 64 | return true; 65 | } 66 | 67 | // If action was responded to, emit change event 68 | MessagesStore.emitChange(); 69 | 70 | return true; 71 | 72 | }); 73 | 74 | function clearMessages(){ 75 | _messages = {}; 76 | } 77 | 78 | function addServerMessage(data){ 79 | var parsed = JSON.parse(data.text); 80 | var messageId = addMessage(parsed.message || parsed.error); 81 | setTimeout(function(){ 82 | removeMessage(messageId); 83 | }, MessageTimeout); 84 | } 85 | 86 | function removeMessage(messageId){ 87 | _messages = _.omit(_messages, messageId); 88 | MessagesStore.emitChange(); 89 | } 90 | 91 | function addMessage(message){ 92 | messageCount++; 93 | _messages[messageCount] = message; 94 | return messageCount; 95 | } 96 | 97 | export default MessagesStore; 98 | 99 | -------------------------------------------------------------------------------- /web/static/js/app/stores/messages.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'react-router'; 3 | import MessagesStore from './messages'; 4 | import MessageActions from '../actions/messages'; 5 | import Dispatcher from "../../common/dispatcher"; 6 | 7 | describe('MessagesStore', () => { 8 | 9 | beforeEach(() => { 10 | jasmine.clock().install(); // Mock out the built in timers 11 | }); 12 | 13 | afterEach(() => { 14 | jasmine.clock().uninstall(); 15 | }); 16 | 17 | describe("No messages", () => { 18 | 19 | beforeEach(() => { 20 | // Ensure the store is empty 21 | 22 | //////NEED TO PUT SPOOF PROPS IN HERE FOR componentWillUnmount ////////// 23 | 24 | MessageActions.clearMessages(); 25 | jasmine.clock().tick(); // Advance the clock to the next tick 26 | }); 27 | 28 | describe("current", () => { 29 | it("returns current messages", (done) => { 30 | var messages = MessagesStore.current(); 31 | expect(messages).toEqual({}); 32 | done(); 33 | }); 34 | }); 35 | 36 | describe("hasMessages", () => { 37 | it("returns false", (done) => { 38 | expect(MessagesStore.hasMessages()).toBe(false); 39 | done(); 40 | }); 41 | }); 42 | 43 | }); 44 | 45 | describe("Has messages", () => { 46 | 47 | var message = "A message to test has messages in the message spec store."; 48 | 49 | beforeEach(() => { 50 | MessageActions.addMessage(message); 51 | jasmine.clock().tick(); // Advance the clock to the next tick 52 | }); 53 | 54 | describe("current", () => { 55 | it("returns current messages", (done) => { 56 | var messages = MessagesStore.current(); 57 | var storedMessage = _.find(messages, (v, k) => { 58 | return v == message; 59 | }); 60 | expect(storedMessage).toEqual(message); 61 | done(); 62 | }); 63 | }); 64 | 65 | describe("hasMessages", () => { 66 | it("returns true", (done) => { 67 | expect(MessagesStore.hasMessages()).toBe(true); 68 | done(); 69 | }); 70 | }); 71 | 72 | }); 73 | 74 | }); -------------------------------------------------------------------------------- /web/static/js/app/stores/user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Constants from "../../common/constants"; 4 | import Dispatcher from "../../common/dispatcher"; 5 | import StoreCommon from "../../common/stores/store_common"; 6 | import assign from "object-assign"; 7 | 8 | var _user = {}; 9 | 10 | // log the user in 11 | function login(payload){ 12 | _user = payload.data.body.user; 13 | } 14 | 15 | // Register 16 | function register(payload){ 17 | _user = payload.data.body.user; 18 | } 19 | 20 | function loadUserFromSettings(payload) { 21 | _user.email = payload.data.email; 22 | _user.displayName = payload.data.displayName; 23 | } 24 | 25 | // Extend User Store with EventEmitter to add eventing capabilities 26 | var UserStore = assign({}, StoreCommon, { 27 | 28 | // Return current user 29 | current(){ 30 | return _user; 31 | }, 32 | 33 | }); 34 | 35 | // Register callback with Dispatcher 36 | Dispatcher.register((payload) => { 37 | var action = payload.action; 38 | 39 | switch(action){ 40 | 41 | // Respond to LOGIN action 42 | case Constants.LOGIN: 43 | login(payload); 44 | break; 45 | 46 | // Respond to REGISTER action 47 | case Constants.REGISTER: 48 | register(payload); 49 | break; 50 | 51 | case Constants.LOGOUT: 52 | _user = {}; 53 | break; 54 | 55 | default: 56 | return true; 57 | } 58 | 59 | // If action was responded to, emit change event 60 | UserStore.emitChange(); 61 | 62 | return true; 63 | 64 | }); 65 | 66 | export default UserStore; 67 | 68 | -------------------------------------------------------------------------------- /web/static/js/app/themes/mui_theme.jsx: -------------------------------------------------------------------------------- 1 | var Colors = require('material-ui/lib/styles/colors'); 2 | var ColorManipulator = require('material-ui/lib/utils/color-manipulator'); 3 | var Spacing = require('material-ui/lib/styles/spacing'); 4 | 5 | 6 | module.exports = { 7 | spacing: Spacing, 8 | fontFamily: 'Fira Sans, sans-serif', 9 | palette: { 10 | primary1Color: Colors.white, 11 | primary2Color: '#A3311D', 12 | primary3Color: Colors.lightBlack, 13 | accent1Color: '#6CA547', 14 | accent2Color: '#A3311D', 15 | accent3Color: Colors.grey500, 16 | textColor: Colors.white, 17 | alternateTextColor: '#636266', 18 | canvasColor: '#47474A', 19 | borderColor: Colors.grey300, 20 | disabledColor: ColorManipulator.fade(Colors.white, 0.7) 21 | } 22 | }; -------------------------------------------------------------------------------- /web/static/js/app_routes.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from 'react'; 4 | import { Router, Route, Link, IndexRoute } from 'react-router'; 5 | 6 | var appDirectory = './app'; 7 | 8 | // ------------------ Components -------------------------- 9 | import Index from './app/components/index'; 10 | import Home from './app/components/main/home'; 11 | import Login from './app/components/users/login'; 12 | import Logout from './app/components/users/logout'; 13 | import Register from './app/components/users/register'; 14 | import Dashboard from './app/components/main/dashboard'; 15 | import NotFound from './app/components/not_found'; 16 | import About from './app/components/main/about'; 17 | // ------------------ Components -------------------------- 18 | 19 | // ------------------ Stores -------------------------- 20 | import SettingsStore from './common/stores/settings'; 21 | // ------------------ Stores -------------------------- 22 | 23 | export default class Routes { 24 | 25 | requireAuth(nextState, replaceState) { 26 | if(!SettingsStore.loggedIn()){ 27 | // Redirects the user to sign in. Onced authenticated, we can redirect 28 | // to the path they originally were attempting to access by using: 29 | // this.props.locations.state.nextPathname 30 | replaceState({ nextPathname: nextState.location.pathname }, '/login'); 31 | } 32 | } 33 | 34 | Routes() { 35 | return( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /web/static/js/common/actions/api.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Request from "superagent"; 4 | import Constants from "../constants"; 5 | import Dispatcher from "../dispatcher"; 6 | import SettingsStore from '../stores/settings'; 7 | import _ from "lodash"; 8 | 9 | const TIMEOUT = 10000; 10 | 11 | var _pendingRequests = {}; 12 | var _cache = {}; 13 | 14 | function abortPendingRequests(url) { 15 | if(_pendingRequests[url]) { 16 | _pendingRequests[url]._callback = function() {}; 17 | _pendingRequests[url].abort(); 18 | _pendingRequests[url] = null; 19 | } 20 | } 21 | 22 | // Get the JWT from the Settings 23 | function jwt() { 24 | return SettingsStore.jwt(); 25 | } 26 | 27 | function csrfToken() { 28 | return SettingsStore.current().csrfToken; 29 | } 30 | 31 | function makeUrl(part){ 32 | if(part.indexOf("http") >= 0){ 33 | return part; 34 | } else { 35 | var slash = _.last(SettingsStore.current().apiUrl.split("")) == "/" ? "" :"/"; 36 | if(part[0] == "/"){ 37 | part = part.slice(1); 38 | } 39 | return SettingsStore.current().apiUrl + slash + part; 40 | } 41 | } 42 | 43 | // GET request with a JWT token and CSRF token 44 | function get(url) { 45 | return Request 46 | .get(url) 47 | .timeout(TIMEOUT) 48 | .set('Accept', 'application/json') 49 | .set('Authorization', 'Bearer ' + jwt()) 50 | .set('X-CSRF-Token', csrfToken()); 51 | } 52 | 53 | // POST request with a JWT token and CSRF token 54 | function post(url, body) { 55 | return Request 56 | .post(url) 57 | .send(body) 58 | .set('Accept', 'application/json') 59 | .timeout(TIMEOUT) 60 | .set('Authorization', 'Bearer ' + jwt()) 61 | .set('X-CSRF-Token', csrfToken()); 62 | } 63 | 64 | // PUT request with a JWT token and CSRF token 65 | function put(url, body) { 66 | return Request 67 | .put(url) 68 | .send(body) 69 | .set('Accept', 'application/json') 70 | .timeout(TIMEOUT) 71 | .set('Authorization', 'Bearer ' + jwt()) 72 | .set('X-CSRF-Token', csrfToken()); 73 | } 74 | 75 | // DELETER request with a JWT token and CSRF token 76 | function del(url) { 77 | return Request 78 | .del(url) 79 | .set('Accept', 'application/json') 80 | .timeout(TIMEOUT) 81 | .set('Authorization', 'Bearer ' + jwt()) 82 | .set('X-CSRF-Token', csrfToken()); 83 | } 84 | 85 | function dispatch(key, response) { 86 | Dispatcher.dispatch({ 87 | action: key, 88 | data: response 89 | }); 90 | return true; 91 | } 92 | 93 | // Dispatch a response based on the server response 94 | function dispatchResponse(key) { 95 | return (err, response) => { 96 | if(err && err.timeout === TIMEOUT) { 97 | return dispatch(Constants.TIMEOUT, response); 98 | } else if(response.status === 401) { 99 | return dispatch(Constants.NOT_AUTHORIZED, response); 100 | } else if(response.status === 400) { 101 | return dispatch(Constants.ERROR, response); 102 | } else if(response.status === 302) { 103 | window.location = response.body.url; 104 | } else if(!response.ok) { 105 | return dispatch(Constants.ERROR, response); 106 | } else if(key) { 107 | return dispatch(key, response); 108 | } 109 | return false; 110 | }; 111 | } 112 | 113 | function doRequest(key, url, requestMethod){ 114 | 115 | var requestContainer = buildRequest(url, requestMethod); 116 | if(requestContainer.promise){ 117 | return requestContainer.promise; 118 | } 119 | 120 | var promise = new Promise((resolve, reject) => { 121 | requestContainer.request.end((error, res) => { 122 | disposeRequest(url); 123 | var handled = dispatchResponse(key)(error, res); 124 | if(error && !handled){ 125 | reject(error); 126 | } else { 127 | resolve(res); 128 | } 129 | }); 130 | }); 131 | 132 | requestContainer.promise = promise; 133 | return promise; 134 | } 135 | 136 | function buildRequest(url, requestMethod){ 137 | //abortPendingRequests(url); 138 | if (!_pendingRequests[url]) { 139 | _pendingRequests[url] = { 140 | request: requestMethod(makeUrl(url)) 141 | }; 142 | } 143 | return _pendingRequests[url]; 144 | } 145 | 146 | function disposeRequest(url){ 147 | delete _pendingRequests[url]; 148 | } 149 | 150 | function promisify(request) { 151 | return new Promise((resolve, reject) => { 152 | request.end((error, res) => { 153 | if (error) { 154 | reject(error); 155 | } else { 156 | resolve(res); 157 | } 158 | }); 159 | }); 160 | } 161 | 162 | function *doCacheRequest(url, key, requestMethod){ 163 | 164 | var promise; 165 | var fullUrl = makeUrl(url); 166 | 167 | if (_cache[fullUrl]) { 168 | setTimeout(() => { 169 | dispatchResponse(key)(null, _cache[fullUrl]); 170 | }, 1); 171 | 172 | promise = new Promise((resolve, reject) => { 173 | resolve(_cache[fullUrl]); 174 | }); 175 | 176 | yield promise; 177 | }; 178 | 179 | var requestContainer = buildRequest(url, requestMethod); 180 | if(requestContainer.promise){ 181 | yield requestContainer.promise; 182 | } else { 183 | promise = promisify(requestContainer.request); 184 | requestContainer.promise = promise; 185 | promise.then((result) => { 186 | disposeRequest(url); 187 | dispatchResponse(key)(null, result); 188 | _cache[fullUrl] = result; 189 | _cache[fullUrl].isCached = true; 190 | return result; 191 | }, (err) => { 192 | dispatchResponse(key)(err, err.response); 193 | }); 194 | yield promise; 195 | } 196 | 197 | } 198 | 199 | var API = { 200 | 201 | get(key, url){ 202 | return doRequest(key, url, (fullUrl) => { 203 | return get(fullUrl); 204 | }); 205 | }, 206 | 207 | post(key, url, body){ 208 | return doRequest(key, url, (fullUrl) => { 209 | return post(fullUrl, body); 210 | }); 211 | }, 212 | 213 | put(key, url, body){ 214 | return doRequest(key, url, (fullUrl) => { 215 | return put(fullUrl, body); 216 | }); 217 | }, 218 | 219 | del(key, url){ 220 | return doRequest(key, url, (fullUrl) => { 221 | return del(fullUrl); 222 | }); 223 | }, 224 | 225 | async cacheGet(url, params, key, refresh){ 226 | url = `${url}${API.queryStringFrom(params)}`; 227 | var request = doCacheRequest(url, key, (fullUrl) => { 228 | return get(fullUrl); 229 | }); 230 | if (key) { 231 | // We have a key. Invoke the generate to get data and dispatch. 232 | var response = request.next(); 233 | while (refresh && !response.done){ 234 | response = request.next(); 235 | } 236 | } else { 237 | // Return the generator and let the calling code invoke it. 238 | return request; 239 | } 240 | }, 241 | 242 | queryStringFrom(params){ 243 | var query = _.chain(params) 244 | .map((val, key) => { 245 | if(val){ 246 | return `${key}=${val}`; 247 | } else { 248 | return ""; 249 | } 250 | }) 251 | .compact() 252 | .value(); 253 | 254 | if(query.length > 0){ 255 | return `?${query.join("&")}`; 256 | } else { 257 | return ""; 258 | } 259 | 260 | } 261 | 262 | }; 263 | 264 | export default API; 265 | -------------------------------------------------------------------------------- /web/static/js/common/actions/socket.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from "../dispatcher"; 2 | import SettingsStore from '../stores/settings'; 3 | import _ from "lodash"; 4 | import {Socket} from "../../../../../deps/phoenix/web/static/js/phoenix" 5 | 6 | var DevSocket = {}; 7 | var pendingMessages = {}; 8 | var pendingChannels = []; 9 | var connectedComponents = {}; 10 | 11 | var socket = { 12 | dispatch(key, response) { 13 | Dispatcher.dispatch({ 14 | action: key, 15 | data: response 16 | }); 17 | return true; 18 | }, 19 | 20 | connect(jwt) { 21 | if (!DevSocket.connected) { 22 | DevSocket = new Socket("/socket", {params: {jwt: jwt}}) 23 | console.info("[Socket] Connected."); 24 | DevSocket.connected = true; 25 | DevSocket.onMessage((message) => { 26 | if (message.event == "phx_reply") { 27 | if (message.payload.response.event) { 28 | this.dispatch(message.payload.response.event, message.payload.response.payload); 29 | } 30 | } else { 31 | this.dispatch(message.event, message.payload); 32 | } 33 | }) 34 | DevSocket.onClose(() => { 35 | console.info("[Socket] Disconnected."); 36 | this.dispatch("disconnect", null) 37 | }) 38 | DevSocket.onOpen(() => { 39 | _.forEach(pendingChannels, (topic) => { 40 | this.connectChannel(topic) 41 | }) 42 | }) 43 | DevSocket.connect() 44 | } 45 | }, 46 | 47 | disconnect() { 48 | if (DevSocket.connected) { 49 | DevSocket.params = {}; 50 | DevSocket.disconnect(); 51 | DevSocket.connected = false; 52 | } 53 | }, 54 | 55 | connectChannel(topic) { 56 | if (!_.isFinite(connectedComponents[topic])) { 57 | connectedComponents[topic] = 0; 58 | } 59 | if (DevSocket.connected) { 60 | var channel = this.findChannel(topic); 61 | connectedComponents[topic] += 1; 62 | if (!channel || channel.state == "closed") { 63 | channel = DevSocket.channel(topic); 64 | channel.join() 65 | .receive("ok", resp => { 66 | console.info(`[Socket] Joined ${topic}`) 67 | _.forEach(pendingMessages[topic], (message) => { 68 | channel.push(message.key, message.data); 69 | }) 70 | pendingMessages[topic] = null; 71 | }) 72 | .receive("error", resp => { console.info(`[Socket] Unable to join ${topic}`, resp) }) 73 | } 74 | } else { 75 | pendingChannels.push(topic); 76 | } 77 | }, 78 | 79 | findChannel(topic) { 80 | return _.find(DevSocket.channels, (channel) => { 81 | return channel.topic == topic && channel.state == "joined" 82 | }); 83 | }, 84 | 85 | sendMessage(topic, messageKey, data) { 86 | var channel = this.findChannel(topic) 87 | if (channel) { 88 | channel.push(messageKey, data); 89 | } else { 90 | pendingMessages[topic] = pendingMessages[topic] || []; 91 | pendingMessages[topic].push({key: messageKey, data: data}); 92 | } 93 | 94 | }, 95 | 96 | disconnectChannel(topic) { 97 | connectedComponents[topic] -= 1; 98 | if (connectedComponents[topic] == 0) { 99 | var channel = this.findChannel(topic); 100 | if (channel && channel.state != "closed") { 101 | channel.leave(); 102 | console.info(`[Socket] Left ${topic}`); 103 | } 104 | } 105 | } 106 | } 107 | 108 | if (localStorage.getItem('jwt')) { 109 | socket.connect(localStorage.getItem('jwt')) 110 | } 111 | 112 | export default socket; 113 | -------------------------------------------------------------------------------- /web/static/js/common/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export default { 4 | 5 | // User 6 | LOGIN: "login", 7 | LOGIN_PENDING: "login_pending", 8 | REGISTER: "register", 9 | REGISTER_PENDING: "register_pending", 10 | LOGOUT_PENDING: "logout_pending", 11 | LOGOUT: "logout", 12 | DISCONNECT: "disconnect", 13 | 14 | CLEAR_FORM_ERRORS: "clear_form_errors", 15 | 16 | ADD_MESSAGE: "add_message", 17 | REMOVE_MESSAGE: "remove_message", 18 | CLEAR_MESSAGES: "clear_messages", 19 | 20 | // settings 21 | SETTINGS_LOAD: "settings_load", 22 | 23 | // Errors 24 | TIMEOUT: "timeout", 25 | ERROR: "error", 26 | NOT_AUTHORIZED: "not_authorized", 27 | 28 | // Admin 29 | CHANGE_MAIN_TAB_PENDING: "change_main_tab_pending", 30 | 31 | // Accounts 32 | ACCOUNTS_LOADING: "accounts_loading", 33 | ACCOUNTS_LOADED: "accounts_loaded", 34 | 35 | USERS_LOADING: "users_loading", 36 | USERS_LOADED: "users_loaded", 37 | LOADING_SELECTED_USER_DATA: "loading_selected_user_data", 38 | 39 | USER_UPDATING: "user_updating", 40 | USER_UPDATED: "user_updated", 41 | 42 | RESET_USERS: "reset_users", 43 | 44 | ADD_USER: "add_user", 45 | REMOVE_USER: "remove_user", 46 | 47 | DELETE_USERS: "delete_users", 48 | DELETING_USERS: "deleting_users", 49 | NAV_CHANGED: "nav_changed", 50 | CREATED_USER: "created_user", 51 | 52 | NEW_MSG: "new_msg", 53 | }; 54 | -------------------------------------------------------------------------------- /web/static/js/common/dispatcher.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Dispatcher } from 'flux'; 4 | 5 | export default new Dispatcher(); 6 | -------------------------------------------------------------------------------- /web/static/js/common/stores/settings.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Constants from "../../common/constants"; 4 | import Dispatcher from "../../common/dispatcher"; 5 | import StoreCommon from "./store_common"; 6 | import assign from "object-assign"; 7 | import QueryString from '../utils/query_string'; 8 | import socket from '../actions/socket'; 9 | 10 | var _settings = {}; 11 | 12 | function login(jwt) { 13 | if(jwt !== null) { 14 | _settings.jwt = jwt; 15 | localStorage.setItem('jwt', jwt); 16 | socket.connect(jwt); 17 | } else { 18 | _settings.jwt = null; 19 | localStorage.removeItem('jwt'); 20 | socket.disconnect(); 21 | } 22 | } 23 | 24 | function logout(){ 25 | localStorage.removeItem('jwt'); 26 | _settings.jwt = null; 27 | socket.disconnect(); 28 | } 29 | 30 | function loadSettings(defaultSettings){ 31 | 32 | defaultSettings = defaultSettings || {}; 33 | 34 | var bestValue = function(settings_prop, params_prop, default_prop){ 35 | return defaultSettings[settings_prop] || QueryString.params()[params_prop] || default_prop; 36 | }; 37 | 38 | // var jwt = (defaultSettings.jwt && defaultSettings.jwt.length) ? defaultSettings.jwt : null; 39 | // if(jwt!==null) { 40 | // localStorage.setItem('jwt', jwt); 41 | // } else { 42 | // localStorage.removeItem('jwt'); 43 | // } 44 | 45 | _settings = { 46 | apiUrl : defaultSettings.api_url || "/", 47 | csrfToken : defaultSettings.csrfToken || null, 48 | jwt : null 49 | }; 50 | 51 | } 52 | 53 | // Extend Message Store with EventEmitter to add eventing capabilities 54 | var SettingsStore = assign({}, StoreCommon, { 55 | 56 | // Return current messages 57 | current(){ 58 | return _settings; 59 | }, 60 | 61 | jwt(){ 62 | return _settings.jwt || localStorage.getItem('jwt'); 63 | }, 64 | 65 | loggedIn(){ 66 | return !_.isEmpty(_settings.jwt) || !_.isEmpty(localStorage.getItem('jwt')); 67 | }, 68 | 69 | loggedOut(){ 70 | return _settings.jwt == null && localStorage.getItem('jwt') == null; 71 | }, 72 | 73 | }); 74 | 75 | // Register callback with Dispatcher 76 | Dispatcher.register(function(payload) { 77 | 78 | switch(payload.action){ 79 | 80 | // Respond to TIMEOUT action 81 | case Constants.SETTINGS_LOAD: 82 | loadSettings(payload.data); 83 | break; 84 | 85 | // Respond to LOGIN action 86 | case Constants.LOGIN: 87 | login(payload.data.body.user.jwt); 88 | break; 89 | 90 | // Respond to REGISTER action 91 | case Constants.REGISTER: 92 | login(payload.data.body.user.jwt); 93 | break; 94 | 95 | case Constants.LOGOUT: 96 | logout(payload); 97 | break; 98 | 99 | case Constants.DISCONNECT: 100 | logout(payload); 101 | break; 102 | 103 | default: 104 | return true; 105 | } 106 | 107 | // If action was responded to, emit change event 108 | SettingsStore.emitChange(); 109 | 110 | return true; 111 | 112 | }); 113 | 114 | export default SettingsStore; 115 | 116 | -------------------------------------------------------------------------------- /web/static/js/common/stores/store_common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import assign from "object-assign"; 4 | import EventEmitter from "events"; 5 | 6 | const CHANGE_EVENT = 'change'; 7 | 8 | export default assign({}, EventEmitter.prototype, { 9 | 10 | // Emit Change event 11 | emitChange(){ 12 | this.emit(CHANGE_EVENT); 13 | }, 14 | 15 | // Add change listener 16 | addChangeListener(callback){ 17 | this.on(CHANGE_EVENT, callback); 18 | }, 19 | 20 | // Remove change listener 21 | removeChangeListener(callback){ 22 | this.removeListener(CHANGE_EVENT, callback); 23 | } 24 | 25 | }); -------------------------------------------------------------------------------- /web/static/js/common/utils/query_string.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | params(){ 4 | var queryDict = {}; 5 | var vars = window.location.search.substring(1).split('&'); 6 | for (var i = 0; i < vars.length; i++){ 7 | var pair = vars[i].split('='); 8 | queryDict[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); 9 | } 10 | return queryDict; 11 | } 12 | 13 | }; -------------------------------------------------------------------------------- /web/static/js/common/utils/utils.js: -------------------------------------------------------------------------------- 1 | var Utils = { 2 | 3 | currentTime: function(){ 4 | return new Date().getTime(); 5 | }, 6 | 7 | makeId: function(){ 8 | var result, i, j; 9 | result = ''; 10 | for(j=0; j<32; j++) 11 | { 12 | if( j == 8 || j == 12|| j == 16|| j == 20) 13 | result = result + '-'; 14 | i = Math.floor(Math.random()*16).toString(16).toUpperCase(); 15 | result = result + i; 16 | } 17 | return result; 18 | }, 19 | 20 | htmlDecode: function(input){ 21 | var e = document.createElement('div'); 22 | e.innerHTML = input; 23 | return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue; 24 | }, 25 | 26 | htmlDecodeWithRoot: function(input){ 27 | return '' + Utils.htmlDecode(input) + ''; 28 | }, 29 | 30 | getLocation: function(href){ 31 | var l = document.createElement("a"); 32 | l.href = href; 33 | return l; 34 | } 35 | 36 | }; 37 | 38 | export default Utils; -------------------------------------------------------------------------------- /web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hello Phoenix! 11 | "> 12 | 15 | 16 | 17 | 18 | <%= @inner %> 19 | <%= if Mix.env == :dev do %> 20 | 21 | <% else %> 22 | 23 | <% end %> 24 | 25 | 26 | -------------------------------------------------------------------------------- /web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.ErrorView do 2 | use PhoenixReact.Web, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Server internal error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.html", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.LayoutView do 2 | use PhoenixReact.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.PageView do 2 | use PhoenixReact.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixReact.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use PhoenixReact.Web, :controller 9 | use PhoenixReact.Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. 17 | """ 18 | 19 | def model do 20 | quote do 21 | use Ecto.Model 22 | end 23 | end 24 | 25 | def controller do 26 | quote do 27 | use Phoenix.Controller 28 | 29 | alias PhoenixReact.Repo 30 | import Ecto.Model 31 | import Ecto.Query, only: [from: 2] 32 | 33 | import PhoenixReact.Router.Helpers 34 | end 35 | end 36 | 37 | def view do 38 | quote do 39 | use Phoenix.View, root: "web/templates" 40 | 41 | # Import convenience functions from controllers 42 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 43 | 44 | # Use all HTML functionality (forms, tags, etc) 45 | use Phoenix.HTML 46 | 47 | import PhoenixReact.Router.Helpers 48 | end 49 | end 50 | 51 | def router do 52 | quote do 53 | use Phoenix.Router 54 | end 55 | end 56 | 57 | def channel do 58 | quote do 59 | use Phoenix.Channel 60 | 61 | alias PhoenixReact.Repo 62 | import Ecto.Model 63 | import Ecto.Query, only: [from: 2] 64 | 65 | end 66 | end 67 | 68 | @doc """ 69 | When used, dispatch to the appropriate controller/view/etc. 70 | """ 71 | defmacro __using__(which) when is_atom(which) do 72 | apply(__MODULE__, which, []) 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /webpack.hot.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import path from 'path'; 3 | import webpack from 'webpack'; 4 | import WebpackDevServer from 'webpack-dev-server'; 5 | import config from './config/webpack.config'; 6 | 7 | 8 | // #Relay 9 | // import express from 'express'; 10 | // import graphQLHTTP from 'express-graphql'; 11 | // Expose a GraphQL endpoint 12 | // var graphQLServer = express(); 13 | // graphQLServer.use(function(req, res, next) { 14 | // res.header("Access-Control-Allow-Origin", "*"); 15 | // res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 16 | // next(); 17 | // }); 18 | // graphQLServer.use('/', graphQLHTTP({schema: StarWarsSchema, pretty: true})); 19 | // graphQLServer.listen(config.graphql_port, function() { 20 | // console.log("GraphQL Server is now running on http://localhost:" + config.graphql_port) 21 | // }); 22 | 23 | 24 | // Serve the app 25 | var app = new WebpackDevServer(webpack(config), { 26 | contentBase: 'http://localhost:' + config.app_port, 27 | publicPath: config.output.publicPath, 28 | watch: true, 29 | hot: true, 30 | inline: true, 31 | progress: true, 32 | // proxy: {'/graphql': 'http://localhost:' + config.graphql_port}, #Relay 33 | stats: {colors: true}, 34 | headers: { "Access-Control-Allow-Origin": "*" } 35 | }); 36 | 37 | app.listen(config.app_port, function (err, result) { 38 | console.log('Webpack hot load server is now running on: ' + config.app_port); 39 | }); 40 | 41 | // Exit on end of STDIN 42 | process.stdin.resume() 43 | process.stdin.on('end', function () { 44 | process.exit(0) 45 | }) 46 | -------------------------------------------------------------------------------- /webpack.tests.js: -------------------------------------------------------------------------------- 1 | // require all modules ending in ".spec.js" from the 2 | // js directory and all subdirectories 3 | 4 | 5 | // Create a Webpack require context so we can dynamically require our 6 | // project's modules. Exclude test files in this context. 7 | var context = require.context("./web/static/js", true, /\.spec\.js$/); 8 | 9 | // Extract the module ids that Webpack uses to track modules. 10 | var projectModuleIds = context.keys().map(module => 11 | String(context.resolve(module))); 12 | 13 | // See http://kentor.me/posts/testing-react-and-flux-applications-with-karma-and-webpack/ 14 | beforeEach(() => { 15 | // Remove our modules from the require cache before each test case. 16 | projectModuleIds.forEach(id => delete require.cache[id]); 17 | }); 18 | 19 | context.keys().forEach(context); 20 | --------------------------------------------------------------------------------