Эх сурвалжийг харах

WIP: load drafts straight from 17lands

Cadel Watson 4 сар өмнө
parent
commit
3f2d7f6622
7 өөрчлөгдсөн 262 нэмэгдсэн , 52 устгасан
  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);
 const openDBRequest = indexedDB.open("set_database", 2);
 
 
 openDBRequest.onerror = (event) => {
 openDBRequest.onerror = (event) => {
@@ -78,11 +86,10 @@ openDBRequest.onsuccess = (event) => {
             flags: {
             flags: {
                 sets: sets,
                 sets: sets,
                 draftData: JSON.stringify(draftData),
                 draftData: JSON.stringify(draftData),
+                eventHistoryUrl: getSavedEventHistoryUrl()
             }
             }
         });
         });
 
 
-        console.log(app.ports);
-
         app.ports.sendDoesSetHaveLocalData.subscribe((setCode) => {
         app.ports.sendDoesSetHaveLocalData.subscribe((setCode) => {
             getSetData(database, setCode, (data) => {
             getSetData(database, setCode, (data) => {
                 app.ports.receiveDoesSetHaveLocalData.send(JSON.stringify(data));
                 app.ports.receiveDoesSetHaveLocalData.send(JSON.stringify(data));
@@ -91,12 +98,15 @@ openDBRequest.onsuccess = (event) => {
 
 
         app.ports.sendSaveLocalData.subscribe((setData) => {
         app.ports.sendSaveLocalData.subscribe((setData) => {
             saveSetData(database, setData);
             saveSetData(database, setData);
-        })
+        });
 
 
         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);
+        });
     })
     })
 };
 };
 
 

+ 81 - 0
server/main.py

@@ -1,6 +1,10 @@
+import asyncio
 import json
 import json
 import os
 import os
+import re
+from typing import TypedDict, List, Dict, Any
 
 
+import httpx
 from fastapi import FastAPI
 from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.middleware.cors import CORSMiddleware
 
 
@@ -80,3 +84,80 @@ def read_item(set_name: str):
         ratings_data = json.load(f)
         ratings_data = json.load(f)
 
 
     return {"code": set_name, "data": {"cards": card_data, "ratings": ratings_data}}
     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 Database exposing (Database)
+import DraftMeta exposing (DraftMeta)
 import Http
 import Http
 import Json.Decode as Decode
 import Json.Decode as Decode
+import Url.Builder as UrlB
 
 
 
 
 apiUrl : String
 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 : Http.Error -> String
 httpErrorToString error =
 httpErrorToString error =
     case error of
     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 as Decode exposing (Decoder)
 import Json.Decode.Pipeline as Decode
 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 Deck
 import Dict exposing (Dict)
 import Dict exposing (Dict)
 import Draft exposing (Draft)
 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 Html.Keyed as Keyed
 import Icon exposing (chevronDown, chevronUp)
 import Icon exposing (chevronDown, chevronUp)
 import Json.Decode exposing (decodeString)
 import Json.Decode exposing (decodeString)
@@ -72,9 +73,18 @@ type SetLoadStatus
     | DeletingLocalData
     | DeletingLocalData
 
 
 
 
+type AvailableDrafts
+    = DraftsNotChecked
+    | DraftsLoading
+    | AvailableDrafts (List 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
     }
     }
 
 
 
 
@@ -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 =
 init flags =
     let
     let
         setStatus : Dict String SetLoadStatus
         setStatus : Dict String SetLoadStatus
@@ -119,12 +129,24 @@ 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
         }
         }
-    , Cmd.batch (API.getSets IOGotSets :: setCmds)
+    , Cmd.batch (API.getSets IOGotSets :: setCmds ++ draftCmds)
     )
     )
 
 
 
 
@@ -132,6 +154,8 @@ type Msg
     = Increment
     = Increment
     | Decrement
     | Decrement
     | Highlight String
     | Highlight String
+    | SetEventHistoryUrlField String
+    | SubmitEventHistoryUrlField
     | FlipHighlightedCard
     | FlipHighlightedCard
     | SetFocusStat FocusStat
     | SetFocusStat FocusStat
     | SetSortOrder SortOrder
     | SetSortOrder SortOrder
@@ -142,6 +166,7 @@ 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))
     | 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
@@ -281,6 +306,24 @@ 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 )
+
+                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 )
                     ( ChooseSet mdl, Cmd.none )
 
 
@@ -359,6 +402,9 @@ update msg model =
                 IOGotDeleteSetData setCode ->
                 IOGotDeleteSetData setCode ->
                     ( Ready mdl, Cmd.none )
                     ( Ready mdl, Cmd.none )
 
 
+                IOGotDrafts _ ->
+                    ( Ready mdl, Cmd.none )
+
                 PortReceiveDoesSetHaveLocalData _ ->
                 PortReceiveDoesSetHaveLocalData _ ->
                     ( Ready mdl, Cmd.none )
                     ( Ready mdl, Cmd.none )
 
 
@@ -368,6 +414,12 @@ update msg model =
                 OpenCardExplorer _ _ ->
                 OpenCardExplorer _ _ ->
                     ( Ready mdl, Cmd.none )
                     ( Ready mdl, Cmd.none )
 
 
+                SetEventHistoryUrlField _ ->
+                    ( Ready mdl, Cmd.none )
+
+                SubmitEventHistoryUrlField ->
+                    ( Ready mdl, Cmd.none )
+
         CardExplorer mdl ->
         CardExplorer mdl ->
             case msg of
             case msg of
                 Highlight card ->
                 Highlight card ->
@@ -434,8 +486,13 @@ viewChooseSet model =
                         , Button.make "Delete" (IODeleteSetData setCode) |> Button.view
                         , Button.make "Delete" (IODeleteSetData setCode) |> Button.view
                         ]
                         ]
     in
     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" ]
             [ ul [ class "divide-y divide-slate-600" ]
                 (List.map
                 (List.map
                     (\s ->
                     (\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 : ReadyModel -> Html Msg
 viewReady model =
 viewReady model =
     div [ class "grid grid-cols-12 gap-6 h-full bg-slate-100" ]
     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 receiveDidDeleteLocalData : (String -> msg) -> Sub msg
+
+
+port sendSaveEventHistoryUrl : String -> Cmd msg