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"],
|
errorFields: @["username", "password"],
|
||||||
message: "Invalid username or password"
|
message: "Invalid username or password"
|
||||||
)
|
)
|
||||||
resp $(%err), "application/json"
|
resp Http403, $(%err), "application/json"
|
||||||
|
|
||||||
get "/karax/status.json":
|
get "/karax/status.json":
|
||||||
createTFD()
|
createTFD()
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@ when defined(js):
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
import karax / [kajax]
|
import karax / [kajax]
|
||||||
|
|
||||||
|
import login
|
||||||
import karaxutils
|
import karaxutils
|
||||||
|
|
||||||
from dom import setTimeout, window, document, getElementById
|
from dom import setTimeout, window, document, getElementById, focus
|
||||||
|
|
||||||
type
|
type
|
||||||
State = ref object
|
State = ref object
|
||||||
|
|
@ -20,18 +20,23 @@ when defined(js):
|
||||||
loading: bool
|
loading: bool
|
||||||
status: HttpCode
|
status: HttpCode
|
||||||
lastUpdate: Time
|
lastUpdate: Time
|
||||||
|
loginModal: LoginModal
|
||||||
|
|
||||||
proc newState(): State =
|
proc newState(): State
|
||||||
State(
|
|
||||||
data: none[UserStatus](),
|
|
||||||
loading: false,
|
|
||||||
status: Http200
|
|
||||||
)
|
|
||||||
|
|
||||||
var
|
var
|
||||||
state = newState()
|
state = newState()
|
||||||
|
|
||||||
proc getStatus
|
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) =
|
proc onStatus(httpStatus: int, response: kstring) =
|
||||||
state.loading = false
|
state.loading = false
|
||||||
state.status = httpStatus.HttpCode
|
state.status = httpStatus.HttpCode
|
||||||
|
|
@ -52,46 +57,6 @@ when defined(js):
|
||||||
let uri = makeUri("status.json")
|
let uri = makeUri("status.json")
|
||||||
ajaxGet(uri, @[], onStatus)
|
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 =
|
proc genSignUpModal(): VNode =
|
||||||
result = buildHtml():
|
result = buildHtml():
|
||||||
tdiv(class="modal", id="signup-modal"):
|
tdiv(class="modal", id="signup-modal"):
|
||||||
|
|
@ -136,7 +101,9 @@ when defined(js):
|
||||||
img(src="images/crown.png", id="img-logo") # TODO: Customisation.
|
img(src="images/crown.png", id="img-logo") # TODO: Customisation.
|
||||||
section(class="navbar-section"):
|
section(class="navbar-section"):
|
||||||
tdiv(class="input-group input-inline"):
|
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:
|
if state.loading:
|
||||||
tdiv(class="loading")
|
tdiv(class="loading")
|
||||||
elif user.isNone:
|
elif user.isNone:
|
||||||
|
|
@ -144,14 +111,14 @@ when defined(js):
|
||||||
button(class="btn btn-primary btn-sm"):
|
button(class="btn btn-primary btn-sm"):
|
||||||
italic(class="fas fa-user-plus")
|
italic(class="fas fa-user-plus")
|
||||||
text " Sign up"
|
text " Sign up"
|
||||||
a(href="#login-modal", id="login-btn"):
|
button(class="btn btn-primary btn-sm",
|
||||||
button(class="btn btn-primary btn-sm"):
|
onClick=(e: Event, n: VNode) => state.loginModal.show()):
|
||||||
italic(class="fas fa-sign-in-alt")
|
italic(class="fas fa-sign-in-alt")
|
||||||
text " Log in"
|
text " Log in"
|
||||||
else:
|
else:
|
||||||
render(user.get(), "avatar")
|
render(user.get(), "avatar")
|
||||||
|
|
||||||
# Modals
|
# Modals
|
||||||
genLoginModal()
|
render(state.loginModal)
|
||||||
|
|
||||||
genSignUpModal()
|
genSignUpModal()
|
||||||
|
|
@ -12,7 +12,7 @@ proc class*(classes: varargs[tuple[name: string, present: bool]],
|
||||||
for class in classes:
|
for class in classes:
|
||||||
if class.present: result.add(class.name & " ")
|
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.
|
## Concatenates ``relative`` to the current URL in a way that is sane.
|
||||||
var relative = relative
|
var relative = relative
|
||||||
assert appName in $window.location.pathname
|
assert appName in $window.location.pathname
|
||||||
|
|
@ -23,10 +23,10 @@ proc makeUri*(relative: string, appName=appName): string =
|
||||||
appName &
|
appName &
|
||||||
relative &
|
relative &
|
||||||
$window.location.search &
|
$window.location.search &
|
||||||
$window.location.hash
|
(if includeHash: $window.location.hash else: "")
|
||||||
|
|
||||||
proc makeUri*(relative: string, params: varargs[(string, string)],
|
proc makeUri*(relative: string, params: varargs[(string, string)],
|
||||||
appName=appName): string =
|
appName=appName, includeHash=false): string =
|
||||||
var query = ""
|
var query = ""
|
||||||
for i in 0 ..< params.len:
|
for i in 0 ..< params.len:
|
||||||
let param = params[i]
|
let param = params[i]
|
||||||
|
|
@ -38,18 +38,20 @@ proc makeUri*(relative: string, params: varargs[(string, string)],
|
||||||
else:
|
else:
|
||||||
makeUri(relative, appName)
|
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?
|
proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb?
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
# TODO: Why does Karax have it's own Node type? That's just silly.
|
# TODO: Why does Karax have it's own Node type? That's just silly.
|
||||||
let url = cast[dom.Node](n.dom).getAttribute(cstring"href")
|
let url = cast[dom.Node](n.dom).getAttribute(cstring"href")
|
||||||
|
|
||||||
# TODO: This was annoying. Karax also shouldn't have its own `window`.
|
navigateTo(url)
|
||||||
dom.pushState(dom.window.history, 5, cstring"Thread", url)
|
|
||||||
|
|
||||||
# Fire the popState event.
|
|
||||||
dom.window.dispatchEvent(newEvent("popstate"))
|
|
||||||
|
|
||||||
|
|
||||||
type
|
type
|
||||||
FormData* = ref object
|
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