From f9efbe04d303e604bbcb6ce67eb3cc5372a34381 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 15:49:22 +0100 Subject: [PATCH] Implements proper error handling for login form. --- forum.nim | 2 +- redesign/header.nim | 77 ++++++++-------------------- redesign/karaxutils.nim | 20 ++++---- redesign/login.nim | 109 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 65 deletions(-) create mode 100644 redesign/login.nim diff --git a/forum.nim b/forum.nim index b353ddd..03b9390 100644 --- a/forum.nim +++ b/forum.nim @@ -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() diff --git a/redesign/header.nim b/redesign/header.nim index f9b9d05..1d7b3bb 100644 --- a/redesign/header.nim +++ b/redesign/header.nim @@ -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() \ No newline at end of file diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index 6830aa4..2c7f7ad 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -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 diff --git a/redesign/login.nim b/redesign/login.nim new file mode 100644 index 0000000..aa92fac --- /dev/null +++ b/redesign/login.nim @@ -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" \ No newline at end of file