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"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
askama = { version = "0.12.1", features=[ "with-axum" ] }
|
||||||
|
askama_axum = "0.4.0"
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
http = "1.1.0"
|
http = "1.1.0"
|
||||||
|
@ -12,4 +14,4 @@ serde_json = "1.0"
|
||||||
sqlx = { version = "0.7", features=[ "runtime-tokio", "sqlite" ] }
|
sqlx = { version = "0.7", features=[ "runtime-tokio", "sqlite" ] }
|
||||||
tokio = { version = "1.38", features=[ "full" ] }
|
tokio = { version = "1.38", features=[ "full" ] }
|
||||||
tower = "0.4.13"
|
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()
|
let app = axum::Router::new()
|
||||||
.fallback(handle404)
|
.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(
|
.route(
|
||||||
"/tasks",
|
"/tasks",
|
||||||
get(routes::tasks::list).post(routes::tasks::create),
|
get(routes::tasks::list).post(routes::tasks::create),
|
||||||
|
|
|
@ -3,9 +3,9 @@ use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, FromRow)]
|
#[derive(Serialize, Deserialize, FromRow)]
|
||||||
pub struct Task {
|
pub struct Task {
|
||||||
id: i64,
|
pub id: i64,
|
||||||
title: String,
|
pub title: String,
|
||||||
description: Option<String>,
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Task {
|
impl Task {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use askama_axum::Template;
|
||||||
use axum::extract::{Json, Path, State};
|
use axum::extract::{Json, Path, 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;
|
||||||
|
@ -7,6 +7,18 @@ use serde::Deserialize;
|
||||||
use crate::global::FormErrorResponse;
|
use crate::global::FormErrorResponse;
|
||||||
use crate::{global::AppState, models::Task};
|
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 {
|
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();
|
||||||
|
|
||||||
|
|
|
@ -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