├── assets ├── css │ ├── phoenix.css │ ├── footer.css │ ├── account.css │ ├── tab.css │ ├── pagination.css │ ├── auth.css │ ├── home.css │ ├── app.css │ ├── chat.css │ ├── raise-modal.css │ ├── rooms.css │ ├── toasts.css │ ├── modal.css │ ├── profile.css │ ├── room.css │ ├── controls.css │ └── header.css ├── static │ └── images │ │ ├── poker.jpg │ │ ├── office.jpg │ │ ├── phoenix.png │ │ ├── spades.png │ │ ├── card-back.svg.png │ │ └── cards │ │ ├── 2_of_spades.svg │ │ ├── 3_of_spades.svg │ │ └── 4_of_spades.svg ├── elm │ ├── src │ │ ├── Data │ │ │ ├── Configuration.elm │ │ │ ├── Notifications │ │ │ │ ├── Delete.elm │ │ │ │ └── Invite.elm │ │ │ ├── WinningHand.elm │ │ │ ├── Facebook.elm │ │ │ ├── Session.elm │ │ │ ├── Chat.elm │ │ │ ├── AuthToken.elm │ │ │ ├── PasswordReset.elm │ │ │ ├── RoomPage.elm │ │ │ ├── Profile.elm │ │ │ ├── Player.elm │ │ │ ├── Room.elm │ │ │ └── Card.elm │ │ ├── Views │ │ │ ├── Helpers.elm │ │ │ ├── Account.elm │ │ │ ├── Form.elm │ │ │ ├── Chat.elm │ │ │ ├── Footer.elm │ │ │ ├── Bank.elm │ │ │ ├── Header.elm │ │ │ └── Actions.elm │ │ ├── Page │ │ │ ├── NotFound.elm │ │ │ ├── Errored.elm │ │ │ ├── Room │ │ │ │ ├── PushMessages.elm │ │ │ │ ├── Helpers.elm │ │ │ │ └── SocketConfig.elm │ │ │ ├── Home.elm │ │ │ ├── Register.elm │ │ │ ├── Login.elm │ │ │ ├── ResetPassword.elm │ │ │ ├── ForgotPassword.elm │ │ │ └── Room.elm │ │ ├── Widgets │ │ │ ├── FacebookLogin.elm │ │ │ ├── Toast.elm │ │ │ ├── Modal.elm │ │ │ ├── Dropdown.elm │ │ │ ├── PlayerToolbar.elm │ │ │ └── Pagination.elm │ │ ├── Ports.elm │ │ ├── Types │ │ │ ├── Room │ │ │ │ ├── ModalState.elm │ │ │ │ └── Messages.elm │ │ │ ├── Dropdowns.elm │ │ │ └── Page.elm │ │ ├── Util.elm │ │ ├── Route.elm │ │ └── Request │ │ │ └── Player.elm │ └── vendor │ │ └── Phoenix │ │ ├── Internal │ │ ├── Message.elm │ │ ├── Helpers.elm │ │ ├── Channel.elm │ │ ├── Presence.elm │ │ └── Socket.elm │ │ ├── Push.elm │ │ ├── Presence.elm │ │ └── Socket.elm ├── package.json ├── elm-package.json ├── brunch-config.js └── js │ ├── socket.js │ └── app.js ├── Procfile ├── test ├── test_helper.exs ├── poker_ex_client_web │ ├── views │ │ ├── page_view_test.exs │ │ ├── layout_view_test.exs │ │ └── error_view_test.exs │ └── controllers │ │ └── page_controller_test.exs └── support │ ├── channel_case.ex │ └── conn_case.ex ├── lib ├── poker_ex_client_web │ ├── templates │ │ ├── page │ │ │ └── index.html.eex │ │ └── layout │ │ │ └── app.html.eex │ ├── views │ │ ├── page_view.ex │ │ ├── layout_view.ex │ │ ├── error_view.ex │ │ └── error_helpers.ex │ ├── controllers │ │ └── page_controller.ex │ ├── router.ex │ ├── gettext.ex │ ├── channels │ │ └── user_socket.ex │ └── endpoint.ex ├── poker_ex_client.ex ├── poker_ex_client │ └── application.ex └── poker_ex_client_web.ex ├── elixir_buildpack.config ├── phoenix_static_buildpack.config ├── config ├── test.exs ├── config.exs ├── dev.exs └── prod.exs ├── priv └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── compile ├── README.md ├── .gitignore ├── mix.exs └── mix.lock /assets/css/phoenix.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: MIX_ENV=prod mix phx.server -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | -------------------------------------------------------------------------------- /lib/poker_ex_client_web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /assets/static/images/poker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zkayser/pokerex_client/HEAD/assets/static/images/poker.jpg -------------------------------------------------------------------------------- /elixir_buildpack.config: -------------------------------------------------------------------------------- 1 | erlang_version=20.0 2 | elixir_version=1.6.0 3 | always_rebuild=false 4 | runtime_path=/app -------------------------------------------------------------------------------- /assets/static/images/office.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zkayser/pokerex_client/HEAD/assets/static/images/office.jpg -------------------------------------------------------------------------------- /assets/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zkayser/pokerex_client/HEAD/assets/static/images/phoenix.png -------------------------------------------------------------------------------- /assets/static/images/spades.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zkayser/pokerex_client/HEAD/assets/static/images/spades.png -------------------------------------------------------------------------------- /lib/poker_ex_client_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.PageView do 2 | use PokerExClientWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /assets/css/footer.css: -------------------------------------------------------------------------------- 1 | .bottom { 2 | position: relative; 3 | bottom: -15vh; 4 | } 5 | 6 | .footer-row { 7 | margin-bottom: 0; 8 | } -------------------------------------------------------------------------------- /lib/poker_ex_client_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.LayoutView do 2 | use PokerExClientWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /assets/static/images/card-back.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zkayser/pokerex_client/HEAD/assets/static/images/card-back.svg.png -------------------------------------------------------------------------------- /test/poker_ex_client_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.PageViewTest do 2 | use PokerExClientWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/poker_ex_client_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.LayoutViewTest do 2 | use PokerExClientWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /assets/elm/src/Data/Configuration.elm: -------------------------------------------------------------------------------- 1 | module Data.Configuration exposing (..) 2 | 3 | 4 | type alias Configuration = 5 | { socketUrl : String 6 | , apiUrl : String 7 | } 8 | -------------------------------------------------------------------------------- /phoenix_static_buildpack.config: -------------------------------------------------------------------------------- 1 | clean_cache=false 2 | compile="compile" 3 | node_version=9.0.0 4 | phoenix_relative_path=. 5 | remove_node=false 6 | assets_path=./assets 7 | phoenix_ex=phx -------------------------------------------------------------------------------- /assets/css/account.css: -------------------------------------------------------------------------------- 1 | .account-info { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: center; 5 | align-items: center; 6 | } 7 | 8 | .account-info-item { 9 | flex: 1; 10 | } -------------------------------------------------------------------------------- /lib/poker_ex_client_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.PageController do 2 | use PokerExClientWeb, :controller 3 | 4 | def index(conn, _params) do 5 | render conn, "index.html" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/poker_ex_client_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.PageControllerTest do 2 | use PokerExClientWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get conn, "/" 6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /assets/elm/src/Views/Helpers.elm: -------------------------------------------------------------------------------- 1 | module Views.Helpers exposing (ActivePage(..)) 2 | 3 | {- 4 | Determines which navbar link will be rendered as active 5 | -} 6 | 7 | 8 | type ActivePage 9 | = Other 10 | | Login 11 | | Registration 12 | | Home 13 | | Room 14 | | Rooms 15 | | Profile 16 | -------------------------------------------------------------------------------- /lib/poker_ex_client.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClient do 2 | @moduledoc """ 3 | PokerExClient keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /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 :poker_ex_client, PokerExClientWeb.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | -------------------------------------------------------------------------------- /assets/css/tab.css: -------------------------------------------------------------------------------- 1 | .tab-holder { 2 | z-index: 100; 3 | box-shadow: 0.05rem 0.05rem 0.05rem 0.05rem black; 4 | } 5 | 6 | a.active.active-tab { 7 | font-weight: 600; 8 | text-decoration: underline; 9 | transform: scale(1.1); 10 | transition: transform 0.5s ease-in-out; 11 | } 12 | 13 | .tabs .tab a { 14 | cursor: pointer; 15 | } 16 | 17 | .row.tab-holder-row { 18 | margin-bottom: 0; 19 | } -------------------------------------------------------------------------------- /assets/elm/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Data.Session as Session exposing (Session) 4 | import Html exposing (Html, div, h1, main_, text) 5 | import Html.Attributes exposing (class, id, src, tabindex) 6 | 7 | 8 | view : Session -> Html msg 9 | view session = 10 | main_ [ id "content", class "container", tabindex -1 ] 11 | [ h1 [] [ text "Not Found" ] ] 12 | -------------------------------------------------------------------------------- /assets/elm/src/Widgets/FacebookLogin.elm: -------------------------------------------------------------------------------- 1 | module Widgets.FacebookLogin exposing (viewFBLogin) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (class) 5 | import Html.Events exposing (onClick) 6 | 7 | 8 | viewFBLogin : msg -> Html msg 9 | viewFBLogin msg = 10 | button [ class "btn auth-btn waves-effect blue darken-4 white-text", onClick msg ] 11 | [ text "Login with Facebook" ] 12 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | -------------------------------------------------------------------------------- /assets/elm/src/Data/Notifications/Delete.elm: -------------------------------------------------------------------------------- 1 | module Data.Notifications.Delete exposing (..) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Json.Decode.Pipeline exposing (decode, required) 5 | 6 | 7 | type alias Delete = 8 | { title : String 9 | , owner : String 10 | } 11 | 12 | 13 | decoder : Decoder Delete 14 | decoder = 15 | decode Delete 16 | |> required "title" Decode.string 17 | |> required "owner" Decode.string 18 | -------------------------------------------------------------------------------- /assets/elm/src/Data/Notifications/Invite.elm: -------------------------------------------------------------------------------- 1 | module Data.Notifications.Invite exposing (..) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Json.Decode.Pipeline exposing (decode, required) 5 | 6 | 7 | type alias Invite = 8 | { title : String 9 | , owner : String 10 | } 11 | 12 | 13 | decoder : Decoder Invite 14 | decoder = 15 | decode Invite 16 | |> required "title" Decode.string 17 | |> required "owner" Decode.string 18 | -------------------------------------------------------------------------------- /assets/elm/src/Ports.elm: -------------------------------------------------------------------------------- 1 | port module Ports exposing (..) 2 | 3 | import Json.Encode exposing (Value) 4 | 5 | 6 | port storeSession : Maybe String -> Cmd msg 7 | 8 | 9 | port logout : () -> Cmd msg 10 | 11 | 12 | port triggerFBInviteRequest : () -> Cmd msg 13 | 14 | 15 | port scrollChatToTop : () -> Cmd msg 16 | 17 | 18 | port onSessionChange : (Value -> msg) -> Sub msg 19 | 20 | 21 | port loginWithFB : () -> Cmd msg 22 | 23 | 24 | port onFBLogin : (Value -> msg) -> Sub msg 25 | -------------------------------------------------------------------------------- /lib/poker_ex_client_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.ErrorView do 2 | use PokerExClientWeb, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Internal server 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 | -------------------------------------------------------------------------------- /assets/elm/src/Types/Room/ModalState.elm: -------------------------------------------------------------------------------- 1 | module Types.Room.ModalState exposing (BottomModalType(..), ModalState(..)) 2 | 3 | import Data.WinningHand as WinningHand exposing (WinningHand) 4 | 5 | 6 | type ModalState 7 | = Closed 8 | | JoinModalOpen 9 | | BankModalOpen 10 | | BottomModalOpen BottomModalType 11 | | RaiseModalOpen 12 | | WinningHandModal WinningHand 13 | 14 | 15 | type BottomModalType 16 | = Actions 17 | | Account 18 | | Chat 19 | | MobileMenu 20 | -------------------------------------------------------------------------------- /assets/css/pagination.css: -------------------------------------------------------------------------------- 1 | .pagination-list { 2 | display: flex; 3 | justify-content: space-evenly; 4 | } 5 | 6 | .pagination-list > * { 7 | padding: 1rem; 8 | font-size: 1.5rem; 9 | font-weight: 600; 10 | cursor: pointer; 11 | } 12 | 13 | .pagination li { 14 | height: inherit; 15 | } 16 | 17 | .pagination-list-item { 18 | color: var(--teal); 19 | } 20 | 21 | .active-page { 22 | background-color: var(--light-blue); 23 | color: white; 24 | } 25 | 26 | .pagination li.disabled-page-icon a i { 27 | color: lightgray; 28 | } -------------------------------------------------------------------------------- /assets/css/auth.css: -------------------------------------------------------------------------------- 1 | .auth-page { 2 | display: -webkit-box; 3 | display: -ms-flexbox; 4 | display: flex; 5 | -webkit-box-align: center; 6 | -ms-flex-align: center; 7 | align-items: center; 8 | -webkit-box-pack: center; 9 | -ms-flex-pack: center; 10 | justify-content: center; 11 | margin-top: 8em; 12 | } 13 | 14 | .auth-form { 15 | padding: 3em; 16 | max-width: 40em; 17 | margin-bottom: 8em; 18 | } 19 | 20 | .auth-btn { 21 | margin: 1rem; 22 | } 23 | 24 | .forgot-password-link { 25 | cursor: pointer; 26 | } -------------------------------------------------------------------------------- /assets/elm/src/Types/Dropdowns.elm: -------------------------------------------------------------------------------- 1 | module Types.Dropdowns exposing (..) 2 | 3 | 4 | type OpenDropdown 5 | = AllClosed 6 | | NavBarDropdown 7 | 8 | 9 | type DropdownMsg 10 | = Toggle OpenDropdown 11 | | NavItemPicked DropdownNavbarLink 12 | | Blur 13 | 14 | 15 | type DropdownItem 16 | = None 17 | | AnyItem String 18 | 19 | 20 | type DropdownNavbarLink 21 | = Logout 22 | | Login 23 | | Register 24 | | Room -- TODO: Change this back to `Rooms` when the route is ready to go. 25 | | Rooms 26 | | Profile 27 | -------------------------------------------------------------------------------- /compile: -------------------------------------------------------------------------------- 1 | echo "Running elm make..." 2 | 3 | elm make elm/src/Main.elm --output js/main.js --yes 4 | 5 | echo "Now building static assets..." 6 | echo "PWD: " 7 | echo `pwd` 8 | 9 | 10 | brunch build --production --debug 11 | 12 | echo "Brunch build completed" 13 | 14 | echo "Changing directories to phoenix_dir:" 15 | echo $phoenix_dir 16 | echo "Running mix phx.digest..." 17 | 18 | cd $phoenix_dir 19 | 20 | mix "${phoenix_ex}.digest" 21 | 22 | if mix help "${phoenix_ex}.digest.clean" 1>/dev/null 2>&1; then 23 | mix "${phoenix_ex}.digest.clean" 24 | fi -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "license": "MIT", 4 | "scripts": { 5 | "deploy": "brunch build --production", 6 | "watch": "brunch watch --stdin" 7 | }, 8 | "dependencies": { 9 | "phoenix": "file:../deps/phoenix", 10 | "phoenix_html": "file:../deps/phoenix_html", 11 | "elm": "0.18" 12 | }, 13 | "devDependencies": { 14 | "babel-brunch": "6.1.1", 15 | "brunch": "2.10.9", 16 | "clean-css-brunch": "2.10.0", 17 | "elm-brunch": "^0.10.0", 18 | "gulp": "^3.9.1", 19 | "uglify-js-brunch": "2.10.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /assets/elm/src/Data/WinningHand.elm: -------------------------------------------------------------------------------- 1 | module Data.WinningHand exposing (..) 2 | 3 | import Data.Card as Card exposing (Card) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Decode.Pipeline exposing (decode, required) 6 | 7 | 8 | type alias WinningHand = 9 | { cards : List Card 10 | , winner : String 11 | , handType : String 12 | } 13 | 14 | 15 | decoder : Decoder WinningHand 16 | decoder = 17 | decode WinningHand 18 | |> required "cards" (Decode.list Card.decoder) 19 | |> required "winner" Decode.string 20 | |> required "type" Decode.string 21 | -------------------------------------------------------------------------------- /assets/elm/src/Data/Facebook.elm: -------------------------------------------------------------------------------- 1 | module Data.Facebook exposing (..) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Json.Decode.Pipeline exposing (decode, required) 5 | import Json.Encode as Encode 6 | 7 | 8 | type alias FBData = 9 | { name : String, id : String } 10 | 11 | 12 | decoder : Decoder FBData 13 | decoder = 14 | decode FBData 15 | |> required "name" Decode.string 16 | |> required "id" Decode.string 17 | 18 | 19 | encode : FBData -> Encode.Value 20 | encode data = 21 | Encode.object [ ( "name", Encode.string data.name ), ( "facebook_id", Encode.string data.id ) ] 22 | -------------------------------------------------------------------------------- /assets/elm/src/Data/Session.elm: -------------------------------------------------------------------------------- 1 | module Data.Session exposing (Session, attempt) 2 | 3 | import Data.AuthToken exposing (AuthToken) 4 | import Data.Player as Player exposing (Player) 5 | 6 | 7 | type alias Session = 8 | { player : Maybe Player } 9 | 10 | 11 | attempt : String -> (AuthToken -> Cmd msg) -> Session -> ( List String, Cmd msg ) 12 | attempt attemptedAction toCmd session = 13 | case Maybe.map .token session.player of 14 | Nothing -> 15 | ( [ "You have been signed out. Please sign back in to " ++ attemptedAction ++ "." ], Cmd.none ) 16 | 17 | Just token -> 18 | ( [], toCmd token ) 19 | -------------------------------------------------------------------------------- /assets/elm/src/Types/Page.elm: -------------------------------------------------------------------------------- 1 | module Types.Page exposing (..) 2 | 3 | import Page.ForgotPassword as ForgotPassword 4 | import Page.Home as Home 5 | import Page.Login as Login 6 | import Page.Profile as Profile 7 | import Page.Register as Register 8 | import Page.ResetPassword as ResetPassword 9 | import Page.Room as Room 10 | import Page.Rooms as Rooms 11 | 12 | 13 | type Page 14 | = Blank 15 | | NotFound 16 | | Login Login.Model 17 | | Register Register.Model 18 | | Home Home.Model 19 | | Rooms Rooms.Model 20 | | Room Room.Model 21 | | Profile Profile.Model 22 | | ForgotPassword ForgotPassword.Model 23 | | ResetPassword ResetPassword.Model 24 | -------------------------------------------------------------------------------- /lib/poker_ex_client_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.Router do 2 | use PokerExClientWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | end 11 | 12 | pipeline :api do 13 | plug :accepts, ["json"] 14 | end 15 | 16 | scope "/", PokerExClientWeb do 17 | pipe_through :browser # Use the default browser stack 18 | 19 | get "/*path", PageController, :index 20 | end 21 | 22 | # Other scopes may use custom stacks. 23 | # scope "/api", PokerExClientWeb do 24 | # pipe_through :api 25 | # end 26 | end 27 | -------------------------------------------------------------------------------- /test/poker_ex_client_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.ErrorViewTest do 2 | use PokerExClientWeb.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(PokerExClientWeb.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(PokerExClientWeb.ErrorView, "500.html", []) == 14 | "Internal server error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(PokerExClientWeb.ErrorView, "505.html", []) == 19 | "Internal server error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /assets/css/home.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | background: url("/images/poker.jpg") no-repeat center center fixed; 3 | min-height: 45vh; 4 | -webkit-box-pack: center; 5 | -ms-flex-pack: center; 6 | justify-content: center; 7 | -webkit-box-orient: vertical; 8 | -webkit-box-direction: normal; 9 | -ms-flex-direction: column; 10 | flex-direction: column; 11 | } 12 | 13 | .welcome { 14 | color: #84ffff; 15 | text-shadow: 2px 2px 2em #004d40; 16 | } 17 | 18 | @media (max-width: 993px) { 19 | .landing-item:first-of-type { 20 | margin-top: 1em; 21 | } 22 | } 23 | 24 | @media (min-width: 993px) { 25 | .landing { 26 | height: 16rem; 27 | } 28 | 29 | .landing-item { 30 | margin-top: 0.5em; 31 | height: 100%; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PokerExClient 2 | 3 | To start your Phoenix server: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Install Node.js dependencies with `cd assets && npm install` 7 | * Start Phoenix endpoint with `mix phx.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: https://hexdocs.pm/phoenix 18 | * Mailing list: http://groups.google.com/group/phoenix-talk 19 | * Source: https://github.com/phoenixframework/phoenix 20 | -------------------------------------------------------------------------------- /assets/elm/src/Data/Chat.elm: -------------------------------------------------------------------------------- 1 | module Data.Chat exposing (..) 2 | 3 | import Data.Player exposing (Player, encodeUsername) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Decode.Pipeline as Pipeline exposing (decode, required) 6 | import Json.Encode as Encode exposing (Value) 7 | 8 | 9 | type alias Chat = 10 | { playerName : String 11 | , message : String 12 | } 13 | 14 | 15 | decoder : Decoder Chat 16 | decoder = 17 | decode Chat 18 | |> required "player" Decode.string 19 | |> required "message" Decode.string 20 | 21 | 22 | encode : Player -> String -> Value 23 | encode player message = 24 | Encode.object 25 | [ ( "player", encodeUsername player.username ) 26 | , ( "message", Encode.string message ) 27 | ] 28 | -------------------------------------------------------------------------------- /lib/poker_ex_client_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import PokerExClientWeb.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :poker_ex_client 24 | end 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Generated on crash by NPM 11 | npm-debug.log 12 | 13 | # Static artifacts 14 | /node_modules 15 | /package-lock.json 16 | /assets/node_modules 17 | /assets/elm/elm-stuff 18 | /assets/elm-stuff 19 | /assets/js/main.js 20 | /elm-stuff 21 | # Since we are building assets from assets/, 22 | # we ignore priv/static. You may want to comment 23 | # this depending on your deployment strategy. 24 | /priv/static/ 25 | 26 | # Files matching config/*.secret.exs pattern contain sensitive 27 | # data and you should not commit them into version control. 28 | # 29 | # Alternatively, you may comment the line below and commit the 30 | # secrets files as long as you replace their contents by environment 31 | # variables. 32 | /config/*.secret.exs 33 | -------------------------------------------------------------------------------- /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 :poker_ex_client, PokerExClientWeb.Endpoint, 10 | url: [host: "localhost"], 11 | secret_key_base: System.get_env("SECRET_KEY_BASE"), 12 | render_errors: [view: PokerExClientWeb.ErrorView, accepts: ~w(html json)], 13 | pubsub: [name: PokerExClient.PubSub, 14 | adapter: Phoenix.PubSub.PG2] 15 | 16 | # Configures Elixir's Logger 17 | config :logger, :console, 18 | format: "$time $metadata[$level] $message\n", 19 | metadata: [:request_id] 20 | 21 | # Import environment specific config. This must remain at the bottom 22 | # of this file so it overrides the configuration defined above. 23 | import_config "#{Mix.env}.exs" 24 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.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 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 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 | # The default endpoint for testing 24 | @endpoint PokerExClientWeb.Endpoint 25 | end 26 | end 27 | 28 | 29 | setup _tags do 30 | :ok 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /assets/elm/src/Page/Errored.elm: -------------------------------------------------------------------------------- 1 | module Page.Errored exposing (..) 2 | 3 | import Data.Session as Session exposing (Session) 4 | import Html exposing (Html, div, h1, main_, p, text) 5 | import Html.Attributes exposing (class, id) 6 | import Views.Helpers as Helpers exposing (ActivePage) 7 | 8 | 9 | type PageLoadError 10 | = PageLoadError Model 11 | 12 | 13 | type alias Model = 14 | { activePage : ActivePage 15 | , errorMessage : String 16 | } 17 | 18 | 19 | pageLoadError : ActivePage -> String -> PageLoadError 20 | pageLoadError activePage errorMessage = 21 | PageLoadError { activePage = activePage, errorMessage = errorMessage } 22 | 23 | 24 | view : Session -> PageLoadError -> Html msg 25 | view session (PageLoadError model) = 26 | main_ [ id "content", class "container" ] 27 | [ h1 [] [ text "Error Loading Page" ] 28 | , div [ class "row" ] 29 | [ p [] [ text model.errorMessage ] ] 30 | ] 31 | -------------------------------------------------------------------------------- /assets/elm/src/Data/AuthToken.elm: -------------------------------------------------------------------------------- 1 | module Data.AuthToken exposing (..) 2 | 3 | import HttpBuilder exposing (RequestBuilder, withHeader) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | 7 | 8 | type AuthToken 9 | = AuthToken String 10 | 11 | 12 | encode : AuthToken -> Value 13 | encode (AuthToken token) = 14 | Encode.string token 15 | 16 | 17 | decoder : Decoder AuthToken 18 | decoder = 19 | Decode.string 20 | |> Decode.map AuthToken 21 | 22 | 23 | withAuthorization : Maybe AuthToken -> RequestBuilder a -> RequestBuilder a 24 | withAuthorization maybeToken builder = 25 | case maybeToken of 26 | Just (AuthToken token) -> 27 | builder 28 | |> withHeader "Bearer" ("Token" ++ token) 29 | 30 | Nothing -> 31 | builder 32 | 33 | 34 | authTokenToString : AuthToken -> String 35 | authTokenToString (AuthToken token) = 36 | token 37 | -------------------------------------------------------------------------------- /assets/elm/src/Data/PasswordReset.elm: -------------------------------------------------------------------------------- 1 | module Data.PasswordReset exposing (..) 2 | 3 | import Data.Player as Player exposing (Player) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Decode.Pipeline exposing (decode, required) 6 | 7 | 8 | type alias PasswordReset = 9 | { message : String, type_ : RequestResult } 10 | 11 | 12 | type RequestResult 13 | = Success 14 | | Error 15 | 16 | 17 | decoder : Decoder PasswordReset 18 | decoder = 19 | decode PasswordReset 20 | |> required "message" Decode.string 21 | |> required "type" resultDecoder 22 | 23 | 24 | resultDecoder : Decoder RequestResult 25 | resultDecoder = 26 | Decode.string 27 | |> Decode.andThen (\str -> Decode.succeed (stringToResult str)) 28 | 29 | 30 | stringToResult : String -> RequestResult 31 | stringToResult string = 32 | case string of 33 | "success" -> 34 | Success 35 | 36 | _ -> 37 | Error 38 | -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application css. */ 2 | :root { 3 | --teal: #009688; 4 | --bright-red: rgb(244, 67, 54); 5 | --green-accent: #00c853; 6 | --mobile: 600px; 7 | --light-blue: #039be5; 8 | --material-pink: #ee6e73; 9 | } 10 | 11 | body { 12 | min-height: 100vh; 13 | font-family: Helvetica, Arial, sans-serif; 14 | } 15 | 16 | @media (max-width: 600px) { 17 | body { 18 | width: 100vw; 19 | } 20 | } 21 | 22 | main { 23 | min-height: inherit; 24 | } 25 | 26 | .rounded { 27 | border-radius: 1.5em; 28 | } 29 | 30 | .large-text { 31 | font-size: xx-large; 32 | } 33 | 34 | .medium-text { 35 | font-size: 1.05rem; 36 | } 37 | 38 | .pop-text { 39 | font-size: 1.25rem; 40 | } 41 | 42 | .center-align { 43 | text-align: center; 44 | } 45 | 46 | @media (max-width: 560px) { 47 | .center-align-small { 48 | text-align: center; 49 | } 50 | 51 | .pad-left-small { 52 | padding-left: 10em; 53 | } 54 | 55 | .margin-bottom-small { 56 | margin-bottom: 2em; 57 | } 58 | } -------------------------------------------------------------------------------- /assets/elm/src/Views/Account.elm: -------------------------------------------------------------------------------- 1 | module Views.Account exposing (view) 2 | 3 | import Data.Player as Player exposing (Player) 4 | import Html as Html exposing (..) 5 | import Html.Attributes as Attrs exposing (..) 6 | import Html.Events as Events exposing (onClick) 7 | import Route 8 | 9 | 10 | view : Player -> Html msg 11 | view player = 12 | div [ class "account-container" ] 13 | [ span [ class "modal-header red-text" ] [ text "Your Account" ] 14 | , div [ class "row account-info" ] 15 | [ span [ class "account-info-item" ] [ h5 [] [ text <| "Username: " ++ Player.usernameToString player.username ] ] 16 | , span [ class "account-info-item" ] [ h5 [] [ text <| "Chips: " ++ toString player.chips ] ] 17 | ] 18 | , div [ class "row account-info" ] 19 | [ span [ class "account-info-item" ] [ h5 [] [ text <| "Email: " ++ player.email ] ] 20 | , span [ class "account-info-item" ] [ a [ Route.href Route.Home ] [ text "Go to your account" ] ] 21 | ] 22 | ] 23 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.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 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 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 | import PokerExClientWeb.Router.Helpers 23 | 24 | # The default endpoint for testing 25 | @endpoint PokerExClientWeb.Endpoint 26 | end 27 | end 28 | 29 | 30 | setup _tags do 31 | {:ok, conn: Phoenix.ConnTest.build_conn()} 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /assets/elm/src/Util.elm: -------------------------------------------------------------------------------- 1 | module Util exposing ((=>), appendErrors, onClickStopPropagation, pair, viewIf) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Html.Events exposing (Options, defaultOptions, onWithOptions) 5 | import Json.Decode as Decode 6 | 7 | 8 | (=>) : a -> b -> ( a, b ) 9 | (=>) = 10 | (,) 11 | infixl 0 => 12 | 13 | 14 | pair : a -> b -> ( a, b ) 15 | pair first second = 16 | first => second 17 | 18 | 19 | viewIf : Bool -> Html msg -> Html msg 20 | viewIf condition content = 21 | if condition then 22 | content 23 | else 24 | Html.text "" 25 | 26 | 27 | onClickStopPropagation : msg -> Attribute msg 28 | onClickStopPropagation msg = 29 | onWithOptions "click" noBubble (Decode.succeed msg) 30 | 31 | 32 | noBubble : Options 33 | noBubble = 34 | { stopPropagation = True 35 | , preventDefault = True 36 | } 37 | 38 | 39 | appendErrors : { model | errors : List error } -> List error -> { model | errors : List error } 40 | appendErrors model errors = 41 | { model | errors = model.errors ++ errors } 42 | -------------------------------------------------------------------------------- /assets/elm/src/Data/RoomPage.elm: -------------------------------------------------------------------------------- 1 | module Data.RoomPage exposing (RoomPage) 2 | 3 | import Data.Player as Player exposing (Player) 4 | import Data.Room as Room exposing (Room) 5 | import Phoenix.Channel as Channel exposing (Channel) 6 | import Types.Room.Messages as Messages exposing (RoomMsg) 7 | import Types.Room.ModalState as ModalState exposing (ModalState) 8 | import Views.Chat as Chat exposing (Chat) 9 | 10 | 11 | type alias RoomPage = 12 | { room : String 13 | , roomModel : Room 14 | , roomType : String 15 | , roomMessages : List String 16 | , players : List Player 17 | , player : Player 18 | , joinValue : String 19 | , joined : Bool 20 | , channelSubscriptions : List (Channel RoomMsg) 21 | , modalRendered : ModalState 22 | , errorMessages : List String 23 | , raiseAmount : Int 24 | , raiseInterval : Int 25 | , chipsAvailable : Int 26 | , chipsAvailableForJoin : Int 27 | , addAmount : Int 28 | , chat : Chat 29 | , currentChatMsg : String 30 | , socketUrl : String 31 | , apiUrl : String 32 | } 33 | -------------------------------------------------------------------------------- /lib/poker_ex_client/application.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClient.Application do 2 | use Application 3 | 4 | # See https://hexdocs.pm/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Start the endpoint when the application starts 12 | supervisor(PokerExClientWeb.Endpoint, []), 13 | # Start your own worker by calling: PokerExClient.Worker.start_link(arg1, arg2, arg3) 14 | # worker(PokerExClient.Worker, [arg1, arg2, arg3]), 15 | ] 16 | 17 | # See https://hexdocs.pm/elixir/Supervisor.html 18 | # for other strategies and supported options 19 | opts = [strategy: :one_for_one, name: PokerExClient.Supervisor] 20 | Supervisor.start_link(children, opts) 21 | end 22 | 23 | # Tell Phoenix to update the endpoint configuration 24 | # whenever the application is updated. 25 | def config_change(changed, _new, removed) do 26 | PokerExClientWeb.Endpoint.config_change(changed, removed) 27 | :ok 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /assets/css/chat.css: -------------------------------------------------------------------------------- 1 | .message-container { 2 | display: flex; 3 | justify-content: baseline; 4 | align-items: center; 5 | font-size: 1.25rem; 6 | } 7 | 8 | .chat-container { 9 | height: 30vh; 10 | overflow-y: scroll; 11 | } 12 | 13 | .chat-player-name { 14 | flex: 1; 15 | font-weight: 600; 16 | } 17 | 18 | .chat-message-item { 19 | text-align: left; 20 | flex: 6; 21 | } 22 | 23 | .chat-input-container { 24 | display: flex; 25 | justify-content: baseline; 26 | align-items: center; 27 | padding-right: 4rem; 28 | padding-left: 4rem; 29 | } 30 | 31 | .chat-input { 32 | flex: 3; 33 | } 34 | 35 | .chat-submit-btn { 36 | flex: 0.2; 37 | transform: translateY(-1rem); 38 | } 39 | 40 | .chat-submit-mobile { 41 | display: none; 42 | } 43 | 44 | .close-chat { 45 | position: absolute; 46 | top: 5px; 47 | right: 5px; 48 | } 49 | 50 | @media (max-width: 600px) { 51 | .chat-input-container { 52 | padding-right: 2rem; 53 | } 54 | 55 | .chat-container { 56 | height: 25vh; 57 | } 58 | 59 | .chat-submit-btn { 60 | display: none; 61 | } 62 | 63 | .chat-submit-mobile { 64 | display: initial; 65 | flex: 0.5; 66 | margin-left: 1rem; 67 | translateY: (-1rem); 68 | } 69 | } -------------------------------------------------------------------------------- /assets/css/raise-modal.css: -------------------------------------------------------------------------------- 1 | .raise-buttons-container { 2 | display: -webkit-box; 3 | display: -ms-flexbox; 4 | display: flex; 5 | -webkit-box-orient: horizontal; 6 | -webkit-box-direction: normal; 7 | -ms-flex-direction: row; 8 | flex-direction: row; 9 | -webkit-box-pack: justify; 10 | -ms-flex-pack: justify; 11 | justify-content: space-between; 12 | margin-top: 1rem; 13 | } 14 | 15 | .raise-buttons-container:nth-child(1) { 16 | -webkit-box-flex: 1; 17 | -ms-flex: 1; 18 | flex: 1; 19 | } 20 | 21 | .raise-buttons-container:nth-child(2) { 22 | -webkit-box-flex: 1; 23 | -ms-flex: 1; 24 | flex: 1; 25 | } 26 | 27 | .raise-buttons-container:nth-child(3) { 28 | -webkit-box-flex: 1; 29 | -ms-flex: 1; 30 | flex: 1; 31 | } 32 | 33 | .raise-modal-header { 34 | text-align: center; 35 | } 36 | 37 | .raise-modal-close-row { 38 | display: -webkit-box; 39 | display: -ms-flexbox; 40 | display: flex; 41 | -webkit-box-pack: end; 42 | -ms-flex-pack: end; 43 | justify-content: flex-end; 44 | margin-top: 5rem; 45 | } 46 | 47 | @media (max-width: 400px) { 48 | .raise-modal-close-row { 49 | margin-top: 3rem; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/poker_ex_client_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | > 10 | > 11 | > 12 | 13 | PokerEx 14 | "> 15 | 16 | "> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PokerExClient.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :poker_ex_client, 7 | version: "0.5.0", 8 | elixir: "~> 1.4", 9 | elixirc_paths: elixirc_paths(Mix.env), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 11 | start_permanent: Mix.env == :prod, 12 | deps: deps() 13 | ] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [ 21 | mod: {PokerExClient.Application, []}, 22 | extra_applications: [:logger, :runtime_tools] 23 | ] 24 | end 25 | 26 | # Specifies which paths to compile per environment. 27 | defp elixirc_paths(:test), do: ["lib", "test/support"] 28 | defp elixirc_paths(_), do: ["lib"] 29 | 30 | # Specifies your project dependencies. 31 | # 32 | # Type `mix help deps` for examples and options. 33 | defp deps do 34 | [ 35 | {:phoenix, "~> 1.3.0"}, 36 | {:phoenix_pubsub, "~> 1.0"}, 37 | {:phoenix_html, "~> 2.10"}, 38 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 39 | {:gettext, "~> 0.11"}, 40 | {:cowboy, "~> 1.0"} 41 | ] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /assets/css/rooms.css: -------------------------------------------------------------------------------- 1 | .lobby-container { 2 | padding: 4rem; 3 | } 4 | 5 | @media (max-width: 600px) { 6 | .lobby-container { 7 | padding: 0; 8 | } 9 | } 10 | 11 | .lobby-title { 12 | text-align: center; 13 | } 14 | 15 | .rooms-list { 16 | width: 100%; 17 | } 18 | 19 | .room-list-title { 20 | font-size: 2rem; 21 | text-transform: capitalize; 22 | font-weight: 400; 23 | flex: 1.5; 24 | margin: 0.75rem; 25 | display: flex; 26 | align-items: center; 27 | padding-left: 2rem; 28 | } 29 | 30 | @media (max-width: 600px) { 31 | .room-list-title { 32 | font-size: 1.25rem; 33 | font-weight: 600; 34 | margin: 0 0.75rem 0 0; 35 | padding-left: 0.75rem; 36 | } 37 | } 38 | 39 | .room-list-status { 40 | font-size: 1.25rem; 41 | margin: 0.75rem; 42 | flex: 1.5; 43 | } 44 | 45 | .room-list-status:nth-child { 46 | padding: 0.75rem; 47 | } 48 | 49 | @media (max-width: 600px) { 50 | .room-list-status { 51 | font-size: 1rem; 52 | } 53 | } 54 | 55 | .room-info-item { 56 | display: flex; 57 | justify-content: space-between; 58 | margin: 0.75rem 2rem 0.75rem 2rem; 59 | } 60 | 61 | .room-info-item .room-list-title { 62 | margin: 0.75rem 0 0.75rem 2rem; 63 | } 64 | 65 | .room-info-item .status { 66 | margin: 0.75rem 2rem 0.75rem 0; 67 | } -------------------------------------------------------------------------------- /assets/elm/src/Data/Profile.elm: -------------------------------------------------------------------------------- 1 | module Data.Profile exposing (..) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Json.Decode.Pipeline exposing (decode, hardcoded, optional, required) 5 | import Json.Encode as Encode 6 | 7 | 8 | type alias Profile = 9 | { errors : List Error 10 | , id : Int 11 | , username : String 12 | , email : String 13 | , chips : Int 14 | , blurb : String 15 | , isNewProfile : Bool 16 | } 17 | 18 | 19 | type alias Error = 20 | ( Field, String ) 21 | 22 | 23 | type Field 24 | = Chips 25 | | E_mail 26 | | Blurb 27 | | Server 28 | 29 | 30 | initialProfile : Profile 31 | initialProfile = 32 | { errors = [] 33 | , id = 0 34 | , username = "" 35 | , email = "" 36 | , chips = 1000 37 | , blurb = "" 38 | , isNewProfile = True 39 | } 40 | 41 | 42 | decoder : Decoder Profile 43 | decoder = 44 | decode Profile 45 | |> hardcoded [] 46 | -- Come back to this later 47 | |> required "id" Decode.int 48 | |> required "name" Decode.string 49 | |> required "email" Decode.string 50 | |> required "chips" Decode.int 51 | |> required "blurb" Decode.string 52 | |> hardcoded False 53 | -------------------------------------------------------------------------------- /assets/css/toasts.css: -------------------------------------------------------------------------------- 1 | .messages-container { 2 | position: fixed; 3 | z-index: 100; 4 | min-width: 25%; 5 | right: 0%; 6 | top: 15%; 7 | display: -webkit-box; 8 | display: -ms-flexbox; 9 | display: flex; 10 | -webkit-box-orient: vertical; 11 | -webkit-box-direction: normal; 12 | -ms-flex-direction: column; 13 | flex-direction: column; 14 | -webkit-box-align: baseline; 15 | -ms-flex-align: baseline; 16 | align-items: baseline; 17 | } 18 | 19 | .message { 20 | color: white; 21 | font-size: 1.25rem; 22 | margin: 0.5rem 0; 23 | padding: 1rem; 24 | -webkit-box-shadow: 0 0 1rem 0 black; 25 | box-shadow: 0 0 1rem 0 black; 26 | -webkit-animation: slideInMessage 0.75s ease-in-out; 27 | animation: slideInMessage 0.75s ease-in-out; 28 | } 29 | 30 | .error-message { 31 | background-color: var(--bright-red); 32 | } 33 | 34 | .success-message { 35 | background-color: var(--green-accent); 36 | } 37 | 38 | @media (max-width: 600px) { 39 | .message-container { 40 | min-width: 100%; 41 | max-width: 100%; 42 | bottom: 0%; 43 | position: fixed; 44 | } 45 | 46 | .message { 47 | margin: 0; 48 | font-size: 1.5rem; 49 | text-align: center; 50 | bottom: 0%; 51 | width: 100%; 52 | position: fixed; 53 | } 54 | } -------------------------------------------------------------------------------- /assets/elm/src/Views/Form.elm: -------------------------------------------------------------------------------- 1 | module Views.Form exposing (input, password, textarea, viewErrors) 2 | 3 | import Html exposing (Attribute, Html, fieldset, li, text, ul) 4 | import Html.Attributes exposing (class, type_) 5 | 6 | 7 | password : List (Attribute msg) -> List (Html msg) -> Html msg 8 | password attrs = 9 | control Html.input ([ type_ "password" ] ++ attrs) 10 | 11 | 12 | input : List (Attribute msg) -> List (Html msg) -> Html msg 13 | input attrs = 14 | control Html.input ([ type_ "text" ] ++ attrs) 15 | 16 | 17 | textarea : List (Attribute msg) -> List (Html msg) -> Html msg 18 | textarea = 19 | control Html.textarea 20 | 21 | 22 | viewErrors : List ( a, String ) -> Html msg 23 | viewErrors errors = 24 | case errors of 25 | [] -> 26 | text "" 27 | 28 | _ -> 29 | errors 30 | |> List.map (\( _, error ) -> li [] [ text error ]) 31 | |> ul [ class "card-panel red darken-1 white-text" ] 32 | 33 | 34 | 35 | -- INTERNAL -- 36 | 37 | 38 | control : (List (Attribute msg) -> List (Html msg) -> Html msg) -> List (Attribute msg) -> List (Html msg) -> Html msg 39 | control element attributes children = 40 | fieldset [ class "form-group" ] 41 | [ element (class "form-control" :: attributes) children ] 42 | -------------------------------------------------------------------------------- /assets/css/modal.css: -------------------------------------------------------------------------------- 1 | .modal-backdrop { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | background-color: rgba(0, 0, 0, 0.3); 8 | } 9 | 10 | .modal-content { 11 | position: fixed; 12 | top: 20%; 13 | bottom: 20%; 14 | right: 20%; 15 | left: 20%; 16 | padding: 2rem 3rem; 17 | overflow: auto; 18 | } 19 | 20 | @media (max-width: 600px) { 21 | .modal-content { 22 | right: 0; 23 | left: 0; 24 | } 25 | } 26 | 27 | .bottom-modal { 28 | position: fixed; 29 | width: 100vw; 30 | height: 25vh; 31 | bottom: 0; 32 | text-align: center; 33 | } 34 | 35 | .modal-header { 36 | font-weight: bold; 37 | font-size: 1.5rem; 38 | text-decoration: underline; 39 | } 40 | 41 | .close-modal { 42 | cursor: pointer; 43 | } 44 | 45 | .actions-container .close-modal, .bank-container .close-modal { 46 | position: absolute; 47 | right: 0; 48 | bottom: 0; 49 | margin: 0 1rem 1rem 0; 50 | } 51 | 52 | .close-mobile-menu { 53 | position: absolute; 54 | top: 0.25rem; 55 | left: 1rem; 56 | } 57 | 58 | .join-modal { 59 | text-align: center; 60 | } 61 | 62 | .winning-hand-modal { 63 | animation: fade-in-winning-hand 1s ease-in; 64 | } 65 | 66 | @keyframes fade-in-winning-hand { 67 | 0% { -webkit-transform: scale(0, 0); transform: scale(0, 0); } 68 | 100% { -webkit-transform: scale(1, 1); transform: scale(1, 1); } 69 | } -------------------------------------------------------------------------------- /lib/poker_ex_client_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", PokerExClientWeb.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket, 9 | timeout: 45_000 10 | # transport :longpoll, Phoenix.Transports.LongPoll 11 | 12 | # Socket params are passed from the client and can 13 | # be used to verify and authenticate a user. After 14 | # verification, you can put default assigns into 15 | # the socket that will be set for all channels, ie 16 | # 17 | # {:ok, assign(socket, :user_id, verified_user_id)} 18 | # 19 | # To deny connection, return `:error`. 20 | # 21 | # See `Phoenix.Token` documentation for examples in 22 | # performing token verification on connect. 23 | def connect(_params, socket) do 24 | {:ok, socket} 25 | end 26 | 27 | # Socket id's are topics that allow you to identify all sockets for a given user: 28 | # 29 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 30 | # 31 | # Would allow you to broadcast a "disconnect" event and terminate 32 | # all active sockets and channels for a given user: 33 | # 34 | # PokerExClientWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 35 | # 36 | # Returning `nil` makes this socket anonymous. 37 | def id(_socket), do: nil 38 | end 39 | -------------------------------------------------------------------------------- /assets/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | ".", 8 | "./elm/src", 9 | "./elm/vendor" 10 | ], 11 | "exposed-modules": [], 12 | "dependencies": { 13 | "NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0", 14 | "debois/elm-mdl": "8.1.0 <= v < 9.0.0", 15 | "elm-community/json-extra": "2.1.0 <= v < 3.0.0", 16 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 17 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 18 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 19 | "elm-lang/mouse": "1.0.0 <= v < 2.0.0", 20 | "elm-lang/dom": "1.0.0 <= v < 2.0.0", 21 | "elm-lang/navigation": "2.1.0 <= v < 3.0.0", 22 | "elm-lang/websocket": "1.0.2 <= v < 2.0.0", 23 | "evancz/url-parser": "2.0.1 <= v < 3.0.0", 24 | "krisajenkins/remotedata": "4.3.0 <= v < 5.0.0", 25 | "lukewestby/elm-http-builder": "5.1.0 <= v < 6.0.0", 26 | "mgold/elm-date-format": "1.3.0 <= v < 2.0.0", 27 | "rgrempel/elm-http-decorators": "2.0.0 <= v < 3.0.0", 28 | "rtfeldman/elm-validate": "1.1.3 <= v < 2.0.0", 29 | "rtfeldman/selectlist": "1.0.0 <= v < 2.0.0", 30 | "simonh1000/elm-jwt": "5.0.0 <= v < 6.0.0" 31 | }, 32 | "elm-version": "0.18.0 <= v < 0.19.0" 33 | } 34 | -------------------------------------------------------------------------------- /assets/elm/src/Widgets/Toast.elm: -------------------------------------------------------------------------------- 1 | module Widgets.Toast exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (class) 5 | 6 | 7 | type alias Toast r = 8 | { r | errors : List String, messages : List String } 9 | 10 | 11 | type ToastMessage 12 | = Message String 13 | | Error String 14 | 15 | 16 | viewMessages : Toast r -> Html msg 17 | viewMessages toastData = 18 | let 19 | errorMessages = 20 | case toastData.errors of 21 | [] -> 22 | [] 23 | 24 | _ -> 25 | List.map (\msg -> Error msg) toastData.errors 26 | 27 | messages = 28 | case toastData.messages of 29 | [] -> 30 | [] 31 | 32 | _ -> 33 | List.map (\msg -> Message msg) toastData.messages 34 | 35 | allMessages = 36 | messages ++ errorMessages 37 | in 38 | case allMessages of 39 | [] -> 40 | text "" 41 | 42 | _ -> 43 | div [ class "messages-container" ] <| 44 | List.map viewMessage allMessages 45 | 46 | 47 | viewMessage : ToastMessage -> Html msg 48 | viewMessage toastMessage = 49 | case toastMessage of 50 | Message message -> 51 | div [ class "message success-message" ] [ text message ] 52 | 53 | Error message -> 54 | div [ class "message error-message" ] [ text message ] 55 | -------------------------------------------------------------------------------- /lib/poker_ex_client_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn (error) -> 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end) 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. 25 | # Ecto will pass the :count keyword if the error message is 26 | # meant to be pluralized. 27 | # On your own code and templates, depending on whether you 28 | # need the message to be pluralized or not, this could be 29 | # written simply as: 30 | # 31 | # dngettext "errors", "1 file", "%{count} files", count 32 | # dgettext "errors", "is invalid" 33 | # 34 | if count = opts[:count] do 35 | Gettext.dngettext(PokerExClientWeb.Gettext, "errors", msg, msg, count, opts) 36 | else 37 | Gettext.dgettext(PokerExClientWeb.Gettext, "errors", msg, opts) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /assets/elm/src/Page/Room/PushMessages.elm: -------------------------------------------------------------------------------- 1 | module Page.Room.PushMessages exposing (..) 2 | 3 | import Json.Decode as Decode exposing (Value) 4 | import Json.Encode as Encode 5 | import Phoenix 6 | import Phoenix.Push as Push 7 | import Types.Room.Messages as Messages exposing (..) 8 | 9 | 10 | type alias Msg = 11 | RoomMsg 12 | 13 | 14 | actionPush : String -> String -> Value -> String -> Cmd Msg 15 | actionPush room actionString value socketUrl = 16 | let 17 | push = 18 | Push.init ("games:" ++ room) actionString 19 | |> Push.withPayload value 20 | in 21 | Phoenix.push socketUrl push 22 | 23 | 24 | playerInfoPush : String -> String -> String -> Cmd Msg 25 | playerInfoPush username msgToChannel socketUrl = 26 | let 27 | push = 28 | Push.init ("players:" ++ username) msgToChannel 29 | |> Push.withPayload (Encode.object [ ( "player", Encode.string username ) ]) 30 | in 31 | Phoenix.push socketUrl push 32 | 33 | rejoinPush : String -> String -> String -> String -> String -> Cmd Msg 34 | rejoinPush roomId username actionString amountString socketUrl = 35 | case String.toInt amountString of 36 | Ok joinAmount -> 37 | Phoenix.push socketUrl (Push.init ("games:" ++ roomId) actionString 38 | |> Push.withPayload (Encode.object [("player", Encode.string username), 39 | ("amount", Encode.int joinAmount)])) 40 | Err _ -> Cmd.none -------------------------------------------------------------------------------- /assets/elm/src/Widgets/Modal.elm: -------------------------------------------------------------------------------- 1 | module Widgets.Modal exposing (..) 2 | 3 | import Data.Player as Player exposing (Player) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (class, id, style) 6 | import Html.Events exposing (onClick) 7 | 8 | 9 | type alias Config msg = 10 | { classes : List String 11 | , contentHtml : List (Html msg) 12 | , styles : Maybe (List ( String, String )) 13 | } 14 | 15 | 16 | view : Config msg -> Html msg 17 | view config = 18 | let 19 | classes = 20 | List.map (\str -> str ++ " ") config.classes 21 | |> String.concat 22 | |> (++) "modal-content card-panel " 23 | |> String.trim 24 | in 25 | div [ class "modal-backdrop" ] 26 | [ div [ class classes, styles config ] 27 | config.contentHtml 28 | ] 29 | 30 | 31 | bottomModalView : Config msg -> Html msg 32 | bottomModalView config = 33 | let 34 | classes = 35 | List.map (\str -> str ++ " ") config.classes 36 | |> String.concat 37 | |> (++) "bottom-modal " 38 | |> String.trim 39 | in 40 | div [ class "modal-backdrop" ] 41 | [ div [ class classes, styles config ] 42 | config.contentHtml 43 | ] 44 | 45 | 46 | styles : Config msg -> Html.Attribute msg 47 | styles config = 48 | case config.styles of 49 | Nothing -> 50 | style [] 51 | 52 | Just theStyles -> 53 | style theStyles 54 | -------------------------------------------------------------------------------- /assets/elm/vendor/Phoenix/Internal/Message.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Internal.Message exposing (..) 2 | 3 | import Json.Decode as JD exposing (Value) 4 | import Json.Encode as JE 5 | import Phoenix.Push as Push exposing (Push) 6 | 7 | 8 | type alias Message = 9 | { topic : String 10 | , event : String 11 | , payload : Value 12 | , ref : Maybe Int 13 | } 14 | 15 | 16 | type alias Topic = 17 | String 18 | 19 | 20 | type alias Event = 21 | String 22 | 23 | 24 | type alias Ref = 25 | Int 26 | 27 | 28 | init : Topic -> Event -> Message 29 | init topic event = 30 | Message topic event (JE.object []) Nothing 31 | 32 | 33 | payload : Value -> Message -> Message 34 | payload payload_ message = 35 | { message | payload = payload_ } 36 | 37 | 38 | ref : Ref -> Message -> Message 39 | ref ref_ message = 40 | { message | ref = Just ref_ } 41 | 42 | 43 | fromPush : Push msg -> Message 44 | fromPush push = 45 | init push.topic push.event 46 | |> payload push.payload 47 | 48 | 49 | encode : Message -> String 50 | encode { topic, event, payload, ref } = 51 | JE.object 52 | [ ( "topic", JE.string topic ) 53 | , ( "event", JE.string event ) 54 | , ( "ref", Maybe.map JE.int ref |> Maybe.withDefault JE.null ) 55 | , ( "payload", payload ) 56 | ] 57 | |> JE.encode 0 58 | 59 | 60 | decode : String -> Result String Message 61 | decode msg = 62 | let 63 | decoder = 64 | JD.map4 Message 65 | (JD.field "topic" JD.string) 66 | (JD.field "event" JD.string) 67 | (JD.field "payload" JD.value) 68 | (JD.field "ref" (JD.oneOf [ JD.map Just JD.int, JD.null Nothing ])) 69 | in 70 | JD.decodeString decoder msg 71 | -------------------------------------------------------------------------------- /assets/brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: "js/app.js" 6 | 7 | // To use a separate vendor.js bundle, specify two files path 8 | // http://brunch.io/docs/config#-files- 9 | // joinTo: { 10 | // "js/app.js": /^js/, 11 | // "js/vendor.js": /^(?!js)/ 12 | // } 13 | // 14 | // To change the order of concatenation of files, explicitly mention here 15 | // order: { 16 | // before: [ 17 | // "vendor/js/jquery-2.1.1.js", 18 | // "vendor/js/bootstrap.min.js" 19 | // ] 20 | // } 21 | }, 22 | stylesheets: { 23 | joinTo: "css/app.css" 24 | }, 25 | templates: { 26 | joinTo: "js/app.js" 27 | } 28 | }, 29 | 30 | conventions: { 31 | // This option sets where we should place non-css and non-js assets in. 32 | // By default, we set this to "/assets/static". Files in this directory 33 | // will be copied to `paths.public`, which is "priv/static" by default. 34 | assets: /^(static)/ 35 | }, 36 | 37 | // Phoenix paths configuration 38 | paths: { 39 | // Dependencies and current project directories to watch 40 | watched: ["static", "css", "js", "vendor", "elm"], 41 | // Where to compile files to 42 | public: "../priv/static" 43 | }, 44 | 45 | // Configure your plugins 46 | plugins: { 47 | babel: { 48 | // Do not use ES6 compiler in vendor code 49 | ignore: [/vendor/] 50 | }, 51 | elmBrunch: { 52 | mainModules: ["elm/src/Main.elm"], 53 | makeParameters: [""], 54 | outputFolder: "js/" 55 | }, 56 | }, 57 | 58 | modules: { 59 | autoRequire: { 60 | "js/app.js": ["js/app"] 61 | } 62 | }, 63 | 64 | npm: { 65 | enabled: true 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /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 | config :poker_ex_client, PokerExClientWeb.Endpoint, 10 | http: [port: 8081], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin", 15 | cd: Path.expand("../assets", __DIR__)]] 16 | 17 | # ## SSL Support 18 | # 19 | # In order to use HTTPS in development, a self-signed 20 | # certificate can be generated by running the following 21 | # command from your terminal: 22 | # 23 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem 24 | # 25 | # The `http:` config above can be replaced with: 26 | # 27 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"], 28 | # 29 | # If desired, both `http:` and `https:` keys can be 30 | # configured to run both http and https servers on 31 | # different ports. 32 | 33 | # Watch static and templates for browser reloading. 34 | config :poker_ex_client, PokerExClientWeb.Endpoint, 35 | live_reload: [ 36 | patterns: [ 37 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 38 | ~r{priv/gettext/.*(po)$}, 39 | ~r{lib/poker_ex_client_web/views/.*(ex)$}, 40 | ~r{lib/poker_ex_client_web/templates/.*(eex)$} 41 | ] 42 | ] 43 | 44 | # Do not include metadata nor timestamps in development logs 45 | config :logger, :console, format: "[$level] $message\n" 46 | 47 | # Set a higher stacktrace during development. Avoid configuring such 48 | # in production as building large stacktraces may be expensive. 49 | config :phoenix, :stacktrace_depth, 20 50 | -------------------------------------------------------------------------------- /lib/poker_ex_client_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :poker_ex_client 3 | 4 | socket "/socket", PokerExClientWeb.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: :poker_ex_client, 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 | # The session will be stored in the cookie and signed, 34 | # this means its contents can be read but not tampered with. 35 | # Set :encryption_salt if you would also like to encrypt it. 36 | plug Plug.Session, 37 | store: :cookie, 38 | key: "_poker_ex_client_key", 39 | signing_salt: "1oZ0QX72" 40 | 41 | plug PokerExClientWeb.Router 42 | 43 | @doc """ 44 | Callback invoked for dynamically configuring the endpoint. 45 | 46 | It receives the endpoint configuration and checks if 47 | configuration should be loaded from the system environment. 48 | """ 49 | def init(_key, config) do 50 | if config[:load_from_system_env] do 51 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 52 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 53 | else 54 | {:ok, config} 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/poker_ex_client_web.ex: -------------------------------------------------------------------------------- 1 | defmodule PokerExClientWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use PokerExClientWeb, :controller 9 | use PokerExClientWeb, :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. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: PokerExClientWeb 23 | import Plug.Conn 24 | import PokerExClientWeb.Router.Helpers 25 | import PokerExClientWeb.Gettext 26 | end 27 | end 28 | 29 | def view do 30 | quote do 31 | use Phoenix.View, root: "lib/poker_ex_client_web/templates", 32 | namespace: PokerExClientWeb 33 | 34 | # Import convenience functions from controllers 35 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1] 36 | 37 | # Use all HTML functionality (forms, tags, etc) 38 | use Phoenix.HTML 39 | 40 | import PokerExClientWeb.Router.Helpers 41 | import PokerExClientWeb.ErrorHelpers 42 | import PokerExClientWeb.Gettext 43 | end 44 | end 45 | 46 | def router do 47 | quote do 48 | use Phoenix.Router 49 | import Plug.Conn 50 | import Phoenix.Controller 51 | end 52 | end 53 | 54 | def channel do 55 | quote do 56 | use Phoenix.Channel 57 | import PokerExClientWeb.Gettext 58 | end 59 | end 60 | 61 | @doc """ 62 | When used, dispatch to the appropriate controller/view/etc. 63 | """ 64 | defmacro __using__(which) when is_atom(which) do 65 | apply(__MODULE__, which, []) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /assets/elm/src/Types/Room/Messages.elm: -------------------------------------------------------------------------------- 1 | module Types.Room.Messages exposing (RoomExternalMsg(..), RoomMessageType(..), RoomMsg(..)) 2 | 3 | import Data.Player as Player exposing (Player) 4 | import Json.Decode as Decode exposing (Value) 5 | import Json.Encode as Encode 6 | import Time exposing (Time) 7 | 8 | 9 | type RoomMsg 10 | = JoinRoom Player -- START BUTTON ACTION MESSAGES 11 | | LeaveRoom Player 12 | | ActionPressed 13 | | BankPressed 14 | | AccountPressed 15 | | ChatPressed 16 | | OpenRaisePressed 17 | | MobileToolbarPressed -- END BUTTON ACTION MESSAGES 18 | | Join -- START JOIN ACTION MESSAGES 19 | | JoinedChannel 20 | | JoinFailed Value 21 | | SetJoinValue String 22 | | ConnectedToPlayerChannel 23 | | ChipInfo Encode.Value -- END JOIN ACTION MESSAGES 24 | | IncreaseRaise Int -- START RAISE ACTION MESSAGES 25 | | DecreaseRaise Int 26 | | SetRaise String -- END RAISE ACTION MESSAGES 27 | | ActionMsg String Encode.Value -- ACTION MSG 28 | | SetBankInfo Value -- BANK ACTION 29 | | SetAddAmount String -- BANK ACTION 30 | | Update Value -- START MESSAGES FROM GAME SERVER 31 | | GameStarted Value 32 | | WinnerMessage Value 33 | | Clear Value 34 | | NewMessage Value 35 | | PresentWinningHand Value -- END MESSAGES FROM GAME SERVER 36 | | NewChatMsg Value -- START CHAT MESSAGES 37 | | SetChatMsg String 38 | | CloseModal 39 | | SubmitChat -- END CHAT MESSAGES 40 | | SocketOpened -- START SOCKET MESSAGES 41 | | SocketClosed 42 | | SocketClosedAbnormally 43 | | Rejoined Value -- END SOCKET MESSAGES 44 | | Blur -- START MODAL-RELATED MESSAGES 45 | | ClearErrorMessage Time 46 | | ClearRoomMessage Time 47 | | ClearWinningHandModal Time 48 | | CloseRaiseModal 49 | | CloseWinningHandModal -- END MODAL-RELATED MESSAGES 50 | 51 | 52 | type RoomExternalMsg 53 | = NoOp 54 | 55 | 56 | type RoomMessageType 57 | = RoomMessage String 58 | | ErrorMessage String 59 | -------------------------------------------------------------------------------- /assets/elm/src/Views/Chat.elm: -------------------------------------------------------------------------------- 1 | module Views.Chat exposing (Chat, view) 2 | 3 | import Html as Html exposing (..) 4 | import Html.Attributes as Attrs exposing (..) 5 | import Html.Events as Events 6 | import Json.Encode as Encode exposing (Value) 7 | 8 | 9 | type alias PlayerName = 10 | String 11 | 12 | 13 | type alias Message = 14 | String 15 | 16 | 17 | type alias ChatMessage = 18 | { playerName : String 19 | , message : String 20 | } 21 | 22 | 23 | type alias Chat = 24 | List ChatMessage 25 | 26 | 27 | view : Chat -> String -> (String -> msg) -> msg -> msg -> Html msg 28 | view chat currentMsg inputMsg submitMsg closeModalMsg = 29 | div [ class "chat-wrapper" ] 30 | [ span [ class "modal-header red-text" ] [ text "Chat" ] 31 | , i [ class "close-chat material-icons", Events.onClick closeModalMsg ] [ text "close" ] 32 | , ul [ class "chat-container collection", id "chat" ] 33 | (List.map viewMessage chat) 34 | , Html.form [ class "chat-input-container", Events.onSubmit submitMsg ] 35 | [ chatInput currentMsg inputMsg 36 | , a [ class "btn blue white-text chat-submit-btn", Events.onClick submitMsg ] [ text "Send" ] 37 | , a [ class "btn-floating green white-text chat-submit-mobile", Events.onClick submitMsg ] 38 | [ i [ class "material-icons" ] 39 | [ text "send" ] 40 | ] 41 | ] 42 | ] 43 | 44 | 45 | viewMessage : ChatMessage -> Html msg 46 | viewMessage message = 47 | li [ class "collection-item message-container" ] 48 | [ span [ class "chat-player-name red-text" ] 49 | [ text <| message.playerName ++ " says: " ] 50 | , span [ class "chat-message-item" ] 51 | [ text message.message ] 52 | ] 53 | 54 | 55 | chatInput : String -> (String -> msg) -> Html msg 56 | chatInput currentMsg inputMsg = 57 | input 58 | [ type_ "text" 59 | , class "chat-input" 60 | , value currentMsg 61 | , placeholder "Say something" 62 | , Events.onInput inputMsg 63 | ] 64 | [] 65 | -------------------------------------------------------------------------------- /assets/elm/src/Widgets/Dropdown.elm: -------------------------------------------------------------------------------- 1 | module Widgets.Dropdown exposing (Config, Context, view) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (class, classList, style) 5 | import Html.Events exposing (onWithOptions) 6 | import Json.Decode as Decode 7 | import Types.Dropdowns as DropdownType exposing (DropdownItem, DropdownNavbarLink) 8 | 9 | 10 | type alias Context = 11 | { selectedItem : DropdownItem 12 | , isOpen : Bool 13 | } 14 | 15 | 16 | type alias Config msg = 17 | { topLevelHtml : Html msg 18 | , clickedMsg : msg 19 | , itemPickedMsg : DropdownNavbarLink -> msg 20 | } 21 | 22 | 23 | view : Config msg -> Context -> List DropdownNavbarLink -> Html msg 24 | view config context data = 25 | let 26 | length = 27 | List.length data * 3 28 | 29 | displayStyles = 30 | if context.isOpen then 31 | [ ( "transform", "scaleY(1)" ) 32 | , ( "transform-origin", "top" ) 33 | , ( "transition", "transform 1s ease-in-out" ) 34 | , ( "max-height", toString length ++ "em" ) 35 | , ( "transition", "max-height 0.5s ease-in-out" ) 36 | ] 37 | else 38 | [ ( "transform", "scaleY(0)" ) 39 | , ( "transform-origin", "top" ) 40 | , ( "transition", "transform 1s ease-in-out" ) 41 | , ( "max-height", "0" ) 42 | , ( "transition", "max-height 0.5s ease-in-out" ) 43 | ] 44 | in 45 | ul 46 | [ style displayStyles 47 | , classList [ ( "dropdown-menu", context.isOpen ), ( "collection", True ) ] 48 | , class "nav-dropdown" 49 | ] 50 | (List.map (viewItem config) data) 51 | 52 | 53 | viewItem : Config msg -> DropdownNavbarLink -> Html msg 54 | viewItem config item = 55 | li [ onClick (config.itemPickedMsg item), class "collection-item nav-dropdown-item" ] 56 | [ span [ class "cursor-pointer" ] [ text <| toString item ] ] 57 | 58 | 59 | 60 | -- Helper to cancel click anywhere -- 61 | 62 | 63 | onClick : msg -> Attribute msg 64 | onClick message = 65 | onWithOptions 66 | "click" 67 | { stopPropagation = True 68 | , preventDefault = False 69 | } 70 | (Decode.succeed message) 71 | -------------------------------------------------------------------------------- /assets/elm/src/Views/Footer.elm: -------------------------------------------------------------------------------- 1 | module Views.Footer exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (class, href) 5 | 6 | 7 | view : Html msg 8 | view = 9 | footer [ class "page-footer blue-grey darken-3 white-text bottom" ] 10 | [ div [ class "container" ] 11 | [ div [ class "row footer-row" ] 12 | [ div [ class "col m6 l6 s12 center-align-small" ] 13 | [ h5 [ class "white-text" ] [ text "POKEREX" ] 14 | , p [ class "grey-text text-lighten-4" ] 15 | [ text "PokerEx was designed and developed by Zack Kayser. " 16 | , text "Connect with me on Twitter at @kayserzl or on " 17 | , a [ href "https://www.linkedin.com/in/zack-kayser-93b96b88" ] [ text "LinkedIn. " ] 18 | , text "Feel free to check out the open-source code on " 19 | , a [ href "https://github.com/zkayser/pokerex_client" ] [ text "Github" ] 20 | , text "! " 21 | , text "You can also check out the server-side Elixir/Phoenix project " 22 | , a [ href "https://github.com/zkayser/poker_ex" ] [ text "here" ] 23 | , text "." 24 | ] 25 | ] 26 | , div [ class "col m4 l4 offset-l2 s12" ] 27 | [ h5 [ class "white-text center-align-small" ] [ text "Links" ] 28 | , ul [ class "text-center" ] 29 | [ li [] [ a [ href "https://github.com/zkayser/pokerex_client" ] [ text "Github" ] ] 30 | , li [] [ a [ href "https://github.com/zkayser/poker_ex" ] [ text "Server Side Elixir Project" ] ] 31 | , li [] [ a [ href "http://elm-lang.org/" ] [ text "Elm-lang home page" ] ] 32 | , li [] [ a [ href "http://www.phoenixframework.org/" ] [ text "Phoenix Framework" ] ] 33 | , li [] [ a [ href "http://elixir-lang.org/" ] [ text "Elixir Lang Home Page" ] ] 34 | ] 35 | ] 36 | ] 37 | ] 38 | , div [ class "footer-copyright" ] 39 | [ div [ class "container" ] [ text "©Zack Kayser, 2017" ] ] 40 | ] 41 | -------------------------------------------------------------------------------- /assets/elm/src/Route.elm: -------------------------------------------------------------------------------- 1 | module Route exposing (..) 2 | 3 | import Html exposing (Attribute) 4 | import Html.Attributes as Attr 5 | import Navigation exposing (Location) 6 | import UrlParser as Url exposing ((), Parser, oneOf, parseHash, s) 7 | 8 | 9 | type Route 10 | = Home 11 | | Login 12 | | Logout 13 | | Register 14 | | Room String String 15 | | Rooms 16 | | Profile String 17 | | ForgotPassword 18 | | ResetPassword String 19 | 20 | 21 | route : Parser (Route -> a) a 22 | route = 23 | oneOf 24 | [ Url.map Home (s "") 25 | , Url.map Login (s "login") 26 | , Url.map Logout (s "logout") 27 | , Url.map Register (s "register") 28 | , Url.map Room (s "rooms" Url.string Url.string) 29 | , Url.map Rooms (s "rooms") 30 | , Url.map Profile (s "profile" Url.string) 31 | , Url.map ForgotPassword (s "forgot-password") 32 | , Url.map ResetPassword (s "reset-password" Url.string) 33 | ] 34 | 35 | 36 | fromLocation : Location -> Maybe Route 37 | fromLocation location = 38 | if String.isEmpty location.hash then 39 | Just Home 40 | else 41 | parseHash route location 42 | 43 | 44 | href : Route -> Attribute msg 45 | href route = 46 | Attr.href (routeToString route) 47 | 48 | 49 | modifyUrl : Route -> Cmd msg 50 | modifyUrl = 51 | routeToString >> Navigation.modifyUrl 52 | 53 | 54 | routeToString : Route -> String 55 | routeToString page = 56 | let 57 | pieces = 58 | case page of 59 | Home -> 60 | [] 61 | 62 | Login -> 63 | [ "login" ] 64 | 65 | Logout -> 66 | [ "logout" ] 67 | 68 | Register -> 69 | [ "register" ] 70 | 71 | Room roomType roomTitle -> 72 | [ "rooms", roomType, roomTitle ] 73 | 74 | Rooms -> 75 | [ "rooms" ] 76 | 77 | Profile user -> 78 | [ "profile", user ] 79 | 80 | ForgotPassword -> 81 | [ "forgot-password" ] 82 | 83 | ResetPassword resetToken -> 84 | [ "reset-password", resetToken ] 85 | in 86 | "#/" ++ String.join "/" pieces 87 | -------------------------------------------------------------------------------- /assets/elm/src/Views/Bank.elm: -------------------------------------------------------------------------------- 1 | module Views.Bank exposing (view) 2 | 3 | import Data.Player as Player exposing (Player, Username) 4 | import Html as Html exposing (..) 5 | import Html.Attributes as Attrs exposing (..) 6 | import Html.Events as Events exposing (onClick, onInput, onSubmit) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | type alias Model r = 11 | { r 12 | | addAmount : Int 13 | , player : Player 14 | , chipsAvailable : Int 15 | } 16 | 17 | 18 | view : Model r -> ( String -> msg, String -> Value -> msg, msg ) -> Html msg 19 | view model ( setMsg, submitMsg, closeMsg ) = 20 | div [ class "bank-container center-align" ] 21 | [ span [ class "modal-header red-text" ] [ viewTitle model ] 22 | , h4 [ class "red-text" ] [ text <| "You currently have " ++ toString model.chipsAvailable ++ " chips available." ] 23 | , div [ class "bank-form-container" ] 24 | [ div [ class "bank-form" ] 25 | [ Html.form [ onSubmit (submitMsg "action_add_chips" (encodePayload model)) ] 26 | [ div [ class "input-field" ] 27 | [ input 28 | [ placeholder "How many chips would you like to add?" 29 | , type_ "number" 30 | , class "validate" 31 | , onInput setMsg 32 | ] 33 | [] 34 | ] 35 | ] 36 | , button 37 | [ type_ "submit" 38 | , onClick (submitMsg "action_add_chips" (encodePayload model)) 39 | , class "btn blue white-text" 40 | ] 41 | [ text "Add chips" ] 42 | ] 43 | ] 44 | , i [ class "close-modal material-icons small", onClick closeMsg ] [ text "close" ] 45 | ] 46 | 47 | 48 | viewTitle : Model r -> Html msg 49 | viewTitle model = 50 | if model.addAmount == 0 then 51 | text "Add chips?" 52 | else 53 | text <| "Add " ++ toString model.addAmount ++ " chips?" 54 | 55 | 56 | encodePayload : Model r -> Value 57 | encodePayload model = 58 | Encode.object 59 | [ ( "player", Player.encodeUsername model.player.username ) 60 | , ( "amount", Encode.int model.addAmount ) 61 | ] 62 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, 2 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, 3 | "file_system": {:hex, :file_system, "0.2.1", "c4bec8f187d2aabace4beb890f0d4e468f65ca051593db768e533a274d0df587", [:mix], [], "hexpm"}, 4 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], [], "hexpm"}, 5 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], [], "hexpm"}, 6 | "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "phoenix_html": {:hex, :phoenix_html, "2.10.4", "d4f99c32d5dc4918b531fdf163e1fd7cf20acdd7703f16f5d02d4db36de803b7", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.2", "dfd31cc1eb091533b4419bbdb67bec9767bb26c9fe09602e6cca313fab5302d0", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"}, 10 | "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, 11 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 12 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}} 13 | -------------------------------------------------------------------------------- /assets/css/profile.css: -------------------------------------------------------------------------------- 1 | .profile-pane-container { 2 | display: flex; 3 | } 4 | 5 | .profile-pane-container > * { 6 | flex: 1; 7 | min-height: 74.5vh; 8 | padding: 1rem; 9 | } 10 | 11 | .profile-pane-break { 12 | display: none; 13 | } 14 | 15 | @media (max-width: 860px) { 16 | .profile-pane-container { 17 | flex-direction: column; 18 | } 19 | 20 | .profile-pane-container > * { 21 | min-height: 0; 22 | } 23 | 24 | .profile-pane.game-info-pane { 25 | order: 1; 26 | } 27 | 28 | .profile-pane.profile-info-pane { 29 | order: 3; 30 | } 31 | 32 | .profile-pane-break { 33 | display: block; 34 | flex: 1; 35 | order: 2; 36 | height: 1px; 37 | padding: 0; 38 | margin: 1rem; 39 | } 40 | } 41 | 42 | @media (max-width: 435px) { 43 | .fb-invite-btn { 44 | font-size: 0.75rem; 45 | } 46 | } 47 | 48 | .profile-greeting { 49 | text-align: center; 50 | } 51 | 52 | .chip-restore { 53 | text-align: center; 54 | } 55 | 56 | .fb-invite-container { 57 | margin-top: 2rem; 58 | text-align: center; 59 | } 60 | 61 | .delete-profile-container { 62 | margin-top: 2rem; 63 | text-align: center; 64 | } 65 | 66 | .game-info-header { 67 | text-align: center; 68 | text-decoration: underline; 69 | } 70 | 71 | .game-info-text { 72 | text-align: center; 73 | font-size: 1.25rem; 74 | font-weight: 400; 75 | } 76 | 77 | .current-games a .game-info-text { 78 | text-align: center; 79 | } 80 | 81 | .current-games-tab-container { 82 | box-shadow: 0rem 0.01rem 0.01rem 0.01rem black; 83 | z-index: 100; 84 | } 85 | 86 | .create-game-tab-container { 87 | box-shadow: 0rem 0.01rem 0.01rem 0.01rem black; 88 | z-index: 100; 89 | } 90 | 91 | .game-form-header { 92 | text-align: center; 93 | text-decoration: underline; 94 | } 95 | 96 | .invite-btn { 97 | width: 11rem; 98 | display: flex; 99 | align-items: center; 100 | } 101 | 102 | .invite-btn i { 103 | margin-right: 1rem; 104 | } 105 | 106 | .player-list-elem { 107 | display: flex; 108 | align-items: center; 109 | justify-content: space-between; 110 | } 111 | 112 | .player-list-collection { 113 | padding-top: 1.5rem; 114 | } 115 | 116 | .active-sub-tab { 117 | background-color: var(--teal); 118 | color: white; 119 | } 120 | 121 | .submit-game-btn-container { 122 | display: flex; 123 | justify-content: center; 124 | align-items: center; 125 | margin-bottom: 1rem; 126 | } -------------------------------------------------------------------------------- /assets/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "assets/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket 5 | // and connect at the socket path in "lib/web/endpoint.ex": 6 | import {Socket} from "phoenix" 7 | 8 | let socket = new Socket("/socket", {params: {token: window.userToken}}) 9 | 10 | // When you connect, you'll often need to authenticate the client. 11 | // For example, imagine you have an authentication plug, `MyAuth`, 12 | // which authenticates the session and assigns a `:current_user`. 13 | // If the current user exists you can assign the user's token in 14 | // the connection for use in the layout. 15 | // 16 | // In your "lib/web/router.ex": 17 | // 18 | // pipeline :browser do 19 | // ... 20 | // plug MyAuth 21 | // plug :put_user_token 22 | // end 23 | // 24 | // defp put_user_token(conn, _) do 25 | // if current_user = conn.assigns[:current_user] do 26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 27 | // assign(conn, :user_token, token) 28 | // else 29 | // conn 30 | // end 31 | // end 32 | // 33 | // Now you need to pass this token to JavaScript. You can do so 34 | // inside a script tag in "lib/web/templates/layout/app.html.eex": 35 | // 36 | // 37 | // 38 | // You will need to verify the user token in the "connect/2" function 39 | // in "lib/web/channels/user_socket.ex": 40 | // 41 | // def connect(%{"token" => token}, socket) do 42 | // # max_age: 1209600 is equivalent to two weeks in seconds 43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 44 | // {:ok, user_id} -> 45 | // {:ok, assign(socket, :user, user_id)} 46 | // {:error, reason} -> 47 | // :error 48 | // end 49 | // end 50 | // 51 | // Finally, pass the token on connect as below. Or remove it 52 | // from connect if you don't care about authentication. 53 | 54 | socket.connect() 55 | 56 | // Now that you are connected, you can join channels with a topic: 57 | let channel = socket.channel("topic:subtopic", {}) 58 | channel.join() 59 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 60 | .receive("error", resp => { console.log("Unable to join", resp) }) 61 | 62 | export default socket 63 | -------------------------------------------------------------------------------- /assets/css/room.css: -------------------------------------------------------------------------------- 1 | .room-container { 2 | min-height: 88vh; 3 | display: -webkit-box; 4 | display: -ms-flexbox; 5 | display: flex; 6 | -webkit-box-orient: vertical; 7 | -webkit-box-direction: normal; 8 | -ms-flex-direction: column; 9 | flex-direction: column; 10 | -webkit-box-align: center; 11 | -ms-flex-align: center; 12 | align-items: center; 13 | -webkit-box-pack: center; 14 | -ms-flex-pack: center; 15 | justify-content: center; 16 | position: relative; 17 | } 18 | 19 | .room-message-container { 20 | position: fixed; 21 | z-index: 100; 22 | min-width: 25%; 23 | right: 0%; 24 | top: 15%; 25 | display: -webkit-box; 26 | display: -ms-flexbox; 27 | display: flex; 28 | -webkit-box-orient: vertical; 29 | -webkit-box-direction: normal; 30 | -ms-flex-direction: column; 31 | flex-direction: column; 32 | -webkit-box-align: baseline; 33 | -ms-flex-align: baseline; 34 | align-items: baseline; 35 | } 36 | 37 | .message { 38 | color: white; 39 | font-size: 1.25rem; 40 | margin: 0.5rem 0; 41 | padding: 1rem; 42 | -webkit-box-shadow: 0 0 1rem 0 black; 43 | box-shadow: 0 0 1rem 0 black; 44 | -webkit-animation: slideInMessage 0.75s ease-in-out; 45 | animation: slideInMessage 0.75s ease-in-out; 46 | } 47 | 48 | @media (max-width: 600px) { 49 | .room-message-container { 50 | position: fixed; 51 | min-width: 100%; 52 | max-width: 100%; 53 | bottom: 0%; 54 | } 55 | } 56 | 57 | .error-message { 58 | background-color: var(--bright-red); 59 | } 60 | 61 | .room-message { 62 | background-color: var(--green-accent); 63 | } 64 | 65 | @-webkit-keyframes slideInMessage { 66 | 0% { 67 | -webkit-transform: translateY(100rem); 68 | transform: translateY(100rem); 69 | opacity: 0; 70 | } 71 | 50% { 72 | opacity: 0; 73 | } 74 | 75% { 75 | opacity: 0; 76 | } 77 | 100% { 78 | -webkit-transform: translateY(0); 79 | transform: translateY(0); 80 | opacity: 1; 81 | } 82 | } 83 | 84 | @keyframes slideInMessage { 85 | 0% { 86 | -webkit-transform: translateY(100rem); 87 | transform: translateY(100rem); 88 | opacity: 0; 89 | } 90 | 50% { 91 | opacity: 0; 92 | } 93 | 75% { 94 | opacity: 0; 95 | } 96 | 100% { 97 | -webkit-transform: translateY(0); 98 | transform: translateY(0); 99 | opacity: 1; 100 | } 101 | } -------------------------------------------------------------------------------- /assets/elm/src/Page/Room/Helpers.elm: -------------------------------------------------------------------------------- 1 | module Page.Room.Helpers exposing (..) 2 | 3 | import Data.Card as Card exposing (Card) 4 | import Data.Player as Player exposing (Player) 5 | import Data.Room as Room exposing (..) 6 | import Data.RoomPage as RoomPage exposing (RoomPage) 7 | import Dict exposing (Dict) 8 | 9 | 10 | type alias Model = 11 | RoomPage 12 | 13 | 14 | handWhereIs : Player.Username -> List Room.PlayerHand -> Player -> List Card 15 | handWhereIs username playerHands player = 16 | let 17 | theHand = 18 | case List.filter (\playerHand -> Player.equals username playerHand.player) playerHands of 19 | [] -> 20 | Nothing 21 | 22 | [ playerHand ] -> 23 | Just playerHand 24 | 25 | _ -> 26 | Nothing 27 | 28 | handForPlayer = 29 | case theHand of 30 | Just hand -> 31 | if Player.equals hand.player player.username then 32 | hand.hand 33 | else 34 | [ { rank = Card.RankError, suit = Card.SuitError }, { rank = Card.RankError, suit = Card.SuitError } ] 35 | 36 | _ -> 37 | [ { rank = Card.RankError, suit = Card.SuitError }, { rank = Card.RankError, suit = Card.SuitError } ] 38 | in 39 | handForPlayer 40 | 41 | 42 | getChips : Model -> Dict String Int -> Int 43 | getChips model dict = 44 | case Dict.get (Player.usernameToString model.player.username) dict of 45 | Nothing -> 46 | 0 47 | 48 | Just chips -> 49 | chips 50 | 51 | 52 | getIsActive : Model -> Bool 53 | getIsActive model = 54 | case model.roomModel.active of 55 | Nothing -> 56 | False 57 | 58 | Just username -> 59 | Player.equals model.player.username username 60 | 61 | 62 | possibleActions : List String 63 | possibleActions = 64 | [ "action_raise", "action_check", "action_call", "action_fold", "action_add_chips" ] 65 | 66 | 67 | joinValToInt : String -> Int 68 | joinValToInt stringAmount = 69 | case String.toInt stringAmount of 70 | Ok value -> 71 | value 72 | 73 | Err _ -> 74 | 0 75 | 76 | 77 | formatTitle : String -> String 78 | formatTitle title = 79 | String.split "%20" title 80 | |> String.join "_" 81 | |> String.split " " 82 | |> String.join "_" 83 | -------------------------------------------------------------------------------- /assets/elm/src/Data/Player.elm: -------------------------------------------------------------------------------- 1 | module Data.Player exposing (..) 2 | 3 | import Data.AuthToken as AuthToken exposing (AuthToken) 4 | import Data.Profile as Profile exposing (Profile) 5 | import Html exposing (Html) 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Decode.Pipeline as Pipeline exposing (decode, optional, required) 8 | import Json.Encode as Encode exposing (Value) 9 | import Json.Encode.Extra as EncodeExtra 10 | import UrlParser 11 | 12 | 13 | type alias Player = 14 | { email : String 15 | , token : AuthToken 16 | , username : Username 17 | , chips : Int 18 | } 19 | 20 | 21 | type alias TablePlayer = 22 | { name : Username 23 | , chips : Int 24 | } 25 | 26 | 27 | 28 | -- Initialization -- 29 | 30 | 31 | defaultPlayer : Player 32 | defaultPlayer = 33 | { email = "" 34 | , token = AuthToken.AuthToken "" 35 | , username = Username "" 36 | , chips = 0 37 | } 38 | 39 | 40 | 41 | -- Serialization -- 42 | 43 | 44 | decoder : Decoder Player 45 | decoder = 46 | decode Player 47 | |> optional "email" Decode.string "" 48 | |> required "token" AuthToken.decoder 49 | |> required "username" usernameDecoder 50 | |> required "chips" Decode.int 51 | 52 | 53 | encode : Player -> Value 54 | encode player = 55 | Encode.object 56 | [ ( "email", Encode.string player.email ) 57 | , ( "token", AuthToken.encode player.token ) 58 | , ( "username", encodeUsername player.username ) 59 | , ( "chips", Encode.int player.chips ) 60 | ] 61 | 62 | 63 | tablePlayerDecoder : Decoder TablePlayer 64 | tablePlayerDecoder = 65 | decode TablePlayer 66 | |> required "name" usernameDecoder 67 | |> required "chips" Decode.int 68 | 69 | 70 | 71 | -- Identifiers -- 72 | 73 | 74 | type Username 75 | = Username String 76 | 77 | 78 | usernameToString : Username -> String 79 | usernameToString (Username username) = 80 | username 81 | 82 | 83 | usernameDecoder : Decoder Username 84 | usernameDecoder = 85 | Decode.map Username Decode.string 86 | 87 | 88 | encodeUsername : Username -> Value 89 | encodeUsername (Username username) = 90 | Encode.string username 91 | 92 | 93 | usernameToHtml : Username -> Html msg 94 | usernameToHtml (Username username) = 95 | Html.text username 96 | 97 | 98 | equals : Username -> Username -> Bool 99 | equals usernameOne usernameTwo = 100 | usernameOne == usernameTwo 101 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we often load configuration from external 4 | # sources, such as your system environment. For this reason, 5 | # you won't find the :http configuration below, but set inside 6 | # PokerExClientWeb.Endpoint.init/2 when load_from_system_env is 7 | # true. Any dynamic configuration should be done there. 8 | # 9 | # Don't forget to configure the url host to something meaningful, 10 | # Phoenix uses this information when generating URLs. 11 | # 12 | # Finally, we also include the path to a cache manifest 13 | # containing the digested version of static files. This 14 | # manifest is generated by the mix phx.digest task 15 | # which you typically run after static files are built. 16 | config :poker_ex_client, PokerExClientWeb.Endpoint, 17 | load_from_system_env: true, 18 | url: [scheme: "https", host: "poker-ex.herokuapp.com", port: 80], 19 | force_ssl: [rewrite_on: [:x_forwarded_proto]], 20 | cache_static_manifest: "priv/static/cache_manifest.json" 21 | 22 | # Do not print debug messages in production 23 | config :logger, level: :info 24 | 25 | # ## SSL Support 26 | # 27 | # To get SSL working, you will need to add the `https` key 28 | # to the previous section and set your `:url` port to 443: 29 | # 30 | # config :poker_ex_client, PokerExClientWeb.Endpoint, 31 | # ... 32 | # url: [host: "example.com", port: 443], 33 | # https: [:inet6, 34 | # port: 443, 35 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 36 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 37 | # 38 | # Where those two env variables return an absolute path to 39 | # the key and cert in disk or a relative path inside priv, 40 | # for example "priv/ssl/server.key". 41 | # 42 | # We also recommend setting `force_ssl`, ensuring no data is 43 | # ever sent via http, always redirecting to https: 44 | # 45 | # config :poker_ex_client, PokerExClientWeb.Endpoint, 46 | # force_ssl: [hsts: true] 47 | # 48 | # Check `Plug.SSL` for all available options in `force_ssl`. 49 | 50 | # ## Using releases 51 | # 52 | # If you are doing OTP releases, you need to instruct Phoenix 53 | # to start the server for all endpoints: 54 | # 55 | # config :phoenix, :serve_endpoints, true 56 | # 57 | # Alternatively, you can configure exactly which server to 58 | # start per endpoint: 59 | # 60 | # config :poker_ex_client, PokerExClientWeb.Endpoint, server: true 61 | # 62 | 63 | # Finally import the config/prod.secret.exs 64 | # which should be versioned separately. 65 | # import_config "prod.secret.exs" 66 | -------------------------------------------------------------------------------- /assets/elm/vendor/Phoenix/Push.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Push exposing (Push, init, map, onError, onOk, withPayload) 2 | 3 | {-| A message to push informations to a channel. 4 | 5 | 6 | # Definition 7 | 8 | @docs Push 9 | 10 | 11 | # Helpers 12 | 13 | @docs init, withPayload, onOk, onError, map 14 | 15 | -} 16 | 17 | import Json.Encode exposing (Value) 18 | 19 | 20 | {-| The message abstraction 21 | -} 22 | type alias Push msg = 23 | PhoenixPush msg 24 | 25 | 26 | type alias PhoenixPush msg = 27 | { topic : String 28 | , event : String 29 | , payload : Value 30 | , onOk : Maybe (Value -> msg) 31 | , onError : Maybe (Value -> msg) 32 | } 33 | 34 | 35 | type alias Topic = 36 | String 37 | 38 | 39 | type alias Event = 40 | String 41 | 42 | 43 | {-| Initialize a message with a topic and an event. 44 | 45 | init "room:lobby" "new_msg" 46 | 47 | -} 48 | init : Topic -> Event -> Push msg 49 | init topic event = 50 | PhoenixPush topic event (Json.Encode.object []) Nothing Nothing 51 | 52 | 53 | {-| Attach a payload to a message 54 | 55 | payload = 56 | Json.Encode.object [("msg", "Hello Phoenix")] 57 | 58 | init "room:lobby" "new_msg" 59 | |> withPayload 60 | 61 | -} 62 | withPayload : Value -> Push msg -> Push msg 63 | withPayload payload push = 64 | { push | payload = payload } 65 | 66 | 67 | {-| Callback if the server replies with an "ok" status. 68 | 69 | type Msg = MessageArrived | ... 70 | 71 | payload = 72 | Json.Encode.object [("msg", "Hello Phoenix")] 73 | 74 | init "room:lobby" "new_msg" 75 | |> withPayload 76 | |> onOk (\_ -> MessageArrived) 77 | 78 | -} 79 | onOk : (Value -> msg) -> Push msg -> Push msg 80 | onOk cb push = 81 | { push | onOk = Just cb } 82 | 83 | 84 | {-| Callback if the server replies with an "error" status. 85 | 86 | type Msg = MessageFailed Value | ... 87 | 88 | payload = 89 | Json.Encode.object [("msg", "Hello Phoenix")] 90 | 91 | init "room:lobby" "new_msg" 92 | |> withPayload 93 | |> onError MessageFailed 94 | 95 | -} 96 | onError : (Value -> msg) -> Push msg -> Push msg 97 | onError cb push = 98 | { push | onError = Just cb } 99 | 100 | 101 | {-| Applies the function on the onOk and onError callback 102 | -} 103 | map : (a -> b) -> Push a -> Push b 104 | map func push = 105 | let 106 | f = 107 | Maybe.map ((<<) func) 108 | in 109 | { push | onOk = f push.onOk, onError = f push.onError } 110 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | // Brunch automatically concatenates all files in your 2 | // watched paths. Those paths can be configured at 3 | // config.paths.watched in "brunch-config.js". 4 | // 5 | // However, those files will only be executed if 6 | // explicitly imported. The only exception are files 7 | // in vendor, which are never wrapped in imports and 8 | // therefore are always executed. 9 | 10 | // Import dependencies 11 | // 12 | // If you no longer want to use a dependency, remember 13 | // to also remove its path from "config.paths.watched". 14 | import "phoenix_html" 15 | 16 | import Elm from "./main.js"; 17 | 18 | // FB SDK Setup/Functions 19 | const fbId = document.querySelector("[data-fb-id]").getAttribute('data-fb-id'); 20 | 21 | window.fbAsyncInit = () => { 22 | FB.init({ 23 | appId : `${fbId}`, 24 | xfbml : true, 25 | version : 'v2.9' 26 | }); 27 | FB.AppEvents.logPageView(); 28 | }; 29 | 30 | (function(d, s, id){ 31 | var js, fjs = d.getElementsByTagName(s)[0]; 32 | if (d.getElementById(id)) {return;} 33 | js = d.createElement(s); js.id = id; 34 | js.src = "//connect.facebook.net/en_US/sdk.js"; 35 | fjs.parentNode.insertBefore(js, fjs); 36 | }(document, 'script', 'facebook-jssdk')); 37 | 38 | const ELM_DIV = document.getElementById("elm-div"); 39 | let elmApp = Elm.Main.embed(ELM_DIV, { 40 | session: localStorage.session, 41 | socketUrl: document.querySelector("[data-ws-url]").getAttribute('data-ws-url'), 42 | apiUrl: document.querySelector("[data-api-url]").getAttribute('data-api-url') 43 | }); 44 | 45 | elmApp.ports.storeSession.subscribe((session) => { 46 | localStorage.session = session; 47 | }); 48 | 49 | elmApp.ports.scrollChatToTop.subscribe(() => { 50 | let chat = document.getElementById('chat'); 51 | chat ? chat.scrollTo(0, 0) : null; 52 | }); 53 | 54 | elmApp.ports.logout.subscribe(() => { 55 | localStorage.removeItem('session'); 56 | }); 57 | 58 | elmApp.ports.triggerFBInviteRequest.subscribe(() => { 59 | if (window.FB) { 60 | FB.ui({method: 'apprequests', 61 | message: 'Join me for a game of Poker on PokerEX!' 62 | }, (response) => { "ok" }); 63 | } 64 | }); 65 | 66 | elmApp.ports.loginWithFB.subscribe(() => { 67 | FB.login((response) => { 68 | if (response.authResponse) { 69 | FB.api("/me", (response) => { 70 | elmApp.ports.onFBLogin.send(response); 71 | }); 72 | } 73 | }); 74 | }); 75 | 76 | window.addEventListener("storage", (event) => { 77 | if (event.storageArea === localStorage && event.key === "session") { 78 | app.ports.onSessionChange.send(event.newValue); 79 | } 80 | }, false); 81 | -------------------------------------------------------------------------------- /assets/css/controls.css: -------------------------------------------------------------------------------- 1 | .controls-container { 2 | background-color: rgb(244, 67, 54); 3 | /*position: absolute;*/ 4 | bottom: 0; 5 | display: -webkit-box; 6 | display: -ms-flexbox; 7 | display: flex; 8 | -webkit-box-pack: justify; 9 | -ms-flex-pack: justify; 10 | justify-content: space-between; 11 | width: 100%; 12 | color: white; 13 | padding: 1rem 0 1rem 0; 14 | } 15 | 16 | @media (max-width: 600px) { 17 | .controls-container { 18 | position: absolute; 19 | right: 0; 20 | display: none; 21 | } 22 | } 23 | 24 | .controls-container li { 25 | list-style-type: none; 26 | -webkit-box-flex: 1; 27 | -ms-flex: 1; 28 | flex: 1; 29 | display: -webkit-box; 30 | display: -ms-flexbox; 31 | display: flex; 32 | -webkit-box-pack: center; 33 | -ms-flex-pack: center; 34 | justify-content: center; 35 | cursor: pointer; 36 | } 37 | 38 | .controls-disabled { 39 | opacity: 0.4; 40 | justify-content: center; 41 | align-items: center; 42 | } 43 | 44 | .controls-container li a { 45 | text-decoration: none; 46 | color: white; 47 | font-size: 1.5rem; 48 | width: 100%; 49 | text-align: center; 50 | } 51 | 52 | @-webkit-keyframes highlight-active { 53 | from { 54 | background-color: rgb(244, 67, 54); 55 | } 56 | to { 57 | background-color: #ff8a80; 58 | } 59 | } 60 | 61 | @keyframes highlight-active { 62 | from { 63 | background-color: rgb(244, 67, 54); 64 | } 65 | to { 66 | background-color: #ff8a80; 67 | } 68 | } 69 | 70 | .control-active { 71 | -webkit-animation: 2s linear 0s infinite alternate highlight-active; 72 | animation: 2s linear 0s infinite alternate highlight-active; 73 | } 74 | 75 | .mobile-controls-container { 76 | display: none; 77 | } 78 | 79 | @media (max-width: 600px) { 80 | .mobile-controls-container { 81 | display: inline; 82 | position: fixed; 83 | bottom: 22%; 84 | left: 10%; 85 | } 86 | } 87 | 88 | .mobile-menu-item a { 89 | cursor: pointer; 90 | } 91 | 92 | .actions-container-mobile { 93 | display: none; 94 | } 95 | 96 | @media (max-width: 600px) { 97 | .actions-container-mobile { 98 | display: flex; 99 | justify-content: space-evenly; 100 | position: fixed; 101 | bottom: 10%; 102 | right: 10%; 103 | } 104 | 105 | .actions-container-mobile > a { 106 | flex: 1; 107 | margin: 0 1rem 0 1rem; 108 | -webkit-animation: fade-controls-in 0.5s ease-in; 109 | animation: fade-controls-in 0.5s ease-in; 110 | } 111 | } 112 | 113 | @keyframes fade-controls-in { 114 | 0% { -webkit-transform: scale(0, 0); transform: scale(0, 0); } 115 | 100% { -webkit-transform: scale(1, 1); transform: scale(1, 1); } 116 | } 117 | -------------------------------------------------------------------------------- /assets/css/header.css: -------------------------------------------------------------------------------- 1 | .nav-container { 2 | display: -webkit-box; 3 | display: -ms-flexbox; 4 | display: flex; 5 | -webkit-box-orient: horizontal; 6 | -webkit-box-direction: normal; 7 | -ms-flex-direction: row; 8 | flex-direction: row; 9 | -webkit-box-align: center; 10 | -ms-flex-align: center; 11 | align-items: center; 12 | -webkit-box-pack: center; 13 | -ms-flex-pack: center; 14 | justify-content: center; 15 | -ms-flex-wrap: nowrap; 16 | flex-wrap: nowrap; 17 | padding-top: 4em; 18 | padding-bottom: 4em; 19 | } 20 | 21 | .nav-dropdown { 22 | width: 100%; 23 | margin-top: 0; 24 | padding-top: 0; 25 | margin-bottom: 0; 26 | border: 0; 27 | } 28 | 29 | .logo-container { 30 | -webkit-box-flex: 2; 31 | -ms-flex: 2; 32 | flex: 2; 33 | display: -webkit-box; 34 | display: -ms-flexbox; 35 | display: flex; 36 | -webkit-box-align: center; 37 | -ms-flex-align: center; 38 | align-items: center; 39 | -webkit-box-pack: center; 40 | -ms-flex-pack: center; 41 | justify-content: center; 42 | } 43 | 44 | .logo { 45 | font-size: 2.25em; 46 | } 47 | 48 | .filler { 49 | -webkit-box-flex: 1; 50 | -ms-flex: 1; 51 | flex: 1; 52 | height: inherit; 53 | display: -webkit-box; 54 | display: -ms-flexbox; 55 | display: flex; 56 | -webkit-box-pack: center; 57 | -ms-flex-pack: center; 58 | justify-content: center; 59 | } 60 | 61 | .nav-dropdown-item { 62 | width: 100%; 63 | text-align: center; 64 | color: var(--teal); 65 | } 66 | 67 | .nav-links { 68 | height: inherit; 69 | -webkit-box-flex: 1; 70 | -ms-flex: 1; 71 | flex: 1; 72 | display: -webkit-box; 73 | display: -ms-flexbox; 74 | display: flex; 75 | -webkit-box-pack: center; 76 | -ms-flex-pack: center; 77 | justify-content: center; 78 | } 79 | 80 | .dropdown-menu { 81 | -webkit-transition: max-height 1s; 82 | -o-transition: max-height 1s; 83 | transition: max-height 1s; 84 | } 85 | 86 | .cursor-pointer { 87 | cursor: pointer; 88 | } 89 | 90 | nav ul a { 91 | font-size: 1.4em; 92 | } 93 | 94 | .active-link a { 95 | color: #1de9b6; 96 | background-color: #004d40; 97 | text-decoration: underline; 98 | -webkit-transition: -webkit-transform 0.5s ease-out; 99 | transition: -webkit-transform 0.5s ease-out; 100 | -o-transition: transform 0.5s ease-out; 101 | transition: transform 0.5s ease-out; 102 | transition: transform 0.5s ease-out, -webkit-transform 0.5s ease-out; 103 | -webkit-transform: scale(1.25); 104 | -ms-transform: scale(1.25); 105 | transform: scale(1.25); 106 | } 107 | 108 | nav ul a:hover { 109 | background-color: #004d40; 110 | } 111 | 112 | .material-icons.nav-dropdown-btn { 113 | font-size: 3em; 114 | margin-right: 0.5em; 115 | cursor: pointer; 116 | } -------------------------------------------------------------------------------- /assets/elm/vendor/Phoenix/Presence.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Presence exposing (Presence, create, map, onChange, onJoins, onLeaves) 2 | 3 | {-| Presence is an extension for channels to support the Presence feature of Phoenix. 4 | 5 | 6 | # Definition 7 | 8 | @docs Presence 9 | 10 | 11 | # Helpers 12 | 13 | @docs init, withPayload, on, onJoin, onRequestJoin, onJoinError, onError, onDisconnect, onRejoin, onLeave, onLeaveError, withDebug, map 14 | @docs init, withPayload, on, onJoin, onRequestJoin, onJoinError, onError, onDisconnect, onRejoin, onLeave, onLeaveError, withDebug, withPresence, map 15 | 16 | -} 17 | 18 | import Dict exposing (Dict) 19 | import Json.Decode as JD exposing (Decoder, Value) 20 | 21 | 22 | -- Presence 23 | 24 | 25 | {-| Representation of a Presence configuration 26 | -} 27 | type alias Presence msg = 28 | PhoenixPresence msg 29 | 30 | 31 | type alias PhoenixPresence msg = 32 | { onChange : Maybe (Dict String (List Value) -> msg) 33 | , onJoins : Maybe (Dict String (List Value) -> msg) 34 | , onLeaves : Maybe (Dict String (List Value) -> msg) 35 | } 36 | 37 | 38 | {-| Create a Presence configuration 39 | -} 40 | create : PhoenixPresence msg 41 | create = 42 | { onChange = Nothing 43 | , onJoins = Nothing 44 | , onLeaves = Nothing 45 | } 46 | 47 | 48 | {-| This will be called each time the Presence state changes. The `Dict` contains as keys your presence keys and 49 | as values a list of the payloads you sent from the server. 50 | If you have on the elixir side `Presence.track(socket, user_name, %{online_at: now()})` 51 | then an example would be a Dict with 52 | 53 | { "user1": [{online_at: 1491493666123}] 54 | , "user2": [{online_at: 1491492646123}, {online_at: 1491492646624}] 55 | } 56 | 57 | -} 58 | onChange : (Dict String (List Value) -> msg) -> PhoenixPresence msg -> PhoenixPresence msg 59 | onChange func presence = 60 | { presence | onChange = Just func } 61 | 62 | 63 | {-| This will be called each time user some user joins. This callback is useful to have special events 64 | if a user joins. To obtain a list of all users use `onChange`. 65 | -} 66 | onJoins : (Dict String (List Value) -> msg) -> PhoenixPresence msg -> PhoenixPresence msg 67 | onJoins func presence = 68 | { presence | onJoins = Just func } 69 | 70 | 71 | {-| This will be called each time user some user leaves. This callback is useful to have special events 72 | if a user leaves. To obtain a list of all users use `onChange`. 73 | -} 74 | onLeaves : (Dict String (List Value) -> msg) -> PhoenixPresence msg -> PhoenixPresence msg 75 | onLeaves func presence = 76 | { presence | onLeaves = Just func } 77 | 78 | 79 | {-| Maps the callbacks 80 | -} 81 | map : (a -> b) -> PhoenixPresence a -> PhoenixPresence b 82 | map func pres = 83 | let 84 | f = 85 | Maybe.map ((<<) func) 86 | in 87 | { pres | onChange = f pres.onChange, onJoins = f pres.onJoins, onLeaves = f pres.onLeaves } 88 | -------------------------------------------------------------------------------- /assets/elm/src/Data/Room.elm: -------------------------------------------------------------------------------- 1 | module Data.Room exposing (..) 2 | 3 | import Data.Card as Card exposing (Card) 4 | import Data.Player as Player exposing (Player, TablePlayer, Username, tablePlayerDecoder, usernameDecoder) 5 | import Dict as Dict exposing (Dict) 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Decode.Pipeline exposing (decode, optional, required) 8 | 9 | 10 | type alias Room = 11 | { active : Maybe Username 12 | , currentBigBlind : Maybe Int 13 | , currentSmallBlind : Maybe Int 14 | , state : String 15 | , paid : ChipTracker 16 | , toCall : Int 17 | , players : List TablePlayer 18 | , chipRoll : ChipTracker 19 | , seating : List Seating 20 | , playerHands : List PlayerHand 21 | , round : ChipTracker 22 | , pot : Int 23 | , table : Table 24 | , leaving : List Username 25 | } 26 | 27 | 28 | type alias ChipTracker = 29 | Dict String Int 30 | 31 | 32 | type alias Seating = 33 | { name : Username 34 | , position : Int 35 | } 36 | 37 | 38 | type alias PlayerHand = 39 | { player : Username 40 | , hand : List Card 41 | } 42 | 43 | 44 | type alias Table = 45 | List Card 46 | 47 | 48 | decoder : Decoder Room 49 | decoder = 50 | decode Room 51 | |> required "active" (Decode.nullable usernameDecoder) 52 | |> required "current_big_blind" (Decode.nullable Decode.int) 53 | |> required "current_small_blind" (Decode.nullable Decode.int) 54 | |> required "state" Decode.string 55 | |> required "paid" chipTrackerDecoder 56 | |> required "to_call" Decode.int 57 | |> required "players" (Decode.list tablePlayerDecoder) 58 | |> required "chip_roll" chipTrackerDecoder 59 | |> required "seating" (Decode.list seatingDecoder) 60 | |> required "player_hands" (Decode.list playerHandDecoder) 61 | |> required "round" chipTrackerDecoder 62 | |> required "pot" Decode.int 63 | |> required "table" (Decode.list Card.decoder) 64 | |> optional "leaving" (Decode.list usernameDecoder) [] 65 | 66 | 67 | chipTrackerDecoder : Decoder ChipTracker 68 | chipTrackerDecoder = 69 | Decode.dict Decode.int 70 | 71 | 72 | seatingDecoder : Decoder Seating 73 | seatingDecoder = 74 | decode Seating 75 | |> required "name" usernameDecoder 76 | |> required "position" Decode.int 77 | 78 | 79 | playerHandDecoder : Decoder PlayerHand 80 | playerHandDecoder = 81 | decode PlayerHand 82 | |> required "player" usernameDecoder 83 | |> required "hand" (Decode.list Card.decoder) 84 | 85 | 86 | defaultRoom : Room 87 | defaultRoom = 88 | { active = Nothing 89 | , currentBigBlind = Nothing 90 | , currentSmallBlind = Nothing 91 | , state = "idle" 92 | , paid = Dict.empty 93 | , toCall = 0 94 | , players = [] 95 | , chipRoll = Dict.empty 96 | , seating = [] 97 | , playerHands = [] 98 | , round = Dict.empty 99 | , pot = 0 100 | , table = [] 101 | , leaving = [] 102 | } 103 | -------------------------------------------------------------------------------- /assets/elm/src/Widgets/PlayerToolbar.elm: -------------------------------------------------------------------------------- 1 | module Widgets.PlayerToolbar exposing (Config, view, viewMobile, viewMobileMenu) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (class) 5 | import Html.Events exposing (onClick) 6 | 7 | 8 | type alias Config msg = 9 | { joinLeaveMsg : msg 10 | , btnText : String 11 | , actionPressedMsg : msg 12 | , isActive : Bool 13 | , bankPressedMsg : msg 14 | , accountPressedMsg : msg 15 | , chatPressedMsg : msg 16 | , mobileToolbarPressed : msg 17 | , closeModalMsg : msg 18 | , isLeaving : Bool 19 | } 20 | 21 | 22 | view : Config msg -> Html msg 23 | view config = 24 | case config.isLeaving of 25 | True -> 26 | div [ class "controls-container controls-disabled" ] 27 | [ h4 [ class "controls-msg" ] 28 | [ text "You have left the game"] 29 | ] 30 | False -> 31 | div [ class "controls-container" ] 32 | [ li [ class "control-item" ] 33 | [ a [ onClick config.joinLeaveMsg ] [ text config.btnText ] ] 34 | , li [ class "control-item" ] 35 | [ viewActionBtn config ] 36 | , li [ class "control-item" ] 37 | [ a [ onClick config.accountPressedMsg ] [ text "Account" ] ] 38 | , li [ class "control-item" ] 39 | [ a [ onClick config.chatPressedMsg ] [ text "Chat" ] ] 40 | , li [ class "control-item" ] 41 | [ a [ onClick config.bankPressedMsg ] [ text "Bank" ] ] 42 | ] 43 | 44 | 45 | viewActionBtn : Config msg -> Html msg 46 | viewActionBtn config = 47 | if config.isActive then 48 | a [ onClick config.actionPressedMsg, class "control-active" ] [ text "Actions" ] 49 | else 50 | a [] [ text "" ] 51 | 52 | 53 | viewMobile : Config msg -> Html msg 54 | viewMobile config = 55 | div 56 | [ class "mobile-controls-container btn-floating btn-large white-text red waves-effect" 57 | , onClick config.mobileToolbarPressed 58 | ] 59 | [ i [ class "material-icons" ] [ text "add" ] ] 60 | 61 | 62 | viewMobileMenu : Config msg -> Html msg 63 | viewMobileMenu config = 64 | let 65 | baseClasses = 66 | "collection-item mobile-menu-item green-text" 67 | in 68 | ul [ class "collection" ] 69 | [ li [ class baseClasses ] 70 | [ a [ onClick config.joinLeaveMsg ] [ text config.btnText ] 71 | ] 72 | , li [ class baseClasses ] 73 | [ a [ onClick config.accountPressedMsg ] [ text "Account" ] 74 | ] 75 | , li [ class baseClasses, onClick config.chatPressedMsg ] 76 | [ a [ onClick config.chatPressedMsg ] [ text "Chat" ] 77 | ] 78 | , li [ class baseClasses, onClick config.bankPressedMsg ] 79 | [ a [ onClick config.bankPressedMsg ] [ text "Bank" ] 80 | ] 81 | , i [ class "material-icons close-modal close-mobile-menu", onClick config.closeModalMsg ] 82 | [ text "close" ] 83 | ] -------------------------------------------------------------------------------- /assets/elm/src/Page/Room/SocketConfig.elm: -------------------------------------------------------------------------------- 1 | module Page.Room.SocketConfig exposing (..) 2 | 3 | import Data.AuthToken as AuthToken 4 | import Data.Player as Player exposing (Player) 5 | import Data.RoomPage as RoomPage exposing (RoomPage) 6 | import Data.Session as Session exposing (Session) 7 | import Json.Encode as Encode 8 | import Page.Room.Helpers as Helpers exposing (..) 9 | import Phoenix.Channel as Channel exposing (Channel) 10 | import Phoenix.Socket as Socket exposing (Socket) 11 | import Types.Room.Messages as Messages exposing (..) 12 | 13 | 14 | type alias Msg = 15 | RoomMsg 16 | 17 | 18 | type alias Model = 19 | RoomPage 20 | 21 | 22 | socket : Session -> String -> Socket Msg 23 | socket session socketUrl = 24 | let 25 | params = 26 | case session.player of 27 | Just player -> 28 | let 29 | token = 30 | AuthToken.authTokenToString player.token 31 | in 32 | [ ( "guardian_token", token ) ] 33 | 34 | Nothing -> 35 | [] 36 | in 37 | Socket.init socketUrl 38 | |> Socket.withParams params 39 | |> Socket.onOpen SocketOpened 40 | |> Socket.onClose (\_ -> SocketClosed) 41 | |> Socket.onAbnormalClose (\_ -> SocketClosedAbnormally) 42 | 43 | 44 | room : Model -> Channel Msg 45 | room model = 46 | Channel.init ("games:" ++ model.room) 47 | |> Channel.withPayload (Encode.object [ ( "type", Encode.string model.roomType ), ( "amount", Encode.int <| joinValToInt model.joinValue ) ]) 48 | |> roomChannel 49 | 50 | 51 | privateRoom : String -> Player -> Channel Msg 52 | privateRoom roomTitle player = 53 | Channel.init ("games:" ++ roomTitle) 54 | |> Channel.withPayload (Encode.object [ ( "type", Encode.string "private" ), ( "amount", Encode.int 0 ) ]) 55 | |> roomChannel 56 | 57 | 58 | roomChannel : Channel Msg -> Channel Msg 59 | roomChannel channel = 60 | channel 61 | |> Channel.onJoin (\_ -> JoinedChannel) 62 | |> Channel.onJoinError (\json -> JoinFailed json) 63 | |> Channel.onRejoin (\json -> Rejoined json) 64 | |> Channel.on "update" (\payload -> Update payload) 65 | |> Channel.on "game_started" (\payload -> GameStarted payload) 66 | |> Channel.on "winner_message" (\payload -> WinnerMessage payload) 67 | |> Channel.on "present_winning_hand" (\payload -> PresentWinningHand payload) 68 | |> Channel.on "clear_ui" Clear 69 | |> Channel.on "bank_info" (\payload -> SetBankInfo payload) 70 | |> Channel.on "new_chat_msg" (\payload -> NewChatMsg payload) 71 | |> Channel.on "new_message" (\payload -> NewMessage payload) 72 | 73 | 74 | playerInfoChannel : Player -> Channel Msg 75 | playerInfoChannel player = 76 | Channel.init ("players:" ++ Player.usernameToString player.username) 77 | |> Channel.onJoin (\_ -> ConnectedToPlayerChannel) 78 | |> Channel.on "chip_info" (\payload -> ChipInfo payload) 79 | 80 | 81 | 82 | -- Channel helpers 83 | 84 | 85 | subscribeToChannels : Player -> String -> String -> List (Channel Msg) 86 | subscribeToChannels player roomTitle roomType = 87 | case roomType of 88 | "private" -> 89 | [ playerInfoChannel player, privateRoom roomTitle player ] 90 | 91 | _ -> 92 | [ playerInfoChannel player ] 93 | -------------------------------------------------------------------------------- /assets/elm/vendor/Phoenix/Internal/Helpers.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Internal.Helpers exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Json.Decode as Decode exposing (Value) 5 | import Task exposing (Task) 6 | 7 | 8 | getIn : comparable -> comparable_ -> Dict comparable (Dict comparable_ value) -> Maybe value 9 | getIn a b dict = 10 | Dict.get a dict 11 | |> Maybe.andThen (Dict.get b) 12 | 13 | 14 | updateIn : comparable -> comparable_ -> (Maybe value -> Maybe value) -> Dict comparable (Dict comparable_ value) -> Dict comparable (Dict comparable_ value) 15 | updateIn a b update dict = 16 | let 17 | update_ maybeDict = 18 | let 19 | dict_ = 20 | Dict.update b update (Maybe.withDefault Dict.empty maybeDict) 21 | in 22 | if Dict.isEmpty dict_ then 23 | Nothing 24 | else 25 | Just dict_ 26 | in 27 | Dict.update a update_ dict 28 | 29 | 30 | insertIn : comparable -> comparable_ -> value -> Dict comparable (Dict comparable_ value) -> Dict comparable (Dict comparable_ value) 31 | insertIn a b value dict = 32 | let 33 | update_ maybeValue = 34 | case maybeValue of 35 | Nothing -> 36 | Just (Dict.singleton b value) 37 | 38 | Just dict_ -> 39 | Just (Dict.insert b value dict_) 40 | in 41 | Dict.update a update_ dict 42 | 43 | 44 | removeIn : comparable -> comparable_ -> Dict comparable (Dict comparable_ value) -> Dict comparable (Dict comparable_ value) 45 | removeIn a b dict = 46 | let 47 | remove maybeDict_ = 48 | case maybeDict_ of 49 | Nothing -> 50 | Nothing 51 | 52 | Just dict_ -> 53 | let 54 | newDict = 55 | Dict.remove b dict_ 56 | in 57 | if Dict.isEmpty newDict then 58 | Nothing 59 | else 60 | Just newDict 61 | in 62 | Dict.update a remove dict 63 | 64 | 65 | add : a -> Maybe (List a) -> Maybe (List a) 66 | add value maybeList = 67 | case maybeList of 68 | Nothing -> 69 | Just [ value ] 70 | 71 | Just list -> 72 | Just (value :: list) 73 | 74 | 75 | decodeReplyPayload : Value -> Maybe (Result Value Value) 76 | decodeReplyPayload value = 77 | let 78 | result = 79 | Decode.decodeValue (Decode.field "status" Decode.string |> Decode.andThen statusInfo) 80 | value 81 | in 82 | case result of 83 | Err err -> 84 | let 85 | _ = 86 | Debug.log err 87 | in 88 | Nothing 89 | 90 | Ok payload -> 91 | Just payload 92 | 93 | 94 | statusInfo : String -> Decode.Decoder (Result Value Value) 95 | statusInfo status = 96 | case status of 97 | "ok" -> 98 | Decode.map Ok (Decode.field "response" Decode.value) 99 | 100 | "error" -> 101 | Decode.map Err (Decode.field "response" Decode.value) 102 | 103 | _ -> 104 | Decode.fail (status ++ " is a not supported status") 105 | 106 | 107 | (&>) : Task b a -> Task b c -> Task b c 108 | (&>) t1 t2 = 109 | Task.andThen (\_ -> t2) t1 110 | 111 | 112 | (<&>) : Task b a -> (a -> Task b c) -> Task b c 113 | (<&>) x f = 114 | Task.andThen f x 115 | -------------------------------------------------------------------------------- /assets/elm/src/Views/Header.elm: -------------------------------------------------------------------------------- 1 | module Views.Header exposing (..) 2 | 3 | import Data.Player as Player exposing (Player) 4 | import Data.Session as Session exposing (Session) 5 | import Html exposing (..) 6 | import Html.Attributes exposing (..) 7 | import Html.Events exposing (Options, onClick) 8 | import Html.Lazy exposing (lazy2) 9 | import Route exposing (Route) 10 | import Types.Dropdowns as DropdownType exposing (DropdownItem, DropdownMsg, DropdownNavbarLink, OpenDropdown) 11 | import Types.Page as Page exposing (Page) 12 | import Util exposing (onClickStopPropagation) 13 | import Views.Helpers as Helpers exposing (ActivePage) 14 | import Widgets.Dropdown as Dropdown 15 | 16 | 17 | type alias NavDropdownInfo r = 18 | { r 19 | | openDropdown : OpenDropdown 20 | , selectedItem : DropdownItem 21 | } 22 | 23 | 24 | viewNavBarLinks : msg -> Session -> ActivePage -> List (Html msg) 25 | viewNavBarLinks msg session page = 26 | case session.player of 27 | Just player -> 28 | [ li [] [ a [ onClick msg ] [ text "Signout" ] ] 29 | , navBarLink (page == Helpers.Profile) 30 | (Route.Profile <| Player.usernameToString player.username) 31 | [ text "Profile" ] 32 | , navBarLink (page == Helpers.Rooms) Route.Rooms [ text "Rooms" ] 33 | ] 34 | 35 | Nothing -> 36 | [ navBarLink (page == Helpers.Login) Route.Login [ text "Login" ] 37 | , navBarLink (page == Helpers.Registration) Route.Register [ text "Signup" ] 38 | ] 39 | 40 | 41 | navBarLink : Bool -> Route -> List (Html msg) -> Html msg 42 | navBarLink isActive route linkContent = 43 | li [ classList [ ( "active-link", isActive ) ] ] 44 | [ a [ Route.href route ] linkContent ] 45 | 46 | 47 | activePageFrom : Page -> ActivePage 48 | activePageFrom page = 49 | case page of 50 | Page.Login _ -> 51 | Helpers.Login 52 | 53 | Page.Register _ -> 54 | Helpers.Registration 55 | 56 | Page.Home _ -> 57 | Helpers.Home 58 | 59 | Page.Room _ -> 60 | Helpers.Room 61 | 62 | Page.Rooms _ -> 63 | Helpers.Rooms 64 | 65 | Page.Profile _ -> 66 | Helpers.Profile 67 | 68 | _ -> 69 | Helpers.Other 70 | 71 | 72 | 73 | -- NavDropdown -- 74 | 75 | 76 | navDropdownConfig : Dropdown.Config DropdownMsg 77 | navDropdownConfig = 78 | { topLevelHtml = 79 | i 80 | [ class "material-icons nav-dropdown-btn hide-on-large-only" 81 | , onClick (DropdownType.Toggle DropdownType.NavBarDropdown) 82 | ] 83 | [ text "reorder" ] 84 | , clickedMsg = DropdownType.Toggle DropdownType.NavBarDropdown 85 | , itemPickedMsg = DropdownType.NavItemPicked 86 | } 87 | 88 | 89 | navDropdownContext : NavDropdownInfo r -> Dropdown.Context 90 | navDropdownContext model = 91 | { selectedItem = model.selectedItem 92 | , isOpen = model.openDropdown == DropdownType.NavBarDropdown 93 | } 94 | 95 | 96 | navLinks : Session -> List DropdownNavbarLink 97 | navLinks session = 98 | case session.player of 99 | Just player -> 100 | [ DropdownType.Logout, DropdownType.Profile, DropdownType.Rooms ] 101 | 102 | Nothing -> 103 | [ DropdownType.Login, DropdownType.Register ] 104 | 105 | 106 | 107 | --This id should come from index.Html 108 | --You can use it to scroll to the top of the page (by ID) 109 | --when switching pages in the pagination sense 110 | 111 | 112 | bodyId : String 113 | bodyId = 114 | "page-body" 115 | -------------------------------------------------------------------------------- /assets/elm/src/Request/Player.elm: -------------------------------------------------------------------------------- 1 | module Request.Player exposing (..) 2 | 3 | import Data.AuthToken as AuthToken exposing (AuthToken, withAuthorization) 4 | import Data.PasswordReset as PasswordReset exposing (PasswordReset) 5 | import Data.Player as Player exposing (Player, Username, encodeUsername) 6 | import Http 7 | import HttpBuilder exposing (RequestBuilder, withExpect, withQueryParams) 8 | import Json.Decode as Decode 9 | import Json.Encode as Encode 10 | import Json.Encode.Extra as EncodeExtra 11 | import Ports 12 | 13 | 14 | storeSession : Player -> Cmd msg 15 | storeSession player = 16 | Player.encode player 17 | |> Encode.encode 0 18 | |> Just 19 | |> Ports.storeSession 20 | 21 | 22 | type alias Registration r = 23 | { r 24 | | username : String 25 | , password : String 26 | , email : String 27 | , firstName : String 28 | , lastName : String 29 | , blurb : String 30 | , apiUrl : String 31 | } 32 | 33 | 34 | 35 | type alias ResetPassword r = 36 | { r 37 | | resetToken : String 38 | , newPassword : String 39 | , apiUrl : String 40 | } 41 | 42 | 43 | login : { r | username : String, password : String, apiUrl : String } -> Http.Request Player 44 | login { username, password, apiUrl } = 45 | let 46 | player = 47 | Encode.object 48 | [ ( "username", Encode.string username ) 49 | , ( "password", Encode.string password ) 50 | ] 51 | 52 | body = 53 | Encode.object [ ( "player", player ) ] 54 | |> Http.jsonBody 55 | in 56 | Decode.field "player" Player.decoder 57 | |> Http.post (apiUrl ++ "/sessions") body 58 | 59 | 60 | facebookLogin : Encode.Value -> String -> Http.Request Player 61 | facebookLogin playerData apiUrl = 62 | Decode.field "player" Player.decoder 63 | |> Http.post (apiUrl ++ "/auth") (playerData |> Http.jsonBody) 64 | 65 | 66 | passwordReset : { r | email : String, apiUrl : String } -> Http.Request PasswordReset 67 | passwordReset data = 68 | let 69 | apiUrl = 70 | data.apiUrl 71 | 72 | body = 73 | Encode.object [ ( "email", Encode.string data.email ) ] 74 | |> Http.jsonBody 75 | in 76 | Decode.field "data" PasswordReset.decoder 77 | |> Http.post (apiUrl ++ "/forgot_password") body 78 | 79 | 80 | resetPassword : ResetPassword r -> Http.Request Player 81 | resetPassword data = 82 | let 83 | apiUrl = 84 | data.apiUrl 85 | 86 | body = 87 | Encode.object 88 | [ ( "password", Encode.string data.newPassword ) 89 | , ( "reset_token", Encode.string data.resetToken ) 90 | ] 91 | |> Http.jsonBody 92 | in 93 | Decode.field "player" Player.decoder 94 | |> Http.post (apiUrl ++ "/reset_password") body 95 | 96 | 97 | register : Registration r -> Http.Request Player 98 | register registration = 99 | let 100 | apiUrl = 101 | registration.apiUrl 102 | 103 | registrationAttrs = 104 | Encode.object 105 | [ ( "name", Encode.string registration.username ) 106 | , ( "email", Encode.string registration.email ) 107 | , ( "first_name", Encode.string registration.firstName ) 108 | , ( "last_name", Encode.string registration.lastName ) 109 | , ( "blurb", Encode.string registration.blurb ) 110 | , ( "password", Encode.string registration.password ) 111 | ] 112 | 113 | body = 114 | Encode.object [ ( "registration", registrationAttrs ) ] 115 | |> Http.jsonBody 116 | in 117 | Decode.field "player" Player.decoder 118 | |> Http.post (apiUrl ++ "/registrations") body 119 | -------------------------------------------------------------------------------- /assets/elm/vendor/Phoenix/Internal/Channel.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Internal.Channel exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Json.Decode as Decode exposing (Value) 5 | import Phoenix.Channel as Channel 6 | import Phoenix.Internal.Helpers as Helpers 7 | import Phoenix.Internal.Message as Message exposing (Message) 8 | import Phoenix.Internal.Presence as Presence exposing (PresenceState) 9 | 10 | 11 | {-| The current channel state. This is completely handled by the effect manager. 12 | -} 13 | type State 14 | = Closed 15 | | Joining 16 | | Joined 17 | | Errored 18 | | Disconnected 19 | 20 | 21 | type alias InternalChannel msg = 22 | { state : State, presenceState : PresenceState, channel : Channel.Channel msg } 23 | 24 | 25 | type alias Endpoint = 26 | String 27 | 28 | 29 | type alias Topic = 30 | String 31 | 32 | 33 | type alias Event = 34 | String 35 | 36 | 37 | map : (a -> b) -> InternalChannel a -> InternalChannel b 38 | map func { state, presenceState, channel } = 39 | InternalChannel state presenceState (Channel.map func channel) 40 | 41 | 42 | joinMessage : InternalChannel msg -> Message 43 | joinMessage { channel } = 44 | let 45 | base = 46 | Message.init channel.topic "phx_join" 47 | in 48 | case channel.payload of 49 | Nothing -> 50 | base 51 | 52 | Just payload_ -> 53 | Message.payload payload_ base 54 | 55 | 56 | leaveMessage : InternalChannel msg -> Message 57 | leaveMessage { channel } = 58 | Message.init channel.topic "phx_leave" 59 | 60 | 61 | 62 | -- GETTER, SETTER, UPDATER 63 | 64 | 65 | type alias InternalChannelsDict msg = 66 | Dict Endpoint (Dict Topic (InternalChannel msg)) 67 | 68 | 69 | get : Endpoint -> Topic -> InternalChannelsDict msg -> Maybe (InternalChannel msg) 70 | get endpoint topic channelsDict = 71 | Helpers.getIn endpoint topic channelsDict 72 | 73 | 74 | getState : Endpoint -> Topic -> InternalChannelsDict msg -> Maybe State 75 | getState endpoint topic channelsDict = 76 | get endpoint topic channelsDict 77 | |> Maybe.map (\{ state } -> state) 78 | 79 | 80 | {-| Inserts the state, identity if channel for given endpoint topic doesn_t exist 81 | -} 82 | insertState : Endpoint -> Topic -> State -> InternalChannelsDict msg -> InternalChannelsDict msg 83 | insertState endpoint topic state dict = 84 | let 85 | update = 86 | Maybe.map (updateState state) 87 | in 88 | Helpers.updateIn endpoint topic update dict 89 | 90 | 91 | updatePresenceState : PresenceState -> InternalChannel msg -> InternalChannel msg 92 | updatePresenceState presenceState internalChannel = 93 | -- TODO: debug? 94 | InternalChannel internalChannel.state presenceState internalChannel.channel 95 | 96 | 97 | updateState : State -> InternalChannel msg -> InternalChannel msg 98 | updateState state internalChannel = 99 | if internalChannel.channel.debug then 100 | let 101 | _ = 102 | case ( state, internalChannel.state ) of 103 | ( Closed, Closed ) -> 104 | state 105 | 106 | ( Joining, Joining ) -> 107 | state 108 | 109 | ( Joined, Joined ) -> 110 | state 111 | 112 | ( Errored, Errored ) -> 113 | state 114 | 115 | ( Disconnected, Disconnected ) -> 116 | state 117 | 118 | _ -> 119 | Debug.log ("Channel \"" ++ internalChannel.channel.topic ++ "\"") state 120 | in 121 | InternalChannel state internalChannel.presenceState internalChannel.channel 122 | else 123 | InternalChannel state internalChannel.presenceState internalChannel.channel 124 | 125 | 126 | updatePayload : Maybe Value -> InternalChannel msg -> InternalChannel msg 127 | updatePayload payload { state, presenceState, channel } = 128 | InternalChannel state presenceState { channel | payload = payload } 129 | 130 | 131 | updateOn : Dict Topic (Value -> msg) -> InternalChannel msg -> InternalChannel msg 132 | updateOn on { state, presenceState, channel } = 133 | InternalChannel state presenceState { channel | on = on } 134 | -------------------------------------------------------------------------------- /assets/elm/vendor/Phoenix/Internal/Presence.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Internal.Presence exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Json.Decode as JD exposing (Decoder, Value) 5 | 6 | 7 | type alias PresenceState = 8 | Dict PresenceKey PresenceStateMetaWrapper 9 | 10 | 11 | type alias PresenceDiff = 12 | { leaves : PresenceState 13 | , joins : PresenceState 14 | } 15 | 16 | 17 | type alias PresenceKey = 18 | String 19 | 20 | 21 | type alias PresenceStateMetaWrapper = 22 | { metas : List PresenceStateMetaValue } 23 | 24 | 25 | type alias PresenceStateMetaValue = 26 | { phx_ref : PresenceRef, payload : Value } 27 | 28 | 29 | type alias PresenceRef = 30 | String 31 | 32 | 33 | getPresenceState : PresenceState -> Dict String (List Value) 34 | getPresenceState presenceState = 35 | let 36 | getMetas { metas } = 37 | metas 38 | 39 | getPayload presenceKey presenceStateMetaWrapper = 40 | List.map .payload (getMetas presenceStateMetaWrapper) 41 | in 42 | Dict.map getPayload presenceState 43 | 44 | 45 | syncPresenceDiff : PresenceDiff -> PresenceState -> PresenceState 46 | syncPresenceDiff presenceDiff presenceState = 47 | let 48 | mergeJoins joins state = 49 | let 50 | mergeMetaWrappers joinMetaWrapper stateMetaWrapper = 51 | PresenceStateMetaWrapper (joinMetaWrapper.metas ++ stateMetaWrapper.metas) 52 | 53 | addedStep key joinMetaWrapper addedMetaWrappers = 54 | Dict.insert key joinMetaWrapper addedMetaWrappers 55 | 56 | retainedStep key joinMetaWrapper stateMetaWrapper addedMetaWrappers = 57 | Dict.insert key (mergeMetaWrappers joinMetaWrapper stateMetaWrapper) addedMetaWrappers 58 | 59 | unchangedStep key stateMetaWrapper addedMetaWrappers = 60 | Dict.insert key stateMetaWrapper addedMetaWrappers 61 | in 62 | Dict.merge addedStep retainedStep unchangedStep joins state Dict.empty 63 | 64 | mergeLeaves leaves state = 65 | let 66 | mergeMetaWrappers leaves stateKey stateMetaWrapper = 67 | case Dict.get stateKey leaves of 68 | Nothing -> 69 | stateMetaWrapper 70 | 71 | Just leaveMetaWrapper -> 72 | let 73 | leaveRefs = 74 | List.map .phx_ref leaveMetaWrapper.metas 75 | in 76 | stateMetaWrapper.metas 77 | |> List.filter 78 | (\metaValue -> 79 | not (List.any (\phx_ref -> metaValue.phx_ref == phx_ref) leaveRefs) 80 | ) 81 | |> PresenceStateMetaWrapper 82 | in 83 | state 84 | |> Dict.map (mergeMetaWrappers leaves) 85 | |> Dict.filter (\_ metaWrapper -> metaWrapper.metas /= []) 86 | in 87 | presenceState 88 | |> mergeJoins presenceDiff.joins 89 | |> mergeLeaves presenceDiff.leaves 90 | 91 | 92 | decodePresenceDiff : Value -> Result String PresenceDiff 93 | decodePresenceDiff payload = 94 | JD.decodeValue presenceDiffDecoder payload 95 | 96 | 97 | decodePresenceState : Value -> Result String PresenceState 98 | decodePresenceState payload = 99 | JD.decodeValue presenceStateDecoder payload 100 | 101 | 102 | presenceDiffDecoder : Decoder PresenceDiff 103 | presenceDiffDecoder = 104 | JD.map2 PresenceDiff 105 | (JD.field "leaves" <| presenceStateDecoder) 106 | (JD.field "joins" <| presenceStateDecoder) 107 | 108 | 109 | presenceStateDecoder : Decoder PresenceState 110 | presenceStateDecoder = 111 | JD.dict presenceStateMetaWrapperDecoder 112 | 113 | 114 | presenceStateMetaWrapperDecoder : Decoder PresenceStateMetaWrapper 115 | presenceStateMetaWrapperDecoder = 116 | JD.map PresenceStateMetaWrapper 117 | (JD.field "metas" <| JD.list presenceStateMetaValueDecoder) 118 | 119 | 120 | presenceStateMetaValueDecoder : Decoder PresenceStateMetaValue 121 | presenceStateMetaValueDecoder = 122 | let 123 | createFinalRecord phxRef payload = 124 | JD.succeed (PresenceStateMetaValue phxRef payload) 125 | 126 | decodeWithPhxRef phxRef = 127 | JD.andThen (createFinalRecord phxRef) JD.value 128 | in 129 | JD.andThen decodeWithPhxRef (JD.field "phx_ref" JD.string) 130 | -------------------------------------------------------------------------------- /assets/elm/src/Widgets/Pagination.elm: -------------------------------------------------------------------------------- 1 | module Widgets.Pagination exposing (Config, paginate) 2 | 3 | import Html as Html exposing (..) 4 | import Html.Attributes exposing (class) 5 | import Html.Events exposing (onClick) 6 | 7 | 8 | type alias Model r = 9 | { r 10 | | totalPages : Int 11 | , page : Int 12 | } 13 | 14 | 15 | type alias Config msg = 16 | { onClickMsg : String -> msg 17 | , linksToShow : Int 18 | } 19 | 20 | 21 | paginate : Model r -> Config msg -> Html msg 22 | paginate model config = 23 | let 24 | paginationHtml = 25 | case model.totalPages of 26 | 1 -> 27 | text "" 28 | 29 | _ -> 30 | ul [ class "pagination pagination-list" ] 31 | (List.map (\text -> viewPaginationItem model config text) (paginationText model config)) 32 | in 33 | paginationHtml 34 | 35 | 36 | viewPaginationItem : Model r -> Config msg -> String -> Html msg 37 | viewPaginationItem model config paginationText = 38 | let 39 | active = 40 | case String.toInt paginationText of 41 | Ok num -> 42 | if model.page == num then 43 | "active" 44 | else 45 | "" 46 | 47 | Err _ -> 48 | "" 49 | 50 | disabledStart = 51 | if model.page == 1 && paginationText == "keyboard_arrow_left" then 52 | "disabled-page-icon" 53 | else 54 | "" 55 | 56 | disabledEnd = 57 | if model.page == model.totalPages && paginationText == "keyboard_arrow_right" then 58 | "disabled-page-icon" 59 | else 60 | "" 61 | 62 | firstPage = 63 | if paginationText == "First" && model.page == 1 then 64 | "active" 65 | else 66 | "" 67 | 68 | lastPage = 69 | if paginationText == "Last" && model.page == model.totalPages then 70 | "active" 71 | else 72 | "" 73 | 74 | extraClasses = 75 | String.join " " [ active, disabledStart, disabledEnd, firstPage, lastPage ] 76 | 77 | eventAttrs = 78 | case paginationText of 79 | "keyboard_arrow_left" -> 80 | if disabledStart == "" then 81 | [ onClick (config.onClickMsg paginationText) ] 82 | else 83 | [] 84 | 85 | "keyboard_arrow_right" -> 86 | if disabledEnd == "" then 87 | [ onClick (config.onClickMsg paginationText) ] 88 | else 89 | [] 90 | 91 | _ -> 92 | [ onClick (config.onClickMsg paginationText) ] 93 | in 94 | li [ class (String.trim extraClasses) ] 95 | [ a eventAttrs (viewItemText paginationText) ] 96 | 97 | 98 | viewItemText : String -> List (Html msg) 99 | viewItemText paginationText = 100 | if List.member paginationText [ "keyboard_arrow_left", "keyboard_arrow_right" ] then 101 | [ i [ class "material-icons" ] [ text paginationText ] ] 102 | else 103 | [ text paginationText ] 104 | 105 | 106 | paginationText : Model r -> Config msg -> List String 107 | paginationText model config = 108 | let 109 | currentInterval = 110 | getPaginationIntervalFor model.page model.totalPages config.linksToShow 111 | in 112 | [ "keyboard_arrow_left" ] 113 | ++ [ if model.totalPages > config.linksToShow then 114 | "First" 115 | else 116 | "" 117 | ] 118 | ++ List.map (\num -> toString num) currentInterval 119 | ++ [ if model.totalPages > config.linksToShow then 120 | "Last" 121 | else 122 | "" 123 | ] 124 | ++ [ "keyboard_arrow_right" ] 125 | 126 | 127 | getPaginationIntervalFor : Int -> Int -> Int -> List Int 128 | getPaginationIntervalFor currentPage totalPages numLinksToShow = 129 | case ( totalPages <= numLinksToShow, totalPages <= currentPage, (currentPage + numLinksToShow) >= totalPages ) of 130 | ( True, _, _ ) -> 131 | List.range 1 totalPages 132 | 133 | ( _, True, _ ) -> 134 | List.range (currentPage - numLinksToShow) currentPage 135 | 136 | ( _, _, True ) -> 137 | List.range (totalPages - numLinksToShow) totalPages 138 | 139 | _ -> 140 | List.range currentPage (currentPage + numLinksToShow) 141 | -------------------------------------------------------------------------------- /assets/elm/src/Page/Home.elm: -------------------------------------------------------------------------------- 1 | module Page.Home exposing (..) 2 | 3 | import Data.Player as Player exposing (Player, usernameToString) 4 | import Data.Session as Session exposing (Session) 5 | import Html exposing (..) 6 | import Html.Attributes exposing (class, style) 7 | import Route as Route exposing (Route) 8 | 9 | 10 | -- MODEL -- 11 | 12 | 13 | type alias Model = 14 | { greeting : String } 15 | 16 | 17 | initialModel : Model 18 | initialModel = 19 | { greeting = "Welcome to PokerEx" } 20 | 21 | 22 | 23 | -- VIEW -- 24 | 25 | 26 | view : Session -> Model -> Html msg 27 | view session model = 28 | let 29 | personalizedGreeting = 30 | case session.player of 31 | Just player -> 32 | ", " ++ (usernameToString <| player.username) 33 | 34 | Nothing -> 35 | "!" 36 | in 37 | main_ [] 38 | [ div [ class "hero valign-wrapper" ] 39 | [ h1 [ class "welcome", style [ ( "text-align", "center" ) ] ] 40 | [ text (model.greeting ++ personalizedGreeting) ] 41 | , br [] [] 42 | , viewButton session.player 43 | ] 44 | , div [ class "row landing" ] 45 | [ div [ class "col s12 m12 l4 landing-item" ] 46 | [ div [ class "card teal darken-2 landing-item" ] 47 | [ div [ class "card-content white-text text-center" ] 48 | [ span [ class "card-title center-align" ] 49 | [ text "Create your own poker room and invite friends" ] 50 | , p [ class "medium-text" ] 51 | [ text """Sign up to create your own private poker rooms. Invite friends 52 | or send invitations to other PokerEx players. Once you make a 53 | private poker room, your game and table state will be kept alive 54 | until you decide to close the room.""" 55 | ] 56 | ] 57 | ] 58 | ] 59 | , div [ class "col s12 m12 l4 landing-item" ] 60 | [ div [ class "card cyan darken-3 landing-item" ] 61 | [ div [ class "card-content white-text text-center" ] 62 | [ span [ class "card-title center-align" ] 63 | [ text "What is PokerEx?" ] 64 | , p [ class "pop-text" ] 65 | [ text """PokerEx is an online poker environment 66 | designed to deliver an engaging, real-time 67 | experience.""" 68 | ] 69 | ] 70 | ] 71 | ] 72 | , div [ class "col s12 m12 l4 landing-item" ] 73 | [ div [ class "card indigo darken-4 landing-item" ] 74 | [ div [ class "card-content white-text text-center" ] 75 | [ span [ class "card-title center-align" ] 76 | [ text "Join our public tables for a quick round" ] 77 | , p [ class "medium-text" ] 78 | [ text """Once you join us on PokerEx, you can sit down and play at 79 | any one of our public rooms. Public rooms are a perfect fit 80 | if you are looking to play a short round or two and get on 81 | your way.""" 82 | ] 83 | ] 84 | ] 85 | ] 86 | ] 87 | ] 88 | 89 | 90 | viewButton : Maybe Player -> Html msg 91 | viewButton player = 92 | case player of 93 | Just player -> 94 | a [ class "waves-effect waves-light btn-large white-text margin-bottom-small", Route.href (Route.Profile (Player.usernameToString player.username)) ] 95 | [ i [ class "material-icons left large-text" ] 96 | [ text "account_box" ] 97 | , text "Go to your account" 98 | ] 99 | 100 | _ -> 101 | a [ class "waves-effect waves-light btn-large white-text margin-bottom-small", Route.href Route.Register ] 102 | [ i [ class "material-icons left large-text" ] 103 | [ text "add_circle" ] 104 | , text "Join PokerEx Now!" 105 | ] 106 | 107 | 108 | 109 | -- UPDATE -- 110 | 111 | 112 | type Msg 113 | = DoNothing -- There is no reason yet for a Msg that does anything here. 114 | 115 | 116 | type ExternalMsg 117 | = NoOp 118 | 119 | 120 | update : Msg -> Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 121 | update msg model = 122 | case msg of 123 | _ -> 124 | ( ( model, Cmd.none ), NoOp ) 125 | -------------------------------------------------------------------------------- /assets/elm/src/Data/Card.elm: -------------------------------------------------------------------------------- 1 | module Data.Card exposing (..) 2 | 3 | import Html exposing (Html, img) 4 | import Html.Attributes exposing (class, id, src) 5 | import Json.Decode as Decode exposing (Decoder) 6 | import Json.Decode.Pipeline as Pipeline exposing (decode, required) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | type alias Card = 11 | { rank : Rank 12 | , suit : Suit 13 | } 14 | 15 | 16 | type Suit 17 | = Hearts 18 | | Spades 19 | | Clubs 20 | | Diamonds 21 | | SuitError 22 | 23 | 24 | type Rank 25 | = Two 26 | | Three 27 | | Four 28 | | Five 29 | | Six 30 | | Seven 31 | | Eight 32 | | Nine 33 | | Ten 34 | | Jack 35 | | Queen 36 | | King 37 | | Ace 38 | | RankError 39 | 40 | 41 | stringToSuit : String -> Suit 42 | stringToSuit stringSuit = 43 | case stringSuit of 44 | "hearts" -> 45 | Hearts 46 | 47 | "diamonds" -> 48 | Diamonds 49 | 50 | "spades" -> 51 | Spades 52 | 53 | "clubs" -> 54 | Clubs 55 | 56 | _ -> 57 | SuitError 58 | 59 | 60 | suitToString : Card -> String 61 | suitToString card = 62 | case card.suit of 63 | Hearts -> 64 | "hearts" 65 | 66 | Diamonds -> 67 | "diamonds" 68 | 69 | Spades -> 70 | "spades" 71 | 72 | Clubs -> 73 | "clubs" 74 | 75 | _ -> 76 | "error" 77 | 78 | 79 | stringToRank : String -> Rank 80 | stringToRank stringRank = 81 | case stringRank of 82 | "two" -> 83 | Two 84 | 85 | "three" -> 86 | Three 87 | 88 | "four" -> 89 | Four 90 | 91 | "five" -> 92 | Five 93 | 94 | "six" -> 95 | Six 96 | 97 | "seven" -> 98 | Seven 99 | 100 | "eight" -> 101 | Eight 102 | 103 | "nine" -> 104 | Nine 105 | 106 | "ten" -> 107 | Ten 108 | 109 | "jack" -> 110 | Jack 111 | 112 | "queen" -> 113 | Queen 114 | 115 | "king" -> 116 | King 117 | 118 | "ace" -> 119 | Ace 120 | 121 | _ -> 122 | RankError 123 | 124 | 125 | rankToString : Card -> String 126 | rankToString card = 127 | case card.rank of 128 | Two -> 129 | "2" 130 | 131 | Three -> 132 | "3" 133 | 134 | Four -> 135 | "4" 136 | 137 | Five -> 138 | "5" 139 | 140 | Six -> 141 | "6" 142 | 143 | Seven -> 144 | "7" 145 | 146 | Eight -> 147 | "8" 148 | 149 | Nine -> 150 | "9" 151 | 152 | Ten -> 153 | "10" 154 | 155 | Jack -> 156 | "jack" 157 | 158 | Queen -> 159 | "queen" 160 | 161 | King -> 162 | "king" 163 | 164 | Ace -> 165 | "ace" 166 | 167 | _ -> 168 | "error" 169 | 170 | 171 | 172 | -- Needs to be changed in prod 173 | 174 | 175 | rootCardsAssetsUrl : String 176 | rootCardsAssetsUrl = 177 | "https://poker-ex.herokuapp.com/images/cards/" 178 | 179 | 180 | 181 | -- Card back also needs to be changed in prod 182 | 183 | 184 | sourceUrlForCardImage : Card -> String 185 | sourceUrlForCardImage card = 186 | case ( card.rank, card.suit ) of 187 | ( RankError, SuitError ) -> 188 | "https://poker-ex.herokuapp.com/images/card-back.svg.png" 189 | 190 | _ -> 191 | rootCardsAssetsUrl ++ rankToString card ++ "_of_" ++ suitToString card ++ ".svg" 192 | 193 | 194 | tableCardImageFor : Card -> Html msg 195 | tableCardImageFor card = 196 | img [ src (sourceUrlForCardImage card), id "table-card" ] [] 197 | 198 | 199 | playerHandCardImageFor : Int -> Card -> Html msg 200 | playerHandCardImageFor index card = 201 | case ( card.rank, card.suit ) of 202 | ( RankError, SuitError ) -> 203 | img 204 | [ src (sourceUrlForCardImage card) 205 | , id ("player-hand-card-" ++ toString index) 206 | , class "card-back" 207 | ] 208 | [] 209 | 210 | _ -> 211 | img [ src (sourceUrlForCardImage card), id ("player-hand-card-" ++ toString index) ] [] 212 | 213 | 214 | rankDecoder : Decoder Rank 215 | rankDecoder = 216 | Decode.string 217 | |> Decode.andThen (\str -> Decode.succeed (stringToRank str)) 218 | 219 | 220 | suitDecoder : Decoder Suit 221 | suitDecoder = 222 | Decode.string 223 | |> Decode.andThen (\str -> Decode.succeed (stringToSuit str)) 224 | 225 | 226 | decoder : Decoder Card 227 | decoder = 228 | decode Card 229 | |> required "rank" rankDecoder 230 | |> required "suit" suitDecoder 231 | -------------------------------------------------------------------------------- /assets/elm/src/Page/Register.elm: -------------------------------------------------------------------------------- 1 | module Page.Register exposing (..) 2 | 3 | import Data.Configuration exposing (Configuration) 4 | import Data.Player as Player exposing (Player) 5 | import Data.Session as Session exposing (Session) 6 | import Html exposing (..) 7 | import Html.Attributes exposing (..) 8 | import Html.Events exposing (..) 9 | import Http 10 | import Json.Decode as Decode exposing (Decoder, decodeString, field, string) 11 | import Json.Decode.Pipeline as Pipeline exposing (decode, optional) 12 | import Ports 13 | import Request.Player exposing (storeSession) 14 | import Route 15 | import Validate exposing (..) 16 | import Views.Form as Form 17 | import Widgets.FacebookLogin as FBLogin 18 | 19 | 20 | -- Model -- 21 | 22 | 23 | type alias Model = 24 | { errors : List Error 25 | , username : String 26 | , password : String 27 | , firstName : String 28 | , lastName : String 29 | , blurb : String 30 | , email : String 31 | , apiUrl : String 32 | } 33 | 34 | 35 | type alias Error = 36 | ( Field, String ) 37 | 38 | 39 | initialModel : Configuration -> Model 40 | initialModel envConfig = 41 | { errors = [] 42 | , username = "" 43 | , password = "" 44 | , firstName = "" 45 | , lastName = "" 46 | , blurb = "" 47 | , email = "" 48 | , apiUrl = envConfig.apiUrl 49 | } 50 | 51 | 52 | 53 | -- VIEW -- 54 | 55 | 56 | view : Session -> Model -> Html Msg 57 | view session model = 58 | div [ class "auth-page", style [ ( "text-align", "center" ) ] ] 59 | [ div [ class "auth-form card-panel z-depth-4 rounded" ] 60 | [ Form.viewErrors model.errors 61 | , viewForm model 62 | , FBLogin.viewFBLogin LoginWithFB 63 | ] 64 | ] 65 | 66 | 67 | viewForm : Model -> Html Msg 68 | viewForm model = 69 | Html.form [ onSubmit SubmitForm ] 70 | [ inputFor model.username "Username" Username "text" 71 | , inputFor model.password "Password" Password "password" 72 | , inputFor model.firstName "First Name" FirstName "text" 73 | , inputFor model.lastName "Last Name" LastName "text" 74 | , inputFor model.blurb "Message" Blurb "text" 75 | , inputFor model.email "Email" Email "text" 76 | , button [ class "btn blue waves-effect" ] 77 | [ text "Sign Up" ] 78 | ] 79 | 80 | 81 | inputFor : String -> String -> (String -> RegistrationAttr) -> String -> Html Msg 82 | inputFor val holder attr inputType = 83 | input 84 | [ placeholder holder 85 | , type_ inputType 86 | , value val 87 | , autocomplete False 88 | , onInput (\s -> Set (attr s)) 89 | ] 90 | [] 91 | 92 | 93 | 94 | -- UPDATE -- 95 | 96 | 97 | type Msg 98 | = SubmitForm 99 | | Set RegistrationAttr 100 | | RegistrationCompleted (Result Http.Error Player) 101 | | LoginWithFB 102 | 103 | 104 | type ExternalMsg 105 | = NoOp 106 | | SetPlayer Player 107 | 108 | 109 | type RegistrationAttr 110 | = Username String 111 | | Password String 112 | | FirstName String 113 | | LastName String 114 | | Blurb String 115 | | Email String 116 | 117 | 118 | type Field 119 | = Name 120 | | Pass 121 | | E_mail 122 | | Server 123 | 124 | 125 | validate : Model -> List Error 126 | validate = 127 | Validate.all 128 | [ .username >> ifBlank ( Name, "Username can't be blank." ) 129 | , .password >> ifBlank ( Pass, "Password can't be blank." ) 130 | , .email >> ifBlank ( E_mail, "Email can't be blank." ) 131 | ] 132 | 133 | 134 | update : Msg -> Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 135 | update msg model = 136 | case msg of 137 | SubmitForm -> 138 | case validate model of 139 | [] -> 140 | ( ( model, Http.send RegistrationCompleted (Request.Player.register model) ), NoOp ) 141 | 142 | errors -> 143 | ( ( { model | errors = errors }, Cmd.none ), NoOp ) 144 | 145 | Set (Username name) -> 146 | ( ( { model | username = name }, Cmd.none ), NoOp ) 147 | 148 | Set (Password password) -> 149 | ( ( { model | password = password }, Cmd.none ), NoOp ) 150 | 151 | Set (FirstName firstName) -> 152 | ( ( { model | firstName = firstName }, Cmd.none ), NoOp ) 153 | 154 | Set (LastName lastName) -> 155 | ( ( { model | lastName = lastName }, Cmd.none ), NoOp ) 156 | 157 | Set (Blurb blurb) -> 158 | ( ( { model | blurb = blurb }, Cmd.none ), NoOp ) 159 | 160 | Set (Email email) -> 161 | ( ( { model | email = email }, Cmd.none ), NoOp ) 162 | 163 | RegistrationCompleted (Err error) -> 164 | ( ( { model | errors = [ ( Server, toString error ) ] }, Cmd.none ), NoOp ) 165 | 166 | RegistrationCompleted (Ok player) -> 167 | ( ( model, Cmd.batch [ storeSession player, Route.modifyUrl Route.Home ] ), SetPlayer player ) 168 | 169 | LoginWithFB -> 170 | ( ( model, Ports.loginWithFB () ), NoOp ) 171 | -------------------------------------------------------------------------------- /assets/elm/src/Page/Login.elm: -------------------------------------------------------------------------------- 1 | module Page.Login exposing (ExternalMsg(..), Model, Msg, initialModel, update, view) 2 | 3 | import Data.Configuration exposing (Configuration) 4 | import Data.Player as Player exposing (Player) 5 | import Data.Session as Session exposing (Session) 6 | import Html exposing (..) 7 | import Html.Attributes exposing (..) 8 | import Html.Events exposing (..) 9 | import Http 10 | import Json.Decode as Decode exposing (Decoder, decodeString, field, string) 11 | import Json.Decode.Pipeline as Pipeline exposing (decode, optional) 12 | import Ports 13 | import Request.Player exposing (storeSession) 14 | import Route 15 | import Validate exposing (..) 16 | import Views.Form as Form 17 | import Widgets.FacebookLogin as FBLogin 18 | 19 | 20 | -- Model -- 21 | 22 | 23 | type alias Model = 24 | { errors : List Error 25 | , username : String 26 | , password : String 27 | , apiUrl : String 28 | } 29 | 30 | 31 | type alias Error = 32 | ( Field, String ) 33 | 34 | 35 | initialModel : Configuration -> Model 36 | initialModel envConfig = 37 | { errors = [] 38 | , username = "" 39 | , password = "" 40 | , apiUrl = envConfig.apiUrl 41 | } 42 | 43 | 44 | 45 | -- View -- 46 | 47 | 48 | view : Session -> Model -> Html Msg 49 | view session model = 50 | div [ class "auth-page", style [ ( "text-align", "center" ) ] ] 51 | [ div [ class "auth-form card-panel z-depth-4 rounded" ] 52 | [ Form.viewErrors model.errors 53 | , viewForm model 54 | , FBLogin.viewFBLogin LoginWithFb 55 | , br [] [] 56 | , div [ class "forgot-password-link grey-text", onClick GoToForgotPassword ] [ text "Forgot password?" ] 57 | ] 58 | ] 59 | 60 | 61 | viewForm : Model -> Html Msg 62 | viewForm model = 63 | Html.form [ onSubmit SubmitForm ] 64 | [ input 65 | [ placeholder "Username" 66 | , type_ "text" 67 | , value model.username 68 | , onInput SetUsername 69 | , autocomplete False 70 | ] 71 | [] 72 | , input 73 | [ placeholder "Password" 74 | , type_ "password" 75 | , value model.password 76 | , onInput SetPassword 77 | , autocomplete False 78 | ] 79 | [] 80 | , button [ class "btn waves-effect blue" ] 81 | [ text "Login" ] 82 | ] 83 | 84 | 85 | 86 | -- Update -- 87 | 88 | 89 | type Msg 90 | = SubmitForm 91 | | SetUsername String 92 | | SetPassword String 93 | | LoginCompleted (Result Http.Error Player) 94 | | LoginWithFb 95 | | GoToForgotPassword 96 | 97 | 98 | type ExternalMsg 99 | = NoOp 100 | | SetPlayer Player 101 | 102 | 103 | update : Msg -> Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 104 | update msg model = 105 | case msg of 106 | SubmitForm -> 107 | case validate model of 108 | [] -> 109 | ( ( { model | errors = [] }, Http.send LoginCompleted (Request.Player.login model) ), NoOp ) 110 | 111 | errors -> 112 | ( ( { model | errors = errors }, Cmd.none ), NoOp ) 113 | 114 | SetUsername name -> 115 | ( ( { model | username = name }, Cmd.none ), NoOp ) 116 | 117 | SetPassword pass -> 118 | ( ( { model | password = pass }, Cmd.none ), NoOp ) 119 | 120 | LoginCompleted (Err error) -> 121 | let 122 | errorMessages = 123 | case error of 124 | Http.BadStatus response -> 125 | response.body 126 | |> decodeString (field "errors" errorsDecoder) 127 | |> Result.withDefault [] 128 | 129 | _ -> 130 | [ "unable to process login" ] 131 | in 132 | ( ( { model | errors = List.map (\errorMessage -> ( Form, errorMessage )) errorMessages }, Cmd.none ), NoOp ) 133 | 134 | LoginCompleted (Ok player) -> 135 | ( ( model, Cmd.batch [ storeSession player, Route.modifyUrl Route.Home ] ), SetPlayer player ) 136 | 137 | LoginWithFb -> 138 | ( ( model, Ports.loginWithFB () ), NoOp ) 139 | 140 | GoToForgotPassword -> 141 | ( ( model, Route.modifyUrl Route.ForgotPassword ), NoOp ) 142 | 143 | 144 | 145 | -- VALIDATION -- 146 | 147 | 148 | type Field 149 | = Form 150 | | Username 151 | | Password 152 | 153 | 154 | validate : Model -> List Error 155 | validate = 156 | Validate.all 157 | [ .username >> ifBlank ( Username, "Username can't be blank." ) 158 | , .password >> ifBlank ( Password, "Password can't be blank." ) 159 | ] 160 | 161 | 162 | errorsDecoder : Decoder (List String) 163 | errorsDecoder = 164 | decode (\username password -> List.concat [ username, password ]) 165 | |> optionalError "username" 166 | |> optionalError "password" 167 | 168 | 169 | optionalError : String -> Decoder (List String -> a) -> Decoder a 170 | optionalError fieldName = 171 | let 172 | errorToString errorMessage = 173 | String.join " " [ fieldName, errorMessage ] 174 | in 175 | optional fieldName (Decode.list (Decode.map errorToString string)) [] 176 | -------------------------------------------------------------------------------- /assets/elm/src/Page/ResetPassword.elm: -------------------------------------------------------------------------------- 1 | module Page.ResetPassword exposing (..) 2 | 3 | import Data.Configuration as Configuration exposing (Configuration) 4 | import Data.Player as Player exposing (Player) 5 | import Html exposing (..) 6 | import Html.Attributes exposing (..) 7 | import Html.Events exposing (onClick, onInput, onSubmit) 8 | import Http 9 | import Request.Player as Request 10 | import Route 11 | import Time exposing (Time) 12 | import Widgets.Toast as Toast 13 | 14 | 15 | type alias Model = 16 | { resetToken : String 17 | , newPassword : String 18 | , errors : List String 19 | , messages : List String 20 | , apiUrl : String 21 | } 22 | 23 | 24 | type Msg 25 | = SetPassword String 26 | | SubmitForm 27 | | ClearMessage Time 28 | | ClearError Time 29 | | ServerResult (Result Http.Error Player) 30 | 31 | 32 | type ExternalMsg 33 | = NoOp 34 | | SetPlayer Player 35 | 36 | 37 | initialModel : String -> Configuration -> Model 38 | initialModel resetToken envConfig = 39 | { resetToken = resetToken 40 | , newPassword = "" 41 | , errors = [] 42 | , messages = [] 43 | , apiUrl = envConfig.apiUrl 44 | } 45 | 46 | 47 | view : Model -> Html Msg 48 | view model = 49 | div 50 | [] 51 | [ h1 [ class "page-title teal-text", style [ ( "text-align", "center" ) ] ] [ text "Reset Password" ] 52 | , Toast.viewMessages model 53 | , div [ class "auth-page", style [ ( "text-align", "center" ) ] ] 54 | [ div [ class "auth-form card-panel z-depth-4 rounded" ] 55 | [ viewForm model ] 56 | ] 57 | ] 58 | 59 | 60 | viewForm : Model -> Html Msg 61 | viewForm model = 62 | Html.form [ onSubmit SubmitForm ] 63 | [ input 64 | [ placeholder "Enter your new password" 65 | , type_ "password" 66 | , value model.newPassword 67 | , onInput SetPassword 68 | ] 69 | [] 70 | , button [ class "btn waves-effect blue", type_ "button", onClick SubmitForm ] 71 | [ text "Reset Password" ] 72 | ] 73 | 74 | 75 | update : Msg -> Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 76 | update msg model = 77 | case msg of 78 | SetPassword password -> 79 | handleSetPassword model password 80 | 81 | SubmitForm -> 82 | handleSubmitForm model 83 | 84 | ClearError _ -> 85 | handleClearError model 86 | 87 | ClearMessage _ -> 88 | handleClearMessage model 89 | 90 | ServerResult result -> 91 | handleServerResult model result 92 | 93 | 94 | handleSetPassword : Model -> String -> ( ( Model, Cmd Msg ), ExternalMsg ) 95 | handleSetPassword model password = 96 | ( ( { model | newPassword = password }, Cmd.none ), NoOp ) 97 | 98 | 99 | handleSubmitForm : Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 100 | handleSubmitForm model = 101 | let 102 | ( newModel, cmd ) = 103 | case String.length model.newPassword >= 6 of 104 | True -> 105 | ( model, Http.send ServerResult (Request.resetPassword model) ) 106 | 107 | False -> 108 | ( { model | errors = "Password must be at least 6 characters" :: model.errors }, Cmd.none ) 109 | in 110 | ( ( newModel, cmd ), NoOp ) 111 | 112 | 113 | handleClearMessage : Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 114 | handleClearMessage model = 115 | case List.tail model.messages of 116 | Just newList -> 117 | ( ( { model | messages = newList }, Cmd.none ), NoOp ) 118 | 119 | Nothing -> 120 | ( ( { model | messages = [] }, Cmd.none ), NoOp ) 121 | 122 | 123 | handleClearError : Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 124 | handleClearError model = 125 | case List.tail model.errors of 126 | Just newList -> 127 | ( ( { model | errors = newList }, Cmd.none ), NoOp ) 128 | 129 | Nothing -> 130 | ( ( { model | errors = [] }, Cmd.none ), NoOp ) 131 | 132 | 133 | handleServerResult : Model -> Result Http.Error Player -> ( ( Model, Cmd Msg ), ExternalMsg ) 134 | handleServerResult model result = 135 | case result of 136 | Ok player -> 137 | ( ( model, Cmd.batch [ Request.storeSession player, Route.modifyUrl Route.Home ] ), SetPlayer player ) 138 | 139 | Err reason -> 140 | let 141 | errorMessage = 142 | "Your request failed. Your reset token may have expired, or you may have an invalid token." 143 | ++ " Try initiating a new password reset request from the login page and try again." 144 | in 145 | ( ( { model | errors = errorMessage :: model.errors }, Cmd.none ), NoOp ) 146 | 147 | 148 | 149 | -- SUBSCRIPTIONS -- 150 | 151 | 152 | subscriptions : Model -> Sub Msg 153 | subscriptions model = 154 | let 155 | clearMessages = 156 | case model.messages of 157 | [] -> 158 | Sub.none 159 | 160 | _ -> 161 | Time.every 5000 ClearMessage 162 | 163 | clearErrors = 164 | case model.errors of 165 | [] -> 166 | Sub.none 167 | 168 | _ -> 169 | Time.every 5000 ClearError 170 | in 171 | Sub.batch [ clearErrors, clearMessages ] 172 | -------------------------------------------------------------------------------- /assets/elm/vendor/Phoenix/Internal/Socket.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Internal.Socket exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Phoenix.Internal.Message as Message exposing (Message) 5 | import Phoenix.Socket as Socket 6 | import Process 7 | import String 8 | import Task exposing (Task) 9 | import WebSocket.LowLevel as WS 10 | 11 | 12 | type alias Endpoint = 13 | String 14 | 15 | 16 | type alias Ref = 17 | Int 18 | 19 | 20 | {-| The underlying low-level InternalSocket connection. 21 | -} 22 | type Connection 23 | = Closed 24 | | Opening Int Process.Id 25 | | Connected WS.WebSocket Int 26 | 27 | 28 | type alias InternalSocket msg = 29 | { connection : Connection, socket : Socket.Socket msg } 30 | 31 | 32 | internalSocket : Socket.Socket msg -> InternalSocket msg 33 | internalSocket socket = 34 | { connection = Closed, socket = socket } 35 | 36 | 37 | 38 | -- INSPECT 39 | 40 | 41 | isOpening : InternalSocket msg -> Bool 42 | isOpening internalSocket = 43 | case internalSocket.connection of 44 | Opening _ _ -> 45 | True 46 | 47 | _ -> 48 | False 49 | 50 | 51 | 52 | -- MODIFY 53 | 54 | 55 | opening : Int -> Process.Id -> InternalSocket msg -> InternalSocket msg 56 | opening backoff pid socket = 57 | { socket | connection = Opening backoff pid } 58 | 59 | 60 | connected : WS.WebSocket -> InternalSocket msg -> InternalSocket msg 61 | connected ws socket = 62 | { socket | connection = Connected ws 0 } 63 | 64 | 65 | increaseRef : InternalSocket msg -> InternalSocket msg 66 | increaseRef socket = 67 | case socket.connection of 68 | Connected ws ref -> 69 | { socket | connection = Connected ws (ref + 1) } 70 | 71 | _ -> 72 | socket 73 | 74 | 75 | update : Socket.Socket msg -> InternalSocket msg -> InternalSocket msg 76 | update nextSocket { connection, socket } = 77 | let 78 | updatedConnection = 79 | if nextSocket.params /= socket.params then 80 | resetBackoff connection 81 | else 82 | connection 83 | in 84 | InternalSocket updatedConnection nextSocket 85 | 86 | 87 | resetBackoff : Connection -> Connection 88 | resetBackoff connection = 89 | case connection of 90 | Opening backoff pid -> 91 | Opening 0 pid 92 | 93 | _ -> 94 | connection 95 | 96 | 97 | 98 | -- PUSH 99 | 100 | 101 | push : Message -> InternalSocket msg -> Task x (Maybe Ref) 102 | push message { connection, socket } = 103 | case connection of 104 | Connected ws ref -> 105 | let 106 | message_ = 107 | if socket.debug then 108 | Debug.log "Send" (Message.ref ref message) 109 | else 110 | Message.ref ref message 111 | in 112 | WS.send ws (Message.encode message_) 113 | |> Task.map 114 | (\maybeBadSend -> 115 | case maybeBadSend of 116 | Nothing -> 117 | Just ref 118 | 119 | Just badSend -> 120 | if socket.debug then 121 | let 122 | _ = 123 | Debug.log "BadSend" badSend 124 | in 125 | Nothing 126 | else 127 | Nothing 128 | ) 129 | 130 | _ -> 131 | Task.succeed Nothing 132 | 133 | 134 | 135 | -- OPEN CONNECTIONs 136 | 137 | 138 | open : InternalSocket msg -> WS.Settings -> Task WS.BadOpen WS.WebSocket 139 | open { socket } settings = 140 | let 141 | query = 142 | socket.params 143 | |> List.map (\( key, val ) -> key ++ "=" ++ val) 144 | |> String.join "&" 145 | 146 | url = 147 | if String.contains "?" socket.endpoint then 148 | socket.endpoint ++ "&" ++ query 149 | else 150 | socket.endpoint ++ "?" ++ query 151 | in 152 | WS.open url settings 153 | 154 | 155 | after : Float -> Task x () 156 | after backoff = 157 | if backoff < 1 then 158 | Task.succeed () 159 | else 160 | Process.sleep backoff 161 | 162 | 163 | 164 | -- CLOSE CONNECTIONS 165 | 166 | 167 | close : InternalSocket msg -> Task x () 168 | close { connection } = 169 | case connection of 170 | Opening _ pid -> 171 | Process.kill pid 172 | 173 | Connected socket _ -> 174 | WS.close socket 175 | 176 | Closed -> 177 | Task.succeed () 178 | 179 | 180 | 181 | -- HELPERS 182 | 183 | 184 | get : Endpoint -> Dict Endpoint (InternalSocket msg) -> Maybe (InternalSocket msg) 185 | get endpoint dict = 186 | Dict.get endpoint dict 187 | 188 | 189 | getRef : Endpoint -> Dict Endpoint (InternalSocket msg) -> Maybe Ref 190 | getRef endpoint dict = 191 | get endpoint dict |> Maybe.andThen ref 192 | 193 | 194 | ref : InternalSocket msg -> Maybe Ref 195 | ref { connection } = 196 | case connection of 197 | Connected _ ref_ -> 198 | Just ref_ 199 | 200 | _ -> 201 | Nothing 202 | 203 | 204 | debugLogMessage : InternalSocket msg -> a -> a 205 | debugLogMessage { socket } msg = 206 | if socket.debug then 207 | Debug.log "Received" msg 208 | else 209 | msg 210 | -------------------------------------------------------------------------------- /assets/elm/src/Page/ForgotPassword.elm: -------------------------------------------------------------------------------- 1 | module Page.ForgotPassword exposing (..) 2 | 3 | import Data.Configuration exposing (Configuration) 4 | import Data.PasswordReset as PasswordReset exposing (PasswordReset) 5 | import Html exposing (..) 6 | import Html.Attributes exposing (..) 7 | import Html.Events exposing (onClick, onInput, onSubmit) 8 | import Http 9 | import Request.Player as Request 10 | import Time exposing (Time) 11 | import Validate exposing (..) 12 | import Widgets.Toast as Toast 13 | 14 | 15 | type alias Model = 16 | { email : String 17 | , errors : List String 18 | , messages : List String 19 | , apiUrl : String 20 | } 21 | 22 | 23 | type Msg 24 | = SendPasswordReset 25 | | SubmitForm 26 | | SetEmail String 27 | | ServerResult (Result Http.Error PasswordReset) 28 | | ClearMessage Time 29 | | ClearError Time 30 | 31 | 32 | type ExternalMsg 33 | = NoOp 34 | 35 | 36 | initialModel : Configuration -> Model 37 | initialModel envConfig = 38 | { email = "", errors = [], messages = [], apiUrl = envConfig.apiUrl } 39 | 40 | 41 | view : Model -> Html Msg 42 | view model = 43 | div 44 | [] 45 | [ h1 [ class "page-title teal-text", style [ ( "text-align", "center" ) ] ] [ text "Forgot Password" ] 46 | , Toast.viewMessages model 47 | , div [ class "auth-page", style [ ( "text-align", "center" ) ] ] 48 | [ div [ class "auth-form card-panel z-depth-4 rounded" ] 49 | [ viewForm model ] 50 | ] 51 | ] 52 | 53 | 54 | viewForm : Model -> Html Msg 55 | viewForm model = 56 | Html.form [ onSubmit SubmitForm ] 57 | [ input 58 | [ placeholder "Enter your email" 59 | , type_ "text" 60 | , value model.email 61 | , onInput SetEmail 62 | ] 63 | [] 64 | , button [ class "btn waves-effect blue", type_ "button", onClick SubmitForm ] 65 | [ text "Send Password Reset" ] 66 | ] 67 | 68 | 69 | update : Msg -> Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 70 | update msg model = 71 | case msg of 72 | SendPasswordReset -> 73 | handleSendPasswordReset model 74 | 75 | SubmitForm -> 76 | handleSubmitForm model 77 | 78 | SetEmail email -> 79 | handleSetEmail model email 80 | 81 | ServerResult result -> 82 | handleServerResult model result 83 | 84 | ClearMessage _ -> 85 | handleClearMessage model 86 | 87 | ClearError _ -> 88 | handleClearError model 89 | 90 | 91 | handleSendPasswordReset : Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 92 | handleSendPasswordReset model = 93 | ( ( model, Cmd.none ), NoOp ) 94 | 95 | 96 | handleSubmitForm : Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 97 | handleSubmitForm model = 98 | case validate model of 99 | [] -> 100 | ( ( { model | errors = [] }, Http.send ServerResult (Request.passwordReset model) ), NoOp ) 101 | 102 | errors -> 103 | ( ( { model | errors = errors }, Cmd.none ), NoOp ) 104 | 105 | 106 | handleSetEmail : Model -> String -> ( ( Model, Cmd Msg ), ExternalMsg ) 107 | handleSetEmail model email = 108 | ( ( { model | email = email }, Cmd.none ), NoOp ) 109 | 110 | 111 | handleClearMessage : Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 112 | handleClearMessage model = 113 | case List.tail model.messages of 114 | Just newList -> 115 | ( ( { model | messages = newList }, Cmd.none ), NoOp ) 116 | 117 | Nothing -> 118 | ( ( { model | messages = [] }, Cmd.none ), NoOp ) 119 | 120 | 121 | handleClearError : Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 122 | handleClearError model = 123 | case List.tail model.errors of 124 | Just newList -> 125 | ( ( { model | errors = newList }, Cmd.none ), NoOp ) 126 | 127 | Nothing -> 128 | ( ( { model | errors = [] }, Cmd.none ), NoOp ) 129 | 130 | 131 | handleServerResult : Model -> Result Http.Error PasswordReset -> ( ( Model, Cmd Msg ), ExternalMsg ) 132 | handleServerResult model result = 133 | case result of 134 | Ok passwordReset -> 135 | let 136 | newModel = 137 | case passwordReset.type_ of 138 | PasswordReset.Success -> 139 | { model | messages = passwordReset.message :: model.messages } 140 | 141 | PasswordReset.Error -> 142 | { model | errors = passwordReset.message :: model.errors } 143 | in 144 | ( ( newModel, Cmd.none ), NoOp ) 145 | 146 | Err _ -> 147 | let 148 | errorMessage = 149 | "Something went wrong with your request. Please submit the form and try again" 150 | in 151 | ( ( { model | errors = errorMessage :: model.errors }, Cmd.none ), NoOp ) 152 | 153 | 154 | 155 | -- VALIDATION -- 156 | 157 | 158 | validate : Model -> List String 159 | validate = 160 | Validate.all 161 | [ .email >> ifBlank "You must enter an email" ] 162 | 163 | 164 | 165 | -- SUBSCRIPTIONS -- 166 | 167 | 168 | subscriptions : Model -> Sub Msg 169 | subscriptions model = 170 | let 171 | clearMessages = 172 | case model.messages of 173 | [] -> 174 | Sub.none 175 | 176 | _ -> 177 | Time.every 5000 ClearMessage 178 | 179 | clearErrors = 180 | case model.errors of 181 | [] -> 182 | Sub.none 183 | 184 | _ -> 185 | Time.every 5000 ClearError 186 | in 187 | Sub.batch [ clearErrors, clearMessages ] 188 | -------------------------------------------------------------------------------- /assets/elm/vendor/Phoenix/Socket.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Socket 2 | exposing 3 | ( AbnormalClose 4 | , Socket 5 | , heartbeatIntervallSeconds 6 | , init 7 | , map 8 | , onAbnormalClose 9 | , onClose 10 | , onNormalClose 11 | , onOpen 12 | , reconnectTimer 13 | , withDebug 14 | , withParams 15 | , withoutHeartbeat 16 | ) 17 | 18 | {-| A socket declares to which endpoint a socket connection should be established. 19 | 20 | 21 | # Definition 22 | 23 | @docs Socket, AbnormalClose 24 | 25 | 26 | # Helpers 27 | 28 | @docs init, withParams, heartbeatIntervallSeconds, withoutHeartbeat, reconnectTimer, withDebug, onAbnormalClose, onNormalClose, onOpen, onClose, map 29 | 30 | -} 31 | 32 | import Time exposing (Time) 33 | 34 | 35 | {-| Representation of a Socket connection 36 | -} 37 | type alias Socket msg = 38 | PhoenixSocket msg 39 | 40 | 41 | {-| Representation of an Abnormal close. 42 | -} 43 | type alias AbnormalClose = 44 | { reconnectAttempt : Int 45 | , reconnectWait : Time 46 | } 47 | 48 | 49 | type alias PhoenixSocket msg = 50 | { endpoint : String 51 | , params : List ( String, String ) 52 | , heartbeatIntervall : Time 53 | , withoutHeartbeat : Bool 54 | , reconnectTimer : Int -> Float 55 | , debug : Bool 56 | , onOpen : Maybe msg 57 | , onClose : Maybe ({ code : Int, reason : String, wasClean : Bool } -> msg) 58 | , onAbnormalClose : Maybe (AbnormalClose -> msg) 59 | , onNormalClose : Maybe msg 60 | } 61 | 62 | 63 | {-| Initialize a Socket connection with an endpoint. 64 | 65 | init "ws://localhost:4000/socket/websocket" 66 | 67 | -} 68 | init : String -> Socket msg 69 | init endpoint = 70 | { endpoint = endpoint 71 | , params = [] 72 | , heartbeatIntervall = 30 * Time.second 73 | , withoutHeartbeat = False 74 | , reconnectTimer = defaultReconnectTimer 75 | , debug = False 76 | , onOpen = Nothing 77 | , onClose = Nothing 78 | , onAbnormalClose = Nothing 79 | , onNormalClose = Nothing 80 | } 81 | 82 | 83 | {-| Attach parameters to the socket connecton. You can use this to do authentication on the socket level. This will be the first argument (as a map) in your `connect/2` callback on the server. 84 | 85 | init "ws://localhost:4000/socket/websocket" 86 | |> withParams [("token", "GYMXZwXzKFzfxyGntVkYt7uAJnscVnFJ")] 87 | 88 | -} 89 | withParams : List ( String, String ) -> Socket msg -> Socket msg 90 | withParams params socket = 91 | { socket | params = params } 92 | 93 | 94 | {-| The client regularly sends a heartbeat to the server. With this function you can specify the intervall in which the heartbeats are send. By default it_s 30 seconds. 95 | 96 | init "ws://localhost:4000/socket/websocket" 97 | |> heartbeatIntervallSeconds 60 98 | 99 | -} 100 | heartbeatIntervallSeconds : Int -> Socket msg -> Socket msg 101 | heartbeatIntervallSeconds intervall socket = 102 | { socket | heartbeatIntervall = toFloat intervall * Time.second } 103 | 104 | 105 | {-| The client regularly sends a heartbeat to the sever. With this function you can disable the heartbeat. 106 | 107 | init "ws://localhost:4000/socket/websocket" 108 | |> withoutHeartbeat 109 | 110 | -} 111 | withoutHeartbeat : Socket msg -> Socket msg 112 | withoutHeartbeat socket = 113 | { socket | withoutHeartbeat = True } 114 | 115 | 116 | {-| The effect manager will try to establish a socket connection. If it fails it will try again with a specified backoff. By default the effect manager will use the following exponential backoff strategy: 117 | 118 | defaultReconnectTimer failedAttempts = 119 | if backoff < 1 then 120 | 0 121 | else 122 | toFloat (10 * 2 ^ failedAttempts) 123 | 124 | With this function you can specify a custom strategy. 125 | 126 | -} 127 | reconnectTimer : (Int -> Time) -> Socket msg -> Socket msg 128 | reconnectTimer timerFunc socket = 129 | { socket | reconnectTimer = timerFunc } 130 | 131 | 132 | {-| Enable debug logs for the socket. Every incoming and outgoing message will be printed. 133 | -} 134 | withDebug : Socket msg -> Socket msg 135 | withDebug socket = 136 | { socket | debug = True } 137 | 138 | 139 | {-| Set a callback which will be called if the socket connection gets open. 140 | -} 141 | onOpen : msg -> Socket msg -> Socket msg 142 | onOpen onOpen socket = 143 | { socket | onOpen = Just onOpen } 144 | 145 | 146 | {-| Set a callback which will be called if the socket connection got closed abnormal, i.e., if the server declined the socket authentication. So this callback is useful for updating query params like access tokens. 147 | 148 | type Msg = 149 | RefreshAccessToken | ... 150 | 151 | init "ws://localhost:4000/socket/websocket" 152 | |> withParams [ ( "accessToken", "abc123" ) ] 153 | |> onAbnormalClose RefreshAccessToken 154 | 155 | -} 156 | onAbnormalClose : (AbnormalClose -> msg) -> Socket msg -> Socket msg 157 | onAbnormalClose onAbnormalClose_ socket = 158 | { socket | onAbnormalClose = Just onAbnormalClose_ } 159 | 160 | 161 | {-| Set a callback which will be called if the socket connection got closed normal. Useful if you have to do some additional clean up. 162 | -} 163 | onNormalClose : msg -> Socket msg -> Socket msg 164 | onNormalClose onNormalClose_ socket = 165 | { socket | onNormalClose = Just onNormalClose_ } 166 | 167 | 168 | {-| Set a callback which will be called if the socket connection got closed. You can learn more about the code [here](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent). 169 | -} 170 | onClose : ({ code : Int, reason : String, wasClean : Bool } -> msg) -> Socket msg -> Socket msg 171 | onClose onClose_ socket = 172 | { socket | onClose = Just onClose_ } 173 | 174 | 175 | defaultReconnectTimer : Int -> Time 176 | defaultReconnectTimer failedAttempts = 177 | if failedAttempts < 1 then 178 | 0 179 | else 180 | toFloat (min 15000 (1000 * failedAttempts)) 181 | 182 | 183 | {-| Composes each callback with the function `a -> b`. 184 | -} 185 | map : (a -> b) -> Socket a -> Socket b 186 | map func socket = 187 | { socket 188 | | onOpen = Maybe.map func socket.onOpen 189 | , onClose = Maybe.map ((<<) func) socket.onClose 190 | , onNormalClose = Maybe.map func socket.onNormalClose 191 | , onAbnormalClose = Maybe.map ((<<) func) socket.onAbnormalClose 192 | } 193 | -------------------------------------------------------------------------------- /assets/static/images/cards/2_of_spades.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | image/svg+xml 47 | 50 | 54 | 57 | 59 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 69 | 70 | 71 | 74 | 76 | 77 | 78 | 79 | 80 | 83 | 85 | 86 | 87 | 88 | 89 | 90 | 2 102 | 103 | 104 | 112 | 2 132 | 133 | 148 | -------------------------------------------------------------------------------- /assets/static/images/cards/3_of_spades.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | image/svg+xml 47 | 50 | 54 | 57 | 59 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 69 | 70 | 71 | 74 | 76 | 77 | 78 | 79 | 80 | 83 | 85 | 86 | 87 | 88 | 89 | 90 | 3 102 | 103 | 104 | 112 | 3 132 | 133 | 155 | -------------------------------------------------------------------------------- /assets/static/images/cards/4_of_spades.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | image/svg+xml 47 | 50 | 54 | 57 | 59 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 69 | 70 | 71 | 74 | 76 | 77 | 78 | 79 | 80 | 83 | 85 | 86 | 87 | 88 | 89 | 90 | 4 102 | 103 | 104 | 105 | 113 | 4 133 | 134 | 135 | 164 | -------------------------------------------------------------------------------- /assets/elm/src/Page/Room.elm: -------------------------------------------------------------------------------- 1 | module Page.Room exposing (..) 2 | 3 | import Data.Configuration exposing (Configuration) 4 | import Data.Player as Player exposing (Player) 5 | import Data.Room as Room exposing (Room) 6 | import Data.RoomPage as RoomPage exposing (RoomPage) 7 | import Data.Session as Session exposing (Session) 8 | import Html exposing (Html, div, text) 9 | import Html.Attributes exposing (..) 10 | import Html.Events exposing (onClick, onInput, onSubmit) 11 | import Page.Room.Helpers as Helpers exposing (..) 12 | import Page.Room.SocketConfig as SocketConfig exposing (..) 13 | import Page.Room.UpdateHelpers as Updaters exposing (..) 14 | import Page.Room.ViewHelpers exposing (..) 15 | import Phoenix 16 | import Time exposing (Time) 17 | import Types.Room.Messages as Messages exposing (..) 18 | import Types.Room.ModalState as ModalState exposing (..) 19 | import Views.Actions as Actions 20 | import Widgets.PlayerToolbar as PlayerToolbar 21 | 22 | 23 | -- Types 24 | 25 | 26 | type alias Msg = 27 | RoomMsg 28 | 29 | 30 | type alias ExternalMsg = 31 | RoomExternalMsg 32 | 33 | 34 | type alias Model = 35 | RoomPage 36 | 37 | 38 | type alias MessageType = 39 | RoomMessageType 40 | 41 | 42 | 43 | -- INITIALIZATION -- 44 | 45 | 46 | initialModel : 47 | Player 48 | -> String 49 | -> String 50 | -> Configuration 51 | -> RoomPage 52 | initialModel player roomTitle roomType envConfig = 53 | { room = formatTitle roomTitle 54 | , roomModel = Room.defaultRoom 55 | , roomType = roomType 56 | , roomMessages = [] 57 | , players = [] 58 | , player = player 59 | , joined = False 60 | , joinValue = "0" 61 | , channelSubscriptions = subscribeToChannels player (formatTitle roomTitle) roomType 62 | , modalRendered = Closed 63 | , errorMessages = [] 64 | , raiseAmount = 0 65 | , raiseInterval = 5 66 | , chipsAvailable = player.chips 67 | , chipsAvailableForJoin = player.chips 68 | , addAmount = 0 69 | , chat = [] 70 | , currentChatMsg = "" 71 | , socketUrl = envConfig.socketUrl 72 | , apiUrl = envConfig.apiUrl 73 | } 74 | 75 | 76 | 77 | -- VIEW -- 78 | 79 | 80 | view : Session -> Model -> Html Msg 81 | view session model = 82 | let 83 | mobileToolbarView = 84 | case model.modalRendered of 85 | BottomModalOpen _ -> 86 | text "" 87 | 88 | _ -> 89 | PlayerToolbar.viewMobile (toolbarConfig model) 90 | in 91 | div [ class "room-container" ] 92 | [ div [ class "table-container" ] 93 | (viewTableCenter model.roomModel :: viewTableCards model.roomModel :: viewPlayers session model) 94 | , PlayerToolbar.view (toolbarConfig model) 95 | , mobileToolbarView 96 | , Actions.viewMobile (actionsViewConfig model) 97 | , maybeViewModal model 98 | , viewMessages model 99 | ] 100 | 101 | 102 | 103 | -- UPDATE -- 104 | 105 | 106 | update : Msg -> Model -> ( ( Model, Cmd Msg ), ExternalMsg ) 107 | update msg model = 108 | case msg of 109 | JoinedChannel -> 110 | handleJoinedChannel model 111 | 112 | Join -> 113 | handleJoin model 114 | 115 | JoinFailed value -> 116 | handleJoinFailed model value 117 | 118 | SetJoinValue amount -> 119 | handleSetJoinValue model amount 120 | 121 | Update payload -> 122 | handleUpdate model payload 123 | 124 | GameStarted payload -> 125 | handleUpdate model payload 126 | 127 | WinnerMessage payload -> 128 | handleWinnerMessage model payload 129 | 130 | PresentWinningHand payload -> 131 | handlePresentWinningHand model payload 132 | 133 | SetBankInfo payload -> 134 | handleSetBankInfo model payload 135 | 136 | Clear _ -> 137 | handleClear model 138 | 139 | ConnectedToPlayerChannel -> 140 | ( ( model, Cmd.none ), NoOp ) 141 | 142 | ChipInfo payload -> 143 | handleChipInfo model payload 144 | 145 | ActionPressed -> 146 | ( ( { model | modalRendered = BottomModalOpen Actions }, Cmd.none ), NoOp ) 147 | 148 | ActionMsg action val -> 149 | handleActionMsg model action val 150 | 151 | NewChatMsg value -> 152 | handleNewChatMsg model value 153 | 154 | NewMessage value -> 155 | handleNewMessage model value 156 | 157 | BankPressed -> 158 | handleBankPressed model 159 | 160 | AccountPressed -> 161 | handleAccountPressed model 162 | 163 | MobileToolbarPressed -> 164 | ( ( { model | modalRendered = BottomModalOpen MobileMenu }, Cmd.none ), NoOp ) 165 | 166 | ChatPressed -> 167 | ( ( { model | modalRendered = BottomModalOpen ModalState.Chat }, Cmd.none ), NoOp ) 168 | 169 | CloseRaiseModal -> 170 | ( ( { model | modalRendered = BottomModalOpen Actions }, Cmd.none ), NoOp ) 171 | 172 | IncreaseRaise amount -> 173 | handleIncreaseRaise model amount 174 | 175 | DecreaseRaise amount -> 176 | handleDecreaseRaise model amount 177 | 178 | SetRaise amount -> 179 | handleSetRaise model amount 180 | 181 | SetAddAmount amount -> 182 | handleSetAddAmount model amount 183 | 184 | SetChatMsg message -> 185 | handleSetChatMsg model message 186 | 187 | SubmitChat -> 188 | handleSubmitChat model 189 | 190 | SocketOpened -> 191 | ( ( model, Cmd.none ), NoOp ) 192 | 193 | SocketClosed -> 194 | ( ( model, Cmd.none ), NoOp ) 195 | 196 | SocketClosedAbnormally -> 197 | ( ( model, Cmd.none ), NoOp ) 198 | 199 | Rejoined _ -> 200 | handleRejoin model 201 | 202 | JoinRoom player -> 203 | handleJoinRoom model player 204 | 205 | Blur -> 206 | ( ( { model | modalRendered = Closed }, Cmd.none ), NoOp ) 207 | 208 | OpenRaisePressed -> 209 | ( ( { model | modalRendered = RaiseModalOpen }, Cmd.none ), NoOp ) 210 | 211 | ClearErrorMessage _ -> 212 | clearErrorMessage model 213 | 214 | ClearRoomMessage _ -> 215 | clearRoomMessage model 216 | 217 | ClearWinningHandModal _ -> 218 | clearWinningHandModal model 219 | 220 | CloseWinningHandModal -> 221 | clearWinningHandModal model 222 | 223 | CloseModal -> 224 | ( ( { model | modalRendered = Closed }, Cmd.none ), NoOp ) 225 | 226 | LeaveRoom player -> 227 | handleLeaveRoom player model 228 | 229 | 230 | 231 | -- SUBSCRIPTIONS -- 232 | 233 | 234 | subscriptions : Model -> Session -> Sub Msg 235 | subscriptions model session = 236 | let 237 | phoenixSubscriptions = 238 | [ Phoenix.connect (socket session model.socketUrl) model.channelSubscriptions ] 239 | 240 | withBlur = 241 | case model.modalRendered of 242 | WinningHandModal _ -> 243 | Time.every 5000 ClearWinningHandModal 244 | 245 | _ -> 246 | Sub.none 247 | 248 | withClearError = 249 | case model.errorMessages of 250 | [] -> 251 | Sub.none 252 | 253 | _ -> 254 | Time.every 3000 ClearErrorMessage 255 | 256 | withClearRoomMessage = 257 | case model.roomMessages of 258 | [] -> 259 | Sub.none 260 | 261 | _ -> 262 | Time.every 3000 ClearRoomMessage 263 | in 264 | Sub.batch (phoenixSubscriptions ++ [ withBlur, withClearError, withClearRoomMessage ]) 265 | -------------------------------------------------------------------------------- /assets/elm/src/Views/Actions.elm: -------------------------------------------------------------------------------- 1 | module Views.Actions exposing (..) 2 | 3 | import Data.Player as Player exposing (Username) 4 | import Html exposing (..) 5 | import Html.Attributes as Attrs exposing (..) 6 | import Html.Events exposing (on, onClick, targetValue) 7 | import Json.Decode as Decode 8 | import Json.Encode as Encode exposing (Value) 9 | 10 | 11 | type Action 12 | = Raise 13 | | Fold 14 | | Check 15 | | Call 16 | 17 | 18 | type Platform 19 | = Mobile 20 | | Desktop 21 | 22 | 23 | type alias ActionsModel msg = 24 | { isActive : Bool 25 | , chips : Int 26 | , paidInRound : Int 27 | , toCall : Int 28 | , player : Username 29 | , actionMsg : String -> Value -> msg 30 | , openRaiseMsg : msg 31 | , closeModalMsg : msg 32 | , closeRaiseMsg : msg 33 | , increaseRaiseMsg : Int -> msg 34 | , decreaseRaiseMsg : Int -> msg 35 | , setRaiseMsg : String -> msg 36 | , raiseAmount : Int 37 | , raiseMax : Int 38 | , raiseMin : Int 39 | , raiseInterval : Int 40 | } 41 | 42 | 43 | view : ActionsModel msg -> Html msg 44 | view actionsModel = 45 | div [ class "actions-container" ] 46 | [ span [ class "modal-header red-text" ] [ text "Actions" ] 47 | , div [ class "row" ] 48 | [ div [ class "col s6" ] 49 | [ viewActionBtn actionsModel Call Desktop ] 50 | , div [ class "col s6" ] 51 | [ viewActionBtn actionsModel Raise Desktop ] 52 | ] 53 | , div [ class "row" ] 54 | [ div [ class "col s6" ] 55 | [ viewActionBtn actionsModel Check Desktop ] 56 | , div [ class "col s6" ] 57 | [ viewActionBtn actionsModel Fold Desktop ] 58 | , i [ class "material-icons close-modal", onClick actionsModel.closeModalMsg ] 59 | [ text "close" ] 60 | ] 61 | ] 62 | 63 | 64 | viewMobile : ActionsModel msg -> Html msg 65 | viewMobile actionsModel = 66 | div [ class "actions-container-mobile" ] 67 | [ viewActionBtn actionsModel Call Mobile 68 | , viewActionBtn actionsModel Raise Mobile 69 | , viewActionBtn actionsModel Check Mobile 70 | , viewActionBtn actionsModel Fold Mobile 71 | ] 72 | 73 | 74 | encodeUsernamePayload : Username -> Value 75 | encodeUsernamePayload username = 76 | Encode.object 77 | [ ( "player", Player.encodeUsername username ) ] 78 | 79 | 80 | actionBtnClass : String -> Platform -> String 81 | actionBtnClass color platform = 82 | let 83 | extraClasses = 84 | case platform of 85 | Desktop -> 86 | "" 87 | 88 | Mobile -> 89 | "btn-floating" 90 | in 91 | "waves-effect waves-effect-light btn white-text " ++ color ++ " " ++ extraClasses 92 | 93 | 94 | viewActionBtn : ActionsModel msg -> Action -> Platform -> Html msg 95 | viewActionBtn actionsModel action platform = 96 | let 97 | ( color, message, btnText ) = 98 | getAttributesFor actionsModel action 99 | in 100 | case canCallAction actionsModel action of 101 | True -> 102 | a [ class (actionBtnClass color platform), onClick message ] [ childrenForBtn btnText platform actionsModel.toCall ] 103 | 104 | False -> 105 | text "" 106 | 107 | 108 | childrenForBtn : String -> Platform -> Int -> Html msg 109 | childrenForBtn btnText platform toCall = 110 | case platform of 111 | Desktop -> 112 | text btnText 113 | 114 | Mobile -> 115 | getIconForMobileBtn btnText toCall 116 | 117 | 118 | getIconForMobileBtn : String -> Int -> Html msg 119 | getIconForMobileBtn btnText toCall = 120 | case toCall of 121 | 0 -> 122 | case btnText of 123 | "Raise" -> 124 | i [ class "material-icons" ] [ text "arrow_upward" ] 125 | 126 | "Check" -> 127 | i [ class "material-icons" ] [ text "check" ] 128 | 129 | _ -> 130 | text "" 131 | 132 | callAmount -> 133 | case btnText of 134 | "Raise" -> 135 | i [ class "material-icons" ] [ text "arrow_upward" ] 136 | 137 | "Check" -> 138 | i [ class "material-icons" ] [ text "check" ] 139 | 140 | "Fold" -> 141 | i [ class "material-icons" ] [ text "block" ] 142 | 143 | _ -> 144 | text <| toString callAmount 145 | 146 | 147 | raiseContent : ActionsModel msg -> Html msg 148 | raiseContent actionsModel = 149 | div [ class "raise-modal" ] 150 | [ div [ class "raise-modal-header" ] 151 | [ h3 [ class "red-text" ] 152 | [ text <| "Raise to " ++ toString actionsModel.raiseAmount ] 153 | ] 154 | , input 155 | [ type_ "number" 156 | , Attrs.min <| toString actionsModel.raiseMin 157 | , Attrs.max <| toString actionsModel.raiseMax 158 | , Attrs.placeholder "Enter your raise here" 159 | , onRangeChange actionsModel.setRaiseMsg 160 | ] 161 | [] 162 | , div [ class "raise-buttons-container" ] 163 | [ a [ onClick <| actionsModel.decreaseRaiseMsg actionsModel.raiseInterval, class "btn btn-large waves-effect raise-btn" ] 164 | [ i [ class "large material-icons" ] [ text "remove" ] ] 165 | , a [ class "btn btn-large green waves-effect white-text raise-submit", onClick <| raiseMsgWith actionsModel "action_raise" ] 166 | [ text "Raise" ] 167 | , a [ onClick <| actionsModel.increaseRaiseMsg actionsModel.raiseInterval, class "btn btn-large waves-effect raise-btn" ] 168 | [ i [ class "large material-icons" ] [ text "add" ] ] 169 | ] 170 | , div [ class "raise-modal-close-row" ] 171 | [ i [ class "material-icons close-modal", onClick actionsModel.closeRaiseMsg ] [ text "close" ] ] 172 | ] 173 | 174 | 175 | actionMsgWith : ActionsModel msg -> String -> msg 176 | actionMsgWith actionsModel pushMessage = 177 | actionsModel.actionMsg pushMessage (encodeUsernamePayload actionsModel.player) 178 | 179 | 180 | raiseMsgWith : ActionsModel msg -> String -> msg 181 | raiseMsgWith actionsModel pushMessage = 182 | actionsModel.actionMsg pushMessage 183 | (Encode.object <| 184 | [ ( "player", Player.encodeUsername actionsModel.player ) 185 | , ( "amount", Encode.int actionsModel.raiseAmount ) 186 | ] 187 | ) 188 | 189 | 190 | canCallAction : ActionsModel msg -> Action -> Bool 191 | canCallAction actionsModel action = 192 | if actionsModel.isActive == False then 193 | False 194 | else 195 | case action of 196 | Call -> 197 | actionsModel.paidInRound < actionsModel.toCall 198 | 199 | Check -> 200 | actionsModel.paidInRound == actionsModel.toCall 201 | 202 | Raise -> 203 | actionsModel.chips > actionsModel.toCall 204 | 205 | Fold -> 206 | actionsModel.paidInRound < actionsModel.toCall 207 | 208 | 209 | onRangeChange : (String -> msg) -> Attribute msg 210 | onRangeChange msg = 211 | on "change" <| Decode.map msg targetValue 212 | 213 | 214 | getAttributesFor : ActionsModel msg -> Action -> ( String, msg, String ) 215 | getAttributesFor actionsModel action = 216 | case action of 217 | Call -> 218 | ( "blue darken-3", actionMsgWith actionsModel "action_call", "Call " ++ toString actionsModel.toCall ) 219 | 220 | Raise -> 221 | ( "red darken-3", actionsModel.openRaiseMsg, "Raise" ) 222 | 223 | Check -> 224 | ( "green accent-3", actionMsgWith actionsModel "action_check", "Check" ) 225 | 226 | Fold -> 227 | ( "teal darken-4", actionMsgWith actionsModel "action_fold", "Fold" ) 228 | --------------------------------------------------------------------------------