From 13e07e42df346ff83ea791fb2b9202a8f7b5ae32 Mon Sep 17 00:00:00 2001 From: Drew Galbraith Date: Fri, 5 Jul 2024 20:53:54 -0700 Subject: [PATCH] Add an elm frontend for the tasks api. --- .gitignore | 2 + frontend/elm.json | 27 ++++++++ frontend/src/Dashboard.elm | 86 +++++++++++++++++++++++++ frontend/src/Main.elm | 129 +++++++++++++++++++++++++++++++++++++ src/routes/tasks.rs | 13 +++- 5 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 frontend/elm.json create mode 100644 frontend/src/Dashboard.elm create mode 100644 frontend/src/Main.elm diff --git a/.gitignore b/.gitignore index ab22836..4412991 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ target/ .env *.db + +frontend/elm-stuff diff --git a/frontend/elm.json b/frontend/elm.json new file mode 100644 index 0000000..b6da5ad --- /dev/null +++ b/frontend/elm.json @@ -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": {} + } +} diff --git a/frontend/src/Dashboard.elm b/frontend/src/Dashboard.elm new file mode 100644 index 0000000..12ee2aa --- /dev/null +++ b/frontend/src/Dashboard.elm @@ -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") ] diff --git a/frontend/src/Main.elm b/frontend/src/Main.elm new file mode 100644 index 0000000..3e24de2 --- /dev/null +++ b/frontend/src/Main.elm @@ -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 ] ] diff --git a/src/routes/tasks.rs b/src/routes/tasks.rs index bb38f7c..397abaf 100644 --- a/src/routes/tasks.rs +++ b/src/routes/tasks.rs @@ -1,4 +1,5 @@ use axum::extract::{Json, State}; +use axum::http::header; use axum::http::StatusCode; use axum::response::IntoResponse; use serde::Deserialize; @@ -8,7 +9,11 @@ use crate::{global::AppState, models::Task}; pub async fn list(state: State) -> impl IntoResponse { 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)] @@ -22,5 +27,9 @@ pub async fn create(state: State, Json(req): Json) -> impl In 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(), + ) }