Browse Source

Load draft from 17lands public URL

Cadel Watson 4 months ago
parent
commit
9339ad2982
8 changed files with 203 additions and 112 deletions
  1. 0 13
      js/app.js
  2. 10 1
      src/API.elm
  3. 20 0
      src/Database.elm
  4. 1 1
      src/Draft.elm
  5. 34 8
      src/DraftMeta.elm
  6. 111 88
      src/Main.elm
  7. 12 1
      tests/TestDatabase.elm
  8. 15 0
      tests/TestDraftMeta.elm

+ 0 - 13
js/app.js

@@ -56,14 +56,6 @@ function getAllLocalSets(db, callback) {
 
 
 }
 }
 
 
-function getSavedEventHistoryUrl() {
-    return localStorage.getItem("eventHistoryURL");
-}
-
-function putSavedEventHistoryUrl(url) {
-    return localStorage.getItem("eventHistoryURL", url);
-}
-
 const openDBRequest = indexedDB.open("set_database", 2);
 const openDBRequest = indexedDB.open("set_database", 2);
 
 
 openDBRequest.onerror = (event) => {
 openDBRequest.onerror = (event) => {
@@ -86,7 +78,6 @@ openDBRequest.onsuccess = (event) => {
             flags: {
             flags: {
                 sets: sets,
                 sets: sets,
                 draftData: JSON.stringify(draftData),
                 draftData: JSON.stringify(draftData),
-                eventHistoryUrl: getSavedEventHistoryUrl()
             }
             }
         });
         });
 
 
@@ -103,10 +94,6 @@ openDBRequest.onsuccess = (event) => {
         app.ports.sendDeleteLocalData.subscribe((setCode) => {
         app.ports.sendDeleteLocalData.subscribe((setCode) => {
             deleteSetData(database, setCode, (setCode) => app.ports.receiveDidDeleteLocalData.send(setCode))
             deleteSetData(database, setCode, (setCode) => app.ports.receiveDidDeleteLocalData.send(setCode))
         });
         });
-
-        app.ports.sendSaveEventHistoryUrl.subscribe((url) => {
-            putSavedEventHistoryUrl(url);
-        });
     })
     })
 };
 };
 
 

+ 10 - 1
src/API.elm

@@ -1,4 +1,4 @@
-module API exposing (getDrafts, getSetData, getSets)
+module API exposing (getDraft, getDrafts, getSetData, getSets)
 
 
 import Database exposing (Database)
 import Database exposing (Database)
 import DraftMeta exposing (DraftMeta)
 import DraftMeta exposing (DraftMeta)
@@ -54,6 +54,15 @@ getDrafts historyUrl onSuccess =
         }
         }
 
 
 
 
+getDraft : String -> (Result String DraftMeta -> msg) -> Cmd msg
+getDraft draftID onSuccess =
+    Http.get
+        { url = "https://www.17lands.com" ++ UrlB.absolute [ "data", "draft" ] [ UrlB.string "draft_id" draftID ]
+        , expect =
+            Http.expectJson (Result.mapError httpErrorToString >> onSuccess) DraftMeta.decoder
+        }
+
+
 httpErrorToString : Http.Error -> String
 httpErrorToString : Http.Error -> String
 httpErrorToString error =
 httpErrorToString error =
     case error of
     case error of

+ 20 - 0
src/Database.elm

@@ -248,9 +248,29 @@ encodeCardDetails card =
         , ( "colors", Encode.list encodeManaColor card.details.colors )
         , ( "colors", Encode.list encodeManaColor card.details.colors )
         , ( "mana_cost", Encode.string card.details.rawManaCost )
         , ( "mana_cost", Encode.string card.details.rawManaCost )
         , ( "image_uris", Encode.object [ ( "large", Encode.string card.details.imageUrl ) ] )
         , ( "image_uris", Encode.object [ ( "large", Encode.string card.details.imageUrl ) ] )
+        , ( "rarity", encodeRarity card.details.rarity )
         ]
         ]
 
 
 
 
+encodeRarity : Rarity -> Encode.Value
+encodeRarity r =
+    case r of
+        Common ->
+            Encode.string "common"
+
+        Uncommon ->
+            Encode.string "uncommon"
+
+        Rare ->
+            Encode.string "rare"
+
+        Mythic ->
+            Encode.string "mythic"
+
+        NoRarity ->
+            Encode.string ""
+
+
 decodeCardDetails : Decoder ( String, CardDetails )
 decodeCardDetails : Decoder ( String, CardDetails )
 decodeCardDetails =
 decodeCardDetails =
     Decode.map2 pair
     Decode.map2 pair

+ 1 - 1
src/Draft.elm

@@ -29,7 +29,7 @@ decode x =
 
 
 decodeDraft : Decoder Draft
 decodeDraft : Decoder Draft
 decodeDraft =
 decodeDraft =
-    Decode.field "picks" (Decode.list decodePick)
+    Decode.list decodePick
         |> Decode.map Zipper.fromList
         |> Decode.map Zipper.fromList
         |> Decode.andThen
         |> Decode.andThen
             (\z ->
             (\z ->

+ 34 - 8
src/DraftMeta.elm

@@ -1,15 +1,14 @@
-module DraftMeta exposing (DraftMeta, decoder)
+module DraftMeta exposing (DraftMeta, decoder, parseDraftIDFromPublicURL)
 
 
 import Draft exposing (Draft)
 import Draft exposing (Draft)
-import Html as Decode
 import Json.Decode as Decode exposing (Decoder)
 import Json.Decode as Decode exposing (Decoder)
 import Json.Decode.Pipeline as Decode
 import Json.Decode.Pipeline as Decode
+import Url
+import Url.Parser as UrlP exposing ((</>))
 
 
 
 
 type alias DraftMeta =
 type alias DraftMeta =
     { setCode : String
     { setCode : String
-    , format : String
-    , timestamp : String
     , draftData : Draft
     , draftData : Draft
     }
     }
 
 
@@ -17,7 +16,34 @@ type alias DraftMeta =
 decoder : Decoder DraftMeta
 decoder : Decoder DraftMeta
 decoder =
 decoder =
     Decode.succeed DraftMeta
     Decode.succeed DraftMeta
-        |> Decode.required "set_code" Decode.string
-        |> Decode.required "format" Decode.string
-        |> Decode.required "timestamp" Decode.string
-        |> Decode.required "draft_data" Draft.decodeDraft
+        |> Decode.required "expansion" (Decode.string |> Decode.map (\s -> String.toLower s))
+        |> Decode.required "picks" Draft.decodeDraft
+
+
+{-| Parse the draft ID from the public URL.
+
+e.g. the public URL for draft ID `490bda1edc574d71a2768a7d7a415a25` is
+<https://www.17lands.com/draft/490bda1edc574d71a2768a7d7a415a25>
+
+-}
+parseDraftIDFromPublicURL : String -> Result String String
+parseDraftIDFromPublicURL url =
+    let
+        draftURLP =
+            UrlP.s "draft" </> UrlP.string
+    in
+    String.trim url
+        |> Url.fromString
+        |> (\maybeUrl ->
+                case maybeUrl of
+                    Just u ->
+                        case UrlP.parse draftURLP u of
+                            Just draftID ->
+                                Result.Ok draftID
+
+                            Nothing ->
+                                Result.Err "parsing failed"
+
+                    Nothing ->
+                        Result.Err "not a valid URL"
+           )

+ 111 - 88
src/Main.elm

@@ -10,7 +10,7 @@ import Database
 import Deck
 import Deck
 import Dict exposing (Dict)
 import Dict exposing (Dict)
 import Draft exposing (Draft)
 import Draft exposing (Draft)
-import DraftMeta exposing (DraftMeta)
+import DraftMeta exposing (DraftMeta, parseDraftIDFromPublicURL)
 import Html exposing (Html, a, button, div, h1, img, input, label, li, p, span, text, ul)
 import Html exposing (Html, a, button, div, h1, img, input, label, li, p, span, text, ul)
 import Html.Attributes exposing (alt, class, classList, disabled, href, src, type_, value)
 import Html.Attributes exposing (alt, class, classList, disabled, href, src, type_, value)
 import Html.Events as Events exposing (onClick, onInput, onMouseEnter)
 import Html.Events as Events exposing (onClick, onInput, onMouseEnter)
@@ -73,18 +73,18 @@ type SetLoadStatus
     | DeletingLocalData
     | DeletingLocalData
 
 
 
 
-type AvailableDrafts
-    = DraftsNotChecked
-    | DraftsLoading
-    | AvailableDrafts (List DraftMeta)
+type ImportDraft
+    = EditingImportUrl String
+    | LoadingDraft
+    | ImportDraftParseError String
+    | DraftImported DraftMeta
 
 
 
 
 type alias ChooseSetModel =
 type alias ChooseSetModel =
     { draftData : String
     { draftData : String
     , sets : Dict String SetLoadStatus
     , sets : Dict String SetLoadStatus
-    , eventHistoryUrl : Maybe String -- Resolved event history URL
-    , eventHistoryUrlField : String -- Working state of input
-    , drafts : AvailableDrafts
+    , importDraft : ImportDraft
+    , importDraftParseError : Maybe String
     }
     }
 
 
 
 
@@ -115,7 +115,7 @@ type alias ErrorModel =
     }
     }
 
 
 
 
-init : { sets : List String, draftData : String, eventHistoryUrl : Maybe String } -> ( Model, Cmd Msg )
+init : { sets : List String, draftData : String } -> ( Model, Cmd Msg )
 init flags =
 init flags =
     let
     let
         setStatus : Dict String SetLoadStatus
         setStatus : Dict String SetLoadStatus
@@ -129,24 +129,14 @@ init flags =
             List.map
             List.map
                 (\s -> sendDoesSetHaveLocalData s)
                 (\s -> sendDoesSetHaveLocalData s)
                 flags.sets
                 flags.sets
-
-        draftCmds : List (Cmd Msg)
-        draftCmds =
-            case flags.eventHistoryUrl of
-                Nothing ->
-                    []
-
-                Just url ->
-                    [ API.getDrafts url IOGotDrafts ]
     in
     in
     ( ChooseSet
     ( ChooseSet
         { draftData = flags.draftData
         { draftData = flags.draftData
         , sets = setStatus
         , sets = setStatus
-        , eventHistoryUrl = flags.eventHistoryUrl
-        , eventHistoryUrlField = ""
-        , drafts = DraftsNotChecked
+        , importDraft = EditingImportUrl ""
+        , importDraftParseError = Nothing
         }
         }
-    , Cmd.batch (API.getSets IOGotSets :: setCmds ++ draftCmds)
+    , Cmd.batch (API.getSets IOGotSets :: setCmds)
     )
     )
 
 
 
 
@@ -154,8 +144,9 @@ type Msg
     = Increment
     = Increment
     | Decrement
     | Decrement
     | Highlight String
     | Highlight String
-    | SetEventHistoryUrlField String
-    | SubmitEventHistoryUrlField
+    | ChangeDraftImportUrlField String
+    | AcknowledgeParseError
+    | SubmitImportDraft
     | FlipHighlightedCard
     | FlipHighlightedCard
     | SetFocusStat FocusStat
     | SetFocusStat FocusStat
     | SetSortOrder SortOrder
     | SetSortOrder SortOrder
@@ -166,41 +157,35 @@ type Msg
     | ToggleDeckList
     | ToggleDeckList
     | SetDeckSortMethod Deck.DeckSortMethod
     | SetDeckSortMethod Deck.DeckSortMethod
     | IOGotSets (Result String (List String))
     | IOGotSets (Result String (List String))
-    | IOGotDrafts (Result String (List DraftMeta))
+    | IOGotDraft (Result String DraftMeta)
     | IOFetchSetData String
     | IOFetchSetData String
     | IOGotSetData (Result String ( String, Maybe Database.Database ))
     | IOGotSetData (Result String ( String, Maybe Database.Database ))
     | IODeleteSetData String -- Delete by set code
     | IODeleteSetData String -- Delete by set code
     | IOGotDeleteSetData String -- Successful deletion by set code
     | IOGotDeleteSetData String -- Successful deletion by set code
     | PortReceiveDoesSetHaveLocalData String
     | PortReceiveDoesSetHaveLocalData String
-    | StartDraft String Database.Database
     | OpenCardExplorer String Database.Database
     | OpenCardExplorer String Database.Database
 
 
 
 
-startDraft : ChooseSetModel -> Database.Database -> ( Model, Cmd Msg )
-startDraft initFlags database =
-    case Draft.decode initFlags.draftData of
-        Ok draftData ->
-            ( Ready
-                { draft = draftData
-                , database = database
-                , performanceDistributions = calculatePerformanceDistributions (Database.getAll database)
-                , highlighted = Nothing
-                , flipHighlighted = False
-                , focusStat = FocusPickRate
-                , deckProgress = DeckUpToPick
-                , toolboxAccordion =
-                    { cmc = True
-                    , signals = False
-                    , signalsDelta = False
-                    , deckList = False
-                    }
-                , deckSortOrder = Deck.SortByPickNumber
-                }
-            , Cmd.none
-            )
-
-        Err err ->
-            ( Error { error = "Error decoding draft data: " ++ err }, Cmd.none )
+startDraft : DraftMeta -> Database.Database -> ( Model, Cmd Msg )
+startDraft draft database =
+    ( Ready
+        { draft = draft.draftData
+        , database = database
+        , performanceDistributions = calculatePerformanceDistributions (Database.getAll database)
+        , highlighted = Nothing
+        , flipHighlighted = False
+        , focusStat = FocusPickRate
+        , deckProgress = DeckUpToPick
+        , toolboxAccordion =
+            { cmc = True
+            , signals = False
+            , signalsDelta = False
+            , deckList = False
+            }
+        , deckSortOrder = Deck.SortByPickNumber
+        }
+    , Cmd.none
+    )
 
 
 
 
 openCardExplorer : ChooseSetModel -> Database.Database -> ( Model, Cmd Msg )
 openCardExplorer : ChooseSetModel -> Database.Database -> ( Model, Cmd Msg )
@@ -221,9 +206,6 @@ update msg model =
     case model of
     case model of
         ChooseSet mdl ->
         ChooseSet mdl ->
             case msg of
             case msg of
-                StartDraft setCode database ->
-                    startDraft mdl database
-
                 OpenCardExplorer setCode database ->
                 OpenCardExplorer setCode database ->
                     openCardExplorer mdl database
                     openCardExplorer mdl database
 
 
@@ -306,23 +288,58 @@ update msg model =
                 IOGotSetData (Err e) ->
                 IOGotSetData (Err e) ->
                     ( Error { error = "Error fetching remote set data (" ++ e ++ ")" }, Cmd.none )
                     ( Error { error = "Error fetching remote set data (" ++ e ++ ")" }, Cmd.none )
 
 
-                SetEventHistoryUrlField v ->
-                    ( ChooseSet { mdl | eventHistoryUrlField = v }, Cmd.none )
+                ChangeDraftImportUrlField v ->
+                    let
+                        newImportDraft =
+                            case mdl.importDraft of
+                                EditingImportUrl _ ->
+                                    EditingImportUrl v
 
 
-                SubmitEventHistoryUrlField ->
-                    ( ChooseSet
-                        { mdl
-                            | eventHistoryUrl = Just mdl.eventHistoryUrlField
-                            , drafts = DraftsLoading
-                        }
-                    , API.getDrafts mdl.eventHistoryUrlField IOGotDrafts
-                    )
+                                _ ->
+                                    mdl.importDraft
+                    in
+                    ( ChooseSet { mdl | importDraft = newImportDraft }, Cmd.none )
+
+                AcknowledgeParseError ->
+                    ( ChooseSet { mdl | importDraft = EditingImportUrl "" }, Cmd.none )
+
+                SubmitImportDraft ->
+                    case mdl.importDraft of
+                        EditingImportUrl url ->
+                            case parseDraftIDFromPublicURL url of
+                                Ok draftID ->
+                                    ( ChooseSet
+                                        { mdl
+                                            | importDraft = LoadingDraft
+                                        }
+                                    , API.getDraft draftID IOGotDraft
+                                    )
+
+                                Err e ->
+                                    ( ChooseSet
+                                        { mdl
+                                            | importDraft = ImportDraftParseError e
+                                        }
+                                    , Cmd.none
+                                    )
 
 
-                IOGotDrafts (Ok drafts) ->
-                    ( ChooseSet { mdl | drafts = AvailableDrafts drafts }, Cmd.none )
+                        _ ->
+                            ( ChooseSet mdl, Cmd.none )
 
 
-                IOGotDrafts (Err e) ->
-                    ( Error { error = "Error fetching drafts (" ++ e ++ ")" }, Cmd.none )
+                IOGotDraft (Ok draft) ->
+                    let
+                        setData =
+                            Dict.get draft.setCode mdl.sets
+                    in
+                    case setData of
+                        Just (HasLocalData db) ->
+                            startDraft draft db
+
+                        _ ->
+                            ( Error { error = "You must download the set data for the draft before you can open it. (" ++ draft.setCode ++ ")" }, Cmd.none )
+
+                IOGotDraft (Err e) ->
+                    ( Error { error = "Error fetching draft (" ++ e ++ ")" }, Cmd.none )
 
 
                 _ ->
                 _ ->
                     ( ChooseSet mdl, Cmd.none )
                     ( ChooseSet mdl, Cmd.none )
@@ -402,22 +419,22 @@ update msg model =
                 IOGotDeleteSetData setCode ->
                 IOGotDeleteSetData setCode ->
                     ( Ready mdl, Cmd.none )
                     ( Ready mdl, Cmd.none )
 
 
-                IOGotDrafts _ ->
+                IOGotDraft _ ->
                     ( Ready mdl, Cmd.none )
                     ( Ready mdl, Cmd.none )
 
 
                 PortReceiveDoesSetHaveLocalData _ ->
                 PortReceiveDoesSetHaveLocalData _ ->
                     ( Ready mdl, Cmd.none )
                     ( Ready mdl, Cmd.none )
 
 
-                StartDraft _ _ ->
+                OpenCardExplorer _ _ ->
                     ( Ready mdl, Cmd.none )
                     ( Ready mdl, Cmd.none )
 
 
-                OpenCardExplorer _ _ ->
+                ChangeDraftImportUrlField _ ->
                     ( Ready mdl, Cmd.none )
                     ( Ready mdl, Cmd.none )
 
 
-                SetEventHistoryUrlField _ ->
+                SubmitImportDraft ->
                     ( Ready mdl, Cmd.none )
                     ( Ready mdl, Cmd.none )
 
 
-                SubmitEventHistoryUrlField ->
+                AcknowledgeParseError ->
                     ( Ready mdl, Cmd.none )
                     ( Ready mdl, Cmd.none )
 
 
         CardExplorer mdl ->
         CardExplorer mdl ->
@@ -482,7 +499,6 @@ viewChooseSet model =
                 HasLocalData database ->
                 HasLocalData database ->
                     div [ class "flex gap-2" ]
                     div [ class "flex gap-2" ]
                         [ Button.make "Explore" (OpenCardExplorer setCode database) |> Button.view
                         [ Button.make "Explore" (OpenCardExplorer setCode database) |> Button.view
-                        , Button.make "View draft" (StartDraft setCode database) |> Button.view
                         , Button.make "Delete" (IODeleteSetData setCode) |> Button.view
                         , Button.make "Delete" (IODeleteSetData setCode) |> Button.view
                         ]
                         ]
     in
     in
@@ -490,7 +506,7 @@ viewChooseSet model =
         [ div [ class "max-w-4xl max-h-2xl bg-slate-500 rounded-lg p-6 shadow-xl" ]
         [ div [ class "max-w-4xl max-h-2xl bg-slate-500 rounded-lg p-6 shadow-xl" ]
             [ h1 [ class "text-3xl text-white font-medium" ] [ text "Drafter" ]
             [ h1 [ class "text-3xl text-white font-medium" ] [ text "Drafter" ]
             , p [ class "text-white font-medium" ] [ text "Explore sets and analyse your draft events." ]
             , p [ class "text-white font-medium" ] [ text "Explore sets and analyse your draft events." ]
-            , viewEventHistoryURL model
+            , viewDraftURL model
             ]
             ]
         , div [ class "max-w-4xl max-h-2xl bg-slate-500 rounded-lg p-6 shadow-xl" ]
         , div [ class "max-w-4xl max-h-2xl bg-slate-500 rounded-lg p-6 shadow-xl" ]
             [ ul [ class "divide-y divide-slate-600" ]
             [ ul [ class "divide-y divide-slate-600" ]
@@ -512,27 +528,34 @@ viewChooseSet model =
         ]
         ]
 
 
 
 
-viewEventHistoryURL : ChooseSetModel -> Html Msg
-viewEventHistoryURL model =
-    case model.eventHistoryUrl of
-        Just url ->
-            div []
-                [ p [] [ text "17Lands public event history:" ]
-                , a [ href url ] [ text url ]
-                ]
-
-        Nothing ->
+viewDraftURL : ChooseSetModel -> Html Msg
+viewDraftURL model =
+    case model.importDraft of
+        EditingImportUrl url ->
             div [ class "mt-4" ]
             div [ class "mt-4" ]
                 [ label [ class "block text-sm font-medium text-white" ] [ text "Enter your 17Lands public event history URL:" ]
                 [ label [ class "block text-sm font-medium text-white" ] [ text "Enter your 17Lands public event history URL:" ]
                 , input
                 , input
                     [ type_ "text"
                     [ type_ "text"
-                    , onInput SetEventHistoryUrlField
-                    , value model.eventHistoryUrlField
+                    , onInput ChangeDraftImportUrlField
+                    , value url
                     ]
                     ]
                     []
                     []
-                , Button.make "Save" SubmitEventHistoryUrlField |> Button.view
+                , Button.make "Save" SubmitImportDraft |> Button.view
+                ]
+
+        ImportDraftParseError e ->
+            div [ class "mt-4 text-white" ]
+                [ p [] [ text ("Could not extract draft ID from URL: " ++ e) ]
+                , p [] [ text "Please make sure your URL looks like: https://www.17lands.com/draft/490bda1edc574d71a2768a7d7a415a25" ]
                 ]
                 ]
 
 
+        LoadingDraft ->
+            div [ class "mt-4" ]
+                [ text "Loading..." ]
+
+        _ ->
+            div [] []
+
 
 
 viewReady : ReadyModel -> Html Msg
 viewReady : ReadyModel -> Html Msg
 viewReady model =
 viewReady model =

+ 12 - 1
tests/TestDatabase.elm

@@ -1,6 +1,6 @@
 module TestDatabase exposing (..)
 module TestDatabase exposing (..)
 
 
-import Card exposing (CardData, CardDetails, CardPerformanceData, CardType(..), ManaColor(..), ManaCost(..), Power(..))
+import Card exposing (CardData, CardDetails, CardPerformanceData, CardType(..), ManaColor(..), ManaCost(..), Power(..), Rarity(..))
 import Database exposing (Database)
 import Database exposing (Database)
 import Expect
 import Expect
 import Fuzz exposing (Fuzzer)
 import Fuzz exposing (Fuzzer)
@@ -55,6 +55,7 @@ fuzzCardDetails name =
         |> Fuzz.andMap (Fuzz.constant "{X}{R}")
         |> Fuzz.andMap (Fuzz.constant "{X}{R}")
         |> Fuzz.andMap (Fuzz.constant (Just [ X, Color Red ]))
         |> Fuzz.andMap (Fuzz.constant (Just [ X, Color Red ]))
         |> Fuzz.andMap Fuzz.string
         |> Fuzz.andMap Fuzz.string
+        |> Fuzz.andMap fuzzRarity
 
 
 
 
 fuzzCardType : Fuzzer CardType
 fuzzCardType : Fuzzer CardType
@@ -80,6 +81,16 @@ fuzzPower =
         ]
         ]
 
 
 
 
+fuzzRarity : Fuzzer Rarity
+fuzzRarity =
+    Fuzz.oneOfValues
+        [ Common
+        , Uncommon
+        , Rare
+        , Mythic
+        ]
+
+
 fuzzManaColor : Fuzzer ManaColor
 fuzzManaColor : Fuzzer ManaColor
 fuzzManaColor =
 fuzzManaColor =
     Fuzz.oneOfValues
     Fuzz.oneOfValues

+ 15 - 0
tests/TestDraftMeta.elm

@@ -0,0 +1,15 @@
+module TestDraftMeta exposing (..)
+
+import DraftMeta
+import Expect exposing (Expectation)
+import Fuzz exposing (Fuzzer, int, list, string)
+import Signals
+import Test exposing (..)
+
+
+suite : Test
+suite =
+    describe "DraftMeta"
+        [ test "parseDraftIDFromPublicURL" <|
+            \_ -> Expect.equal (DraftMeta.parseDraftIDFromPublicURL "https://www.17lands.com/draft/490bda1edc574d71a2768a7d7a415a25") (Ok "490bda1edc574d71a2768a7d7a415a25")
+        ]