Version 3.1 #71

Merged
danieljsummers merged 9 commits from v3.1 into main 2022-07-30 21:02:58 +00:00
8 changed files with 69 additions and 39 deletions
Showing only changes of commit 924fbdebe5 - Show all commits

View File

@ -46,16 +46,14 @@ let twoMonths = 86_400.
open System
/// Convert from a JavaScript "ticks" value to a date/time
let fromJs ticks = DateTime.UnixEpoch + TimeSpan.FromTicks (ticks * 10_000L)
let formatDistance (startDate : Instant) (endDate : Instant) =
/// Format the distance between two instants in approximate English terms
let formatDistance (startOn : Instant) (endOn : Instant) =
let format (token, number) locale =
let labels = locales |> Map.find locale
match number with 1 -> fst labels[token] | _ -> sprintf (snd labels[token]) number
let round (it : float) = Math.Round it |> int
let diff = startDate - endDate
let diff = startOn - endOn
let minutes = Math.Abs diff.TotalMinutes
let formatToken =
let months = minutes / aMonth |> round
@ -74,5 +72,5 @@ let formatDistance (startDate : Instant) (endDate : Instant) =
| _ -> AlmostXYears, years + 1
format formatToken "en-US"
|> match startDate > endDate with true -> sprintf "%s ago" | false -> sprintf "in %s"
|> match startOn > endOn with true -> sprintf "%s ago" | false -> sprintf "in %s"

View File

@ -72,6 +72,15 @@ type HttpContext with
/// Get the current instant from the system clock
member this.Now = this.Clock.GetCurrentInstant
/// Get the time zone from the X-Time-Zone header (default UTC)
member this.TimeZone =
match this.TryGetRequestHeader "X-Time-Zone" with
| Some tz ->
match this.GetService<IDateTimeZoneProvider>().GetZoneOrNull tz with
| null -> DateTimeZone.Utc
| zone -> zone
| None -> DateTimeZone.Utc
/// Handler helpers
@ -247,13 +256,13 @@ module Components =
| _, _ -> false
let! journal = Data.journalByUserId ctx.UserId ctx.Db
let shown = journal |> List.filter shouldBeShown
return! renderComponent [ Views.Journal.journalItems now shown ] next ctx
return! renderComponent [ Views.Journal.journalItems now ctx.TimeZone shown ] next ctx
}
// GET /components/request-item/[req-id]
let requestItem reqId : HttpHandler = requireUser >=> fun next ctx -> task {
match! Data.tryJournalById (RequestId.ofString reqId) ctx.UserId ctx.Db with
| Some req -> return! renderComponent [ Views.Request.reqListItem (ctx.Now ()) req ] next ctx
| Some req -> return! renderComponent [ Views.Request.reqListItem (ctx.Now ()) ctx.TimeZone req ] next ctx
| None -> return! Error.notFound next ctx
}
@ -264,7 +273,7 @@ module Components =
// GET /components/request/[req-id]/notes
let notes requestId : HttpHandler = requireUser >=> fun next ctx -> task {
let! notes = Data.notesById (RequestId.ofString requestId) ctx.UserId ctx.Db
return! renderComponent (Views.Request.notes (ctx.Now ()) (List.ofArray notes)) next ctx
return! renderComponent (Views.Request.notes (ctx.Now ()) ctx.TimeZone (List.ofArray notes)) next ctx
}
// GET /components/request/[req-id]/snooze
@ -367,7 +376,7 @@ module Request =
// GET /requests/active
let active : HttpHandler = requireUser >=> fun next ctx -> task {
let! reqs = Data.journalByUserId ctx.UserId ctx.Db
return! partial "Active Requests" (Views.Request.active (ctx.Now ()) reqs) next ctx
return! partial "Active Requests" (Views.Request.active (ctx.Now ()) ctx.TimeZone reqs) next ctx
}
// GET /requests/snoozed
@ -376,19 +385,19 @@ module Request =
let now = ctx.Now ()
let snoozed = reqs
|> List.filter (fun it -> defaultArg (it.SnoozedUntil |> Option.map (fun it -> it > now)) false)
return! partial "Snoozed Requests" (Views.Request.snoozed now snoozed) next ctx
return! partial "Snoozed Requests" (Views.Request.snoozed now ctx.TimeZone snoozed) next ctx
}
// GET /requests/answered
let answered : HttpHandler = requireUser >=> fun next ctx -> task {
let! reqs = Data.answeredRequests ctx.UserId ctx.Db
return! partial "Answered Requests" (Views.Request.answered (ctx.Now ()) reqs) next ctx
return! partial "Answered Requests" (Views.Request.answered (ctx.Now ()) ctx.TimeZone reqs) next ctx
}
// GET /request/[req-id]/full
let getFull requestId : HttpHandler = requireUser >=> fun next ctx -> task {
match! Data.tryFullRequestById (RequestId.ofString requestId) ctx.UserId ctx.Db with
| Some req -> return! partial "Prayer Request" (Views.Request.full ctx.Clock req) next ctx
| Some req -> return! partial "Prayer Request" (Views.Request.full ctx.Clock ctx.TimeZone req) next ctx
| None -> return! Error.notFound next ctx
}

View File

@ -24,7 +24,7 @@
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.6.1" />
<PackageReference Include="LiteDB" Version="5.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.10" />
<PackageReference Include="NodaTime" Version="3.0.9" />
<PackageReference Include="NodaTime" Version="3.1.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />

View File

@ -74,7 +74,8 @@ module Configure =
let _ = bldr.Services.AddRouting ()
let _ = bldr.Services.AddGiraffe ()
let _ = bldr.Services.AddSingleton<IClock>(SystemClock.Instance)
let _ = bldr.Services.AddSingleton<IClock> SystemClock.Instance
let _ = bldr.Services.AddSingleton<IDateTimeZoneProvider> DateTimeZoneProviders.Tzdb
let _ =
bldr.Services.Configure<CookiePolicyOptions>(fun (opts : CookiePolicyOptions) ->

View File

@ -27,5 +27,5 @@ let noResults heading link buttonText text =
]
/// Create a date with a span tag, displaying the relative date with the full date/time in the tooltip
let relativeDate (date : Instant) now =
span [ _title (date.ToDateTimeOffset().ToString ("f", null)) ] [ Dates.formatDistance now date |> str ]
let relativeDate (date : Instant) now (tz : DateTimeZone) =
span [ _title (date.InZone(tz).ToDateTimeOffset().ToString ("f", null)) ] [ Dates.formatDistance now date |> str ]

View File

@ -7,7 +7,7 @@ open Giraffe.ViewEngine.Htmx
open MyPrayerJournal
/// Display a card for this prayer request
let journalCard now req =
let journalCard now tz req =
let reqId = RequestId.toString req.RequestId
let spacer = span [] [ rawText "&nbsp;" ]
div [ _class "col" ] [
@ -50,8 +50,8 @@ let journalCard now req =
div [ _class "card-footer text-end text-muted px-1 py-0" ] [
em [] [
match req.LastPrayed with
| Some dt -> str "last prayed "; relativeDate dt now
| None -> str "last activity "; relativeDate req.AsOf now
| Some dt -> str "last prayed "; relativeDate dt now tz
| None -> str "last activity "; relativeDate req.AsOf now tz
]
]
]
@ -120,7 +120,7 @@ let journal user =
]
/// The journal items
let journalItems now items =
let journalItems now tz items =
match items |> List.isEmpty with
| true ->
noResults "No Active Requests" "/request/new/edit" "Add a Request" [
@ -129,7 +129,7 @@ let journalItems now items =
]
| false ->
items
|> List.map (journalCard now)
|> List.map (journalCard now tz)
|> section [ _id "journalItems"
_class "row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-3"
_hxTarget "this"

View File

@ -7,7 +7,7 @@ open MyPrayerJournal
open NodaTime
/// Create a request within the list
let reqListItem now req =
let reqListItem now tz req =
let isFuture instant = defaultArg (instant |> Option.map (fun it -> it > now)) false
let reqId = RequestId.toString req.RequestId
let isAnswered = req.LastStatus = Answered
@ -28,32 +28,32 @@ let reqListItem now req =
if isSnoozed || isPending || isAnswered then
br []
small [ _class "text-muted" ] [
if isSnoozed then [ str "Snooze expires "; relativeDate req.SnoozedUntil.Value now ]
elif isPending then [ str "Request appears next "; relativeDate req.ShowAfter.Value now ]
else (* isAnswered *) [ str "Answered "; relativeDate req.AsOf now ]
if isSnoozed then [ str "Snooze expires "; relativeDate req.SnoozedUntil.Value now tz ]
elif isPending then [ str "Request appears next "; relativeDate req.ShowAfter.Value now tz ]
else (* isAnswered *) [ str "Answered "; relativeDate req.AsOf now tz ]
|> em []
]
]
]
/// Create a list of requests
let reqList now reqs =
let reqList now tz reqs =
reqs
|> List.map (reqListItem now)
|> List.map (reqListItem now tz)
|> div [ _class "list-group" ]
/// View for Active Requests page
let active now reqs =
let active now tz reqs =
article [ _class "container mt-3" ] [
h2 [ _class "pb-3" ] [ str "Active Requests" ]
if List.isEmpty reqs then
noResults "No Active Requests" "/journal" "Return to your journal"
[ str "Your prayer journal has no active requests" ]
else reqList now reqs
else reqList now tz reqs
]
/// View for Answered Requests page
let answered now reqs =
let answered now tz reqs =
article [ _class "container mt-3" ] [
h2 [ _class "pb-3" ] [ str "Answered Requests" ]
if List.isEmpty reqs then
@ -61,18 +61,18 @@ let answered now reqs =
str "Your prayer journal has no answered requests; once you have marked one as "
rawText "&ldquo;Answered&rdquo;, it will appear here"
]
else reqList now reqs
else reqList now tz reqs
]
/// View for Snoozed Requests page
let snoozed now reqs =
let snoozed now tz reqs =
article [ _class "container mt-3" ] [
h2 [ _class "pb-3" ] [ str "Snoozed Requests" ]
reqList now reqs
reqList now tz reqs
]
/// View for Full Request page
let full (clock : IClock) (req : Request) =
let full (clock : IClock) tz (req : Request) =
let now = clock.GetCurrentInstant ()
let answered =
req.History
@ -110,7 +110,7 @@ let full (clock : IClock) (req : Request) =
str "Answered "
date.ToDateTimeOffset().ToString ("D", null) |> str
str " ("
relativeDate date now
relativeDate date now tz
rawText ") &bull; "
| None -> ()
rawText $"Prayed %s{prayed} times &bull; Open %s{daysOpen} days"
@ -259,9 +259,9 @@ let edit (req : JournalRequest) returnTo isNew =
]
/// Display a list of notes for a request
let notes now notes =
let notes now tz notes =
let toItem (note : Note) =
p [] [ small [ _class "text-muted" ] [ relativeDate note.AsOf now ]; br []; str note.Notes ]
p [] [ small [ _class "text-muted" ] [ relativeDate note.AsOf now tz ]; br []; str note.Notes ]
[ 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" ]

View File

@ -1,7 +1,7 @@
"use strict"
/** myPrayerJournal script */
const mpj = {
this.mpj = {
/**
* Show a message via toast
* @param {string} message The message to show
@ -66,6 +66,19 @@ const mpj = {
const isDisabled = target.value === "Immediate"
;["recurCount", "recurInterval"].forEach(it => document.getElementById(it).disabled = isDisabled)
}
},
/**
* The time zone of the current browser
* @type {string}
**/
timeZone: undefined,
/**
* Derive the time zone from the current browser
*/
deriveTimeZone () {
try {
this.timeZone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone
} catch (_) { }
}
}
@ -80,3 +93,12 @@ htmx.on("htmx:afterOnLoad", function (evt) {
document.getElementById(evt.detail.xhr.getResponseHeader("x-hide-modal") + "Dismiss").click()
}
})
htmx.on("htmx:configRequest", function (evt) {
// Send the user's current time zone so that we can display local time
if (mpj.timeZone) {
evt.detail.headers["X-Time-Zone"] = mpj.timeZone
}
})
mpj.deriveTimeZone()