Add deck initial editor page with card listing
This commit is contained in:
parent
32e213a16d
commit
d51554f435
8 changed files with 345 additions and 12 deletions
|
@ -339,7 +339,7 @@ async def store_deck_card(
|
|||
) -> None:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO "deck_cards" ("deck_id", "oracle_id", "quantity")
|
||||
INSERT INTO "deck_list" ("deck_id", "oracle_id", "quantity")
|
||||
VALUES (%(deck_id)s, %(oracle_id)s, %(quantity)s)
|
||||
""",
|
||||
{"deck_id": deck_id, "oracle_id": str(oracle_id), "quantity": quantity},
|
||||
|
@ -352,7 +352,25 @@ async def get_decks(
|
|||
db.row_factory = psycopg.rows.dict_row
|
||||
await db.execute(
|
||||
"""
|
||||
SELECT * FROM "decks"
|
||||
SELECT "decks"."deck_id"
|
||||
, "decks"."name"
|
||||
, JSON_STRIP_NULLS(JSON_AGG(JSON_BUILD_OBJECT(
|
||||
'oracle_id', "deck_list"."oracle_id",
|
||||
'name', "oracle"."name",
|
||||
'color_identity', "oracle"."color_identity",
|
||||
'cmc', "oracle"."cmc",
|
||||
'type_line', "oracle"."type_line",
|
||||
'edhrec_rank', "oracle"."edhrec_rank",
|
||||
'oracle_text', "oracle"."oracle_text",
|
||||
'games', "oracle"."games",
|
||||
'legalities', "oracle"."legalities",
|
||||
'quantity', "deck_list"."quantity"
|
||||
))) AS "cards"
|
||||
FROM "decks"
|
||||
LEFT JOIN "deck_list" USING ("deck_id")
|
||||
LEFT JOIN "oracle" USING ("oracle_id")
|
||||
GROUP BY "decks"."deck_id"
|
||||
, "decks"."name"
|
||||
ORDER BY "decks"."deck_id"
|
||||
LIMIT %(limit)s OFFSET %(offset)s
|
||||
""",
|
||||
|
@ -360,11 +378,91 @@ async def get_decks(
|
|||
)
|
||||
rows = await db.fetchall()
|
||||
return [
|
||||
tutor.models.Deck(deck_id=row["deck_id"], name=row["name"], cards=[])
|
||||
tutor.models.Deck(
|
||||
deck_id=row["deck_id"],
|
||||
name=row["name"],
|
||||
cards=[
|
||||
tutor.models.DeckCard(
|
||||
card=tutor.models.OracleCard(
|
||||
oracle_id=card["oracle_id"],
|
||||
name=card["name"],
|
||||
color_identity=tutor.models.Color.from_string(
|
||||
card["color_identity"]
|
||||
),
|
||||
cmc=card["cmc"],
|
||||
type_line=card["type_line"],
|
||||
games=set(),
|
||||
legalities={},
|
||||
edhrec_rank=card.get("edhrec_rank"),
|
||||
oracle_text=card.get("oracle_text"),
|
||||
),
|
||||
quantity=card["quantity"],
|
||||
)
|
||||
for card in row["cards"]
|
||||
if card and card.get("oracle_id")
|
||||
],
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
async def get_deck(
|
||||
db: psycopg.Cursor, deck_id: int
|
||||
) -> typing.Optional[tutor.models.Deck]:
|
||||
db.row_factory = psycopg.rows.dict_row
|
||||
await db.execute(
|
||||
"""
|
||||
SELECT "decks"."deck_id"
|
||||
, "decks"."name"
|
||||
, JSON_STRIP_NULLS(JSON_AGG(JSON_BUILD_OBJECT(
|
||||
'oracle_id', "deck_list"."oracle_id",
|
||||
'name', "oracle"."name",
|
||||
'color_identity', "oracle"."color_identity",
|
||||
'cmc', "oracle"."cmc",
|
||||
'type_line', "oracle"."type_line",
|
||||
'edhrec_rank', "oracle"."edhrec_rank",
|
||||
'oracle_text', "oracle"."oracle_text",
|
||||
'games', "oracle"."games",
|
||||
'legalities', "oracle"."legalities",
|
||||
'quantity', "deck_list"."quantity"
|
||||
))) AS "cards"
|
||||
FROM "decks"
|
||||
LEFT JOIN "deck_list" USING ("deck_id")
|
||||
LEFT JOIN "oracle" USING ("oracle_id")
|
||||
WHERE "decks"."deck_id" = %(deck_id)s
|
||||
GROUP BY "decks"."deck_id"
|
||||
, "decks"."name"
|
||||
""",
|
||||
{"deck_id": deck_id},
|
||||
)
|
||||
row = await db.fetchone()
|
||||
if row:
|
||||
return tutor.models.Deck(
|
||||
deck_id=row["deck_id"],
|
||||
name=row["name"],
|
||||
cards=[
|
||||
tutor.models.DeckCard(
|
||||
card=tutor.models.OracleCard(
|
||||
oracle_id=card["oracle_id"],
|
||||
name=card["name"],
|
||||
color_identity=tutor.models.Color.from_string(
|
||||
card["color_identity"]
|
||||
),
|
||||
cmc=card["cmc"],
|
||||
type_line=card["type_line"],
|
||||
games=set(),
|
||||
legalities={},
|
||||
edhrec_rank=card.get("edhrec_rank"),
|
||||
oracle_text=card.get("oracle_text"),
|
||||
),
|
||||
quantity=card["quantity"],
|
||||
)
|
||||
for card in row["cards"]
|
||||
if card and card.get("oracle_id")
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
async def store_var(db: psycopg.Cursor, key: str, value: str) -> None:
|
||||
await db.execute(
|
||||
"""
|
||||
|
|
|
@ -183,7 +183,20 @@ class DecksHandler(RequestHandler):
|
|||
{
|
||||
"deck_id": deck.deck_id,
|
||||
"name": deck.name,
|
||||
"cards": [],
|
||||
"cards": [
|
||||
{
|
||||
"quantity": card.quantity,
|
||||
"card": {
|
||||
"oracle_id": str(card.card.oracle_id),
|
||||
"name": card.card.name,
|
||||
"color_identity": tutor.models.Color.to_string(
|
||||
card.card.color_identity
|
||||
),
|
||||
"oracle_text": card.card.oracle_text,
|
||||
},
|
||||
}
|
||||
for card in deck.cards
|
||||
],
|
||||
}
|
||||
for deck in decks
|
||||
]
|
||||
|
@ -194,6 +207,42 @@ class DecksHandler(RequestHandler):
|
|||
...
|
||||
|
||||
|
||||
class DeckHandler(RequestHandler):
|
||||
async def get(self, deck_id) -> None:
|
||||
self.set_header("Content-Type", "application/json")
|
||||
self.set_header("Access-Control-Allow-Origin", "*")
|
||||
async with self.application.pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
deck = await tutor.database.get_deck(cursor, deck_id)
|
||||
if not deck:
|
||||
raise tornado.web.HTTPError(404)
|
||||
self.write(
|
||||
json.dumps(
|
||||
{
|
||||
"deck_id": deck.deck_id,
|
||||
"name": deck.name,
|
||||
"cards": [
|
||||
{
|
||||
"quantity": card.quantity,
|
||||
"card": {
|
||||
"oracle_id": str(card.card.oracle_id),
|
||||
"name": card.card.name,
|
||||
"color_identity": tutor.models.Color.to_string(
|
||||
card.card.color_identity
|
||||
),
|
||||
"oracle_text": card.card.oracle_text,
|
||||
},
|
||||
}
|
||||
for card in deck.cards
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
async def put(self, deck_id) -> None:
|
||||
...
|
||||
|
||||
|
||||
class TemplateHandler(RequestHandler):
|
||||
def initialize(
|
||||
self,
|
||||
|
@ -214,7 +263,14 @@ class StaticFileHandler(tornado.web.StaticFileHandler):
|
|||
@classmethod
|
||||
def get_absolute_path(cls, root: str, path: str) -> str:
|
||||
# Rewrite paths to load the index
|
||||
if path in ("collection", "decks", "decks/new123123"):
|
||||
if any(
|
||||
re.match(pattern, path)
|
||||
for pattern in [
|
||||
r"^collection$",
|
||||
r"^decks$",
|
||||
r"^decks/.*",
|
||||
]
|
||||
):
|
||||
path = "index.html"
|
||||
return tornado.web.StaticFileHandler.get_absolute_path(root, path)
|
||||
|
||||
|
@ -243,12 +299,13 @@ class Application(tornado.web.Application):
|
|||
(r"/api/search", SearchHandler),
|
||||
(r"/api/collection", CollectionHandler),
|
||||
(r"/api/decks", DecksHandler),
|
||||
(r"/api/decks/([1-9][0-9]*)", DeckHandler),
|
||||
]
|
||||
if static_path := settings.get("static"):
|
||||
paths.extend(
|
||||
[
|
||||
(
|
||||
fr"/(.*)",
|
||||
rf"/(.*)",
|
||||
StaticFileHandler,
|
||||
{"path": static_path, "default_filename": "index.html"},
|
||||
),
|
||||
|
|
|
@ -18,6 +18,7 @@ import Http
|
|||
import Json.Decode
|
||||
import Maybe.Extra
|
||||
import Pages.Collection
|
||||
import Pages.DeckEditor
|
||||
import Pages.DeckList
|
||||
import Paginated
|
||||
import Route
|
||||
|
@ -46,6 +47,7 @@ type Msg
|
|||
| LinkClicked Browser.UrlRequest
|
||||
| CollectionMsg Pages.Collection.Msg
|
||||
| DeckListMsg Pages.DeckList.Msg
|
||||
| DeckEditorMsg Pages.DeckEditor.Msg
|
||||
| SpinnerMsg Spinner.Msg
|
||||
|
||||
|
||||
|
@ -53,6 +55,7 @@ type Page
|
|||
= NotFound
|
||||
| Collection Pages.Collection.Model
|
||||
| DeckList Pages.DeckList.Model
|
||||
| DeckEditor Pages.DeckEditor.Model
|
||||
|
||||
|
||||
init : Json.Decode.Value -> Url.Url -> Browser.Navigation.Key -> ( Model, Cmd Msg )
|
||||
|
@ -81,6 +84,10 @@ init _ url key =
|
|||
initWith DeckList DeckListMsg <|
|
||||
Pages.DeckList.init key url device
|
||||
|
||||
Just (Route.Deck deckId) ->
|
||||
initWith DeckEditor DeckEditorMsg <|
|
||||
Pages.DeckEditor.init key url device deckId
|
||||
|
||||
_ ->
|
||||
( NotFound, Cmd.none )
|
||||
in
|
||||
|
@ -128,6 +135,10 @@ update msg model =
|
|||
Pages.DeckList.init model.navigationKey url model.device
|
||||
|> updateWith DeckList DeckListMsg model
|
||||
|
||||
( _, Just (Route.Deck deckId) ) ->
|
||||
Pages.DeckEditor.init model.navigationKey url model.device deckId
|
||||
|> updateWith DeckEditor DeckEditorMsg model
|
||||
|
||||
_ ->
|
||||
( { model | page = NotFound }, Cmd.none )
|
||||
in
|
||||
|
@ -161,6 +172,10 @@ update msg model =
|
|||
Pages.DeckList.update pageMsg pageModel
|
||||
|> updateWith DeckList DeckListMsg model
|
||||
|
||||
( DeckEditorMsg pageMsg, DeckEditor pageModel ) ->
|
||||
Pages.DeckEditor.update pageMsg pageModel
|
||||
|> updateWith DeckEditor DeckEditorMsg model
|
||||
|
||||
( _, _ ) ->
|
||||
( model, Cmd.none )
|
||||
|
||||
|
@ -210,6 +225,9 @@ view model =
|
|||
DeckList pageModel ->
|
||||
E.map DeckListMsg <| Pages.DeckList.view pageModel
|
||||
|
||||
DeckEditor pageModel ->
|
||||
E.map DeckEditorMsg <| Pages.DeckEditor.view pageModel
|
||||
|
||||
NotFound ->
|
||||
E.column
|
||||
[ E.width E.fill
|
||||
|
@ -265,6 +283,9 @@ subscriptions model =
|
|||
DeckList pageModel ->
|
||||
Sub.batch (Sub.map DeckListMsg (Pages.DeckList.subscriptions pageModel) :: global)
|
||||
|
||||
DeckEditor pageModel ->
|
||||
Sub.batch (Sub.map DeckEditorMsg (Pages.DeckEditor.subscriptions pageModel) :: global)
|
||||
|
||||
_ ->
|
||||
Sub.batch global
|
||||
|
||||
|
|
|
@ -13,6 +13,13 @@ type alias Prices =
|
|||
}
|
||||
|
||||
|
||||
type alias Oracle =
|
||||
{ oracleId : String
|
||||
, name : String
|
||||
, oracleText : String
|
||||
}
|
||||
|
||||
|
||||
type alias Card =
|
||||
{ scryfallId : String
|
||||
, name : String
|
||||
|
@ -25,6 +32,17 @@ type alias Card =
|
|||
}
|
||||
|
||||
|
||||
decodeOracle : Json.Decode.Decoder Oracle
|
||||
decodeOracle =
|
||||
Json.Decode.succeed Oracle
|
||||
|> JDP.required "oracle_id" Json.Decode.string
|
||||
|> JDP.required "name" Json.Decode.string
|
||||
|> JDP.required "oracle_text"
|
||||
(Json.Decode.nullable Json.Decode.string
|
||||
|> Json.Decode.map (Maybe.withDefault "")
|
||||
)
|
||||
|
||||
|
||||
decode : Json.Decode.Decoder Card
|
||||
decode =
|
||||
Json.Decode.succeed Card
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
module Deck exposing (..)
|
||||
|
||||
import Card
|
||||
import Json.Decode
|
||||
import Json.Decode.Pipeline as JDP
|
||||
|
||||
|
@ -7,6 +8,13 @@ import Json.Decode.Pipeline as JDP
|
|||
type alias Deck =
|
||||
{ id : Int
|
||||
, name : String
|
||||
, cards : List Card
|
||||
}
|
||||
|
||||
|
||||
type alias Card =
|
||||
{ oracle : Card.Oracle
|
||||
, quantity : Int
|
||||
}
|
||||
|
||||
|
||||
|
@ -15,3 +23,11 @@ decode =
|
|||
Json.Decode.succeed Deck
|
||||
|> JDP.required "deck_id" Json.Decode.int
|
||||
|> JDP.required "name" Json.Decode.string
|
||||
|> JDP.required "cards" (Json.Decode.list decodeCard)
|
||||
|
||||
|
||||
decodeCard : Json.Decode.Decoder Card
|
||||
decodeCard =
|
||||
Json.Decode.succeed Card
|
||||
|> JDP.required "card" Card.decodeOracle
|
||||
|> JDP.required "quantity" Json.Decode.int
|
||||
|
|
|
@ -1,8 +1,125 @@
|
|||
module DeckEditor exposing (..)
|
||||
module Pages.DeckEditor exposing (..)
|
||||
|
||||
import Card
|
||||
import Browser
|
||||
import Browser.Events
|
||||
import Browser.Navigation
|
||||
import Deck
|
||||
import Element as E
|
||||
import Element.Background as Background
|
||||
import Element.Events as Events
|
||||
import Element.Font as Font
|
||||
import Http
|
||||
import Paginated
|
||||
import Route
|
||||
import Spinner
|
||||
import UI
|
||||
import Url
|
||||
import Url.Builder
|
||||
|
||||
|
||||
type alias Deck =
|
||||
{ cards : List Card.Card
|
||||
type alias Model =
|
||||
{ navigationKey : Browser.Navigation.Key
|
||||
, url : Url.Url
|
||||
, device : E.Device
|
||||
, spinner : Spinner.Model
|
||||
, deck : Deck
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= ViewportChanged UI.Dimensions
|
||||
| SpinnerMsg Spinner.Msg
|
||||
| GotDeck (Result Http.Error Deck.Deck)
|
||||
|
||||
|
||||
type Deck
|
||||
= Ready Deck.Deck
|
||||
| Loading
|
||||
| NotFound
|
||||
| Failed
|
||||
|
||||
|
||||
init : Browser.Navigation.Key -> Url.Url -> E.Device -> Int -> ( Model, Cmd Msg )
|
||||
init key url device deckId =
|
||||
( { navigationKey = key
|
||||
, url = url
|
||||
, device = device
|
||||
, spinner = Spinner.init
|
||||
, deck = Loading
|
||||
}
|
||||
, getDeck deckId
|
||||
)
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
ViewportChanged viewport ->
|
||||
( { model
|
||||
| device = E.classifyDevice viewport
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
SpinnerMsg msg_ ->
|
||||
( { model | spinner = Spinner.update msg_ model.spinner }, Cmd.none )
|
||||
|
||||
GotDeck (Ok deck) ->
|
||||
( { model | deck = Ready deck }, Cmd.none )
|
||||
|
||||
GotDeck (Err _) ->
|
||||
( { model | deck = Failed }, Cmd.none )
|
||||
|
||||
|
||||
view : Model -> E.Element Msg
|
||||
view model =
|
||||
case model.deck of
|
||||
Failed ->
|
||||
E.none
|
||||
|
||||
NotFound ->
|
||||
E.none
|
||||
|
||||
Loading ->
|
||||
E.el [ E.height E.fill, E.centerX ] <|
|
||||
E.html <|
|
||||
Spinner.view UI.manaSpinner
|
||||
model.spinner
|
||||
|
||||
Ready deck ->
|
||||
let
|
||||
cardRow card =
|
||||
E.row
|
||||
[ E.width E.fill
|
||||
, E.spacing 10
|
||||
, E.padding 3
|
||||
]
|
||||
[ E.column [ E.centerY, E.height E.fill, E.width E.fill, E.clipX ]
|
||||
[ UI.title card.oracle.name
|
||||
, UI.subtitle ("x" ++ String.fromInt card.quantity)
|
||||
]
|
||||
]
|
||||
in
|
||||
E.column [ E.width E.fill, E.centerX, E.spacing 5 ] <|
|
||||
[ E.paragraph [ Font.heavy, Font.size 24, Font.center ] [ UI.title deck.name ] ]
|
||||
++ List.map cardRow deck.cards
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Sub.batch
|
||||
[ Browser.Events.onResize
|
||||
(\w h -> ViewportChanged { width = w, height = h })
|
||||
, Sub.map SpinnerMsg Spinner.subscription
|
||||
]
|
||||
|
||||
|
||||
getDeck : Int -> Cmd Msg
|
||||
getDeck deckId =
|
||||
Http.get
|
||||
{ url =
|
||||
Url.Builder.absolute
|
||||
[ "api", "decks", String.fromInt deckId ]
|
||||
[]
|
||||
, expect = Http.expectJson GotDeck Deck.decode
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ view model =
|
|||
E.row
|
||||
[ E.width E.fill
|
||||
, E.padding 10
|
||||
, E.spacing 10
|
||||
, E.spacing 3
|
||||
, E.pointer
|
||||
, E.mouseOver [ Background.color UI.colors.hover ]
|
||||
, Events.onClick <| DeckClicked deck.id
|
||||
|
|
|
@ -4,6 +4,7 @@ module UI exposing
|
|||
, getViewport
|
||||
, isMobile
|
||||
, manaSpinner
|
||||
, subtitle
|
||||
, text
|
||||
, title
|
||||
)
|
||||
|
@ -140,3 +141,8 @@ text string =
|
|||
title : String -> E.Element msg
|
||||
title string =
|
||||
E.el [ Font.color colors.title ] <| E.text string
|
||||
|
||||
|
||||
subtitle : String -> E.Element msg
|
||||
subtitle string =
|
||||
E.el [ Font.size 16, Font.italic, Font.color colors.subtitle ] <| E.text string
|
||||
|
|
Loading…
Reference in a new issue