Version 3.1 #71
No files matched your search
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Program.fs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MyPrayerJournal\MyPrayerJournal.fsproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
open MyPrayerJournal.Domain
|
||||||
|
open NodaTime
|
||||||
|
|
||||||
|
/// The old definition of the history entry
|
||||||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
type OldHistory =
|
||||||
|
{ /// The time when this history entry was made
|
||||||
|
asOf : int64
|
||||||
|
/// The status for this history entry
|
||||||
|
status : RequestAction
|
||||||
|
/// The text of the update, if applicable
|
||||||
|
text : string option
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The old definition of of the note entry
|
||||||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
type OldNote =
|
||||||
|
{ /// The time when this note was made
|
||||||
|
asOf : int64
|
||||||
|
|
||||||
|
/// The text of the notes
|
||||||
|
notes : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request is the identifying record for a prayer request
|
||||||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
type OldRequest =
|
||||||
|
{ /// The ID of the request
|
||||||
|
id : RequestId
|
||||||
|
|
||||||
|
/// The time this request was initially entered
|
||||||
|
enteredOn : int64
|
||||||
|
|
||||||
|
/// The ID of the user to whom this request belongs ("sub" from the JWT)
|
||||||
|
userId : UserId
|
||||||
|
|
||||||
|
/// The time at which this request should reappear in the user's journal by manual user choice
|
||||||
|
snoozedUntil : int64
|
||||||
|
|
||||||
|
/// The time at which this request should reappear in the user's journal by recurrence
|
||||||
|
showAfter : int64
|
||||||
|
|
||||||
|
/// The type of recurrence for this request
|
||||||
|
recurType : string
|
||||||
|
|
||||||
|
/// How many of the recurrence intervals should occur between appearances in the journal
|
||||||
|
recurCount : int16
|
||||||
|
|
||||||
|
/// The history entries for this request
|
||||||
|
history : OldHistory[]
|
||||||
|
|
||||||
|
/// The notes for this request
|
||||||
|
notes : OldNote[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open LiteDB
|
||||||
|
open MyPrayerJournal.Data
|
||||||
|
|
||||||
|
let db = new LiteDatabase ("Filename=./mpj.db")
|
||||||
|
Startup.ensureDb db
|
||||||
|
|
||||||
|
/// Map the old recurrence to the new style
|
||||||
|
let mapRecurrence old =
|
||||||
|
match old.recurType with
|
||||||
|
| "Days" -> Days old.recurCount
|
||||||
|
| "Hours" -> Hours old.recurCount
|
||||||
|
| "Weeks" -> Weeks old.recurCount
|
||||||
|
| _ -> Immediate
|
||||||
|
|
||||||
|
/// Convert an old history entry to the new form
|
||||||
|
let convertHistory (old : OldHistory) =
|
||||||
|
{ AsOf = Instant.FromUnixTimeMilliseconds old.asOf
|
||||||
|
Status = old.status
|
||||||
|
Text = old.text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert an old note to the new form
|
||||||
|
let convertNote (old : OldNote) =
|
||||||
|
{ AsOf = Instant.FromUnixTimeMilliseconds old.asOf
|
||||||
|
Notes = old.notes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert items that may be Instant.MinValue or Instant(0) to None
|
||||||
|
let noneIfOld ms =
|
||||||
|
match Instant.FromUnixTimeMilliseconds ms with
|
||||||
|
| instant when instant > Instant.FromUnixTimeMilliseconds 0 -> Some instant
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
|
/// Map the old request to the new request
|
||||||
|
let convert old =
|
||||||
|
{ Id = old.id
|
||||||
|
EnteredOn = Instant.FromUnixTimeMilliseconds old.enteredOn
|
||||||
|
UserId = old.userId
|
||||||
|
SnoozedUntil = noneIfOld old.snoozedUntil
|
||||||
|
ShowAfter = noneIfOld old.showAfter
|
||||||
|
Recurrence = mapRecurrence old
|
||||||
|
History = old.history |> Array.map convertHistory |> List.ofArray
|
||||||
|
Notes = old.notes |> Array.map convertNote |> List.ofArray
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the old request, add the converted one (removes recurType / recurCount fields)
|
||||||
|
let replace (req : Request) =
|
||||||
|
db.Requests.Delete (Mapping.RequestId.toBson req.Id) |> ignore
|
||||||
|
db.Requests.Insert req |> ignore
|
||||||
|
db.Checkpoint ()
|
||||||
|
|
||||||
|
db.GetCollection<OldRequest>("request").FindAll ()
|
||||||
|
|> Seq.map convert
|
||||||
|
|> List.ofSeq
|
||||||
|
|> List.iter replace
|
||||||
|
|
||||||
|
// For more information see https://aka.ms/fsharp-console-apps
|
||||||
|
printfn "Done"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 16
|
||||||
|
VisualStudioVersion = 16.0.30114.105
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyPrayerJournal", "MyPrayerJournal\MyPrayerJournal.fsproj", "{6BD5A3C8-F859-42A0-ACD7-A5819385E828}"
|
||||||
|
EndProject
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyPrayerJournal.ConvertRecurrence", "MyPrayerJournal.ConvertRecurrence\MyPrayerJournal.ConvertRecurrence.fsproj", "{72B57736-8721-4636-A309-49FA4222416E}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{6BD5A3C8-F859-42A0-ACD7-A5819385E828}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6BD5A3C8-F859-42A0-ACD7-A5819385E828}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6BD5A3C8-F859-42A0-ACD7-A5819385E828}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6BD5A3C8-F859-42A0-ACD7-A5819385E828}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{72B57736-8721-4636-A309-49FA4222416E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{72B57736-8721-4636-A309-49FA4222416E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{72B57736-8721-4636-A309-49FA4222416E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{72B57736-8721-4636-A309-49FA4222416E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
+84
-94
@@ -1,23 +1,21 @@
|
|||||||
module MyPrayerJournal.Data
|
module MyPrayerJournal.Data
|
||||||
|
|
||||||
open LiteDB
|
open LiteDB
|
||||||
open NodaTime
|
open MyPrayerJournal
|
||||||
open System
|
|
||||||
open System.Threading.Tasks
|
open System.Threading.Tasks
|
||||||
|
|
||||||
// fsharplint:disable MemberNames
|
|
||||||
|
|
||||||
/// LiteDB extensions
|
/// LiteDB extensions
|
||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
module Extensions =
|
module Extensions =
|
||||||
|
|
||||||
/// Extensions on the LiteDatabase class
|
/// Extensions on the LiteDatabase class
|
||||||
type LiteDatabase with
|
type LiteDatabase with
|
||||||
|
|
||||||
/// The Request collection
|
/// The Request collection
|
||||||
member this.requests
|
member this.Requests = this.GetCollection<Request> "request"
|
||||||
with get () = this.GetCollection<Request> "request"
|
|
||||||
/// Async version of the checkpoint command (flushes log)
|
/// Async version of the checkpoint command (flushes log)
|
||||||
member this.saveChanges () =
|
member this.SaveChanges () =
|
||||||
this.Checkpoint ()
|
this.Checkpoint ()
|
||||||
Task.CompletedTask
|
Task.CompletedTask
|
||||||
|
|
||||||
@@ -27,72 +25,61 @@ module Extensions =
|
|||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Mapping =
|
module Mapping =
|
||||||
|
|
||||||
/// Map a history entry to BSON
|
open NodaTime
|
||||||
let historyToBson (hist : History) : BsonValue =
|
open NodaTime.Text
|
||||||
let doc = BsonDocument ()
|
|
||||||
doc["asOf"] <- hist.asOf.ToUnixTimeMilliseconds ()
|
|
||||||
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
|
/// A NodaTime instant pattern to use for parsing instants from the database
|
||||||
let historyFromBson (doc : BsonValue) =
|
let instantPattern = InstantPattern.CreateWithInvariantCulture "g"
|
||||||
{ asOf = Instant.FromUnixTimeMilliseconds 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
|
/// Mapping for NodaTime's Instant type
|
||||||
let noteToBson (note : Note) : BsonValue =
|
module Instant =
|
||||||
let doc = BsonDocument ()
|
let fromBson (value : BsonValue) = (instantPattern.Parse value.AsString).Value
|
||||||
doc["asOf"] <- note.asOf.ToUnixTimeMilliseconds ()
|
let toBson (value : Instant) : BsonValue = value.ToString ("g", null)
|
||||||
doc["notes"] <- note.notes
|
|
||||||
upcast doc
|
|
||||||
|
|
||||||
/// Map a BSON document to a note entry
|
/// Mapping for option types
|
||||||
let noteFromBson (doc : BsonValue) =
|
module Option =
|
||||||
{ asOf = Instant.FromUnixTimeMilliseconds doc["asOf"].AsInt64
|
let instantFromBson (value : BsonValue) = if value.IsNull then None else Some (Instant.fromBson value)
|
||||||
notes = doc["notes"].AsString
|
let instantToBson (value : Instant option) = match value with Some it -> Instant.toBson it | None -> null
|
||||||
}
|
|
||||||
|
|
||||||
/// Map a request to its BSON representation
|
let stringFromBson (value : BsonValue) = match value.AsString with "" -> None | x -> Some x
|
||||||
let requestToBson req : BsonValue =
|
let stringToBson (value : string option) : BsonValue = match value with Some txt -> txt | None -> ""
|
||||||
let doc = BsonDocument ()
|
|
||||||
doc["_id"] <- RequestId.toString req.id
|
|
||||||
doc["enteredOn"] <- req.enteredOn.ToUnixTimeMilliseconds ()
|
|
||||||
doc["userId"] <- UserId.toString req.userId
|
|
||||||
doc["snoozedUntil"] <- req.snoozedUntil.ToUnixTimeMilliseconds ()
|
|
||||||
doc["showAfter"] <- req.showAfter.ToUnixTimeMilliseconds ()
|
|
||||||
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
|
/// Mapping for Recurrence
|
||||||
let requestFromBson (doc : BsonValue) =
|
module Recurrence =
|
||||||
{ id = RequestId.ofString doc["_id"].AsString
|
let fromBson (value : BsonValue) = Recurrence.ofString value
|
||||||
enteredOn = Instant.FromUnixTimeMilliseconds doc["enteredOn"].AsInt64
|
let toBson (value : Recurrence) : BsonValue = Recurrence.toString value
|
||||||
userId = UserId doc["userId"].AsString
|
|
||||||
snoozedUntil = Instant.FromUnixTimeMilliseconds doc["snoozedUntil"].AsInt64
|
/// Mapping for RequestAction
|
||||||
showAfter = Instant.FromUnixTimeMilliseconds doc["showAfter"].AsInt64
|
module RequestAction =
|
||||||
recurType = Recurrence.ofString doc["recurType"].AsString
|
let fromBson (value : BsonValue) = RequestAction.ofString value.AsString
|
||||||
recurCount = int16 doc["recurCount"].AsInt32
|
let toBson (value : RequestAction) : BsonValue = RequestAction.toString value
|
||||||
history = doc["history"].AsArray |> Seq.map historyFromBson |> List.ofSeq
|
|
||||||
notes = doc["notes"].AsArray |> Seq.map noteFromBson |> List.ofSeq
|
/// Mapping for RequestId
|
||||||
}
|
module RequestId =
|
||||||
|
let fromBson (value : BsonValue) = RequestId.ofString value.AsString
|
||||||
|
let toBson (value : RequestId) : BsonValue = RequestId.toString value
|
||||||
|
|
||||||
|
/// Mapping for UserId
|
||||||
|
module UserId =
|
||||||
|
let fromBson (value : BsonValue) = UserId value.AsString
|
||||||
|
let toBson (value : UserId) : BsonValue = UserId.toString value
|
||||||
|
|
||||||
/// Set up the mapping
|
/// Set up the mapping
|
||||||
let register () =
|
let register () =
|
||||||
BsonMapper.Global.RegisterType<Request>(
|
BsonMapper.Global.RegisterType<Instant>(Instant.toBson, Instant.fromBson)
|
||||||
Func<Request, BsonValue> requestToBson, Func<BsonValue, Request> requestFromBson)
|
BsonMapper.Global.RegisterType<Instant option>(Option.instantToBson, Option.instantFromBson)
|
||||||
|
BsonMapper.Global.RegisterType<Recurrence>(Recurrence.toBson, Recurrence.fromBson)
|
||||||
|
BsonMapper.Global.RegisterType<RequestAction>(RequestAction.toBson, RequestAction.fromBson)
|
||||||
|
BsonMapper.Global.RegisterType<RequestId>(RequestId.toBson, RequestId.fromBson)
|
||||||
|
BsonMapper.Global.RegisterType<string option>(Option.stringToBson, Option.stringFromBson)
|
||||||
|
BsonMapper.Global.RegisterType<UserId>(UserId.toBson, UserId.fromBson)
|
||||||
|
|
||||||
/// Code to be run at startup
|
/// Code to be run at startup
|
||||||
module Startup =
|
module Startup =
|
||||||
|
|
||||||
/// Ensure the database is set up
|
/// Ensure the database is set up
|
||||||
let ensureDb (db : LiteDatabase) =
|
let ensureDb (db : LiteDatabase) =
|
||||||
db.requests.EnsureIndex (fun it -> it.userId) |> ignore
|
db.Requests.EnsureIndex (fun it -> it.UserId) |> ignore
|
||||||
Mapping.register ()
|
Mapping.register ()
|
||||||
|
|
||||||
|
|
||||||
@@ -112,98 +99,101 @@ module private Helpers =
|
|||||||
|
|
||||||
/// Async wrapper around a request update
|
/// Async wrapper around a request update
|
||||||
let doUpdate (db : LiteDatabase) (req : Request) =
|
let doUpdate (db : LiteDatabase) (req : Request) =
|
||||||
db.requests.Update req |> ignore
|
db.Requests.Update req |> ignore
|
||||||
Task.CompletedTask
|
Task.CompletedTask
|
||||||
|
|
||||||
|
|
||||||
/// Retrieve a request, including its history and notes, by its ID and user ID
|
/// Retrieve a request, including its history and notes, by its ID and user ID
|
||||||
let tryFullRequestById reqId userId (db : LiteDatabase) = backgroundTask {
|
let tryFullRequestById reqId userId (db : LiteDatabase) = backgroundTask {
|
||||||
let! req = db.requests.Find (Query.EQ ("_id", RequestId.toString reqId)) |> 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
|
return match box req with null -> None | _ when req.UserId = userId -> Some req | _ -> None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a history entry
|
/// Add a history entry
|
||||||
let addHistory reqId userId hist db = backgroundTask {
|
let addHistory reqId userId hist db = backgroundTask {
|
||||||
match! tryFullRequestById reqId userId db with
|
match! tryFullRequestById reqId userId db with
|
||||||
| Some req -> do! doUpdate db { req with history = hist :: req.history }
|
| Some req -> do! doUpdate db { req with History = Array.append [| hist |] req.History }
|
||||||
| None -> invalidOp $"{RequestId.toString reqId} not found"
|
| None -> invalidOp $"{RequestId.toString reqId} not found"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a note
|
/// Add a note
|
||||||
let addNote reqId userId note db = backgroundTask {
|
let addNote reqId userId note db = backgroundTask {
|
||||||
match! tryFullRequestById reqId userId db with
|
match! tryFullRequestById reqId userId db with
|
||||||
| Some req -> do! doUpdate db { req with notes = note :: req.notes }
|
| Some req -> do! doUpdate db { req with Notes = Array.append [| note |] req.Notes }
|
||||||
| None -> invalidOp $"{RequestId.toString reqId} not found"
|
| None -> invalidOp $"{RequestId.toString reqId} not found"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a request
|
/// Add a request
|
||||||
let addRequest (req : Request) (db : LiteDatabase) =
|
let addRequest (req : Request) (db : LiteDatabase) =
|
||||||
db.requests.Insert req |> ignore
|
db.Requests.Insert req |> ignore
|
||||||
|
|
||||||
// FIXME: make a common function here
|
/// Find all requests for the given user
|
||||||
|
let private getRequestsForUser (userId : UserId) (db : LiteDatabase) = backgroundTask {
|
||||||
|
return! db.Requests.Find (Query.EQ (nameof Request.empty.UserId, Mapping.UserId.toBson userId)) |> toListAsync
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieve all answered requests for the given user
|
/// Retrieve all answered requests for the given user
|
||||||
let answeredRequests userId (db : LiteDatabase) = backgroundTask {
|
let answeredRequests userId db = backgroundTask {
|
||||||
let! reqs = db.requests.Find (Query.EQ ("userId", UserId.toString userId)) |> toListAsync
|
let! reqs = getRequestsForUser userId db
|
||||||
return
|
return
|
||||||
reqs
|
reqs
|
||||||
|> Seq.map JournalRequest.ofRequestFull
|
|> Seq.map JournalRequest.ofRequestFull
|
||||||
|> Seq.filter (fun it -> it.lastStatus = Answered)
|
|> Seq.filter (fun it -> it.LastStatus = Answered)
|
||||||
|> Seq.sortByDescending (fun it -> it.asOf)
|
|> Seq.sortByDescending (fun it -> it.AsOf)
|
||||||
|> List.ofSeq
|
|> List.ofSeq
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the user's current journal
|
/// Retrieve the user's current journal
|
||||||
let journalByUserId userId (db : LiteDatabase) = backgroundTask {
|
let journalByUserId userId db = backgroundTask {
|
||||||
let! jrnl = db.requests.Find (Query.EQ ("userId", UserId.toString userId)) |> toListAsync
|
let! reqs = getRequestsForUser userId db
|
||||||
return
|
return
|
||||||
jrnl
|
reqs
|
||||||
|> Seq.map JournalRequest.ofRequestLite
|
|> Seq.map JournalRequest.ofRequestLite
|
||||||
|> Seq.filter (fun it -> it.lastStatus <> Answered)
|
|> Seq.filter (fun it -> it.LastStatus <> Answered)
|
||||||
|> Seq.sortBy (fun it -> it.asOf)
|
|> Seq.sortBy (fun it -> it.AsOf)
|
||||||
|> List.ofSeq
|
|> List.ofSeq
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Does the user have any snoozed requests?
|
/// Does the user have any snoozed requests?
|
||||||
let hasSnoozed userId now (db : LiteDatabase) = backgroundTask {
|
let hasSnoozed userId now (db : LiteDatabase) = backgroundTask {
|
||||||
let! jrnl = journalByUserId userId db
|
let! jrnl = journalByUserId userId db
|
||||||
return jrnl |> List.exists (fun r -> r.snoozedUntil > now)
|
return jrnl |> List.exists (fun r -> defaultArg (r.SnoozedUntil |> Option.map (fun it -> it > now)) false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve a request by its ID and user ID (without notes and history)
|
/// Retrieve a request by its ID and user ID (without notes and history)
|
||||||
let tryRequestById reqId userId db = backgroundTask {
|
let tryRequestById reqId userId db = backgroundTask {
|
||||||
let! req = tryFullRequestById reqId userId db
|
let! req = tryFullRequestById reqId userId db
|
||||||
return req |> Option.map (fun r -> { r with history = []; notes = [] })
|
return req |> Option.map (fun r -> { r with History = [||]; Notes = [||] })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve notes for a request by its ID and user ID
|
/// Retrieve notes for a request by its ID and user ID
|
||||||
let notesById reqId userId (db : LiteDatabase) = backgroundTask {
|
let notesById reqId userId (db : LiteDatabase) = backgroundTask {
|
||||||
match! tryFullRequestById reqId userId db with | Some req -> return req.notes | None -> return []
|
match! tryFullRequestById reqId userId db with | Some req -> return req.Notes | None -> return [||]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve a journal request by its ID and user ID
|
/// Retrieve a journal request by its ID and user ID
|
||||||
let tryJournalById reqId userId (db : LiteDatabase) = backgroundTask {
|
let tryJournalById reqId userId (db : LiteDatabase) = backgroundTask {
|
||||||
let! req = tryFullRequestById reqId userId db
|
let! req = tryFullRequestById reqId userId db
|
||||||
return req |> Option.map JournalRequest.ofRequestLite
|
return req |> Option.map JournalRequest.ofRequestLite
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the recurrence for a request
|
/// Update the recurrence for a request
|
||||||
let updateRecurrence reqId userId recurType recurCount db = backgroundTask {
|
let updateRecurrence reqId userId recurType db = backgroundTask {
|
||||||
match! tryFullRequestById reqId userId db with
|
match! tryFullRequestById reqId userId db with
|
||||||
| Some req -> do! doUpdate db { req with recurType = recurType; recurCount = recurCount }
|
| Some req -> do! doUpdate db { req with Recurrence = recurType }
|
||||||
| None -> invalidOp $"{RequestId.toString reqId} not found"
|
| None -> invalidOp $"{RequestId.toString reqId} not found"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update a snoozed request
|
/// Update a snoozed request
|
||||||
let updateSnoozed reqId userId until db = backgroundTask {
|
let updateSnoozed reqId userId until db = backgroundTask {
|
||||||
match! tryFullRequestById reqId userId db with
|
match! tryFullRequestById reqId userId db with
|
||||||
| Some req -> do! doUpdate db { req with snoozedUntil = until; showAfter = until }
|
| 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
|
/// Update the "show after" timestamp for a request
|
||||||
let updateShowAfter reqId userId showAfter db = backgroundTask {
|
let updateShowAfter reqId userId showAfter db = backgroundTask {
|
||||||
match! tryFullRequestById reqId userId db with
|
match! tryFullRequestById reqId userId db with
|
||||||
| Some req -> do! doUpdate db { req with showAfter = showAfter }
|
| Some req -> do! doUpdate db { req with ShowAfter = showAfter }
|
||||||
| None -> invalidOp $"{RequestId.toString reqId} not found"
|
| None -> invalidOp $"{RequestId.toString reqId} not found"
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|
||||||
+209
-142
@@ -1,67 +1,84 @@
|
|||||||
[<AutoOpen>]
|
/// The data model for myPrayerJournal
|
||||||
/// The data model for myPrayerJournal
|
[<AutoOpen>]
|
||||||
module MyPrayerJournal.Domain
|
module MyPrayerJournal.Domain
|
||||||
|
|
||||||
// fsharplint:disable RecordFieldNames
|
open System
|
||||||
|
|
||||||
open Cuid
|
open Cuid
|
||||||
open NodaTime
|
open NodaTime
|
||||||
|
|
||||||
/// An identifier for a request
|
/// An identifier for a request
|
||||||
type RequestId =
|
type RequestId = RequestId of Cuid
|
||||||
| RequestId of Cuid
|
|
||||||
|
|
||||||
/// Functions to manipulate request IDs
|
/// Functions to manipulate request IDs
|
||||||
module RequestId =
|
module RequestId =
|
||||||
|
|
||||||
/// The string representation of the request ID
|
/// The string representation of the request ID
|
||||||
let toString = function RequestId x -> Cuid.toString x
|
let toString = function RequestId x -> Cuid.toString x
|
||||||
|
|
||||||
/// Create a request ID from a string representation
|
/// Create a request ID from a string representation
|
||||||
let ofString = Cuid >> RequestId
|
let ofString = Cuid >> RequestId
|
||||||
|
|
||||||
|
|
||||||
/// The identifier of a user (the "sub" part of the JWT)
|
/// The identifier of a user (the "sub" part of the JWT)
|
||||||
type UserId =
|
type UserId = UserId of string
|
||||||
| UserId of string
|
|
||||||
|
|
||||||
/// Functions to manipulate user IDs
|
/// Functions to manipulate user IDs
|
||||||
module UserId =
|
module UserId =
|
||||||
|
|
||||||
/// The string representation of the user ID
|
/// The string representation of the user ID
|
||||||
let toString = function UserId x -> x
|
let toString = function UserId x -> x
|
||||||
|
|
||||||
|
|
||||||
/// How frequently a request should reappear after it is marked "Prayed"
|
/// How frequently a request should reappear after it is marked "Prayed"
|
||||||
type Recurrence =
|
type Recurrence =
|
||||||
|
|
||||||
|
/// A request should reappear immediately at the bottom of the list
|
||||||
| Immediate
|
| Immediate
|
||||||
| Hours
|
|
||||||
| Days
|
/// A request should reappear in the given number of hours
|
||||||
| Weeks
|
| Hours of int16
|
||||||
|
|
||||||
|
/// A request should reappear in the given number of days
|
||||||
|
| Days of int16
|
||||||
|
|
||||||
|
/// A request should reappear in the given number of weeks (7-day increments)
|
||||||
|
| Weeks of int16
|
||||||
|
|
||||||
/// Functions to manipulate recurrences
|
/// Functions to manipulate recurrences
|
||||||
module Recurrence =
|
module Recurrence =
|
||||||
|
|
||||||
/// Create a string representation of a recurrence
|
/// Create a string representation of a recurrence
|
||||||
let toString =
|
let toString =
|
||||||
function
|
function
|
||||||
| Immediate -> "Immediate"
|
| Immediate -> "Immediate"
|
||||||
| Hours -> "Hours"
|
| Hours h -> $"{h} Hours"
|
||||||
| Days -> "Days"
|
| Days d -> $"{d} Days"
|
||||||
| Weeks -> "Weeks"
|
| Weeks w -> $"{w} Weeks"
|
||||||
|
|
||||||
/// Create a recurrence value from a string
|
/// Create a recurrence value from a string
|
||||||
let ofString =
|
let ofString =
|
||||||
function
|
function
|
||||||
| "Immediate" -> Immediate
|
| "Immediate" -> Immediate
|
||||||
| "Hours" -> Hours
|
| it when it.Contains " " ->
|
||||||
| "Days" -> Days
|
let parts = it.Split " "
|
||||||
| "Weeks" -> Weeks
|
let length = Convert.ToInt16 parts[0]
|
||||||
|
match parts[1] with
|
||||||
|
| "Hours" -> Hours length
|
||||||
|
| "Days" -> Days length
|
||||||
|
| "Weeks" -> Weeks length
|
||||||
|
| _ -> invalidOp $"{parts[1]} is not a valid recurrence"
|
||||||
| it -> invalidOp $"{it} is not a valid recurrence"
|
| it -> invalidOp $"{it} is not a valid recurrence"
|
||||||
|
|
||||||
/// An hour's worth of seconds
|
/// An hour's worth of seconds
|
||||||
let private oneHour = 3_600L
|
let private oneHour = 3_600L
|
||||||
|
|
||||||
/// The duration of the recurrence (in milliseconds)
|
/// The duration of the recurrence (in milliseconds)
|
||||||
let duration x =
|
let duration =
|
||||||
(match x with
|
function
|
||||||
| Immediate -> 0L
|
| Immediate -> 0L
|
||||||
| Hours -> oneHour
|
| Hours h -> int64 h * oneHour
|
||||||
| Days -> oneHour * 24L
|
| Days d -> int64 d * oneHour * 24L
|
||||||
| Weeks -> oneHour * 24L * 7L)
|
| Weeks w -> int64 w * oneHour * 24L * 7L
|
||||||
|
|
||||||
|
|
||||||
/// The action taken on a request as part of a history entry
|
/// The action taken on a request as part of a history entry
|
||||||
@@ -71,125 +88,9 @@ type RequestAction =
|
|||||||
| Updated
|
| Updated
|
||||||
| Answered
|
| Answered
|
||||||
|
|
||||||
|
|
||||||
/// History is a record of action taken on a prayer request, including updates to its text
|
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
|
||||||
type History = {
|
|
||||||
/// The time when this history entry was made
|
|
||||||
asOf : Instant
|
|
||||||
/// The status for this history entry
|
|
||||||
status : RequestAction
|
|
||||||
/// The text of the update, if applicable
|
|
||||||
text : string option
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Note is a note regarding a prayer request that does not result in an update to its text
|
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
|
||||||
type Note = {
|
|
||||||
/// The time when this note was made
|
|
||||||
asOf : Instant
|
|
||||||
/// The text of the notes
|
|
||||||
notes : string
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request is the identifying record for a prayer request
|
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
|
||||||
type Request = {
|
|
||||||
/// The ID of the request
|
|
||||||
id : RequestId
|
|
||||||
/// The time this request was initially entered
|
|
||||||
enteredOn : Instant
|
|
||||||
/// The ID of the user to whom this request belongs ("sub" from the JWT)
|
|
||||||
userId : UserId
|
|
||||||
/// The time at which this request should reappear in the user's journal by manual user choice
|
|
||||||
snoozedUntil : Instant
|
|
||||||
/// The time at which this request should reappear in the user's journal by recurrence
|
|
||||||
showAfter : Instant
|
|
||||||
/// The type of recurrence for this request
|
|
||||||
recurType : Recurrence
|
|
||||||
/// How many of the recurrence intervals should occur between appearances in the journal
|
|
||||||
recurCount : int16
|
|
||||||
/// The history entries for this request
|
|
||||||
history : History list
|
|
||||||
/// The notes for this request
|
|
||||||
notes : Note list
|
|
||||||
}
|
|
||||||
with
|
|
||||||
/// An empty request
|
|
||||||
static member empty =
|
|
||||||
{ id = Cuid.generate () |> RequestId
|
|
||||||
enteredOn = Instant.MinValue
|
|
||||||
userId = UserId ""
|
|
||||||
snoozedUntil = Instant.MinValue
|
|
||||||
showAfter = Instant.MinValue
|
|
||||||
recurType = Immediate
|
|
||||||
recurCount = 0s
|
|
||||||
history = []
|
|
||||||
notes = []
|
|
||||||
}
|
|
||||||
|
|
||||||
/// JournalRequest is the form of a prayer request returned for the request journal display. It also contains
|
|
||||||
/// properties that may be filled for history and notes.
|
|
||||||
[<NoComparison; NoEquality>]
|
|
||||||
type JournalRequest = {
|
|
||||||
/// The ID of the request (just the CUID part)
|
|
||||||
requestId : RequestId
|
|
||||||
/// The ID of the user to whom the request belongs
|
|
||||||
userId : UserId
|
|
||||||
/// The current text of the request
|
|
||||||
text : string
|
|
||||||
/// The last time action was taken on the request
|
|
||||||
asOf : Instant
|
|
||||||
/// The last status for the request
|
|
||||||
lastStatus : RequestAction
|
|
||||||
/// The time that this request should reappear in the user's journal
|
|
||||||
snoozedUntil : Instant
|
|
||||||
/// The time after which this request should reappear in the user's journal by configured recurrence
|
|
||||||
showAfter : Instant
|
|
||||||
/// The type of recurrence for this request
|
|
||||||
recurType : Recurrence
|
|
||||||
/// How many of the recurrence intervals should occur between appearances in the journal
|
|
||||||
recurCount : int16
|
|
||||||
/// History entries for the request
|
|
||||||
history : History list
|
|
||||||
/// Note entries for the request
|
|
||||||
notes : Note list
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Functions to manipulate journal requests
|
|
||||||
module JournalRequest =
|
|
||||||
|
|
||||||
/// Convert a request to the form used for the journal (precomputed values, no notes or history)
|
|
||||||
let ofRequestLite (req : Request) =
|
|
||||||
let hist = req.history |> List.sortByDescending (fun it -> it.asOf) |> List.tryHead
|
|
||||||
{ requestId = req.id
|
|
||||||
userId = req.userId
|
|
||||||
text = req.history
|
|
||||||
|> List.filter (fun it -> Option.isSome it.text)
|
|
||||||
|> List.sortByDescending (fun it -> it.asOf)
|
|
||||||
|> List.tryHead
|
|
||||||
|> Option.map (fun h -> Option.get h.text)
|
|
||||||
|> Option.defaultValue ""
|
|
||||||
asOf = match hist with Some h -> h.asOf | None -> Instant.MinValue
|
|
||||||
lastStatus = match hist with Some h -> h.status | None -> Created
|
|
||||||
snoozedUntil = req.snoozedUntil
|
|
||||||
showAfter = req.showAfter
|
|
||||||
recurType = req.recurType
|
|
||||||
recurCount = req.recurCount
|
|
||||||
history = []
|
|
||||||
notes = []
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Same as `ofRequestLite`, but with notes and history
|
|
||||||
let ofRequestFull req =
|
|
||||||
{ ofRequestLite req with
|
|
||||||
history = req.history
|
|
||||||
notes = req.notes
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Functions to manipulate request actions
|
/// Functions to manipulate request actions
|
||||||
module RequestAction =
|
module RequestAction =
|
||||||
|
|
||||||
/// Create a string representation of an action
|
/// Create a string representation of an action
|
||||||
let toString =
|
let toString =
|
||||||
function
|
function
|
||||||
@@ -197,6 +98,7 @@ module RequestAction =
|
|||||||
| Prayed -> "Prayed"
|
| Prayed -> "Prayed"
|
||||||
| Updated -> "Updated"
|
| Updated -> "Updated"
|
||||||
| Answered -> "Answered"
|
| Answered -> "Answered"
|
||||||
|
|
||||||
/// Create a RequestAction from a string
|
/// Create a RequestAction from a string
|
||||||
let ofString =
|
let ofString =
|
||||||
function
|
function
|
||||||
@@ -205,9 +107,174 @@ module RequestAction =
|
|||||||
| "Updated" -> Updated
|
| "Updated" -> Updated
|
||||||
| "Answered" -> Answered
|
| "Answered" -> Answered
|
||||||
| it -> invalidOp $"Bad request action {it}"
|
| it -> invalidOp $"Bad request action {it}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// History is a record of action taken on a prayer request, including updates to its text
|
||||||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
type History =
|
||||||
|
{ /// The time when this history entry was made
|
||||||
|
AsOf : Instant
|
||||||
|
|
||||||
|
/// The status for this history entry
|
||||||
|
Status : RequestAction
|
||||||
|
|
||||||
|
/// The text of the update, if applicable
|
||||||
|
Text : string option
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Functions to manipulate history entries
|
||||||
|
module History =
|
||||||
|
|
||||||
/// Determine if a history's status is `Created`
|
/// Determine if a history's status is `Created`
|
||||||
let isCreated hist = hist.status = Created
|
let isCreated hist = hist.Status = Created
|
||||||
|
|
||||||
/// Determine if a history's status is `Prayed`
|
/// Determine if a history's status is `Prayed`
|
||||||
let isPrayed hist = hist.status = Prayed
|
let isPrayed hist = hist.Status = Prayed
|
||||||
|
|
||||||
/// Determine if a history's status is `Answered`
|
/// Determine if a history's status is `Answered`
|
||||||
let isAnswered hist = hist.status = Answered
|
let isAnswered hist = hist.Status = Answered
|
||||||
|
|
||||||
|
|
||||||
|
/// Note is a note regarding a prayer request that does not result in an update to its text
|
||||||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
type Note =
|
||||||
|
{ /// The time when this note was made
|
||||||
|
AsOf : Instant
|
||||||
|
|
||||||
|
/// The text of the notes
|
||||||
|
Notes : string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Request is the identifying record for a prayer request
|
||||||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
type Request =
|
||||||
|
{ /// The ID of the request
|
||||||
|
Id : RequestId
|
||||||
|
|
||||||
|
/// The time this request was initially entered
|
||||||
|
EnteredOn : Instant
|
||||||
|
|
||||||
|
/// The ID of the user to whom this request belongs ("sub" from the JWT)
|
||||||
|
UserId : UserId
|
||||||
|
|
||||||
|
/// The time at which this request should reappear in the user's journal by manual user choice
|
||||||
|
SnoozedUntil : Instant option
|
||||||
|
|
||||||
|
/// The time at which this request should reappear in the user's journal by recurrence
|
||||||
|
ShowAfter : Instant option
|
||||||
|
|
||||||
|
/// The recurrence for this request
|
||||||
|
Recurrence : Recurrence
|
||||||
|
|
||||||
|
/// The history entries for this request
|
||||||
|
History : History[]
|
||||||
|
|
||||||
|
/// The notes for this request
|
||||||
|
Notes : Note[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Functions to support requests
|
||||||
|
module Request =
|
||||||
|
|
||||||
|
/// An empty request
|
||||||
|
let empty =
|
||||||
|
{ Id = Cuid.generate () |> RequestId
|
||||||
|
EnteredOn = Instant.MinValue
|
||||||
|
UserId = UserId ""
|
||||||
|
SnoozedUntil = None
|
||||||
|
ShowAfter = None
|
||||||
|
Recurrence = Immediate
|
||||||
|
History = [||]
|
||||||
|
Notes = [||]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// JournalRequest is the form of a prayer request returned for the request journal display. It also contains
|
||||||
|
/// properties that may be filled for history and notes.
|
||||||
|
[<NoComparison; NoEquality>]
|
||||||
|
type JournalRequest =
|
||||||
|
{ /// The ID of the request (just the CUID part)
|
||||||
|
RequestId : RequestId
|
||||||
|
|
||||||
|
/// The ID of the user to whom the request belongs
|
||||||
|
UserId : UserId
|
||||||
|
|
||||||
|
/// The current text of the request
|
||||||
|
Text : string
|
||||||
|
|
||||||
|
/// The last time action was taken on the request
|
||||||
|
AsOf : Instant
|
||||||
|
|
||||||
|
/// The last time a request was marked as prayed
|
||||||
|
LastPrayed : Instant option
|
||||||
|
|
||||||
|
/// The last status for the request
|
||||||
|
LastStatus : RequestAction
|
||||||
|
|
||||||
|
/// The time that this request should reappear in the user's journal
|
||||||
|
SnoozedUntil : Instant option
|
||||||
|
|
||||||
|
/// The time after which this request should reappear in the user's journal by configured recurrence
|
||||||
|
ShowAfter : Instant option
|
||||||
|
|
||||||
|
/// The recurrence for this request
|
||||||
|
Recurrence : Recurrence
|
||||||
|
|
||||||
|
/// History entries for the request
|
||||||
|
History : History list
|
||||||
|
|
||||||
|
/// Note entries for the request
|
||||||
|
Notes : Note list
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Functions to manipulate journal requests
|
||||||
|
module JournalRequest =
|
||||||
|
|
||||||
|
/// Convert a request to the form used for the journal (precomputed values, no notes or history)
|
||||||
|
let ofRequestLite (req : Request) =
|
||||||
|
let lastHistory = req.History |> Array.sortByDescending (fun it -> it.AsOf) |> Array.tryHead
|
||||||
|
// Requests are sorted by the "as of" field in this record; for sorting to work properly, we will put the
|
||||||
|
// largest of the last prayed date, the "snoozed until". or the "show after" date; if none of those are filled,
|
||||||
|
// we will use the last activity date. This will mean that:
|
||||||
|
// - Immediately shown requests will be at the top of the list, in order from least recently prayed to most.
|
||||||
|
// - Non-immediate requests will enter the list as if they were marked as prayed at that time; this will put
|
||||||
|
// them at the bottom of the list.
|
||||||
|
// - Snoozed requests will reappear at the bottom of the list when they return.
|
||||||
|
// - New requests will go to the bottom of the list, but will rise as others are marked as prayed.
|
||||||
|
let lastActivity = lastHistory |> Option.map (fun it -> it.AsOf) |> Option.defaultValue Instant.MinValue
|
||||||
|
let showAfter = defaultArg req.ShowAfter Instant.MinValue
|
||||||
|
let snoozedUntil = defaultArg req.SnoozedUntil Instant.MinValue
|
||||||
|
let lastPrayed =
|
||||||
|
req.History
|
||||||
|
|> Array.sortByDescending (fun it -> it.AsOf)
|
||||||
|
|> Array.filter History.isPrayed
|
||||||
|
|> Array.tryHead
|
||||||
|
|> Option.map (fun it -> it.AsOf)
|
||||||
|
|> Option.defaultValue Instant.MinValue
|
||||||
|
let asOf = List.max [ lastPrayed; showAfter; snoozedUntil ]
|
||||||
|
{ RequestId = req.Id
|
||||||
|
UserId = req.UserId
|
||||||
|
Text = req.History
|
||||||
|
|> Array.filter (fun it -> Option.isSome it.Text)
|
||||||
|
|> Array.sortByDescending (fun it -> it.AsOf)
|
||||||
|
|> Array.tryHead
|
||||||
|
|> Option.map (fun h -> Option.get h.Text)
|
||||||
|
|> Option.defaultValue ""
|
||||||
|
AsOf = if asOf > Instant.MinValue then asOf else lastActivity
|
||||||
|
LastPrayed = if lastPrayed = Instant.MinValue then None else Some lastPrayed
|
||||||
|
LastStatus = match lastHistory with Some h -> h.Status | None -> Created
|
||||||
|
SnoozedUntil = req.SnoozedUntil
|
||||||
|
ShowAfter = req.ShowAfter
|
||||||
|
Recurrence = req.Recurrence
|
||||||
|
History = []
|
||||||
|
Notes = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `ofRequestLite`, but with notes and history
|
||||||
|
let ofRequestFull req =
|
||||||
|
{ ofRequestLite req with
|
||||||
|
History = List.ofArray req.History
|
||||||
|
Notes = List.ofArray req.Notes
|
||||||
|
}
|
||||||
+201
-225
@@ -2,23 +2,18 @@
|
|||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module MyPrayerJournal.Handlers
|
module MyPrayerJournal.Handlers
|
||||||
|
|
||||||
// fsharplint:disable RecordFieldNames
|
|
||||||
|
|
||||||
open Giraffe
|
open Giraffe
|
||||||
open Giraffe.Htmx
|
open Giraffe.Htmx
|
||||||
open Microsoft.AspNetCore.Authentication
|
|
||||||
open Microsoft.AspNetCore.Http
|
|
||||||
open System
|
open System
|
||||||
open System.Security.Claims
|
|
||||||
open NodaTime
|
|
||||||
|
|
||||||
/// Helper function to be able to split out log on
|
/// Helper function to be able to split out log on
|
||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
module private LogOnHelpers =
|
module private LogOnHelpers =
|
||||||
|
|
||||||
|
open Microsoft.AspNetCore.Authentication
|
||||||
|
|
||||||
/// Log on, optionally specifying a redirected URL once authentication is complete
|
/// Log on, optionally specifying a redirected URL once authentication is complete
|
||||||
let logOn url : HttpHandler =
|
let logOn url : HttpHandler = fun next ctx -> task {
|
||||||
fun next ctx -> backgroundTask {
|
|
||||||
match url with
|
match url with
|
||||||
| Some it ->
|
| Some it ->
|
||||||
do! ctx.ChallengeAsync ("Auth0", AuthenticationProperties (RedirectUri = it))
|
do! ctx.ChallengeAsync ("Auth0", AuthenticationProperties (RedirectUri = it))
|
||||||
@@ -26,79 +21,93 @@ module private LogOnHelpers =
|
|||||||
| None -> return! challenge "Auth0" next ctx
|
| None -> return! challenge "Auth0" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handlers for error conditions
|
/// Handlers for error conditions
|
||||||
module Error =
|
module Error =
|
||||||
|
|
||||||
open Microsoft.Extensions.Logging
|
open Microsoft.Extensions.Logging
|
||||||
open System.Threading.Tasks
|
|
||||||
|
|
||||||
/// Handle errors
|
/// Handle errors
|
||||||
let error (ex : Exception) (log : ILogger) =
|
let error (ex : Exception) (log : ILogger) =
|
||||||
log.LogError (EventId(), ex, "An unhandled exception has occurred while executing the request.")
|
log.LogError (EventId (), ex, "An unhandled exception has occurred while executing the request.")
|
||||||
clearResponse
|
clearResponse
|
||||||
>=> setStatusCode 500
|
>=> setStatusCode 500
|
||||||
>=> setHttpHeader "X-Toast" (sprintf "error|||%s: %s" (ex.GetType().Name) ex.Message)
|
>=> setHttpHeader "X-Toast" $"error|||{ex.GetType().Name}: {ex.Message}"
|
||||||
>=> text ex.Message
|
>=> text ex.Message
|
||||||
|
|
||||||
/// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized reponse
|
/// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response
|
||||||
let notAuthorized : HttpHandler =
|
let notAuthorized : HttpHandler = fun next ctx ->
|
||||||
fun next ctx ->
|
(if ctx.Request.Method = "GET" then logOn None next else setStatusCode 401 earlyReturn) ctx
|
||||||
(next, ctx)
|
|
||||||
||> match ctx.Request.Method with
|
|
||||||
| "GET" -> logOn None
|
|
||||||
| _ -> setStatusCode 401 >=> fun _ _ -> Task.FromResult<HttpContext option> None
|
|
||||||
|
|
||||||
/// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there
|
/// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there
|
||||||
let notFound : HttpHandler =
|
let notFound : HttpHandler =
|
||||||
setStatusCode 404 >=> text "Not found"
|
setStatusCode 404 >=> text "Not found"
|
||||||
|
|
||||||
|
|
||||||
/// Handler helpers
|
open System.Security.Claims
|
||||||
[<AutoOpen>]
|
open LiteDB
|
||||||
module private Helpers =
|
open Microsoft.AspNetCore.Http
|
||||||
|
open NodaTime
|
||||||
|
|
||||||
open LiteDB
|
/// Extensions on the HTTP context
|
||||||
open Microsoft.Extensions.Logging
|
type HttpContext with
|
||||||
open Microsoft.Net.Http.Headers
|
|
||||||
|
|
||||||
let debug (ctx : HttpContext) message =
|
/// The LiteDB database
|
||||||
let fac = ctx.GetService<ILoggerFactory>()
|
member this.Db = this.GetService<LiteDatabase> ()
|
||||||
let log = fac.CreateLogger "Debug"
|
|
||||||
log.LogInformation message
|
|
||||||
|
|
||||||
/// Get the LiteDB database
|
/// The "sub" for the current user (None if no user is authenticated)
|
||||||
let db (ctx : HttpContext) = ctx.GetService<LiteDatabase>()
|
member this.CurrentUser =
|
||||||
|
this.User
|
||||||
/// Get the user's "sub" claim
|
|
||||||
let user (ctx : HttpContext) =
|
|
||||||
ctx.User
|
|
||||||
|> Option.ofObj
|
|> Option.ofObj
|
||||||
|> Option.map (fun user -> user.Claims |> Seq.tryFind (fun u -> u.Type = ClaimTypes.NameIdentifier))
|
|> Option.map (fun user -> user.Claims |> Seq.tryFind (fun u -> u.Type = ClaimTypes.NameIdentifier))
|
||||||
|> Option.flatten
|
|> Option.flatten
|
||||||
|> Option.map (fun claim -> claim.Value)
|
|> Option.map (fun claim -> claim.Value)
|
||||||
|
|
||||||
/// Get the current user's ID
|
/// The current user's ID
|
||||||
// NOTE: this may raise if you don't run the request through the requiresAuthentication handler first
|
// NOTE: this may raise if you don't run the request through the requireUser handler first
|
||||||
let userId ctx =
|
member this.UserId = UserId this.CurrentUser.Value
|
||||||
(user >> Option.get) ctx |> UserId
|
|
||||||
|
|
||||||
/// Get the system clock
|
/// The system clock
|
||||||
let clock (ctx : HttpContext) =
|
member this.Clock = this.GetService<IClock> ()
|
||||||
ctx.GetService<IClock> ()
|
|
||||||
|
|
||||||
/// Get the current instant
|
/// Get the current instant from the system clock
|
||||||
let now ctx =
|
member this.Now = this.Clock.GetCurrentInstant
|
||||||
(clock ctx).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
|
||||||
|
[<AutoOpen>]
|
||||||
|
module private Helpers =
|
||||||
|
|
||||||
|
open Microsoft.Extensions.Logging
|
||||||
|
open Microsoft.Net.Http.Headers
|
||||||
|
|
||||||
|
/// Require a user to be logged on
|
||||||
|
let requireUser : HttpHandler =
|
||||||
|
requiresAuthentication Error.notAuthorized
|
||||||
|
|
||||||
|
/// Debug logger
|
||||||
|
let debug (ctx : HttpContext) message =
|
||||||
|
let fac = ctx.GetService<ILoggerFactory> ()
|
||||||
|
let log = fac.CreateLogger "Debug"
|
||||||
|
log.LogInformation message
|
||||||
|
|
||||||
/// Return a 201 CREATED response
|
/// Return a 201 CREATED response
|
||||||
let created =
|
let created =
|
||||||
setStatusCode 201
|
setStatusCode 201
|
||||||
|
|
||||||
/// Return a 201 CREATED response with the location header set for the created resource
|
/// Return a 201 CREATED response with the location header set for the created resource
|
||||||
let createdAt url : HttpHandler =
|
let createdAt url : HttpHandler = fun next ctx ->
|
||||||
fun next ctx ->
|
Successful.CREATED
|
||||||
(sprintf "%s://%s%s" ctx.Request.Scheme ctx.Request.Host.Value url |> setHttpHeader HeaderNames.Location
|
($"{ctx.Request.Scheme}://{ctx.Request.Host.Value}{url}" |> setHttpHeader HeaderNames.Location) next ctx
|
||||||
>=> created) next ctx
|
|
||||||
|
|
||||||
/// Return a 303 SEE OTHER response (forces a GET on the redirected URL)
|
/// Return a 303 SEE OTHER response (forces a GET on the redirected URL)
|
||||||
let seeOther (url : string) =
|
let seeOther (url : string) =
|
||||||
@@ -107,50 +116,50 @@ module private Helpers =
|
|||||||
/// Render a component result
|
/// Render a component result
|
||||||
let renderComponent nodes : HttpHandler =
|
let renderComponent nodes : HttpHandler =
|
||||||
noResponseCaching
|
noResponseCaching
|
||||||
>=> fun next ctx -> backgroundTask {
|
>=> fun _ ctx -> backgroundTask {
|
||||||
return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
|
return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
open Views.Layout
|
open Views.Layout
|
||||||
|
open System.Threading.Tasks
|
||||||
|
|
||||||
/// Create a page rendering context
|
/// Create a page rendering context
|
||||||
let pageContext (ctx : HttpContext) pageTitle content = backgroundTask {
|
let pageContext (ctx : HttpContext) pageTitle content = backgroundTask {
|
||||||
let! hasSnoozed = backgroundTask {
|
let! hasSnoozed =
|
||||||
match user ctx with
|
match ctx.CurrentUser with
|
||||||
| Some _ -> return! Data.hasSnoozed (userId ctx) (now ctx) (db ctx)
|
| Some _ -> Data.hasSnoozed ctx.UserId (ctx.Now ()) ctx.Db
|
||||||
| None -> return false
|
| None -> Task.FromResult false
|
||||||
}
|
return
|
||||||
return {
|
{ IsAuthenticated = Option.isSome ctx.CurrentUser
|
||||||
isAuthenticated = (user >> Option.isSome) ctx
|
HasSnoozed = hasSnoozed
|
||||||
hasSnoozed = hasSnoozed
|
CurrentUrl = ctx.Request.Path.Value
|
||||||
currentUrl = ctx.Request.Path.Value
|
PageTitle = pageTitle
|
||||||
pageTitle = pageTitle
|
Content = content
|
||||||
content = content
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Composable handler to write a view to the output
|
/// Composable handler to write a view to the output
|
||||||
let writeView view : HttpHandler =
|
let writeView view : HttpHandler = fun _ ctx -> backgroundTask {
|
||||||
fun next ctx -> backgroundTask {
|
|
||||||
return! ctx.WriteHtmlViewAsync view
|
return! ctx.WriteHtmlViewAsync view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Hold messages across redirects
|
/// Hold messages across redirects
|
||||||
module Messages =
|
module Messages =
|
||||||
|
|
||||||
/// The messages being held
|
/// The messages being held
|
||||||
let mutable private messages : Map<string, (string * string)> = Map.empty
|
let mutable private messages : Map<UserId, string * string> = Map.empty
|
||||||
|
|
||||||
/// Locked update to prevent updates by multiple threads
|
/// Locked update to prevent updates by multiple threads
|
||||||
let private upd8 = obj ()
|
let private upd8 = obj ()
|
||||||
|
|
||||||
/// Push a new message into the list
|
/// Push a new message into the list
|
||||||
let push ctx message url = lock upd8 (fun () ->
|
let push (ctx : HttpContext) message url = lock upd8 (fun () ->
|
||||||
messages <- messages.Add (ctx |> (user >> Option.get), (message, url)))
|
messages <- messages.Add (ctx.UserId, (message, url)))
|
||||||
|
|
||||||
/// Add a success message header to the response
|
/// Add a success message header to the response
|
||||||
let pushSuccess ctx message url =
|
let pushSuccess ctx message url =
|
||||||
push ctx (sprintf "success|||%s" message) url
|
push ctx $"success|||%s{message}" url
|
||||||
|
|
||||||
/// Pop the messages for the given user
|
/// Pop the messages for the given user
|
||||||
let pop userId = lock upd8 (fun () ->
|
let pop userId = lock upd8 (fun () ->
|
||||||
@@ -159,17 +168,16 @@ module private Helpers =
|
|||||||
msg)
|
msg)
|
||||||
|
|
||||||
/// Send a partial result if this is not a full page load (does not append no-cache headers)
|
/// Send a partial result if this is not a full page load (does not append no-cache headers)
|
||||||
let partialStatic (pageTitle : string) content : HttpHandler =
|
let partialStatic (pageTitle : string) content : HttpHandler = fun next ctx -> task {
|
||||||
fun next ctx -> backgroundTask {
|
|
||||||
let isPartial = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
|
let isPartial = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
|
||||||
let! pageCtx = pageContext ctx pageTitle content
|
let! pageCtx = pageContext ctx pageTitle content
|
||||||
let view = (match isPartial with true -> partial | false -> view) pageCtx
|
let view = (match isPartial with true -> partial | false -> view) pageCtx
|
||||||
return!
|
return!
|
||||||
(next, ctx)
|
(next, ctx)
|
||||||
||> match user ctx with
|
||> match ctx.CurrentUser with
|
||||||
| Some u ->
|
| Some _ ->
|
||||||
match Messages.pop u with
|
match Messages.pop ctx.UserId with
|
||||||
| Some (msg, url) -> setHttpHeader "X-Toast" msg >=> withHxPush url >=> writeView view
|
| Some (msg, url) -> setHttpHeader "X-Toast" msg >=> withHxPushUrl url >=> writeView view
|
||||||
| None -> writeView view
|
| None -> writeView view
|
||||||
| None -> writeView view
|
| None -> writeView view
|
||||||
}
|
}
|
||||||
@@ -192,34 +200,40 @@ module Models =
|
|||||||
|
|
||||||
/// An additional note
|
/// An additional note
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type NoteEntry = {
|
type NoteEntry =
|
||||||
/// The notes being added
|
{ /// The notes being added
|
||||||
notes : string
|
notes : string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A prayer request
|
/// A prayer request
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type Request = {
|
type Request =
|
||||||
/// The ID of the request
|
{ /// The ID of the request
|
||||||
requestId : string
|
requestId : string
|
||||||
|
|
||||||
/// Where to redirect after saving
|
/// Where to redirect after saving
|
||||||
returnTo : string
|
returnTo : string
|
||||||
|
|
||||||
/// The text of the request
|
/// The text of the request
|
||||||
requestText : string
|
requestText : string
|
||||||
|
|
||||||
/// The additional status to record
|
/// The additional status to record
|
||||||
status : string option
|
status : string option
|
||||||
|
|
||||||
/// The recurrence type
|
/// The recurrence type
|
||||||
recurType : string
|
recurType : string
|
||||||
|
|
||||||
/// The recurrence count
|
/// The recurrence count
|
||||||
recurCount : int16 option
|
recurCount : int16 option
|
||||||
|
|
||||||
/// The recurrence interval
|
/// The recurrence interval
|
||||||
recurInterval : string option
|
recurInterval : string option
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The date until which a request should not appear in the journal
|
/// The date until which a request should not appear in the journal
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type SnoozeUntil = {
|
type SnoozeUntil =
|
||||||
/// The date (YYYY-MM-DD) at which the request should reappear
|
{ /// The date (YYYY-MM-DD) at which the request should reappear
|
||||||
until : string
|
until : string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,41 +245,40 @@ open NodaTime.Text
|
|||||||
module Components =
|
module Components =
|
||||||
|
|
||||||
// GET /components/journal-items
|
// GET /components/journal-items
|
||||||
let journalItems : HttpHandler =
|
let journalItems : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
let now = ctx.Now ()
|
||||||
>=> fun next ctx -> backgroundTask {
|
let shouldBeShown (req : JournalRequest) =
|
||||||
let now = now ctx
|
match req.SnoozedUntil, req.ShowAfter with
|
||||||
let! jrnl = Data.journalByUserId (userId ctx) (db ctx)
|
| None, None -> true
|
||||||
let shown = jrnl |> List.filter (fun it -> now > it.snoozedUntil && now > it.showAfter)
|
| Some snooze, Some hide when snooze < now && hide < now -> true
|
||||||
return! renderComponent [ Views.Journal.journalItems now shown ] next ctx
|
| Some snooze, _ when snooze < now -> true
|
||||||
|
| _, Some hide when hide < now -> true
|
||||||
|
| _, _ -> false
|
||||||
|
let! journal = Data.journalByUserId ctx.UserId ctx.Db
|
||||||
|
let shown = journal |> List.filter shouldBeShown
|
||||||
|
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 =
|
let requestItem reqId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
match! Data.tryJournalById (RequestId.ofString reqId) ctx.UserId ctx.Db with
|
||||||
>=> fun next ctx -> backgroundTask {
|
| Some req -> return! renderComponent [ Views.Request.reqListItem (ctx.Now ()) ctx.TimeZone req ] next ctx
|
||||||
match! Data.tryJournalById (RequestId.ofString reqId) (userId ctx) (db ctx) with
|
|
||||||
| Some req -> return! renderComponent [ Views.Request.reqListItem (now ctx) req ] next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /components/request/[req-id]/add-notes
|
// GET /components/request/[req-id]/add-notes
|
||||||
let addNotes requestId : HttpHandler =
|
let addNotes requestId : HttpHandler =
|
||||||
requiresAuthentication Error.notAuthorized
|
requireUser >=> renderComponent (Views.Journal.notesEdit (RequestId.ofString requestId))
|
||||||
>=> renderComponent (Views.Journal.notesEdit (RequestId.ofString requestId))
|
|
||||||
|
|
||||||
// GET /components/request/[req-id]/notes
|
// GET /components/request/[req-id]/notes
|
||||||
let notes requestId : HttpHandler =
|
let notes requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
let! notes = Data.notesById (RequestId.ofString requestId) ctx.UserId ctx.Db
|
||||||
>=> fun next ctx -> backgroundTask {
|
return! renderComponent (Views.Request.notes (ctx.Now ()) ctx.TimeZone (List.ofArray notes)) next ctx
|
||||||
let! notes = Data.notesById (RequestId.ofString requestId) (userId ctx) (db ctx)
|
|
||||||
return! renderComponent (Views.Request.notes (now ctx) notes) next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /components/request/[req-id]/snooze
|
// GET /components/request/[req-id]/snooze
|
||||||
let snooze requestId : HttpHandler =
|
let snooze requestId : HttpHandler =
|
||||||
requiresAuthentication Error.notAuthorized
|
requireUser >=> renderComponent [ RequestId.ofString requestId |> Views.Journal.snooze ]
|
||||||
>=> renderComponent [ RequestId.ofString requestId |> Views.Journal.snooze ]
|
|
||||||
|
|
||||||
|
|
||||||
/// / URL
|
/// / URL
|
||||||
@@ -280,16 +293,14 @@ module Home =
|
|||||||
module Journal =
|
module Journal =
|
||||||
|
|
||||||
// GET /journal
|
// GET /journal
|
||||||
let journal : HttpHandler =
|
let journal : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
|
||||||
>=> fun next ctx -> backgroundTask {
|
|
||||||
let usr =
|
let usr =
|
||||||
ctx.User.Claims
|
ctx.User.Claims
|
||||||
|> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName)
|
|> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName)
|
||||||
|> Option.map (fun c -> c.Value)
|
|> Option.map (fun c -> c.Value)
|
||||||
|> Option.defaultValue "Your"
|
|> Option.defaultValue "Your"
|
||||||
let title = usr |> match usr with "Your" -> sprintf "%s" | _ -> sprintf "%s's"
|
let title = usr |> match usr with "Your" -> sprintf "%s" | _ -> sprintf "%s's"
|
||||||
return! partial (sprintf "%s Prayer Journal" title) (Views.Journal.journal usr) next ctx
|
return! partial $"{title} Prayer Journal" (Views.Journal.journal usr) next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -309,11 +320,9 @@ module Legal =
|
|||||||
module Request =
|
module Request =
|
||||||
|
|
||||||
// GET /request/[req-id]/edit
|
// GET /request/[req-id]/edit
|
||||||
let edit requestId : HttpHandler =
|
let edit requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
|
||||||
>=> fun next ctx -> backgroundTask {
|
|
||||||
let returnTo =
|
let returnTo =
|
||||||
match ctx.Request.Headers.Referer.[0] with
|
match ctx.Request.Headers.Referer[0] with
|
||||||
| it when it.EndsWith "/active" -> "active"
|
| it when it.EndsWith "/active" -> "active"
|
||||||
| it when it.EndsWith "/snoozed" -> "snoozed"
|
| it when it.EndsWith "/snoozed" -> "snoozed"
|
||||||
| _ -> "journal"
|
| _ -> "journal"
|
||||||
@@ -322,7 +331,7 @@ module Request =
|
|||||||
return! partial "Add Prayer Request"
|
return! partial "Add Prayer Request"
|
||||||
(Views.Request.edit (JournalRequest.ofRequestLite Request.empty) returnTo true) next ctx
|
(Views.Request.edit (JournalRequest.ofRequestLite Request.empty) returnTo true) next ctx
|
||||||
| _ ->
|
| _ ->
|
||||||
match! Data.tryJournalById (RequestId.ofString requestId) (userId ctx) (db ctx) with
|
match! Data.tryJournalById (RequestId.ofString requestId) ctx.UserId ctx.Db with
|
||||||
| Some req ->
|
| Some req ->
|
||||||
debug ctx "Found - sending view"
|
debug ctx "Found - sending view"
|
||||||
return! partial "Edit Prayer Request" (Views.Request.edit req returnTo false) next ctx
|
return! partial "Edit Prayer Request" (Views.Request.edit req returnTo false) next ctx
|
||||||
@@ -332,117 +341,93 @@ module Request =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PATCH /request/[req-id]/prayed
|
// PATCH /request/[req-id]/prayed
|
||||||
let prayed requestId : HttpHandler =
|
let prayed requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
let db = ctx.Db
|
||||||
>=> fun next ctx -> backgroundTask {
|
let userId = ctx.UserId
|
||||||
let db = db ctx
|
|
||||||
let usrId = userId ctx
|
|
||||||
let reqId = RequestId.ofString requestId
|
let reqId = RequestId.ofString requestId
|
||||||
match! Data.tryRequestById reqId usrId db with
|
match! Data.tryRequestById reqId userId db with
|
||||||
| Some req ->
|
| Some req ->
|
||||||
let now = now ctx
|
let now = ctx.Now ()
|
||||||
do! Data.addHistory reqId usrId { asOf = now; status = Prayed; text = None } db
|
do! Data.addHistory reqId userId { AsOf = now; Status = Prayed; Text = None } db
|
||||||
let nextShow =
|
let nextShow =
|
||||||
match Recurrence.duration req.recurType with
|
match Recurrence.duration req.Recurrence with
|
||||||
| 0L -> Instant.MinValue
|
| 0L -> None
|
||||||
| duration -> now.Plus (Duration.FromSeconds (duration * int64 req.recurCount))
|
| duration -> Some <| now.Plus (Duration.FromSeconds duration)
|
||||||
do! Data.updateShowAfter reqId usrId nextShow db
|
do! Data.updateShowAfter reqId userId nextShow db
|
||||||
do! db.saveChanges ()
|
do! db.SaveChanges ()
|
||||||
return! (withSuccessMessage "Request marked as prayed" >=> Components.journalItems) next ctx
|
return! (withSuccessMessage "Request marked as prayed" >=> Components.journalItems) next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /request/[req-id]/note
|
/// POST /request/[req-id]/note
|
||||||
let addNote requestId : HttpHandler =
|
let addNote requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
let db = ctx.Db
|
||||||
>=> fun next ctx -> backgroundTask {
|
let userId = ctx.UserId
|
||||||
let db = db ctx
|
|
||||||
let usrId = userId ctx
|
|
||||||
let reqId = RequestId.ofString requestId
|
let reqId = RequestId.ofString requestId
|
||||||
match! Data.tryRequestById reqId usrId db with
|
match! Data.tryRequestById reqId userId db with
|
||||||
| Some _ ->
|
| Some _ ->
|
||||||
let! notes = ctx.BindFormAsync<Models.NoteEntry> ()
|
let! notes = ctx.BindFormAsync<Models.NoteEntry> ()
|
||||||
do! Data.addNote reqId usrId { asOf = now ctx; notes = notes.notes } db
|
do! Data.addNote reqId userId { AsOf = ctx.Now (); Notes = notes.notes } db
|
||||||
do! db.saveChanges ()
|
do! db.SaveChanges ()
|
||||||
return! (withSuccessMessage "Added Notes" >=> hideModal "notes" >=> created) next ctx
|
return! (withSuccessMessage "Added Notes" >=> hideModal "notes" >=> created) next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /requests/active
|
// GET /requests/active
|
||||||
let active : HttpHandler =
|
let active : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
let! reqs = Data.journalByUserId ctx.UserId ctx.Db
|
||||||
>=> fun next ctx -> backgroundTask {
|
return! partial "Active Requests" (Views.Request.active (ctx.Now ()) ctx.TimeZone reqs) next ctx
|
||||||
let! reqs = Data.journalByUserId (userId ctx) (db ctx)
|
|
||||||
return! partial "Active Requests" (Views.Request.active (now ctx) reqs) next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /requests/snoozed
|
// GET /requests/snoozed
|
||||||
let snoozed : HttpHandler =
|
let snoozed : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
let! reqs = Data.journalByUserId ctx.UserId ctx.Db
|
||||||
>=> fun next ctx -> backgroundTask {
|
let now = ctx.Now ()
|
||||||
let! reqs = Data.journalByUserId (userId ctx) (db ctx)
|
let snoozed = reqs
|
||||||
let now = now ctx
|
|> List.filter (fun it -> defaultArg (it.SnoozedUntil |> Option.map (fun it -> it > now)) false)
|
||||||
let snoozed = reqs |> List.filter (fun it -> it.snoozedUntil > now)
|
return! partial "Snoozed Requests" (Views.Request.snoozed now ctx.TimeZone snoozed) next ctx
|
||||||
return! partial "Active Requests" (Views.Request.snoozed now snoozed) next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /requests/answered
|
// GET /requests/answered
|
||||||
let answered : HttpHandler =
|
let answered : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
let! reqs = Data.answeredRequests ctx.UserId ctx.Db
|
||||||
>=> fun next ctx -> backgroundTask {
|
return! partial "Answered Requests" (Views.Request.answered (ctx.Now ()) ctx.TimeZone reqs) next ctx
|
||||||
let! reqs = Data.answeredRequests (userId ctx) (db ctx)
|
|
||||||
return! partial "Answered Requests" (Views.Request.answered (now ctx) reqs) next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/request/[req-id]
|
|
||||||
let get requestId : HttpHandler =
|
|
||||||
requiresAuthentication Error.notAuthorized
|
|
||||||
>=> fun next ctx -> backgroundTask {
|
|
||||||
match! Data.tryJournalById (RequestId.ofString requestId) (userId ctx) (db ctx) with
|
|
||||||
| Some req -> return! json req next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /request/[req-id]/full
|
// GET /request/[req-id]/full
|
||||||
let getFull requestId : HttpHandler =
|
let getFull requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
match! Data.tryFullRequestById (RequestId.ofString requestId) ctx.UserId ctx.Db with
|
||||||
>=> fun next ctx -> backgroundTask {
|
| Some req -> return! partial "Prayer Request" (Views.Request.full ctx.Clock ctx.TimeZone req) next ctx
|
||||||
match! Data.tryFullRequestById (RequestId.ofString requestId) (userId ctx) (db ctx) with
|
|
||||||
| Some req -> return! partial "Prayer Request" (Views.Request.full (clock ctx) req) next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH /request/[req-id]/show
|
// PATCH /request/[req-id]/show
|
||||||
let show requestId : HttpHandler =
|
let show requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
let db = ctx.Db
|
||||||
>=> fun next ctx -> backgroundTask {
|
let userId = ctx.UserId
|
||||||
let db = db ctx
|
|
||||||
let usrId = userId ctx
|
|
||||||
let reqId = RequestId.ofString requestId
|
let reqId = RequestId.ofString requestId
|
||||||
match! Data.tryRequestById reqId usrId db with
|
match! Data.tryRequestById reqId userId db with
|
||||||
| Some _ ->
|
| Some _ ->
|
||||||
do! Data.updateShowAfter reqId usrId Instant.MinValue db
|
do! Data.updateShowAfter reqId userId None db
|
||||||
do! db.saveChanges ()
|
do! db.SaveChanges ()
|
||||||
return! (withSuccessMessage "Request now shown" >=> Components.requestItem requestId) next ctx
|
return! (withSuccessMessage "Request now shown" >=> Components.requestItem requestId) next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH /request/[req-id]/snooze
|
// PATCH /request/[req-id]/snooze
|
||||||
let snooze requestId : HttpHandler =
|
let snooze requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
let db = ctx.Db
|
||||||
>=> fun next ctx -> backgroundTask {
|
let userId = ctx.UserId
|
||||||
let db = db ctx
|
|
||||||
let usrId = userId ctx
|
|
||||||
let reqId = RequestId.ofString requestId
|
let reqId = RequestId.ofString requestId
|
||||||
match! Data.tryRequestById reqId usrId db with
|
match! Data.tryRequestById reqId userId db with
|
||||||
| Some _ ->
|
| Some _ ->
|
||||||
let! until = ctx.BindFormAsync<Models.SnoozeUntil> ()
|
let! until = ctx.BindFormAsync<Models.SnoozeUntil> ()
|
||||||
let date =
|
let date =
|
||||||
LocalDatePattern.CreateWithInvariantCulture("yyyy-MM-dd").Parse(until.until).Value
|
LocalDatePattern.CreateWithInvariantCulture("yyyy-MM-dd").Parse(until.until).Value
|
||||||
.AtStartOfDayInZone(DateTimeZone.Utc)
|
.AtStartOfDayInZone(DateTimeZone.Utc)
|
||||||
.ToInstant ()
|
.ToInstant ()
|
||||||
do! Data.updateSnoozed reqId usrId date db
|
do! Data.updateSnoozed reqId userId (Some date) db
|
||||||
do! db.saveChanges ()
|
do! db.SaveChanges ()
|
||||||
return!
|
return!
|
||||||
(withSuccessMessage $"Request snoozed until {until.until}"
|
(withSuccessMessage $"Request snoozed until {until.until}"
|
||||||
>=> hideModal "snooze"
|
>=> hideModal "snooze"
|
||||||
@@ -451,78 +436,70 @@ module Request =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PATCH /request/[req-id]/cancel-snooze
|
// PATCH /request/[req-id]/cancel-snooze
|
||||||
let cancelSnooze requestId : HttpHandler =
|
let cancelSnooze requestId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
let db = ctx.Db
|
||||||
>=> fun next ctx -> backgroundTask {
|
let userId = ctx.UserId
|
||||||
let db = db ctx
|
|
||||||
let usrId = userId ctx
|
|
||||||
let reqId = RequestId.ofString requestId
|
let reqId = RequestId.ofString requestId
|
||||||
match! Data.tryRequestById reqId usrId db with
|
match! Data.tryRequestById reqId userId db with
|
||||||
| Some _ ->
|
| Some _ ->
|
||||||
do! Data.updateSnoozed reqId usrId Instant.MinValue db
|
do! Data.updateSnoozed reqId userId None db
|
||||||
do! db.saveChanges ()
|
do! db.SaveChanges ()
|
||||||
return! (withSuccessMessage "Request unsnoozed" >=> Components.requestItem requestId) next ctx
|
return! (withSuccessMessage "Request unsnoozed" >=> Components.requestItem requestId) next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive a recurrence and interval from its primitive representation in the form
|
/// Derive a recurrence from its representation in the form
|
||||||
let private parseRecurrence (form : Models.Request) =
|
let private parseRecurrence (form : Models.Request) =
|
||||||
(Recurrence.ofString (match form.recurInterval with Some x -> x | _ -> "Immediate"),
|
match form.recurInterval with Some x -> $"{defaultArg form.recurCount 0s} {x}" | None -> "Immediate"
|
||||||
defaultArg form.recurCount (int16 0))
|
|> Recurrence.ofString
|
||||||
|
|
||||||
// POST /request
|
// POST /request
|
||||||
let add : HttpHandler =
|
let add : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
|
||||||
>=> fun next ctx -> backgroundTask {
|
|
||||||
let! form = ctx.BindModelAsync<Models.Request> ()
|
let! form = ctx.BindModelAsync<Models.Request> ()
|
||||||
let db = db ctx
|
let db = ctx.Db
|
||||||
let usrId = userId ctx
|
let userId = ctx.UserId
|
||||||
let now = now ctx
|
let now = ctx.Now ()
|
||||||
let (recur, interval) = parseRecurrence form
|
|
||||||
let req =
|
let req =
|
||||||
{ Request.empty with
|
{ Request.empty with
|
||||||
userId = usrId
|
UserId = userId
|
||||||
enteredOn = now
|
EnteredOn = now
|
||||||
showAfter = Instant.MinValue
|
ShowAfter = None
|
||||||
recurType = recur
|
Recurrence = parseRecurrence form
|
||||||
recurCount = interval
|
History = [|
|
||||||
history = [
|
{ AsOf = now
|
||||||
{ asOf = now
|
Status = Created
|
||||||
status = Created
|
Text = Some form.requestText
|
||||||
text = Some form.requestText
|
|
||||||
}
|
}
|
||||||
]
|
|]
|
||||||
}
|
}
|
||||||
Data.addRequest req db
|
Data.addRequest req db
|
||||||
do! db.saveChanges ()
|
do! db.SaveChanges ()
|
||||||
Messages.pushSuccess ctx "Added prayer request" "/journal"
|
Messages.pushSuccess ctx "Added prayer request" "/journal"
|
||||||
return! seeOther "/journal" next ctx
|
return! seeOther "/journal" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH /request
|
// PATCH /request
|
||||||
let update : HttpHandler =
|
let update : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
|
||||||
>=> fun next ctx -> backgroundTask {
|
|
||||||
let! form = ctx.BindModelAsync<Models.Request> ()
|
let! form = ctx.BindModelAsync<Models.Request> ()
|
||||||
let db = db ctx
|
let db = ctx.Db
|
||||||
let usrId = userId ctx
|
let userId = ctx.UserId
|
||||||
match! Data.tryJournalById (RequestId.ofString form.requestId) usrId db with
|
match! Data.tryJournalById (RequestId.ofString form.requestId) userId db with
|
||||||
| Some req ->
|
| Some req ->
|
||||||
// update recurrence if changed
|
// update recurrence if changed
|
||||||
let (recur, interval) = parseRecurrence form
|
let recur = parseRecurrence form
|
||||||
match recur = req.recurType && interval = req.recurCount with
|
match recur = req.Recurrence with
|
||||||
| true -> ()
|
| true -> ()
|
||||||
| false ->
|
| false ->
|
||||||
do! Data.updateRecurrence req.requestId usrId recur interval db
|
do! Data.updateRecurrence req.RequestId userId recur db
|
||||||
match recur with
|
match recur with
|
||||||
| Immediate -> do! Data.updateShowAfter req.requestId usrId Instant.MinValue db
|
| Immediate -> do! Data.updateShowAfter req.RequestId userId None db
|
||||||
| _ -> ()
|
| _ -> ()
|
||||||
// append history
|
// append history
|
||||||
let upd8Text = form.requestText.Trim ()
|
let upd8Text = form.requestText.Trim ()
|
||||||
let text = match upd8Text = req.text with true -> None | false -> Some upd8Text
|
let text = if upd8Text = req.Text then None else Some upd8Text
|
||||||
do! Data.addHistory req.requestId usrId
|
do! Data.addHistory req.RequestId userId
|
||||||
{ asOf = now ctx; status = (Option.get >> RequestAction.ofString) form.status; text = text } db
|
{ AsOf = ctx.Now (); Status = (Option.get >> RequestAction.ofString) form.status; Text = text } db
|
||||||
do! db.saveChanges ()
|
do! db.SaveChanges ()
|
||||||
let nextUrl =
|
let nextUrl =
|
||||||
match form.returnTo with
|
match form.returnTo with
|
||||||
| "active" -> "/requests/active"
|
| "active" -> "/requests/active"
|
||||||
@@ -537,6 +514,7 @@ module Request =
|
|||||||
/// Handlers for /user URLs
|
/// Handlers for /user URLs
|
||||||
module User =
|
module User =
|
||||||
|
|
||||||
|
open Microsoft.AspNetCore.Authentication
|
||||||
open Microsoft.AspNetCore.Authentication.Cookies
|
open Microsoft.AspNetCore.Authentication.Cookies
|
||||||
|
|
||||||
// GET /user/log-on
|
// GET /user/log-on
|
||||||
@@ -544,9 +522,7 @@ module User =
|
|||||||
logOn (Some "/journal")
|
logOn (Some "/journal")
|
||||||
|
|
||||||
// GET /user/log-off
|
// GET /user/log-off
|
||||||
let logOff : HttpHandler =
|
let logOff : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
requiresAuthentication Error.notAuthorized
|
|
||||||
>=> fun next ctx -> task {
|
|
||||||
do! ctx.SignOutAsync ("Auth0", AuthenticationProperties (RedirectUri = "/"))
|
do! ctx.SignOutAsync ("Auth0", AuthenticationProperties (RedirectUri = "/"))
|
||||||
do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme
|
do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme
|
||||||
return! next ctx
|
return! next ctx
|
||||||
@@ -556,8 +532,8 @@ module User =
|
|||||||
open Giraffe.EndpointRouting
|
open Giraffe.EndpointRouting
|
||||||
|
|
||||||
/// The routes for myPrayerJournal
|
/// The routes for myPrayerJournal
|
||||||
let routes =
|
let routes = [
|
||||||
[ GET_HEAD [ route "/" Home.home ]
|
GET_HEAD [ route "/" Home.home ]
|
||||||
subRoute "/components/" [
|
subRoute "/components/" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
route "journal-items" Components.journalItems
|
route "journal-items" Components.journalItems
|
||||||
@@ -600,4 +576,4 @@ let routes =
|
|||||||
route "log-on" User.logOn
|
route "log-on" User.logOn
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Version>3.0.0.0</Version>
|
<Version>3.1.0</Version>
|
||||||
|
<NoWarn>3391</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Domain.fs" />
|
<Compile Include="Domain.fs" />
|
||||||
@@ -16,14 +17,14 @@
|
|||||||
<Compile Include="Program.fs" />
|
<Compile Include="Program.fs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FSharp.SystemTextJson" Version="0.17.4" />
|
<PackageReference Include="FSharp.SystemTextJson" Version="0.19.13" />
|
||||||
<PackageReference Include="FunctionalCuid" Version="1.0.0" />
|
<PackageReference Include="FunctionalCuid" Version="1.0.0" />
|
||||||
<PackageReference Include="Giraffe" Version="5.0.0" />
|
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||||
<PackageReference Include="Giraffe.Htmx" Version="0.9.2" />
|
<PackageReference Include="Giraffe.Htmx" Version="1.8.0" />
|
||||||
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="0.9.2" />
|
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.8.0" />
|
||||||
<PackageReference Include="LiteDB" Version="5.0.11" />
|
<PackageReference Include="LiteDB" Version="5.0.12" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.7" />
|
||||||
<PackageReference Include="NodaTime" Version="3.0.9" />
|
<PackageReference Include="NodaTime" Version="3.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="wwwroot\" />
|
<Folder Include="wwwroot\" />
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ module Configure =
|
|||||||
|
|
||||||
/// Configure logging
|
/// Configure logging
|
||||||
let logging (bldr : WebApplicationBuilder) =
|
let logging (bldr : WebApplicationBuilder) =
|
||||||
match bldr.Environment.IsDevelopment () with
|
if bldr.Environment.IsDevelopment () then bldr.Logging.AddFilter (fun l -> l > LogLevel.Information) |> ignore
|
||||||
| true -> ()
|
|
||||||
| false -> bldr.Logging.AddFilter (fun l -> l > LogLevel.Information) |> ignore
|
|
||||||
bldr.Logging.AddConsole().AddDebug() |> ignore
|
bldr.Logging.AddConsole().AddDebug() |> ignore
|
||||||
bldr
|
bldr
|
||||||
|
|
||||||
@@ -74,27 +72,26 @@ module Configure =
|
|||||||
| SameSiteMode.None, false -> opts.SameSite <- SameSiteMode.Unspecified
|
| SameSiteMode.None, false -> opts.SameSite <- SameSiteMode.Unspecified
|
||||||
| _, _ -> ()
|
| _, _ -> ()
|
||||||
|
|
||||||
bldr.Services
|
let _ = bldr.Services.AddRouting ()
|
||||||
.AddRouting()
|
let _ = bldr.Services.AddGiraffe ()
|
||||||
.AddGiraffe()
|
let _ = bldr.Services.AddSingleton<IClock> SystemClock.Instance
|
||||||
.AddSingleton<IClock>(SystemClock.Instance)
|
let _ = bldr.Services.AddSingleton<IDateTimeZoneProvider> DateTimeZoneProviders.Tzdb
|
||||||
.Configure<CookiePolicyOptions>(
|
|
||||||
fun (opts : CookiePolicyOptions) ->
|
let _ =
|
||||||
|
bldr.Services.Configure<CookiePolicyOptions>(fun (opts : CookiePolicyOptions) ->
|
||||||
opts.MinimumSameSitePolicy <- SameSiteMode.Unspecified
|
opts.MinimumSameSitePolicy <- SameSiteMode.Unspecified
|
||||||
opts.OnAppendCookie <- fun ctx -> sameSite ctx.CookieOptions
|
opts.OnAppendCookie <- fun ctx -> sameSite ctx.CookieOptions
|
||||||
opts.OnDeleteCookie <- fun ctx -> sameSite ctx.CookieOptions)
|
opts.OnDeleteCookie <- fun ctx -> sameSite ctx.CookieOptions)
|
||||||
.AddAuthentication(
|
let _ =
|
||||||
/// Use HTTP "Bearer" authentication with JWTs
|
bldr.Services.AddAuthentication(fun opts ->
|
||||||
fun opts ->
|
|
||||||
opts.DefaultAuthenticateScheme <- CookieAuthenticationDefaults.AuthenticationScheme
|
opts.DefaultAuthenticateScheme <- CookieAuthenticationDefaults.AuthenticationScheme
|
||||||
opts.DefaultSignInScheme <- CookieAuthenticationDefaults.AuthenticationScheme
|
opts.DefaultSignInScheme <- CookieAuthenticationDefaults.AuthenticationScheme
|
||||||
opts.DefaultChallengeScheme <- CookieAuthenticationDefaults.AuthenticationScheme)
|
opts.DefaultChallengeScheme <- CookieAuthenticationDefaults.AuthenticationScheme)
|
||||||
.AddCookie()
|
.AddCookie()
|
||||||
.AddOpenIdConnect("Auth0",
|
.AddOpenIdConnect("Auth0", fun opts ->
|
||||||
/// Configure OIDC with Auth0 options from configuration
|
// Configure OIDC with Auth0 options from configuration
|
||||||
fun opts ->
|
|
||||||
let cfg = bldr.Configuration.GetSection "Auth0"
|
let cfg = bldr.Configuration.GetSection "Auth0"
|
||||||
opts.Authority <- sprintf "https://%s/" cfg["Domain"]
|
opts.Authority <- $"""https://{cfg["Domain"]}/"""
|
||||||
opts.ClientId <- cfg["Id"]
|
opts.ClientId <- cfg["Id"]
|
||||||
opts.ClientSecret <- cfg["Secret"]
|
opts.ClientSecret <- cfg["Secret"]
|
||||||
opts.ResponseType <- OpenIdConnectResponseType.Code
|
opts.ResponseType <- OpenIdConnectResponseType.Code
|
||||||
@@ -118,30 +115,27 @@ module Configure =
|
|||||||
| true ->
|
| true ->
|
||||||
// transform to absolute
|
// transform to absolute
|
||||||
let request = ctx.Request
|
let request = ctx.Request
|
||||||
sprintf "%s://%s%s%s" request.Scheme request.Host.Value request.PathBase.Value redirUri
|
$"{request.Scheme}://{request.Host.Value}{request.PathBase.Value}{redirUri}"
|
||||||
| false -> redirUri
|
| false -> redirUri
|
||||||
Uri.EscapeDataString finalRedirUri |> sprintf "&returnTo=%s"
|
Uri.EscapeDataString $"&returnTo={finalRedirUri}"
|
||||||
sprintf "https://%s/v2/logout?client_id=%s%s" cfg["Domain"] cfg["Id"] returnTo
|
ctx.Response.Redirect $"""https://{cfg["Domain"]}/v2/logout?client_id={cfg["Id"]}{returnTo}"""
|
||||||
|> ctx.Response.Redirect
|
|
||||||
ctx.HandleResponse ()
|
ctx.HandleResponse ()
|
||||||
|
|
||||||
Task.CompletedTask
|
Task.CompletedTask
|
||||||
opts.Events.OnRedirectToIdentityProvider <- fun ctx ->
|
opts.Events.OnRedirectToIdentityProvider <- fun ctx ->
|
||||||
let bldr = UriBuilder ctx.ProtocolMessage.RedirectUri
|
let bldr = UriBuilder ctx.ProtocolMessage.RedirectUri
|
||||||
bldr.Scheme <- cfg["Scheme"]
|
bldr.Scheme <- cfg["Scheme"]
|
||||||
bldr.Port <- int cfg["Port"]
|
bldr.Port <- int cfg["Port"]
|
||||||
ctx.ProtocolMessage.RedirectUri <- string bldr
|
ctx.ProtocolMessage.RedirectUri <- string bldr
|
||||||
Task.CompletedTask
|
Task.CompletedTask)
|
||||||
)
|
|
||||||
|> ignore
|
|
||||||
let jsonOptions = JsonSerializerOptions ()
|
let jsonOptions = JsonSerializerOptions ()
|
||||||
jsonOptions.Converters.Add (JsonFSharpConverter ())
|
jsonOptions.Converters.Add (JsonFSharpConverter ())
|
||||||
let db = new LiteDatabase (bldr.Configuration.GetConnectionString "db")
|
let db = new LiteDatabase (bldr.Configuration.GetConnectionString "db")
|
||||||
Data.Startup.ensureDb db
|
Data.Startup.ensureDb db
|
||||||
bldr.Services.AddSingleton(jsonOptions)
|
let _ = bldr.Services.AddSingleton jsonOptions
|
||||||
.AddSingleton<Json.ISerializer, SystemTextJson.Serializer>()
|
let _ = bldr.Services.AddSingleton<Json.ISerializer, SystemTextJson.Serializer> ()
|
||||||
.AddSingleton<LiteDatabase> db
|
let _ = bldr.Services.AddSingleton<LiteDatabase> db
|
||||||
|> ignore
|
|
||||||
bldr.Build ()
|
bldr.Build ()
|
||||||
|
|
||||||
|
|
||||||
@@ -149,18 +143,12 @@ module Configure =
|
|||||||
|
|
||||||
/// Configure the web application
|
/// Configure the web application
|
||||||
let application (app : WebApplication) =
|
let application (app : WebApplication) =
|
||||||
// match app.Environment.IsDevelopment () with
|
let _ = app.UseStaticFiles ()
|
||||||
// | true -> app.UseDeveloperExceptionPage ()
|
let _ = app.UseCookiePolicy ()
|
||||||
// | false -> app.UseGiraffeErrorHandler Handlers.Error.error
|
let _ = app.UseRouting ()
|
||||||
// |> ignore
|
let _ = app.UseAuthentication ()
|
||||||
app
|
let _ = app.UseGiraffeErrorHandler Handlers.Error.error
|
||||||
.UseStaticFiles()
|
let _ = app.UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.routes)
|
||||||
.UseCookiePolicy()
|
|
||||||
.UseRouting()
|
|
||||||
.UseAuthentication()
|
|
||||||
.UseGiraffeErrorHandler(Handlers.Error.error)
|
|
||||||
.UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.routes |> ignore)
|
|
||||||
|> ignore
|
|
||||||
app
|
app
|
||||||
|
|
||||||
/// Compose all the configurations into one
|
/// Compose all the configurations into one
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
module private MyPrayerJournal.Views.Helpers
|
module private MyPrayerJournal.Views.Helpers
|
||||||
|
|
||||||
|
open Giraffe.Htmx
|
||||||
open Giraffe.ViewEngine
|
open Giraffe.ViewEngine
|
||||||
open Giraffe.ViewEngine.Htmx
|
open Giraffe.ViewEngine.Htmx
|
||||||
open MyPrayerJournal
|
open MyPrayerJournal
|
||||||
@@ -10,7 +11,7 @@ open NodaTime
|
|||||||
/// Create a link that targets the `#top` element and pushes a URL to history
|
/// Create a link that targets the `#top` element and pushes a URL to history
|
||||||
let pageLink href attrs =
|
let pageLink href attrs =
|
||||||
attrs
|
attrs
|
||||||
|> List.append [ _href href; _hxBoost; _hxTarget "#top"; _hxSwap HxSwap.InnerHtml; _hxPushUrl ]
|
|> List.append [ _href href; _hxBoost; _hxTarget "#top"; _hxSwap HxSwap.InnerHtml; _hxPushUrl "true" ]
|
||||||
|> a
|
|> a
|
||||||
|
|
||||||
/// Create a Material icon
|
/// Create a Material icon
|
||||||
@@ -27,5 +28,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 ]
|
||||||
@@ -1,60 +1,66 @@
|
|||||||
/// Views for journal pages and components
|
/// Views for journal pages and components
|
||||||
module MyPrayerJournal.Views.Journal
|
module MyPrayerJournal.Views.Journal
|
||||||
|
|
||||||
|
open Giraffe.Htmx
|
||||||
open Giraffe.ViewEngine
|
open Giraffe.ViewEngine
|
||||||
open Giraffe.ViewEngine.Accessibility
|
open Giraffe.ViewEngine.Accessibility
|
||||||
open Giraffe.ViewEngine.Htmx
|
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" ] [
|
||||||
div [ _class "card h-100" ] [
|
div [ _class "card h-100" ] [
|
||||||
div [ _class "card-header p-0 d-flex"; _roleToolBar ] [
|
div [ _class "card-header p-0 d-flex"; _roleToolBar ] [
|
||||||
pageLink $"/request/{reqId}/edit" [ _class "btn btn-secondary"; _title "Edit Request" ] [ icon "edit" ]
|
pageLink $"/request/{reqId}/edit" [ _class "btn btn-secondary"; _title "Edit Request" ] [ icon "edit" ]
|
||||||
spacer
|
spacer
|
||||||
button [
|
button [ _type "button"
|
||||||
_type "button"
|
|
||||||
_class "btn btn-secondary"
|
_class "btn btn-secondary"
|
||||||
_title "Add Notes"
|
_title "Add Notes"
|
||||||
_data "bs-toggle" "modal"
|
_data "bs-toggle" "modal"
|
||||||
_data "bs-target" "#notesModal"
|
_data "bs-target" "#notesModal"
|
||||||
_hxGet $"/components/request/{reqId}/add-notes"
|
_hxGet $"/components/request/{reqId}/add-notes"
|
||||||
_hxTarget "#notesBody"
|
_hxTarget "#notesBody"
|
||||||
_hxSwap HxSwap.InnerHtml
|
_hxSwap HxSwap.InnerHtml ] [
|
||||||
] [ icon "comment" ]
|
icon "comment"
|
||||||
|
]
|
||||||
spacer
|
spacer
|
||||||
button [
|
button [ _type "button"
|
||||||
_type "button"
|
|
||||||
_class "btn btn-secondary"
|
_class "btn btn-secondary"
|
||||||
_title "Snooze Request"
|
_title "Snooze Request"
|
||||||
_data "bs-toggle" "modal"
|
_data "bs-toggle" "modal"
|
||||||
_data "bs-target" "#snoozeModal"
|
_data "bs-target" "#snoozeModal"
|
||||||
_hxGet $"/components/request/{reqId}/snooze"
|
_hxGet $"/components/request/{reqId}/snooze"
|
||||||
_hxTarget "#snoozeBody"
|
_hxTarget "#snoozeBody"
|
||||||
_hxSwap HxSwap.InnerHtml
|
_hxSwap HxSwap.InnerHtml ] [
|
||||||
] [ icon "schedule" ]
|
icon "schedule"
|
||||||
|
]
|
||||||
div [ _class "flex-grow-1" ] []
|
div [ _class "flex-grow-1" ] []
|
||||||
button [
|
button [ _type "button"
|
||||||
_type "button"
|
|
||||||
_class "btn btn-success w-25"
|
_class "btn btn-success w-25"
|
||||||
_hxPatch $"/request/{reqId}/prayed"
|
_hxPatch $"/request/{reqId}/prayed"
|
||||||
_title "Mark as Prayed"
|
_title "Mark as Prayed" ] [
|
||||||
] [ icon "done" ]
|
icon "done"
|
||||||
|
]
|
||||||
]
|
]
|
||||||
div [ _class "card-body" ] [
|
div [ _class "card-body" ] [
|
||||||
p [ _class "request-text" ] [ str req.text ]
|
p [ _class "request-text" ] [ str req.Text ]
|
||||||
]
|
]
|
||||||
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 [] [ str "last activity "; relativeDate req.asOf now ]
|
em [] [
|
||||||
|
match req.LastPrayed with
|
||||||
|
| Some dt -> str "last prayed "; relativeDate dt now tz
|
||||||
|
| None -> str "last activity "; relativeDate req.AsOf now tz
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
/// The journal loading page
|
/// The journal loading page
|
||||||
let journal user = article [ _class "container-fluid mt-3" ] [
|
let journal user =
|
||||||
|
article [ _class "container-fluid mt-3" ] [
|
||||||
h2 [ _class "pb-3" ] [
|
h2 [ _class "pb-3" ] [
|
||||||
str user
|
str user
|
||||||
match user with "Your" -> () | _ -> rawText "’s"
|
match user with "Your" -> () | _ -> rawText "’s"
|
||||||
@@ -66,13 +72,11 @@ let journal user = article [ _class "container-fluid mt-3" ] [
|
|||||||
p [ _hxGet "/components/journal-items"; _hxSwap HxSwap.OuterHtml; _hxTrigger HxTrigger.Load ] [
|
p [ _hxGet "/components/journal-items"; _hxSwap HxSwap.OuterHtml; _hxTrigger HxTrigger.Load ] [
|
||||||
rawText "Loading your prayer journal…"
|
rawText "Loading your prayer journal…"
|
||||||
]
|
]
|
||||||
div [
|
div [ _id "notesModal"
|
||||||
_id "notesModal"
|
|
||||||
_class "modal fade"
|
_class "modal fade"
|
||||||
_tabindex "-1"
|
_tabindex "-1"
|
||||||
_ariaLabelledBy "nodesModalLabel"
|
_ariaLabelledBy "nodesModalLabel"
|
||||||
_ariaHidden "true"
|
_ariaHidden "true" ] [
|
||||||
] [
|
|
||||||
div [ _class "modal-dialog modal-dialog-scrollable" ] [
|
div [ _class "modal-dialog modal-dialog-scrollable" ] [
|
||||||
div [ _class "modal-content" ] [
|
div [ _class "modal-content" ] [
|
||||||
div [ _class "modal-header" ] [
|
div [ _class "modal-header" ] [
|
||||||
@@ -81,20 +85,21 @@ let journal user = article [ _class "container-fluid mt-3" ] [
|
|||||||
]
|
]
|
||||||
div [ _class "modal-body"; _id "notesBody" ] [ ]
|
div [ _class "modal-body"; _id "notesBody" ] [ ]
|
||||||
div [ _class "modal-footer" ] [
|
div [ _class "modal-footer" ] [
|
||||||
button [ _type "button"; _id "notesDismiss"; _class "btn btn-secondary"; _data "bs-dismiss" "modal" ] [
|
button [ _type "button"
|
||||||
|
_id "notesDismiss"
|
||||||
|
_class "btn btn-secondary"
|
||||||
|
_data "bs-dismiss" "modal" ] [
|
||||||
str "Close"
|
str "Close"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [
|
div [ _id "snoozeModal"
|
||||||
_id "snoozeModal"
|
|
||||||
_class "modal fade"
|
_class "modal fade"
|
||||||
_tabindex "-1"
|
_tabindex "-1"
|
||||||
_ariaLabelledBy "snoozeModalLabel"
|
_ariaLabelledBy "snoozeModalLabel"
|
||||||
_ariaHidden "true"
|
_ariaHidden "true" ] [
|
||||||
] [
|
|
||||||
div [ _class "modal-dialog modal-sm" ] [
|
div [ _class "modal-dialog modal-sm" ] [
|
||||||
div [ _class "modal-content" ] [
|
div [ _class "modal-content" ] [
|
||||||
div [ _class "modal-header" ] [
|
div [ _class "modal-header" ] [
|
||||||
@@ -103,7 +108,10 @@ let journal user = article [ _class "container-fluid mt-3" ] [
|
|||||||
]
|
]
|
||||||
div [ _class "modal-body"; _id "snoozeBody" ] [ ]
|
div [ _class "modal-body"; _id "snoozeBody" ] [ ]
|
||||||
div [ _class "modal-footer" ] [
|
div [ _class "modal-footer" ] [
|
||||||
button [ _type "button"; _id "snoozeDismiss"; _class "btn btn-secondary"; _data "bs-dismiss" "modal" ] [
|
button [ _type "button"
|
||||||
|
_id "snoozeDismiss"
|
||||||
|
_class "btn btn-secondary"
|
||||||
|
_data "bs-dismiss" "modal" ] [
|
||||||
str "Close"
|
str "Close"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -113,7 +121,7 @@ let journal user = article [ _class "container-fluid mt-3" ] [
|
|||||||
]
|
]
|
||||||
|
|
||||||
/// 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" [
|
||||||
@@ -122,27 +130,24 @@ let journalItems now items =
|
|||||||
]
|
]
|
||||||
| false ->
|
| false ->
|
||||||
items
|
items
|
||||||
|> List.map (journalCard now)
|
|> List.map (journalCard now tz)
|
||||||
|> section [
|
|> section [ _id "journalItems"
|
||||||
_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"
|
||||||
_hxSwap HxSwap.OuterHtml
|
_hxSwap HxSwap.OuterHtml
|
||||||
]
|
_ariaLabel "Prayer Requests" ]
|
||||||
|
|
||||||
/// The notes edit modal body
|
/// The notes edit modal body
|
||||||
let notesEdit requestId =
|
let notesEdit requestId =
|
||||||
let reqId = RequestId.toString requestId
|
let reqId = RequestId.toString requestId
|
||||||
[ form [ _hxPost $"/request/{reqId}/note" ] [
|
[ form [ _hxPost $"/request/{reqId}/note" ] [
|
||||||
div [ _class "form-floating pb-3" ] [
|
div [ _class "form-floating pb-3" ] [
|
||||||
textarea [
|
textarea [ _id "notes"
|
||||||
_id "notes"
|
|
||||||
_name "notes"
|
_name "notes"
|
||||||
_class "form-control"
|
_class "form-control"
|
||||||
_style "min-height: 8rem;"
|
_style "min-height: 8rem;"
|
||||||
_placeholder "Notes"
|
_placeholder "Notes"
|
||||||
_autofocus; _required
|
_autofocus; _required ] [ ]
|
||||||
] [ ]
|
|
||||||
label [ _for "notes" ] [ str "Notes" ]
|
label [ _for "notes" ] [ str "Notes" ]
|
||||||
]
|
]
|
||||||
p [ _class "text-end" ] [ button [ _type "submit"; _class "btn btn-primary" ] [ str "Add Notes" ] ]
|
p [ _class "text-end" ] [ button [ _type "submit"; _class "btn btn-primary" ] [ str "Add Notes" ] ]
|
||||||
@@ -150,13 +155,13 @@ let notesEdit requestId =
|
|||||||
hr [ _style "margin: .5rem -1rem" ]
|
hr [ _style "margin: .5rem -1rem" ]
|
||||||
div [ _id "priorNotes" ] [
|
div [ _id "priorNotes" ] [
|
||||||
p [ _class "text-center pt-3" ] [
|
p [ _class "text-center pt-3" ] [
|
||||||
button [
|
button [ _type "button"
|
||||||
_type "button"
|
|
||||||
_class "btn btn-secondary"
|
_class "btn btn-secondary"
|
||||||
_hxGet $"/components/request/{reqId}/notes"
|
_hxGet $"/components/request/{reqId}/notes"
|
||||||
_hxSwap HxSwap.OuterHtml
|
_hxSwap HxSwap.OuterHtml
|
||||||
_hxTarget "#priorNotes"
|
_hxTarget "#priorNotes" ] [
|
||||||
] [str "Load Prior Notes" ]
|
str "Load Prior Notes"
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -164,11 +169,9 @@ let notesEdit requestId =
|
|||||||
/// The snooze edit form
|
/// The snooze edit form
|
||||||
let snooze requestId =
|
let snooze requestId =
|
||||||
let today = System.DateTime.Today.ToString "yyyy-MM-dd"
|
let today = System.DateTime.Today.ToString "yyyy-MM-dd"
|
||||||
form [
|
form [ _hxPatch $"/request/{RequestId.toString requestId}/snooze"
|
||||||
_hxPatch $"/request/{RequestId.toString requestId}/snooze"
|
|
||||||
_hxTarget "#journalItems"
|
_hxTarget "#journalItems"
|
||||||
_hxSwap HxSwap.OuterHtml
|
_hxSwap HxSwap.OuterHtml ] [
|
||||||
] [
|
|
||||||
div [ _class "form-floating pb-3" ] [
|
div [ _class "form-floating pb-3" ] [
|
||||||
input [ _type "date"; _id "until"; _name "until"; _class "form-control"; _min today; _required ]
|
input [ _type "date"; _id "until"; _name "until"; _class "form-control"; _min today; _required ]
|
||||||
label [ _for "until" ] [ str "Until" ]
|
label [ _for "until" ] [ str "Until" ]
|
||||||
|
|||||||
@@ -1,37 +1,40 @@
|
|||||||
/// Layout / home views
|
/// Layout / home views
|
||||||
module MyPrayerJournal.Views.Layout
|
module MyPrayerJournal.Views.Layout
|
||||||
|
|
||||||
// fsharplint:disable RecordFieldNames
|
|
||||||
|
|
||||||
open Giraffe.ViewEngine
|
open Giraffe.ViewEngine
|
||||||
open Giraffe.ViewEngine.Accessibility
|
open Giraffe.ViewEngine.Accessibility
|
||||||
|
|
||||||
/// The data needed to render a page-level view
|
/// The data needed to render a page-level view
|
||||||
type PageRenderContext = {
|
type PageRenderContext =
|
||||||
/// Whether the user is authenticated
|
{ /// Whether the user is authenticated
|
||||||
isAuthenticated : bool
|
IsAuthenticated : bool
|
||||||
|
|
||||||
/// Whether the user has snoozed requests
|
/// Whether the user has snoozed requests
|
||||||
hasSnoozed : bool
|
HasSnoozed : bool
|
||||||
|
|
||||||
/// The current URL
|
/// The current URL
|
||||||
currentUrl : string
|
CurrentUrl : string
|
||||||
|
|
||||||
/// The title for the page to be rendered
|
/// The title for the page to be rendered
|
||||||
pageTitle : string
|
PageTitle : string
|
||||||
|
|
||||||
/// The content of the page
|
/// The content of the page
|
||||||
content : XmlNode
|
Content : XmlNode
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The home page
|
/// The home page
|
||||||
let home = article [ _class "container mt-3" ] [
|
let home =
|
||||||
|
article [ _class "container mt-3" ] [
|
||||||
p [] [ rawText " " ]
|
p [] [ rawText " " ]
|
||||||
p [] [
|
p [] [
|
||||||
str "myPrayerJournal is a place where individuals can record their prayer requests, record that they prayed for "
|
str "myPrayerJournal is a place where individuals can record their prayer requests, record that they "
|
||||||
str "them, update them as God moves in the situation, and record a final answer received on that request. It also "
|
str "prayed for them, update them as God moves in the situation, and record a final answer received on "
|
||||||
str "allows individuals to review their answered prayers."
|
str "that request. It also allows individuals to review their answered prayers."
|
||||||
]
|
]
|
||||||
p [] [
|
p [] [
|
||||||
str "This site is open and available to the general public. To get started, simply click the "
|
str "This site is open and available to the general public. To get started, simply click the "
|
||||||
rawText "“Log On” link above, and log on with either a Microsoft or Google account. You can also "
|
rawText "“Log On” link above, and log on with either a Microsoft or Google account. You can "
|
||||||
rawText "learn more about the site at the “Docs” link, also above."
|
rawText "also learn more about the site at the “Docs” link, also above."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -46,16 +49,15 @@ let private navBar ctx =
|
|||||||
]
|
]
|
||||||
seq {
|
seq {
|
||||||
let navLink (matchUrl : string) =
|
let navLink (matchUrl : string) =
|
||||||
match ctx.currentUrl.StartsWith matchUrl with true -> [ _class "is-active-route" ] | false -> []
|
match ctx.CurrentUrl.StartsWith matchUrl with true -> [ _class "is-active-route" ] | false -> []
|
||||||
|> pageLink matchUrl
|
|> pageLink matchUrl
|
||||||
match ctx.isAuthenticated with
|
if ctx.IsAuthenticated then
|
||||||
| true ->
|
|
||||||
li [ _class "nav-item" ] [ navLink "/journal" [ str "Journal" ] ]
|
li [ _class "nav-item" ] [ navLink "/journal" [ str "Journal" ] ]
|
||||||
li [ _class "nav-item" ] [ navLink "/requests/active" [ str "Active" ] ]
|
li [ _class "nav-item" ] [ navLink "/requests/active" [ str "Active" ] ]
|
||||||
if ctx.hasSnoozed then li [ _class "nav-item" ] [ navLink "/requests/snoozed" [ str "Snoozed" ] ]
|
if ctx.HasSnoozed then li [ _class "nav-item" ] [ navLink "/requests/snoozed" [ str "Snoozed" ] ]
|
||||||
li [ _class "nav-item" ] [ navLink "/requests/answered" [ str "Answered" ] ]
|
li [ _class "nav-item" ] [ navLink "/requests/answered" [ str "Answered" ] ]
|
||||||
li [ _class "nav-item" ] [ a [ _href "/user/log-off" ] [ str "Log Off" ] ]
|
li [ _class "nav-item" ] [ a [ _href "/user/log-off" ] [ str "Log Off" ] ]
|
||||||
| false -> li [ _class "nav-item"] [ a [ _href "/user/log-on" ] [ str "Log On" ] ]
|
else li [ _class "nav-item"] [ a [ _href "/user/log-on" ] [ str "Log On" ] ]
|
||||||
li [ _class "nav-item" ] [
|
li [ _class "nav-item" ] [
|
||||||
a [ _href "https://docs.prayerjournal.me"; _target "_blank"; _rel "noopener" ] [ str "Docs" ]
|
a [ _href "https://docs.prayerjournal.me"; _target "_blank"; _rel "noopener" ] [ str "Docs" ]
|
||||||
]
|
]
|
||||||
@@ -66,7 +68,8 @@ let private navBar ctx =
|
|||||||
]
|
]
|
||||||
|
|
||||||
/// The title tag with the application name appended
|
/// The title tag with the application name appended
|
||||||
let titleTag ctx = title [] [ str ctx.pageTitle; rawText " « myPrayerJournal" ]
|
let titleTag ctx =
|
||||||
|
title [] [ str ctx.PageTitle; rawText " « myPrayerJournal" ]
|
||||||
|
|
||||||
/// The HTML `head` element
|
/// The HTML `head` element
|
||||||
let htmlHead ctx =
|
let htmlHead ctx =
|
||||||
@@ -74,12 +77,10 @@ let htmlHead ctx =
|
|||||||
meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ]
|
meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ]
|
||||||
meta [ _name "description"; _content "Online prayer journal - free w/Google or Microsoft account" ]
|
meta [ _name "description"; _content "Online prayer journal - free w/Google or Microsoft account" ]
|
||||||
titleTag ctx
|
titleTag ctx
|
||||||
link [
|
link [ _href "https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css"
|
||||||
_href "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
|
||||||
_rel "stylesheet"
|
_rel "stylesheet"
|
||||||
_integrity "sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
|
_integrity "sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx"
|
||||||
_crossorigin "anonymous"
|
_crossorigin "anonymous" ]
|
||||||
]
|
|
||||||
link [ _href "https://fonts.googleapis.com/icon?family=Material+Icons"; _rel "stylesheet" ]
|
link [ _href "https://fonts.googleapis.com/icon?family=Material+Icons"; _rel "stylesheet" ]
|
||||||
link [ _href "/style/style.css"; _rel "stylesheet" ]
|
link [ _href "/style/style.css"; _rel "stylesheet" ]
|
||||||
]
|
]
|
||||||
@@ -94,7 +95,7 @@ let toaster =
|
|||||||
let htmlFoot =
|
let htmlFoot =
|
||||||
footer [ _class "container-fluid" ] [
|
footer [ _class "container-fluid" ] [
|
||||||
p [ _class "text-muted text-end" ] [
|
p [ _class "text-muted text-end" ] [
|
||||||
str "myPrayerJournal v3"
|
str "myPrayerJournal v3.1"
|
||||||
br []
|
br []
|
||||||
em [] [
|
em [] [
|
||||||
small [] [
|
small [] [
|
||||||
@@ -106,24 +107,20 @@ let htmlFoot =
|
|||||||
str "Developed"
|
str "Developed"
|
||||||
]
|
]
|
||||||
str " and hosted by "
|
str " and hosted by "
|
||||||
a [ _href "https://bitbadger.solutions"; _target "_blank"; _rel "noopener" ] [ str "Bit Badger Solutions" ]
|
a [ _href "https://bitbadger.solutions"; _target "_blank"; _rel "noopener" ] [
|
||||||
|
str "Bit Badger Solutions"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
script [
|
]
|
||||||
_src "https://unpkg.com/htmx.org@1.5.0"
|
Htmx.Script.minified
|
||||||
_integrity "sha384-oGA+prIp5Vchu6we2YkI51UtVzN9Jpx2Z7PnR1I78PnZlN8LkrCT4lqqqmDkyrvI"
|
|
||||||
_crossorigin "anonymous"
|
|
||||||
] []
|
|
||||||
script [] [
|
script [] [
|
||||||
rawText "if (!htmx) document.write('<script src=\"/script/htmx-1.5.0.min.js\"><\/script>')"
|
rawText "if (!htmx) document.write('<script src=\"/script/htmx.min.js\"><\/script>')"
|
||||||
]
|
]
|
||||||
script [
|
script [ _async
|
||||||
_async
|
_src "https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
|
||||||
_src "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
_integrity "sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
|
||||||
_integrity "sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
_crossorigin "anonymous" ] []
|
||||||
_crossorigin "anonymous"
|
|
||||||
] []
|
|
||||||
script [] [
|
script [] [
|
||||||
rawText "setTimeout(function () { "
|
rawText "setTimeout(function () { "
|
||||||
rawText "if (!bootstrap) document.write('<script src=\"/script/bootstrap.bundle.min.js\"><\/script>') "
|
rawText "if (!bootstrap) document.write('<script src=\"/script/bootstrap.bundle.min.js\"><\/script>') "
|
||||||
@@ -137,7 +134,7 @@ let view ctx =
|
|||||||
html [ _lang "en" ] [
|
html [ _lang "en" ] [
|
||||||
htmlHead ctx
|
htmlHead ctx
|
||||||
body [] [
|
body [] [
|
||||||
section [ _id "top" ] [ navBar ctx; main [ _roleMain ] [ ctx.content ] ]
|
section [ _id "top"; _ariaLabel "Top navigation" ] [ navBar ctx; main [ _roleMain ] [ ctx.Content ] ]
|
||||||
toaster
|
toaster
|
||||||
htmlFoot
|
htmlFoot
|
||||||
]
|
]
|
||||||
@@ -147,5 +144,5 @@ let view ctx =
|
|||||||
let partial ctx =
|
let partial ctx =
|
||||||
html [ _lang "en" ] [
|
html [ _lang "en" ] [
|
||||||
head [] [ titleTag ctx ]
|
head [] [ titleTag ctx ]
|
||||||
body [] [ navBar ctx; main [ _roleMain ] [ ctx.content ] ]
|
body [] [ navBar ctx; main [ _roleMain ] [ ctx.Content ] ]
|
||||||
]
|
]
|
||||||
@@ -4,23 +4,26 @@ module MyPrayerJournal.Views.Legal
|
|||||||
open Giraffe.ViewEngine
|
open Giraffe.ViewEngine
|
||||||
|
|
||||||
/// View for the "Privacy Policy" page
|
/// View for the "Privacy Policy" page
|
||||||
let privacyPolicy = article [ _class "container mt-3" ] [
|
let privacyPolicy =
|
||||||
|
article [ _class "container mt-3" ] [
|
||||||
h2 [ _class "mb-2" ] [ str "Privacy Policy" ]
|
h2 [ _class "mb-2" ] [ str "Privacy Policy" ]
|
||||||
h6 [ _class "text-muted pb-3" ] [ str "as of May 21"; sup [] [ str "st"]; str ", 2018" ]
|
h6 [ _class "text-muted pb-3" ] [ str "as of May 21"; sup [] [ str "st"]; str ", 2018" ]
|
||||||
p [] [
|
p [] [
|
||||||
str "The nature of the service is one where privacy is a must. The items below will help you understand the data "
|
str "The nature of the service is one where privacy is a must. The items below will help you understand "
|
||||||
str "we collect, access, and store on your behalf as you use this service."
|
str "the data we collect, access, and store on your behalf as you use this service."
|
||||||
]
|
]
|
||||||
div [ _class "card" ] [
|
div [ _class "card" ] [
|
||||||
div [ _class "list-group list-group-flush" ] [
|
div [ _class "list-group list-group-flush" ] [
|
||||||
div [ _class "list-group-item"] [
|
div [ _class "list-group-item"] [
|
||||||
h3 [] [ str "Third Party Services" ]
|
h3 [] [ str "Third Party Services" ]
|
||||||
p [ _class "card-text" ] [
|
p [ _class "card-text" ] [
|
||||||
str "myPrayerJournal utilizes a third-party authentication and identity provider. You should familiarize "
|
str "myPrayerJournal utilizes a third-party authentication and identity provider. You should "
|
||||||
str "yourself with the privacy policy for "
|
str "familiarize yourself with the privacy policy for "
|
||||||
a [ _href "https://auth0.com/privacy"; _target "_blank" ] [ str "Auth0" ]
|
a [ _href "https://auth0.com/privacy"; _target "_blank" ] [ str "Auth0" ]
|
||||||
str ", as well as your chosen provider ("
|
str ", as well as your chosen provider ("
|
||||||
a [ _href "https://privacy.microsoft.com/en-us/privacystatement"; _target "_blank" ] [ str "Microsoft"]
|
a [ _href "https://privacy.microsoft.com/en-us/privacystatement"; _target "_blank" ] [
|
||||||
|
str "Microsoft"
|
||||||
|
]
|
||||||
str " or "
|
str " or "
|
||||||
a [ _href "https://policies.google.com/privacy"; _target "_blank" ] [ str "Google" ]
|
a [ _href "https://policies.google.com/privacy"; _target "_blank" ] [ str "Google" ]
|
||||||
str ")."
|
str ")."
|
||||||
@@ -31,22 +34,23 @@ let privacyPolicy = article [ _class "container mt-3" ] [
|
|||||||
h4 [] [ str "Identifying Data" ]
|
h4 [] [ str "Identifying Data" ]
|
||||||
ul [] [
|
ul [] [
|
||||||
li [] [
|
li [] [
|
||||||
rawText "The only identifying data myPrayerJournal stores is the subscriber (“sub”) field from "
|
str "The only identifying data myPrayerJournal stores is the subscriber "
|
||||||
str "the token we receive from Auth0, once you have signed in through their hosted service. All "
|
rawText "(“sub”) field from the token we receive from Auth0, once you have "
|
||||||
str "information is associated with you via this field."
|
str "signed in through their hosted service. All information is associated with you via "
|
||||||
|
str "this field."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
str "While you are signed in, within your browser, the service has access to your first and last names, "
|
str "While you are signed in, within your browser, the service has access to your first "
|
||||||
str "along with a URL to the profile picture (provided by your selected identity provider). This "
|
str "and last names, along with a URL to the profile picture (provided by your selected "
|
||||||
rawText "information is not transmitted to the server, and is removed when “Log Off” is "
|
str "identity provider). This information is not transmitted to the server, and is removed "
|
||||||
str "clicked."
|
rawText "when “Log Off” is clicked."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
h4 [] [ str "User Provided Data" ]
|
h4 [] [ str "User Provided Data" ]
|
||||||
ul [ _class "mb-0" ] [
|
ul [ _class "mb-0" ] [
|
||||||
li [] [
|
li [] [
|
||||||
str "myPrayerJournal stores the information you provide, including the text of prayer requests, updates, "
|
str "myPrayerJournal stores the information you provide, including the text of prayer "
|
||||||
str "and notes; and the date/time when certain actions are taken."
|
str "requests, updates, and notes; and the date/time when certain actions are taken."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -54,35 +58,38 @@ let privacyPolicy = article [ _class "container mt-3" ] [
|
|||||||
h3 [] [ str "How Your Data Is Accessed / Secured" ]
|
h3 [] [ str "How Your Data Is Accessed / Secured" ]
|
||||||
ul [ _class "mb-0" ] [
|
ul [ _class "mb-0" ] [
|
||||||
li [] [
|
li [] [
|
||||||
str "Your provided data is returned to you, as required, to display your journal or your answered "
|
str "Your provided data is returned to you, as required, to display your journal or your "
|
||||||
str "requests. On the server, it is stored in a controlled-access database."
|
str "answered requests. On the server, it is stored in a controlled-access database."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
str "Your data is backed up, along with other Bit Badger Solutions hosted systems, in a rolling manner; "
|
str "Your data is backed up, along with other Bit Badger Solutions hosted systems, in a "
|
||||||
str "backups are preserved for the prior 7 days, and backups from the 1"
|
str "rolling manner; backups are preserved for the prior 7 days, and backups from the 1"
|
||||||
sup [] [ str "st" ]
|
sup [] [ str "st" ]
|
||||||
str " and 15"
|
str " and 15"
|
||||||
sup [] [ str "th" ]
|
sup [] [ str "th" ]
|
||||||
str " are preserved for 3 months. These backups are stored in a private cloud data repository."
|
str " are preserved for 3 months. These backups are stored in a private cloud data "
|
||||||
|
str "repository."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
str "The data collected and stored is the absolute minimum necessary for the functionality of the service. "
|
str "The data collected and stored is the absolute minimum necessary for the functionality "
|
||||||
rawText "There are no plans to “monetize” this service, and storing the minimum amount of "
|
rawText "of the service. There are no plans to “monetize” this service, and "
|
||||||
str "information means that the data we have is not interesting to purchasers (or those who may have more "
|
str "storing the minimum amount of information means that the data we have is not "
|
||||||
str "nefarious purposes)."
|
str "interesting to purchasers (or those who may have more nefarious purposes)."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
str "Access to servers and backups is strictly controlled and monitored for unauthorized access attempts."
|
str "Access to servers and backups is strictly controlled and monitored for unauthorized "
|
||||||
|
str "access attempts."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "list-group-item" ] [
|
div [ _class "list-group-item" ] [
|
||||||
h3 [] [ str "Removing Your Data" ]
|
h3 [] [ str "Removing Your Data" ]
|
||||||
p [ _class "card-text" ] [
|
p [ _class "card-text" ] [
|
||||||
str "At any time, you may choose to discontinue using this service. Both Microsoft and Google provide ways "
|
str "At any time, you may choose to discontinue using this service. Both Microsoft and Google "
|
||||||
str "to revoke access from this application. However, if you want your data removed from the database, "
|
str "provide ways to revoke access from this application. However, if you want your data "
|
||||||
str "please contact daniel at bitbadger.solutions (via e-mail, replacing at with @) prior to doing so, to "
|
str "removed from the database, please contact daniel at bitbadger.solutions (via e-mail, "
|
||||||
str "ensure we can determine which subscriber ID belongs to you."
|
str "replacing at with @) prior to doing so, to ensure we can determine which subscriber ID "
|
||||||
|
str "belongs to you."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -90,7 +97,8 @@ let privacyPolicy = article [ _class "container mt-3" ] [
|
|||||||
]
|
]
|
||||||
|
|
||||||
/// View for the "Terms of Service" page
|
/// View for the "Terms of Service" page
|
||||||
let termsOfService = article [ _class "container mt-3" ] [
|
let termsOfService =
|
||||||
|
article [ _class "container mt-3" ] [
|
||||||
h2 [ _class "mb-2" ] [ str "Terms of Service" ]
|
h2 [ _class "mb-2" ] [ str "Terms of Service" ]
|
||||||
h6 [ _class "text-muted pb-3"] [ str "as of May 21"; sup [] [ str "st" ]; str ", 2018" ]
|
h6 [ _class "text-muted pb-3"] [ str "as of May 21"; sup [] [ str "st" ]; str ", 2018" ]
|
||||||
div [ _class "card" ] [
|
div [ _class "card" ] [
|
||||||
@@ -98,17 +106,17 @@ let termsOfService = article [ _class "container mt-3" ] [
|
|||||||
div [ _class "list-group-item" ] [
|
div [ _class "list-group-item" ] [
|
||||||
h3 [] [ str "1. Acceptance of Terms" ]
|
h3 [] [ str "1. Acceptance of Terms" ]
|
||||||
p [ _class "card-text" ] [
|
p [ _class "card-text" ] [
|
||||||
str "By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you "
|
str "By accessing this web site, you are agreeing to be bound by these Terms and Conditions, "
|
||||||
str "are responsible to ensure that your use of this site complies with all applicable laws. Your continued "
|
str "and that you are responsible to ensure that your use of this site complies with all "
|
||||||
str "use of this site implies your acceptance of these terms."
|
str "applicable laws. Your continued use of this site implies your acceptance of these terms."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "list-group-item" ] [
|
div [ _class "list-group-item" ] [
|
||||||
h3 [] [ str "2. Description of Service and Registration" ]
|
h3 [] [ str "2. Description of Service and Registration" ]
|
||||||
p [ _class "card-text" ] [
|
p [ _class "card-text" ] [
|
||||||
str "myPrayerJournal is a service that allows individuals to enter and amend their prayer requests. It "
|
str "myPrayerJournal is a service that allows individuals to enter and amend their prayer "
|
||||||
str "requires no registration by itself, but access is granted based on a successful login with an external "
|
str "requests. It requires no registration by itself, but access is granted based on a "
|
||||||
str "identity provider. See "
|
str "successful login with an external identity provider. See "
|
||||||
pageLink "/legal/privacy-policy" [] [ str "our privacy policy" ]
|
pageLink "/legal/privacy-policy" [] [ str "our privacy policy" ]
|
||||||
str " for details on how that information is accessed and stored."
|
str " for details on how that information is accessed and stored."
|
||||||
]
|
]
|
||||||
@@ -116,11 +124,13 @@ let termsOfService = article [ _class "container mt-3" ] [
|
|||||||
div [ _class "list-group-item" ] [
|
div [ _class "list-group-item" ] [
|
||||||
h3 [] [ str "3. Third Party Services" ]
|
h3 [] [ str "3. Third Party Services" ]
|
||||||
p [ _class "card-text" ] [
|
p [ _class "card-text" ] [
|
||||||
str "This service utilizes a third-party service provider for identity management. Review the terms of "
|
str "This service utilizes a third-party service provider for identity management. Review the "
|
||||||
str "service for "
|
str "terms of service for "
|
||||||
a [ _href "https://auth0.com/terms"; _target "_blank" ] [ str "Auth0"]
|
a [ _href "https://auth0.com/terms"; _target "_blank" ] [ str "Auth0"]
|
||||||
str ", as well as those for the selected authorization provider ("
|
str ", as well as those for the selected authorization provider ("
|
||||||
a [ _href "https://www.microsoft.com/en-us/servicesagreement"; _target "_blank" ] [ str "Microsoft"]
|
a [ _href "https://www.microsoft.com/en-us/servicesagreement"; _target "_blank" ] [
|
||||||
|
str "Microsoft"
|
||||||
|
]
|
||||||
str " or "
|
str " or "
|
||||||
a [ _href "https://policies.google.com/terms"; _target "_blank" ] [ str "Google" ]
|
a [ _href "https://policies.google.com/terms"; _target "_blank" ] [ str "Google" ]
|
||||||
str ")."
|
str ")."
|
||||||
@@ -129,17 +139,17 @@ let termsOfService = article [ _class "container mt-3" ] [
|
|||||||
div [ _class "list-group-item" ] [
|
div [ _class "list-group-item" ] [
|
||||||
h3 [] [ str "4. Liability" ]
|
h3 [] [ str "4. Liability" ]
|
||||||
p [ _class "card-text" ] [
|
p [ _class "card-text" ] [
|
||||||
rawText "This service is provided “as is”, and no warranty (express or implied) exists. The "
|
rawText "This service is provided “as is”, and no warranty (express or implied) "
|
||||||
str "service and its developers may not be held liable for any damages that may arise through the use of "
|
str "exists. The service and its developers may not be held liable for any damages that may "
|
||||||
str "this service."
|
str "arise through the use of this service."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "list-group-item" ] [
|
div [ _class "list-group-item" ] [
|
||||||
h3 [] [ str "5. Updates to Terms" ]
|
h3 [] [ str "5. Updates to Terms" ]
|
||||||
p [ _class "card-text" ] [
|
p [ _class "card-text" ] [
|
||||||
str "These terms and conditions may be updated at any time, and this service does not have the capability to "
|
str "These terms and conditions may be updated at any time, and this service does not have the "
|
||||||
str "notify users when these change. The date at the top of the page will be updated when any of the text of "
|
str "capability to notify users when these change. The date at the top of the page will be "
|
||||||
str "these terms is updated."
|
str "updated when any of the text of these terms is updated."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -150,4 +160,3 @@ let termsOfService = article [ _class "container mt-3" ] [
|
|||||||
str " to learn how we handle your data."
|
str " to learn how we handle your data."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1,108 +1,106 @@
|
|||||||
/// Views for request pages and components
|
/// Views for request pages and components
|
||||||
module MyPrayerJournal.Views.Request
|
module MyPrayerJournal.Views.Request
|
||||||
|
|
||||||
|
open Giraffe.Htmx
|
||||||
open Giraffe.ViewEngine
|
open Giraffe.ViewEngine
|
||||||
open Giraffe.ViewEngine.Htmx
|
open Giraffe.ViewEngine.Htmx
|
||||||
open MyPrayerJournal
|
open MyPrayerJournal
|
||||||
open NodaTime
|
open NodaTime
|
||||||
open System
|
|
||||||
|
|
||||||
/// Create a request within the list
|
/// Create a request within the list
|
||||||
let reqListItem now req =
|
let reqListItem now tz req =
|
||||||
let reqId = RequestId.toString req.requestId
|
let isFuture instant = defaultArg (instant |> Option.map (fun it -> it > now)) false
|
||||||
let isAnswered = req.lastStatus = Answered
|
let reqId = RequestId.toString req.RequestId
|
||||||
let isSnoozed = req.snoozedUntil > now
|
let isAnswered = req.LastStatus = Answered
|
||||||
let isPending = (not isSnoozed) && req.showAfter > now
|
let isSnoozed = isFuture req.SnoozedUntil
|
||||||
|
let isPending = (not isSnoozed) && isFuture req.ShowAfter
|
||||||
let btnClass = _class "btn btn-light mx-2"
|
let btnClass = _class "btn btn-light mx-2"
|
||||||
let restoreBtn (link : string) title =
|
let restoreBtn (link : string) title =
|
||||||
button [ btnClass; _hxPatch $"/request/{reqId}/{link}"; _title title ] [ icon "restore" ]
|
button [ btnClass; _hxPatch $"/request/{reqId}/{link}"; _title title ] [ icon "restore" ]
|
||||||
div [ _class "list-group-item px-0 d-flex flex-row align-items-start"; _hxTarget "this"; _hxSwap HxSwap.OuterHtml ] [
|
div [ _class "list-group-item px-0 d-flex flex-row align-items-start"
|
||||||
|
_hxTarget "this"
|
||||||
|
_hxSwap HxSwap.OuterHtml ] [
|
||||||
pageLink $"/request/{reqId}/full" [ btnClass; _title "View Full Request" ] [ icon "description" ]
|
pageLink $"/request/{reqId}/full" [ btnClass; _title "View Full Request" ] [ icon "description" ]
|
||||||
match isAnswered with
|
if not isAnswered then pageLink $"/request/{reqId}/edit" [ btnClass; _title "Edit Request" ] [ icon "edit" ]
|
||||||
| true -> ()
|
if isSnoozed then restoreBtn "cancel-snooze" "Cancel Snooze"
|
||||||
| false -> pageLink $"/request/{reqId}/edit" [ btnClass; _title "Edit Request" ] [ icon "edit" ]
|
elif isPending then restoreBtn "show" "Show Now"
|
||||||
match true with
|
|
||||||
| _ when isSnoozed -> restoreBtn "cancel-snooze" "Cancel Snooze"
|
|
||||||
| _ when isPending -> restoreBtn "show" "Show Now"
|
|
||||||
| _ -> ()
|
|
||||||
p [ _class "request-text mb-0" ] [
|
p [ _class "request-text mb-0" ] [
|
||||||
str req.text
|
str req.Text
|
||||||
match isSnoozed || isPending || isAnswered with
|
if isSnoozed || isPending || isAnswered then
|
||||||
| true ->
|
|
||||||
br []
|
br []
|
||||||
small [ _class "text-muted" ] [
|
small [ _class "text-muted" ] [
|
||||||
match () with
|
if isSnoozed then [ str "Snooze expires "; relativeDate req.SnoozedUntil.Value now tz ]
|
||||||
| _ when isSnoozed -> [ str "Snooze expires "; relativeDate req.snoozedUntil now ]
|
elif isPending then [ str "Request appears next "; relativeDate req.ShowAfter.Value now tz ]
|
||||||
| _ when isPending -> [ str "Request appears next "; relativeDate req.showAfter now ]
|
else (* isAnswered *) [ str "Answered "; relativeDate req.AsOf now tz ]
|
||||||
| _ (* isAnswered *) -> [ str "Answered "; relativeDate req.asOf now ]
|
|
||||||
|> em []
|
|> em []
|
||||||
]
|
]
|
||||||
| false -> ()
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
/// 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 = article [ _class "container mt-3" ] [
|
let active now tz reqs =
|
||||||
|
article [ _class "container mt-3" ] [
|
||||||
h2 [ _class "pb-3" ] [ str "Active Requests" ]
|
h2 [ _class "pb-3" ] [ str "Active Requests" ]
|
||||||
match reqs |> List.isEmpty with
|
if List.isEmpty reqs then
|
||||||
| true ->
|
|
||||||
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" ]
|
||||||
| false -> reqList now reqs
|
else reqList now tz reqs
|
||||||
]
|
]
|
||||||
|
|
||||||
/// View for Answered Requests page
|
/// View for Answered Requests page
|
||||||
let answered now reqs = article [ _class "container mt-3" ] [
|
let answered now tz reqs =
|
||||||
|
article [ _class "container mt-3" ] [
|
||||||
h2 [ _class "pb-3" ] [ str "Answered Requests" ]
|
h2 [ _class "pb-3" ] [ str "Answered Requests" ]
|
||||||
match reqs |> List.isEmpty with
|
if List.isEmpty reqs then
|
||||||
| true ->
|
noResults "No Answered Requests" "/journal" "Return to your journal" [
|
||||||
noResults "No Active Requests" "/journal" "Return to your journal" [
|
str "Your prayer journal has no answered requests; once you have marked one as "
|
||||||
rawText "Your prayer journal has no answered requests; once you have marked one as “Answered”, "
|
rawText "“Answered”, it will appear here"
|
||||||
str "it will appear here"
|
|
||||||
]
|
]
|
||||||
| false -> reqList now reqs
|
else reqList now tz reqs
|
||||||
]
|
]
|
||||||
|
|
||||||
/// View for Snoozed Requests page
|
/// View for Snoozed Requests page
|
||||||
let snoozed now reqs = article [ _class "container mt-3" ] [
|
let snoozed now tz reqs =
|
||||||
|
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
|
||||||
|> List.filter RequestAction.isAnswered
|
|> Array.filter History.isAnswered
|
||||||
|> List.tryHead
|
|> Array.tryHead
|
||||||
|> Option.map (fun x -> x.asOf)
|
|> Option.map (fun x -> x.AsOf)
|
||||||
let prayed = (req.history |> List.filter RequestAction.isPrayed |> List.length).ToString "N0"
|
let prayed = (req.History |> Array.filter History.isPrayed |> Array.length).ToString "N0"
|
||||||
let daysOpen =
|
let daysOpen =
|
||||||
let asOf = defaultArg answered now
|
let asOf = defaultArg answered now
|
||||||
((asOf - (req.history |> List.filter RequestAction.isCreated |> List.head).asOf).TotalDays |> int).ToString "N0"
|
((asOf - (req.History |> Array.filter History.isCreated |> Array.head).AsOf).TotalDays |> int).ToString "N0"
|
||||||
let lastText =
|
let lastText =
|
||||||
req.history
|
req.History
|
||||||
|> List.filter (fun h -> Option.isSome h.text)
|
|> Array.filter (fun h -> Option.isSome h.Text)
|
||||||
|> List.sortByDescending (fun h -> h.asOf)
|
|> Array.sortByDescending (fun h -> h.AsOf)
|
||||||
|> List.map (fun h -> Option.get h.text)
|
|> Array.map (fun h -> Option.get h.Text)
|
||||||
|> List.head
|
|> Array.head
|
||||||
// The history log including notes (and excluding the final entry for answered requests)
|
// The history log including notes (and excluding the final entry for answered requests)
|
||||||
let log =
|
let log =
|
||||||
let toDisp (h : History) = {| asOf = h.asOf; text = h.text; status = RequestAction.toString h.status |}
|
let toDisp (h : History) = {| asOf = h.AsOf; text = h.Text; status = RequestAction.toString h.Status |}
|
||||||
let all =
|
let all =
|
||||||
req.notes
|
req.Notes
|
||||||
|> List.map (fun n -> {| asOf = n.asOf; text = Some n.notes; status = "Notes" |})
|
|> Array.map (fun n -> {| asOf = n.AsOf; text = Some n.Notes; status = "Notes" |})
|
||||||
|> List.append (req.history |> List.map toDisp)
|
|> Array.append (req.History |> Array.map toDisp)
|
||||||
|> List.sortByDescending (fun it -> it.asOf)
|
|> Array.sortByDescending (fun it -> it.asOf)
|
||||||
|
|> List.ofArray
|
||||||
// Skip the first entry for answered requests; that info is already displayed
|
// Skip the first entry for answered requests; that info is already displayed
|
||||||
match answered with Some _ -> all |> List.skip 1 | None -> all
|
match answered with Some _ -> all.Tail | None -> all
|
||||||
article [ _class "container mt-3" ] [
|
article [ _class "container mt-3" ] [
|
||||||
div [_class "card" ] [
|
div [_class "card" ] [
|
||||||
h5 [ _class "card-header" ] [ str "Full Prayer Request" ]
|
h5 [ _class "card-header" ] [ str "Full Prayer Request" ]
|
||||||
@@ -113,15 +111,16 @@ 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 -> ()
|
||||||
sprintf "Prayed %s times • Open %s days" prayed daysOpen |> rawText
|
rawText $"Prayed %s{prayed} times • Open %s{daysOpen} days"
|
||||||
]
|
]
|
||||||
p [ _class "card-text" ] [ str lastText ]
|
p [ _class "card-text" ] [ str lastText ]
|
||||||
]
|
]
|
||||||
log
|
log
|
||||||
|> List.map (fun it -> li [ _class "list-group-item" ] [
|
|> List.map (fun it ->
|
||||||
|
li [ _class "list-group-item" ] [
|
||||||
p [ _class "m-0" ] [
|
p [ _class "m-0" ] [
|
||||||
str it.status
|
str it.status
|
||||||
rawText " "
|
rawText " "
|
||||||
@@ -142,40 +141,45 @@ let edit (req : JournalRequest) returnTo isNew =
|
|||||||
| "active" -> "/requests/active"
|
| "active" -> "/requests/active"
|
||||||
| "snoozed" -> "/requests/snoozed"
|
| "snoozed" -> "/requests/snoozed"
|
||||||
| _ (* "journal" *) -> "/journal"
|
| _ (* "journal" *) -> "/journal"
|
||||||
|
let recurCount =
|
||||||
|
match req.Recurrence with
|
||||||
|
| Immediate -> None
|
||||||
|
| Hours h -> Some h
|
||||||
|
| Days d -> Some d
|
||||||
|
| Weeks w -> Some w
|
||||||
|
|> Option.map string
|
||||||
|
|> Option.defaultValue ""
|
||||||
article [ _class "container" ] [
|
article [ _class "container" ] [
|
||||||
h2 [ _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 [
|
form [ _hxBoost
|
||||||
_hxBoost
|
|
||||||
_hxTarget "#top"
|
_hxTarget "#top"
|
||||||
_hxPushUrl
|
_hxPushUrl "true"
|
||||||
"/request" |> match isNew with true -> _hxPost | false -> _hxPatch
|
"/request" |> match isNew with true -> _hxPost | false -> _hxPatch ] [
|
||||||
] [
|
input [ _type "hidden"
|
||||||
input [
|
|
||||||
_type "hidden"
|
|
||||||
_name "requestId"
|
_name "requestId"
|
||||||
_value (match isNew with true -> "new" | false -> RequestId.toString req.requestId)
|
_value (match isNew with true -> "new" | false -> RequestId.toString req.RequestId) ]
|
||||||
]
|
|
||||||
input [ _type "hidden"; _name "returnTo"; _value returnTo ]
|
input [ _type "hidden"; _name "returnTo"; _value returnTo ]
|
||||||
div [ _class "form-floating pb-3" ] [
|
div [ _class "form-floating pb-3" ] [
|
||||||
textarea [
|
textarea [ _id "requestText"
|
||||||
_id "requestText"
|
|
||||||
_name "requestText"
|
_name "requestText"
|
||||||
_class "form-control"
|
_class "form-control"
|
||||||
_style "min-height: 8rem;"
|
_style "min-height: 8rem;"
|
||||||
_placeholder "Enter the text of the request"
|
_placeholder "Enter the text of the request"
|
||||||
_autofocus; _required
|
_autofocus; _required ] [ str req.Text ]
|
||||||
] [ str req.text ]
|
|
||||||
label [ _for "requestText" ] [ str "Prayer Request" ]
|
label [ _for "requestText" ] [ str "Prayer Request" ]
|
||||||
]
|
]
|
||||||
br []
|
br []
|
||||||
match isNew with
|
if not isNew then
|
||||||
| true -> ()
|
|
||||||
| false ->
|
|
||||||
div [ _class "pb-3" ] [
|
div [ _class "pb-3" ] [
|
||||||
label [] [ str "Also Mark As" ]
|
label [] [ str "Also Mark As" ]
|
||||||
br []
|
br []
|
||||||
div [ _class "form-check form-check-inline" ] [
|
div [ _class "form-check form-check-inline" ] [
|
||||||
input [ _type "radio"; _class "form-check-input"; _id "sU"; _name "status"; _value "Updated"; _checked ]
|
input [ _type "radio"
|
||||||
|
_class "form-check-input"
|
||||||
|
_id "sU"
|
||||||
|
_name "status"
|
||||||
|
_value "Updated"
|
||||||
|
_checked ]
|
||||||
label [ _for "sU" ] [ str "Updated" ]
|
label [ _for "sU" ] [ str "Updated" ]
|
||||||
]
|
]
|
||||||
div [ _class "form-check form-check-inline" ] [
|
div [ _class "form-check form-check-inline" ] [
|
||||||
@@ -195,55 +199,53 @@ let edit (req : JournalRequest) returnTo isNew =
|
|||||||
]
|
]
|
||||||
div [ _class "d-flex flex-row flex-wrap justify-content-center align-items-center" ] [
|
div [ _class "d-flex flex-row flex-wrap justify-content-center align-items-center" ] [
|
||||||
div [ _class "form-check mx-2" ] [
|
div [ _class "form-check mx-2" ] [
|
||||||
input [
|
input [ _type "radio"
|
||||||
_type "radio"
|
|
||||||
_class "form-check-input"
|
_class "form-check-input"
|
||||||
_id "rI"
|
_id "rI"
|
||||||
_name "recurType"
|
_name "recurType"
|
||||||
_value "Immediate"
|
_value "Immediate"
|
||||||
_onclick "mpj.edit.toggleRecurrence(event)"
|
_onclick "mpj.edit.toggleRecurrence(event)"
|
||||||
match req.recurType with Immediate -> _checked | _ -> ()
|
match req.Recurrence with Immediate -> _checked | _ -> () ]
|
||||||
]
|
|
||||||
label [ _for "rI" ] [ str "Immediately" ]
|
label [ _for "rI" ] [ str "Immediately" ]
|
||||||
]
|
]
|
||||||
div [ _class "form-check mx-2"] [
|
div [ _class "form-check mx-2"] [
|
||||||
input [
|
input [ _type "radio"
|
||||||
_type "radio"
|
|
||||||
_class "form-check-input"
|
_class "form-check-input"
|
||||||
_id "rO"
|
_id "rO"
|
||||||
_name "recurType"
|
_name "recurType"
|
||||||
_value "Other"
|
_value "Other"
|
||||||
_onclick "mpj.edit.toggleRecurrence(event)"
|
_onclick "mpj.edit.toggleRecurrence(event)"
|
||||||
match req.recurType with Immediate -> () | _ -> _checked
|
match req.Recurrence with Immediate -> () | _ -> _checked ]
|
||||||
]
|
|
||||||
label [ _for "rO" ] [ rawText "Every…" ]
|
label [ _for "rO" ] [ rawText "Every…" ]
|
||||||
]
|
]
|
||||||
div [ _class "form-floating mx-2"] [
|
div [ _class "form-floating mx-2"] [
|
||||||
input [
|
input [ _type "number"
|
||||||
_type "number"
|
|
||||||
_class "form-control"
|
_class "form-control"
|
||||||
_id "recurCount"
|
_id "recurCount"
|
||||||
_name "recurCount"
|
_name "recurCount"
|
||||||
_placeholder "0"
|
_placeholder "0"
|
||||||
_value (string req.recurCount)
|
_value recurCount
|
||||||
_style "width:6rem;"
|
_style "width:6rem;"
|
||||||
_required
|
_required
|
||||||
match req.recurType with Immediate -> _disabled | _ -> ()
|
match req.Recurrence with Immediate -> _disabled | _ -> () ]
|
||||||
]
|
|
||||||
label [ _for "recurCount" ] [ str "Count" ]
|
label [ _for "recurCount" ] [ str "Count" ]
|
||||||
]
|
]
|
||||||
div [ _class "form-floating mx-2" ] [
|
div [ _class "form-floating mx-2" ] [
|
||||||
select [
|
select [ _class "form-control"
|
||||||
_class "form-control"
|
|
||||||
_id "recurInterval"
|
_id "recurInterval"
|
||||||
_name "recurInterval"
|
_name "recurInterval"
|
||||||
_style "width:6rem;"
|
_style "width:6rem;"
|
||||||
_required
|
_required
|
||||||
match req.recurType with Immediate -> _disabled | _ -> ()
|
match req.Recurrence with Immediate -> _disabled | _ -> () ] [
|
||||||
] [
|
option [ _value "Hours"; match req.Recurrence with Hours _ -> _selected | _ -> () ] [
|
||||||
option [ _value "Hours"; match req.recurType with Hours -> _selected | _ -> () ] [ str "hours" ]
|
str "hours"
|
||||||
option [ _value "Days"; match req.recurType with Days -> _selected | _ -> () ] [ str "days" ]
|
]
|
||||||
option [ _value "Weeks"; match req.recurType with Weeks -> _selected | _ -> () ] [ str "weeks" ]
|
option [ _value "Days"; match req.Recurrence with Days _ -> _selected | _ -> () ] [
|
||||||
|
str "days"
|
||||||
|
]
|
||||||
|
option [ _value "Weeks"; match req.Recurrence with Weeks _ -> _selected | _ -> () ] [
|
||||||
|
str "weeks"
|
||||||
|
]
|
||||||
]
|
]
|
||||||
label [ _form "recurInterval" ] [ str "Interval" ]
|
label [ _form "recurInterval" ] [ str "Interval" ]
|
||||||
]
|
]
|
||||||
@@ -258,9 +260,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 @@
|
|||||||
/*!
|
/*!
|
||||||
* Bootstrap v5.0.2 (https://getbootstrap.com/)
|
* Bootstrap v5.2.0 (https://getbootstrap.com/)
|
||||||
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
* Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
*/
|
*/
|
||||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]}},e=t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t},i=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i="#"+i.split("#")[1]),e=i&&"#"!==i?i.trim():null}return e},n=t=>{const e=i(t);return e&&document.querySelector(e)?e:null},s=t=>{const e=i(t);return e?document.querySelector(e):null},o=t=>{t.dispatchEvent(new Event("transitionend"))},r=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),a=e=>r(e)?e.jquery?e[0]:e:"string"==typeof e&&e.length>0?t.findOne(e):null,l=(t,e,i)=>{Object.keys(i).forEach(n=>{const s=i[n],o=e[n],a=o&&r(o)?"element":null==(l=o)?""+l:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(s).test(a))throw new TypeError(`${t.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)})},c=t=>!(!r(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),h=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),d=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?d(t.parentNode):null},u=()=>{},f=t=>t.offsetHeight,p=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},m=[],g=()=>"rtl"===document.documentElement.dir,_=t=>{var e;e=()=>{const e=p();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",()=>{m.forEach(t=>t())}),m.push(e)):e()},b=t=>{"function"==typeof t&&t()},v=(t,e,i=!0)=>{if(!i)return void b(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const r=({target:i})=>{i===e&&(s=!0,e.removeEventListener("transitionend",r),b(t))};e.addEventListener("transitionend",r),setTimeout(()=>{s||o(e)},n)},y=(t,e,i,n)=>{let s=t.indexOf(e);if(-1===s)return t[!i&&n?t.length-1:0];const o=t.length;return s+=i?1:-1,n&&(s=(s+o)%o),t[Math.max(0,Math.min(s,o-1))]},w=/[^.]*(?=\..*)\.|.*/,E=/\..*/,A=/::\d+$/,T={};let O=1;const C={mouseenter:"mouseover",mouseleave:"mouseout"},k=/^(mouseenter|mouseleave)/i,L=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function x(t,e){return e&&`${e}::${O++}`||t.uidEvent||O++}function D(t){const e=x(t);return t.uidEvent=e,T[e]=T[e]||{},T[e]}function S(t,e,i=null){const n=Object.keys(t);for(let s=0,o=n.length;s<o;s++){const o=t[n[s]];if(o.originalHandler===e&&o.delegationSelector===i)return o}return null}function I(t,e,i){const n="string"==typeof e,s=n?i:e;let o=M(t);return L.has(o)||(o=t),[n,s,o]}function N(t,e,i,n,s){if("string"!=typeof e||!t)return;if(i||(i=n,n=null),k.test(e)){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=I(e,i,n),l=D(t),c=l[a]||(l[a]={}),h=S(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=x(r,e.replace(w,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parLine truncated
|
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t="transitionend",e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},m=t=>{"function"==typeof t&&t()},_=(e,i,n=!0)=>{if(!n)return void m(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),m(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=N(t);return C.has(o)||(o=t),[n,s,o]}function D(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return j(s,{delegateTarget:r}),n.oneOff&&P.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return j(n,{delegateTarget:t}),i.oneOff&&P.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function S(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function I(t,e,i,n){const s=e[i]||{};for(const o of Object.keys(s))if(o.includes(n)){const n=s[o];S(t,e,i,n.callable,n.delegationSelector)}}function N(t){return t=t.replace(y,""),T[t]||t}const P={on(t,e,i,n){D(t,e,i,n,!1)},one(t,e,i,n){D(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))I(t,l,i,e.slice(1));for(const i of Object.keys(c)){const n=i.replace(w,"");if(!a||e.includes(n)){const
Line truncated
|
||||||
//# sourceMappingURL=bootstrap.bundle.min.js.map
|
//# sourceMappingURL=bootstrap.bundle.min.js.map
|
||||||
@@ -1 +0,0 @@
|
|||||||
(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else{e.htmx=t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var D={onLoad:t,process:nt,on:N,off:I,trigger:ft,ajax:Jt,find:w,findAll:S,closest:L,values:function(e,t){var r=Tt(e,t||"post");return r.values},remove:E,addClass:C,removeClass:q,toggleClass:R,takeClass:O,defineExtension:er,removeExtension:tr,logAll:b,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth"},parseInterval:h,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){return new WebSocket(e,[])},version:"1.5.0"};var r=["get","post","put","delete","patch"];var n=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function h(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}return parseFloat(e)||undefined}function c(e,t){return e.getAttribute&&e.getAttribute(t)}function s(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function F(e,t){return c(e,t)||c(e,"data-"+t)}function l(e){return e.parentElement}function X(){return document}function d(e,t){if(t(e)){return e}else if(l(e)){return d(l(e),t)}else{return null}}function P(e,t){var r=null;d(e,function(e){return r=F(e,t)});return r}function v(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function i(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function o(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=X().createDocumentFragment()}return i}function u(e){if(D.config.useTemplateFragments){var t=o("<body><template>"+e+"</template></body>",0);return t.querySelector("template").content}else{var r=i(e);switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return o("<table>"+e+"</table>",1);case"col":return o("<table><colgroup>"+e+"</colgroup></table>",2);case"tr":return o("<table><tbody>"+e+"</tbody></table>",2);case"td":case"th":return o("<table><tbody><tr>"+e+"</tr></tbody></table>",3);case"script":return o("<div>"+e+"</div>",1);default:return o(e,0)}}}function U(e){if(e){e()}}function a(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function f(e){return a(e,"Function")}function g(e){return a(e,"Object")}function j(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function p(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function B(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function m(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function z(e){return X().body.contains(e)}function y(e){return e.trim().split(/\s+/)}function V(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function x(e){try{return JSON.parse(e)}catch(e){lt(e);return null}}function e(e){return jt(X().body,function(){return eval(e)})}function t(t){var e=D.on("htmx:load",function(e){t(e.detail.elt)});return e}function b(){D.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function w(e,t){if(t){return e.querySelector(t)}else{return w(X(),e)}}function S(e,t){if(t){return e.querySelectorAll(t)}else{return S(X(),e)}}function E(e,t){e=H(e);if(t){setTimeout(function(){E(e)},t)}else{e.parentElement.removeChild(e)}}function C(e,t,r){e=H(e);if(r){setTimeout(function(){C(e,t)},r)}else{e.classList.add(t)}}function q(e,t,r){e=H(e);if(r){setTimeout(function(){q(e,t)},r)}else{e.classList.remove(t)}}function R(e,t){e=H(e);e.classList.toggle(t)}function O(e,t){e=H(e);B(e.parentElement.children,function(e){q(e,t)});C(e,t)}function L(e,t){e=H(e);if(e.closest){return e.closest(t)}else{do{if(e==null||v(e,t)){return e}}while(e=e&&l(e))}}function T(e,t){if(t.indexOf("closest ")===0){return[L(e,t.substr(8))]}else if(t.indexOf("find ")===0){return[w(e,t.substr(5))]}else if(t==="document"){return[document]}else{return X().querySelectorAll(t)}}function A(e,t){return T(e,t)[0]}function H(e){if(a(e,"String")){return w(e)}else{return e}}function k(e,t,r){if(f(t)){return{target:X().body,event:e,listener:t}}else{return{target:H(e),event:t,listener:r}}}function N(t,r,n){nr(function(){var e=k(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=f(r);return e?r:n}function I(t,r,n){Line truncated
|
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
<!DOCTYPE html><html lang="en"><head><script async="" src="https://www.googletagmanager.com/gtag/js?id=UA-140352188-1"></script><script>window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', 'UA-140352188-1');</script><meta charSet="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/><meta name="description" content="The CDN for htmx.org"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"/><meta name="timestamp" content="2022-07-30T20:48:48.149Z"/><link rel="shortcut icon" href="/favicon.ico"/><title>UNPKG - htmx.org</title><script>window.Promise || document.write('\x3Cscript src="/es6-promise@4.2.5/dist/es6-promise.min.js">\x3C/script>\x3Cscript>ES6Promise.polyfill()\x3C/script>')</script><script>window.fetch || document.write('\x3Cscript src="/whatwg-fetch@3.0.0/dist/fetch.umd.js">\x3C/script>')</script><script>window.__DATA__ = {"packageName":"htmx.org","packageVersion":"1.8.0","availableVersions":["0.0.1","0.0.2","0.0.3","0.0.4","0.0.5","0.0.6","0.0.7","0.0.8","0.1.0","0.1.1","0.1.2","0.2.0","0.3.0","0.4.0","0.4.1","1.0.0","1.0.1","1.0.2","1.1.0","1.2.0","1.2.1","1.3.0","1.3.1","1.3.2","1.3.3","1.4.0","1.4.1","1.5.0","1.6.0","1.6.1","1.7.0","1.8.0"],"filename":"/dist/htmx.min.js","target":{"path":"/dist/htmx.min.js","type":"file","details":{"contentType":"application/javascript","integrity":"sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc","language":"JavaScript","size":39433,"uri":null,"highlights":["(<span class=\"code-function\"><span class=\"code-keyword\">function</span>(<span class=\"code-params\">e,t</span>)</span>{<span class=\"code-keyword\">if</span>(<span class=\"code-keyword\">typeof</span> define===<span class=\"code-string\">\"function\"</span>&&define.amd){define([],t)}<span class=\"code-keyword\">else</span>{e.htmx=e.htmx||t()}})(<span class=\"code-keyword\">typeof</span> self!==<span class=\"code-string\">\"undefined\"</span>?self:<span class=\"code-keyword\">this</span>,<span class=\"code-function\"><span class=\"code-keyword\">function</span>(<span class=\"code-params\"></span>)</span>{<span class=\"code-keyword\">return</span> <span class=\"code-function\"><span class=\"code-keyword\">function</span>(<span class=\"code-params\"></span>)</span>{<span class=\"code-string\">\"use strict\"</span>;<span class=\"code-keyword\">var</span> U={<span class=\"code-attr\">onLoad</span>:t,<span class=\"code-attr\">process</span>:vt,<span class=\"code-attr\">on</span>:X,<span class=\"code-attr\">off</span>:F,<span class=\"code-attr\">trigger</span>:$,<span class=\"code-attr\">ajax</span>:nr,<span class=\"code-attr\">find</span>:R,<span class=\"code-attr\">findAll</span>:O,<span class=\"code-attr\">closest</span>:N,<span class=\"code-attr\">values</span>:<span class=\"code-function\"><span class=\"code-keyword\">function</span>(<span class=\"code-params\">e,t</span>)</span>{<span class=\"code-keyword\">var</span> r=Pt(e,t||<span class=\"code-string\">\"post\"</span>);<span class=\"code-keyword\">return</span> r.values},<span class=\"code-attr\">remove</span>:q,<span class=\"code-attr\">addClass</span>:L,<span class=\"code-attr\">removeClass</span>:T,<span class=\"code-attr\">toggleClass</span>:A,<span class=\"code-attr\">takeClass</span>:H,<span class=\"code-attr\">defineExtension</span>:fr,<span class=\"code-attr\">removeExtension</span>:cr,<span class=\"code-attr\">logAll</span>:C,<span class=\"code-attr\">logger</span>:<span class=\"code-literal\">null</span>,<span class=\"code-attr\">config</span>:{<span class=\"code-attr\">historyEnabled</span>:<span class=\"code-literal\">true</span>,<span class=\"code-attr\">historyCacheSize</span>:<span class=\"code-number\">10</span>,<span class=\"code-attr\">refreshOnHistoryMiss</span>:<span class=\"code-literal\">false</span>,<span class=\"code-attr\">defaultSwapStyle</span>:<span class=\"code-string\">\"innerHTML\"</span>,<span class=\"code-attr\">defaultSwapDelay</span>:<span class=\"code-number\">0</span>,<span class=\"code-attr\">defaultSettleDelay</span>:<span class=\"code-number\">20</span>,<span class=\"code-attr\">includeIndicatorStyles</span>:<span class=\"code-literal\">true</span>,<span class=\"code-attr\">indicatorClass</span>:<span class=\"code-string\">\"htmx-indicator\"</span>,<span class=\"code-attr\">requestClass</span>:<span class=\"code-string\">\"htmx-request\"</span>,<span class=\"code-attr\">addedClass</span>:<span class=\"code-string\">\"htmx-added\"</span>,<span class=\"code-attr\">settlingClass</span>:<span class=\"code-string\">\"htmx-settling\"</span>,<span class=\"code-attr\">swappingClass</span>:<span class=\"code-string\">\"htmx-swapping\"</span>,<span class=\"code-attr\">allowEval</span>:<span class=\"code-literal\">true</span>,<span class=\"code-attr\">inlineScriptNonce</span>:<span class=\"code-string\">\"\"</span>,<span class=\"code-attr\">attributesToSettle</span>:[<span class=\"code-string\">\"class\"</span>,<span class=\"code-string\">\"style\"</span>,<span class=\"code-string\">\"width\"</span>,<span class=\"code-string\">\"height\"</span>],<span class=\"code-attr\">withCredentials</span>:<span class=\"codLine truncated
|
||||||
|
"default")?a["default"]:a}function D(a,b){return b={exports:{}},a(b,b.exports),b.exports}function J(a,b,e,c,d){for(var g in a)if(ua(a,g)){try{if("function"!==typeof a[g]){var r=Error((c||"React class")+": "+e+" type `"+g+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof a[g]+"`.");r.name="Invariant Violation";throw r;}var k=a[g](b,g,c,e,null,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED")}catch(q){k=q}!k||k instanceof Error||K((c||"React class")+": type specification of "+
|
||||||
|
e+" `"+g+"` is invalid; the type checker function must return `null` or an `Error` but returned a "+typeof k+". You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).");if(k instanceof Error&&!(k.message in L)){L[k.message]=!0;var B=d?d():"";K("Failed "+e+" type: "+k.message+(null!=B?B:""))}}}function G(){return null}function S(a){var b,e=a.children;a=a.css;return c.jsx("div",{css:w((b={border:"1px solid #dfe2e5",
|
||||||
|
borderRadius:3},b["@media (max-width: 700px)"]={borderRightWidth:0,borderLeftWidth:0},b),a)},e)}function T(a){var b,e=a.children;a=a.css;return c.jsx("div",{css:w((b={padding:10,background:"#f6f8fa",color:"#424242",border:"1px solid #d1d5da",borderTopLeftRadius:3,borderTopRightRadius:3,margin:"-1px -1px 0",display:"flex",flexDirection:"row",alignItems:"center",justifyContent:"space-between"},b["@media (max-width: 700px)"]={paddingRight:20,paddingLeft:20},b),a)},e)}function U(a){return a&&a.map(function(a,
|
||||||
|
c){return t.createElement(a.tag,z({key:c},a.attr),U(a.child))})}function E(a){return function(b){return t.createElement(va,z({attr:z({},a.attr)},b),U(a.child))}}function va(a){var b=function(b){var c=a.size||b.size||"1em";if(b.className)var e=b.className;a.className&&(e=(e?e+" ":"")+a.className);var h=a.attr,r=a.title,k=["attr","title"],B={},q;for(q in a)Object.prototype.hasOwnProperty.call(a,q)&&0>k.indexOf(q)&&(B[q]=a[q]);if(null!=a&&"function"===typeof Object.getOwnPropertySymbols){var p=0;for(q=
|
||||||
|
Object.getOwnPropertySymbols(a);p<q.length;p++)0>k.indexOf(q[p])&&(B[q[p]]=a[q[p]])}return t.createElement("svg",z({stroke:"currentColor",fill:"currentColor",strokeWidth:"0"},b.attr,h,B,{className:e,style:z({color:a.color||b.color},b.style,a.style),height:c,width:c,xmlns:"http://www.w3.org/2000/svg"}),r&&t.createElement("title",null,r),a.children)};return void 0!==V?t.createElement(V.Consumer,null,function(a){return b(a)}):b(W)}function F(a,b){var e=b.css;b=P(b,["css"]);return c.jsx(a,w({css:w({},
|
||||||
|
e,{verticalAlign:"text-bottom"})},b))}function wa(a){return F(X,a)}function xa(a){return F(Y,a)}function ya(a){return F(Z,a)}function za(a){return F(aa,a)}function Aa(a){return F(ba,a)}function ca(a){var b=a.path,e=a.details,g=Object.keys(e).reduce(function(a,b){var c=a.subdirs,g=a.files;b=e[b];"directory"===b.type?c.push(b):"file"===b.type&&g.push(b);return a},{subdirs:[],files:[]});a=g.subdirs;g=g.files;a.sort(da("path"));g.sort(da("path"));var d=[];"/"!==b&&d.push(c.jsx("tr",{key:".."},c.jsx("td",
|
||||||
|
{css:M}),c.jsx("td",{css:y},c.jsx("a",{title:"Parent directory",href:"../",css:N},"..")),c.jsx("td",{css:y}),c.jsx("td",{css:O})));a.forEach(function(a){a=a.path.substr(1<b.length?b.length+1:1);var e=a+"/";d.push(c.jsx("tr",{key:a},c.jsx("td",{css:M},c.jsx(ya,null)),c.jsx("td",{css:y},c.jsx("a",{title:a,href:e,css:N},a)),c.jsx("td",{css:y},"-"),c.jsx("td",{css:O},"-")))});g.forEach(function(a){var e=a.size,g=a.contentType;a=a.path.substr(1<b.length?b.length+1:1);d.push(c.jsx("tr",{key:a},c.jsx("td",
|
||||||
|
{css:M},"text/plain"===g||"text/markdown"===g?c.jsx(wa,null):c.jsx(xa,null)),c.jsx("td",{css:y},c.jsx("a",{title:a,href:a,css:N},a)),c.jsx("td",{css:y},ea(e)),c.jsx("td",{css:O},g)))});var h=[];0<g.length&&h.push(g.length+" file"+(1===g.length?"":"s"));0<a.length&&h.push(a.length+" folder"+(1===a.length?"":"s"));return c.jsx(S,null,c.jsx(T,null,c.jsx("span",null,h.join(", "))),c.jsx("table",{css:{width:"100%",borderCollapse:"collapse",borderRadius:2,background:"#fff","@media (max-width: 700px)":{"& th + th + th + th, & td + td + td + td":{display:"none"}},
|
||||||
|
"& tr:first-of-type td":{borderTop:0}}},c.jsx("thead",null,c.jsx("tr",null,c.jsx("th",null,c.jsx(H,null,"Icon")),c.jsx("th",null,c.jsx(H,null,"Name")),c.jsx("th",null,c.jsx(H,null,"Size")),c.jsx("th",null,c.jsx(H,null,"Content Type")))),c.jsx("tbody",null,d)))}function Ba(a){a=a.split("/");return a[a.length-1]}function Ca(a){var b=a.uri;return c.jsx("div",{css:{padding:20,textAlign:"center"}},c.jsx("img",{alt:Ba(a.path),src:b}))}function Da(a){a=a.highlights.slice(0);var b=a.length&&""===a[a.length-
|
||||||
|
1];b&&a.pop();return c.jsx("div",{className:"code-listing",css:{overflowX:"auto",overflowY:"hidden",paddingTop:5,paddingBottom:5}},c.jsx("table",{css:{border:"none",borderCollapse:"collapse",borderSpacing:0}},c.jsx("tbody",null,a.map(function(a,b){var e=b+1;return c.jsx("tr",{key:b},c.jsx("td",{id:"L"+e,css:{paddingLeft:10,paddingRight:10,color:"rgba(27,31,35,.3)",textAlign:"right",verticalAlign:"top",width:"1%",minWidth:50,userSelect:"none"}},c.jsx("span",null,e)),c.jsx("td",{id:"LC"+e,css:{paddingLeft:10,
|
||||||
|
paddingRight:10,color:"#24292e",whiteSpace:"pre"}},c.jsx("code",{dangerouslySetInnerHTML:{__html:a}})))}),!b&&c.jsx("tr",{key:"no-newline"},c.jsx("td",{css:{paddingLeft:10,paddingRight:10,color:"rgba(27,31,35,.3)",textAlign:"right",verticalAlign:"top",width:"1%",minWidth:50,userSelect:"none"}},"\\"),c.jsx("td",{css:{paddingLeft:10,color:"rgba(27,31,35,.3)",userSelect:"none"}},"No newline at end of file")))))}function Ea(){return c.jsx("div",{css:{padding:20}},c.jsx("p",{css:{textAlign:"center"}},
|
||||||
|
"No preview available."))}function fa(a){var b=a.packageName,e=a.packageVersion,g=a.path;a=a.details;var d=a.highlights,h=a.uri,r=a.language;return c.jsx(S,null,c.jsx(T,null,c.jsx("span",null,ea(a.size)),c.jsx("span",null,r),c.jsx("span",null,c.jsx("a",{href:"/"+b+"@"+e+g,css:{display:"inline-block",marginLeft:8,padding:"2px 8px",textDecoration:"none",fontWeight:600,fontSize:"0.9rem",color:"#24292e",backgroundColor:"#eff3f6",border:"1px solid rgba(27,31,35,.2)",borderRadius:3,":hover":{backgroundColor:"#e6ebf1",
|
||||||
|
borderColor:"rgba(27,31,35,.35)"},":active":{backgroundColor:"#e9ecef",borderColor:"rgba(27,31,35,.35)",boxShadow:"inset 0 0.15em 0.3em rgba(27,31,35,.15)"}}},"View Raw"))),d?c.jsx(Da,{highlights:d}):h?c.jsx(Ca,{path:g,uri:h}):c.jsx(Ea,null))}function ha(){var a=Q(["\n .code-listing {\n background: #fbfdff;\n color: #383a42;\n }\n .code-comment,\n .code-quote {\n color: #a0a1a7;\n font-style: italic;\n }\n .code-doctag,\n .code-keyword,\n .code-link,\n .code-formula {\n color: #a626a4;\n }\n .code-section,\n .code-name,\n .code-selector-tag,\n .code-deletion,\n .code-subst {\n color: #e45649;\n }\n .code-literal {\n color: #0184bb;\n }\n .code-string,\n .code-regexp,\n .code-addition,\n .code-attribute,\n .code-meta-string {\n color: #50a14f;\n }\n .code-built_in,\n .code-class .code-title {\n color: #c18401;\n }\n .code-attr,\n .code-variable,\n .code-template-variable,\n .code-type,\n .code-selector-class,\n .code-selector-attr,\n .code-selector-pseudo,\n .code-number {\n color: #986801;\n }\n .code-symbol,\n .code-bullet,\n .code-meta,\n .code-selector-id,\n .code-title {\n color: #4078f2;\n }\n .code-emphasis {\n font-style: italic;\n }\n .code-strong {\n font-weight: bold;\n }\n"]);
|
||||||
|
ha=function(){return a};return a}function ia(){var a=Q(["\n html {\n box-sizing: border-box;\n }\n *,\n *:before,\n *:after {\n box-sizing: inherit;\n }\n\n html,\n body,\n #root {\n height: 100%;\n margin: 0;\n }\n\n body {\n ","\n font-size: 16px;\n line-height: 1.5;\n overflow-wrap: break-word;\n background: white;\n color: black;\n }\n\n code {\n ","\n }\n\n th,\n td {\n padding: 0;\n }\n\n select {\n font-size: inherit;\n }\n\n #root {\n display: flex;\n flex-direction: column;\n }\n"]);
|
||||||
|
ia=function(){return a};return a}function ja(a){var b=a.css;a=P(a,["css"]);return c.jsx("a",w({},a,{css:w({color:"#0076ff",textDecoration:"none",":hover":{textDecoration:"underline"}},b)}))}function Fa(){return c.jsx("header",{css:{marginTop:"2rem"}},c.jsx("h1",{css:{textAlign:"center",fontSize:"3rem",letterSpacing:"0.05em"}},c.jsx("a",{href:"/",css:{color:"#000",textDecoration:"none"}},"UNPKG")))}function Ga(a){var b=a.packageName,e=a.packageVersion,g=a.availableVersions;a=a.filename;var d=[];if("/"===
|
||||||
|
a)d.push(b);else{var h="/browse/"+b+"@"+e;d.push(c.jsx(ja,{href:h+"/"},b));b=a.replace(/^\/+/,"").replace(/\/+$/,"").split("/");a=b.pop();b.forEach(function(a){h+="/"+a;d.push(c.jsx(ja,{href:h+"/"},a))});d.push(a)}return c.jsx("header",{css:{display:"flex",flexDirection:"row",alignItems:"center","@media (max-width: 700px)":{flexDirection:"column-reverse",alignItems:"flex-start"}}},c.jsx("h1",{css:{fontSize:"1.5rem",fontWeight:"normal",flex:1,wordBreak:"break-all"}},c.jsx("nav",null,d.map(function(a,
|
||||||
|
b,e){return c.jsx(t.Fragment,{key:b},0!==b&&c.jsx("span",{css:{paddingLeft:5,paddingRight:5}},"/"),b===e.length-1?c.jsx("strong",null,a):a)}))),c.jsx(Ha,{packageVersion:e,availableVersions:g,onChange:function(a){window.location.href=window.location.href.replace("@"+e,"@"+a)}}))}function Ha(a){var b=a.onChange;return c.jsx("p",{css:{marginLeft:20,"@media (max-width: 700px)":{marginLeft:0,marginBottom:0}}},c.jsx("label",null,"Version:"," ",c.jsx("select",{name:"version",defaultValue:a.packageVersion,
|
||||||
|
onChange:function(a){b&&b(a.target.value)},css:{appearance:"none",cursor:"pointer",padding:"4px 24px 4px 8px",fontWeight:600,fontSize:"0.9em",color:"#24292e",border:"1px solid rgba(27,31,35,.2)",borderRadius:3,backgroundColor:"#eff3f6",backgroundImage:"url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAKCAYAAAC9vt6cAAAAAXNSR0IArs4c6QAAARFJREFUKBVjZAACNS39RhBNKrh17WI9o4quoT3Dn78HSNUMUs/CzOTI/O7Vi4dCYpJ3/jP+92BkYGAlyiBGhm8MjIxJt65e3MQM0vDu9YvLYmISILYZELOBxHABRkaGr0yMzF23r12YDFIDNgDEePv65SEhEXENBkYGFSAXuyGMjF8Z/jOsvX3tYiFIDwgwQSgIaaijnvj/P8M5IO8HsjiY/f//D4b//88A1SQhywG9jQr09PS4v/1mPAeUUPzP8B8cJowMjL+Bqu6xMQmaXL164AuyDgwDQJLa2qYSP//9vARkCoMVMzK8YeVkNbh+9uxzMB+JwGoASF5Vx0jz/98/18BqmZi171w9D2EjaaYKEwAEK00XQLdJuwAAAABJRU5ErkJggg==)",
|
||||||
|
backgroundPosition:"right 8px center",backgroundRepeat:"no-repeat",backgroundSize:"auto 25%",":hover":{backgroundColor:"#e6ebf1",borderColor:"rgba(27,31,35,.35)"},":active":{backgroundColor:"#e9ecef",borderColor:"rgba(27,31,35,.35)",boxShadow:"inset 0 0.15em 0.3em rgba(27,31,35,.15)"}}},a.availableVersions.map(function(a){return c.jsx("option",{key:a,value:a},a)}))))}function Ia(a){var b=a.packageName,e=a.packageVersion;a=a.target;return"directory"===a.type?c.jsx(ca,{path:a.path,details:a.details}):
|
||||||
|
"file"===a.type?c.jsx(fa,{packageName:b,packageVersion:e,path:a.path,details:a.details}):null}function ka(a){var b=a.packageName,e=a.packageVersion,g=a.availableVersions;g=void 0===g?[]:g;var d=a.filename;a=a.target;return c.jsx(t.Fragment,null,c.jsx(c.Global,{styles:Ja}),c.jsx(c.Global,{styles:Ka}),c.jsx("div",{css:{flex:"1 0 auto"}},c.jsx("div",{css:{maxWidth:940,padding:"0 20px",margin:"0 auto"}},c.jsx(Fa,null)),c.jsx("div",{css:{maxWidth:940,padding:"0 20px",margin:"0 auto"}},c.jsx(Ga,{packageName:b,
|
||||||
|
packageVersion:e,availableVersions:g,filename:d})),c.jsx("div",{css:{maxWidth:940,padding:"0 20px",margin:"0 auto","@media (max-width: 700px)":{padding:0,margin:0}}},c.jsx(Ia,{packageName:b,packageVersion:e,target:a}))),c.jsx("footer",{css:{marginTop:"5rem",background:"black",color:"#aaa"}},c.jsx("div",{css:{maxWidth:940,padding:"10px 20px",margin:"0 auto",display:"flex",flexDirection:"row",alignItems:"center",justifyContent:"space-between"}},c.jsx("p",null,c.jsx("span",null,"Build: ","a7ebffa")),
|
||||||
|
c.jsx("p",null,c.jsx("span",null,"\u00a9 ",(new Date).getFullYear()," UNPKG")),c.jsx("p",{css:{fontSize:"1.5rem"}},c.jsx("a",{href:"https://twitter.com/unpkg",css:{color:"#aaa",display:"inline-block",":hover":{color:"white"}}},c.jsx(za,null)),c.jsx("a",{href:"https://github.com/mjackson/unpkg",css:{color:"#aaa",display:"inline-block",":hover":{color:"white"},marginLeft:"1rem"}},c.jsx(Aa,null))))))}var la="default"in t?t["default"]:t;A=A&&A.hasOwnProperty("default")?A["default"]:A;var La="undefined"!==
|
||||||
|
typeof globalThis?globalThis:"undefined"!==typeof window?window:"undefined"!==typeof global?global:"undefined"!==typeof self?self:{},m=D(function(a,b){function c(a){if("object"===typeof a&&null!==a){var b=a.$$typeof;switch(b){case d:switch(a=a.type,a){case l:case f:case r:case m:case k:case v:return a;default:switch(a=a&&a.$$typeof,a){case p:case n:case q:return a;default:return b}}case x:case u:case h:return b}}}function g(a){return c(a)===f}Object.defineProperty(b,"__esModule",{value:!0});var d=
|
||||||
|
(a="function"===typeof Symbol&&Symbol.for)?Symbol.for("react.element"):60103,h=a?Symbol.for("react.portal"):60106,r=a?Symbol.for("react.fragment"):60107,k=a?Symbol.for("react.strict_mode"):60108,m=a?Symbol.for("react.profiler"):60114,q=a?Symbol.for("react.provider"):60109,p=a?Symbol.for("react.context"):60110,l=a?Symbol.for("react.async_mode"):60111,f=a?Symbol.for("react.concurrent_mode"):60111,n=a?Symbol.for("react.forward_ref"):60112,v=a?Symbol.for("react.suspense"):60113,u=a?Symbol.for("react.memo"):
|
||||||
|
60115,x=a?Symbol.for("react.lazy"):60116;b.typeOf=c;b.AsyncMode=l;b.ConcurrentMode=f;b.ContextConsumer=p;b.ContextProvider=q;b.Element=d;b.ForwardRef=n;b.Fragment=r;b.Lazy=x;b.Memo=u;b.Portal=h;b.Profiler=m;b.StrictMode=k;b.Suspense=v;b.isValidElementType=function(a){return"string"===typeof a||"function"===typeof a||a===r||a===f||a===m||a===k||a===v||"object"===typeof a&&null!==a&&(a.$$typeof===x||a.$$typeof===u||a.$$typeof===q||a.$$typeof===p||a.$$typeof===n)};b.isAsyncMode=function(a){return g(a)||
|
||||||
|
c(a)===l};b.isConcurrentMode=g;b.isContextConsumer=function(a){return c(a)===p};b.isContextProvider=function(a){return c(a)===q};b.isElement=function(a){return"object"===typeof a&&null!==a&&a.$$typeof===d};b.isForwardRef=function(a){return c(a)===n};b.isFragment=function(a){return c(a)===r};b.isLazy=function(a){return c(a)===x};b.isMemo=function(a){return c(a)===u};b.isPortal=function(a){return c(a)===h};b.isProfiler=function(a){return c(a)===m};b.isStrictMode=function(a){return c(a)===k};b.isSuspense=
|
||||||
|
function(a){return c(a)===v}});R(m);var na=D(function(a,b){(function(){function a(a){if("object"===typeof a&&null!==a){var b=a.$$typeof;switch(b){case h:switch(a=a.type,a){case f:case n:case k:case q:case m:case u:return a;default:switch(a=a&&a.$$typeof,a){case l:case v:case p:return a;default:return b}}case I:case x:case r:return b}}}function c(b){return a(b)===n}Object.defineProperty(b,"__esModule",{value:!0});var d="function"===typeof Symbol&&Symbol.for,h=d?Symbol.for("react.element"):60103,r=
|
||||||
|
d?Symbol.for("react.portal"):60106,k=d?Symbol.for("react.fragment"):60107,m=d?Symbol.for("react.strict_mode"):60108,q=d?Symbol.for("react.profiler"):60114,p=d?Symbol.for("react.provider"):60109,l=d?Symbol.for("react.context"):60110,f=d?Symbol.for("react.async_mode"):60111,n=d?Symbol.for("react.concurrent_mode"):60111,v=d?Symbol.for("react.forward_ref"):60112,u=d?Symbol.for("react.suspense"):60113,x=d?Symbol.for("react.memo"):60115,I=d?Symbol.for("react.lazy"):60116;d=function(){};var Ma=function(a){for(var b=
|
||||||
|
arguments.length,f=Array(1<b?b-1:0),c=1;c<b;c++)f[c-1]=arguments[c];var n=0;b="Warning: "+a.replace(/%s/g,function(){return f[n++]});"undefined"!==typeof console&&console.warn(b);try{throw Error(b);}catch(Xa){}},Na=d=function(a,b){if(void 0===b)throw Error("`lowPriorityWarning(condition, format, ...args)` requires a warning message argument");if(!a){for(var f=arguments.length,c=Array(2<f?f-2:0),n=2;n<f;n++)c[n-2]=arguments[n];Ma.apply(void 0,[b].concat(c))}},ma=!1;b.typeOf=a;b.AsyncMode=f;b.ConcurrentMode=
|
||||||
|
n;b.ContextConsumer=l;b.ContextProvider=p;b.Element=h;b.ForwardRef=v;b.Fragment=k;b.Lazy=I;b.Memo=x;b.Portal=r;b.Profiler=q;b.StrictMode=m;b.Suspense=u;b.isValidElementType=function(a){return"string"===typeof a||"function"===typeof a||a===k||a===n||a===q||a===m||a===u||"object"===typeof a&&null!==a&&(a.$$typeof===I||a.$$typeof===x||a.$$typeof===p||a.$$typeof===l||a.$$typeof===v)};b.isAsyncMode=function(b){ma||(ma=!0,Na(!1,"The ReactIs.isAsyncMode() alias has been deprecated, and will be removed in React 17+. Update your code to use ReactIs.isConcurrentMode() instead. It has the exact same API."));
|
||||||
|
return c(b)||a(b)===f};b.isConcurrentMode=c;b.isContextConsumer=function(b){return a(b)===l};b.isContextProvider=function(b){return a(b)===p};b.isElement=function(a){return"object"===typeof a&&null!==a&&a.$$typeof===h};b.isForwardRef=function(b){return a(b)===v};b.isFragment=function(b){return a(b)===k};b.isLazy=function(b){return a(b)===I};b.isMemo=function(b){return a(b)===x};b.isPortal=function(b){return a(b)===r};b.isProfiler=function(b){return a(b)===q};b.isStrictMode=function(b){return a(b)===
|
||||||
|
m};b.isSuspense=function(b){return a(b)===u}})()});R(na);var oa=D(function(a){a.exports=na}),pa=Object.getOwnPropertySymbols,Oa=Object.prototype.hasOwnProperty,Pa=Object.prototype.propertyIsEnumerable,Qa=function(){try{if(!Object.assign)return!1;var a=new String("abc");a[5]="de";if("5"===Object.getOwnPropertyNames(a)[0])return!1;var b={};for(a=0;10>a;a++)b["_"+String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var c={};"abcdefghijklmnopqrst".split("").forEach(function(a){c[a]=
|
||||||
|
a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},c)).join("")?!1:!0}catch(g){return!1}}()?Object.assign:function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var c=Object(a);for(var g,d=1;d<arguments.length;d++){var h=Object(arguments[d]);for(var r in h)Oa.call(h,r)&&(c[r]=h[r]);if(pa){g=pa(h);for(var k=0;k<g.length;k++)Pa.call(h,g[k])&&(c[g[k]]=h[g[k]])}}return c},K=function(){},L={},ua=Function.call.bind(Object.prototype.hasOwnProperty);
|
||||||
|
K=function(a){a="Warning: "+a;"undefined"!==typeof console&&console.error(a);try{throw Error(a);}catch(b){}};J.resetWarningCache=function(){L={}};var Ra=Function.call.bind(Object.prototype.hasOwnProperty),C=function(){};C=function(a){a="Warning: "+a;"undefined"!==typeof console&&console.error(a);try{throw Error(a);}catch(b){}};var Sa=function(a,b){function c(a,b){return a===b?0!==a||1/a===1/b:a!==a&&b!==b}function g(a){this.message=a;this.stack=""}function d(a){function c(c,n,v,d,e,u,h){d=d||"<<anonymous>>";
|
||||||
|
u=u||v;if("SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"!==h){if(b)throw c=Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use `PropTypes.checkPropTypes()` to call them. Read more at http://fb.me/use-check-prop-types"),c.name="Invariant Violation",c;"undefined"!==typeof console&&(h=d+":"+v,!f[h]&&3>l&&(C("You are manually calling a React.PropTypes validation function for the `"+u+"` prop on `"+d+"`. This is deprecated and will throw in the standalone `prop-types` package. You may be seeing this warning due to a third-party PropTypes library. See https://fb.me/react-warning-dont-call-proptypes for details."),
|
||||||
|
f[h]=!0,l++))}return null==n[v]?c?null===n[v]?new g("The "+e+" `"+u+"` is marked as required "+("in `"+d+"`, but its value is `null`.")):new g("The "+e+" `"+u+"` is marked as required in "+("`"+d+"`, but its value is `undefined`.")):null:a(n,v,d,e,u)}var f={},l=0,d=c.bind(null,!1);d.isRequired=c.bind(null,!0);return d}function h(a){return d(function(b,c,f,d,l,e){b=b[c];return k(b)!==a?(b=m(b),new g("Invalid "+d+" `"+l+"` of type "+("`"+b+"` supplied to `"+f+"`, expected ")+("`"+a+"`."))):null})}function r(b){switch(typeof b){case "number":case "string":case "undefined":return!0;
|
||||||
|
case "boolean":return!b;case "object":if(Array.isArray(b))return b.every(r);if(null===b||a(b))return!0;var c=b&&(p&&b[p]||b["@@iterator"]);var f="function"===typeof c?c:void 0;if(f)if(c=f.call(b),f!==b.entries)for(;!(b=c.next()).done;){if(!r(b.value))return!1}else for(;!(b=c.next()).done;){if((b=b.value)&&!r(b[1]))return!1}else return!1;return!0;default:return!1}}function k(a){var b=typeof a;return Array.isArray(a)?"array":a instanceof RegExp?"object":"symbol"===b||a&&("Symbol"===a["@@toStringTag"]||
|
||||||
|
"function"===typeof Symbol&&a instanceof Symbol)?"symbol":b}function m(a){if("undefined"===typeof a||null===a)return""+a;var b=k(a);if("object"===b){if(a instanceof Date)return"date";if(a instanceof RegExp)return"regexp"}return b}function q(a){a=m(a);switch(a){case "array":case "object":return"an "+a;case "boolean":case "date":case "regexp":return"a "+a;default:return a}}var p="function"===typeof Symbol&&Symbol.iterator,l={array:h("array"),bool:h("boolean"),func:h("function"),number:h("number"),object:h("object"),
|
||||||
|
string:h("string"),symbol:h("symbol"),any:d(G),arrayOf:function(a){return d(function(b,c,f,d,l){if("function"!==typeof a)return new g("Property `"+l+"` of component `"+f+"` has invalid PropType notation inside arrayOf.");b=b[c];if(!Array.isArray(b))return b=k(b),new g("Invalid "+d+" `"+l+"` of type "+("`"+b+"` supplied to `"+f+"`, expected an array."));for(c=0;c<b.length;c++){var n=a(b,c,f,d,l+"["+c+"]","SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");if(n instanceof Error)return n}return null})},
|
||||||
|
element:function(){return d(function(b,c,d,l,e){b=b[c];return a(b)?null:(b=k(b),new g("Invalid "+l+" `"+e+"` of type "+("`"+b+"` supplied to `"+d+"`, expected a single ReactElement.")))})}(),elementType:function(){return d(function(a,b,c,d,l){a=a[b];return oa.isValidElementType(a)?null:(a=k(a),new g("Invalid "+d+" `"+l+"` of type "+("`"+a+"` supplied to `"+c+"`, expected a single ReactElement type.")))})}(),instanceOf:function(a){return d(function(b,c,f,d,l){if(!(b[c]instanceof a)){var n=a.name||
|
||||||
|
"<<anonymous>>";b=b[c];b=b.constructor&&b.constructor.name?b.constructor.name:"<<anonymous>>";return new g("Invalid "+d+" `"+l+"` of type "+("`"+b+"` supplied to `"+f+"`, expected ")+("instance of `"+n+"`."))}return null})},node:function(){return d(function(a,b,c,d,l){return r(a[b])?null:new g("Invalid "+d+" `"+l+"` supplied to "+("`"+c+"`, expected a ReactNode."))})}(),objectOf:function(a){return d(function(b,c,f,d,l){if("function"!==typeof a)return new g("Property `"+l+"` of component `"+f+"` has invalid PropType notation inside objectOf.");
|
||||||
|
b=b[c];c=k(b);if("object"!==c)return new g("Invalid "+d+" `"+l+"` of type "+("`"+c+"` supplied to `"+f+"`, expected an object."));for(var n in b)if(Ra(b,n)&&(c=a(b,n,f,d,l+"."+n,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"),c instanceof Error))return c;return null})},oneOf:function(a){return Array.isArray(a)?d(function(b,f,d,l,e){b=b[f];for(f=0;f<a.length;f++)if(c(b,a[f]))return null;f=JSON.stringify(a,function(a,b){return"symbol"===m(b)?String(b):b});return new g("Invalid "+l+" `"+e+"` of value `"+
|
||||||
|
String(b)+"` "+("supplied to `"+d+"`, expected one of "+f+"."))}):(1<arguments.length?C("Invalid arguments supplied to oneOf, expected an array, got "+arguments.length+" arguments. A common mistake is to write oneOf(x, y, z) instead of oneOf([x, y, z])."):C("Invalid argument supplied to oneOf, expected an array."),G)},oneOfType:function(a){if(!Array.isArray(a))return C("Invalid argument supplied to oneOfType, expected an instance of array."),G;for(var b=0;b<a.length;b++){var c=a[b];if("function"!==
|
||||||
|
typeof c)return C("Invalid argument supplied to oneOfType. Expected an array of check functions, but received "+q(c)+" at index "+b+"."),G}return d(function(b,c,f,d,l){for(var e=0;e<a.length;e++)if(null==(0,a[e])(b,c,f,d,l,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"))return null;return new g("Invalid "+d+" `"+l+"` supplied to "+("`"+f+"`."))})},shape:function(a){return d(function(b,c,d,l,f){b=b[c];c=k(b);if("object"!==c)return new g("Invalid "+l+" `"+f+"` of type `"+c+"` "+("supplied to `"+d+"`, expected `object`."));
|
||||||
|
for(var e in a)if(c=a[e])if(c=c(b,e,d,l,f+"."+e,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"))return c;return null})},exact:function(a){return d(function(b,c,d,l,f){var e=b[c],n=k(e);if("object"!==n)return new g("Invalid "+l+" `"+f+"` of type `"+n+"` "+("supplied to `"+d+"`, expected `object`."));n=Qa({},b[c],a);for(var h in n){n=a[h];if(!n)return new g("Invalid "+l+" `"+f+"` key `"+h+"` supplied to `"+d+"`.\nBad object: "+JSON.stringify(b[c],null," ")+"\nValid keys: "+JSON.stringify(Object.keys(a),
|
||||||
|
null," "));if(n=n(e,h,d,l,f+"."+h,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"))return n}return null})}};g.prototype=Error.prototype;l.checkPropTypes=J;l.resetWarningCache=J.resetWarningCache;return l.PropTypes=l};m=D(function(a){a.exports=Sa(oa.isElement,!0)});var Ta=Object.assign||function(a){for(var b=1;b<arguments.length;b++){var c=arguments[b],g;for(g in c)Object.prototype.hasOwnProperty.call(c,g)&&(a[g]=c[g])}return a},Ua={border:0,clip:"rect(0 0 0 0)",height:"1px",width:"1px",margin:"-1px",
|
||||||
|
padding:0,overflow:"hidden",position:"absolute"},H=function(a){return la.createElement("div",Ta({style:Ua},a))},qa=D(function(a){(function(b,c){a.exports=c()})(La,function(){function a(a){if(!a)return!0;if(!d(a)||0!==a.length)for(var b in a)if(q.call(a,b))return!1;return!0}function c(a){return"number"===typeof a||"[object Number]"===t.call(a)}function g(a){return"string"===typeof a||"[object String]"===t.call(a)}function d(a){return"object"===typeof a&&"number"===typeof a.length&&"[object Array]"===
|
||||||
|
t.call(a)}function h(a){var b=parseInt(a);return b.toString()===a?b:a}function m(b,d,e,k){c(d)&&(d=[d]);if(a(d))return b;if(g(d))return m(b,d.split("."),e,k);var f=h(d[0]);if(1===d.length)return d=b[f],void 0!==d&&k||(b[f]=e),d;void 0===b[f]&&(c(f)?b[f]=[]:b[f]={});return m(b[f],d.slice(1),e,k)}function k(b,f){c(f)&&(f=[f]);if(!a(b)){if(a(f))return b;if(g(f))return k(b,f.split("."));var e=h(f[0]),l=b[e];if(1===f.length)void 0!==l&&(d(b)?b.splice(e,1):delete b[e]);else if(void 0!==b[e])return k(b[e],
|
||||||
|
f.slice(1));return b}}var t=Object.prototype.toString,q=Object.prototype.hasOwnProperty,p={ensureExists:function(a,b,c){return m(a,b,c,!0)},set:function(a,b,c,d){return m(a,b,c,d)},insert:function(a,b,c,e){var f=p.get(a,b);e=~~e;d(f)||(f=[],p.set(a,b,f));f.splice(e,0,c)},empty:function(b,f){if(a(f))return b;if(!a(b)){var e,h;if(!(e=p.get(b,f)))return b;if(g(e))return p.set(b,f,"");if("boolean"===typeof e||"[object Boolean]"===t.call(e))return p.set(b,f,!1);if(c(e))return p.set(b,f,0);if(d(e))e.length=
|
||||||
|
0;else if("object"===typeof e&&"[object Object]"===t.call(e))for(h in e)q.call(e,h)&&delete e[h];else return p.set(b,f,null)}},push:function(a,b){var c=p.get(a,b);d(c)||(c=[],p.set(a,b,c));c.push.apply(c,Array.prototype.slice.call(arguments,2))},coalesce:function(a,b,c){for(var d,e=0,f=b.length;e<f;e++)if(void 0!==(d=p.get(a,b[e])))return d;return c},get:function(b,d,e){c(d)&&(d=[d]);if(a(d))return b;if(a(b))return e;if(g(d))return p.get(b,d.split("."),e);var f=h(d[0]);return 1===d.length?void 0===
|
||||||
|
b[f]?e:b[f]:p.get(b[f],d.slice(1),e)},del:function(a,b){return k(a,b)}};return p})});var ra=function(a){return function(b){return typeof b===a}};var Va=function(a,b){var c=1,g=b||function(a,b){return b};"-"===a[0]&&(c=-1,a=a.substr(1));return function(b,e){var d;b=g(a,qa.get(b,a));e=g(a,qa.get(e,a));b<e&&(d=-1);b>e&&(d=1);b===e&&(d=0);return d*c}};var da=function(){var a=Array.prototype.slice.call(arguments),b=a.filter(ra("string")),c=a.filter(ra("function"))[0];return function(a,d){for(var e=b.length,
|
||||||
|
g=0,k=0;0===g&&k<e;)g=Va(b[k],c)(a,d),k++;return g}};let sa="B kB MB GB TB PB EB ZB YB".split(" "),ta=(a,b)=>{let c=a;"string"===typeof b?c=a.toLocaleString(b):!0===b&&(c=a.toLocaleString());return c};var ea=(a,b)=>{if(!Number.isFinite(a))throw new TypeError(`Expected a finite number, got ${typeof a}: ${a}`);b=Object.assign({},b);if(b.signed&&0===a)return" 0 B";var c=0>a;let g=c?"-":b.signed?"+":"";c&&(a=-a);if(1>a)return a=ta(a,b.locale),g+a+" B";c=Math.min(Math.floor(Math.log10(a)/3),sa.length-
|
||||||
|
1);a=Number((a/Math.pow(1E3,c)).toPrecision(3));a=ta(a,b.locale);return g+a+" "+sa[c]},W={color:void 0,size:void 0,className:void 0,style:void 0,attr:void 0},V=t.createContext&&t.createContext(W),z=function(){z=Object.assign||function(a){for(var b,c=1,g=arguments.length;c<g;c++){b=arguments[c];for(var d in b)Object.prototype.hasOwnProperty.call(b,d)&&(a[d]=b[d])}return a};return z.apply(this,arguments)},Y=function(a){return E({tag:"svg",attr:{viewBox:"0 0 12 16"},child:[{tag:"path",attr:{fillRule:"evenodd",
|
||||||
|
d:"M8.5 1H1c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V4.5L8.5 1zM11 14H1V2h7l3 3v9zM5 6.98L3.5 8.5 5 10l-.5 1L2 8.5 4.5 6l.5.98zM7.5 6L10 8.5 7.5 11l-.5-.98L8.5 8.5 7 7l.5-1z"}}]})(a)};Y.displayName="GoFileCode";var Z=function(a){return E({tag:"svg",attr:{viewBox:"0 0 14 16"},child:[{tag:"path",attr:{fillRule:"evenodd",d:"M13 4H7V3c0-.66-.31-1-1-1H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zM6 4H1V3h5v1z"}}]})(a)};Z.displayName="GoFileDirectory";var X=function(a){return E({tag:"svg",
|
||||||
|
attr:{viewBox:"0 0 12 16"},child:[{tag:"path",attr:{fillRule:"evenodd",d:"M6 5H2V4h4v1zM2 8h7V7H2v1zm0 2h7V9H2v1zm0 2h7v-1H2v1zm10-7.5V14c0 .55-.45 1-1 1H1c-.55 0-1-.45-1-1V2c0-.55.45-1 1-1h7.5L12 4.5zM11 5L8 2H1v12h10V5z"}}]})(a)};X.displayName="GoFile";var ba=function(a){return E({tag:"svg",attr:{viewBox:"0 0 496 512"},child:[{tag:"path",attr:{d:"M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"}}]})(a)};
|
||||||
|
ba.displayName="FaGithub";var aa=function(a){return E({tag:"svg",attr:{viewBox:"0 0 512 512"},child:[{tag:"path",attr:{d:"M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"}}]})(a)};
|
||||||
|
aa.displayName="FaTwitter";var N={color:"#0076ff",textDecoration:"none",":hover":{textDecoration:"underline"}},y={paddingTop:6,paddingRight:3,paddingBottom:6,paddingLeft:3,borderTop:"1px solid #eaecef"},M=w({},y,{color:"#424242",width:17,paddingRight:2,paddingLeft:10,"@media (max-width: 700px)":{paddingLeft:20}}),O=w({},y,{textAlign:"right",paddingRight:10,"@media (max-width: 700px)":{paddingRight:20}});ca.propTypes={path:m.string.isRequired,details:m.objectOf(m.shape({path:m.string.isRequired,type:m.oneOf(["directory",
|
||||||
|
"file"]).isRequired,contentType:m.string,integrity:m.string,size:m.number})).isRequired};fa.propTypes={path:m.string.isRequired,details:m.shape({contentType:m.string.isRequired,highlights:m.arrayOf(m.string),uri:m.string,integrity:m.string.isRequired,language:m.string.isRequired,size:m.number.isRequired}).isRequired};var Ja=c.css(ia(),'\nfont-family: -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n "Roboto",\n "Oxygen",\n "Ubuntu",\n "Cantarell",\n "Fira Sans",\n "Droid Sans",\n "Helvetica Neue",\n sans-serif;\n',
|
||||||
|
"\nfont-family: Menlo,\n Monaco,\n Lucida Console,\n Liberation Mono,\n DejaVu Sans Mono,\n Bitstream Vera Sans Mono,\n Courier New,\n monospace;\n"),Ka=c.css(ha()),Wa=m.shape({path:m.string.isRequired,type:m.oneOf(["directory","file"]).isRequired,details:m.object.isRequired});ka.propTypes={packageName:m.string.isRequired,packageVersion:m.string.isRequired,availableVersions:m.arrayOf(m.string),filename:m.string.isRequired,target:Wa.isRequired};A.hydrate(la.createElement(ka,window.__DATA__||
|
||||||
|
{}),document.getElementById("root"))})(React,ReactDOM,emotionCore);
|
||||||
|
</script></body></html>
|
||||||
@@ -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()
|
||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
@charset "UTF-8";/*!
|
@charset "UTF-8";/*!
|
||||||
* Bootstrap v5.0.2 (https://getbootstrap.com/)
|
* Bootstrap v5.2.0 (https://getbootstrap.com/)
|
||||||
* Copyright 2011-2021 The Bootstrap Authors
|
* Copyright 2011-2022 The Bootstrap Authors
|
||||||
* Copyright 2011-2021 Twitter, Inc.
|
* Copyright 2011-2022 Twitter, Inc.
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
*/:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0))}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:30Line truncated
|
*/:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):noLine truncated
|
||||||
/*# sourceMappingURL=bootstrap.min.css.map */
|
/*# sourceMappingURL=bootstrap.min.css.map */
|
||||||
Reference in new issue
Block a user