From 3d098bea8480dd2d9a348e8dfd31b1ef9d88a7b8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 22 Oct 2021 23:30:15 -0400 Subject: [PATCH] Snooze works --- src/MyPrayerJournal/Server/Data.fs | 8 +- src/MyPrayerJournal/Server/Handlers.fs | 89 ++++++++++++--------- src/MyPrayerJournal/Server/Views/Journal.fs | 52 +++++++++++- 3 files changed, 106 insertions(+), 43 deletions(-) diff --git a/src/MyPrayerJournal/Server/Data.fs b/src/MyPrayerJournal/Server/Data.fs index f4b6138..0204131 100644 --- a/src/MyPrayerJournal/Server/Data.fs +++ b/src/MyPrayerJournal/Server/Data.fs @@ -149,7 +149,7 @@ let answeredRequests userId (db : LiteDatabase) = backgroundTask { |> Seq.sortByDescending (fun it -> Ticks.toLong it.asOf) |> List.ofSeq } - + /// Retrieve the user's current journal let journalByUserId userId (db : LiteDatabase) = backgroundTask { let! jrnl = db.requests.Find (Query.EQ ("userId", UserId.toString userId)) |> toListAsync @@ -161,6 +161,12 @@ let journalByUserId userId (db : LiteDatabase) = backgroundTask { |> List.ofSeq } +/// Does the user have any snoozed requests? +let hasSnoozed userId now (db : LiteDatabase) = backgroundTask { + let! jrnl = journalByUserId userId db + return jrnl |> List.exists (fun r -> Ticks.toLong r.snoozedUntil > Ticks.toLong now) + } + /// Retrieve a request by its ID and user ID (without notes and history) let tryRequestById reqId userId db = backgroundTask { let! req = tryFullRequestById reqId userId db diff --git a/src/MyPrayerJournal/Server/Handlers.fs b/src/MyPrayerJournal/Server/Handlers.fs index f53d1aa..379b444 100644 --- a/src/MyPrayerJournal/Server/Handlers.fs +++ b/src/MyPrayerJournal/Server/Handlers.fs @@ -95,9 +95,13 @@ module private Helpers = let seeOther (url : string) = noResponseCaching >=> setStatusCode 303 >=> setHttpHeader "Location" url + /// Convert a date/time to JS-style ticks + let toJs (date : DateTime) = + date.Subtract(DateTime (1970, 1, 1, 0, 0, 0)).TotalSeconds |> (int64 >> ( * ) 1_000L >> Ticks) + /// The "now" time in JavaScript as Ticks let jsNow () = - DateTime.UtcNow.Subtract(DateTime (1970, 1, 1, 0, 0, 0)).TotalSeconds |> (int64 >> ( * ) 1_000L >> Ticks) + toJs DateTime.UtcNow /// Render a component result let renderComponent nodes : HttpHandler = @@ -106,13 +110,22 @@ module private Helpers = return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes) } + open Views.Layout + /// Create a page rendering context - let pageContext (ctx : HttpContext) pageTitle content : Views.Layout.PageRenderContext = - { isAuthenticated = (user >> Option.isSome) ctx - hasSnoozed = false + let pageContext (ctx : HttpContext) pageTitle content = backgroundTask { + let! hasSnoozed = backgroundTask { + match user ctx with + | Some _ -> return! Data.hasSnoozed (userId ctx) (jsNow ()) (db ctx) + | None -> return false + } + return { + isAuthenticated = (user >> Option.isSome) ctx + hasSnoozed = hasSnoozed currentUrl = ctx.Request.Path.Value pageTitle = pageTitle content = content + } } /// Composable handler to write a view to the output @@ -146,18 +159,19 @@ module private Helpers = /// 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 = - pageContext ctx pageTitle content - |> match isPartial with true -> Views.Layout.partial | false -> Views.Layout.view - (next, ctx) - ||> match user ctx with - | Some u -> - match Messages.pop u with - | Some (msg, url) -> setHttpHeader "X-Toast" msg >=> withHxPush url >=> writeView view - | None -> writeView view - | None -> writeView view + fun next ctx -> backgroundTask { + let isPartial = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh + let! pageCtx = pageContext ctx pageTitle content + let view = (match isPartial with true -> partial | false -> view) pageCtx + return! + (next, ctx) + ||> match user ctx with + | Some u -> + match Messages.pop u with + | 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 = @@ -201,11 +215,11 @@ module Models = recurInterval : string option } - /// The time until which a request should not appear in the journal + /// The date until which a request should not appear in the journal [] type SnoozeUntil = { - /// The time at which the request should reappear - until : int64 + /// The date (YYYY-MM-DD) at which the request should reappear + until : string } @@ -233,18 +247,23 @@ module Components = | None -> return! Error.notFound next ctx } - /// GET /components/request/[req-id]/add-notes + // 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 + // GET /components/request/[req-id]/notes let notes requestId : HttpHandler = requiresAuthentication Error.notAuthorized >=> fun next ctx -> backgroundTask { let! notes = Data.notesById (RequestId.ofString requestId) (userId ctx) (db ctx) return! renderComponent (Views.Request.notes notes) next ctx } + + // GET /components/request/[req-id]/snooze + let snooze requestId : HttpHandler = + requiresAuthentication Error.notAuthorized + >=> renderComponent [ RequestId.ofString requestId |> Views.Journal.snooze ] /// / URL @@ -369,7 +388,7 @@ module Request = return! partial "Answered Requests" (Views.Request.answered reqs) next ctx } - /// GET /api/request/[req-id] + // GET /api/request/[req-id] let get requestId : HttpHandler = requiresAuthentication Error.notAuthorized >=> fun next ctx -> backgroundTask { @@ -402,7 +421,7 @@ module Request = | None -> return! Error.notFound next ctx } - /// PATCH /api/request/[req-id]/snooze + // PATCH /request/[req-id]/snooze let snooze requestId : HttpHandler = requiresAuthentication Error.notAuthorized >=> fun next ctx -> backgroundTask { @@ -411,10 +430,14 @@ module Request = let reqId = RequestId.ofString requestId match! Data.tryRequestById reqId usrId db with | Some _ -> - let! until = ctx.BindJsonAsync () - do! Data.updateSnoozed reqId usrId (Ticks until.until) db + let! until = ctx.BindFormAsync () + let date = sprintf "%s 00:00:00" until.until |> DateTime.Parse + do! Data.updateSnoozed reqId usrId (toJs date) db do! db.saveChanges () - return! setStatusCode 204 next ctx + return! + (withSuccessMessage $"Request snoozed until {until.until}" + >=> hideModal "snooze" + >=> Components.journalItems) next ctx | None -> return! Error.notFound next ctx } @@ -532,6 +555,7 @@ let routes = routef "request/%s/add-notes" Components.addNotes routef "request/%s/item" Components.requestItem routef "request/%s/notes" Components.notes + routef "request/%s/snooze" Components.snooze ] ] GET_HEAD [ route "/journal" Journal.journal ] @@ -554,6 +578,7 @@ let routes = routef "/%s/cancel-snooze" Request.cancelSnooze routef "/%s/prayed" Request.prayed routef "/%s/show" Request.show + routef "/%s/snooze" Request.snooze ] POST [ route "" Request.add @@ -566,16 +591,4 @@ let routes = route "log-on" User.logOn ] ] - subRoute "/api/" [ - GET [ - subRoute "request" [ - routef "/%s" Request.get - ] - ] - PATCH [ - subRoute "request" [ - routef "/%s/snooze" Request.snooze - ] - ] - ] ] diff --git a/src/MyPrayerJournal/Server/Views/Journal.fs b/src/MyPrayerJournal/Server/Views/Journal.fs index 807ecb9..8bdb4e0 100644 --- a/src/MyPrayerJournal/Server/Views/Journal.fs +++ b/src/MyPrayerJournal/Server/Views/Journal.fs @@ -26,10 +26,16 @@ let journalCard req = _hxSwap HxSwap.InnerHtml ] [ icon "comment" ] spacer - // md-button(@click.stop='snooze()').md-icon-button.md-raised - // md-icon schedule - // md-tooltip(md-direction='top' - // md-delay=1000) Snooze Request + button [ + _type "button" + _class "btn btn-secondary" + _title "Snooze Request" + _data "bs-toggle" "modal" + _data "bs-target" "#snoozeModal" + _hxGet $"/components/request/{reqId}/snooze" + _hxTarget "#snoozeBody" + _hxSwap HxSwap.InnerHtml + ] [ icon "schedule" ] div [ _class "flex-grow-1" ] [] button [ _type "button" @@ -82,6 +88,28 @@ let journal user = article [ _class "container-fluid mt-3" ] [ ] ] ] + div [ + _id "snoozeModal" + _class "modal fade" + _tabindex "-1" + _ariaLabelledBy "snoozeModalLabel" + _ariaHidden "true" + ] [ + div [ _class "modal-dialog modal-sm" ] [ + div [ _class "modal-content" ] [ + div [ _class "modal-header" ] [ + h5 [ _class "modal-title"; _id "snoozeModalLabel" ] [ str "Snooze Prayer Request" ] + button [ _type "button"; _class "btn-close"; _data "bs-dismiss" "modal"; _ariaLabel "Close" ] [] + ] + div [ _class "modal-body"; _id "snoozeBody" ] [ ] + div [ _class "modal-footer" ] [ + button [ _type "button"; _id "snoozeDismiss"; _class "btn btn-secondary"; _data "bs-dismiss" "modal" ] [ + str "Close" + ] + ] + ] + ] + ] ] /// The journal items @@ -96,6 +124,7 @@ let journalItems items = items |> List.map journalCard |> section [ + _id "journalItems" _class "row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-3" _hxTarget "this" _hxSwap HxSwap.OuterHtml @@ -131,3 +160,18 @@ let notesEdit requestId = ] ] ] + +/// The snooze edit form +let snooze requestId = + let today = System.DateTime.Today.ToString "yyyy-MM-dd" + form [ + _hxPatch $"/request/{RequestId.toString requestId}/snooze" + _hxTarget "#journalItems" + _hxSwap HxSwap.OuterHtml + ] [ + div [ _class "form-floating pb-3" ] [ + input [ _type "date"; _id "until"; _name "until"; _class "form-control"; _min today ] + label [ _for "until" ] [ str "Until" ] + ] + p [ _class "text-end mb-0" ] [ button [ _type "submit"; _class "btn btn-primary" ] [ str "Snooze" ] ] + ]