switch to elm-spa
This commit is contained in:
parent
edce6f1f37
commit
18d5a716e1
|
@ -88,6 +88,7 @@ func init() {
|
|||
"/static/favicon.png": true,
|
||||
"/debug/requests": true,
|
||||
"/debug/events": true,
|
||||
"/sign-in": true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
elm-stuff
|
||||
node_modules
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.DS_Store
|
||||
dist
|
||||
elm-stuff
|
||||
node_modules
|
|
@ -0,0 +1,24 @@
|
|||
# your elm-spa
|
||||
> learn more at [https://elm-spa.dev](https://elm-spa.dev)
|
||||
|
||||
### local development
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## folder structure
|
||||
|
||||
```elm
|
||||
README.md -- this file you're reading 👀
|
||||
elm.json -- has project dependencies
|
||||
src/
|
||||
Main.elm -- the entrypoint to the app
|
||||
Global.elm -- share state across pages
|
||||
Transitions.elm -- smoothly animate between pages
|
||||
Ports.elm -- communicate with JS
|
||||
Pages/ -- where all your pages go
|
||||
Layouts/ -- reusable views around pages
|
||||
Components/ -- views shared across the site
|
||||
Utils/ -- a place for helper functions
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ui": "Element"
|
||||
}
|
|
@ -1,24 +1,25 @@
|
|||
{
|
||||
"type": "application",
|
||||
"source-directories": [
|
||||
"src"
|
||||
"src",
|
||||
"elm-stuff/.elm-spa"
|
||||
],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"elm/browser": "1.0.2",
|
||||
"elm/core": "1.0.4",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/http": "2.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.1.3"
|
||||
"mdgriffith/elm-ui": "1.1.5",
|
||||
"ryannhg/elm-spa": "3.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/browser": "1.0.2",
|
||||
"elm/bytes": "1.0.8",
|
||||
"elm/file": "1.0.5",
|
||||
"elm/parser": "1.1.0",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.2"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# sends all routes to /index.html
|
||||
# (so you can handle 404s there!)
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "my-elm-spa-project",
|
||||
"version": "1.0.0",
|
||||
"description": "learn more at https://elm-spa.dev",
|
||||
"scripts": {
|
||||
"start": "npm install && npm run dev",
|
||||
"dev": "npm run elm:spa:build && npm run build:watch",
|
||||
"build": "npm run elm:spa:build && npm run elm:compile",
|
||||
"build:watch": "concurrently --raw --kill-others \"npm run elm:spa:watch\" \"npm run elm:live\"",
|
||||
"elm:compile": "elm make src/Main.elm --output=public/dist/elm.compiled.js --optimize",
|
||||
"elm:live": "elm-live src/Main.elm --dir=public --start-page=index.html --open --pushstate --port=1234 -- --output=public/dist/elm.compiled.js --debug",
|
||||
"elm:spa:build": "elm-spa build .",
|
||||
"elm:spa:watch": "chokidar src/Pages -c \"npm run elm:spa:build\""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "2.1.0",
|
||||
"concurrently": "5.0.0",
|
||||
"elm": "0.19.1-3",
|
||||
"elm-live": "4.0.1",
|
||||
"elm-spa": "3.0.3"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>our-elm-spa</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<script src="/dist/elm.compiled.js"></script>
|
||||
<script src="/ports.js"></script>
|
||||
<script>
|
||||
window.addEventListener('load', _ =>
|
||||
window.ports.init(Elm.Main.init())
|
||||
)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
// On load, listen to Elm!
|
||||
window.addEventListener('load', _ => {
|
||||
window.ports = {
|
||||
init: (app) =>
|
||||
app.ports.outgoing.subscribe(({ action, data }) =>
|
||||
actions[action]
|
||||
? actions[action](data)
|
||||
: console.warn(`I didn't recognize action "${action}".`)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// maps actions to functions!
|
||||
const actions = {
|
||||
'LOG': (message) =>
|
||||
console.log(`From Elm:`, message)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
/* you can include CSS here */
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
height: 100%;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# src/Components
|
||||
> views shared across the site
|
|
@ -0,0 +1,60 @@
|
|||
module Global exposing
|
||||
( Flags
|
||||
, Model
|
||||
, Msg(..)
|
||||
, init
|
||||
, subscriptions
|
||||
, update
|
||||
)
|
||||
|
||||
import Generated.Routes as Routes exposing (Route)
|
||||
import Ports
|
||||
|
||||
|
||||
type alias Flags =
|
||||
()
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ token : Maybe String
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= SignIn String
|
||||
| SignOut
|
||||
|
||||
|
||||
type alias Commands msg =
|
||||
{ navigate : Route -> Cmd msg
|
||||
}
|
||||
|
||||
|
||||
init : Commands msg -> Flags -> ( Model, Cmd Msg, Cmd msg )
|
||||
init _ _ =
|
||||
( { token = Nothing
|
||||
}
|
||||
, Cmd.none
|
||||
, Ports.log "Hello!"
|
||||
)
|
||||
|
||||
|
||||
update : Commands msg -> Msg -> Model -> ( Model, Cmd Msg, Cmd msg )
|
||||
update _ msg model =
|
||||
case msg of
|
||||
SignIn token ->
|
||||
( { model | token = Just token }
|
||||
, Cmd.none
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
SignOut ->
|
||||
( { model | token = Nothing }
|
||||
, Cmd.none
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions _ =
|
||||
Sub.none
|
|
@ -0,0 +1,70 @@
|
|||
module Layout exposing (view)
|
||||
|
||||
import Element exposing (..)
|
||||
import Element.Font as Font
|
||||
import Generated.Routes as Routes exposing (Route, routes)
|
||||
import Utils.Spa as Spa
|
||||
|
||||
|
||||
view : Spa.LayoutContext msg -> Element msg
|
||||
view { page, route, global } =
|
||||
case global.token of
|
||||
Just _ ->
|
||||
column [ height fill, width (fill |> maximum 750), centerX ]
|
||||
[ el [ padding 15 ] (viewHeader route)
|
||||
, page
|
||||
]
|
||||
|
||||
Nothing ->
|
||||
if route /= routes.signIn then
|
||||
el [ centerX, centerY ]
|
||||
(link
|
||||
[ Font.underline
|
||||
, Font.color (rgb255 204 75 75)
|
||||
, Font.size 48
|
||||
]
|
||||
{ label = text "Login"
|
||||
, url = Routes.toPath routes.signIn
|
||||
}
|
||||
)
|
||||
|
||||
else
|
||||
el
|
||||
[ centerX, centerY ]
|
||||
page
|
||||
|
||||
|
||||
viewHeader : Route -> Element msg
|
||||
viewHeader currentRoute =
|
||||
row
|
||||
[ spacing 24
|
||||
, paddingEach { top = 32, left = 16, right = 16, bottom = 0 }
|
||||
, centerX
|
||||
, width (fill |> maximum 750)
|
||||
]
|
||||
[ viewLink currentRoute ( "Mi", routes.top )
|
||||
, viewLink currentRoute ( "Switch Data", routes.switches )
|
||||
]
|
||||
|
||||
|
||||
viewLink : Route -> ( String, Route ) -> Element msg
|
||||
viewLink currentRoute ( label, route ) =
|
||||
if currentRoute == route then
|
||||
el
|
||||
[ Font.underline
|
||||
, Font.color (rgb255 204 75 75)
|
||||
, alpha 0.5
|
||||
, Font.size 16
|
||||
]
|
||||
(text label)
|
||||
|
||||
else
|
||||
link
|
||||
[ Font.underline
|
||||
, Font.color (rgb255 204 75 75)
|
||||
, Font.size 16
|
||||
, mouseOver [ alpha 0.5 ]
|
||||
]
|
||||
{ label = text label
|
||||
, url = Routes.toPath route
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# src/Layouts
|
||||
> where all your pages go
|
|
@ -1,185 +1,27 @@
|
|||
module Main exposing (main)
|
||||
|
||||
import Browser
|
||||
import Browser.Navigation as Nav
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onInput)
|
||||
import Http
|
||||
import Json.Decode as D
|
||||
import Mi exposing (..)
|
||||
import Page exposing (..)
|
||||
import Page.SwitchData as PSD
|
||||
import SwitchData
|
||||
import Url
|
||||
import Url.Builder
|
||||
import Generated.Pages as Pages
|
||||
import Generated.Routes as Routes exposing (routes)
|
||||
import Global
|
||||
import Spa
|
||||
import Transitions
|
||||
|
||||
|
||||
main : Program () Model Msg
|
||||
main : Spa.Program Global.Flags Global.Model Global.Msg Pages.Model Pages.Msg
|
||||
main =
|
||||
Browser.application
|
||||
{ init = init
|
||||
, view = view
|
||||
, update = update
|
||||
, subscriptions = subscriptions
|
||||
, onUrlChange = UrlChanged
|
||||
, onUrlRequest = LinkClicked
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ key : Nav.Key
|
||||
, url : Url.Url
|
||||
, token : Maybe String
|
||||
, token_data : Maybe TokenData
|
||||
, switch_data_model : PSD.Model
|
||||
}
|
||||
|
||||
|
||||
init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
|
||||
init flags url key =
|
||||
( Model
|
||||
key
|
||||
url
|
||||
Nothing
|
||||
Nothing
|
||||
PSD.init
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
type Page
|
||||
= NotFound
|
||||
| Index
|
||||
| SwitchData PSD.Model
|
||||
|
||||
|
||||
type Msg
|
||||
= LinkClicked Browser.UrlRequest
|
||||
| UrlChanged Url.Url
|
||||
| TokenInput String
|
||||
| TokenValidate (Result Http.Error TokenData)
|
||||
| Logout
|
||||
| GotSwitchDataMSG PSD.Msg
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
LinkClicked urlRequest ->
|
||||
case urlRequest of
|
||||
Browser.Internal url ->
|
||||
( model
|
||||
, Nav.pushUrl
|
||||
model.key
|
||||
(Url.toString url)
|
||||
)
|
||||
|
||||
Browser.External href ->
|
||||
( model, Nav.load href )
|
||||
|
||||
UrlChanged url ->
|
||||
case url.path of
|
||||
"/logout" ->
|
||||
( { model | token = Nothing, token_data = Nothing }
|
||||
, Nav.pushUrl
|
||||
model.key
|
||||
"/"
|
||||
)
|
||||
|
||||
default ->
|
||||
( { model | url = url }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
TokenInput token ->
|
||||
( { model | token = Just token }
|
||||
, request
|
||||
"GET"
|
||||
token
|
||||
"/.within/tokeninfo"
|
||||
Http.emptyBody
|
||||
(expectJson TokenValidate tokenDecoder)
|
||||
)
|
||||
|
||||
TokenValidate result ->
|
||||
case result of
|
||||
Ok data ->
|
||||
( { model | token_data = Just data }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Err _ ->
|
||||
( { model | token = Nothing }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Logout ->
|
||||
( { model | token = Nothing, token_data = Nothing }
|
||||
, Nav.load "/"
|
||||
)
|
||||
|
||||
GotSwitchDataMSG psd_msg ->
|
||||
let
|
||||
( psd_model, cmd ) =
|
||||
PSD.update
|
||||
(Maybe.withDefault "" model.token)
|
||||
psd_msg
|
||||
model.switch_data_model
|
||||
in
|
||||
( { model | switch_data_model = psd_model }
|
||||
, Cmd.map GotSwitchDataMSG cmd
|
||||
)
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions _ =
|
||||
Sub.none
|
||||
|
||||
|
||||
view : Model -> Browser.Document Msg
|
||||
view model =
|
||||
case model.token_data of
|
||||
Nothing ->
|
||||
{ title = "Login"
|
||||
, body =
|
||||
[ node "main"
|
||||
[ style "align" "center" ]
|
||||
[ h1 [] [ text "Login" ]
|
||||
, viewInput "password" "API Token" "" TokenInput
|
||||
]
|
||||
]
|
||||
Spa.create
|
||||
{ ui = Spa.usingElmUi
|
||||
, transitions = Transitions.transitions
|
||||
, routing =
|
||||
{ routes = Routes.parsers
|
||||
, toPath = Routes.toPath
|
||||
, notFound = routes.notFound
|
||||
, afterNavigate = Nothing
|
||||
}
|
||||
|
||||
Just token_data ->
|
||||
case model.url.path of
|
||||
"/" ->
|
||||
template "Mi"
|
||||
[ h1 [] [ text "Mi" ]
|
||||
, h2 [] [ text "TODO" ]
|
||||
, ul []
|
||||
[ li [] [ text "Switch CRUD" ]
|
||||
, li [] [ text "POSSE manual announcement" ]
|
||||
]
|
||||
, h2 [] [ text "Token data" ]
|
||||
, p []
|
||||
[ text "Token sub: "
|
||||
, text token_data.sub
|
||||
, Html.br [] []
|
||||
, text "ID: "
|
||||
, text token_data.jti
|
||||
]
|
||||
]
|
||||
|
||||
"/switch" ->
|
||||
PSD.view model.switch_data_model
|
||||
|
||||
other ->
|
||||
template "Not found"
|
||||
[ h1 [] [ text "Not found" ]
|
||||
, p []
|
||||
[ text "The requested URL "
|
||||
, b [] [ text other ]
|
||||
, text " was not found."
|
||||
]
|
||||
]
|
||||
, global =
|
||||
{ init = Global.init
|
||||
, update = Global.update
|
||||
, subscriptions = Global.subscriptions
|
||||
}
|
||||
, page = Pages.page
|
||||
}
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
module Page exposing (..)
|
||||
|
||||
import Browser exposing (Document)
|
||||
import Html exposing (Html, a, div, input, node, p, span, text)
|
||||
import Html.Attributes exposing (class, href, placeholder, style, type_, value)
|
||||
import Html.Events exposing (onInput)
|
||||
|
||||
|
||||
template : String -> List (Html msg) -> Browser.Document msg
|
||||
template title body =
|
||||
{ title = title
|
||||
, body =
|
||||
[ node "main"
|
||||
[]
|
||||
[ navBar
|
||||
, div [] body
|
||||
, footer
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
navBar : Html msg
|
||||
navBar =
|
||||
node "nav"
|
||||
[]
|
||||
[ p []
|
||||
[ viewLink "/" "Mi"
|
||||
, text " - "
|
||||
, viewLink "/switch" "Switch tracker"
|
||||
, span
|
||||
[ class "right" ]
|
||||
[ viewLink "/logout" "Logout" ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
footer : Html msg
|
||||
footer =
|
||||
node "footer"
|
||||
[]
|
||||
[ p []
|
||||
[ a [ href "https://within.website" ] [ text "From Within" ]
|
||||
, text " - "
|
||||
, a [ href "https://tulpa.dev/cadey/mi" ] [ text "Source code" ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewInput : String -> String -> String -> (String -> msg) -> Html msg
|
||||
viewInput t p v toMsg =
|
||||
input [ type_ t, placeholder p, value v, onInput toMsg ] []
|
||||
|
||||
|
||||
viewLink : String -> String -> Html msg
|
||||
viewLink path title =
|
||||
a [ href path ] [ text title ]
|
|
@ -1,102 +0,0 @@
|
|||
module Page.SwitchData exposing
|
||||
( Model
|
||||
, Msg
|
||||
, init
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Browser exposing (Document)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Http
|
||||
import Json.Decode exposing (list)
|
||||
import Mi
|
||||
import Page
|
||||
import SwitchData
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ page : Int
|
||||
, limit : Int
|
||||
, data : Data
|
||||
}
|
||||
|
||||
|
||||
type Data
|
||||
= Init
|
||||
| HaveData (List SwitchData.Switch)
|
||||
| Error String
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
Model
|
||||
0
|
||||
40
|
||||
Init
|
||||
|
||||
|
||||
type Msg
|
||||
= NeedData
|
||||
| Settings Int Int
|
||||
| GotData (Result Http.Error (List SwitchData.Switch))
|
||||
|
||||
|
||||
update : String -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update token msg model =
|
||||
if token == "" then
|
||||
( model, Cmd.none )
|
||||
|
||||
else
|
||||
case msg of
|
||||
NeedData ->
|
||||
( model
|
||||
, Mi.request
|
||||
"GET"
|
||||
token
|
||||
(SwitchData.listURL model.limit model.page)
|
||||
Http.emptyBody
|
||||
(Mi.expectJson GotData (Json.Decode.list SwitchData.decoder))
|
||||
)
|
||||
|
||||
Settings page limit ->
|
||||
( { model | page = page, limit = limit }
|
||||
, Mi.request
|
||||
"GET"
|
||||
token
|
||||
(SwitchData.listURL model.limit model.page)
|
||||
Http.emptyBody
|
||||
(Mi.expectJson GotData (Json.Decode.list SwitchData.decoder))
|
||||
)
|
||||
|
||||
GotData result ->
|
||||
case result of
|
||||
Ok data ->
|
||||
( { model | data = HaveData data }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Err _ ->
|
||||
( { model | data = Error "got an error" }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
view : Model -> Browser.Document msg
|
||||
view model =
|
||||
case model.data of
|
||||
Init ->
|
||||
Page.template "Switch data"
|
||||
[ h1 [] [ text "loading data..." ] ]
|
||||
|
||||
HaveData _ ->
|
||||
Page.template "Switch data"
|
||||
[ h1 [] [ text "Switch data here" ]
|
||||
]
|
||||
|
||||
Error msg ->
|
||||
Page.template "Switch data error"
|
||||
[ h1 [] [ text "oh no got an error" ]
|
||||
, p [] [ text msg ]
|
||||
]
|
|
@ -1,104 +0,0 @@
|
|||
module Page.Token exposing
|
||||
( Model
|
||||
, Msg
|
||||
, init
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Browser
|
||||
import Html exposing (Html, h1, h2, li, p, text, ul)
|
||||
import Http exposing (..)
|
||||
import Mi exposing (TokenData, expectJson, request, tokenDecoder)
|
||||
import Page
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ token : Maybe String
|
||||
, token_data : Maybe TokenData
|
||||
, error : Maybe String
|
||||
}
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
Model Nothing Nothing Nothing
|
||||
|
||||
|
||||
type Msg
|
||||
= Init
|
||||
| GotTokenData (Result Http.Error TokenData)
|
||||
|
||||
|
||||
update : Msg -> String -> Model -> ( Model, Cmd Msg )
|
||||
update msg token model =
|
||||
case msg of
|
||||
Init ->
|
||||
( { model | token = Just token }
|
||||
, Mi.request
|
||||
"GET"
|
||||
token
|
||||
"/.within/tokeninfo"
|
||||
Http.emptyBody
|
||||
(Mi.expectJson GotTokenData tokenDecoder)
|
||||
)
|
||||
|
||||
GotTokenData result ->
|
||||
case result of
|
||||
Ok data ->
|
||||
( { model | token_data = Just data }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Err (BadUrl val) ->
|
||||
( { model | error = Just ("bad URL " ++ val) }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Err Timeout ->
|
||||
( { model | error = Just "Timeout" }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Err NetworkError ->
|
||||
( { model | error = Just "network error" }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Err (BadStatus code) ->
|
||||
( { model | error = Just ("bad status code " ++ String.fromInt code) }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Err (BadBody err_msg) ->
|
||||
( { model | error = Just err_msg }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
view : Model -> Browser.Document msg
|
||||
view model =
|
||||
case model.token_data of
|
||||
Nothing ->
|
||||
Page.template "No token data?"
|
||||
[ h1 [] [ text "No token data?" ]
|
||||
, p [] [ text "this should be impossible" ]
|
||||
]
|
||||
|
||||
Just token_data ->
|
||||
Page.template "Mi"
|
||||
[ h1 [] [ text "Mi" ]
|
||||
, h2 [] [ text "TODO" ]
|
||||
, ul []
|
||||
[ li [] [ text "Switch CRUD" ]
|
||||
, li [] [ text "POSSE manual announcement" ]
|
||||
]
|
||||
, h2 [] [ text "Token data" ]
|
||||
, p []
|
||||
[ text "Token sub: "
|
||||
, text token_data.sub
|
||||
, Html.br [] []
|
||||
, text "ID: "
|
||||
, text token_data.jti
|
||||
]
|
||||
]
|
|
@ -0,0 +1,39 @@
|
|||
module Pages.NotFound exposing (Model, Msg, page)
|
||||
|
||||
import Element exposing (..)
|
||||
import Element.Font as Font
|
||||
import Generated.Params as Params
|
||||
import Generated.Routes as Routes exposing (routes)
|
||||
import Spa.Page
|
||||
import Utils.Spa exposing (Page)
|
||||
|
||||
|
||||
type alias Model =
|
||||
()
|
||||
|
||||
|
||||
type alias Msg =
|
||||
Never
|
||||
|
||||
|
||||
page : Page Params.NotFound Model Msg model msg appMsg
|
||||
page =
|
||||
Spa.Page.static
|
||||
{ title = always "not found | elm-spa"
|
||||
, view = always view
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
view : Element Msg
|
||||
view =
|
||||
column [ centerX, centerY, spacing 16 ]
|
||||
[ el [ Font.size 32, Font.semiBold ] (text "404 is life.")
|
||||
, link [ Font.size 16, Font.underline, centerX, Font.color (rgb255 204 75 75), mouseOver [ alpha 0.5 ] ]
|
||||
{ label = text "back home?"
|
||||
, url = Routes.toPath routes.top
|
||||
}
|
||||
]
|
|
@ -0,0 +1,3 @@
|
|||
# src/Pages
|
||||
> where all your pages go
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
module Pages.SignIn exposing (Model, Msg, page)
|
||||
|
||||
import Element exposing (..)
|
||||
import Element.Input as Input
|
||||
import Generated.Params as Params
|
||||
import Global
|
||||
import Http
|
||||
import Mi
|
||||
import Spa.Page
|
||||
import Utils.Spa exposing (Page)
|
||||
|
||||
|
||||
page : Page Params.SignIn Model Msg model msg appMsg
|
||||
page =
|
||||
Spa.Page.component
|
||||
{ title = always "Login"
|
||||
, init = always init
|
||||
, update = always update
|
||||
, subscriptions = always subscriptions
|
||||
, view = always view
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- INIT
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ token : String
|
||||
, tokenData : Maybe Mi.TokenData
|
||||
, error : Maybe String
|
||||
}
|
||||
|
||||
|
||||
init : Params.SignIn -> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||
init _ =
|
||||
( { token = ""
|
||||
, tokenData = Nothing
|
||||
, error = Nothing
|
||||
}
|
||||
, Cmd.none
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
type Msg
|
||||
= SignIn
|
||||
| SignOut
|
||||
| ValidateToken (Result Http.Error Mi.TokenData)
|
||||
| UpdateToken String
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
UpdateToken token ->
|
||||
( { model | token = token }
|
||||
, Cmd.none
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
ValidateToken result ->
|
||||
case result of
|
||||
Ok data ->
|
||||
( { model | tokenData = Just data }
|
||||
, Cmd.none
|
||||
, Spa.Page.send (Global.SignIn model.token)
|
||||
)
|
||||
|
||||
Err _ ->
|
||||
( { model | token = "", error = Just "got an error :(" }
|
||||
, Cmd.none
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
SignIn ->
|
||||
( model
|
||||
, Mi.request
|
||||
"GET"
|
||||
model.token
|
||||
"/.within/tokeninfo"
|
||||
Http.emptyBody
|
||||
(Mi.expectJson ValidateToken Mi.tokenDecoder)
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
SignOut ->
|
||||
( model
|
||||
, Cmd.none
|
||||
, Spa.Page.send Global.SignOut
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- SUBSCRIPTIONS
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Sub.none
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
view : Model -> Element Msg
|
||||
view model =
|
||||
let
|
||||
errMsg =
|
||||
case model.error of
|
||||
Just msg ->
|
||||
msg
|
||||
|
||||
Nothing ->
|
||||
""
|
||||
|
||||
tData =
|
||||
case model.tokenData of
|
||||
Just data ->
|
||||
Element.paragraph []
|
||||
[ text ("ID: " ++ data.jti)
|
||||
, paragraph [] [ text ("Sub: " ++ data.sub) ]
|
||||
]
|
||||
|
||||
Nothing ->
|
||||
Element.column []
|
||||
[ Input.text []
|
||||
{ label = Input.labelAbove [] (text "API Token")
|
||||
, onChange = UpdateToken
|
||||
, text = model.token
|
||||
, placeholder = Just (Input.placeholder [] (text ""))
|
||||
}
|
||||
, Input.button [] { onPress = Just SignIn, label = text "Login" }
|
||||
]
|
||||
in
|
||||
column [ spacing 16 ]
|
||||
[ tData
|
||||
, text errMsg
|
||||
]
|
|
@ -0,0 +1,68 @@
|
|||
module Pages.Switches exposing (Model, Msg, page)
|
||||
|
||||
import Spa.Page
|
||||
import Element exposing (..)
|
||||
import Generated.Params as Params
|
||||
import Global
|
||||
import Utils.Spa exposing (Page)
|
||||
|
||||
|
||||
page : Page Params.Switches Model Msg model msg appMsg
|
||||
page =
|
||||
Spa.Page.component
|
||||
{ title = always "Switches"
|
||||
, init = always init
|
||||
, update = always update
|
||||
, subscriptions = always subscriptions
|
||||
, view = always view
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- INIT
|
||||
|
||||
|
||||
type alias Model =
|
||||
{}
|
||||
|
||||
|
||||
init : Params.Switches -> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||
init _ =
|
||||
( {}
|
||||
, Cmd.none
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
type Msg
|
||||
= Msg
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||
update msg model =
|
||||
( model
|
||||
, Cmd.none
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- SUBSCRIPTIONS
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Sub.none
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
view : Model -> Element Msg
|
||||
view model =
|
||||
text "Switches"
|
|
@ -0,0 +1,59 @@
|
|||
module Pages.Top exposing (Model, Msg, page)
|
||||
|
||||
import Element exposing (..)
|
||||
import Element.Font as Font
|
||||
import Generated.Params as Params
|
||||
import Generated.Routes as Routes exposing (Route, routes)
|
||||
import Spa.Page
|
||||
import Utils.Spa exposing (Page)
|
||||
|
||||
|
||||
type alias Model =
|
||||
()
|
||||
|
||||
|
||||
type alias Msg =
|
||||
Never
|
||||
|
||||
|
||||
page : Page Params.Top Model Msg model msg appMsg
|
||||
page =
|
||||
Spa.Page.static
|
||||
{ title = always "/"
|
||||
, view = view
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
view : Utils.Spa.PageContext -> Element Msg
|
||||
view { global } =
|
||||
case global.token of
|
||||
Just _ ->
|
||||
column
|
||||
[ spacing 12
|
||||
]
|
||||
[ row [ spacing 14 ]
|
||||
[ el [ Font.size 48, Font.semiBold ] (text "Mi")
|
||||
, el [ alpha 0.5 ] (text "POSSE and stuff")
|
||||
]
|
||||
, text "TODO:"
|
||||
, text "* POSSE"
|
||||
, text "* Switch Data"
|
||||
]
|
||||
|
||||
Nothing ->
|
||||
el
|
||||
[ Font.size 48, centerX, centerY ]
|
||||
(link
|
||||
[ Font.underline
|
||||
, Font.color (rgb255 204 75 75)
|
||||
, Font.size 48
|
||||
, mouseOver [ alpha 0.5 ]
|
||||
]
|
||||
{ label = text "Login"
|
||||
, url = Routes.toPath routes.signIn
|
||||
}
|
||||
)
|
|
@ -0,0 +1,14 @@
|
|||
port module Ports exposing (log)
|
||||
|
||||
import Json.Encode as Json
|
||||
|
||||
|
||||
port outgoing : { action : String, data : Json.Value } -> Cmd msg
|
||||
|
||||
|
||||
log : String -> Cmd msg
|
||||
log message =
|
||||
outgoing
|
||||
{ action = "LOG"
|
||||
, data = Json.string message
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
module SwitchData exposing
|
||||
( Switch
|
||||
, decoder
|
||||
, frontURL
|
||||
, idURL
|
||||
, listURL
|
||||
, switchURL
|
||||
)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Iso8601
|
||||
import Json.Decode exposing (Decoder, field, int, map5, nullable, string)
|
||||
import Time exposing (Posix)
|
||||
import Url.Builder as UB
|
||||
|
||||
|
||||
type alias Switch =
|
||||
{ id : String
|
||||
, who : String
|
||||
, started_at : Posix
|
||||
, ended_at : Maybe Posix
|
||||
, duration : Int
|
||||
}
|
||||
|
||||
|
||||
decoder : Decoder Switch
|
||||
decoder =
|
||||
map5 Switch
|
||||
(field "id" string)
|
||||
(field "who" string)
|
||||
(field "started_at" Iso8601.decoder)
|
||||
(field "ended_at" (nullable Iso8601.decoder))
|
||||
(field "duration" int)
|
||||
|
||||
|
||||
switchURL : String
|
||||
switchURL =
|
||||
UB.absolute
|
||||
[ "switches", "switch" ]
|
||||
[]
|
||||
|
||||
|
||||
idURL : String -> String
|
||||
idURL id =
|
||||
UB.absolute
|
||||
[ "switches", "id", id ]
|
||||
[]
|
||||
|
||||
|
||||
frontURL : String
|
||||
frontURL =
|
||||
UB.absolute
|
||||
[ "switches", "current" ]
|
||||
[]
|
||||
|
||||
|
||||
listURL : Int -> Int -> String
|
||||
listURL limit page =
|
||||
UB.absolute
|
||||
[ "switches", "" ]
|
||||
[ UB.int "limit" limit
|
||||
, UB.int "page" page
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
module Transitions exposing (transitions)
|
||||
|
||||
import Spa.Transition as Transition
|
||||
import Utils.Spa as Spa
|
||||
|
||||
|
||||
transitions : Spa.Transitions msg
|
||||
transitions =
|
||||
{ layout = Transition.fadeElmUi 500
|
||||
, page = Transition.fadeElmUi 300
|
||||
, pages = []
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# src/Utils
|
||||
> a place for helper functions
|
|
@ -0,0 +1,72 @@
|
|||
module Utils.Spa exposing
|
||||
( Bundle
|
||||
, Init
|
||||
, LayoutContext
|
||||
, Page
|
||||
, PageContext
|
||||
, Recipe
|
||||
, Transitions
|
||||
, Update
|
||||
, layout
|
||||
, recipe
|
||||
)
|
||||
|
||||
import Element exposing (Element)
|
||||
import Generated.Routes as Routes exposing (Route)
|
||||
import Global
|
||||
import Spa.Page
|
||||
import Spa.Types
|
||||
|
||||
|
||||
type alias Page params model msg layoutModel layoutMsg appMsg =
|
||||
Spa.Types.Page Route params model msg (Element msg) layoutModel layoutMsg (Element layoutMsg) Global.Model Global.Msg appMsg (Element appMsg)
|
||||
|
||||
|
||||
type alias Recipe params model msg layoutModel layoutMsg appMsg =
|
||||
Spa.Types.Recipe Route params model msg layoutModel layoutMsg (Element layoutMsg) Global.Model Global.Msg appMsg (Element appMsg)
|
||||
|
||||
|
||||
type alias Init model msg =
|
||||
Spa.Types.Init Route model msg Global.Model Global.Msg
|
||||
|
||||
|
||||
type alias Update model msg =
|
||||
Spa.Types.Update Route model msg Global.Model Global.Msg
|
||||
|
||||
|
||||
type alias Bundle msg appMsg =
|
||||
Spa.Types.Bundle Route msg (Element msg) Global.Model Global.Msg appMsg (Element appMsg)
|
||||
|
||||
|
||||
type alias LayoutContext msg =
|
||||
Spa.Types.LayoutContext Route msg (Element msg) Global.Model Global.Msg
|
||||
|
||||
|
||||
type alias PageContext =
|
||||
Spa.Types.PageContext Route Global.Model
|
||||
|
||||
|
||||
type alias Layout params model msg appMsg =
|
||||
Spa.Types.Layout Route params model msg (Element msg) Global.Model Global.Msg appMsg (Element appMsg)
|
||||
|
||||
|
||||
layout :
|
||||
Layout params model msg appMsg
|
||||
-> Page params model msg layoutModel layoutMsg appMsg
|
||||
layout =
|
||||
Spa.Page.layout Element.map
|
||||
|
||||
|
||||
type alias Upgrade params model msg layoutModel layoutMsg appMsg =
|
||||
Spa.Types.Upgrade Route params model msg (Element msg) layoutModel layoutMsg (Element layoutMsg) Global.Model Global.Msg appMsg (Element appMsg)
|
||||
|
||||
|
||||
recipe :
|
||||
Upgrade params model msg layoutModel layoutMsg appMsg
|
||||
-> Recipe params model msg layoutModel layoutMsg appMsg
|
||||
recipe =
|
||||
Spa.Page.recipe Element.map
|
||||
|
||||
|
||||
type alias Transitions msg =
|
||||
Spa.Types.Transitions (Element msg)
|
|
@ -1,78 +1,5 @@
|
|||
main {
|
||||
font-family: monospace, monospace;
|
||||
max-width: 38rem;
|
||||
padding: 2rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 736px) {
|
||||
main {
|
||||
padding: 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #d3869b;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #282828;
|
||||
color: #ebdbb2;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #3c3836;
|
||||
padding: 1em;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
a, a:active, a:visited {
|
||||
color: #b16286;
|
||||
background-color: #1d2021;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
margin-bottom: .1rem;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 1px solid #bdae93;
|
||||
margin: 0.5em 10px;
|
||||
padding: 0.5em 10px;
|
||||
}
|
||||
|
||||
footer {
|
||||
align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
body {
|
||||
background: #fbf1c7;
|
||||
color: #3c3836;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #ebdbb2;
|
||||
padding: 1em;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a, a:active, a:visited {
|
||||
color: #b16286;
|
||||
background-color: #f9f5d7;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
margin-bottom: .1rem;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 1px solid #655c54;
|
||||
margin: 0.5em 10px;
|
||||
padding: 0.5em 10px;
|
||||
}
|
||||
}
|
||||
/* you can include CSS here */
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
height: 100%;
|
||||
}
|
Loading…
Reference in New Issue