Implements proper error handling for login form.
This commit is contained in:
parent
c0bbce53e9
commit
f9efbe04d3
4 changed files with 143 additions and 65 deletions
|
|
@ -1166,7 +1166,7 @@ routes:
|
|||
errorFields: @["username", "password"],
|
||||
message: "Invalid username or password"
|
||||
)
|
||||
resp $(%err), "application/json"
|
||||
resp Http403, $(%err), "application/json"
|
||||
|
||||
get "/karax/status.json":
|
||||
createTFD()
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ when defined(js):
|
|||
include karax/prelude
|
||||
import karax / [kajax]
|
||||
|
||||
|
||||
import login
|
||||
import karaxutils
|
||||
|
||||
from dom import setTimeout, window, document, getElementById
|
||||
from dom import setTimeout, window, document, getElementById, focus
|
||||
|
||||
type
|
||||
State = ref object
|
||||
|
|
@ -20,18 +20,23 @@ when defined(js):
|
|||
loading: bool
|
||||
status: HttpCode
|
||||
lastUpdate: Time
|
||||
loginModal: LoginModal
|
||||
|
||||
proc newState(): State =
|
||||
State(
|
||||
data: none[UserStatus](),
|
||||
loading: false,
|
||||
status: Http200
|
||||
)
|
||||
|
||||
proc newState(): State
|
||||
var
|
||||
state = newState()
|
||||
|
||||
proc getStatus
|
||||
proc newState(): State =
|
||||
State(
|
||||
data: none[UserStatus](),
|
||||
loading: false,
|
||||
status: Http200,
|
||||
loginModal: newLoginModal(
|
||||
() => (state.lastUpdate = fromUnix(0); getStatus())
|
||||
)
|
||||
)
|
||||
|
||||
proc onStatus(httpStatus: int, response: kstring) =
|
||||
state.loading = false
|
||||
state.status = httpStatus.HttpCode
|
||||
|
|
@ -52,46 +57,6 @@ when defined(js):
|
|||
let uri = makeUri("status.json")
|
||||
ajaxGet(uri, @[], onStatus)
|
||||
|
||||
proc onLogInPost(httpStatus: int, response: kstring) =
|
||||
kout(response)
|
||||
|
||||
proc onLogInClick(ev: Event, n: VNode) =
|
||||
let uri = makeUri("login")
|
||||
let form = document.getElementById("login-form")
|
||||
# TODO: This is a hack, karax should support this.
|
||||
let formData = newFormData(form)
|
||||
kout(formData.get("username"))
|
||||
ajaxPost(uri, @[], cast[cstring](formData), onLogInPost)
|
||||
|
||||
proc genLoginModal(): VNode =
|
||||
result = buildHtml():
|
||||
tdiv(class="modal modal-sm", id="login-modal"):
|
||||
a(href="#", class="modal-overlay", "aria-label"="close")
|
||||
tdiv(class="modal-container"):
|
||||
tdiv(class="modal-header"):
|
||||
a(href="#", class="btn btn-clear float-right", "aria-label"="close")
|
||||
tdiv(class="modal-title h5"):
|
||||
text "Log in"
|
||||
tdiv(class="modal-body"):
|
||||
tdiv(class="content"):
|
||||
form(id="login-form"):
|
||||
tdiv(class="form-group"):
|
||||
label(class="form-label", `for`="username"):
|
||||
text "Username"
|
||||
input(class="form-input", `type`="text", name="username")
|
||||
tdiv(class="form-group"):
|
||||
label(class="form-label", `for`="password"):
|
||||
text "Password"
|
||||
input(class="form-input", `type`="password", name="password")
|
||||
a(href="#reset-password-modal"):
|
||||
text "Reset your password"
|
||||
tdiv(class="modal-footer"):
|
||||
button(class="btn btn-primary", onClick=onLogInClick):
|
||||
text "Log in"
|
||||
a(href="#signup-modal"):
|
||||
button(class="btn"):
|
||||
text "Create account"
|
||||
|
||||
proc genSignUpModal(): VNode =
|
||||
result = buildHtml():
|
||||
tdiv(class="modal", id="signup-modal"):
|
||||
|
|
@ -136,7 +101,9 @@ when defined(js):
|
|||
img(src="images/crown.png", id="img-logo") # TODO: Customisation.
|
||||
section(class="navbar-section"):
|
||||
tdiv(class="input-group input-inline"):
|
||||
input(class="search-input input-sm", `type`="text", placeholder="search")
|
||||
input(class="search-input input-sm",
|
||||
`type`="text", placeholder="search",
|
||||
id="search-box")
|
||||
if state.loading:
|
||||
tdiv(class="loading")
|
||||
elif user.isNone:
|
||||
|
|
@ -144,14 +111,14 @@ when defined(js):
|
|||
button(class="btn btn-primary btn-sm"):
|
||||
italic(class="fas fa-user-plus")
|
||||
text " Sign up"
|
||||
a(href="#login-modal", id="login-btn"):
|
||||
button(class="btn btn-primary btn-sm"):
|
||||
italic(class="fas fa-sign-in-alt")
|
||||
text " Log in"
|
||||
button(class="btn btn-primary btn-sm",
|
||||
onClick=(e: Event, n: VNode) => state.loginModal.show()):
|
||||
italic(class="fas fa-sign-in-alt")
|
||||
text " Log in"
|
||||
else:
|
||||
render(user.get(), "avatar")
|
||||
|
||||
# Modals
|
||||
genLoginModal()
|
||||
render(state.loginModal)
|
||||
|
||||
genSignUpModal()
|
||||
|
|
@ -12,7 +12,7 @@ proc class*(classes: varargs[tuple[name: string, present: bool]],
|
|||
for class in classes:
|
||||
if class.present: result.add(class.name & " ")
|
||||
|
||||
proc makeUri*(relative: string, appName=appName): string =
|
||||
proc makeUri*(relative: string, appName=appName, includeHash=false): string =
|
||||
## Concatenates ``relative`` to the current URL in a way that is sane.
|
||||
var relative = relative
|
||||
assert appName in $window.location.pathname
|
||||
|
|
@ -23,10 +23,10 @@ proc makeUri*(relative: string, appName=appName): string =
|
|||
appName &
|
||||
relative &
|
||||
$window.location.search &
|
||||
$window.location.hash
|
||||
(if includeHash: $window.location.hash else: "")
|
||||
|
||||
proc makeUri*(relative: string, params: varargs[(string, string)],
|
||||
appName=appName): string =
|
||||
appName=appName, includeHash=false): string =
|
||||
var query = ""
|
||||
for i in 0 ..< params.len:
|
||||
let param = params[i]
|
||||
|
|
@ -38,18 +38,20 @@ proc makeUri*(relative: string, params: varargs[(string, string)],
|
|||
else:
|
||||
makeUri(relative, appName)
|
||||
|
||||
proc navigateTo*(uri: cstring) =
|
||||
# TODO: This was annoying. Karax also shouldn't have its own `window`.
|
||||
dom.pushState(dom.window.history, 0, cstring"", uri)
|
||||
|
||||
# Fire the popState event.
|
||||
dom.window.dispatchEvent(newEvent("popstate"))
|
||||
|
||||
proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb?
|
||||
e.preventDefault()
|
||||
|
||||
# TODO: Why does Karax have it's own Node type? That's just silly.
|
||||
let url = cast[dom.Node](n.dom).getAttribute(cstring"href")
|
||||
|
||||
# TODO: This was annoying. Karax also shouldn't have its own `window`.
|
||||
dom.pushState(dom.window.history, 5, cstring"Thread", url)
|
||||
|
||||
# Fire the popState event.
|
||||
dom.window.dispatchEvent(newEvent("popstate"))
|
||||
|
||||
navigateTo(url)
|
||||
|
||||
type
|
||||
FormData* = ref object
|
||||
|
|
|
|||
109
redesign/login.nim
Normal file
109
redesign/login.nim
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
when defined(js):
|
||||
import sugar, httpcore, options, json
|
||||
import dom except Event
|
||||
|
||||
include karax/prelude
|
||||
import karax / [kajax, kdom]
|
||||
|
||||
import error
|
||||
import karaxutils
|
||||
|
||||
type
|
||||
LoginModal* = ref object
|
||||
shown: bool
|
||||
onLogIn: proc ()
|
||||
error: Option[PostError]
|
||||
|
||||
proc onLogInPost(httpStatus: int, response: kstring, state: LoginModal) =
|
||||
let status = httpStatus.HttpCode
|
||||
if status == Http200:
|
||||
state.shown = false
|
||||
state.onLogIn()
|
||||
else:
|
||||
# TODO: Karax should pass the content-type...
|
||||
try:
|
||||
let parsed = parseJson($response)
|
||||
let error = to(parsed, PostError)
|
||||
|
||||
state.error = some(error)
|
||||
except:
|
||||
kout(getCurrentExceptionMsg().cstring)
|
||||
state.error = some(PostError(
|
||||
errorFields: @[],
|
||||
message: "Unknown error occurred."
|
||||
))
|
||||
|
||||
proc onLogInClick(ev: Event, n: VNode, state: LoginModal) =
|
||||
state.error = none[PostError]()
|
||||
|
||||
let uri = makeUri("login")
|
||||
let form = dom.document.getElementById("login-form")
|
||||
# TODO: This is a hack, karax should support this.
|
||||
let formData = newFormData(form)
|
||||
ajaxPost(uri, @[], cast[cstring](formData),
|
||||
(s: int, r: kstring) => onLogInPost(s, r, state))
|
||||
|
||||
proc onClose(ev: Event, n: VNode, state: LoginModal) =
|
||||
state.shown = false
|
||||
ev.preventDefault()
|
||||
|
||||
proc newLoginModal*(onLogIn: proc ()): LoginModal =
|
||||
LoginModal(
|
||||
shown: false,
|
||||
onLogIn: onLogIn
|
||||
)
|
||||
|
||||
proc show*(state: LoginModal) =
|
||||
state.shown = true
|
||||
|
||||
proc genFormField(error: Option[PostError], name, label, typ: string,
|
||||
isLast: bool): VNode =
|
||||
let hasError =
|
||||
not error.isNone and (
|
||||
name in error.get().errorFields or
|
||||
error.get().errorFields.len == 0)
|
||||
result = buildHtml():
|
||||
tdiv(class=class({"has-error": hasError}, "form-group")):
|
||||
label(class="form-label", `for`=name):
|
||||
text "Username"
|
||||
input(class="form-input", `type`="text", name=name)
|
||||
|
||||
if not error.isNone:
|
||||
let e = error.get()
|
||||
if (e.errorFields.len == 1 and e.errorFields[0] == name) or isLast:
|
||||
p(class="form-input-hint"):
|
||||
text e.message
|
||||
|
||||
proc render*(state: LoginModal): VNode =
|
||||
result = buildHtml():
|
||||
tdiv(class=class({"active": state.shown}, "modal modal-sm"),
|
||||
id="login-modal"):
|
||||
a(href="", class="modal-overlay", "aria-label"="close",
|
||||
onClick=(ev: Event, n: VNode) => onClose(ev, n, state))
|
||||
tdiv(class="modal-container"):
|
||||
tdiv(class="modal-header"):
|
||||
a(href="", class="btn btn-clear float-right",
|
||||
"aria-label"="close",
|
||||
onClick=(ev: Event, n: VNode) => onClose(ev, n, state))
|
||||
tdiv(class="modal-title h5"):
|
||||
text "Log in"
|
||||
tdiv(class="modal-body"):
|
||||
tdiv(class="content"):
|
||||
form(id="login-form"):
|
||||
genFormField(state.error, "username", "Username", "text", false)
|
||||
genFormField(
|
||||
state.error,
|
||||
"password",
|
||||
"Password",
|
||||
"password",
|
||||
true
|
||||
)
|
||||
a(href="#reset-password-modal"):
|
||||
text "Reset your password"
|
||||
tdiv(class="modal-footer"):
|
||||
button(class="btn btn-primary",
|
||||
onClick=(ev: Event, n: VNode) => onLogInClick(ev, n, state)):
|
||||
text "Log in"
|
||||
a(href="#signup-modal"):
|
||||
button(class="btn"):
|
||||
text "Create account"
|
||||
Loading…
Add table
Add a link
Reference in a new issue