Version 3 #67
3
src/MyPrayerJournal/Server/.gitignore
vendored
3
src/MyPrayerJournal/Server/.gitignore
vendored
@ -1,6 +1,9 @@
|
|||||||
## LiteDB database file
|
## LiteDB database file
|
||||||
*.db
|
*.db
|
||||||
|
|
||||||
|
## Auth0 settings
|
||||||
|
wwwroot/auth-config.json
|
||||||
|
|
||||||
## Web application compile output
|
## Web application compile output
|
||||||
wwwroot/favicon.ico
|
wwwroot/favicon.ico
|
||||||
wwwroot/index.html
|
wwwroot/index.html
|
||||||
|
@ -9,7 +9,7 @@ open Giraffe.Htmx
|
|||||||
open MyPrayerJournal.Data.Extensions
|
open MyPrayerJournal.Data.Extensions
|
||||||
|
|
||||||
/// Send a partial result if this is not a full page load
|
/// Send a partial result if this is not a full page load
|
||||||
let partialIfNotRefresh content layout : HttpHandler =
|
let partialIfNotRefresh content : HttpHandler =
|
||||||
fun next ctx -> task {
|
fun next ctx -> task {
|
||||||
let hdrs = Headers.fromRequest ctx
|
let hdrs = Headers.fromRequest ctx
|
||||||
let isHtmx =
|
let isHtmx =
|
||||||
@ -24,7 +24,7 @@ let partialIfNotRefresh content layout : HttpHandler =
|
|||||||
|> function Some (HistoryRestoreRequest hist) -> hist | _ -> false
|
|> function Some (HistoryRestoreRequest hist) -> hist | _ -> false
|
||||||
match isHtmx && not isRefresh with
|
match isHtmx && not isRefresh with
|
||||||
| true -> return! ctx.WriteHtmlViewAsync content
|
| true -> return! ctx.WriteHtmlViewAsync content
|
||||||
| false -> return! layout content |> ctx.WriteHtmlViewAsync
|
| false -> return! Views.Layout.view content |> ctx.WriteHtmlViewAsync
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler to return Vue files
|
/// Handler to return Vue files
|
||||||
@ -33,7 +33,7 @@ module Vue =
|
|||||||
/// The application index page
|
/// The application index page
|
||||||
let app : HttpHandler =
|
let app : HttpHandler =
|
||||||
Headers.toResponse (Trigger "menu-refresh")
|
Headers.toResponse (Trigger "menu-refresh")
|
||||||
>=> partialIfNotRefresh (ViewEngine.HtmlElements.str "It works") Views.Layout.wide
|
>=> partialIfNotRefresh (ViewEngine.HtmlElements.str "It works")
|
||||||
|
|
||||||
|
|
||||||
open System
|
open System
|
||||||
@ -60,6 +60,8 @@ module Error =
|
|||||||
|
|
||||||
open Cuid
|
open Cuid
|
||||||
open LiteDB
|
open LiteDB
|
||||||
|
open System.Security.Claims
|
||||||
|
open Microsoft.Extensions.Logging
|
||||||
|
|
||||||
/// Handler helpers
|
/// Handler helpers
|
||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
@ -67,7 +69,6 @@ module private Helpers =
|
|||||||
|
|
||||||
open Microsoft.AspNetCore.Http
|
open Microsoft.AspNetCore.Http
|
||||||
open System.Threading.Tasks
|
open System.Threading.Tasks
|
||||||
open System.Security.Claims
|
|
||||||
|
|
||||||
/// Get the LiteDB database
|
/// Get the LiteDB database
|
||||||
let db (ctx : HttpContext) = ctx.GetService<LiteDatabase>()
|
let db (ctx : HttpContext) = ctx.GetService<LiteDatabase>()
|
||||||
@ -108,6 +109,18 @@ module private Helpers =
|
|||||||
let asJson<'T> next ctx (o : 'T) =
|
let asJson<'T> next ctx (o : 'T) =
|
||||||
json o next ctx
|
json o next ctx
|
||||||
|
|
||||||
|
/// Trigger a menu item refresh
|
||||||
|
let withMenuRefresh : HttpHandler =
|
||||||
|
// let trigger = //string ctx.Request.Path |> sprintf "{ \"menu-refresh\": \"%s\" }" :> obj |> TriggerAfterSwap
|
||||||
|
Headers.toResponse (TriggerAfterSettle "menu-refresh")
|
||||||
|
|
||||||
|
/// Render a component result
|
||||||
|
let renderComponent nodes : HttpHandler =
|
||||||
|
fun next ctx -> task {
|
||||||
|
return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Strongly-typed models for post requests
|
/// Strongly-typed models for post requests
|
||||||
module Models =
|
module Models =
|
||||||
@ -166,13 +179,29 @@ module Components =
|
|||||||
Headers.fromRequest ctx
|
Headers.fromRequest ctx
|
||||||
|> List.tryFind HtmxReqHeader.isCurrentUrl
|
|> List.tryFind HtmxReqHeader.isCurrentUrl
|
||||||
|> function Some (CurrentUrl u) -> Some u | _ -> None
|
|> function Some (CurrentUrl u) -> Some u | _ -> None
|
||||||
let view = Views.Navigation.currentNav false false url |> ViewEngine.RenderView.AsString.htmlNodes
|
let isAuthorized = ctx |> (user >> Option.isSome)
|
||||||
return! ctx.WriteHtmlStringAsync view
|
return! renderComponent (Views.Navigation.currentNav isAuthorized false url) next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /components/journal-items
|
||||||
|
let journalItems : HttpHandler =
|
||||||
|
authorize
|
||||||
|
>=> fun next ctx -> task {
|
||||||
|
let! jrnl = Data.journalByUserId (userId ctx) (db ctx)
|
||||||
|
do! System.Threading.Tasks.Task.Delay (TimeSpan.FromSeconds 5.)
|
||||||
|
return! renderComponent [ Views.Journal.journalItems jrnl ] next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// / URL
|
||||||
|
module Home =
|
||||||
|
|
||||||
/// /api/journal URLs
|
// GET /
|
||||||
|
let home : HttpHandler =
|
||||||
|
withMenuRefresh >=> partialIfNotRefresh Views.Home.home
|
||||||
|
|
||||||
|
|
||||||
|
/// /api/journal and /journal URLs
|
||||||
module Journal =
|
module Journal =
|
||||||
|
|
||||||
/// GET /api/journal
|
/// GET /api/journal
|
||||||
@ -183,16 +212,25 @@ module Journal =
|
|||||||
return! json jrnl next ctx
|
return! json jrnl next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /journal
|
||||||
|
let journalPage : HttpHandler =
|
||||||
|
authorize
|
||||||
|
>=> withMenuRefresh
|
||||||
|
>=> fun next ctx -> task {
|
||||||
|
let usr = ctx.Request.Headers.["X-Given-Name"].[0]
|
||||||
|
return! partialIfNotRefresh (Views.Journal.journal usr) next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Legalese
|
/// Legalese
|
||||||
module Legal =
|
module Legal =
|
||||||
|
|
||||||
// GET /legal/privacy-policy
|
// GET /legal/privacy-policy
|
||||||
let privacyPolicy : HttpHandler =
|
let privacyPolicy : HttpHandler =
|
||||||
partialIfNotRefresh Views.Legal.privacyPolicy Views.Layout.standard
|
withMenuRefresh >=> partialIfNotRefresh Views.Legal.privacyPolicy
|
||||||
|
|
||||||
let termsOfService : HttpHandler =
|
let termsOfService : HttpHandler =
|
||||||
partialIfNotRefresh Views.Legal.termsOfService Views.Layout.standard
|
withMenuRefresh >=> partialIfNotRefresh Views.Legal.termsOfService
|
||||||
|
|
||||||
|
|
||||||
/// /api/request URLs
|
/// /api/request URLs
|
||||||
@ -362,10 +400,12 @@ open Giraffe.EndpointRouting
|
|||||||
|
|
||||||
/// The routes for myPrayerJournal
|
/// The routes for myPrayerJournal
|
||||||
let routes =
|
let routes =
|
||||||
[ route "/" Vue.app
|
[ route "/" Home.home
|
||||||
subRoute "/components/" [
|
subRoute "/components/" [
|
||||||
|
route "journal-items" Components.journalItems
|
||||||
route "nav-items" Components.navItems
|
route "nav-items" Components.navItems
|
||||||
]
|
]
|
||||||
|
route "/journal" Journal.journalPage
|
||||||
subRoute "/legal/" [
|
subRoute "/legal/" [
|
||||||
route "privacy-policy" Legal.privacyPolicy
|
route "privacy-policy" Legal.privacyPolicy
|
||||||
route "terms-of-service" Legal.termsOfService
|
route "terms-of-service" Legal.termsOfService
|
||||||
|
@ -76,7 +76,7 @@ module Configure =
|
|||||||
fun opts ->
|
fun opts ->
|
||||||
let jwtCfg = bldr.Configuration.GetSection "Auth0"
|
let jwtCfg = bldr.Configuration.GetSection "Auth0"
|
||||||
opts.Authority <- sprintf "https://%s/" jwtCfg.["Domain"]
|
opts.Authority <- sprintf "https://%s/" jwtCfg.["Domain"]
|
||||||
opts.Audience <- jwtCfg.["Id"])
|
opts.Audience <- jwtCfg.["Audience"])
|
||||||
|> ignore
|
|> ignore
|
||||||
let jsonOptions = JsonSerializerOptions ()
|
let jsonOptions = JsonSerializerOptions ()
|
||||||
jsonOptions.Converters.Add (JsonFSharpConverter ())
|
jsonOptions.Converters.Add (JsonFSharpConverter ())
|
||||||
|
@ -4,13 +4,33 @@ open Giraffe.ViewEngine
|
|||||||
open Giraffe.ViewEngine.Htmx
|
open Giraffe.ViewEngine.Htmx
|
||||||
open System
|
open System
|
||||||
|
|
||||||
|
/// Target the `main` tag with boosted links
|
||||||
let toMain = _hxTarget "main"
|
let toMain = _hxTarget "main"
|
||||||
|
|
||||||
|
/// View for home page
|
||||||
|
module Home =
|
||||||
|
|
||||||
|
/// 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 prayed for "
|
||||||
|
str "them, update them as God moves in the situation, and record a final answer received on that request. It "
|
||||||
|
str "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 also "
|
||||||
|
rawText "learn more about the site at the “Docs” link, also above."
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
/// Views for legal pages
|
/// Views for legal pages
|
||||||
module Legal =
|
module Legal =
|
||||||
|
|
||||||
/// View for the "Privacy Policy" page
|
/// View for the "Privacy Policy" page
|
||||||
let privacyPolicy = article [] [
|
let privacyPolicy = article [ _class "container mt-3" ] [
|
||||||
div [ _class "card" ] [
|
div [ _class "card" ] [
|
||||||
h5 [ _class "card-header" ] [ str "Privacy Policy" ]
|
h5 [ _class "card-header" ] [ str "Privacy Policy" ]
|
||||||
div [ _class "card-body" ] [
|
div [ _class "card-body" ] [
|
||||||
@ -92,7 +112,7 @@ module Legal =
|
|||||||
]
|
]
|
||||||
|
|
||||||
/// View for the "Terms of Service" page
|
/// View for the "Terms of Service" page
|
||||||
let termsOfService = article [ _class "container" ] [
|
let termsOfService = article [ _class "container mt-3" ] [
|
||||||
div [ _class "card" ] [
|
div [ _class "card" ] [
|
||||||
h5 [ _class "card-header" ] [ str "Terms of Service" ]
|
h5 [ _class "card-header" ] [ str "Terms of Service" ]
|
||||||
div [ _class "card-body" ] [
|
div [ _class "card-body" ] [
|
||||||
@ -144,14 +164,15 @@ module Legal =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
/// Views for navigation support
|
/// Views for navigation support
|
||||||
module Navigation =
|
module Navigation =
|
||||||
|
|
||||||
/// The default navigation bar, which will load the items on page load, and whenever a refresh event occurs
|
/// The default navigation bar, which will load the items on page load, and whenever a refresh event occurs
|
||||||
let navBar =
|
let navBar =
|
||||||
nav [ _class "navbar navbar-dark"; _hxBoost; toMain ] [
|
nav [ _class "navbar navbar-dark" ] [
|
||||||
div [ _class "container-fluid" ] [
|
div [ _class "container-fluid" ] [
|
||||||
a [ _href "/"; _class "navbar-brand" ] [
|
a [ _href "/"; _class "navbar-brand"; _hxBoost; toMain ] [
|
||||||
span [ _class "m" ] [ str "my" ]
|
span [ _class "m" ] [ str "my" ]
|
||||||
span [ _class "p" ] [ str "Prayer" ]
|
span [ _class "p" ] [ str "Prayer" ]
|
||||||
span [ _class "j" ] [ str "Journal" ]
|
span [ _class "j" ] [ str "Journal" ]
|
||||||
@ -171,23 +192,58 @@ module Navigation =
|
|||||||
match isAuthenticated with
|
match isAuthenticated with
|
||||||
| true ->
|
| true ->
|
||||||
let currUrl = match url with Some u -> (u.PathAndQuery.Split '?').[0] | None -> ""
|
let currUrl = match url with Some u -> (u.PathAndQuery.Split '?').[0] | None -> ""
|
||||||
let deriveClass (matchUrl : string) css =
|
let attrs (matchUrl : string) =
|
||||||
|
[ _href matchUrl
|
||||||
match currUrl.StartsWith matchUrl with
|
match currUrl.StartsWith matchUrl with
|
||||||
| true -> sprintf "%s is-active-route" css
|
| true -> _class "is-active-route"
|
||||||
| false -> css
|
| false -> ()
|
||||||
|> _class
|
_hxBoost; toMain
|
||||||
li [ deriveClass "/journal" "nav-item" ] [ a [ _href "/journal" ] [ str "Journal" ] ]
|
]
|
||||||
li [ deriveClass "/requests/active" "nav-item" ] [ a [ _href "/requests/active" ] [ str "Active" ] ]
|
li [ _class "nav-item" ] [ a (attrs "/journal") [ str "Journal" ] ]
|
||||||
if hasSnoozed then
|
li [ _class "nav-item" ] [ a (attrs "/requests/active") [ str "Active" ] ]
|
||||||
li [ deriveClass "/requests/snoozed" "nav-item" ] [ a [ _href "/requests/snoozed" ] [ str "Snoozed" ] ]
|
if hasSnoozed then li [ _class "nav-item" ] [ a (attrs "/requests/snoozed") [ str "Snoozed" ] ]
|
||||||
li [ deriveClass "/requests/answered" "nav-item" ] [ a [ _href "/requests/answered" ] [ str "Answered" ] ]
|
li [ _class "nav-item" ] [ a (attrs "/requests/answered") [ str "Answered" ] ]
|
||||||
li [ _class "nav-item" ] [ a [ _href "/user/log-off"; _onclick "logOff()" ] [ str "Log Off" ] ]
|
li [ _class "nav-item" ] [ a [ _href "/user/log-off"; _onclick "mpj.logOff(event)" ] [ str "Log Off" ] ]
|
||||||
| false -> li [ _class "nav-item"] [ a [ _href "/user/log-on"; _onclick "logOn()"] [ str "Log On" ] ]
|
| false -> li [ _class "nav-item"] [ a [ _href "/user/log-on"; _onclick "mpj.logOn(event)"] [ str "Log On" ] ]
|
||||||
li [ _class "nav-item" ] [ a [ _href "https://docs.prayerjournal.me"; _target "_blank" ] [ str "Docs" ] ]
|
li [ _class "nav-item" ] [ a [ _href "https://docs.prayerjournal.me"; _target "_blank" ] [ str "Docs" ] ]
|
||||||
}
|
}
|
||||||
|> List.ofSeq
|
|> List.ofSeq
|
||||||
|
|
||||||
|
|
||||||
|
/// Views for journal pages and components
|
||||||
|
module Journal =
|
||||||
|
|
||||||
|
/// The journal loading page
|
||||||
|
let journal user = article [ _class "container-fluid mt-3" ] [
|
||||||
|
h2 [ _class "pb-3" ] [ str user; rawText "’s Prayer Journal" ]
|
||||||
|
p [
|
||||||
|
_hxGet "/components/journal-items"
|
||||||
|
_hxSwap HxSwap.OuterHtml
|
||||||
|
_hxTrigger HxTrigger.Load
|
||||||
|
] [ rawText "Loading your prayer journal…" ]
|
||||||
|
]
|
||||||
|
|
||||||
|
/// The journal items
|
||||||
|
let journalItems items =
|
||||||
|
match items |> List.isEmpty with
|
||||||
|
| true ->
|
||||||
|
div [ _class "card no-requests" ] [
|
||||||
|
h5 [ _class "card-header"] [ str "No Active Requests" ]
|
||||||
|
div [ _class "card-body text-center" ] [
|
||||||
|
p [ _class "card-text" ] [
|
||||||
|
rawText "You have no requests to be shown; see the “Active” link above for snoozed or "
|
||||||
|
rawText "deferred requests, and the “Answered” link for answered requests"
|
||||||
|
]
|
||||||
|
a [
|
||||||
|
_class "btn btn-primary"
|
||||||
|
_href "/request/new/edit"
|
||||||
|
_hxBoost; toMain
|
||||||
|
] [ str "Add a Request" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
| false -> p [] [ str "There are requests" ]
|
||||||
|
|
||||||
|
|
||||||
/// Layout views
|
/// Layout views
|
||||||
module Layout =
|
module Layout =
|
||||||
|
|
||||||
@ -199,7 +255,7 @@ module Layout =
|
|||||||
_integrity "sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
|
_integrity "sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
|
||||||
_crossorigin "anonymous"
|
_crossorigin "anonymous"
|
||||||
]
|
]
|
||||||
link [ _href "/css/style.css"; _rel "stylesheet" ]
|
link [ _href "/style/style.css"; _rel "stylesheet" ]
|
||||||
script [
|
script [
|
||||||
_src "https://unpkg.com/htmx.org@1.5.0"
|
_src "https://unpkg.com/htmx.org@1.5.0"
|
||||||
_integrity "sha384-oGA+prIp5Vchu6we2YkI51UtVzN9Jpx2Z7PnR1I78PnZlN8LkrCT4lqqqmDkyrvI"
|
_integrity "sha384-oGA+prIp5Vchu6we2YkI51UtVzN9Jpx2Z7PnR1I78PnZlN8LkrCT4lqqqmDkyrvI"
|
||||||
@ -225,25 +281,23 @@ module Layout =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
script [
|
script [
|
||||||
|
_async
|
||||||
_src "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
_src "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
||||||
_integrity "sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
_integrity "sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
||||||
_crossorigin "anonymous"
|
_crossorigin "anonymous"
|
||||||
] []
|
] []
|
||||||
|
script [ _src "https://cdn.auth0.com/js/auth0-spa-js/1.13/auth0-spa-js.production.js" ] []
|
||||||
|
script [ _src "/script/mpj.js" ] []
|
||||||
]
|
]
|
||||||
|
|
||||||
let full content =
|
/// Create the full view of the page
|
||||||
|
let view content =
|
||||||
html [ _lang "en" ] [
|
html [ _lang "en" ] [
|
||||||
htmlHead
|
htmlHead
|
||||||
body [] [
|
body [ _hxHeaders "" ] [
|
||||||
Navigation.navBar
|
Navigation.navBar
|
||||||
content
|
main [] [ content ]
|
||||||
htmlFoot
|
htmlFoot
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
let standard content =
|
|
||||||
main [ _class "container" ] [ content ] |> full
|
|
||||||
|
|
||||||
let wide content =
|
|
||||||
main [ _class "container-fluid" ] [ content ] |> full
|
|
||||||
|
|
||||||
|
59
src/MyPrayerJournal/Server/wwwroot/script/mpj.js
Normal file
59
src/MyPrayerJournal/Server/wwwroot/script/mpj.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"use strict"
|
||||||
|
|
||||||
|
/** myPrayerJournal script */
|
||||||
|
const mpj = {
|
||||||
|
/** Auth0 configuration */
|
||||||
|
auth: {
|
||||||
|
/** The Auth0 client */
|
||||||
|
auth0: null,
|
||||||
|
/** Configure the Auth0 client */
|
||||||
|
configureClient: async () => {
|
||||||
|
const response = await fetch("/auth-config.json")
|
||||||
|
const config = await response.json()
|
||||||
|
mpj.auth.auth0 = await createAuth0Client({
|
||||||
|
domain: config.domain,
|
||||||
|
client_id: config.clientId,
|
||||||
|
audience: config.audience
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Whether the user is currently authenticated */
|
||||||
|
isAuthenticated: false,
|
||||||
|
/** Process a log on request */
|
||||||
|
logOn: async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
await mpj.auth.auth0.loginWithRedirect({ redirect_uri: window.location.origin })
|
||||||
|
},
|
||||||
|
/** Log the user off */
|
||||||
|
logOff: (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
mpj.auth.auth0.logout({ returnTo: window.location.origin })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = async () => {
|
||||||
|
/** If the user is authenticated, set the JWT on the `body` tag */
|
||||||
|
const establishAuth = async () => {
|
||||||
|
mpj.isAuthenticated = await mpj.auth.auth0.isAuthenticated()
|
||||||
|
if (mpj.isAuthenticated) {
|
||||||
|
const token = await mpj.auth.auth0.getTokenSilently()
|
||||||
|
const user = await mpj.auth.auth0.getUser()
|
||||||
|
document.querySelector("body")
|
||||||
|
.setAttribute("hx-headers", `{ "Authorization": "Bearer ${token}", "X-Given-Name": "${user.given_name}" }`)
|
||||||
|
htmx.trigger(htmx.find(".navbar-nav"), "menu-refresh")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up Auth0
|
||||||
|
await mpj.auth.configureClient()
|
||||||
|
await establishAuth()
|
||||||
|
if (mpj.isAuthenticated) return
|
||||||
|
|
||||||
|
// Handle log on code, if present
|
||||||
|
const query = window.location.search
|
||||||
|
if (query.includes("code=") && query.includes("state=")) {
|
||||||
|
await mpj.auth.auth0.handleRedirectCallback()
|
||||||
|
await establishAuth()
|
||||||
|
window.history.replaceState({}, document.title, "/")
|
||||||
|
}
|
||||||
|
}
|
37
src/MyPrayerJournal/Server/wwwroot/style/style.css
Normal file
37
src/MyPrayerJournal/Server/wwwroot/style/style.css
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
nav {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
nav .m {
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
nav .p {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
nav .j {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.nav-item a:link,
|
||||||
|
.nav-item a:visited {
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
margin: 0 .5rem;
|
||||||
|
border-radius: .5rem;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-item a:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgba(255, 255, 255, .2);
|
||||||
|
}
|
||||||
|
.navbar-nav .is-active-route {
|
||||||
|
background-color: rgba(255, 255, 255, .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: solid 1px lightgray;
|
||||||
|
margin: 1rem -1rem 0;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
footer p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user