From e235ea9bd34faef2faa61f6cb437b0988fbe25b9 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 22 Oct 2021 18:05:54 -0400 Subject: [PATCH] Notes modal WIP Also updated for F# 6 syntax and added local fallback for HTMX and Bootstrap CSS/JS --- src/MyPrayerJournal/Server/.gitignore | 3 - src/MyPrayerJournal/Server/Data.fs | 86 +++++++++---------- src/MyPrayerJournal/Server/Dates.fs | 62 ++++++------- src/MyPrayerJournal/Server/Domain.fs | 2 +- src/MyPrayerJournal/Server/Handlers.fs | 63 +++++++------- src/MyPrayerJournal/Server/Program.fs | 13 +-- src/MyPrayerJournal/Server/Views.fs | 78 +++++++++-------- .../wwwroot/script/bootstrap.bundle.min.js | 7 ++ .../Server/wwwroot/script/htmx-1.5.0.min.js | 1 + .../Server/wwwroot/script/mpj.js | 16 ++++ .../Server/wwwroot/style/bootstrap.min.css | 7 ++ 11 files changed, 189 insertions(+), 149 deletions(-) create mode 100644 src/MyPrayerJournal/Server/wwwroot/script/bootstrap.bundle.min.js create mode 100644 src/MyPrayerJournal/Server/wwwroot/script/htmx-1.5.0.min.js create mode 100644 src/MyPrayerJournal/Server/wwwroot/style/bootstrap.min.css diff --git a/src/MyPrayerJournal/Server/.gitignore b/src/MyPrayerJournal/Server/.gitignore index da3bc93..22673b1 100644 --- a/src/MyPrayerJournal/Server/.gitignore +++ b/src/MyPrayerJournal/Server/.gitignore @@ -12,8 +12,5 @@ wwwroot/*.js.map wwwroot/css wwwroot/js -## Local library files -wwwroot/script/htmx*.js - ## Development settings appsettings.Development.json diff --git a/src/MyPrayerJournal/Server/Data.fs b/src/MyPrayerJournal/Server/Data.fs index 3538f07..9948e4a 100644 --- a/src/MyPrayerJournal/Server/Data.fs +++ b/src/MyPrayerJournal/Server/Data.fs @@ -14,10 +14,10 @@ module Extensions = type LiteDatabase with /// The Request collection member this.requests - with get () = this.GetCollection("request") + with get () = this.GetCollection "request" /// Async version of the checkpoint command (flushes log) member this.saveChanges () = - this.Checkpoint() + this.Checkpoint () Task.CompletedTask @@ -29,56 +29,56 @@ module Mapping = /// Map a history entry to BSON let historyToBson (hist : History) : BsonValue = let doc = BsonDocument () - doc.["asOf"] <- BsonValue (Ticks.toLong hist.asOf) - doc.["status"] <- BsonValue (RequestAction.toString hist.status) - doc.["text"] <- BsonValue (match hist.text with Some t -> t | None -> "") + doc["asOf"] <- Ticks.toLong hist.asOf + doc["status"] <- RequestAction.toString hist.status + doc["text"] <- match hist.text with Some t -> t | None -> "" upcast doc /// Map a BSON document to a history entry let historyFromBson (doc : BsonValue) = - { asOf = Ticks doc.["asOf"].AsInt64 - status = RequestAction.ofString doc.["status"].AsString - text = match doc.["text"].AsString with "" -> None | txt -> Some txt + { asOf = Ticks doc["asOf"].AsInt64 + status = RequestAction.ofString doc["status"].AsString + text = match doc["text"].AsString with "" -> None | txt -> Some txt } /// Map a note entry to BSON let noteToBson (note : Note) : BsonValue = let doc = BsonDocument () - doc.["asOf"] <- BsonValue (Ticks.toLong note.asOf) - doc.["notes"] <- BsonValue note.notes + doc["asOf"] <- Ticks.toLong note.asOf + doc["notes"] <- note.notes upcast doc /// Map a BSON document to a note entry let noteFromBson (doc : BsonValue) = - { asOf = Ticks doc.["asOf"].AsInt64 - notes = doc.["notes"].AsString + { asOf = Ticks doc["asOf"].AsInt64 + notes = doc["notes"].AsString } /// Map a request to its BSON representation let requestToBson req : BsonValue = let doc = BsonDocument () - doc.["_id"] <- BsonValue (RequestId.toString req.id) - doc.["enteredOn"] <- BsonValue (Ticks.toLong req.enteredOn) - doc.["userId"] <- BsonValue (UserId.toString req.userId) - doc.["snoozedUntil"] <- BsonValue (Ticks.toLong req.snoozedUntil) - doc.["showAfter"] <- BsonValue (Ticks.toLong req.showAfter) - doc.["recurType"] <- BsonValue (Recurrence.toString req.recurType) - doc.["recurCount"] <- BsonValue req.recurCount - doc.["history"] <- BsonArray (req.history |> List.map historyToBson |> Seq.ofList) - doc.["notes"] <- BsonArray (req.notes |> List.map noteToBson |> Seq.ofList) + doc["_id"] <- RequestId.toString req.id + doc["enteredOn"] <- Ticks.toLong req.enteredOn + doc["userId"] <- UserId.toString req.userId + doc["snoozedUntil"] <- Ticks.toLong req.snoozedUntil + doc["showAfter"] <- Ticks.toLong req.showAfter + doc["recurType"] <- Recurrence.toString req.recurType + doc["recurCount"] <- BsonValue req.recurCount + doc["history"] <- BsonArray (req.history |> List.map historyToBson |> Seq.ofList) + doc["notes"] <- BsonArray (req.notes |> List.map noteToBson |> Seq.ofList) upcast doc /// Map a BSON document to a request let requestFromBson (doc : BsonValue) = - { id = RequestId.ofString doc.["_id"].AsString - enteredOn = Ticks doc.["enteredOn"].AsInt64 - userId = UserId doc.["userId"].AsString - snoozedUntil = Ticks doc.["snoozedUntil"].AsInt64 - showAfter = Ticks doc.["showAfter"].AsInt64 - recurType = Recurrence.ofString doc.["recurType"].AsString - recurCount = int16 doc.["recurCount"].AsInt32 - history = doc.["history"].AsArray |> Seq.map historyFromBson |> List.ofSeq - notes = doc.["notes"].AsArray |> Seq.map noteFromBson |> List.ofSeq + { id = RequestId.ofString doc["_id"].AsString + enteredOn = Ticks doc["enteredOn"].AsInt64 + userId = UserId doc["userId"].AsString + snoozedUntil = Ticks doc["snoozedUntil"].AsInt64 + showAfter = Ticks doc["showAfter"].AsInt64 + recurType = Recurrence.ofString doc["recurType"].AsString + recurCount = int16 doc["recurCount"].AsInt32 + history = doc["history"].AsArray |> Seq.map historyFromBson |> List.ofSeq + notes = doc["notes"].AsArray |> Seq.map noteFromBson |> List.ofSeq } /// Set up the mapping @@ -117,7 +117,7 @@ module private Helpers = /// Retrieve a request, including its history and notes, by its ID and user ID let tryFullRequestById reqId userId (db : LiteDatabase) = task { - let! req = db.requests.Find (Query.EQ ("_id", RequestId.toString reqId |> BsonValue)) |> firstAsync + let! req = db.requests.Find (Query.EQ ("_id", RequestId.toString reqId)) |> firstAsync return match box req with null -> None | _ when req.userId = userId -> Some req | _ -> None } @@ -125,14 +125,14 @@ let tryFullRequestById reqId userId (db : LiteDatabase) = task { let addHistory reqId userId hist db = task { match! tryFullRequestById reqId userId db with | Some req -> do! doUpdate db { req with history = hist :: req.history } - | None -> invalidOp $"{RequestId.toString reqId} not found" + | None -> invalidOp $"{RequestId.toString reqId} not found" } /// Add a note let addNote reqId userId note db = task { match! tryFullRequestById reqId userId db with | Some req -> do! doUpdate db { req with notes = note :: req.notes } - | None -> invalidOp $"{RequestId.toString reqId} not found" + | None -> invalidOp $"{RequestId.toString reqId} not found" } /// Add a request @@ -141,7 +141,7 @@ let addRequest (req : Request) (db : LiteDatabase) = /// Retrieve all answered requests for the given user let answeredRequests userId (db : LiteDatabase) = task { - let! reqs = db.requests.Find (Query.EQ ("userId", UserId.toString userId |> BsonValue)) |> toListAsync + let! reqs = db.requests.Find (Query.EQ ("userId", UserId.toString userId)) |> toListAsync return reqs |> Seq.map JournalRequest.ofRequestFull @@ -152,7 +152,7 @@ let answeredRequests userId (db : LiteDatabase) = task { /// Retrieve the user's current journal let journalByUserId userId (db : LiteDatabase) = task { - let! jrnl = db.requests.Find (Query.EQ ("userId", UserId.toString userId |> BsonValue)) |> toListAsync + let! jrnl = db.requests.Find (Query.EQ ("userId", UserId.toString userId)) |> toListAsync return jrnl |> Seq.map JournalRequest.ofRequestLite @@ -163,9 +163,8 @@ let journalByUserId userId (db : LiteDatabase) = task { /// Retrieve a request by its ID and user ID (without notes and history) let tryRequestById reqId userId db = task { - match! tryFullRequestById reqId userId db with - | Some r -> return Some { r with history = []; notes = [] } - | _ -> return None + let! req = tryFullRequestById reqId userId db + return req |> Option.map (fun r -> { r with history = []; notes = [] }) } /// Retrieve notes for a request by its ID and user ID @@ -175,28 +174,27 @@ let notesById reqId userId (db : LiteDatabase) = task { /// Retrieve a journal request by its ID and user ID let tryJournalById reqId userId (db : LiteDatabase) = task { - match! tryFullRequestById reqId userId db with - | Some req -> return req |> (JournalRequest.ofRequestLite >> Some) - | None -> return None + let! req = tryFullRequestById reqId userId db + return req |> Option.map JournalRequest.ofRequestLite } /// Update the recurrence for a request let updateRecurrence reqId userId recurType recurCount db = task { match! tryFullRequestById reqId userId db with | Some req -> do! doUpdate db { req with recurType = recurType; recurCount = recurCount } - | None -> invalidOp $"{RequestId.toString reqId} not found" + | None -> invalidOp $"{RequestId.toString reqId} not found" } /// Update a snoozed request let updateSnoozed reqId userId until db = task { match! tryFullRequestById reqId userId db with | Some req -> do! doUpdate db { req with snoozedUntil = until; showAfter = until } - | None -> invalidOp $"{RequestId.toString reqId} not found" + | None -> invalidOp $"{RequestId.toString reqId} not found" } /// Update the "show after" timestamp for a request let updateShowAfter reqId userId showAfter db = task { match! tryFullRequestById reqId userId db with | Some req -> do! doUpdate db { req with showAfter = showAfter } - | None -> invalidOp $"{RequestId.toString reqId} not found" + | None -> invalidOp $"{RequestId.toString reqId} not found" } diff --git a/src/MyPrayerJournal/Server/Dates.fs b/src/MyPrayerJournal/Server/Dates.fs index 1198ce2..094bea9 100644 --- a/src/MyPrayerJournal/Server/Dates.fs +++ b/src/MyPrayerJournal/Server/Dates.fs @@ -4,19 +4,19 @@ module MyPrayerJournal.Dates type internal FormatDistanceToken = -| LessThanXMinutes -| XMinutes -| AboutXHours -| XHours -| XDays -| AboutXWeeks -| XWeeks -| AboutXMonths -| XMonths -| AboutXYears -| XYears -| OverXYears -| AlmostXYears + | LessThanXMinutes + | XMinutes + | AboutXHours + | XHours + | XDays + | AboutXWeeks + | XWeeks + | AboutXMonths + | XMonths + | AboutXYears + | XYears + | OverXYears + | AlmostXYears let internal locales = let format = PrintfFormat string, unit, string, string> @@ -38,10 +38,10 @@ let internal locales = ] ] -let aDay = 1_440. -let almostTwoDays = 2_520. -let aMonth = 43_200. -let twoMonths = 86_400. +let aDay = 1_440. +let almost2Days = 2_520. +let aMonth = 43_200. +let twoMonths = 86_400. open System @@ -51,7 +51,7 @@ let fromJs ticks = DateTime.UnixEpoch + TimeSpan.FromTicks (ticks * 10_000L) let formatDistance (startDate : DateTime) (endDate : DateTime) = let format (token, number) locale = let labels = locales |> Map.find locale - match number with 1 -> fst labels.[token] | _ -> sprintf (snd labels.[token]) number + match number with 1 -> fst labels[token] | _ -> sprintf (snd labels[token]) number let round (it : float) = Math.Round it |> int let diff = startDate - endDate @@ -60,18 +60,18 @@ let formatDistance (startDate : DateTime) (endDate : DateTime) = let months = minutes / aMonth |> round let years = months / 12 match true with - | _ when minutes < 1. -> LessThanXMinutes, 1 - | _ when minutes < 45. -> XMinutes, round minutes - | _ when minutes < 90. -> AboutXHours, 1 - | _ when minutes < aDay -> AboutXHours, round (minutes / 60.) - | _ when minutes < almostTwoDays -> XDays, 1 - | _ when minutes < aMonth -> XDays, round (minutes / aDay) - | _ when minutes < twoMonths -> AboutXMonths, round (minutes / aMonth) - | _ when months < 12 -> XMonths, round (minutes / aMonth) - | _ when months % 12 < 3 -> AboutXYears, years - | _ when months % 12 < 9 -> OverXYears, years - | _ -> AlmostXYears, years + 1 + | _ when minutes < 1. -> LessThanXMinutes, 1 + | _ when minutes < 45. -> XMinutes, round minutes + | _ when minutes < 90. -> AboutXHours, 1 + | _ when minutes < aDay -> AboutXHours, round (minutes / 60.) + | _ when minutes < almost2Days -> XDays, 1 + | _ when minutes < aMonth -> XDays, round (minutes / aDay) + | _ when minutes < twoMonths -> AboutXMonths, round (minutes / aMonth) + | _ when months < 12 -> XMonths, round (minutes / aMonth) + | _ when months % 12 < 3 -> AboutXYears, years + | _ when months % 12 < 9 -> OverXYears, years + | _ -> AlmostXYears, years + 1 - let words = format formatToken "en-US" - match startDate > endDate with true -> $"{words} ago" | false -> $"in {words}" + format formatToken "en-US" + |> match startDate > endDate with true -> sprintf "%s ago" | false -> sprintf "in %s" diff --git a/src/MyPrayerJournal/Server/Domain.fs b/src/MyPrayerJournal/Server/Domain.fs index 993ee17..5298f9b 100644 --- a/src/MyPrayerJournal/Server/Domain.fs +++ b/src/MyPrayerJournal/Server/Domain.fs @@ -180,7 +180,7 @@ module JournalRequest = |> List.tryHead |> Option.map (fun h -> Option.get h.text) |> Option.defaultValue "" - asOf = match hist with Some h -> h.asOf | None -> Ticks 0L + asOf = match hist with Some h -> h.asOf | None -> Ticks 0L lastStatus = match hist with Some h -> h.status | None -> Created snoozedUntil = req.snoozedUntil showAfter = req.showAfter diff --git a/src/MyPrayerJournal/Server/Handlers.fs b/src/MyPrayerJournal/Server/Handlers.fs index abcf7cd..d0019ec 100644 --- a/src/MyPrayerJournal/Server/Handlers.fs +++ b/src/MyPrayerJournal/Server/Handlers.fs @@ -93,7 +93,7 @@ module private Helpers = /// Return a 303 SEE OTHER response (forces a GET on the redirected URL) let seeOther (url : string) = - setStatusCode 303 >=> setHttpHeader "Location" url + noResponseCaching >=> setStatusCode 303 >=> setHttpHeader "Location" url /// The "now" time in JavaScript as Ticks let jsNow () = @@ -101,7 +101,8 @@ module private Helpers = /// Render a component result let renderComponent nodes : HttpHandler = - fun next ctx -> task { + noResponseCaching + >=> fun next ctx -> task { return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes) } @@ -143,8 +144,8 @@ module private Helpers = msg |> Option.iter (fun _ -> messages <- messages.Remove userId) msg) - /// Send a partial result if this is not a full page load - let partialIfNotRefresh (pageTitle : string) content : HttpHandler = + /// Send a partial result if this is not a full page load (does not append no-cache headers) + let partialStatic (pageTitle : string) content : HttpHandler = fun next ctx -> let isPartial = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh let view = @@ -157,7 +158,10 @@ module private Helpers = | Some (msg, url) -> setHttpHeader "X-Toast" msg >=> withHxPush url >=> writeView view | None -> writeView view | None -> writeView view - + + /// Send an explicitly non-cached result, rendering as a partial if this is not a full page load + let partial pageTitle content = + noResponseCaching >=> partialStatic pageTitle content /// Add a success message header to the response let withSuccessMessage : string -> HttpHandler = @@ -222,9 +226,14 @@ module Components = >=> fun next ctx -> task { match! Data.tryJournalById (RequestId.ofString reqId) (userId ctx) (db ctx) with | Some req -> return! renderComponent [ Views.Request.reqListItem req ] next ctx - | None -> return! Error.notFound next ctx + | None -> return! Error.notFound next ctx } + /// GET /components/request/[req-id]/add-notes + let addNotes requestId : HttpHandler = + requiresAuthentication Error.notAuthorized + >=> renderComponent (Views.Journal.notesEdit (RequestId.ofString requestId)) + /// GET /components/request/[req-id]/notes let notes requestId : HttpHandler = requiresAuthentication Error.notAuthorized @@ -239,7 +248,7 @@ module Home = // GET / let home : HttpHandler = - partialIfNotRefresh "Welcome!" Views.Home.home + partialStatic "Welcome!" Views.Home.home /// /journal URL @@ -255,7 +264,7 @@ module Journal = |> Option.map (fun c -> c.Value) |> Option.defaultValue "Your" let title = usr |> match usr with "Your" -> sprintf "%s" | _ -> sprintf "%s's" - return! partialIfNotRefresh (sprintf "%s Prayer Journal" title) (Views.Journal.journal usr) next ctx + return! partial (sprintf "%s Prayer Journal" title) (Views.Journal.journal usr) next ctx } @@ -264,15 +273,11 @@ module Legal = // GET /legal/privacy-policy let privacyPolicy : HttpHandler = - partialIfNotRefresh "Privacy Policy" Views.Legal.privacyPolicy + partialStatic "Privacy Policy" Views.Legal.privacyPolicy // GET /legal/terms-of-service let termsOfService : HttpHandler = - partialIfNotRefresh "Terms of Service" Views.Legal.termsOfService - - -/// Alias for the Ply task module (The F# "task" CE can't handle differing types well within the same CE) -module Ply = FSharp.Control.Tasks.Affine + partialStatic "Terms of Service" Views.Legal.termsOfService /// /api/request and /request(s) URLs @@ -289,13 +294,12 @@ module Request = | _ -> "journal" match requestId with | "new" -> - return! partialIfNotRefresh "Add Prayer Request" + return! partial "Add Prayer Request" (Views.Request.edit (JournalRequest.ofRequestLite Request.empty) returnTo true) next ctx - | _ -> + | _ -> match! Data.tryJournalById (RequestId.ofString requestId) (userId ctx) (db ctx) with - | Some req -> - return! partialIfNotRefresh "Edit Prayer Request" (Views.Request.edit req returnTo false) next ctx - | None -> return! Error.notFound next ctx + | Some req -> return! partial "Edit Prayer Request" (Views.Request.edit req returnTo false) next ctx + | None -> return! Error.notFound next ctx } // PATCH /request/[req-id]/prayed @@ -311,7 +315,7 @@ module Request = do! Data.addHistory reqId usrId { asOf = now; status = Prayed; text = None } db let nextShow = match Recurrence.duration req.recurType with - | 0L -> 0L + | 0L -> 0L | duration -> (Ticks.toLong now) + (duration * int64 req.recurCount) do! Data.updateShowAfter reqId usrId (Ticks nextShow) db do! db.saveChanges () @@ -340,7 +344,7 @@ module Request = requiresAuthentication Error.notAuthorized >=> fun next ctx -> task { let! reqs = Data.journalByUserId (userId ctx) (db ctx) - return! partialIfNotRefresh "Active Requests" (Views.Request.active reqs) next ctx + return! partial "Active Requests" (Views.Request.active reqs) next ctx } // GET /requests/snoozed @@ -350,7 +354,7 @@ module Request = let! reqs = Data.journalByUserId (userId ctx) (db ctx) let now = (jsNow >> Ticks.toLong) () let snoozed = reqs |> List.filter (fun r -> Ticks.toLong r.snoozedUntil > now) - return! partialIfNotRefresh "Active Requests" (Views.Request.snoozed snoozed) next ctx + return! partial "Active Requests" (Views.Request.snoozed snoozed) next ctx } // GET /requests/answered @@ -358,7 +362,7 @@ module Request = requiresAuthentication Error.notAuthorized >=> fun next ctx -> task { let! reqs = Data.answeredRequests (userId ctx) (db ctx) - return! partialIfNotRefresh "Answered Requests" (Views.Request.answered reqs) next ctx + return! partial "Answered Requests" (Views.Request.answered reqs) next ctx } /// GET /api/request/[req-id] @@ -375,8 +379,8 @@ module Request = requiresAuthentication Error.notAuthorized >=> fun next ctx -> task { match! Data.tryFullRequestById (RequestId.ofString requestId) (userId ctx) (db ctx) with - | Some req -> return! partialIfNotRefresh "Prayer Request" (Views.Request.full req) next ctx - | None -> return! Error.notFound next ctx + | Some req -> return! partial "Prayer Request" (Views.Request.full req) next ctx + | None -> return! Error.notFound next ctx } // PATCH /request/[req-id]/show @@ -462,7 +466,7 @@ module Request = // PATCH /request let update : HttpHandler = requiresAuthentication Error.notAuthorized - >=> fun next ctx -> Ply.task { + >=> fun next ctx -> task { let! form = ctx.BindModelAsync () let db = db ctx let usrId = userId ctx @@ -520,9 +524,10 @@ let routes = [ GET_HEAD [ route "/" Home.home ] subRoute "/components/" [ GET_HEAD [ - route "journal-items" Components.journalItems - routef "request/%s/item" Components.requestItem - routef "request/%s/notes" Components.notes + route "journal-items" Components.journalItems + routef "request/%s/add-notes" Components.addNotes + routef "request/%s/item" Components.requestItem + routef "request/%s/notes" Components.notes ] ] GET_HEAD [ route "/journal" Journal.journal ] diff --git a/src/MyPrayerJournal/Server/Program.fs b/src/MyPrayerJournal/Server/Program.fs index 3db9d1c..04e74f3 100644 --- a/src/MyPrayerJournal/Server/Program.fs +++ b/src/MyPrayerJournal/Server/Program.fs @@ -92,9 +92,9 @@ module Configure = /// Configure OIDC with Auth0 options from configuration fun opts -> let cfg = bldr.Configuration.GetSection "Auth0" - opts.Authority <- sprintf "https://%s/" cfg.["Domain"] - opts.ClientId <- cfg.["Id"] - opts.ClientSecret <- cfg.["Secret"] + opts.Authority <- sprintf "https://%s/" cfg["Domain"] + opts.ClientId <- cfg["Id"] + opts.ClientSecret <- cfg["Secret"] opts.ResponseType <- OpenIdConnectResponseType.Code opts.Scope.Clear () @@ -119,7 +119,7 @@ module Configure = sprintf "%s://%s%s%s" request.Scheme request.Host.Value request.PathBase.Value redirUri | false -> redirUri Uri.EscapeDataString finalRedirUri |> sprintf "&returnTo=%s" - sprintf "https://%s/v2/logout?client_id=%s%s" cfg.["Domain"] cfg.["Id"] returnTo + sprintf "https://%s/v2/logout?client_id=%s%s" cfg["Domain"] cfg["Id"] returnTo |> ctx.Response.Redirect ctx.HandleResponse () @@ -132,7 +132,7 @@ module Configure = Data.Startup.ensureDb db bldr.Services.AddSingleton(jsonOptions) .AddSingleton() - .AddSingleton(db) + .AddSingleton db |> ignore bldr.Build () @@ -155,7 +155,8 @@ module Configure = .UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.routes // TODO: fallback to 404 - e.MapFallbackToFile "index.html" |> ignore) + // e.MapFallbackToFile "index.html" + |> ignore) |> ignore app diff --git a/src/MyPrayerJournal/Server/Views.fs b/src/MyPrayerJournal/Server/Views.fs index 9aacdb4..fb30b8e 100644 --- a/src/MyPrayerJournal/Server/Views.fs +++ b/src/MyPrayerJournal/Server/Views.fs @@ -54,7 +54,7 @@ module private Helpers = span [ _title (date.ToString "f") ] [ Dates.formatDistance DateTime.UtcNow date |> str ] -/// Views for home and log on pages +/// View for home page module Home = /// The home page @@ -71,13 +71,6 @@ module Home = rawText "learn more about the site at the “Docs” link, also above." ] ] - - /// The log on page - let logOn = article [ _class "container mt-3" ] [ - p [] [ - em [] [ str "Verifying..." ] - ] - ] /// Views for legal pages @@ -277,12 +270,14 @@ module Journal = pageLink $"/request/{reqId}/edit" [ _class "btn btn-secondary"; _title "Edit Request" ] [ icon "edit" ] spacer button [ - _type "button" - _class "btn btn-secondary" - _title "Add Notes" - _data "bs-toggle" "modal" - _data "bs-target" "#notesModal" - _data "request-id" reqId + _type "button" + _class "btn btn-secondary" + _title "Add Notes" + _data "bs-toggle" "modal" + _data "bs-target" "#notesModal" + _hxGet $"/components/request/{reqId}/add-notes" + _hxTarget "#notesBody" + _hxSwap HxSwap.InnerHtml ] [ icon "comment" ] spacer // md-button(@click.stop='snooze()').md-icon-button.md-raised @@ -336,27 +331,13 @@ module Journal = h5 [ _class "modal-title"; _id "nodesModalLabel" ] [ str "Add Notes to Prayer Request" ] button [ _type "button"; _class "btn-close"; _data "bs-dismiss" "modal"; _ariaLabel "Close" ] [] ] - div [ _class "modal-body" ] [ - form [ _id "notesForm"; _method "POST"; _action ""; _hxBoost; _hxTarget "#top" ] [ - str "TODO" - button [ _type "submit"; _class "btn btn-primary" ] [ str "Add Notes" ] - ] - hr [] - div [ - _id "notesLoad" - _class "btn btn-secondary" - _hxGet "" - _hxSwap HxSwap.OuterHtml - _hxTarget "this" - ] [ str "Load Prior Notes" ] - ] + div [ _class "modal-body"; _id "notesBody" ] [ ] div [ _class "modal-footer" ] [ button [ _type "button"; _class "btn btn-secondary"; _data "bs-dismiss" "modal" ] [ str "Close" ] ] ] ] ] - script [] [ str "setTimeout(function () { mpj.journal.setUp() }, 1000)" ] ] /// The journal items @@ -376,6 +357,25 @@ module Journal = _hxSwap HxSwap.OuterHtml ] + /// The notes edit modal body + let notesEdit requestId = + let reqId = RequestId.toString requestId + [ form [ _hxPost $"/request/{reqId}/note"; _hxTarget "#top" ] [ + str "TODO" + button [ _type "submit"; _class "btn btn-primary" ] [ str "Add Notes" ] + ] + div [ _id "priorNotes" ] [ + p [ _class "text-center pt-5" ] [ + button [ + _type "button" + _class "btn btn-secondary" + _hxGet $"/components/request/{reqId}/notes" + _hxSwap HxSwap.OuterHtml + _hxTarget "#priorNotes" + ] [str "Load Prior Notes" ] + ] + ] + ] /// Views for request pages and components @@ -396,10 +396,9 @@ module Request = ] [ pageLink $"/request/{reqId}/full" [ btnClass; _title "View Full Request" ] [ icon "description" ] match isAnswered with - | true -> () - | false -> - button [ btnClass; _hxGet $"/components/request/{reqId}/edit"; _title "Edit Request" ] [ icon "edit" ] - match () with + | true -> () + | false -> button [ btnClass; _hxGet $"/components/request/{reqId}/edit"; _title "Edit Request" ] [ icon "edit" ] + match true with | _ when isSnoozed -> button [ btnClass; _hxPatch $"/request/{reqId}/cancel-snooze"; _title "Cancel Snooze" ] [ icon "restore" ] | _ when isPending -> @@ -522,7 +521,7 @@ module Request = | "snoozed" -> "/requests/snoozed" | _ (* "journal" *) -> "/journal" article [ _class "container" ] [ - h5 [ _class "pb-3" ] [ (match isNew with true -> "Add" | false -> "Edit") |> strf "%s Prayer Request" ] + h2 [ _class "pb-3" ] [ (match isNew with true -> "Add" | false -> "Edit") |> strf "%s Prayer Request" ] form [ _hxBoost _hxTarget "#top" @@ -639,7 +638,8 @@ module Request = /// Display a list of notes for a request let notes notes = let toItem (note : Note) = p [] [ small [ _class "text-muted" ] [ relativeDate note.asOf ]; br []; str note.notes ] - [ p [ _class "text-center" ] [ strong [] [ str "Prior Notes for This Request" ] ] + [ hr [ _style "margin: .5rem -1rem" ] + p [ _class "text-center" ] [ strong [] [ str "Prior Notes for This Request" ] ] match notes with | [] -> p [ _class "text-center text-muted" ] [ str "There are no prior notes for this request" ] | _ -> yield! notes |> List.map toItem @@ -696,12 +696,20 @@ module Layout = _integrity "sha384-oGA+prIp5Vchu6we2YkI51UtVzN9Jpx2Z7PnR1I78PnZlN8LkrCT4lqqqmDkyrvI" _crossorigin "anonymous" ] [] + script [] [ + rawText "if (!htmx) document.write('