Explorar o código

WIP: load drafts straight from 17lands

Cadel Watson hai 4 meses
pai
achega
3f2d7f6622
Modificáronse 7 ficheiros con 262 adicións e 52 borrados
  1. 39 39
      elm.json
  2. 14 4
      js/app.js
  3. 81 0
      server/main.py
  4. 15 1
      src/API.elm
  5. 1 1
      src/Draft.elm
  6. 23 0
      src/DraftMeta.elm
  7. 89 7
      src/Main.elm

+ 39 - 39
elm.json

@@ -1,43 +1,43 @@
 {
-  "type": "application",
-  "source-directories": [
-    "src"
-  ],
-  "elm-version": "0.19.1",
-  "dependencies": {
-    "direct": {
-      "NoRedInk/elm-json-decode-pipeline": "1.0.1",
-      "elm/browser": "1.0.2",
-      "elm/core": "1.0.5",
-      "elm/html": "1.0.0",
-      "elm/http": "2.0.0",
-      "elm/json": "1.1.3",
-      "elm/parser": "1.1.0",
-      "elm/svg": "1.0.1",
-      "elm-community/list-extra": "8.7.0",
-      "myrho/elm-round": "1.0.5",
-      "terezka/elm-charts": "4.0.0"
+    "type": "application",
+    "source-directories": [
+        "src"
+    ],
+    "elm-version": "0.19.1",
+    "dependencies": {
+        "direct": {
+            "NoRedInk/elm-json-decode-pipeline": "1.0.1",
+            "elm/browser": "1.0.2",
+            "elm/core": "1.0.5",
+            "elm/html": "1.0.0",
+            "elm/http": "2.0.0",
+            "elm/json": "1.1.3",
+            "elm/parser": "1.1.0",
+            "elm/svg": "1.0.1",
+            "elm/url": "1.0.0",
+            "elm-community/list-extra": "8.7.0",
+            "myrho/elm-round": "1.0.5",
+            "terezka/elm-charts": "4.0.0"
+        },
+        "indirect": {
+            "K-Adam/elm-dom": "1.0.0",
+            "danhandrea/elm-time-extra": "1.1.0",
+            "elm/bytes": "1.0.8",
+            "elm/file": "1.0.5",
+            "elm/time": "1.0.0",
+            "elm/virtual-dom": "1.0.2",
+            "justinmimbs/date": "4.1.0",
+            "justinmimbs/time-extra": "1.2.0",
+            "ryan-haskell/date-format": "1.0.0",
+            "terezka/intervals": "2.0.2"
+        }
     },
-    "indirect": {
-      "K-Adam/elm-dom": "1.0.0",
-      "danhandrea/elm-time-extra": "1.1.0",
-      "elm/bytes": "1.0.8",
-      "elm/file": "1.0.5",
-      "elm/time": "1.0.0",
-      "elm/url": "1.0.0",
-      "elm/virtual-dom": "1.0.2",
-      "justinmimbs/date": "4.1.0",
-      "justinmimbs/time-extra": "1.2.0",
-      "ryan-haskell/date-format": "1.0.0",
-      "terezka/intervals": "2.0.2"
+    "test-dependencies": {
+        "direct": {
+            "elm-explorations/test": "2.2.0"
+        },
+        "indirect": {
+            "elm/random": "1.0.0"
+        }
     }
-  },
-  "test-dependencies": {
-    "direct": {
-      "elm-explorations/test": "2.2.0"
-    },
-    "indirect": {
-      "elm/random": "1.0.0"
-    }
-  }
 }

+ 14 - 4
js/app.js

@@ -56,6 +56,14 @@ 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);
 
 openDBRequest.onerror = (event) => {
@@ -78,11 +86,10 @@ openDBRequest.onsuccess = (event) => {
             flags: {
                 sets: sets,
                 draftData: JSON.stringify(draftData),
+                eventHistoryUrl: getSavedEventHistoryUrl()
             }
         });
 
-        console.log(app.ports);
-
         app.ports.sendDoesSetHaveLocalData.subscribe((setCode) => {
             getSetData(database, setCode, (data) => {
                 app.ports.receiveDoesSetHaveLocalData.send(JSON.stringify(data));
@@ -91,12 +98,15 @@ openDBRequest.onsuccess = (event) => {
 
         app.ports.sendSaveLocalData.subscribe((setData) => {
             saveSetData(database, setData);
-        })
+        });
 
         app.ports.sendDeleteLocalData.subscribe((setCode) => {
             deleteSetData(database, setCode, (setCode) => app.ports.receiveDidDeleteLocalData.send(setCode))
-        })
+        });
 
+        app.ports.sendSaveEventHistoryUrl.subscribe((url) => {
+            putSavedEventHistoryUrl(url);
+        });
     })
 };
 

+ 81 - 0
server/main.py

@@ -1,6 +1,10 @@
+import asyncio
 import json
 import os
+import re
+from typing import TypedDict, List, Dict, Any
 
+import httpx
 from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
 
@@ -80,3 +84,80 @@ def read_item(set_name: str):
         ratings_data = json.load(f)
 
     return {"code": set_name, "data": {"cards": card_data, "ratings": ratings_data}}
+
+USER_ID_REGEX = r'user_history\/(.*)\?'
+
+class DraftMeta(TypedDict):
+    expansion: str
+    format: str
+    timestamp: str
+    draft_id: str
+
+async def get_drafts_from_17lands(user_id: str) -> List[DraftMeta]:
+    url = f"https://www.17lands.com/user/data/{user_id}"
+
+    drafts = []
+    async with httpx.AsyncClient() as client:
+        res = await client.get(
+            url,
+            headers={
+                "Accept": "application/json"
+            }
+        )
+        print(res)
+        json_res = res.json()
+
+        for draft in json_res["drafts"]:
+            drafts.append({
+                "expansion": draft["expansion"],
+                "format": draft["format"],
+                "timestamp": draft["first_event_server_time"],
+                "draft_id": draft["id"],
+            })
+
+    return drafts
+
+async def get_draft_data(draft_id:str, client: httpx.AsyncClient):
+    url = f"https://www.17lands.com/data/draft?draft_id={draft_id}"
+    res = await client.get(
+        url,
+        headers={
+            "Accept": "application/json"
+        }
+    )
+
+    print(res)
+    print(res.text)
+    print(res.json())
+    return res.json()["picks"]
+
+async def get_all_draft_data(draft_ids: List[str]) -> Dict[str, Any]:
+    async with httpx.AsyncClient() as client:
+        tasks = [get_draft_data(draft_id, client) for draft_id in draft_ids]
+        results = await asyncio.gather(*tasks)
+
+    return {
+        draft_id: {"picks": result["picks"]}
+        for draft_id, result in zip(draft_ids, results)
+    }
+
+@app.get("/drafts")
+async def read_item(history: str):
+    user_id = re.search(USER_ID_REGEX, history).group(1)
+
+    all_drafts = await get_drafts_from_17lands(user_id)
+    all_draft_data = await get_all_draft_data([draft["draft_id"] for draft in all_drafts])
+
+    drafts = [
+        {
+            "set_code" : draft["expansion"],
+            "format" : draft["format"],
+            "timestamp" : draft["timestamp"],
+            "draft_data" : all_draft_data[draft["draft_id"]],
+        }
+        for draft in all_drafts
+    ]
+
+    print(drafts)
+
+    return {"drafts":drafts}

+ 15 - 1
src/API.elm

@@ -1,8 +1,10 @@
-module API exposing (getSetData, getSets)
+module API exposing (getDrafts, getSetData, getSets)
 
 import Database exposing (Database)
+import DraftMeta exposing (DraftMeta)
 import Http
 import Json.Decode as Decode
+import Url.Builder as UrlB
 
 
 apiUrl : String
@@ -40,6 +42,18 @@ getSetData setCode onSuccess =
         }
 
 
+getDrafts : String -> (Result String (List DraftMeta) -> msg) -> Cmd msg
+getDrafts historyUrl onSuccess =
+    Http.get
+        { url = apiUrl ++ UrlB.absolute [ "drafts" ] [ UrlB.string "history" historyUrl ]
+        , expect =
+            Http.expectJson (Result.mapError httpErrorToString >> onSuccess)
+                (Decode.field "drafts"
+                    (Decode.list DraftMeta.decoder)
+                )
+        }
+
+
 httpErrorToString : Http.Error -> String
 httpErrorToString error =
     case error of

+ 1 - 1
src/Draft.elm

@@ -1,4 +1,4 @@
-module Draft exposing (Draft, Pick, PickCard, decode)
+module Draft exposing (Draft, Pick, PickCard, decode, decodeDraft)
 
 import Json.Decode as Decode exposing (Decoder)
 import Json.Decode.Pipeline as Decode

+ 23 - 0
src/DraftMeta.elm

@@ -0,0 +1,23 @@
+module DraftMeta exposing (DraftMeta, decoder)
+
+import Draft exposing (Draft)
+import Html as Decode
+import Json.Decode as Decode exposing (Decoder)
+import Json.Decode.Pipeline as Decode
+
+
+type alias DraftMeta =
+    { setCode : String
+    , format : String
+    , timestamp : String
+    , draftData : Draft
+    }
+
+
+decoder : Decoder DraftMeta
+decoder =
+    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

+ 89 - 7
src/Main.elm

@@ -10,9 +10,10 @@ import Database
 import Deck
 import Dict exposing (Dict)
 import Draft exposing (Draft)
-import Html exposing (Html, a, button, div, img, li, p, span, text, ul)
-import Html.Attributes exposing (alt, class, classList, disabled, src)
-import Html.Events as Events exposing (onClick, onMouseEnter)
+import DraftMeta exposing (DraftMeta)
+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.Events as Events exposing (onClick, onInput, onMouseEnter)
 import Html.Keyed as Keyed
 import Icon exposing (chevronDown, chevronUp)
 import Json.Decode exposing (decodeString)
@@ -72,9 +73,18 @@ type SetLoadStatus
     | DeletingLocalData
 
 
+type AvailableDrafts
+    = DraftsNotChecked
+    | DraftsLoading
+    | AvailableDrafts (List DraftMeta)
+
+
 type alias ChooseSetModel =
     { draftData : String
     , sets : Dict String SetLoadStatus
+    , eventHistoryUrl : Maybe String -- Resolved event history URL
+    , eventHistoryUrlField : String -- Working state of input
+    , drafts : AvailableDrafts
     }
 
 
@@ -105,7 +115,7 @@ type alias ErrorModel =
     }
 
 
-init : { sets : List String, draftData : String } -> ( Model, Cmd Msg )
+init : { sets : List String, draftData : String, eventHistoryUrl : Maybe String } -> ( Model, Cmd Msg )
 init flags =
     let
         setStatus : Dict String SetLoadStatus
@@ -119,12 +129,24 @@ init flags =
             List.map
                 (\s -> sendDoesSetHaveLocalData s)
                 flags.sets
+
+        draftCmds : List (Cmd Msg)
+        draftCmds =
+            case flags.eventHistoryUrl of
+                Nothing ->
+                    []
+
+                Just url ->
+                    [ API.getDrafts url IOGotDrafts ]
     in
     ( ChooseSet
         { draftData = flags.draftData
         , sets = setStatus
+        , eventHistoryUrl = flags.eventHistoryUrl
+        , eventHistoryUrlField = ""
+        , drafts = DraftsNotChecked
         }
-    , Cmd.batch (API.getSets IOGotSets :: setCmds)
+    , Cmd.batch (API.getSets IOGotSets :: setCmds ++ draftCmds)
     )
 
 
@@ -132,6 +154,8 @@ type Msg
     = Increment
     | Decrement
     | Highlight String
+    | SetEventHistoryUrlField String
+    | SubmitEventHistoryUrlField
     | FlipHighlightedCard
     | SetFocusStat FocusStat
     | SetSortOrder SortOrder
@@ -142,6 +166,7 @@ type Msg
     | ToggleDeckList
     | SetDeckSortMethod Deck.DeckSortMethod
     | IOGotSets (Result String (List String))
+    | IOGotDrafts (Result String (List DraftMeta))
     | IOFetchSetData String
     | IOGotSetData (Result String ( String, Maybe Database.Database ))
     | IODeleteSetData String -- Delete by set code
@@ -281,6 +306,24 @@ update msg model =
                 IOGotSetData (Err e) ->
                     ( Error { error = "Error fetching remote set data (" ++ e ++ ")" }, Cmd.none )
 
+                SetEventHistoryUrlField v ->
+                    ( ChooseSet { mdl | eventHistoryUrlField = v }, Cmd.none )
+
+                SubmitEventHistoryUrlField ->
+                    ( ChooseSet
+                        { mdl
+                            | eventHistoryUrl = Just mdl.eventHistoryUrlField
+                            , drafts = DraftsLoading
+                        }
+                    , API.getDrafts mdl.eventHistoryUrlField IOGotDrafts
+                    )
+
+                IOGotDrafts (Ok drafts) ->
+                    ( ChooseSet { mdl | drafts = AvailableDrafts drafts }, Cmd.none )
+
+                IOGotDrafts (Err e) ->
+                    ( Error { error = "Error fetching drafts (" ++ e ++ ")" }, Cmd.none )
+
                 _ ->
                     ( ChooseSet mdl, Cmd.none )
 
@@ -359,6 +402,9 @@ update msg model =
                 IOGotDeleteSetData setCode ->
                     ( Ready mdl, Cmd.none )
 
+                IOGotDrafts _ ->
+                    ( Ready mdl, Cmd.none )
+
                 PortReceiveDoesSetHaveLocalData _ ->
                     ( Ready mdl, Cmd.none )
 
@@ -368,6 +414,12 @@ update msg model =
                 OpenCardExplorer _ _ ->
                     ( Ready mdl, Cmd.none )
 
+                SetEventHistoryUrlField _ ->
+                    ( Ready mdl, Cmd.none )
+
+                SubmitEventHistoryUrlField ->
+                    ( Ready mdl, Cmd.none )
+
         CardExplorer mdl ->
             case msg of
                 Highlight card ->
@@ -434,8 +486,13 @@ viewChooseSet model =
                         , Button.make "Delete" (IODeleteSetData setCode) |> Button.view
                         ]
     in
-    div [ class "w-full h-full bg-slate-100 flex justify-center items-center" ]
-        [ div [ class "max-w-2xl max-h-2xl bg-slate-500 rounded-lg p-6 shadow-xl" ]
+    div [ class "w-full h-full bg-slate-100 flex flex-col gap-4 justify-center items-center" ]
+        [ 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" ]
+            , p [ class "text-white font-medium" ] [ text "Explore sets and analyse your draft events." ]
+            , viewEventHistoryURL model
+            ]
+        , div [ class "max-w-4xl max-h-2xl bg-slate-500 rounded-lg p-6 shadow-xl" ]
             [ ul [ class "divide-y divide-slate-600" ]
                 (List.map
                     (\s ->
@@ -455,6 +512,28 @@ 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 ->
+            div [ class "mt-4" ]
+                [ label [ class "block text-sm font-medium text-white" ] [ text "Enter your 17Lands public event history URL:" ]
+                , input
+                    [ type_ "text"
+                    , onInput SetEventHistoryUrlField
+                    , value model.eventHistoryUrlField
+                    ]
+                    []
+                , Button.make "Save" SubmitEventHistoryUrlField |> Button.view
+                ]
+
+
 viewReady : ReadyModel -> Html Msg
 viewReady model =
     div [ class "grid grid-cols-12 gap-6 h-full bg-slate-100" ]
@@ -1066,3 +1145,6 @@ port sendDeleteLocalData : Encode.Value -> Cmd msg
 
 
 port receiveDidDeleteLocalData : (String -> msg) -> Sub msg
+
+
+port sendSaveEventHistoryUrl : String -> Cmd msg