Display date/time in local time (#69)
This commit is contained in:
parent
e465d9af23
commit
924fbdebe5
@ -46,16 +46,14 @@ let twoMonths = 86_400.
|
|||||||
|
|
||||||
open System
|
open System
|
||||||
|
|
||||||
/// Convert from a JavaScript "ticks" value to a date/time
|
/// Format the distance between two instants in approximate English terms
|
||||||
let fromJs ticks = DateTime.UnixEpoch + TimeSpan.FromTicks (ticks * 10_000L)
|
let formatDistance (startOn : Instant) (endOn : Instant) =
|
||||||
|
|
||||||
let formatDistance (startDate : Instant) (endDate : Instant) =
|
|
||||||
let format (token, number) locale =
|
let format (token, number) locale =
|
||||||
let labels = locales |> Map.find 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 round (it : float) = Math.Round it |> int
|
||||||
|
|
||||||
let diff = startDate - endDate
|
let diff = startOn - endOn
|
||||||
let minutes = Math.Abs diff.TotalMinutes
|
let minutes = Math.Abs diff.TotalMinutes
|
||||||
let formatToken =
|
let formatToken =
|
||||||
let months = minutes / aMonth |> round
|
let months = minutes / aMonth |> round
|
||||||
@ -74,5 +72,5 @@ let formatDistance (startDate : Instant) (endDate : Instant) =
|
|||||||
| _ -> AlmostXYears, years + 1
|
| _ -> AlmostXYears, years + 1
|
||||||
|
|
||||||
format formatToken "en-US"
|
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"
|
||||||
|
|
||||||
|
@ -73,6 +73,15 @@ type HttpContext with
|
|||||||
/// Get the current instant from the system clock
|
/// Get the current instant from the system clock
|
||||||
member this.Now = this.Clock.GetCurrentInstant
|
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
|
/// Handler helpers
|
||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
@ -247,13 +256,13 @@ module Components =
|
|||||||
| _, _ -> false
|
| _, _ -> false
|
||||||
let! journal = Data.journalByUserId ctx.UserId ctx.Db
|
let! journal = Data.journalByUserId ctx.UserId ctx.Db
|
||||||
let shown = journal |> List.filter shouldBeShown
|
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]
|
// GET /components/request-item/[req-id]
|
||||||
let requestItem reqId : HttpHandler = requireUser >=> fun next ctx -> task {
|
let requestItem reqId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
match! Data.tryJournalById (RequestId.ofString reqId) ctx.UserId ctx.Db with
|
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
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,7 +273,7 @@ module Components =
|
|||||||
// GET /components/request/[req-id]/notes
|
// GET /components/request/[req-id]/notes
|
||||||
let notes requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
let notes requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
let! notes = Data.notesById (RequestId.ofString requestId) ctx.UserId ctx.Db
|
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
|
// GET /components/request/[req-id]/snooze
|
||||||
@ -367,7 +376,7 @@ module Request =
|
|||||||
// GET /requests/active
|
// GET /requests/active
|
||||||
let active : HttpHandler = requireUser >=> fun next ctx -> task {
|
let active : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
let! reqs = Data.journalByUserId ctx.UserId ctx.Db
|
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
|
// GET /requests/snoozed
|
||||||
@ -376,19 +385,19 @@ module Request =
|
|||||||
let now = ctx.Now ()
|
let now = ctx.Now ()
|
||||||
let snoozed = reqs
|
let snoozed = reqs
|
||||||
|> List.filter (fun it -> defaultArg (it.SnoozedUntil |> Option.map (fun it -> it > now)) false)
|
|> 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
|
// GET /requests/answered
|
||||||
let answered : HttpHandler = requireUser >=> fun next ctx -> task {
|
let answered : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
let! reqs = Data.answeredRequests ctx.UserId ctx.Db
|
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
|
// GET /request/[req-id]/full
|
||||||
let getFull requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
let getFull requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
match! Data.tryFullRequestById (RequestId.ofString requestId) ctx.UserId ctx.Db with
|
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
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.6.1" />
|
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.6.1" />
|
||||||
<PackageReference Include="LiteDB" Version="5.0.11" />
|
<PackageReference Include="LiteDB" Version="5.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.10" />
|
<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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="wwwroot\" />
|
<Folder Include="wwwroot\" />
|
||||||
|
@ -74,7 +74,8 @@ module Configure =
|
|||||||
|
|
||||||
let _ = bldr.Services.AddRouting ()
|
let _ = bldr.Services.AddRouting ()
|
||||||
let _ = bldr.Services.AddGiraffe ()
|
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 _ =
|
let _ =
|
||||||
bldr.Services.Configure<CookiePolicyOptions>(fun (opts : CookiePolicyOptions) ->
|
bldr.Services.Configure<CookiePolicyOptions>(fun (opts : CookiePolicyOptions) ->
|
||||||
|
@ -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
|
/// Create a date with a span tag, displaying the relative date with the full date/time in the tooltip
|
||||||
let relativeDate (date : Instant) now =
|
let relativeDate (date : Instant) now (tz : DateTimeZone) =
|
||||||
span [ _title (date.ToDateTimeOffset().ToString ("f", null)) ] [ Dates.formatDistance now date |> str ]
|
span [ _title (date.InZone(tz).ToDateTimeOffset().ToString ("f", null)) ] [ Dates.formatDistance now date |> str ]
|
||||||
|
@ -7,7 +7,7 @@ open Giraffe.ViewEngine.Htmx
|
|||||||
open MyPrayerJournal
|
open MyPrayerJournal
|
||||||
|
|
||||||
/// Display a card for this prayer request
|
/// Display a card for this prayer request
|
||||||
let journalCard now req =
|
let journalCard now tz req =
|
||||||
let reqId = RequestId.toString req.RequestId
|
let reqId = RequestId.toString req.RequestId
|
||||||
let spacer = span [] [ rawText " " ]
|
let spacer = span [] [ rawText " " ]
|
||||||
div [ _class "col" ] [
|
div [ _class "col" ] [
|
||||||
@ -50,8 +50,8 @@ let journalCard now req =
|
|||||||
div [ _class "card-footer text-end text-muted px-1 py-0" ] [
|
div [ _class "card-footer text-end text-muted px-1 py-0" ] [
|
||||||
em [] [
|
em [] [
|
||||||
match req.LastPrayed with
|
match req.LastPrayed with
|
||||||
| Some dt -> str "last prayed "; relativeDate dt now
|
| Some dt -> str "last prayed "; relativeDate dt now tz
|
||||||
| None -> str "last activity "; relativeDate req.AsOf now
|
| None -> str "last activity "; relativeDate req.AsOf now tz
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -120,7 +120,7 @@ let journal user =
|
|||||||
]
|
]
|
||||||
|
|
||||||
/// The journal items
|
/// The journal items
|
||||||
let journalItems now items =
|
let journalItems now tz items =
|
||||||
match items |> List.isEmpty with
|
match items |> List.isEmpty with
|
||||||
| true ->
|
| true ->
|
||||||
noResults "No Active Requests" "/request/new/edit" "Add a Request" [
|
noResults "No Active Requests" "/request/new/edit" "Add a Request" [
|
||||||
@ -129,7 +129,7 @@ let journalItems now items =
|
|||||||
]
|
]
|
||||||
| false ->
|
| false ->
|
||||||
items
|
items
|
||||||
|> List.map (journalCard now)
|
|> List.map (journalCard now tz)
|
||||||
|> section [ _id "journalItems"
|
|> section [ _id "journalItems"
|
||||||
_class "row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-3"
|
_class "row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-3"
|
||||||
_hxTarget "this"
|
_hxTarget "this"
|
||||||
|
@ -7,7 +7,7 @@ open MyPrayerJournal
|
|||||||
open NodaTime
|
open NodaTime
|
||||||
|
|
||||||
/// Create a request within the list
|
/// 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 isFuture instant = defaultArg (instant |> Option.map (fun it -> it > now)) false
|
||||||
let reqId = RequestId.toString req.RequestId
|
let reqId = RequestId.toString req.RequestId
|
||||||
let isAnswered = req.LastStatus = Answered
|
let isAnswered = req.LastStatus = Answered
|
||||||
@ -28,32 +28,32 @@ let reqListItem now req =
|
|||||||
if isSnoozed || isPending || isAnswered then
|
if isSnoozed || isPending || isAnswered then
|
||||||
br []
|
br []
|
||||||
small [ _class "text-muted" ] [
|
small [ _class "text-muted" ] [
|
||||||
if isSnoozed then [ str "Snooze expires "; relativeDate req.SnoozedUntil.Value 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 ]
|
elif isPending then [ str "Request appears next "; relativeDate req.ShowAfter.Value now tz ]
|
||||||
else (* isAnswered *) [ str "Answered "; relativeDate req.AsOf now ]
|
else (* isAnswered *) [ str "Answered "; relativeDate req.AsOf now tz ]
|
||||||
|> em []
|
|> em []
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
/// Create a list of requests
|
/// Create a list of requests
|
||||||
let reqList now reqs =
|
let reqList now tz reqs =
|
||||||
reqs
|
reqs
|
||||||
|> List.map (reqListItem now)
|
|> List.map (reqListItem now tz)
|
||||||
|> div [ _class "list-group" ]
|
|> div [ _class "list-group" ]
|
||||||
|
|
||||||
/// View for Active Requests page
|
/// View for Active Requests page
|
||||||
let active now reqs =
|
let active now tz reqs =
|
||||||
article [ _class "container mt-3" ] [
|
article [ _class "container mt-3" ] [
|
||||||
h2 [ _class "pb-3" ] [ str "Active Requests" ]
|
h2 [ _class "pb-3" ] [ str "Active Requests" ]
|
||||||
if List.isEmpty reqs then
|
if List.isEmpty reqs then
|
||||||
noResults "No Active Requests" "/journal" "Return to your journal"
|
noResults "No Active Requests" "/journal" "Return to your journal"
|
||||||
[ str "Your prayer journal has no active requests" ]
|
[ str "Your prayer journal has no active requests" ]
|
||||||
else reqList now reqs
|
else reqList now tz reqs
|
||||||
]
|
]
|
||||||
|
|
||||||
/// View for Answered Requests page
|
/// View for Answered Requests page
|
||||||
let answered now reqs =
|
let answered now tz reqs =
|
||||||
article [ _class "container mt-3" ] [
|
article [ _class "container mt-3" ] [
|
||||||
h2 [ _class "pb-3" ] [ str "Answered Requests" ]
|
h2 [ _class "pb-3" ] [ str "Answered Requests" ]
|
||||||
if List.isEmpty reqs then
|
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 "
|
str "Your prayer journal has no answered requests; once you have marked one as "
|
||||||
rawText "“Answered”, it will appear here"
|
rawText "“Answered”, it will appear here"
|
||||||
]
|
]
|
||||||
else reqList now reqs
|
else reqList now tz reqs
|
||||||
]
|
]
|
||||||
|
|
||||||
/// View for Snoozed Requests page
|
/// View for Snoozed Requests page
|
||||||
let snoozed now reqs =
|
let snoozed now tz reqs =
|
||||||
article [ _class "container mt-3" ] [
|
article [ _class "container mt-3" ] [
|
||||||
h2 [ _class "pb-3" ] [ str "Snoozed Requests" ]
|
h2 [ _class "pb-3" ] [ str "Snoozed Requests" ]
|
||||||
reqList now reqs
|
reqList now tz reqs
|
||||||
]
|
]
|
||||||
|
|
||||||
/// View for Full Request page
|
/// View for Full Request page
|
||||||
let full (clock : IClock) (req : Request) =
|
let full (clock : IClock) tz (req : Request) =
|
||||||
let now = clock.GetCurrentInstant ()
|
let now = clock.GetCurrentInstant ()
|
||||||
let answered =
|
let answered =
|
||||||
req.History
|
req.History
|
||||||
@ -110,7 +110,7 @@ let full (clock : IClock) (req : Request) =
|
|||||||
str "Answered "
|
str "Answered "
|
||||||
date.ToDateTimeOffset().ToString ("D", null) |> str
|
date.ToDateTimeOffset().ToString ("D", null) |> str
|
||||||
str " ("
|
str " ("
|
||||||
relativeDate date now
|
relativeDate date now tz
|
||||||
rawText ") • "
|
rawText ") • "
|
||||||
| None -> ()
|
| None -> ()
|
||||||
rawText $"Prayed %s{prayed} times • Open %s{daysOpen} days"
|
rawText $"Prayed %s{prayed} times • Open %s{daysOpen} days"
|
||||||
@ -259,9 +259,9 @@ let edit (req : JournalRequest) returnTo isNew =
|
|||||||
]
|
]
|
||||||
|
|
||||||
/// Display a list of notes for a request
|
/// Display a list of notes for a request
|
||||||
let notes now notes =
|
let notes now tz notes =
|
||||||
let toItem (note : Note) =
|
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" ] ]
|
[ p [ _class "text-center" ] [ strong [] [ str "Prior Notes for This Request" ] ]
|
||||||
match notes with
|
match notes with
|
||||||
| [] -> p [ _class "text-center text-muted" ] [ str "There are no prior notes for this request" ]
|
| [] -> p [ _class "text-center text-muted" ] [ str "There are no prior notes for this request" ]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
/** myPrayerJournal script */
|
/** myPrayerJournal script */
|
||||||
const mpj = {
|
this.mpj = {
|
||||||
/**
|
/**
|
||||||
* Show a message via toast
|
* Show a message via toast
|
||||||
* @param {string} message The message to show
|
* @param {string} message The message to show
|
||||||
@ -66,6 +66,19 @@ const mpj = {
|
|||||||
const isDisabled = target.value === "Immediate"
|
const isDisabled = target.value === "Immediate"
|
||||||
;["recurCount", "recurInterval"].forEach(it => document.getElementById(it).disabled = isDisabled)
|
;["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()
|
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()
|
||||||
|
Loading…
Reference in New Issue
Block a user