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