Add price support
This commit is contained in:
parent
eb61663888
commit
0140c9c889
9 changed files with 206 additions and 26 deletions
11
tables.sql
11
tables.sql
|
@ -24,6 +24,17 @@ CREATE TABLE IF NOT EXISTS `cards` (
|
||||||
`oracle_text` TEXT
|
`oracle_text` TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `card_prices` (
|
||||||
|
`scryfall_id` TEXT,
|
||||||
|
`date` TEXT,
|
||||||
|
`usd` TEXT, -- Decimal value
|
||||||
|
`usd_foil` TEXT, -- Decimal value
|
||||||
|
`eur` TEXT, -- Decimal value
|
||||||
|
`eur_foil` TEXT, -- Decimal value
|
||||||
|
`tix` TEXT, -- Decimal value
|
||||||
|
PRIMARY KEY (`scryfall_id`, `date`)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `sets` (
|
CREATE TABLE IF NOT EXISTS `sets` (
|
||||||
`set_code` TEXT PRIMARY KEY,
|
`set_code` TEXT PRIMARY KEY,
|
||||||
`name` TEXT NOT NULL
|
`name` TEXT NOT NULL
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -98,18 +99,22 @@ def update_scryfall(ctx, filename):
|
||||||
cards = json.loads(buffer)
|
cards = json.loads(buffer)
|
||||||
|
|
||||||
async def import_cards():
|
async def import_cards():
|
||||||
|
today = datetime.date.today()
|
||||||
async with aiosqlite.connect(ctx.obj["database"]) as db:
|
async with aiosqlite.connect(ctx.obj["database"]) as db:
|
||||||
with click.progressbar(
|
with click.progressbar(
|
||||||
cards,
|
cards,
|
||||||
label=f"Importing {humanize.intcomma(len(cards))} cards",
|
label=f"Importing {humanize.intcomma(len(cards))} cards",
|
||||||
) as bar:
|
) as bar:
|
||||||
for card_object in bar:
|
for card_object in bar:
|
||||||
|
card = tutor.scryfall.to_card(card_object)
|
||||||
await tutor.database.store_card(
|
await tutor.database.store_card(
|
||||||
db, tutor.scryfall.to_card(card_object)
|
db, card
|
||||||
)
|
)
|
||||||
|
await tutor.database.store_price(db, today, card)
|
||||||
await tutor.database.store_set(
|
await tutor.database.store_set(
|
||||||
db, card_object["set"].upper(), card_object["set_name"]
|
db, card_object["set"].upper(), card_object["set_name"]
|
||||||
)
|
)
|
||||||
|
await tutor.database.store_var(db, "last_update", str(today))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
tornado.ioloop.IOLoop.current().run_sync(import_cards)
|
tornado.ioloop.IOLoop.current().run_sync(import_cards)
|
||||||
|
|
|
@ -94,6 +94,7 @@ async def advanced_search(
|
||||||
search: tutor.search.Search,
|
search: tutor.search.Search,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
|
sort_by: str = "rarity",
|
||||||
in_collection: typing.Optional[bool] = None,
|
in_collection: typing.Optional[bool] = None,
|
||||||
) -> typing.List[tutor.models.Card]:
|
) -> typing.List[tutor.models.Card]:
|
||||||
db.row_factory = aiosqlite.Row
|
db.row_factory = aiosqlite.Row
|
||||||
|
@ -119,20 +120,18 @@ async def advanced_search(
|
||||||
constraints.append(f"cards.color_identity LIKE :{param}")
|
constraints.append(f"cards.color_identity LIKE :{param}")
|
||||||
params[param] = tutor.models.Color.to_string(criterion.colors)
|
params[param] = tutor.models.Color.to_string(criterion.colors)
|
||||||
if criterion.operator == tutor.search.Operator.lte:
|
if criterion.operator == tutor.search.Operator.lte:
|
||||||
|
colors = list({str(color) for color in criterion.colors} | {""})
|
||||||
constraints.append(
|
constraints.append(
|
||||||
"({})".format(
|
"({})".format(
|
||||||
" OR ".join(
|
" OR ".join(
|
||||||
[
|
[
|
||||||
f"cards.color_identity LIKE :{param}_{color}"
|
f"cards.color_identity LIKE :{param}_{color}"
|
||||||
for color in criterion.colors
|
for color in colors
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
params.update(
|
params.update({f"{param}_{color}": color for color in colors})
|
||||||
{f"{param}_{color}": str(color) for color in criterion.colors}
|
|
||||||
)
|
|
||||||
params[param] = tutor.models.Color.to_string(criterion.colors)
|
|
||||||
if criterion.operator == tutor.search.Operator.gte:
|
if criterion.operator == tutor.search.Operator.gte:
|
||||||
constraints.append(f"cards.color_identity LIKE :{param}")
|
constraints.append(f"cards.color_identity LIKE :{param}")
|
||||||
params[param] = "%{}%".format(
|
params[param] = "%{}%".format(
|
||||||
|
@ -160,24 +159,45 @@ async def advanced_search(
|
||||||
constraints.append("copies.id IS NULL")
|
constraints.append("copies.id IS NULL")
|
||||||
joins.append("JOIN sets ON (cards.set_code = sets.set_code)")
|
joins.append("JOIN sets ON (cards.set_code = sets.set_code)")
|
||||||
joins.append("JOIN rarities ON (cards.rarity = rarities.rarity)")
|
joins.append("JOIN rarities ON (cards.rarity = rarities.rarity)")
|
||||||
|
joins.append(
|
||||||
|
"JOIN card_prices ON (cards.scryfall_id = card_prices.scryfall_id "
|
||||||
|
"AND card_prices.date = (select value from vars where key = :last_update_key))"
|
||||||
|
)
|
||||||
|
orderings = [
|
||||||
|
"rarities.rarity_ord DESC",
|
||||||
|
"length(cards.color_identity) DESC",
|
||||||
|
"CASE "
|
||||||
|
" WHEN length(cards.color_identity) > 0 THEN '0'"
|
||||||
|
" ELSE cards.color_identity END ASC",
|
||||||
|
"cards.name ASC",
|
||||||
|
]
|
||||||
|
if sort_by == "price":
|
||||||
|
orderings = [
|
||||||
|
"CAST(COALESCE(card_prices.usd, card_prices.usd_foil) as decimal) DESC",
|
||||||
|
*orderings,
|
||||||
|
]
|
||||||
|
params["last_update_key"] = "last_update"
|
||||||
query = " ".join(
|
query = " ".join(
|
||||||
[
|
[
|
||||||
"SELECT cards.* FROM cards",
|
"SELECT cards.*, card_prices.* FROM cards",
|
||||||
" ".join(joins),
|
" ".join(joins),
|
||||||
"WHERE" if constraints else "",
|
"WHERE" if constraints else "",
|
||||||
" AND ".join(constraints),
|
" AND ".join(constraints),
|
||||||
"ORDER BY rarities.rarity_ord DESC",
|
"ORDER BY " if orderings else "",
|
||||||
", length(cards.color_identity) DESC",
|
", ".join(orderings),
|
||||||
", CASE ",
|
|
||||||
" WHEN length(cards.color_identity) > 0 THEN '0'"
|
|
||||||
" ELSE cards.color_identity END ASC",
|
|
||||||
", cards.name ASC",
|
|
||||||
f"LIMIT {offset},{limit}",
|
f"LIMIT {offset},{limit}",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
logger.debug("Query: %s", (query, params))
|
logger.debug("Query: %s", (query, params))
|
||||||
cursor = await db.execute(query, params)
|
cursor = await db.execute(query, params)
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
|
|
||||||
|
def convert_price(price: typing.Optional[str]) -> typing.Optional[decimal.Decimal]:
|
||||||
|
if price:
|
||||||
|
return decimal.Decimal(price)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
return [
|
return [
|
||||||
tutor.models.Card(
|
tutor.models.Card(
|
||||||
scryfall_id=uuid.UUID(row["scryfall_id"]),
|
scryfall_id=uuid.UUID(row["scryfall_id"]),
|
||||||
|
@ -193,6 +213,11 @@ async def advanced_search(
|
||||||
legalities={},
|
legalities={},
|
||||||
edhrec_rank=row["edhrec_rank"],
|
edhrec_rank=row["edhrec_rank"],
|
||||||
oracle_text=row["oracle_text"],
|
oracle_text=row["oracle_text"],
|
||||||
|
price_usd=convert_price(row["usd"]),
|
||||||
|
price_usd_foil=convert_price(row["usd_foil"]),
|
||||||
|
price_eur=convert_price(row["eur"]),
|
||||||
|
price_eur_foil=convert_price(row["eur_foil"]),
|
||||||
|
price_tix=convert_price(row["tix"]),
|
||||||
)
|
)
|
||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
|
@ -253,6 +278,31 @@ async def store_card(db: aiosqlite.Connection, card: tutor.models.Card) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def store_price(
|
||||||
|
db: aiosqlite.Connection, date: datetime.date, card: tutor.models.Card
|
||||||
|
) -> None:
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO `card_prices`"
|
||||||
|
" (`scryfall_id`, `date`, `usd`, `usd_foil`, `eur`, `eur_foil`, `tix`) "
|
||||||
|
"VALUES (:scryfall_id, :date, :usd, :usd_foil, :eur, :eur_foil, :tix) "
|
||||||
|
"ON CONFLICT (`scryfall_id`, `date`) DO UPDATE "
|
||||||
|
"SET `usd` = :usd"
|
||||||
|
" , `usd_foil` = :usd_foil"
|
||||||
|
" , `eur` = :eur"
|
||||||
|
" , `eur_foil` = :eur_foil"
|
||||||
|
" , `tix` = :tix",
|
||||||
|
{
|
||||||
|
"scryfall_id": card.scryfall_id,
|
||||||
|
"date": str(date),
|
||||||
|
"usd": str(card.price_usd) if card.price_usd else None,
|
||||||
|
"usd_foil": str(card.price_usd_foil) if card.price_usd_foil else None,
|
||||||
|
"eur": str(card.price_eur) if card.price_eur else None,
|
||||||
|
"eur_foil": str(card.price_eur_foil) if card.price_eur_foil else None,
|
||||||
|
"tix": str(card.price_tix) if card.price_tix else None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def store_set(db: aiosqlite.Connection, set_code: str, name: str) -> None:
|
async def store_set(db: aiosqlite.Connection, set_code: str, name: str) -> None:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT INTO `sets` (`set_code`, `name`) "
|
"INSERT INTO `sets` (`set_code`, `name`) "
|
||||||
|
@ -272,3 +322,13 @@ async def store_copy(db: aiosqlite.Connection, copy: tutor.models.CardCopy) -> N
|
||||||
"condition": copy.condition,
|
"condition": copy.condition,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def store_var(db: aiosqlite.Connection, key: str, value: str) -> None:
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO `vars` (`key`, `value`)"
|
||||||
|
"VALUES (:key, :value)"
|
||||||
|
"ON CONFLICT (`key`) DO UPDATE "
|
||||||
|
"SET `value` = :value",
|
||||||
|
{"key": key, "value": value},
|
||||||
|
)
|
||||||
|
|
|
@ -48,17 +48,20 @@ class Rarity(enum.IntEnum):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise TypeError(f"Unknown rarity '{rarity}'")
|
raise TypeError(f"Unknown rarity '{rarity}'")
|
||||||
|
|
||||||
|
|
||||||
class Game(enum.Enum):
|
class Game(enum.Enum):
|
||||||
Paper = "paper"
|
Paper = "paper"
|
||||||
Arena = "arena"
|
Arena = "arena"
|
||||||
MTGOnline = "mtgo"
|
MTGOnline = "mtgo"
|
||||||
|
|
||||||
|
|
||||||
class Legality(enum.Enum):
|
class Legality(enum.Enum):
|
||||||
Legal = "legal"
|
Legal = "legal"
|
||||||
NotLegal = "not_legal"
|
NotLegal = "not_legal"
|
||||||
Restricted = "restricted"
|
Restricted = "restricted"
|
||||||
Banned = "banned"
|
Banned = "banned"
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class Card:
|
class Card:
|
||||||
scryfall_id: uuid.UUID
|
scryfall_id: uuid.UUID
|
||||||
|
@ -74,6 +77,12 @@ class Card:
|
||||||
legalities: typing.Dict[str, Legality]
|
legalities: typing.Dict[str, Legality]
|
||||||
edhrec_rank: typing.Optional[int] = None
|
edhrec_rank: typing.Optional[int] = None
|
||||||
oracle_text: typing.Optional[str] = None
|
oracle_text: typing.Optional[str] = None
|
||||||
|
price_usd: typing.Optional[decimal.Decimal] = None
|
||||||
|
price_usd_foil: typing.Optional[decimal.Decimal] = None
|
||||||
|
price_eur: typing.Optional[decimal.Decimal] = None
|
||||||
|
price_eur_foil: typing.Optional[decimal.Decimal] = None
|
||||||
|
price_tix: typing.Optional[decimal.Decimal] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class CardCopy:
|
class CardCopy:
|
||||||
|
|
|
@ -5,6 +5,9 @@ import tutor.models
|
||||||
|
|
||||||
|
|
||||||
def to_card(data: dict) -> tutor.models.Card:
|
def to_card(data: dict) -> tutor.models.Card:
|
||||||
|
prices = {
|
||||||
|
k: decimal.Decimal(v) if v else None for k, v in data.get("prices", {}).items()
|
||||||
|
}
|
||||||
return tutor.models.Card(
|
return tutor.models.Card(
|
||||||
scryfall_id=data["id"],
|
scryfall_id=data["id"],
|
||||||
name=data["name"],
|
name=data["name"],
|
||||||
|
@ -26,4 +29,9 @@ def to_card(data: dict) -> tutor.models.Card:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
oracle_text=data.get("oracle_text"),
|
oracle_text=data.get("oracle_text"),
|
||||||
|
price_usd=prices.get("usd"),
|
||||||
|
price_usd_foil=prices.get("usd_foil"),
|
||||||
|
price_eur=prices.get("eur"),
|
||||||
|
price_eur_foil=prices.get("eur_foil"),
|
||||||
|
price_tix=prices.get("tix"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import decimal
|
||||||
import json
|
import json
|
||||||
|
import typing
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
|
@ -59,12 +61,14 @@ class SearchHandler(tornado.web.RequestHandler):
|
||||||
in_collection = self.get_argument("in_collection", None)
|
in_collection = self.get_argument("in_collection", None)
|
||||||
page = max(1, int(self.get_argument("page", 1)))
|
page = max(1, int(self.get_argument("page", 1)))
|
||||||
limit = int(self.get_argument("limit", 10))
|
limit = int(self.get_argument("limit", 10))
|
||||||
|
sort_by = self.get_argument("sort_by", "rarity")
|
||||||
search = tutor.search.search.parse(query)
|
search = tutor.search.search.parse(query)
|
||||||
cards = await tutor.database.advanced_search(
|
cards = await tutor.database.advanced_search(
|
||||||
db,
|
db,
|
||||||
search,
|
search,
|
||||||
limit=limit + 1,
|
limit=limit + 1,
|
||||||
offset=limit * (page - 1),
|
offset=limit * (page - 1),
|
||||||
|
sort_by=sort_by,
|
||||||
in_collection=in_collection in ("yes", "true")
|
in_collection=in_collection in ("yes", "true")
|
||||||
if in_collection
|
if in_collection
|
||||||
else None,
|
else None,
|
||||||
|
@ -78,6 +82,11 @@ class SearchHandler(tornado.web.RequestHandler):
|
||||||
if has_more:
|
if has_more:
|
||||||
links["next"] = update_args(self.request.full_url(), page=page + 1)
|
links["next"] = update_args(self.request.full_url(), page=page + 1)
|
||||||
self.set_links(**links)
|
self.set_links(**links)
|
||||||
|
def price(amount: typing.Optional[decimal.Decimal]) -> typing.Optional[str]:
|
||||||
|
if amount is not None:
|
||||||
|
return str(amount)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
self.write(
|
self.write(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
[
|
[
|
||||||
|
@ -91,6 +100,13 @@ class SearchHandler(tornado.web.RequestHandler):
|
||||||
card.color_identity
|
card.color_identity
|
||||||
),
|
),
|
||||||
"oracle_text": card.oracle_text,
|
"oracle_text": card.oracle_text,
|
||||||
|
"prices": {
|
||||||
|
"usd": price(card.price_usd),
|
||||||
|
"usd_foil": price(card.price_usd_foil),
|
||||||
|
"eur": price(card.price_eur),
|
||||||
|
"eur_foil": price(card.price_eur_foil),
|
||||||
|
"tix": price(card.price_tix),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for card in cards[:limit]
|
for card in cards[:limit]
|
||||||
]
|
]
|
||||||
|
|
|
@ -16,6 +16,7 @@ import Element.Input as Input
|
||||||
import Html.Events
|
import Html.Events
|
||||||
import Http
|
import Http
|
||||||
import Json.Decode
|
import Json.Decode
|
||||||
|
import Maybe.Extra
|
||||||
import Paginated
|
import Paginated
|
||||||
import Spinner
|
import Spinner
|
||||||
import Task
|
import Task
|
||||||
|
@ -33,6 +34,7 @@ type alias Dimensions =
|
||||||
|
|
||||||
type alias Criteria =
|
type alias Criteria =
|
||||||
{ query : String
|
{ query : String
|
||||||
|
, sortBy : String
|
||||||
, ownedOnly : Bool
|
, ownedOnly : Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +64,7 @@ type Msg
|
||||||
|
|
||||||
type CriteriaMsg
|
type CriteriaMsg
|
||||||
= UpdateName String
|
= UpdateName String
|
||||||
|
| UpdateSortBy String
|
||||||
| UpdateOwnedOnly Bool
|
| UpdateOwnedOnly Bool
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,12 +120,13 @@ manaSpinner =
|
||||||
searchQuery : Criteria -> List Url.Builder.QueryParameter
|
searchQuery : Criteria -> List Url.Builder.QueryParameter
|
||||||
searchQuery criteria =
|
searchQuery criteria =
|
||||||
[ Url.Builder.string "q" criteria.query
|
[ Url.Builder.string "q" criteria.query
|
||||||
|
, Url.Builder.string "sort_by" criteria.sortBy
|
||||||
, Url.Builder.string "in_collection"
|
, Url.Builder.string "in_collection"
|
||||||
(if criteria.ownedOnly then
|
(if criteria.ownedOnly then
|
||||||
"yes"
|
"yes"
|
||||||
|
|
||||||
else
|
else
|
||||||
""
|
"no"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -150,6 +154,15 @@ parseUrl =
|
||||||
Url.Parser.Query.string "q"
|
Url.Parser.Query.string "q"
|
||||||
|> Url.Parser.Query.map (Maybe.withDefault "")
|
|> Url.Parser.Query.map (Maybe.withDefault "")
|
||||||
|
|
||||||
|
sortBy =
|
||||||
|
Url.Parser.Query.enum "sort_by"
|
||||||
|
(Dict.fromList
|
||||||
|
[ ( "rarity", "rarity" )
|
||||||
|
, ( "price", "price" )
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|> Url.Parser.Query.map (Maybe.withDefault "rarity")
|
||||||
|
|
||||||
inCollection =
|
inCollection =
|
||||||
Url.Parser.Query.enum "in_collection"
|
Url.Parser.Query.enum "in_collection"
|
||||||
(Dict.fromList
|
(Dict.fromList
|
||||||
|
@ -161,14 +174,14 @@ parseUrl =
|
||||||
)
|
)
|
||||||
|> Url.Parser.Query.map (Maybe.withDefault True)
|
|> Url.Parser.Query.map (Maybe.withDefault True)
|
||||||
in
|
in
|
||||||
Url.Parser.top <?> Url.Parser.Query.map2 Criteria query inCollection
|
Url.Parser.top <?> Url.Parser.Query.map3 Criteria query sortBy inCollection
|
||||||
|
|
||||||
|
|
||||||
criteriaFromUrl : Url.Url -> Criteria
|
criteriaFromUrl : Url.Url -> Criteria
|
||||||
criteriaFromUrl url =
|
criteriaFromUrl url =
|
||||||
let
|
let
|
||||||
emptyCriteria =
|
emptyCriteria =
|
||||||
{ query = "", ownedOnly = True }
|
{ query = "", sortBy = "rarity", ownedOnly = True }
|
||||||
in
|
in
|
||||||
Url.Parser.parse parseUrl url
|
Url.Parser.parse parseUrl url
|
||||||
|> Maybe.withDefault emptyCriteria
|
|> Maybe.withDefault emptyCriteria
|
||||||
|
@ -207,6 +220,9 @@ updateCriteria msg model =
|
||||||
UpdateName text ->
|
UpdateName text ->
|
||||||
{ model | query = text }
|
{ model | query = text }
|
||||||
|
|
||||||
|
UpdateSortBy column ->
|
||||||
|
{ model | sortBy = column }
|
||||||
|
|
||||||
UpdateOwnedOnly value ->
|
UpdateOwnedOnly value ->
|
||||||
{ model | ownedOnly = value }
|
{ model | ownedOnly = value }
|
||||||
|
|
||||||
|
@ -217,7 +233,7 @@ update msg model =
|
||||||
UrlChanged url ->
|
UrlChanged url ->
|
||||||
let
|
let
|
||||||
criteria =
|
criteria =
|
||||||
criteriaFromUrl url
|
Debug.log "criteria" <| criteriaFromUrl url
|
||||||
in
|
in
|
||||||
( { model | criteria = criteria }, search criteria )
|
( { model | criteria = criteria }, search criteria )
|
||||||
|
|
||||||
|
@ -241,19 +257,16 @@ update msg model =
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
|
UpdateSortBy _ ->
|
||||||
|
update Search { model | criteria = newCriteria }
|
||||||
|
|
||||||
UpdateOwnedOnly _ ->
|
UpdateOwnedOnly _ ->
|
||||||
( { model
|
update Search { model | criteria = newCriteria }
|
||||||
| criteria = newCriteria
|
|
||||||
, cardPage = toLoading model.cardPage
|
|
||||||
}
|
|
||||||
, search newCriteria
|
|
||||||
)
|
|
||||||
|
|
||||||
Search ->
|
Search ->
|
||||||
( { model | cardPage = toLoading model.cardPage }
|
( { model | cardPage = toLoading model.cardPage }
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ search model.criteria
|
[ Browser.Navigation.pushUrl model.navigationKey <|
|
||||||
, Browser.Navigation.pushUrl model.navigationKey <|
|
|
||||||
Url.Builder.relative [] (searchQuery model.criteria)
|
Url.Builder.relative [] (searchQuery model.criteria)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -373,13 +386,41 @@ viewCardBrowser model =
|
||||||
Nothing ->
|
Nothing ->
|
||||||
E.el [ E.width E.fill ] E.none
|
E.el [ E.width E.fill ] E.none
|
||||||
|
|
||||||
|
priceBadge { currency, amount } =
|
||||||
|
E.el
|
||||||
|
[ Border.solid
|
||||||
|
, Border.width 1
|
||||||
|
, Border.rounded 5
|
||||||
|
, Border.color colors.text
|
||||||
|
, E.width <| E.px 60
|
||||||
|
, E.padding 2
|
||||||
|
, Font.family [ Font.typeface "sans" ]
|
||||||
|
, Font.size 10
|
||||||
|
]
|
||||||
|
<|
|
||||||
|
E.row [ E.width E.fill ]
|
||||||
|
[ E.el [ E.width <| E.fillPortion 1 ] <| E.text <| String.toUpper currency
|
||||||
|
, E.el [ E.width <| E.fillPortion 2, Font.alignRight ] <| E.text amount
|
||||||
|
]
|
||||||
|
|
||||||
cardDetails card =
|
cardDetails card =
|
||||||
|
let
|
||||||
|
prices =
|
||||||
|
Maybe.Extra.values
|
||||||
|
[ Maybe.map (\usd -> { currency = "usd", amount = usd }) <|
|
||||||
|
Maybe.Extra.or card.prices.usd card.prices.usd_foil
|
||||||
|
, Maybe.map (\eur -> { currency = "eur", amount = eur }) <|
|
||||||
|
Maybe.Extra.or card.prices.eur card.prices.eur_foil
|
||||||
|
, Maybe.map (\tix -> { currency = "tix", amount = tix }) card.prices.tix
|
||||||
|
]
|
||||||
|
in
|
||||||
E.column
|
E.column
|
||||||
[ E.spacing 20
|
[ E.spacing 20
|
||||||
, E.padding 10
|
, E.padding 10
|
||||||
]
|
]
|
||||||
<|
|
<|
|
||||||
E.el [ E.centerX ] (viewCard { width = 192, height = 272 } card)
|
E.el [ E.centerX ] (viewCard { width = 192, height = 272 } card)
|
||||||
|
:: (E.row [ E.spacing 5, E.centerX ] <| List.map priceBadge prices)
|
||||||
:: E.paragraph [ Font.heavy, Font.size 24, Font.center ] [ E.text card.name ]
|
:: E.paragraph [ Font.heavy, Font.size 24, Font.center ] [ E.text card.name ]
|
||||||
:: List.map (\text -> E.paragraph [ Font.size 16 ] [ E.text text ])
|
:: List.map (\text -> E.paragraph [ Font.size 16 ] [ E.text text ])
|
||||||
(String.lines card.oracleText)
|
(String.lines card.oracleText)
|
||||||
|
@ -494,6 +535,15 @@ view model =
|
||||||
{ onPress = Just Search
|
{ onPress = Just Search
|
||||||
, label = E.text "Search"
|
, label = E.text "Search"
|
||||||
}
|
}
|
||||||
|
, Input.radio [ E.padding 10 ]
|
||||||
|
{ onChange = UpdateCriteria << UpdateSortBy
|
||||||
|
, selected = Just model.criteria.sortBy
|
||||||
|
, label = Input.labelLeft [ Font.color colors.text ] (E.text "Sort by")
|
||||||
|
, options =
|
||||||
|
[ Input.option "rarity" <| E.el [ Font.color colors.text ] <| E.text "Rarity DESC"
|
||||||
|
, Input.option "price" <| E.el [ Font.color colors.text ] <| E.text "Price DESC"
|
||||||
|
]
|
||||||
|
}
|
||||||
, Input.checkbox []
|
, Input.checkbox []
|
||||||
{ onChange = UpdateCriteria << UpdateOwnedOnly
|
{ onChange = UpdateCriteria << UpdateOwnedOnly
|
||||||
, icon = Input.defaultCheckbox
|
, icon = Input.defaultCheckbox
|
||||||
|
|
|
@ -4,12 +4,22 @@ import Json.Decode
|
||||||
import Json.Decode.Pipeline as JDP
|
import Json.Decode.Pipeline as JDP
|
||||||
|
|
||||||
|
|
||||||
|
type alias Prices =
|
||||||
|
{ usd : Maybe String
|
||||||
|
, usd_foil : Maybe String
|
||||||
|
, eur : Maybe String
|
||||||
|
, eur_foil : Maybe String
|
||||||
|
, tix : Maybe String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
type alias Card =
|
type alias Card =
|
||||||
{ scryfallId : String
|
{ scryfallId : String
|
||||||
, name : String
|
, name : String
|
||||||
, setCode : String
|
, setCode : String
|
||||||
, rarity : String
|
, rarity : String
|
||||||
, oracleText : String
|
, oracleText : String
|
||||||
|
, prices : Prices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,3 +34,14 @@ decode =
|
||||||
(Json.Decode.nullable Json.Decode.string
|
(Json.Decode.nullable Json.Decode.string
|
||||||
|> Json.Decode.map (Maybe.withDefault "")
|
|> Json.Decode.map (Maybe.withDefault "")
|
||||||
)
|
)
|
||||||
|
|> JDP.required "prices" decodePrices
|
||||||
|
|
||||||
|
|
||||||
|
decodePrices : Json.Decode.Decoder Prices
|
||||||
|
decodePrices =
|
||||||
|
Json.Decode.succeed Prices
|
||||||
|
|> JDP.required "usd" (Json.Decode.nullable Json.Decode.string)
|
||||||
|
|> JDP.required "usd_foil" (Json.Decode.nullable Json.Decode.string)
|
||||||
|
|> JDP.required "eur" (Json.Decode.nullable Json.Decode.string)
|
||||||
|
|> JDP.required "eur_foil" (Json.Decode.nullable Json.Decode.string)
|
||||||
|
|> JDP.required "tix" (Json.Decode.nullable Json.Decode.string)
|
||||||
|
|
|
@ -68,7 +68,7 @@ expectJson toMsg decoder =
|
||||||
Ok values ->
|
Ok values ->
|
||||||
Ok
|
Ok
|
||||||
{ prev =
|
{ prev =
|
||||||
Dict.get "link" (Debug.log "headers" metadata.headers)
|
Dict.get "link" metadata.headers
|
||||||
|> Maybe.map links
|
|> Maybe.map links
|
||||||
|> Maybe.andThen (Dict.get "prev")
|
|> Maybe.andThen (Dict.get "prev")
|
||||||
|> Maybe.andThen Url.fromString
|
|> Maybe.andThen Url.fromString
|
||||||
|
|
Loading…
Reference in a new issue