From 9d2b6ee10ec5359cc91769d430485c8c869ba1a8 Mon Sep 17 00:00:00 2001 From: Akshay Date: Thu, 24 Dec 2020 10:51:40 +0530 Subject: monorepo --- frontend/src/Cart.elm | 164 +++++++++++++++++++++++ frontend/src/Catalog.elm | 125 +++++++++++++++++ frontend/src/Login.elm | 119 +++++++++++++++++ frontend/src/Main.elm | 339 +++++++++++++++++++++++++++++++++++++++++++++++ frontend/src/Product.elm | 302 +++++++++++++++++++++++++++++++++++++++++ frontend/src/Signup.elm | 194 +++++++++++++++++++++++++++ 6 files changed, 1243 insertions(+) create mode 100644 frontend/src/Cart.elm create mode 100644 frontend/src/Catalog.elm create mode 100644 frontend/src/Login.elm create mode 100644 frontend/src/Main.elm create mode 100644 frontend/src/Product.elm create mode 100644 frontend/src/Signup.elm (limited to 'frontend/src') diff --git a/frontend/src/Cart.elm b/frontend/src/Cart.elm new file mode 100644 index 0000000..a1750f6 --- /dev/null +++ b/frontend/src/Cart.elm @@ -0,0 +1,164 @@ +module Cart exposing (..) + +import Browser +import Browser.Navigation as Nav +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Http +import Json.Decode as D +import Json.Encode as Encode +import Url +import Url.Parser as P exposing ((), Parser, int, oneOf, s, string) + + +type alias Product = + { id : Int + , name : String + , kind : Maybe String + , price : Float + , description : Maybe String + } + + +type alias Model = + { pageStatus : Status + , products : List Product + } + + +type Status + = Loading + | Loaded + | NotLoaded + + +type Msg + = CartLoaded (Result Http.Error (List Product)) + | FetchCartItems + | RemoveFromCart Int + | CartItemRemoved (Result Http.Error ()) + + +init : Model +init = + Model NotLoaded [] + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + CartLoaded res -> + case res of + Ok s -> + ( { model | products = s, pageStatus = Loaded }, Cmd.none ) + + Err e -> + let + _ = + Debug.log "error" e + in + ( { model | pageStatus = NotLoaded }, Cmd.none ) + + RemoveFromCart id -> + ( model, removeProduct id ) + + CartItemRemoved _ -> + ( { model | pageStatus = Loading }, fetchCartItems ) + + FetchCartItems -> + ( { model | pageStatus = Loading }, fetchCartItems ) + + +decodeProduct : D.Decoder Product +decodeProduct = + D.map5 Product + (D.field "id" D.int) + (D.field "name" D.string) + (D.field "kind" (D.nullable D.string)) + (D.field "price" D.float) + (D.field "description" (D.nullable D.string)) + + +decodeResponse : D.Decoder (List Product) +decodeResponse = + D.list decodeProduct + + +removeProduct : Int -> Cmd Msg +removeProduct id = + let + _ = + Debug.log "cart" "fetching cart items" + in + Http.riskyRequest + { method = "POST" + , headers = [] + , url = "http://127.0.0.1:7878/cart/remove" + , body = Http.stringBody "application/json" <| String.fromInt id + , expect = Http.expectWhatever CartItemRemoved + , timeout = Nothing + , tracker = Nothing + } + + +fetchCartItems : Cmd Msg +fetchCartItems = + let + _ = + Debug.log "cart" "fetching cart items" + in + Http.riskyRequest + { method = "GET" + , headers = [] + , url = "http://127.0.0.1:7878/cart/items" + , body = Http.emptyBody + , expect = Http.expectJson CartLoaded decodeResponse + , timeout = Nothing + , tracker = Nothing + } + + +viewStatus : Status -> String +viewStatus s = + case s of + Loading -> + "Loading" + + Loaded -> + "Ready!" + + NotLoaded -> + "Not loaded ..." + + +viewProduct : Product -> Html Msg +viewProduct p = + div [] + [ text p.name + , div [] [ text <| Maybe.withDefault "" p.kind ] + , div [] [ text <| Maybe.withDefault "" p.description ] + , div [] [ text <| String.fromFloat p.price ] + , div [] [ button [ onClick (RemoveFromCart p.id) ] [ text "Remove" ] ] + , div [] [ a [ href ("/product/" ++ String.fromInt p.id) ] [ text "View Product" ] ] + ] + + +view : Model -> Html Msg +view model = + case model.pageStatus of + Loading -> + div [] [ text <| viewStatus Loading ] + + _ -> + div [] + [ let + cart = + List.map viewProduct model.products + in + if List.isEmpty cart then + text "No items in cart" + + else + ul [] cart + ] diff --git a/frontend/src/Catalog.elm b/frontend/src/Catalog.elm new file mode 100644 index 0000000..80e5e38 --- /dev/null +++ b/frontend/src/Catalog.elm @@ -0,0 +1,125 @@ +module Catalog exposing (..) + +import Browser +import Browser.Navigation as Nav +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Http +import Json.Decode as D +import Json.Encode as Encode +import Url +import Url.Parser as P exposing ((), Parser, int, oneOf, s, string) + + +type alias Product = + { id : Int + , name : String + , kind : Maybe String + , price : Float + , description : Maybe String + } + + +type alias Model = + { pageStatus : Status + , products : List Product + } + + +type Status + = Loading + | Loaded + | NotLoaded + + +type Msg + = ProductsLoaded (Result Http.Error (List Product)) + | FetchProducts + + +init : Model +init = + Model NotLoaded [] + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + ProductsLoaded res -> + case res of + Ok s -> + ( { model | products = s, pageStatus = Loaded }, Cmd.none ) + + Err e -> + let + _ = + Debug.log "error" e + in + ( { model | pageStatus = NotLoaded }, Cmd.none ) + + FetchProducts -> + ( { model | pageStatus = Loading }, fetchProducts ) + + +decodeProduct : D.Decoder Product +decodeProduct = + D.map5 Product + (D.field "id" D.int) + (D.field "name" D.string) + (D.field "kind" (D.nullable D.string)) + (D.field "price" D.float) + (D.field "description" (D.nullable D.string)) + + +decodeResponse : D.Decoder (List Product) +decodeResponse = + D.list decodeProduct + + +fetchProducts : Cmd Msg +fetchProducts = + let + _ = + Debug.log "err" "fetching products" + in + Http.get + { url = "http://127.0.0.1:7878/product/catalog" + , expect = Http.expectJson ProductsLoaded decodeResponse + } + + +viewStatus : Status -> String +viewStatus s = + case s of + Loading -> + "Loading" + + Loaded -> + "Ready!" + + NotLoaded -> + "Not loaded ..." + + +viewProduct : Product -> Html Msg +viewProduct p = + div [] + [ text p.name + , text <| Maybe.withDefault "" p.kind + , text <| Maybe.withDefault "" p.description + , text <| String.fromFloat p.price + , a [ href ("/product/" ++ String.fromInt p.id) ] [ text "View Product" ] + ] + + +view : Model -> Html Msg +view model = + case model.pageStatus of + Loading -> + div [] [ text <| viewStatus Loading ] + + _ -> + div [] + [ ul [] (List.map viewProduct model.products) + ] diff --git a/frontend/src/Login.elm b/frontend/src/Login.elm new file mode 100644 index 0000000..dd168f0 --- /dev/null +++ b/frontend/src/Login.elm @@ -0,0 +1,119 @@ +module Login exposing (..) + +import Browser +import Browser.Navigation as Nav +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Http +import Json.Encode as Encode +import Url +import Url.Parser as P exposing ((), Parser, int, oneOf, s, string) + + +type alias Model = + { username : String + , password : String + , loginStatus : LoginStatus + } + + +type LoginStatus + = NotLoggedIn + | LoggedIn + | InvalidLogin + | LoggingIn + + +type Msg + = PassEntered String + | UserEntered String + | LoginPressed + | LoginSuccess (Result Http.Error ()) + | LoginFail + + +init : Model +init = + Model "" "" NotLoggedIn + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + PassEntered s -> + ( { model | password = s } + , Cmd.none + ) + + UserEntered s -> + ( { model | username = s } + , Cmd.none + ) + + LoginPressed -> + ( { model | loginStatus = LoggingIn }, tryLogin model ) + + LoginSuccess res -> + case res of + Ok s -> + ( { model | loginStatus = LoggedIn }, Cmd.none ) + + Err e -> + ( { model | loginStatus = InvalidLogin }, Cmd.none ) + + LoginFail -> + ( { model | loginStatus = InvalidLogin }, Cmd.none ) + + +encodeLogin : Model -> Encode.Value +encodeLogin model = + Encode.object + [ ( "username", Encode.string model.username ) + , ( "password", Encode.string model.password ) + ] + + +tryLogin : Model -> Cmd Msg +tryLogin model = + Http.riskyRequest + { method = "POST" + , headers = [] + , url = "http://127.0.0.1:7878/user/login" + , body = model |> encodeLogin |> Http.jsonBody + , expect = Http.expectWhatever LoginSuccess + , timeout = Nothing + , tracker = Nothing + } + + +viewStatus : LoginStatus -> String +viewStatus ls = + case ls of + NotLoggedIn -> + "Not Logged In" + + InvalidLogin -> + "Invalid Login" + + LoggedIn -> + "Logged in!" + + LoggingIn -> + "Logging In ..." + + +viewInput : String -> String -> String -> (String -> msg) -> Html msg +viewInput t p v toMsg = + input [ type_ t, placeholder p, value v, onInput toMsg ] [] + + +view : Model -> Html Msg +view model = + div [] + [ div [] [ viewInput "text" "Enter name here" model.username UserEntered ] + , div [] [ viewInput "password" "Password" model.password PassEntered ] + , div [] [ button [ onClick LoginPressed ] [ text "Login" ] ] + , div [] [ text (viewStatus model.loginStatus) ] + , div [] [ text "Don't have an account? ", a [ href "/signup" ] [ text "Register now!" ] ] + ] diff --git a/frontend/src/Main.elm b/frontend/src/Main.elm new file mode 100644 index 0000000..bf1583c --- /dev/null +++ b/frontend/src/Main.elm @@ -0,0 +1,339 @@ +module Main exposing (Model, Msg(..), init, main, subscriptions, update, view, viewLink) + +import Browser +import Browser.Navigation as Nav +import Cart +import Catalog +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Http +import Json.Encode as Encode +import Login +import Product +import Signup +import Url +import Url.Parser as P exposing ((), Parser, int, oneOf, s, string) + + + +-- MAIN + + +main : Program () Model Msg +main = + Browser.application + { init = init + , view = view + , update = update + , subscriptions = subscriptions + , onUrlChange = UrlChanged + , onUrlRequest = LinkClicked + } + + + +-- MODEL + + +type Route + = LoginPage + | SignupPage + | HomePage + | CatalogPage + | CartPage + | ProductPage Int + | NotFoundPage + + +parseRoute : Parser (Route -> a) a +parseRoute = + oneOf + [ P.map LoginPage (P.s "login") + , P.map HomePage P.top + , P.map CatalogPage (P.s "catalog") + , P.map CartPage (P.s "cart") + , P.map SignupPage (P.s "signup") + , P.map ProductPage (P.s "product" P.int) + + --, P.map ProductPage (P.s "product" int) + ] + + +type alias Model = + { key : Nav.Key + , url : Url.Url + , location : Route + , loginModel : Login.Model + , catalogModel : Catalog.Model + , productModel : Product.Model + , signupModel : Signup.Model + , cartModel : Cart.Model + } + + +init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg ) +init flags url key = + let + start = + HomePage + + login = + Login.init + + catalog = + Catalog.init + + product = + Product.init + + signup = + Signup.init + + cart = + Cart.init + in + ( Model key url start login catalog product signup cart, Cmd.none ) + + + +-- UPDATE + + +type Msg + = LinkClicked Browser.UrlRequest + | UrlChanged Url.Url + | LoginMessage Login.Msg + | CatalogMessage Catalog.Msg + | ProductMessage Product.Msg + | SignupMessage Signup.Msg + | CartMessage Cart.Msg + | LogoutPressed + | LogoutSuccess (Result Http.Error ()) + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + LinkClicked urlRequest -> + case urlRequest of + Browser.Internal url -> + ( model, Nav.pushUrl model.key (Url.toString url) ) + + Browser.External href -> + ( model, Nav.load href ) + + LogoutPressed -> + ( model, tryLogout ) + + LogoutSuccess _ -> + ( model, Nav.replaceUrl model.key "/login" ) + + UrlChanged url -> + let + parsedUrl = + P.parse parseRoute url + in + case parsedUrl of + Just CatalogPage -> + ( { model | location = CatalogPage }, Cmd.map CatalogMessage Catalog.fetchProducts ) + + Just (ProductPage id) -> + let + cmds = + List.map (Cmd.map ProductMessage) + [ Product.fetchListing id + , Product.fetchRatings id + ] + in + ( { model | location = ProductPage id }, Cmd.batch cmds ) + + Just CartPage -> + let + cmd = + Cmd.map CartMessage Cart.fetchCartItems + in + ( { model | location = CartPage }, cmd ) + + Just p -> + ( { model | location = p }, Cmd.none ) + + Nothing -> + ( { model | location = NotFoundPage }, Cmd.none ) + + LoginMessage lm -> + let + ( lmn, cmd ) = + Login.update lm model.loginModel + + redir = + case lmn.loginStatus of + Login.LoggedIn -> + Nav.replaceUrl model.key "/catalog" + + _ -> + Cmd.none + in + ( { model | loginModel = lmn }, Cmd.batch [ Cmd.map LoginMessage cmd, redir ] ) + + SignupMessage sm -> + let + ( smn, cmd ) = + Signup.update sm model.signupModel + + redir = + case smn.status of + Signup.CreatedSuccessfully -> + Nav.replaceUrl model.key "/login" + + _ -> + Cmd.none + in + ( { model | signupModel = smn }, Cmd.batch [ Cmd.map SignupMessage cmd, redir ] ) + + CatalogMessage cm -> + let + ( cmn, cmd ) = + Catalog.update cm model.catalogModel + in + ( { model | catalogModel = cmn }, Cmd.map CatalogMessage cmd ) + + CartMessage cm -> + let + ( cmn, cmd ) = + Cart.update cm model.cartModel + in + ( { model | cartModel = cmn }, Cmd.map CartMessage cmd ) + + ProductMessage pm -> + let + ( pmn, cmd ) = + Product.update pm model.productModel + + redir = + case pm of + Product.AddToCartSuccess _ -> + Nav.replaceUrl model.key "/cart" + + _ -> + Cmd.none + in + ( { model | productModel = pmn }, Cmd.batch [ Cmd.map ProductMessage cmd, redir ] ) + + +tryLogout : Cmd Msg +tryLogout = + Http.riskyRequest + { method = "POST" + , headers = [] + , url = "http://127.0.0.1:7878/user/logout" + , body = Http.emptyBody + , expect = Http.expectWhatever LogoutSuccess + , timeout = Nothing + , tracker = Nothing + } + + + +-- SUBSCRIPTIONS + + +subscriptions : Model -> Sub Msg +subscriptions _ = + Sub.none + + + +-- VIEW + + +view : Model -> Browser.Document Msg +view model = + case model.location of + LoginPage -> + { title = "Login" + , body = [ Html.map LoginMessage (Login.view model.loginModel) ] + } + + SignupPage -> + { title = "Signup" + , body = [ Html.map SignupMessage (Signup.view model.signupModel) ] + } + + HomePage -> + { title = "URL Interceptor" + , body = + [ text "The current URL is: " + , b [] [ text (Url.toString model.url) ] + , ul [] + [ viewLink "/login" + , viewLink "/catalog" + , viewLink "/cart" + , viewLink "/signup" + ] + ] + } + + NotFoundPage -> + { title = "404 - Not Found" + , body = + [ text "404 - Not Found" + , a [ href "/" ] [ text "Go back >" ] + ] + } + + CatalogPage -> + { title = "Catalog" + , body = pageWrap model (Html.map CatalogMessage (Catalog.view model.catalogModel)) + } + + CartPage -> + { title = "Cart" + , body = pageWrap model (Html.map CartMessage (Cart.view model.cartModel)) + } + + ProductPage item -> + { title = "Product " ++ String.fromInt item + , body = pageWrap model (Html.map ProductMessage (Product.view model.productModel)) + } + + +viewHeader : Model -> Html Msg +viewHeader model = + let + links = + [ ( "Home", "/" ) + , ( "Catalog", "/catalog" ) + , ( "Cart", "/cart" ) + ] + in + div [] + [ List.map + (\( name, loc ) -> + li [] + [ a [ href loc ] [ text name ] + ] + ) + links + ++ [ if model.loginModel.loginStatus /= Login.LoggedIn then + li [] [ a [ href "/login" ] [ text "Login" ] ] + + else + button [ onClick LogoutPressed ] [ text "Logout" ] + ] + |> ul [] + ] + + +pageWrap : Model -> Html Msg -> List (Html Msg) +pageWrap model page = + [ div [] + [ viewHeader model + , page + ] + ] + + +viewLink : String -> Html msg +viewLink path = + li [] [ a [ href path ] [ text path ] ] diff --git a/frontend/src/Product.elm b/frontend/src/Product.elm new file mode 100644 index 0000000..0ea0ce1 --- /dev/null +++ b/frontend/src/Product.elm @@ -0,0 +1,302 @@ +module Product exposing (..) + +import Browser +import Browser.Navigation as Nav +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Http +import Json.Decode as D +import Json.Encode as Encode +import Url +import Url.Parser as P exposing ((), Parser, int, oneOf, s, string) + + +type SubmitStatus + = SubmitSuccess + | SubmitFail + | Submitting + | NotSubmitted + + +type alias Product = + { id : Int + , name : String + , kind : Maybe String + , price : Float + , description : Maybe String + } + + +emptyProduct = + Product -1 "" Nothing 0 Nothing + + +type alias Rating = + { commentDate : String + , commentText : Maybe String + , customerName : String + , productName : String + , stars : Int + } + + +type alias Model = + { pageStatus : Status + , listing : Product + , ratings : List Rating + , ratingStars : Int + , ratingText : String + , addRatingStatus : SubmitStatus + } + + +type Status + = Loading + | Loaded + | NotLoaded + + +type Msg + = ListingLoaded (Result Http.Error Product) + | RatingsLoaded (Result Http.Error (List Rating)) + | FetchProduct Int + | FetchRatings Int + | AddRatingStars Int + | AddRatingComment String + | AddRatingPressed + | AddRatingSuccess (Result Http.Error ()) + | AddRatingFail + | AddToCartSuccess (Result Http.Error ()) + | AddToCartPressed + + +init : Model +init = + Model NotLoaded emptyProduct [] 0 "" NotSubmitted + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + ListingLoaded res -> + case res of + Ok s -> + ( { model | listing = s, pageStatus = Loaded }, Cmd.none ) + + Err e -> + let + _ = + Debug.log "error" e + in + ( { model | pageStatus = NotLoaded }, Cmd.none ) + + RatingsLoaded res -> + case res of + Ok s -> + ( { model | ratings = s, pageStatus = Loaded }, Cmd.none ) + + Err e -> + let + _ = + Debug.log "error" e + in + ( { model | pageStatus = NotLoaded }, Cmd.none ) + + FetchProduct id -> + ( { model | pageStatus = Loading }, fetchListing id ) + + FetchRatings id -> + ( { model | pageStatus = Loading }, fetchRatings id ) + + AddRatingStars i -> + ( { model | ratingStars = i }, Cmd.none ) + + AddRatingComment s -> + ( { model | ratingText = s }, Cmd.none ) + + AddRatingPressed -> + ( { model | addRatingStatus = Submitting } + , submitRating model + ) + + AddRatingSuccess res -> + case res of + Ok _ -> + ( { model | addRatingStatus = SubmitSuccess }, fetchRatings model.listing.id ) + + Err _ -> + ( { model | addRatingStatus = SubmitFail }, Cmd.none ) + + AddRatingFail -> + ( { model | addRatingStatus = SubmitFail }, Cmd.none ) + + AddToCartPressed -> + ( model, addToCart model ) + + AddToCartSuccess _ -> + ( model, Cmd.none ) + + +decodeProduct : D.Decoder Product +decodeProduct = + D.map5 Product + (D.field "id" D.int) + (D.field "name" D.string) + (D.field "kind" (D.nullable D.string)) + (D.field "price" D.float) + (D.field "description" (D.nullable D.string)) + + +decodeRating : D.Decoder Rating +decodeRating = + D.map5 Rating + (D.field "comment_date" D.string) + (D.field "comment_text" (D.nullable D.string)) + (D.field "customer_name" D.string) + (D.field "product_name" D.string) + (D.field "stars" D.int) + + +decodeRatings : D.Decoder (List Rating) +decodeRatings = + D.list decodeRating + + +fetchListing : Int -> Cmd Msg +fetchListing id = + let + _ = + Debug.log "err" <| "fetching listing " ++ String.fromInt id + in + Http.get + { url = "http://127.0.0.1:7878/product/" ++ String.fromInt id + , expect = Http.expectJson ListingLoaded decodeProduct + } + + +fetchRatings : Int -> Cmd Msg +fetchRatings id = + let + _ = + Debug.log "err" <| "fetching ratings " ++ String.fromInt id + in + Http.get + { url = "http://127.0.0.1:7878/product/reviews/" ++ String.fromInt id + , expect = Http.expectJson RatingsLoaded decodeRatings + } + + +encodeRatingForm : Model -> Encode.Value +encodeRatingForm model = + Encode.object + [ ( "product_id", Encode.int model.listing.id ) + , ( "stars", Encode.int model.ratingStars ) + , ( "comment_text", Encode.string model.ratingText ) + ] + + +submitRating : Model -> Cmd Msg +submitRating model = + let + _ = + Debug.log "err" <| "submitting rating for" ++ String.fromInt model.listing.id + in + Http.riskyRequest + { method = "POST" + , headers = [] + , url = "http://127.0.0.1:7878/rating/add" + , body = model |> encodeRatingForm |> Http.jsonBody + , expect = Http.expectWhatever AddRatingSuccess + , timeout = Nothing + , tracker = Nothing + } + + +addToCart : Model -> Cmd Msg +addToCart model = + let + _ = + Debug.log "err" <| "adding to cart: " ++ String.fromInt model.listing.id + in + Http.riskyRequest + { method = "POST" + , headers = [] + , url = "http://127.0.0.1:7878/cart/add" + , body = Http.stringBody "applcation/json" <| String.fromInt <| model.listing.id + , expect = Http.expectWhatever AddToCartSuccess + , timeout = Nothing + , tracker = Nothing + } + + +viewStatus : Status -> String +viewStatus s = + case s of + Loading -> + "Loading" + + Loaded -> + "Ready!" + + NotLoaded -> + "Not loaded ..." + + +viewProduct : Product -> Html Msg +viewProduct p = + div [] + [ text p.name + , text <| Maybe.withDefault "" p.kind + , text <| Maybe.withDefault "" p.description + , text <| String.fromFloat p.price + ] + + +viewRating : Rating -> Html Msg +viewRating r = + div [] + [ text <| r.customerName ++ " posted on " + , text <| r.commentDate ++ " " + , text <| Maybe.withDefault "" r.commentText + , text <| " Stars: " ++ String.fromInt r.stars + ] + + +viewInput : String -> String -> String -> (String -> msg) -> Html msg +viewInput t p v toMsg = + input [ type_ t, placeholder p, value v, onInput toMsg ] [] + + +viewStars : Html Msg +viewStars = + ul [] + (List.map + (\i -> button [ onClick (AddRatingStars i) ] [ text <| String.fromInt i ]) + [ 0, 1, 2, 3, 4, 5 ] + ) + + +view : Model -> Html Msg +view model = + case model.pageStatus of + Loading -> + div [] [ text <| viewStatus Loading ] + + _ -> + div [] + [ div [] [ viewProduct model.listing ] + , ul [] (List.map viewRating model.ratings) + , div [] [ text "Add Rating: " ] + , div [] + [ viewStars + , viewInput "text" "Enter Comment Text" model.ratingText AddRatingComment + , button [ onClick AddRatingPressed ] [ text "Submit Rating" ] + ] + , div [] + [ button [ onClick AddToCartPressed ] [ text "Add To Cart" ] + ] + , div [] + [ a [ href "/catalog" ] [ text "Back to catalog" ] + ] + ] diff --git a/frontend/src/Signup.elm b/frontend/src/Signup.elm new file mode 100644 index 0000000..6395b57 --- /dev/null +++ b/frontend/src/Signup.elm @@ -0,0 +1,194 @@ +module Signup exposing (..) + +import Browser +import Browser.Navigation as Nav +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Http +import Json.Encode as Encode +import Url +import Url.Parser as P exposing ((), Parser, int, oneOf, s, string) + + +type alias Model = + { username : String + , password : String + , phoneNumber : String + , emailId : String + , address : Maybe String + , status : Status + } + + +type Status + = UsernameTaken + | InvalidPhone + | InvalidEmail + | CreatedSuccessfully + | CreatingUser + | Empty + + +type Msg + = UserEntered String + | PassEntered String + | PhoneEntered String + | EmailEntered String + | AddressEntered String + | CreatePressed + | CreationSuccess (Result Http.Error ()) + | UsernameExists (Result Http.Error String) + | CreationFail + + +init : Model +init = + Model "" "" "" "" Nothing Empty + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + UserEntered s -> + ( { model | username = s } + , Cmd.none + ) + + PassEntered s -> + ( { model | password = s } + , Cmd.none + ) + + PhoneEntered s -> + let + status = + if String.length s /= 10 || (List.all (not << Char.isDigit) <| String.toList s) then + InvalidPhone + + else + Empty + in + ( { model | phoneNumber = s, status = status } + , Cmd.none + ) + + EmailEntered s -> + let + status = + if not <| String.contains "@" s then + InvalidEmail + + else + Empty + in + ( { model | emailId = s, status = status } + , Cmd.none + ) + + AddressEntered s -> + ( { model | address = Just s } + , Cmd.none + ) + + CreatePressed -> + ( { model | status = CreatingUser }, checkExists model ) + + CreationSuccess res -> + case res of + Ok _ -> + ( { model | status = CreatedSuccessfully }, Cmd.none ) + + Err _ -> + ( model, Cmd.none ) + + CreationFail -> + ( init, Cmd.none ) + + UsernameExists res -> + case res of + Ok "true" -> + ( { model | status = UsernameTaken }, Cmd.none ) + + Ok "false" -> + let + _ = + Debug.log "signup" "Hit create user ..." + in + ( { model | status = CreatingUser }, createUser model ) + + _ -> + ( model, Cmd.none ) + + +encodeCreateUser : Model -> Encode.Value +encodeCreateUser model = + Encode.object + [ ( "username", Encode.string model.username ) + , ( "password", Encode.string model.password ) + , ( "phone_number", Encode.string model.phoneNumber ) + , ( "email_id", Encode.string model.emailId ) + , ( "address", Encode.string <| Maybe.withDefault "" model.address ) + ] + + +checkExists : Model -> Cmd Msg +checkExists model = + Http.post + { url = "http://127.0.0.1:7878/user/existing" + , body = Http.stringBody "application/json" model.username + , expect = Http.expectString UsernameExists + } + + +createUser : Model -> Cmd Msg +createUser model = + Http.riskyRequest + { method = "POST" + , headers = [] + , url = "http://127.0.0.1:7878/user/new" + , body = model |> encodeCreateUser |> Http.jsonBody + , expect = Http.expectWhatever CreationSuccess + , timeout = Nothing + , tracker = Nothing + } + + +viewStatus : Status -> String +viewStatus s = + case s of + UsernameTaken -> + "This username is taken!" + + InvalidPhone -> + "Invalid phone number!" + + InvalidEmail -> + "Invalid email address!" + + CreatedSuccessfully -> + "User created successfully" + + CreatingUser -> + "Creating user ..." + + Empty -> + "" + + +viewInput : String -> String -> String -> (String -> msg) -> Html msg +viewInput t p v toMsg = + input [ type_ t, placeholder p, value v, onInput toMsg ] [] + + +view : Model -> Html Msg +view model = + div [] + [ viewInput "text" "Enter Username" model.username UserEntered + , viewInput "password" "Password" model.password PassEntered + , viewInput "text" "Email" model.emailId EmailEntered + , viewInput "text" "Enter your Phone number" model.phoneNumber PhoneEntered + , viewInput "text" "Enter Shipping address" (Maybe.withDefault "" model.address) AddressEntered + , button [ onClick CreatePressed ] [ text "Create" ] + , text (viewStatus model.status) + ] -- cgit v1.2.3