Add an elm frontend for the tasks api.

This commit is contained in:
Drew Galbraith 2024-07-05 20:53:54 -07:00
parent f507825f5f
commit 13e07e42df
5 changed files with 255 additions and 2 deletions

2
.gitignore vendored
View File

@ -2,3 +2,5 @@ target/
.env .env
*.db *.db
frontend/elm-stuff

27
frontend/elm.json Normal file
View File

@ -0,0 +1,27 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"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/url": "1.0.0"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.3"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -0,0 +1,86 @@
module Dashboard exposing (Model, Msg, Task, init, update, view)
import Browser exposing (Document)
import Browser.Navigation as Nav
import Html exposing (Html, div, text)
import Http
import Json.Decode exposing (Decoder, field, int, list, map3, maybe, string)
type alias Task =
{ id : Int
, title : String
, description : Maybe String
}
type alias Model =
{ navKey : Nav.Key
, tasks : Status (List Task)
}
type Status a
= Failure
| Loading
| Success a
init : Nav.Key -> ( Model, Cmd Msg )
init navKey =
( Model navKey Loading
, Http.get
{ url = "http://localhost:3000/tasks"
, expect = Http.expectJson GotTasks taskDecoder
}
)
type Msg
= GotTasks (Result Http.Error (List Task))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotTasks result ->
case result of
Ok tasks ->
( { model | tasks = Success tasks }, Cmd.none )
Err _ ->
( { model | tasks = Failure }, Cmd.none )
taskDecoder : Decoder (List Task)
taskDecoder =
list
(map3 Task
(field "id" int)
(field "title" string)
(maybe
(field "description"
string
)
)
)
view : Model -> Document Msg
view model =
{ title = "Dashboard"
, body = [ viewBody model ]
}
viewBody : Model -> Html Msg
viewBody model =
case model.tasks of
Failure ->
div [] [ text "Failed to load" ]
Loading ->
div [] [ text "Loading" ]
Success tasks ->
div [] [ text ("Loaded " ++ String.fromInt (List.length tasks) ++ " tasks") ]

129
frontend/src/Main.elm Normal file
View File

@ -0,0 +1,129 @@
module Main exposing (..)
import Browser
import Browser.Navigation as Nav
import Dashboard
import Html exposing (..)
import Html.Attributes exposing (..)
import Url
import Url.Parser as Parser exposing (Parser, oneOf)
main : Program () Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
type Model
= NotFound Nav.Key
| Dashboard Dashboard.Model
init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init _ url key =
stepUrl url (NotFound key)
type Route
= DashboardRoute
parser : Parser (Route -> a) a
parser =
oneOf
[ Parser.map DashboardRoute (Parser.s "tasks")
]
stepUrl : Url.Url -> Model -> ( Model, Cmd Msg )
stepUrl url model =
case Parser.parse parser url of
Just DashboardRoute ->
Dashboard.init (toNavKey model)
|> mapModelAndMsg Dashboard GotDashboardMsg
Nothing ->
( NotFound (toNavKey model), Cmd.none )
mapModelAndMsg : (subModel -> Model) -> (subMsg -> Msg) -> ( subModel, Cmd subMsg ) -> ( Model, Cmd Msg )
mapModelAndMsg toModel toMsg ( subModel, subCmd ) =
( toModel subModel
, Cmd.map toMsg subCmd
)
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
| GotDashboardMsg Dashboard.Msg
toNavKey : Model -> Nav.Key
toNavKey model =
case model of
NotFound key ->
key
Dashboard dashboard ->
dashboard.navKey
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case ( msg, model ) of
( LinkClicked urlRequest, _ ) ->
case urlRequest of
Browser.Internal url ->
( model, Nav.pushUrl (toNavKey model) (Url.toString url) )
Browser.External href ->
( model, Nav.load href )
( UrlChanged url, _ ) ->
stepUrl url model
( GotDashboardMsg dashboardMsg, Dashboard dashboard ) ->
Dashboard.update dashboardMsg dashboard
|> mapModelAndMsg Dashboard GotDashboardMsg
( _, _ ) ->
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
view : Model -> Browser.Document Msg
view model =
let
viewPage toMsg content =
{ title = content.title
, body = List.map (Html.map toMsg) content.body
}
in
case model of
NotFound _ ->
{ title = "Not Found"
, body =
[ text "This page was not found!"
, viewLink "/tasks"
]
}
Dashboard dashboard ->
viewPage GotDashboardMsg (Dashboard.view dashboard)
viewLink : String -> Html msg
viewLink path =
li [] [ a [ href path ] [ text path ] ]

View File

@ -1,4 +1,5 @@
use axum::extract::{Json, State}; use axum::extract::{Json, State};
use axum::http::header;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use serde::Deserialize; use serde::Deserialize;
@ -8,7 +9,11 @@ use crate::{global::AppState, models::Task};
pub async fn list(state: State<AppState>) -> impl IntoResponse { pub async fn list(state: State<AppState>) -> impl IntoResponse {
let tasks = Task::all(&state.db_pool).await.unwrap(); let tasks = Task::all(&state.db_pool).await.unwrap();
(StatusCode::OK, serde_json::to_string(&tasks).unwrap()) (
StatusCode::OK,
[(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
serde_json::to_string(&tasks).unwrap(),
)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -22,5 +27,9 @@ pub async fn create(state: State<AppState>, Json(req): Json<NewTask>) -> impl In
let new_task = task.insert(&state.db_pool).await.unwrap(); let new_task = task.insert(&state.db_pool).await.unwrap();
(StatusCode::OK, serde_json::to_string(&new_task).unwrap()) (
StatusCode::OK,
[(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
serde_json::to_string(&new_task).unwrap(),
)
} }