works on parsing ApiResponse
This commit is contained in:
306
src/Main.elm
306
src/Main.elm
@@ -9,7 +9,8 @@ import Html.Attributes exposing (..)
|
|||||||
import Html.Events exposing (..)
|
import Html.Events exposing (..)
|
||||||
import Svg.Attributes
|
import Svg.Attributes
|
||||||
import Http
|
import Http
|
||||||
import Json.Decode exposing (Decoder, field, list, string, int)
|
import Json.Decode exposing (Decoder, field, list, string, int, succeed)
|
||||||
|
import Json.Encode as E
|
||||||
import Url.Parser as P exposing (Parser, (</>), oneOf, s)
|
import Url.Parser as P exposing (Parser, (</>), oneOf, s)
|
||||||
import Set exposing (Set)
|
import Set exposing (Set)
|
||||||
-- Main
|
-- Main
|
||||||
@@ -155,14 +156,26 @@ valueDecoder thenDecoder =
|
|||||||
|
|
||||||
-- UPDATE
|
-- UPDATE
|
||||||
|
|
||||||
|
type alias HttpResult a = (Result Http.Error a)
|
||||||
|
|
||||||
|
type alias ApiResponse =
|
||||||
|
{ value : Maybe String
|
||||||
|
, updates : Maybe (List DbUpdate)
|
||||||
|
, notification : Maybe String
|
||||||
|
, error : Maybe String
|
||||||
|
}
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= LinkClicked Browser.UrlRequest
|
= LinkClicked Browser.UrlRequest
|
||||||
| UrlChanged Url.Url
|
| UrlChanged Url.Url
|
||||||
| PlayerChanged Int
|
| PlayerChanged Int
|
||||||
| GotPlayer (Result Http.Error Player)
|
| GotPlayer (HttpResult Player)
|
||||||
| GotLoot ToChest (Result Http.Error Loot)
|
| GotLoot ToChest (HttpResult Loot)
|
||||||
| LootViewItemSwitched Int
|
| LootViewItemSwitched Int
|
||||||
| ModeSwitched (Maybe ViewMode)
|
| ModeSwitched (Maybe ViewMode)
|
||||||
|
| ConfirmAction
|
||||||
|
| UndoLastAction
|
||||||
|
| GotActionResult (HttpResult ApiResponse)
|
||||||
|
|
||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
update msg model =
|
update msg model =
|
||||||
@@ -233,29 +246,137 @@ update msg model =
|
|||||||
ModeSwitched newMode ->
|
ModeSwitched newMode ->
|
||||||
let
|
let
|
||||||
state = model.state
|
state = model.state
|
||||||
|
|
||||||
(nextMode, cmd) =
|
|
||||||
case newMode of
|
|
||||||
Nothing -> -- Cancel action
|
|
||||||
(Nothing, Cmd.none)
|
|
||||||
new ->
|
|
||||||
case new of
|
|
||||||
Just Confirm ->
|
|
||||||
-- Confirm action and exit
|
|
||||||
(Nothing, Cmd.none)
|
|
||||||
other ->
|
|
||||||
-- Enter mode
|
|
||||||
(new, Cmd.none)
|
|
||||||
|
|
||||||
in
|
in
|
||||||
( { model | state =
|
( { model | state =
|
||||||
{ state | activeMode = nextMode
|
{ state | activeMode = newMode
|
||||||
, selection = case nextMode of
|
, selection = case newMode of
|
||||||
Nothing -> Nothing
|
Nothing -> Nothing
|
||||||
Just Grab -> Just (Set.fromList [34, 38])
|
Just Grab -> Just (Set.fromList [34, 38])
|
||||||
Just others -> Just Set.empty
|
Just others -> Just Set.empty
|
||||||
}}
|
}}
|
||||||
, cmd)
|
, Cmd.none )
|
||||||
|
|
||||||
|
ConfirmAction ->
|
||||||
|
let
|
||||||
|
currentMode = model.state.activeMode
|
||||||
|
in
|
||||||
|
(model, sendRequest currentMode model)
|
||||||
|
|
||||||
|
UndoLastAction ->
|
||||||
|
let playerId = String.fromInt model.player.id
|
||||||
|
in
|
||||||
|
(model, Http.request
|
||||||
|
{ url = "http://localhost:8088/api/players/" ++ playerId ++"/events/last"
|
||||||
|
, method = "DELETE"
|
||||||
|
, headers = []
|
||||||
|
, body = Http.emptyBody
|
||||||
|
, expect = Http.expectJson GotActionResult apiResponseDecoder
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
})
|
||||||
|
|
||||||
|
GotActionResult response ->
|
||||||
|
case response of
|
||||||
|
Ok r -> (setError (Debug.toString r) model, Cmd.none)
|
||||||
|
Err r -> (setError (Debug.toString r) model, Cmd.none)
|
||||||
|
|
||||||
|
targetItemsFor : ViewMode -> Model -> List Item
|
||||||
|
targetItemsFor mode model =
|
||||||
|
case mode of
|
||||||
|
Add -> []
|
||||||
|
Buy -> Maybe.withDefault [] model.merchantItems
|
||||||
|
Sell ->Maybe.withDefault [] model.loot
|
||||||
|
Grab -> Maybe.withDefault [] model.groupLoot
|
||||||
|
|
||||||
|
buildPayload : ViewMode -> Model -> E.Value
|
||||||
|
buildPayload mode model =
|
||||||
|
let
|
||||||
|
items = targetItemsFor mode model
|
||||||
|
|> List.filter (itemInSelection model.state.selection)
|
||||||
|
in
|
||||||
|
case mode of
|
||||||
|
Buy -> E.object
|
||||||
|
[ ( "items", items |> E.list (\i -> E.list identity [E.int i.id, E.null]))
|
||||||
|
, ("global_mod", E.null )
|
||||||
|
]
|
||||||
|
Sell -> E.object
|
||||||
|
[ ( "items", items |> E.list (\i -> E.list identity [E.int i.id, E.null]))
|
||||||
|
, ("global_mod", E.null )
|
||||||
|
]
|
||||||
|
Grab -> E.object
|
||||||
|
[ ( "items", items |> E.list (\i -> E.int i.id))
|
||||||
|
, ("global_mod", E.null )
|
||||||
|
]
|
||||||
|
Add -> E.object
|
||||||
|
[ ( "items", items |> E.list (\i -> E.int i.id))
|
||||||
|
, ("global_mod", E.null )
|
||||||
|
]
|
||||||
|
|
||||||
|
type DbUpdate
|
||||||
|
= ItemRemoved Item
|
||||||
|
| ItemAdded Item
|
||||||
|
| WealthUpdated Wealth
|
||||||
|
| ClaimAdded ()
|
||||||
|
| ClaimRemoved ()
|
||||||
|
|
||||||
|
-- TODO: update server to produce better json
|
||||||
|
-- like an object with list of updates of the same type
|
||||||
|
-- { ItemRemoved : [..], Wealth : [ .. ], .. }
|
||||||
|
updatesDecoder : Decoder DbUpdate
|
||||||
|
updatesDecoder =
|
||||||
|
-- We expect one update but do not know it's kind
|
||||||
|
Json.Decode.oneOf
|
||||||
|
[ (field "ItemRemoved" (itemDecoder |> Json.Decode.andThen (\i -> succeed <| ItemRemoved i)))
|
||||||
|
, (field "ItemAdded" (itemDecoder |> Json.Decode.andThen (\i -> succeed <| ItemAdded i)))
|
||||||
|
, (field "Wealth" (wealthDecoder |> Json.Decode.andThen (\i -> succeed <| WealthUpdated i)))
|
||||||
|
, (field "ClaimRemoved" (succeed () |> Json.Decode.andThen (\i -> succeed <| ClaimRemoved i)))
|
||||||
|
, (field "ClaimAdded" (succeed () |> Json.Decode.andThen (\i -> succeed <| ClaimAdded i)))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
apiResponseDecoder : Decoder ApiResponse
|
||||||
|
apiResponseDecoder =
|
||||||
|
Json.Decode.map4 ApiResponse
|
||||||
|
(Json.Decode.maybe (field "value" string))
|
||||||
|
(Json.Decode.maybe (field "updates" (Json.Decode.list updatesDecoder)))
|
||||||
|
(Json.Decode.maybe (field "notification" string))
|
||||||
|
(Json.Decode.maybe (field "error" string))
|
||||||
|
|
||||||
|
|
||||||
|
sendRequest : Maybe ViewMode -> Model -> Cmd Msg
|
||||||
|
sendRequest activeMode model =
|
||||||
|
case activeMode of
|
||||||
|
Nothing -> Cmd.none
|
||||||
|
Just mode ->
|
||||||
|
let
|
||||||
|
(endpoint, method) = case mode of
|
||||||
|
Add ->
|
||||||
|
( "http://localhost:8088/api/players/" ++ (String.fromInt model.player.id) ++ "/loot"
|
||||||
|
, "POST"
|
||||||
|
)
|
||||||
|
Buy ->
|
||||||
|
( "http://localhost:8088/api/players/" ++ (String.fromInt model.player.id) ++ "/loot"
|
||||||
|
, "PUT"
|
||||||
|
)
|
||||||
|
Sell ->
|
||||||
|
( "http://localhost:8088/api/players/" ++ (String.fromInt model.player.id) ++ "/loot"
|
||||||
|
, "DELETE"
|
||||||
|
)
|
||||||
|
Grab ->
|
||||||
|
( "http://"
|
||||||
|
, "POST")
|
||||||
|
in
|
||||||
|
Http.request
|
||||||
|
{ method = method
|
||||||
|
, headers = []
|
||||||
|
, url = endpoint
|
||||||
|
, body = Http.jsonBody <| buildPayload mode model
|
||||||
|
, expect = Http.expectJson GotActionResult apiResponseDecoder
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- ERRORS
|
-- ERRORS
|
||||||
|
|
||||||
@@ -301,11 +422,18 @@ type ViewMode
|
|||||||
| Buy
|
| Buy
|
||||||
| Grab
|
| Grab
|
||||||
| Add
|
| Add
|
||||||
| Confirm -- Confirm action and exit mode
|
|
||||||
|
|
||||||
actionButton mode t icon color =
|
canSelectIn : ViewMode -> Bool
|
||||||
|
canSelectIn mode =
|
||||||
|
case mode of
|
||||||
|
Sell -> True
|
||||||
|
Buy -> True
|
||||||
|
Grab -> True
|
||||||
|
Add -> False
|
||||||
|
|
||||||
|
actionButton msg t icon color =
|
||||||
button [ class <| "button is-" ++ color
|
button [ class <| "button is-" ++ color
|
||||||
, onClick (ModeSwitched mode) ]
|
, onClick msg ]
|
||||||
[ span [ class "icon" ] [ i [ Svg.Attributes.class <| "fas fa-" ++ icon ] [] ]
|
[ span [ class "icon" ] [ i [ Svg.Attributes.class <| "fas fa-" ++ icon ] [] ]
|
||||||
, p [] [text t]
|
, p [] [text t]
|
||||||
]
|
]
|
||||||
@@ -313,6 +441,7 @@ actionButton mode t icon color =
|
|||||||
view : Model -> Browser.Document Msg
|
view : Model -> Browser.Document Msg
|
||||||
view model =
|
view model =
|
||||||
let
|
let
|
||||||
|
-- What do we show inside the chest ?
|
||||||
(header, shownLoot) =
|
(header, shownLoot) =
|
||||||
case model.state.route of
|
case model.state.route of
|
||||||
PlayerChest ->
|
PlayerChest ->
|
||||||
@@ -323,21 +452,39 @@ view model =
|
|||||||
("Marchand", Maybe.withDefault [] model.merchantItems)
|
("Marchand", Maybe.withDefault [] model.merchantItems)
|
||||||
NewLoot ->
|
NewLoot ->
|
||||||
("Nouveau trésor :)", [] )
|
("Nouveau trésor :)", [] )
|
||||||
actionControls =
|
|
||||||
|
{- Dynamic renderes to allow the use of ViewMode
|
||||||
|
|
||||||
|
ActionControls is inserted in the PlayerBar's right
|
||||||
|
and rowControls are inserted, to the right of every item rows
|
||||||
|
-}
|
||||||
|
(actionControls, rowControls) =
|
||||||
case model.state.activeMode of
|
case model.state.activeMode of
|
||||||
Just mode -> -- When a mode is active
|
Just mode -> -- When a mode is active
|
||||||
[ div [ class "buttons has-addons"]
|
( [ div [ class "buttons has-addons"]
|
||||||
[ actionButton (Just Confirm) "Valider" "plus" "primary"
|
[ actionButton (ConfirmAction) "Valider" "success" "primary"
|
||||||
, actionButton Nothing "Annuler" "times" "danger"
|
, actionButton (ModeSwitched Nothing) "Annuler" "times" "danger"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
Nothing -> -- Buttons to enter mode
|
, Just (rowControlsForMode mode isSelected)
|
||||||
case model.state.route of
|
)
|
||||||
PlayerChest -> [actionButton (Just Sell) "Vendre" "coins" "danger"]
|
Nothing -> -- Buttons to enter mode
|
||||||
GroupLoot -> [actionButton (Just Grab) "Demander" "coins" "primary"]
|
( actionButton UndoLastAction "Annuler action" "delete" "danger"
|
||||||
Merchant -> [actionButton (Just Buy) "Acheter" "coins" "success"]
|
:: case model.state.route of
|
||||||
NewLoot -> [actionButton (Just Add) "Nouveau loot" "plus" "primary"]
|
PlayerChest -> [actionButton (ModeSwitched (Just Sell)) "Vendre" "coins" "danger"]
|
||||||
|
GroupLoot -> [actionButton (ModeSwitched (Just Grab)) "Demander" "coins" "primary"]
|
||||||
|
Merchant -> [actionButton (ModeSwitched (Just Buy)) "Acheter" "coins" "success"]
|
||||||
|
NewLoot -> [actionButton (ModeSwitched (Just Add)) "Nouveau loot" "plus" "primary"]
|
||||||
|
-- Claim controls for Group chest
|
||||||
|
, case model.state.route of
|
||||||
|
GroupLoot -> Just renderId
|
||||||
|
_ -> Nothing
|
||||||
|
)
|
||||||
|
|
||||||
|
-- TODO: should we extract the Maybe conversion
|
||||||
|
-- and represent cannotSelect with Nothing ??
|
||||||
|
isSelected =
|
||||||
|
itemInSelection model.state.selection
|
||||||
in
|
in
|
||||||
{ title = "Loot-a-lot in ELM"
|
{ title = "Loot-a-lot in ELM"
|
||||||
, body =
|
, body =
|
||||||
@@ -346,7 +493,7 @@ view model =
|
|||||||
, article [class "section container"]
|
, article [class "section container"]
|
||||||
[ p [class "heading"] [text header]
|
[ p [class "heading"] [text header]
|
||||||
, viewSearchBar
|
, viewSearchBar
|
||||||
, viewChest shownLoot model
|
, viewChest isSelected rowControls shownLoot
|
||||||
]
|
]
|
||||||
, hr [] []
|
, hr [] []
|
||||||
, section [class "container"] [viewDebugSection model]
|
, section [class "container"] [viewDebugSection model]
|
||||||
@@ -355,60 +502,38 @@ view model =
|
|||||||
|
|
||||||
-- LOOT Views
|
-- LOOT Views
|
||||||
|
|
||||||
isSelected : Maybe Selection -> Item -> Bool
|
itemInSelection : Maybe Selection -> Item -> Bool
|
||||||
isSelected selection item =
|
itemInSelection selection item =
|
||||||
case selection of
|
Maybe.map (Set.member item.id) selection
|
||||||
Just s ->
|
|> Maybe.withDefault False
|
||||||
Set.member item.id s
|
|
||||||
Nothing ->
|
|
||||||
False
|
|
||||||
|
|
||||||
renderName item =
|
renderId item =
|
||||||
p [] [text item.name]
|
p [] [text <| String.fromInt item.id]
|
||||||
|
|
||||||
viewChest : Loot -> Model -> Html Msg
|
viewChest : (Item -> Bool) -> Maybe (Item -> Html Msg) -> Loot -> Html Msg
|
||||||
viewChest items model =
|
viewChest isSelected rowControls items =
|
||||||
let
|
table [ class "table is-fullwidth is-hoverable"]
|
||||||
-- If a mode is active, render its controls
|
[ thead [ class "table-header" ]
|
||||||
-- Otherwise, controls may be rendered depending on current route
|
|
||||||
rowControls = case model.state.activeMode of
|
|
||||||
Just mode -> Just (rowModeControlsRenderer mode)
|
|
||||||
Nothing ->
|
|
||||||
case model.state.route of
|
|
||||||
GroupLoot -> Just renderName
|
|
||||||
others -> Nothing
|
|
||||||
in
|
|
||||||
table [ class "table is-fullwidth is-striped is-light"]
|
|
||||||
<| thead [ class "table-header" ]
|
|
||||||
[ th [] [ text "Nom" ] ]
|
[ th [] [ text "Nom" ] ]
|
||||||
:: List.map (viewItemTableRow (isSelected model.state.selection) rowControls) items
|
, tbody [] <| List.map (viewItemTableRow isSelected rowControls) items
|
||||||
|
]
|
||||||
|
|
||||||
-- Renders controls for a specific mode
|
-- Renders controls for a specific mode
|
||||||
rowModeControlsRenderer : ViewMode -> Item -> Html Msg
|
rowControlsForMode : ViewMode -> (Item -> Bool) -> Item -> Html Msg
|
||||||
rowModeControlsRenderer mode item =
|
rowControlsForMode mode isSelected item =
|
||||||
let
|
let
|
||||||
(itemInfo, canSelect) = case mode of
|
itemInfo = case mode of
|
||||||
Buy ->
|
Buy -> p [class "level-item"] [ text (String.fromInt item.base_price ++ "po")]
|
||||||
( p [class "level-item"] [ text (String.fromInt item.base_price ++ "po")]
|
Sell -> p [class "level-item"] [ text (String.fromFloat (toFloat item.base_price / 2) ++ "po")]
|
||||||
, True )
|
Grab -> p [class "level-item"] [ text "Grab" ]
|
||||||
Sell ->
|
Add -> p [class "level-item"] [ text "New !" ]
|
||||||
( p [class "level-item"] [ text (String.fromFloat (toFloat item.base_price / 2) ++ "po")]
|
|
||||||
, True )
|
|
||||||
Grab ->
|
|
||||||
( p [class "level-item"] [ text "Grab" ]
|
|
||||||
, True )
|
|
||||||
Add ->
|
|
||||||
( p [class "level-item"] [ text "New !" ]
|
|
||||||
, False )
|
|
||||||
Confirm ->
|
|
||||||
(text ""
|
|
||||||
, False )
|
|
||||||
in
|
in
|
||||||
div [ class "level-right" ]
|
div [ class "level-right" ]
|
||||||
<| itemInfo
|
<| itemInfo
|
||||||
:: if canSelect then
|
:: if canSelectIn mode then
|
||||||
[input [ class "checkbox level-item"
|
[input [ class "checkbox level-item"
|
||||||
, type_ "checkbox"
|
, type_ "checkbox"
|
||||||
|
, checked <| isSelected item
|
||||||
, onCheck (\v -> LootViewItemSwitched item.id)
|
, onCheck (\v -> LootViewItemSwitched item.id)
|
||||||
] [] ]
|
] [] ]
|
||||||
else
|
else
|
||||||
@@ -416,8 +541,8 @@ rowModeControlsRenderer mode item =
|
|||||||
|
|
||||||
|
|
||||||
viewItemTableRow : (Item -> Bool) -> Maybe (Item -> Html Msg) -> Item -> Html Msg
|
viewItemTableRow : (Item -> Bool) -> Maybe (Item -> Html Msg) -> Item -> Html Msg
|
||||||
viewItemTableRow selected rowControls item =
|
viewItemTableRow isSelected rowControls item =
|
||||||
tr [ classList [ ("is-selected", selected item) ] ]
|
tr [ classList [ ("is-selected", isSelected item) ] ]
|
||||||
[ td []
|
[ td []
|
||||||
[ label [ class "level checkbox" ]
|
[ label [ class "level checkbox" ]
|
||||||
<| div [ class "level-left" ]
|
<| div [ class "level-left" ]
|
||||||
@@ -527,7 +652,7 @@ viewPlayerBar player actionControls =
|
|||||||
[ span [ class "icon is-large" ]
|
[ span [ class "icon is-large" ]
|
||||||
[ i [ class "fas fa-2x fa-piggy-bank" ] [] ]]
|
[ i [ class "fas fa-2x fa-piggy-bank" ] [] ]]
|
||||||
]
|
]
|
||||||
++ (showWealth player.wealth)
|
++ (viewWealth player.wealth)
|
||||||
++ (if player.debt > 0 then
|
++ (if player.debt > 0 then
|
||||||
[div [class "level-item"]
|
[div [class "level-item"]
|
||||||
[p [class "heading is-size-4 has-text-danger"]
|
[p [class "heading is-size-4 has-text-danger"]
|
||||||
@@ -541,19 +666,20 @@ viewPlayerBar player actionControls =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
showWealth : Wealth -> List (Html Msg)
|
viewWealth : Wealth -> List (Html Msg)
|
||||||
showWealth wealth =
|
viewWealth wealth =
|
||||||
[ showWealthField "pp" wealth.pp
|
[ showWealthField "pp" <| String.fromInt wealth.pp
|
||||||
, showWealthField "gp" wealth.gp
|
, showWealthField "gp" <| String.padLeft 2 '0' <| String.fromInt wealth.gp
|
||||||
, showWealthField "sp" wealth.sp
|
, showWealthField "sp" <| String.fromInt wealth.sp
|
||||||
, showWealthField "cp" wealth.cp
|
, showWealthField "cp" <| String.fromInt wealth.cp
|
||||||
]
|
]
|
||||||
|
|
||||||
showWealthField : String -> Int -> Html Msg
|
showWealthField : String -> String -> Html Msg
|
||||||
showWealthField name value =
|
showWealthField name value =
|
||||||
div [ class "level-item" ]
|
div [ class "level-item" ]
|
||||||
[ p [ class "is-size-4"] [text (String.fromInt value)]
|
[ p [class "has-text-right"] [ strong [ class "heading is-marginless"] [text name]
|
||||||
, p [class "heading"] [text name]
|
, span [ class <| "is-size-4" ] [ text value ]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
-- Search Bar
|
-- Search Bar
|
||||||
|
|||||||
Reference in New Issue
Block a user