diff --git a/src/Dockerfile b/src/Dockerfile index 0dc5c2b..99c874e 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -5,7 +5,7 @@ RUN dotnet restore COPY ./MyPrayerJournal ./ RUN dotnet publish -c Release -r linux-x64 -RUN rm bin/Release/net8.0/linux-x64/publish/appsettings.*.json +RUN rm bin/Release/net8.0/linux-x64/publish/appsettings.*.json || true FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine as final WORKDIR /app diff --git a/src/MyPrayerJournal/Handlers.fs b/src/MyPrayerJournal/Handlers.fs index 4816900..7672e25 100644 --- a/src/MyPrayerJournal/Handlers.fs +++ b/src/MyPrayerJournal/Handlers.fs @@ -278,13 +278,16 @@ module Components = requireUser >=> renderComponent [ RequestId.ofString requestId |> Views.Journal.snooze ] -/// / URL +/// / URL and documentation module Home = // GET / let home : HttpHandler = partialStatic "Welcome!" Views.Layout.home + // GET /docs + let docs : HttpHandler = + partialStatic "Documentation" Views.Docs.index /// /journal URL module Journal = @@ -296,7 +299,7 @@ module Journal = |> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName) |> Option.map (_.Value) |> 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 $"{title} Prayer Journal" (Views.Journal.journal usr) next ctx } @@ -530,6 +533,7 @@ let routes = [ routef "request/%s/snooze" Components.snooze ] ] + GET_HEAD [ route "/docs" Home.docs ] GET_HEAD [ route "/journal" Journal.journal ] subRoute "/legal/" [ GET_HEAD [ diff --git a/src/MyPrayerJournal/MyPrayerJournal.fsproj b/src/MyPrayerJournal/MyPrayerJournal.fsproj index d8d415c..970d550 100644 --- a/src/MyPrayerJournal/MyPrayerJournal.fsproj +++ b/src/MyPrayerJournal/MyPrayerJournal.fsproj @@ -16,6 +16,7 @@ + diff --git a/src/MyPrayerJournal/Views/Docs.fs b/src/MyPrayerJournal/Views/Docs.fs new file mode 100644 index 0000000..aa24327 --- /dev/null +++ b/src/MyPrayerJournal/Views/Docs.fs @@ -0,0 +1,184 @@ +module MyPrayerJournal.Views.Docs + +open Giraffe.ViewEngine + +/// The "About myPrayerJournal" section +let private about = [ + h3 [ _class "mb-3 mt-4" ] [ rawText "About myPrayerJournal" ] + p [] [ + rawText "Journaling has a long history; it helps people remember what happened, and the act of writing helps " + rawText "people think about what happened and process it. A prayer journal is not a new concept; it helps you " + rawText "keep track of the requests for which you've prayed, you can use it to pray over things repeatedly, " + rawText "and you can write the result when the answer comes "; em [] [ rawText "(or it was “no”)" ] + rawText "." + ] + p [] [ + rawText "myPrayerJournal was borne of out of a personal desire " + a [ _href "https://daniel.summershome.org"; _target "_blank"; _rel "noopener" ] [ rawText "Daniel" ] + rawText " had to have something that would help him with his prayer life. When it’s time to pray, " + rawText "it’s not really time to use an app, so the design goal here is to keep it simple and " + rawText "unobtrusive. It will also help eliminate some of the downsides to a paper prayer journal, like not " + rawText "remembering whether you’ve prayed for a request, or running out of room to write another update " + rawText "on one." + ] +] + +/// The "Signing Up" section +let private signUp = [ + h3 [ _class "mb-3 mt-4" ] [ rawText "Signing Up" ] + p [] [ + rawText "myPrayerJournal uses login services using Google or Microsoft accounts. The only information the " + rawText "application stores in its database is your user Id token it receives from these services, so there " + rawText "are no permissions you should have to accept from these provider other than establishing that you can " + rawText "log on with that account. Because of this, you’ll want to pick the same one each time; the " + rawText "tokens between the two accounts are different, even if you use the same e-mail address to log on to " + rawText "both." + ] +] + +/// The "Your Prayer Journal" section +let private yourJournal = [ + h3 [ _class "mb-3 mt-4" ] [ rawText "Your Prayer Journal" ] + p [] [ + rawText "Your current requests will be presented in columns (usually three, but it could be more or less, " + rawText "depending on the size of your screen or device). Each request is in its own card, and the buttons at " + rawText "the top of each card apply to that request. The last line of each request also tells you how long it " + rawText "has been since anything has been done on that request. Any time you see something like “a few " + rawText "minutes ago,” you can hover over that to see the actual date/time the action was taken." + ] +] + +/// The "Adding a Request" section +let private addRequest = [ + h3 [ _class "mb-3 mt-4" ] [ rawText "Adding a Request" ] + p [] [ + rawText "To add a request, click the “Add a New Request” button at the top of your journal. Then, " + rawText "enter the text of the request as you see fit; there is no right or wrong way, and you are the only " + rawText "person who will see the text you enter. When you save the request, it will go to the bottom of the " + rawText "list of requests." + ] +] + +/// The "Setting Request Recurrence" section +let private setRecurrence = [ + h3 [ _class "mb-3 mt-4" ] [ rawText "Setting Request Recurrence" ] + p [] [ + rawText "When you add or update a request, you can choose whether requests go to the bottom of the journal " + rawText "once they have been marked “Prayed” or whether they will reappear after a delay. You can " + rawText "set recurrence in terms of hours, days, or weeks, but it cannot be longer than 365 days. If you " + rawText "decide you want a request to reappear sooner, you can skip the current delay; click the " + rawText "“Active” menu link, find the request in the list (likely near the bottom), and click the " + rawText "“Show Now” button." + ] +] + +/// The "Praying for Requests" section +let private praying = [ + h3 [ _class "mb-3 mt-4" ] [ rawText "Praying for Requests" ] + p [] [ + rawText "The first button for each request has a checkmark icon; clicking this button will mark the request as " + rawText "“Prayed” and move it to the bottom of the list (or off, if you’ve set a recurrence " + rawText "period for the request). This allows you, if you’re praying through your requests, to start at " + rawText "the top left (with the request that it’s been the longest since you’ve prayed) and click " + rawText "the button as you pray; when the request move below or away, the next-least-recently-prayed request " + rawText "will take the top spot." + ] +] + +/// The "Editing Requests" section +let private editing = [ + h3 [ _class "mb-3 mt-4" ] [ rawText "Editing Requests" ] + p [] [ + rawText "The second button for each request has a pencil icon. This allows you to edit the text of the " + rawText "request, pretty much the same way you entered it; it starts with the current text, and you can add to " + rawText "it, modify it, or completely replace it. By default, updates will go in with an “Updated” " + rawText "status; you have the option to also mark this update as “Prayed” or " + rawText "“Answered”. Answered requests will drop off the journal list." + ] +] + +/// The "Adding Notes" section +let private addNotes = [ + h3 [ _class "mb-3 mt-4" ] [ rawText "Adding Notes" ] + p [] [ + rawText "The third button for each request has an icon that looks like a speech bubble with lines on it; this " + rawText "lets you record notes about the request. If there is something you want to record that doesn’t " + rawText "change the text of the request, this is the place to do it. For example, you may be praying for a " + rawText "long-term health issue, and that person tells you that their status is the same; or, you may want to " + rawText "record something God said to you while you were praying for that request." + ] +] + +/// The "Snoozing Requests" section +let private snoozing = [ + h3 [ _class "mb-3 mt-4" ] [ rawText "Snoozing Requests" ] + p [] [ + rawText "There may be a time where a request does not need to appear. The fourth button, with the clock icon, " + rawText "allows you to snooze requests until the day you specify. Additionally, if you have any snoozed " + rawText "requests, a “Snoozed” menu item will appear next to the “Journal” one; this " + rawText "page allows you to see what requests are snoozed, and return them to your journal by canceling the " + rawText "snooze." + ] +] + +/// The "Viewing a Request and Its History" section +let private viewing = [ + h3 [ _class "mb-3 mt-4" ] [ rawText "Viewing a Request and Its History" ] + p [] [ + rawText "myPrayerJournal tracks all of the actions related to a request; from the “Active” and " + rawText "“Answered” menu links (and “Snoozed”, if it’s showing), there is a " + rawText "“View Full Request” button. That page will show the current text of the request; how many " + rawText "times it has been marked as prayed; how long it has been an active request; and a log of all updates, " + rawText "prayers, and notes you have recorded. That log is listed from most recent to least recent; if you " + rawText "want to read it chronologically, press the “End” key on your keyboard and read it from " + rawText "the bottom up." + ] + p [] [ + rawText "The “Active” link will show all requests that have not yet been marked answered, " + rawText "including snoozed and recurring requests. If requests are snoozed, or in a recurrence period off the " + rawText "journal, there will be a button where you can return the request to the list (either “Cancel " + rawText "Snooze” or “Show Now”). The “Answered” link shows all requests that " + rawText "have been marked answered. The “Snoozed” link only shows snoozed requests." + ] +] + +/// The "Final Notes" section +let private finalNotes = [ + h3 [ _class "mb-3 mt-4" ] [ rawText "Final Notes" ] + ul [] [ + li [] [ + rawText "If you encounter errors, please " + a [ _href "https://git.bitbadger.solutions/bit-badger/myPrayerJournal/issues"; _target "_blank" ] [ + rawText "file an issue" + ]; rawText " (or " + a [ _href "mailto:daniel@bitbadger.solutions?subject=myPrayerJournal+Issue" ] [ rawText "e-mail Daniel" ] + rawText " if you do not have an account on that server) with as much detail as possible. You can also " + rawText "provide suggestions, or browse the list of currently open issues." + ] + li [] [ + rawText "Prayer requests and their history are securely backed up nightly along with other Bit Badger " + rawText "Solutions data." + ] + li [] [ + rawText "Prayer changes things - most of all, the one doing the praying. I pray that this tool enables you " + rawText "to deepen and strengthen your prayer life." + ] + ] +] + +/// The documentation page +let index = + article [ _class "container mt-3" ] [ + h2 [ _class "mb-3" ] [ rawText "Documentation" ] + yield! about + yield! signUp + yield! yourJournal + yield! addRequest + yield! setRecurrence + yield! praying + yield! editing + yield! addNotes + yield! snoozing + yield! viewing + yield! finalNotes + ] diff --git a/src/MyPrayerJournal/Views/Layout.fs b/src/MyPrayerJournal/Views/Layout.fs index cdd0022..fe8d4f3 100644 --- a/src/MyPrayerJournal/Views/Layout.fs +++ b/src/MyPrayerJournal/Views/Layout.fs @@ -6,48 +6,44 @@ open Giraffe.ViewEngine.Accessibility /// The data needed to render a page-level view type PageRenderContext = - { /// Whether the user is authenticated - IsAuthenticated : bool - - /// Whether the user has snoozed requests - HasSnoozed : bool - - /// The current URL - CurrentUrl : string - - /// The title for the page to be rendered - PageTitle : string - - /// The content of the page - Content : XmlNode - } + { /// Whether the user is authenticated + IsAuthenticated: bool + + /// Whether the user has snoozed requests + HasSnoozed: bool + + /// The current URL + CurrentUrl: string + + /// The title for the page to be rendered + PageTitle: string + + /// The content of the page + Content: XmlNode } /// The home page let home = - article [ _class "container mt-3" ] [ - p [] [ rawText " " ] - p [] [ - str "myPrayerJournal is a place where individuals can record their prayer requests, record that they " - str "prayed for them, update them as God moves in the situation, and record a final answer received on " - str "that request. It also allows individuals to review their answered prayers." - ] - p [] [ - 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 " - rawText "also learn more about the site at the “Docs” link, also above." - ] - ] + article [ _class "container mt-3" ] + [ p [] [ rawText " " ] + p [] + [ str "myPrayerJournal is a place where individuals can record their prayer requests, record that they " + str "prayed for them, update them as God moves in the situation, and record a final answer received on " + str "that request. It also allows individuals to review their answered prayers." ] + p [] + [ 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 " + rawText "also learn more about the site at the “Docs” link, also above." ] ] /// The default navigation bar, which will load the items on page load, and whenever a refresh event occurs let private navBar ctx = - nav [ _class "navbar navbar-dark"; _roleNavigation ] [ - div [ _class "container-fluid" ] [ - pageLink "/" [ _class "navbar-brand" ] [ - span [ _class "m" ] [ str "my" ] - span [ _class "p" ] [ str "Prayer" ] - span [ _class "j" ] [ str "Journal" ] - ] - seq { + nav [ _class "navbar navbar-dark"; _roleNavigation ] + [ div [ _class "container-fluid" ] + [ pageLink + "/" [ _class "navbar-brand" ] + [ span [ _class "m" ] [ str "my" ] + span [ _class "p" ] [ str "Prayer" ] + span [ _class "j" ] [ str "Journal" ] ] + seq { let navLink (matchUrl : string) = match ctx.CurrentUrl.StartsWith matchUrl with true -> [ _class "is-active-route" ] | false -> [] |> pageLink matchUrl @@ -58,91 +54,72 @@ let private navBar ctx = li [ _class "nav-item" ] [ navLink "/requests/answered" [ str "Answered" ] ] li [ _class "nav-item" ] [ a [ _href "/user/log-off" ] [ str "Log Off" ] ] else li [ _class "nav-item"] [ a [ _href "/user/log-on" ] [ str "Log On" ] ] - li [ _class "nav-item" ] [ - a [ _href "https://docs.prayerjournal.me"; _target "_blank"; _rel "noopener" ] [ str "Docs" ] - ] - } - |> List.ofSeq - |> ul [ _class "navbar-nav me-auto d-flex flex-row" ] - ] - ] + li [ _class "nav-item" ] [ navLink "/docs" [ str "Docs" ] ] + } + |> List.ofSeq + |> ul [ _class "navbar-nav me-auto d-flex flex-row" ] ] ] /// The title tag with the application name appended let titleTag ctx = - title [] [ str ctx.PageTitle; rawText " « myPrayerJournal" ] + title [] [ rawText ctx.PageTitle; rawText " « myPrayerJournal" ] /// The HTML `head` element let htmlHead ctx = - head [ _lang "en" ] [ - meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] - meta [ _name "description"; _content "Online prayer journal - free w/Google or Microsoft account" ] - titleTag ctx - link [ _href "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" - _rel "stylesheet" - _integrity "sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" - _crossorigin "anonymous" ] - link [ _href "https://fonts.googleapis.com/icon?family=Material+Icons"; _rel "stylesheet" ] - link [ _href "/style/style.css"; _rel "stylesheet" ] - ] + head [ _lang "en" ] + [ meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] + meta [ _name "description"; _content "Online prayer journal - free w/Google or Microsoft account" ] + titleTag ctx + link [ _href "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" + _rel "stylesheet" + _integrity "sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" + _crossorigin "anonymous" ] + link [ _href "https://fonts.googleapis.com/icon?family=Material+Icons"; _rel "stylesheet" ] + link [ _href "/style/style.css"; _rel "stylesheet" ] ] /// Element used to display toasts let toaster = - div [ _ariaLive "polite"; _ariaAtomic "true"; _id "toastHost" ] [ - div [ _class "toast-container position-absolute p-3 bottom-0 end-0"; _id "toasts" ] [] - ] + div [ _ariaLive "polite"; _ariaAtomic "true"; _id "toastHost" ] + [ div [ _class "toast-container position-absolute p-3 bottom-0 end-0"; _id "toasts" ] [] ] /// The page's `footer` element let htmlFoot = - footer [ _class "container-fluid" ] [ - p [ _class "text-muted text-end" ] [ - str $"myPrayerJournal {version}" - br [] - em [] [ - small [] [ - pageLink "/legal/privacy-policy" [] [ str "Privacy Policy" ] - rawText " • " - pageLink "/legal/terms-of-service" [] [ str "Terms of Service" ] - rawText " • " - a [ _href "https://github.com/bit-badger/myprayerjournal"; _target "_blank"; _rel "noopener" ] [ - str "Developed" - ] - str " and hosted by " - a [ _href "https://bitbadger.solutions"; _target "_blank"; _rel "noopener" ] [ - str "Bit Badger Solutions" - ] - ] - ] - ] - Htmx.Script.minified - script [] [ - rawText "if (!htmx) document.write('