Move to using rendered templates in rust.
This commit is contained in:
parent
6050f1cb70
commit
381910d462
File diff suppressed because it is too large
Load Diff
|
@ -4,6 +4,8 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
askama = { version = "0.12.1", features=[ "with-axum" ] }
|
||||
askama_axum = "0.4.0"
|
||||
axum = "0.7"
|
||||
dotenvy = "0.15"
|
||||
http = "1.1.0"
|
||||
|
@ -12,4 +14,4 @@ serde_json = "1.0"
|
|||
sqlx = { version = "0.7", features=[ "runtime-tokio", "sqlite" ] }
|
||||
tokio = { version = "1.38", features=[ "full" ] }
|
||||
tower = "0.4.13"
|
||||
tower-http = { version = "0.5.2", features=[ "cors" ] }
|
||||
tower-http = { version = "0.5.2", features=[ "cors", "fs" ] }
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
auto_https off
|
||||
}
|
||||
|
||||
http://localhost:8000
|
||||
|
||||
handle_errors {
|
||||
rewrite * /index.html
|
||||
file_server
|
||||
}
|
||||
|
||||
file_server
|
||||
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"targets": {
|
||||
"Captain's Log": {
|
||||
"inputs": [
|
||||
"src/Main.elm"
|
||||
],
|
||||
"output": "build/main.js"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"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": {}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Captains Log</title>
|
||||
<script type="text/javascript" src="/build/main.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
||||
crossorigin="anonymous" />
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
var app = Elm.Main.init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,259 +0,0 @@
|
|||
module Dashboard exposing (Model, Msg, Task, init, update, view)
|
||||
|
||||
import Browser exposing (Document)
|
||||
import Browser.Navigation as Nav
|
||||
import Html exposing (Html, button, div, h6, input, p, text, textarea)
|
||||
import Html.Attributes exposing (class, placeholder, value)
|
||||
import Html.Events exposing (onClick, onInput)
|
||||
import Http
|
||||
import Json.Decode exposing (Decoder, field, int, list, map3, maybe, string)
|
||||
import Json.Encode as Encode
|
||||
|
||||
|
||||
type alias Task =
|
||||
{ id : Int
|
||||
, title : String
|
||||
, description : Maybe String
|
||||
}
|
||||
|
||||
|
||||
type alias CreateTaskInfo =
|
||||
{ title : String, description : String }
|
||||
|
||||
|
||||
defaultCreateTask : CreateTaskInfo
|
||||
defaultCreateTask =
|
||||
{ title = ""
|
||||
, description = ""
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ navKey : Nav.Key
|
||||
, tasks : Status (List Task)
|
||||
, createTaskCollapsed : Bool
|
||||
, createTask : CreateTaskInfo
|
||||
}
|
||||
|
||||
|
||||
type Status a
|
||||
= Failure
|
||||
| Loading
|
||||
| Success a
|
||||
|
||||
|
||||
init : Nav.Key -> ( Model, Cmd Msg )
|
||||
init navKey =
|
||||
reloadTasks (Model navKey Loading True defaultCreateTask)
|
||||
|
||||
|
||||
reloadTasks : Model -> ( Model, Cmd Msg )
|
||||
reloadTasks model =
|
||||
( model
|
||||
, Http.get
|
||||
{ url = "http://localhost:3000/tasks"
|
||||
, expect = Http.expectJson GotTasks (list taskDecoder)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
type Msg
|
||||
= GotTasks (Result Http.Error (List Task))
|
||||
| ToggleCreateTask
|
||||
| CreateTaskUpdateTitle String
|
||||
| CreateTaskUpdateDescription String
|
||||
| CreateTask
|
||||
| GotTaskCreated (Result Http.Error Task)
|
||||
| DeleteTask Int
|
||||
| GotTaskDeleted (Result Http.Error ())
|
||||
|
||||
|
||||
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 )
|
||||
|
||||
ToggleCreateTask ->
|
||||
( { model | createTaskCollapsed = not model.createTaskCollapsed }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
CreateTask ->
|
||||
( model
|
||||
, Http.post
|
||||
{ url = "http://localhost:3000/tasks"
|
||||
, body = Http.jsonBody (taskEncoder model.createTask)
|
||||
, expect = Http.expectJson GotTaskCreated taskDecoder
|
||||
}
|
||||
)
|
||||
|
||||
GotTaskCreated result ->
|
||||
case result of
|
||||
Ok task ->
|
||||
case model.tasks of
|
||||
Success tasks ->
|
||||
( { model | tasks = Success (task :: tasks) }, Cmd.none )
|
||||
|
||||
_ ->
|
||||
( { model | tasks = Success [ task ] }, Cmd.none )
|
||||
|
||||
Err _ ->
|
||||
( model, Cmd.none )
|
||||
|
||||
CreateTaskUpdateTitle title ->
|
||||
let
|
||||
createTask =
|
||||
model.createTask
|
||||
in
|
||||
( { model | createTask = { createTask | title = title } }, Cmd.none )
|
||||
|
||||
CreateTaskUpdateDescription description ->
|
||||
let
|
||||
createTask =
|
||||
model.createTask
|
||||
in
|
||||
( { model | createTask = { createTask | description = description } }, Cmd.none )
|
||||
|
||||
DeleteTask id ->
|
||||
( model
|
||||
, Http.request
|
||||
{ method = "DELETE"
|
||||
, headers = []
|
||||
, url = "http://localhost:3000/tasks/" ++ String.fromInt id
|
||||
, body = Http.emptyBody
|
||||
, expect = Http.expectWhatever GotTaskDeleted
|
||||
, timeout = Nothing
|
||||
, tracker = Nothing
|
||||
}
|
||||
)
|
||||
|
||||
GotTaskDeleted _ ->
|
||||
reloadTasks model
|
||||
|
||||
|
||||
taskDecoder : Decoder Task
|
||||
taskDecoder =
|
||||
map3 Task
|
||||
(field "id" int)
|
||||
(field "title" string)
|
||||
(maybe
|
||||
(field "description"
|
||||
string
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
taskEncoder : CreateTaskInfo -> Encode.Value
|
||||
taskEncoder task =
|
||||
Encode.object
|
||||
[ ( "title", Encode.string task.title )
|
||||
, ( "description", Encode.string task.description )
|
||||
]
|
||||
|
||||
|
||||
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 [ class "container", class "bg-light" ]
|
||||
[ viewTaskList tasks
|
||||
, viewCreate model
|
||||
]
|
||||
|
||||
|
||||
viewTaskList : List Task -> Html Msg
|
||||
viewTaskList tasks =
|
||||
div [ class "p-3" ]
|
||||
[ div
|
||||
[ class "list-group" ]
|
||||
(List.map viewTaskListItem tasks
|
||||
++ [ div [ class "list-group-item" ]
|
||||
[ text
|
||||
("Loaded "
|
||||
++ String.fromInt (List.length tasks)
|
||||
++ " tasks"
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
viewTaskListItem : Task -> Html Msg
|
||||
viewTaskListItem task =
|
||||
div [ class "list-group-item d-flex justify-content-between" ]
|
||||
[ input [ Html.Attributes.type_ "checkbox" ] []
|
||||
, div []
|
||||
[ h6 []
|
||||
[ text task.title ]
|
||||
, p []
|
||||
[ text
|
||||
(case task.description of
|
||||
Just desc ->
|
||||
desc
|
||||
|
||||
_ ->
|
||||
""
|
||||
)
|
||||
]
|
||||
]
|
||||
, div []
|
||||
[ button
|
||||
[ class "btn"
|
||||
, class "btn-danger"
|
||||
, onClick (DeleteTask task.id)
|
||||
]
|
||||
[ Html.i [ class "bi-trash3" ] []
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewCreate : Model -> Html Msg
|
||||
viewCreate model =
|
||||
if model.createTaskCollapsed then
|
||||
div [] [ button [ onClick ToggleCreateTask ] [ text "+ Expand" ] ]
|
||||
|
||||
else
|
||||
div []
|
||||
[ button [ onClick ToggleCreateTask ] [ text "- Collapse" ]
|
||||
, div []
|
||||
[ input
|
||||
[ placeholder "Task Title"
|
||||
, value
|
||||
model.createTask.title
|
||||
, onInput CreateTaskUpdateTitle
|
||||
]
|
||||
[]
|
||||
]
|
||||
, div []
|
||||
[ textarea
|
||||
[ placeholder "Task Description"
|
||||
, value
|
||||
model.createTask.description
|
||||
, onInput CreateTaskUpdateDescription
|
||||
]
|
||||
[]
|
||||
]
|
||||
, div []
|
||||
[ button [ onClick CreateTask ] [ text "Create Task" ] ]
|
||||
]
|
|
@ -1,129 +0,0 @@
|
|||
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 ] ]
|
|
@ -25,6 +25,12 @@ async fn main() {
|
|||
|
||||
let app = axum::Router::new()
|
||||
.fallback(handle404)
|
||||
.route("/", get(routes::tasks::home))
|
||||
.nest_service("/assets", tower_http::services::ServeDir::new("assets"))
|
||||
.nest_service(
|
||||
"/favicon.ico",
|
||||
tower_http::services::ServeFile::new("assests/favicon.ico"),
|
||||
)
|
||||
.route(
|
||||
"/tasks",
|
||||
get(routes::tasks::list).post(routes::tasks::create),
|
||||
|
|
|
@ -3,9 +3,9 @@ use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool};
|
|||
|
||||
#[derive(Serialize, Deserialize, FromRow)]
|
||||
pub struct Task {
|
||||
id: i64,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use askama_axum::Template;
|
||||
use axum::extract::{Json, Path, State};
|
||||
use axum::http::header;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use serde::Deserialize;
|
||||
|
@ -7,6 +7,18 @@ use serde::Deserialize;
|
|||
use crate::global::FormErrorResponse;
|
||||
use crate::{global::AppState, models::Task};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
struct HomeTemplate {
|
||||
pub tasks: Vec<Task>,
|
||||
}
|
||||
|
||||
pub async fn home(state: State<AppState>) -> impl IntoResponse {
|
||||
HomeTemplate {
|
||||
tasks: Task::all(&state.db_pool).await.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list(state: State<AppState>) -> impl IntoResponse {
|
||||
let tasks = Task::all(&state.db_pool).await.unwrap();
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Captains Log</title>
|
||||
<script type="text/javascript" src="/build/main.js"></script>
|
||||
<link href="/assets/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom">
|
||||
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none">
|
||||
<svg class="bi me-2" width="40" height="32"><use xlink:href="#bootstrap"></use></svg>
|
||||
<span class="fs-4">Captains Log</span>
|
||||
</a>
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item"><a href="#" class="nav-link active" aria-current="page">Home</a></li>
|
||||
<li class="nav-item"><a href="#" class="nav-link">Features</a></li>
|
||||
<li class="nav-item"><a href="#" class="nav-link">Pricing</a></li>
|
||||
<li class="nav-item"><a href="#" class="nav-link">FAQs</a></li>
|
||||
<li class="nav-item"><a href="#" class="nav-link">About</a></li>
|
||||
</ul>
|
||||
</header>
|
||||
<ul>
|
||||
{% for task in tasks %}
|
||||
<li>{{ task.title }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue