WIP: Add decks
This commit is contained in:
parent
1506960047
commit
f873ba7967
11 changed files with 414 additions and 55 deletions
|
@ -107,7 +107,7 @@ CREATE TABLE IF NOT EXISTS "decks" (
|
|||
"name" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "deck_cards" (
|
||||
CREATE TABLE IF NOT EXISTS "deck_list" (
|
||||
"deck_id" INTEGER NOT NULL,
|
||||
"oracle_id" UUID NOT NULL,
|
||||
"quantity" INTEGER NOT NULL DEFAULT 1,
|
||||
|
@ -115,6 +115,14 @@ CREATE TABLE IF NOT EXISTS "deck_cards" (
|
|||
FOREIGN KEY ("deck_id") REFERENCES "decks" ("deck_id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "deck_cards" (
|
||||
"deck_id" INTEGER NOT NULL,
|
||||
"scryfall_id" UUID NOT NULL,
|
||||
PRIMARY KEY ("deck_id", "scryfall_id"),
|
||||
FOREIGN KEY ("deck_id") REFERENCES "decks" ("deck_id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("scryfall_id") REFERENCES "cards" ("scryfall_id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "vars" (
|
||||
"key" TEXT PRIMARY KEY,
|
||||
"value" TEXT
|
||||
|
|
|
@ -62,6 +62,19 @@ class Legality(enum.Enum):
|
|||
Banned = "banned"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class OracleCard:
|
||||
oracle_id: uuid.UUID
|
||||
name: str
|
||||
color_identity: typing.List[Color]
|
||||
cmc: decimal.Decimal
|
||||
type_line: str
|
||||
games: typing.Set[Game]
|
||||
legalities: typing.Dict[str, Legality]
|
||||
edhrec_rank: typing.Optional[int] = None
|
||||
oracle_text: typing.Optional[str] = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Card:
|
||||
scryfall_id: uuid.UUID
|
||||
|
@ -92,3 +105,15 @@ class CardCopy:
|
|||
language: str = "English"
|
||||
collection: str = "Default"
|
||||
condition: typing.Optional[str] = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DeckCard:
|
||||
card: OracleCard
|
||||
quantity: int = 1
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Deck:
|
||||
name: str
|
||||
cards: typing.List[DeckCard]
|
||||
|
|
|
@ -2,6 +2,7 @@ import decimal
|
|||
import importlib.metadata
|
||||
import importlib.resources
|
||||
import json
|
||||
import re
|
||||
import typing
|
||||
import urllib.parse
|
||||
|
||||
|
@ -10,6 +11,8 @@ import psycopg.rows
|
|||
import psycopg_pool
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
import tornado_openapi3.handler
|
||||
import yaml
|
||||
|
||||
import tutor.database
|
||||
import tutor.models
|
||||
|
@ -36,6 +39,25 @@ def update_args(url: str, **qargs) -> str:
|
|||
)
|
||||
|
||||
|
||||
class OpenAPIRequestHandler(tornado_openapi3.handler.OpenAPIRequestHandler):
|
||||
@property
|
||||
def spec_dict(self):
|
||||
spec = getattr(self.application, "openapi_spec_dict", None)
|
||||
if not spec:
|
||||
version = importlib.metadata.version(__package__)
|
||||
spec = yaml.safe_load(self.render_string("openapi.yaml", version=version))
|
||||
setattr(self.application, "openapi_spec_dict", spec)
|
||||
return spec
|
||||
|
||||
@property
|
||||
def spec(self):
|
||||
spec = getattr(self.application, "openapi_spec", None)
|
||||
if not spec:
|
||||
spec = super().spec
|
||||
setattr(self.application, "openapi_spec", spec)
|
||||
return spec
|
||||
|
||||
|
||||
class SearchHandler(tornado.web.RequestHandler):
|
||||
def url(self, url: str) -> str:
|
||||
scheme_override = self.application.settings["scheme"]
|
||||
|
@ -135,6 +157,18 @@ class CollectionHandler(tornado.web.RequestHandler):
|
|||
)
|
||||
|
||||
|
||||
class DecksHandler(tornado.web.RequestHandler):
|
||||
async def get(self) -> None:
|
||||
self.finish(
|
||||
{
|
||||
"decks": [],
|
||||
}
|
||||
)
|
||||
|
||||
async def post(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
class TemplateHandler(tornado.web.RequestHandler):
|
||||
def initialize(
|
||||
self,
|
||||
|
@ -151,6 +185,14 @@ class TemplateHandler(tornado.web.RequestHandler):
|
|||
return self.render(self.path, **self.vars)
|
||||
|
||||
|
||||
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"):
|
||||
path = "index.html"
|
||||
return tornado.web.StaticFileHandler.get_absolute_path(root, path)
|
||||
|
||||
class Application(tornado.web.Application):
|
||||
def __init__(self, **settings):
|
||||
version = importlib.metadata.version(__package__)
|
||||
|
@ -174,13 +216,14 @@ class Application(tornado.web.Application):
|
|||
),
|
||||
(r"/api/search", SearchHandler),
|
||||
(r"/api/collection", CollectionHandler),
|
||||
(r"/api/decks", DecksHandler),
|
||||
]
|
||||
if static_path := settings.get("static"):
|
||||
paths.extend(
|
||||
[
|
||||
(
|
||||
r"/(.*)",
|
||||
tornado.web.StaticFileHandler,
|
||||
fr"/(.*)",
|
||||
StaticFileHandler,
|
||||
{"path": static_path, "default_filename": "index.html"},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -32,3 +32,140 @@ paths:
|
|||
application/x-yaml:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/search:
|
||||
get:
|
||||
summary: Search the card database
|
||||
tags:
|
||||
- Collection
|
||||
parameters:
|
||||
- name: q
|
||||
in: query
|
||||
description: >-
|
||||
Text in the query string will be used to filter cards having that text in their
|
||||
name. Additionally, the keyword expressions below can be used to search for
|
||||
cards with certain properties.
|
||||
|
||||
### Examples
|
||||
|
||||
`bolt`
|
||||
: Find all cards with "bolt" in the name
|
||||
|
||||
`"God of"`
|
||||
: Find all cards with "God of" in the name
|
||||
|
||||
`t:legendary t:creature c:jund`
|
||||
: Find all legendary creatures with a color
|
||||
identity of red/blue/green
|
||||
|
||||
`color<=ubg`
|
||||
: Find all spells that are blue, black, green, or any
|
||||
combination thereof.
|
||||
|
||||
`color:red set:stx rarity>=rare`
|
||||
: Find all red cards in Strixhaven that are
|
||||
rare or mythic
|
||||
|
||||
`t:enchantment o:"enters the battlefield"`
|
||||
: Find all enchantments with ETB
|
||||
effects
|
||||
|
||||
### Keywords
|
||||
|
||||
#### Colors
|
||||
|
||||
Keywords
|
||||
: `c`, `color`
|
||||
|
||||
Operators
|
||||
: `:` (matches), `>=` (greater than or equal to), `<=` (less than
|
||||
or equal to)
|
||||
|
||||
Matches cards of the chosen color or colors.
|
||||
|
||||
Single colors
|
||||
: `w` or `white`, `u` or `blue`, `b` or `black, =g` or `green`, `r` or `red`
|
||||
|
||||
Any combination of abbreviated single colors
|
||||
: e.g.: `rg`, `uw`, or `wubgr`
|
||||
|
||||
Ravnican guilds
|
||||
: `boros` (white/red), `golgari` (green/black), `selesnya`
|
||||
(green/white), `dimir` (blue/black), `orzhov` (white/black), `izzet`
|
||||
(blue/red), `gruul` (red/green), `azorius` (white/blue), `rakdos` (black/red),
|
||||
`simic` (green/blue)
|
||||
|
||||
Alaran shards
|
||||
: `bant` (white/green/blue), `esper` (blue/white/black),
|
||||
`grixis` (black/blue/red), `jund` (red/black/green), `naya` (green/red/white)
|
||||
|
||||
Tarkirian wedges
|
||||
: `abzan` (white/black/green), `jeskai` (white/blue/red),
|
||||
`sultai` (blue/black/green), `mardu` (white/black/red), `temur`
|
||||
(blue/red/green)
|
||||
|
||||
#### Sets
|
||||
|
||||
Keywords
|
||||
: `s`, `set`, `e`, `expansion`
|
||||
|
||||
Operators
|
||||
: `:` (matches)
|
||||
|
||||
#### Rarity
|
||||
|
||||
Keywords
|
||||
: `r`, `rarity`
|
||||
|
||||
Operators
|
||||
: `:` (matches), `>=` (greater than or equal to), `<=` (less than
|
||||
or equal to)
|
||||
|
||||
#### Type
|
||||
|
||||
Keywords
|
||||
: `t`, `type`
|
||||
|
||||
Operators
|
||||
: `:` (matches)
|
||||
|
||||
#### Oracle Text
|
||||
|
||||
Keywords
|
||||
: `o`, `oracle`
|
||||
|
||||
Operators
|
||||
: `:` (matches)
|
||||
responses:
|
||||
'200':
|
||||
description: Search results
|
||||
/collection:
|
||||
get:
|
||||
summary: Collection statistics
|
||||
tags:
|
||||
- Collection
|
||||
responses:
|
||||
'200':
|
||||
description: Collection statistics
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/collection_statistics'
|
||||
components:
|
||||
schemas:
|
||||
collection_statistics:
|
||||
type: object
|
||||
properties:
|
||||
cards:
|
||||
description: Total number of cards in the collection
|
||||
type: integer
|
||||
example: 765
|
||||
value:
|
||||
description: Total estimated value of the collection
|
||||
type: number
|
||||
format: float
|
||||
example: 1234.56
|
||||
sets:
|
||||
description: Total number of sets in the collection
|
||||
type: integer
|
||||
example: 15
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<head>
|
||||
<title>Tutor</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script type="text/javascript" src="elm.js"></script>
|
||||
<script type="text/javascript" src="/elm.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville&display=swap" rel="stylesheet">
|
||||
|
|
152
www/src/App.elm
152
www/src/App.elm
|
@ -5,7 +5,6 @@ import Browser.Dom
|
|||
import Browser.Events
|
||||
import Browser.Navigation
|
||||
import Card
|
||||
import Collection
|
||||
import Color
|
||||
import Dict
|
||||
import Element as E
|
||||
|
@ -18,7 +17,10 @@ import Html.Events
|
|||
import Http
|
||||
import Json.Decode
|
||||
import Maybe.Extra
|
||||
import Pages.Collection
|
||||
import Pages.DeckList
|
||||
import Paginated
|
||||
import Route
|
||||
import Spinner
|
||||
import Task
|
||||
import UI
|
||||
|
@ -33,7 +35,7 @@ type alias Model =
|
|||
, url : Url.Url
|
||||
, viewport : UI.Dimensions
|
||||
, device : E.Device
|
||||
, route : Route
|
||||
, route : Maybe Route.Route
|
||||
, page : Page
|
||||
}
|
||||
|
||||
|
@ -42,33 +44,15 @@ type Msg
|
|||
= UrlChanged Url.Url
|
||||
| ViewportChanged UI.Dimensions
|
||||
| LinkClicked Browser.UrlRequest
|
||||
| CollectionMsg Collection.Msg
|
||||
| CollectionMsg Pages.Collection.Msg
|
||||
| DeckListMsg Pages.DeckList.Msg
|
||||
| SpinnerMsg Spinner.Msg
|
||||
|
||||
|
||||
type Page
|
||||
= NotFound
|
||||
| Collection Collection.Model
|
||||
| Decks
|
||||
|
||||
|
||||
type Route
|
||||
= Home
|
||||
| MyCollection
|
||||
| MyDecks
|
||||
|
||||
|
||||
routeToUrl : Route -> String
|
||||
routeToUrl route =
|
||||
case route of
|
||||
Home ->
|
||||
Url.Builder.absolute [] []
|
||||
|
||||
MyCollection ->
|
||||
Url.Builder.absolute [ "collection" ] []
|
||||
|
||||
MyDecks ->
|
||||
Url.Builder.absolute [ "decks" ] []
|
||||
| Collection Pages.Collection.Model
|
||||
| DeckList Pages.DeckList.Model
|
||||
|
||||
|
||||
init : Json.Decode.Value -> Url.Url -> Browser.Navigation.Key -> ( Model, Cmd Msg )
|
||||
|
@ -80,19 +64,36 @@ init _ url key =
|
|||
device =
|
||||
E.classifyDevice viewport
|
||||
|
||||
( pageModel, pageCmd ) =
|
||||
Collection.init key url device
|
||||
route =
|
||||
Route.fromUrl url
|
||||
|
||||
initWith : (pageModel -> Page) -> (pageMsg -> Msg) -> ( pageModel, Cmd pageMsg ) -> ( Page, Cmd Msg )
|
||||
initWith pageType pageMsg ( subModel, subCmd ) =
|
||||
( pageType subModel, Cmd.map pageMsg subCmd )
|
||||
|
||||
( page, pageCmd ) =
|
||||
case route of
|
||||
Just Route.Collection ->
|
||||
initWith Collection CollectionMsg <|
|
||||
Pages.Collection.init key url device
|
||||
|
||||
Just Route.DeckList ->
|
||||
initWith DeckList DeckListMsg <|
|
||||
Pages.DeckList.init key url device
|
||||
|
||||
_ ->
|
||||
( NotFound, Cmd.none )
|
||||
in
|
||||
( { navigationKey = key
|
||||
, url = url
|
||||
, viewport = viewport
|
||||
, device = device
|
||||
, route = MyCollection
|
||||
, page = Collection pageModel
|
||||
, route = route
|
||||
, page = page
|
||||
}
|
||||
, Cmd.batch
|
||||
[ UI.getViewport ViewportChanged
|
||||
, Cmd.map CollectionMsg pageCmd
|
||||
, pageCmd
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -105,15 +106,40 @@ updateWith pageType pageMsg model ( subModel, subCmd ) =
|
|||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case ( msg, model.page ) of
|
||||
( UrlChanged url, Collection pageModel ) ->
|
||||
Collection.update (Collection.UrlChanged url) pageModel
|
||||
( UrlChanged url, _ ) ->
|
||||
let
|
||||
oldRoute =
|
||||
model.route
|
||||
|
||||
newRoute =
|
||||
Route.fromUrl url
|
||||
|
||||
( newModel, cmds ) =
|
||||
case ( model.page, newRoute ) of
|
||||
( Collection pageModel, Just Route.Collection ) ->
|
||||
Pages.Collection.update (Pages.Collection.UrlChanged url) pageModel
|
||||
|> updateWith Collection CollectionMsg model
|
||||
|
||||
( UrlChanged url, _ ) ->
|
||||
( model, Cmd.none )
|
||||
( _, Just Route.Collection ) ->
|
||||
Pages.Collection.init model.navigationKey url model.device
|
||||
|> updateWith Collection CollectionMsg model
|
||||
|
||||
( LinkClicked _, _ ) ->
|
||||
( model, Cmd.none )
|
||||
( _, Just Route.DeckList ) ->
|
||||
Pages.DeckList.init model.navigationKey url model.device
|
||||
|> updateWith DeckList DeckListMsg model
|
||||
|
||||
_ ->
|
||||
( { model | page = NotFound }, Cmd.none )
|
||||
in
|
||||
( { newModel | route = Route.fromUrl url }, cmds )
|
||||
|
||||
( LinkClicked urlRequest, _ ) ->
|
||||
case urlRequest of
|
||||
Browser.Internal url ->
|
||||
( model, Browser.Navigation.pushUrl model.navigationKey (Url.toString url) )
|
||||
|
||||
Browser.External url ->
|
||||
( model, Browser.Navigation.load url )
|
||||
|
||||
( ViewportChanged viewport, _ ) ->
|
||||
( { model
|
||||
|
@ -124,11 +150,11 @@ update msg model =
|
|||
)
|
||||
|
||||
( SpinnerMsg spinnerMsg, Collection pageModel ) ->
|
||||
Collection.update (Collection.SpinnerMsg spinnerMsg) pageModel
|
||||
Pages.Collection.update (Pages.Collection.SpinnerMsg spinnerMsg) pageModel
|
||||
|> updateWith Collection CollectionMsg model
|
||||
|
||||
( CollectionMsg pageMsg, Collection pageModel ) ->
|
||||
Collection.update pageMsg pageModel
|
||||
Pages.Collection.update pageMsg pageModel
|
||||
|> updateWith Collection CollectionMsg model
|
||||
|
||||
( _, _ ) ->
|
||||
|
@ -138,15 +164,21 @@ update msg model =
|
|||
navBar : Model -> E.Element Msg
|
||||
navBar model =
|
||||
let
|
||||
navLink : Route -> String -> E.Element Msg
|
||||
navLink : Route.Route -> String -> E.Element Msg
|
||||
navLink route text =
|
||||
E.link
|
||||
[ E.pointer
|
||||
, E.padding 10
|
||||
, Font.center
|
||||
, Background.color UI.colors.primary
|
||||
, Background.color <|
|
||||
if Just route == model.route then
|
||||
UI.colors.primary
|
||||
|
||||
else
|
||||
UI.colors.background
|
||||
, E.mouseOver [ Background.color UI.colors.primary ]
|
||||
]
|
||||
{ url = routeToUrl route, label = E.text text }
|
||||
{ url = Route.toUrl route, label = E.text text }
|
||||
in
|
||||
E.row
|
||||
[ E.padding 10
|
||||
|
@ -156,10 +188,9 @@ navBar model =
|
|||
, Font.color UI.colors.text
|
||||
]
|
||||
[ E.el [ E.width <| E.fillPortion 1 ] <| E.text "Tutor"
|
||||
, E.row [ E.width <| E.fillPortion 4 ]
|
||||
[ navLink MyCollection "Collection"
|
||||
|
||||
-- , navLink "" "Decks"
|
||||
, E.row [ E.width <| E.fillPortion 4, E.spacing 3 ]
|
||||
[ navLink Route.Collection "Collection"
|
||||
, navLink Route.DeckList "Decks"
|
||||
]
|
||||
]
|
||||
|
||||
|
@ -170,10 +201,32 @@ view model =
|
|||
viewPage page =
|
||||
case page of
|
||||
Collection pageModel ->
|
||||
E.map CollectionMsg <| Collection.view pageModel
|
||||
E.map CollectionMsg <| Pages.Collection.view pageModel
|
||||
|
||||
_ ->
|
||||
E.none
|
||||
DeckList pageModel ->
|
||||
E.map DeckListMsg <| Pages.DeckList.view pageModel
|
||||
|
||||
NotFound ->
|
||||
E.column
|
||||
[ E.width E.fill
|
||||
, E.height E.fill
|
||||
, E.padding 20
|
||||
, E.spacing 20
|
||||
]
|
||||
[ E.el
|
||||
[ Font.color UI.colors.title
|
||||
, Font.size 60
|
||||
, E.centerX
|
||||
]
|
||||
<|
|
||||
E.text "404 Not Found"
|
||||
, E.el
|
||||
[ Font.color UI.colors.text
|
||||
, E.centerX
|
||||
]
|
||||
<|
|
||||
E.text "The page you requested could not be found."
|
||||
]
|
||||
in
|
||||
{ title = "Tutor"
|
||||
, body =
|
||||
|
@ -186,7 +239,8 @@ view model =
|
|||
[ E.width E.fill
|
||||
, E.height E.fill
|
||||
]
|
||||
[ viewPage model.page
|
||||
[ navBar model
|
||||
, viewPage model.page
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -202,7 +256,7 @@ subscriptions model =
|
|||
in
|
||||
case model.page of
|
||||
Collection pageModel ->
|
||||
Sub.batch (Sub.map CollectionMsg (Collection.subscriptions pageModel) :: global)
|
||||
Sub.batch (Sub.map CollectionMsg (Pages.Collection.subscriptions pageModel) :: global)
|
||||
|
||||
_ ->
|
||||
Sub.batch global
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
module Collection exposing (..)
|
||||
module Pages.Collection exposing (..)
|
||||
|
||||
import Browser
|
||||
import Browser.Dom
|
8
www/src/Pages/DeckEditor.elm
Normal file
8
www/src/Pages/DeckEditor.elm
Normal file
|
@ -0,0 +1,8 @@
|
|||
module DeckEditor exposing (..)
|
||||
|
||||
import Card
|
||||
|
||||
|
||||
type alias Deck =
|
||||
{ cards : List Card.Card
|
||||
}
|
33
www/src/Pages/DeckList.elm
Normal file
33
www/src/Pages/DeckList.elm
Normal file
|
@ -0,0 +1,33 @@
|
|||
module Pages.DeckList exposing (..)
|
||||
|
||||
import Browser.Navigation
|
||||
import Element as E
|
||||
import Element.Font as Font
|
||||
import UI
|
||||
import Url
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ navigationKey : Browser.Navigation.Key
|
||||
, url : Url.Url
|
||||
, device : E.Device
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= None
|
||||
|
||||
|
||||
init : Browser.Navigation.Key -> Url.Url -> E.Device -> ( Model, Cmd Msg )
|
||||
init key url device =
|
||||
( { navigationKey = key
|
||||
, url = url
|
||||
, device = device
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
view : Model -> E.Element Msg
|
||||
view model =
|
||||
E.column [ E.padding 40, E.centerX, Font.italic ] [ UI.text "No decks yet" ]
|
38
www/src/Route.elm
Normal file
38
www/src/Route.elm
Normal file
|
@ -0,0 +1,38 @@
|
|||
module Route exposing (Route(..), fromUrl, toUrl)
|
||||
|
||||
import Url exposing (Url)
|
||||
import Url.Builder
|
||||
import Url.Parser exposing (Parser, map, oneOf, parse, s, top)
|
||||
|
||||
|
||||
type Route
|
||||
= Home
|
||||
| Collection
|
||||
| DeckList
|
||||
|
||||
|
||||
parser : Parser (Route -> a) a
|
||||
parser =
|
||||
oneOf
|
||||
[ map Home top
|
||||
, map Collection (s "collection")
|
||||
, map DeckList (s "decks")
|
||||
]
|
||||
|
||||
|
||||
toUrl : Route -> String
|
||||
toUrl route =
|
||||
case route of
|
||||
Home ->
|
||||
Url.Builder.absolute [] []
|
||||
|
||||
Collection ->
|
||||
Url.Builder.absolute [ "collection" ] []
|
||||
|
||||
DeckList ->
|
||||
Url.Builder.absolute [ "decks" ] []
|
||||
|
||||
|
||||
fromUrl : Url.Url -> Maybe Route
|
||||
fromUrl url =
|
||||
parse parser url
|
|
@ -4,11 +4,14 @@ module UI exposing
|
|||
, getViewport
|
||||
, isMobile
|
||||
, manaSpinner
|
||||
, text
|
||||
, title
|
||||
)
|
||||
|
||||
import Browser.Dom
|
||||
import Color
|
||||
import Element as E
|
||||
import Element.Font as Font
|
||||
import Spinner
|
||||
import Task
|
||||
|
||||
|
@ -127,3 +130,13 @@ manaSpinner =
|
|||
, width = 20
|
||||
, color = color
|
||||
}
|
||||
|
||||
|
||||
text : String -> E.Element msg
|
||||
text string =
|
||||
E.el [ Font.color colors.text ] <| E.text string
|
||||
|
||||
|
||||
title : String -> E.Element msg
|
||||
title string =
|
||||
E.el [ Font.color colors.title ] <| E.text string
|
||||
|
|
Loading…
Reference in a new issue