summaryrefslogtreecommitdiff
path: root/elm3/User
diff options
context:
space:
mode:
Diffstat (limited to 'elm3/User')
-rw-r--r--elm3/User/Login.elm85
-rw-r--r--elm3/User/PassReset.elm89
-rw-r--r--elm3/User/PassSet.elm93
-rw-r--r--elm3/User/Register.elm108
-rw-r--r--elm3/User/Settings.elm206
5 files changed, 581 insertions, 0 deletions
diff --git a/elm3/User/Login.elm b/elm3/User/Login.elm
new file mode 100644
index 00000000..abfe44ec
--- /dev/null
+++ b/elm3/User/Login.elm
@@ -0,0 +1,85 @@
+module User.Login exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Json.Encode as JE
+import Browser
+import Browser.Navigation exposing (load)
+import Lib.Api as Api
+import Lib.Html exposing (..)
+
+
+main : Program () Model Msg
+main = Browser.element
+ { init = always (Model "" "" Api.Normal, Cmd.none)
+ , subscriptions = always Sub.none
+ , view = view
+ , update = update
+ }
+
+
+encodeForm : Model -> JE.Value
+encodeForm o = JE.object
+ [ ("username", JE.string o.username)
+ , ("password", JE.string o.password) ]
+
+
+type alias Model =
+ { username : String
+ , password : String
+ , state : Api.State
+ }
+
+
+type Msg
+ = Username String
+ | Password String
+ | Submit
+ | Submitted Api.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Username n -> ({ model | username = n }, Cmd.none)
+ Password n -> ({ model | password = n }, Cmd.none)
+
+ Submit -> ( { model | state = Api.Loading }
+ , Api.post "/u/login" (encodeForm model) Submitted
+ )
+
+ Submitted Api.Success -> (model, load "/")
+ Submitted e -> ({ model | state = Api.Error e }, Cmd.none)
+
+
+view : Model -> Html Msg
+view model = form_ Submit (model.state == Api.Loading)
+ [ div [ class "card card--white card--no-separators flex-expand small-card mb-5" ]
+ [ div [ class "card__header" ] [ div [ class "card__title" ] [ text "Log in" ]]
+ , case model.state of
+ Api.Error e ->
+ div [ class "card__section card__section--error fs-medium" ]
+ [ h5 [] [ text "Error" ]
+ , ul []
+ [ li [] [ text <| Api.showResponse e ]
+ , li [] [ text "If you have not used this login form since October 2014, your account has likely been disabled. You can reset your password to regain access." ]
+ ]
+ ]
+ _ -> text ""
+ , div [ class "card__section fs-medium" ]
+ [ div [ class "form-group" ] [ inputText "username" model.username Username [placeholder "Username", required True, pattern "[a-z0-9-]{2,15}"] ]
+ , div [ class "form-group" ] [ inputText "password" model.password Password [placeholder "Password", required True, minlength 4, maxlength 500, type_ "password"] ]
+ , div [ class "d-flex jc-between" ] [ a [ href "/u/newpass" ] [ text "Forgot your password?" ] ]
+ ]
+ , div [ class "card__section" ]
+ [ div [ class "d-flex jc-between" ]
+ [ a [ class "btn btn--subtle", href "/u/register" ] [ text "Create an account" ]
+ , if model.state == Api.Loading
+ then div [ class "spinner spinner--md pull-right" ] []
+ else text ""
+ , input [ type_ "submit", class "btn", tabindex 10, value "Log in" ] []
+ ]
+ ]
+ ]
+ ]
diff --git a/elm3/User/PassReset.elm b/elm3/User/PassReset.elm
new file mode 100644
index 00000000..aa649c24
--- /dev/null
+++ b/elm3/User/PassReset.elm
@@ -0,0 +1,89 @@
+module User.PassReset exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Browser
+import Json.Encode as JE
+import Lib.Api as Api
+import Lib.Html exposing (..)
+
+
+main : Program () Model Msg
+main = Browser.element
+ { init = always (Model "" False Api.Normal, Cmd.none)
+ , subscriptions = always Sub.none
+ , view = view
+ , update = update
+ }
+
+
+encodeForm : Model -> JE.Value
+encodeForm o = JE.object
+ [ ("email", JE.string o.email) ]
+
+
+type alias Model =
+ { email : String
+ , success : Bool
+ , state : Api.State
+ }
+
+
+type Msg
+ = EMail String
+ | Submit
+ | Submitted Api.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ EMail n -> ({ model | email = n }, Cmd.none)
+
+ Submit -> ( { model | state = Api.Loading }
+ , Api.post "/u/newpass" (encodeForm model) Submitted
+ )
+
+ Submitted Api.Success -> ({ model | success = True }, Cmd.none)
+ Submitted e -> ({ model | state = Api.Error e }, Cmd.none)
+
+
+view : Model -> Html Msg
+view model = form_ Submit (model.state == Api.Loading)
+ [ div [ class "card card--white card--no-separators flex-expand small-card mb-5" ] <|
+ [ div [ class "card__header" ] [ div [ class "card__title" ] [ text "Reset password" ]]
+ , case model.state of
+ Api.Error e ->
+ div [ class "card__section card__section--error fs-medium" ]
+ [ h5 [] [ text "Error" ]
+ , text <| Api.showResponse e
+ ]
+ _ -> text ""
+ ] ++ if model.success
+ then
+ [ div [ class "card__section fs-medium" ]
+ [ text "Your password has been reset and instructions to set a new one should reach your mailbox in a few minutes." ]
+ ]
+ else
+ [
+ div [ class "card__section fs-medium" ]
+ [ div [ class "form-group" ]
+ [ div [ class "form-group__help" ]
+ [ text "Forgot your password and can\'t login to VNDB anymore?"
+ , br [] []
+ , text "Don't worry! Just fill in the email address you used to register on VNDB, and you'll receive instructions to set a new password within a few minutes!"
+ ]
+ , inputText "email" model.email EMail [required True, type_ "email"]
+ ]
+ ]
+ , div [ class "card__section" ]
+ [ div [ class "d-flex jc-end" ]
+ [ if model.state == Api.Loading
+ then div [ class "spinner spinner--md pull-right" ] []
+ else text ""
+ , input [ type_ "submit", class "btn", tabindex 10, value "Submit" ] []
+ ]
+ ]
+ ]
+ ]
diff --git a/elm3/User/PassSet.elm b/elm3/User/PassSet.elm
new file mode 100644
index 00000000..13b45de1
--- /dev/null
+++ b/elm3/User/PassSet.elm
@@ -0,0 +1,93 @@
+module User.PassSet exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Json.Encode as JE
+import Browser
+import Browser.Navigation exposing (load)
+import Lib.Api as Api
+import Lib.Html exposing (..)
+
+
+main : Program String Model Msg
+main = Browser.element
+ { init = \s ->
+ ( { url = s
+ , pass1 = ""
+ , pass2 = ""
+ , badPass = False
+ , state = Api.Normal
+ }
+ , Cmd.none)
+ , subscriptions = always Sub.none
+ , view = view
+ , update = update
+ }
+
+
+encodeForm : Model -> JE.Value
+encodeForm o = JE.object
+ [ ("pass", JE.string o.pass1) ]
+
+
+type alias Model =
+ { url : String
+ , pass1 : String
+ , pass2 : String
+ , badPass : Bool
+ , state : Api.State
+ }
+
+
+type Msg
+ = Pass1 String
+ | Pass2 String
+ | Submit
+ | Submitted Api.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Pass1 n -> ({ model | pass1 = n, badPass = False }, Cmd.none)
+ Pass2 n -> ({ model | pass2 = n, badPass = False }, Cmd.none)
+
+ Submit ->
+ if model.pass1 /= model.pass2
+ then ({ model | badPass = True }, Cmd.none)
+ else ( { model | state = Api.Loading }
+ , Api.post model.url (encodeForm model) Submitted)
+
+ Submitted Api.Success -> (model, load "/")
+ Submitted e -> ({ model | state = Api.Error e }, Cmd.none)
+
+
+view : Model -> Html Msg
+view model =
+ let err s =
+ div [ class "card__section card__section--error fs-medium" ]
+ [ h5 [] [ text "Error" ]
+ , text s
+ ]
+ in form_ Submit (model.state == Api.Loading)
+ [ div [ class "card card--white card--no-separators flex-expand small-card mb-5" ]
+ [ div [ class "card__header" ] [ div [ class "card__title" ] [ text "Set password" ]]
+ , case model.state of
+ Api.Error e -> err <| Api.showResponse e
+ _ -> text ""
+ , if model.badPass then err "Passwords to not match" else text ""
+ , div [ class "card__section fs-medium" ]
+ [ div [ class "form-group" ] [ inputText "pass1" model.pass1 Pass1 [placeholder "New password", required True, minlength 4, maxlength 500, type_ "password"] ]
+ , div [ class "form-group" ] [ inputText "pass2" model.pass2 Pass2 [placeholder "Repeat", required True, minlength 4, maxlength 500, type_ "password"] ]
+ ]
+ , div [ class "card__section" ]
+ [ div [ class "d-flex jc-end" ]
+ [ if model.state == Api.Loading
+ then div [ class "spinner spinner--md pull-right" ] []
+ else text ""
+ , input [ type_ "submit", class "btn", tabindex 10, value "Submit" ] []
+ ]
+ ]
+ ]
+ ]
diff --git a/elm3/User/Register.elm b/elm3/User/Register.elm
new file mode 100644
index 00000000..4a956a38
--- /dev/null
+++ b/elm3/User/Register.elm
@@ -0,0 +1,108 @@
+module User.Register exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Browser
+import Json.Encode as JE
+import Lib.Gen exposing (emailPattern)
+import Lib.Api as Api
+import Lib.Html exposing (..)
+
+
+main : Program () Model Msg
+main = Browser.element
+ { init = always (Model "" "" 0 False Api.Normal, Cmd.none)
+ , subscriptions = always Sub.none
+ , view = view
+ , update = update
+ }
+
+
+encodeForm : Model -> JE.Value
+encodeForm o = JE.object
+ [ ("username", JE.string o.username)
+ , ("email", JE.string o.email )
+ , ("vns", JE.int o.vns )]
+
+
+type alias Model =
+ { username : String
+ , email : String
+ , vns : Int
+ , success : Bool
+ , state : Api.State
+ }
+
+type Msg
+ = Username String
+ | EMail String
+ | VNs String
+ | Submit
+ | Submitted Api.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Username n -> ({ model | username = n }, Cmd.none)
+ EMail n -> ({ model | email = n }, Cmd.none)
+ VNs n -> ({ model | vns = Maybe.withDefault 0 (String.toInt n) }, Cmd.none)
+
+ Submit -> ( { model | state = Api.Loading }
+ , Api.post "/u/register" (encodeForm model) Submitted)
+
+ Submitted Api.Success -> ({ model | state = Api.Normal, success = True }, Cmd.none)
+ Submitted e -> ({ model | state = Api.Error e}, Cmd.none)
+
+
+view : Model -> Html Msg
+view model = form_ Submit (model.state == Api.Loading)
+ [ div [ class "card card--white card--no-separators flex-expand small-card mb-5" ] <|
+ [ div [ class "card__header" ] [ div [ class "card__title" ] [ text "Register" ]]
+ , case model.state of
+ Api.Error e ->
+ div [ class "card__section card__section--error fs-medium" ]
+ [ h5 [] [ text "Error" ]
+ , text <| Api.showResponse e
+ ]
+ _ -> text ""
+ ] ++ if model.success
+ then
+ [ div [ class "card__section fs-medium" ]
+ [ text "Your account has been created! In a few minutes, you should receive an email with instructions to set a password and activate your account." ]
+ ]
+ else
+ [
+ div [ class "card__section fs-medium" ]
+ [ div [ class "form-group" ]
+ [ label [ for "username" ] [ text "Username" ]
+ , inputText "username" model.username Username [required True, pattern "[a-z0-9-]{2,15}"]
+ , div [ class "form-group__help" ] [ text "Preferred username. Must be lowercase and can only consist of alphanumeric characters." ]
+ ]
+ , div [ class "form-group" ]
+ [ label [ for "email" ] [ text "Email" ]
+ , inputText "email" model.email EMail [required True, type_ "email", pattern emailPattern]
+ , div [ class "form-group__help" ]
+ [ text "Your email address will only be used in case you lose your password. We will never send spam or newsletters unless you explicitly ask us for it or we get hacked." ]
+ ]
+ , div [ class "form-group" ]
+ [ label [ for "vns" ] [ text "How many visual novels are there in the database?" ]
+ , inputText "vns" (String.fromInt model.vns) VNs [required True, pattern "[0-9]+"]
+ , div [ class "form-group__help" ]
+ [ text "Anti-bot question, you can find the answer on the "
+ , a [ href "/", target "_blank" ] [ text "main page" ]
+ , text "."
+ ]
+ ]
+ ]
+ , div [ class "card__section" ]
+ [ div [ class "d-flex jc-end" ]
+ [ if model.state == Api.Loading
+ then div [ class "spinner spinner--md pull-right" ] []
+ else text ""
+ , input [ type_ "submit", class "btn", tabindex 10, value "Submit" ] []
+ ]
+ ]
+ ]
+ ]
diff --git a/elm3/User/Settings.elm b/elm3/User/Settings.elm
new file mode 100644
index 00000000..538225ca
--- /dev/null
+++ b/elm3/User/Settings.elm
@@ -0,0 +1,206 @@
+module User.Settings exposing (main)
+
+import Bitwise exposing (..)
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Browser
+import Browser.Navigation exposing (reload)
+import Lib.Html exposing (..)
+import Lib.Gen exposing (..)
+import Lib.Api as Api
+
+
+main : Program UserEdit Model Msg
+main = Browser.element
+ { init = init
+ , view = view
+ , update = update
+ , subscriptions = always Sub.none
+ }
+
+
+type alias Model =
+ { state : Api.State
+ , saved : Bool
+ , data : UserEdit
+ , cpass : Bool
+ , pass1 : String
+ , pass2 : String
+ , opass : String
+ , passNeq : Bool
+ }
+
+
+init : UserEdit -> (Model, Cmd Msg)
+init d =
+ ({state = Api.Normal
+ , saved = False
+ , data = d
+ , cpass = False
+ , pass1 = ""
+ , pass2 = ""
+ , opass = ""
+ , passNeq = False
+ }, Cmd.none)
+
+
+encode : Model -> UserEditSend
+encode model =
+ { hide_list = model.data.hide_list
+ , ign_votes = model.data.ign_votes
+ , mail = model.data.mail
+ , password = if model.cpass then Just { old = model.opass, new = model.pass1 } else Nothing
+ , perm = model.data.perm
+ , show_nsfw = model.data.show_nsfw
+ , spoilers = model.data.spoilers
+ , tags_all = model.data.tags_all
+ , tags_cont = model.data.tags_cont
+ , tags_ero = model.data.tags_ero
+ , tags_tech = model.data.tags_tech
+ , traits_sexual = model.data.traits_sexual
+ , username = model.data.username
+ }
+
+
+type UpdateMsg
+ = Username String
+ | Email String
+ | Perm Int Bool
+ | IgnVotes Bool
+ | HideList Bool
+ | ShowNsfw Bool
+ | TraitsSexual Bool
+ | Spoilers String
+ | TagsAll Bool
+ | TagsCont Bool
+ | TagsEro Bool
+ | TagsTech Bool
+
+type Msg
+ = Submit
+ | Submitted Api.Response
+ | Set UpdateMsg
+ | CPass Bool
+ | OPass String
+ | Pass1 String
+ | Pass2 String
+
+
+updateField : UpdateMsg -> UserEdit -> UserEdit
+updateField msg model =
+ case msg of
+ Username s -> { model | username = s }
+ Email s -> { model | mail = s }
+ Perm n b -> { model | perm = if b then or model.perm n else and model.perm (complement n) }
+ IgnVotes b -> { model | ign_votes = b }
+ HideList b -> { model | hide_list = b }
+ ShowNsfw b -> { model | show_nsfw = b }
+ TraitsSexual b -> { model | traits_sexual = b }
+ Spoilers s -> { model | spoilers = Maybe.withDefault model.spoilers (String.toInt s) }
+ TagsAll b -> { model | tags_all = b }
+ TagsCont b -> { model | tags_cont = b }
+ TagsEro b -> { model | tags_ero = b }
+ TagsTech b -> { model | tags_tech = b }
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Set m -> ({ model | saved = False, data = updateField m model.data }, Cmd.none)
+ CPass b -> ({ model | saved = False, cpass = b }, Cmd.none)
+ OPass s -> ({ model | saved = False, opass = s }, Cmd.none)
+ Pass1 s -> ({ model | saved = False, pass1 = s, passNeq = s /= model.pass2 }, Cmd.none)
+ Pass2 s -> ({ model | saved = False, pass2 = s, passNeq = s /= model.pass1 }, Cmd.none)
+
+ Submit ->
+ let
+ path = "/u" ++ String.fromInt model.data.id ++ "/edit"
+ body = usereditSendEncode (encode model)
+ in ({ model | state = Api.Loading }, Api.post path body Submitted)
+
+ Submitted (Api.Success) ->
+ ( { model | state = Api.Normal, saved = True, cpass = False, opass = "", pass1 = "", pass2 = "" }
+ , if model.cpass then reload else Cmd.none
+ )
+ Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
+
+
+view : Model -> Html Msg
+view model =
+ form_ Submit (model.state == Api.Loading)
+ [ card "account" "Account info" [] <|
+
+ [ cardRow "General" Nothing <| formGroups
+ [ [ label [ for "username" ] [ text "Username" ]
+ , inputText "username" model.data.username (Set << Username) [required True, maxlength 200, pattern "[a-z0-9-]{2,15}", disabled (not model.data.authmod)]
+ ]
+ , [ label [ for "email" ] [ text "Email address" ]
+ , inputText "email" model.data.mail (Set << Email) [type_ "email", required True, pattern emailPattern]
+ ]
+ ]
+
+ , cardRow "Password" Nothing <| formGroups <|
+ [ label [ class "checkbox" ]
+ [ inputCheck "" model.cpass CPass
+ , text " Change password" ]
+ ]
+ :: if not model.cpass then [] else
+ [ [ label [ class "opass" ] [ text "Current password" ]
+ , inputText "opass" model.opass OPass [type_ "password", required True, minlength 4, maxlength 500]
+ ]
+ , [ label [ class "pass1" ] [ text "New password" ]
+ , inputText "pass1" model.pass1 Pass1 [type_ "password", required True, minlength 4, maxlength 500]
+ ]
+ , [ label [ class "pass2" ] [ text "Repeat" ]
+ , inputText "pass2" model.pass2 Pass2 [type_ "password", required True, minlength 4, maxlength 500, classList [("is-invalid", model.passNeq)]]
+ , if model.passNeq
+ then div [class "invalid-feedback"]
+ [ text "Passwords do not match." ]
+ else text ""
+ ]
+ ]
+
+ ] ++ if not model.data.authmod then [] else
+ [ cardRow "Mod options" Nothing <| formGroups
+ [ [ label [] [ text "Permissions" ]
+ ] ++ List.map (\(n,s) ->
+ label [ class "checkbox" ] [ inputCheck "" (and model.data.perm n > 0) (Set << Perm n), text (" " ++ s) ]
+ ) userPerms
+ , [ label [] [ text "Other" ]
+ , label [ class "checkbox" ] [ inputCheck "" model.data.ign_votes (Set << IgnVotes), text "Ignore votes in VN statistics" ]
+ ]
+ ]
+ ]
+
+ , card "preferences" "Preferences" [] <|
+
+ [ cardRow "Privacy" Nothing <| formGroup
+ [ label [ class "checkbox" ] [ inputCheck "" model.data.hide_list (Set << HideList), text "Hide my visual novel list, vote list and wishlist" ] ]
+
+ , cardRow "NSFW" Nothing <| formGroups
+ [ [ label [ class "checkbox" ] [ inputCheck "" model.data.show_nsfw (Set << ShowNsfw), text "Disable warnings for images that are not safe for work" ] ]
+ , [ label [ class "checkbox" ] [ inputCheck "" model.data.traits_sexual (Set << TraitsSexual), text "Show sexual traits by default on character pages" ] ]
+ ]
+
+ , cardRow "Spoilers" Nothing <| formGroup
+ [ label [ for "spoilers" ] [ text "Default spoiler level" ]
+ , inputSelect [onInput (Set << Spoilers)] (String.fromInt model.data.spoilers)
+ [ ("0", "Hide spoilers")
+ , ("1", "Show only minor spoilers")
+ , ("2", "Show all spoilers")
+ ]
+ ]
+
+ , cardRow "Tags" Nothing <| formGroups
+ [ [ label [ class "checkbox" ] [ inputCheck "" model.data.tags_all (Set << TagsAll), text "Show all tags by default on visual novel pages (don't summarize)" ] ]
+ , [ label [] [ text "Default tag categories on visual novel pages:" ]
+ , label [ class "chexkbox" ] [ inputCheck "" model.data.tags_cont (Set << TagsCont), text "Content" ]
+ , label [ class "chexkbox" ] [ inputCheck "" model.data.tags_ero (Set << TagsEro ), text "Sexual content" ]
+ , label [ class "chexkbox" ] [ inputCheck "" model.data.tags_tech (Set << TagsTech), text "Technical" ]
+ ]
+ ]
+ ]
+
+ , submitButton (if model.saved then "Saved!" else "Save") model.state (not model.passNeq) False
+ ]