aboutsummaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/Cart.elm164
-rw-r--r--frontend/src/Catalog.elm125
-rw-r--r--frontend/src/Login.elm119
-rw-r--r--frontend/src/Main.elm339
-rw-r--r--frontend/src/Product.elm302
-rw-r--r--frontend/src/Signup.elm194
6 files changed, 1243 insertions, 0 deletions
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 @@
1module Cart exposing (..)
2
3import Browser
4import Browser.Navigation as Nav
5import Html exposing (..)
6import Html.Attributes exposing (..)
7import Html.Events exposing (..)
8import Http
9import Json.Decode as D
10import Json.Encode as Encode
11import Url
12import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string)
13
14
15type alias Product =
16 { id : Int
17 , name : String
18 , kind : Maybe String
19 , price : Float
20 , description : Maybe String
21 }
22
23
24type alias Model =
25 { pageStatus : Status
26 , products : List Product
27 }
28
29
30type Status
31 = Loading
32 | Loaded
33 | NotLoaded
34
35
36type Msg
37 = CartLoaded (Result Http.Error (List Product))
38 | FetchCartItems
39 | RemoveFromCart Int
40 | CartItemRemoved (Result Http.Error ())
41
42
43init : Model
44init =
45 Model NotLoaded []
46
47
48update : Msg -> Model -> ( Model, Cmd Msg )
49update msg model =
50 case msg of
51 CartLoaded res ->
52 case res of
53 Ok s ->
54 ( { model | products = s, pageStatus = Loaded }, Cmd.none )
55
56 Err e ->
57 let
58 _ =
59 Debug.log "error" e
60 in
61 ( { model | pageStatus = NotLoaded }, Cmd.none )
62
63 RemoveFromCart id ->
64 ( model, removeProduct id )
65
66 CartItemRemoved _ ->
67 ( { model | pageStatus = Loading }, fetchCartItems )
68
69 FetchCartItems ->
70 ( { model | pageStatus = Loading }, fetchCartItems )
71
72
73decodeProduct : D.Decoder Product
74decodeProduct =
75 D.map5 Product
76 (D.field "id" D.int)
77 (D.field "name" D.string)
78 (D.field "kind" (D.nullable D.string))
79 (D.field "price" D.float)
80 (D.field "description" (D.nullable D.string))
81
82
83decodeResponse : D.Decoder (List Product)
84decodeResponse =
85 D.list decodeProduct
86
87
88removeProduct : Int -> Cmd Msg
89removeProduct id =
90 let
91 _ =
92 Debug.log "cart" "fetching cart items"
93 in
94 Http.riskyRequest
95 { method = "POST"
96 , headers = []
97 , url = "http://127.0.0.1:7878/cart/remove"
98 , body = Http.stringBody "application/json" <| String.fromInt id
99 , expect = Http.expectWhatever CartItemRemoved
100 , timeout = Nothing
101 , tracker = Nothing
102 }
103
104
105fetchCartItems : Cmd Msg
106fetchCartItems =
107 let
108 _ =
109 Debug.log "cart" "fetching cart items"
110 in
111 Http.riskyRequest
112 { method = "GET"
113 , headers = []
114 , url = "http://127.0.0.1:7878/cart/items"
115 , body = Http.emptyBody
116 , expect = Http.expectJson CartLoaded decodeResponse
117 , timeout = Nothing
118 , tracker = Nothing
119 }
120
121
122viewStatus : Status -> String
123viewStatus s =
124 case s of
125 Loading ->
126 "Loading"
127
128 Loaded ->
129 "Ready!"
130
131 NotLoaded ->
132 "Not loaded ..."
133
134
135viewProduct : Product -> Html Msg
136viewProduct p =
137 div []
138 [ text p.name
139 , div [] [ text <| Maybe.withDefault "" p.kind ]
140 , div [] [ text <| Maybe.withDefault "" p.description ]
141 , div [] [ text <| String.fromFloat p.price ]
142 , div [] [ button [ onClick (RemoveFromCart p.id) ] [ text "Remove" ] ]
143 , div [] [ a [ href ("/product/" ++ String.fromInt p.id) ] [ text "View Product" ] ]
144 ]
145
146
147view : Model -> Html Msg
148view model =
149 case model.pageStatus of
150 Loading ->
151 div [] [ text <| viewStatus Loading ]
152
153 _ ->
154 div []
155 [ let
156 cart =
157 List.map viewProduct model.products
158 in
159 if List.isEmpty cart then
160 text "No items in cart"
161
162 else
163 ul [] cart
164 ]
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 @@
1module Catalog exposing (..)
2
3import Browser
4import Browser.Navigation as Nav
5import Html exposing (..)
6import Html.Attributes exposing (..)
7import Html.Events exposing (..)
8import Http
9import Json.Decode as D
10import Json.Encode as Encode
11import Url
12import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string)
13
14
15type alias Product =
16 { id : Int
17 , name : String
18 , kind : Maybe String
19 , price : Float
20 , description : Maybe String
21 }
22
23
24type alias Model =
25 { pageStatus : Status
26 , products : List Product
27 }
28
29
30type Status
31 = Loading
32 | Loaded
33 | NotLoaded
34
35
36type Msg
37 = ProductsLoaded (Result Http.Error (List Product))
38 | FetchProducts
39
40
41init : Model
42init =
43 Model NotLoaded []
44
45
46update : Msg -> Model -> ( Model, Cmd Msg )
47update msg model =
48 case msg of
49 ProductsLoaded res ->
50 case res of
51 Ok s ->
52 ( { model | products = s, pageStatus = Loaded }, Cmd.none )
53
54 Err e ->
55 let
56 _ =
57 Debug.log "error" e
58 in
59 ( { model | pageStatus = NotLoaded }, Cmd.none )
60
61 FetchProducts ->
62 ( { model | pageStatus = Loading }, fetchProducts )
63
64
65decodeProduct : D.Decoder Product
66decodeProduct =
67 D.map5 Product
68 (D.field "id" D.int)
69 (D.field "name" D.string)
70 (D.field "kind" (D.nullable D.string))
71 (D.field "price" D.float)
72 (D.field "description" (D.nullable D.string))
73
74
75decodeResponse : D.Decoder (List Product)
76decodeResponse =
77 D.list decodeProduct
78
79
80fetchProducts : Cmd Msg
81fetchProducts =
82 let
83 _ =
84 Debug.log "err" "fetching products"
85 in
86 Http.get
87 { url = "http://127.0.0.1:7878/product/catalog"
88 , expect = Http.expectJson ProductsLoaded decodeResponse
89 }
90
91
92viewStatus : Status -> String
93viewStatus s =
94 case s of
95 Loading ->
96 "Loading"
97
98 Loaded ->
99 "Ready!"
100
101 NotLoaded ->
102 "Not loaded ..."
103
104
105viewProduct : Product -> Html Msg
106viewProduct p =
107 div []
108 [ text p.name
109 , text <| Maybe.withDefault "" p.kind
110 , text <| Maybe.withDefault "" p.description
111 , text <| String.fromFloat p.price
112 , a [ href ("/product/" ++ String.fromInt p.id) ] [ text "View Product" ]
113 ]
114
115
116view : Model -> Html Msg
117view model =
118 case model.pageStatus of
119 Loading ->
120 div [] [ text <| viewStatus Loading ]
121
122 _ ->
123 div []
124 [ ul [] (List.map viewProduct model.products)
125 ]
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 @@
1module Login exposing (..)
2
3import Browser
4import Browser.Navigation as Nav
5import Html exposing (..)
6import Html.Attributes exposing (..)
7import Html.Events exposing (..)
8import Http
9import Json.Encode as Encode
10import Url
11import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string)
12
13
14type alias Model =
15 { username : String
16 , password : String
17 , loginStatus : LoginStatus
18 }
19
20
21type LoginStatus
22 = NotLoggedIn
23 | LoggedIn
24 | InvalidLogin
25 | LoggingIn
26
27
28type Msg
29 = PassEntered String
30 | UserEntered String
31 | LoginPressed
32 | LoginSuccess (Result Http.Error ())
33 | LoginFail
34
35
36init : Model
37init =
38 Model "" "" NotLoggedIn
39
40
41update : Msg -> Model -> ( Model, Cmd Msg )
42update msg model =
43 case msg of
44 PassEntered s ->
45 ( { model | password = s }
46 , Cmd.none
47 )
48
49 UserEntered s ->
50 ( { model | username = s }
51 , Cmd.none
52 )
53
54 LoginPressed ->
55 ( { model | loginStatus = LoggingIn }, tryLogin model )
56
57 LoginSuccess res ->
58 case res of
59 Ok s ->
60 ( { model | loginStatus = LoggedIn }, Cmd.none )
61
62 Err e ->
63 ( { model | loginStatus = InvalidLogin }, Cmd.none )
64
65 LoginFail ->
66 ( { model | loginStatus = InvalidLogin }, Cmd.none )
67
68
69encodeLogin : Model -> Encode.Value
70encodeLogin model =
71 Encode.object
72 [ ( "username", Encode.string model.username )
73 , ( "password", Encode.string model.password )
74 ]
75
76
77tryLogin : Model -> Cmd Msg
78tryLogin model =
79 Http.riskyRequest
80 { method = "POST"
81 , headers = []
82 , url = "http://127.0.0.1:7878/user/login"
83 , body = model |> encodeLogin |> Http.jsonBody
84 , expect = Http.expectWhatever LoginSuccess
85 , timeout = Nothing
86 , tracker = Nothing
87 }
88
89
90viewStatus : LoginStatus -> String
91viewStatus ls =
92 case ls of
93 NotLoggedIn ->
94 "Not Logged In"
95
96 InvalidLogin ->
97 "Invalid Login"
98
99 LoggedIn ->
100 "Logged in!"
101
102 LoggingIn ->
103 "Logging In ..."
104
105
106viewInput : String -> String -> String -> (String -> msg) -> Html msg
107viewInput t p v toMsg =
108 input [ type_ t, placeholder p, value v, onInput toMsg ] []
109
110
111view : Model -> Html Msg
112view model =
113 div []
114 [ div [] [ viewInput "text" "Enter name here" model.username UserEntered ]
115 , div [] [ viewInput "password" "Password" model.password PassEntered ]
116 , div [] [ button [ onClick LoginPressed ] [ text "Login" ] ]
117 , div [] [ text (viewStatus model.loginStatus) ]
118 , div [] [ text "Don't have an account? ", a [ href "/signup" ] [ text "Register now!" ] ]
119 ]
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 @@
1module Main exposing (Model, Msg(..), init, main, subscriptions, update, view, viewLink)
2
3import Browser
4import Browser.Navigation as Nav
5import Cart
6import Catalog
7import Html exposing (..)
8import Html.Attributes exposing (..)
9import Html.Events exposing (..)
10import Http
11import Json.Encode as Encode
12import Login
13import Product
14import Signup
15import Url
16import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string)
17
18
19
20-- MAIN
21
22
23main : Program () Model Msg
24main =
25 Browser.application
26 { init = init
27 , view = view
28 , update = update
29 , subscriptions = subscriptions
30 , onUrlChange = UrlChanged
31 , onUrlRequest = LinkClicked
32 }
33
34
35
36-- MODEL
37
38
39type Route
40 = LoginPage
41 | SignupPage
42 | HomePage
43 | CatalogPage
44 | CartPage
45 | ProductPage Int
46 | NotFoundPage
47
48
49parseRoute : Parser (Route -> a) a
50parseRoute =
51 oneOf
52 [ P.map LoginPage (P.s "login")
53 , P.map HomePage P.top
54 , P.map CatalogPage (P.s "catalog")
55 , P.map CartPage (P.s "cart")
56 , P.map SignupPage (P.s "signup")
57 , P.map ProductPage (P.s "product" </> P.int)
58
59 --, P.map ProductPage (P.s "product" </> int)
60 ]
61
62
63type alias Model =
64 { key : Nav.Key
65 , url : Url.Url
66 , location : Route
67 , loginModel : Login.Model
68 , catalogModel : Catalog.Model
69 , productModel : Product.Model
70 , signupModel : Signup.Model
71 , cartModel : Cart.Model
72 }
73
74
75init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
76init flags url key =
77 let
78 start =
79 HomePage
80
81 login =
82 Login.init
83
84 catalog =
85 Catalog.init
86
87 product =
88 Product.init
89
90 signup =
91 Signup.init
92
93 cart =
94 Cart.init
95 in
96 ( Model key url start login catalog product signup cart, Cmd.none )
97
98
99
100-- UPDATE
101
102
103type Msg
104 = LinkClicked Browser.UrlRequest
105 | UrlChanged Url.Url
106 | LoginMessage Login.Msg
107 | CatalogMessage Catalog.Msg
108 | ProductMessage Product.Msg
109 | SignupMessage Signup.Msg
110 | CartMessage Cart.Msg
111 | LogoutPressed
112 | LogoutSuccess (Result Http.Error ())
113
114
115update : Msg -> Model -> ( Model, Cmd Msg )
116update msg model =
117 case msg of
118 LinkClicked urlRequest ->
119 case urlRequest of
120 Browser.Internal url ->
121 ( model, Nav.pushUrl model.key (Url.toString url) )
122
123 Browser.External href ->
124 ( model, Nav.load href )
125
126 LogoutPressed ->
127 ( model, tryLogout )
128
129 LogoutSuccess _ ->
130 ( model, Nav.replaceUrl model.key "/login" )
131
132 UrlChanged url ->
133 let
134 parsedUrl =
135 P.parse parseRoute url
136 in
137 case parsedUrl of
138 Just CatalogPage ->
139 ( { model | location = CatalogPage }, Cmd.map CatalogMessage Catalog.fetchProducts )
140
141 Just (ProductPage id) ->
142 let
143 cmds =
144 List.map (Cmd.map ProductMessage)
145 [ Product.fetchListing id
146 , Product.fetchRatings id
147 ]
148 in
149 ( { model | location = ProductPage id }, Cmd.batch cmds )
150
151 Just CartPage ->
152 let
153 cmd =
154 Cmd.map CartMessage Cart.fetchCartItems
155 in
156 ( { model | location = CartPage }, cmd )
157
158 Just p ->
159 ( { model | location = p }, Cmd.none )
160
161 Nothing ->
162 ( { model | location = NotFoundPage }, Cmd.none )
163
164 LoginMessage lm ->
165 let
166 ( lmn, cmd ) =
167 Login.update lm model.loginModel
168
169 redir =
170 case lmn.loginStatus of
171 Login.LoggedIn ->
172 Nav.replaceUrl model.key "/catalog"
173
174 _ ->
175 Cmd.none
176 in
177 ( { model | loginModel = lmn }, Cmd.batch [ Cmd.map LoginMessage cmd, redir ] )
178
179 SignupMessage sm ->
180 let
181 ( smn, cmd ) =
182 Signup.update sm model.signupModel
183
184 redir =
185 case smn.status of
186 Signup.CreatedSuccessfully ->
187 Nav.replaceUrl model.key "/login"
188
189 _ ->
190 Cmd.none
191 in
192 ( { model | signupModel = smn }, Cmd.batch [ Cmd.map SignupMessage cmd, redir ] )
193
194 CatalogMessage cm ->
195 let
196 ( cmn, cmd ) =
197 Catalog.update cm model.catalogModel
198 in
199 ( { model | catalogModel = cmn }, Cmd.map CatalogMessage cmd )
200
201 CartMessage cm ->
202 let
203 ( cmn, cmd ) =
204 Cart.update cm model.cartModel
205 in
206 ( { model | cartModel = cmn }, Cmd.map CartMessage cmd )
207
208 ProductMessage pm ->
209 let
210 ( pmn, cmd ) =
211 Product.update pm model.productModel
212
213 redir =
214 case pm of
215 Product.AddToCartSuccess _ ->
216 Nav.replaceUrl model.key "/cart"
217
218 _ ->
219 Cmd.none
220 in
221 ( { model | productModel = pmn }, Cmd.batch [ Cmd.map ProductMessage cmd, redir ] )
222
223
224tryLogout : Cmd Msg
225tryLogout =
226 Http.riskyRequest
227 { method = "POST"
228 , headers = []
229 , url = "http://127.0.0.1:7878/user/logout"
230 , body = Http.emptyBody
231 , expect = Http.expectWhatever LogoutSuccess
232 , timeout = Nothing
233 , tracker = Nothing
234 }
235
236
237
238-- SUBSCRIPTIONS
239
240
241subscriptions : Model -> Sub Msg
242subscriptions _ =
243 Sub.none
244
245
246
247-- VIEW
248
249
250view : Model -> Browser.Document Msg
251view model =
252 case model.location of
253 LoginPage ->
254 { title = "Login"
255 , body = [ Html.map LoginMessage (Login.view model.loginModel) ]
256 }
257
258 SignupPage ->
259 { title = "Signup"
260 , body = [ Html.map SignupMessage (Signup.view model.signupModel) ]
261 }
262
263 HomePage ->
264 { title = "URL Interceptor"
265 , body =
266 [ text "The current URL is: "
267 , b [] [ text (Url.toString model.url) ]
268 , ul []
269 [ viewLink "/login"
270 , viewLink "/catalog"
271 , viewLink "/cart"
272 , viewLink "/signup"
273 ]
274 ]
275 }
276
277 NotFoundPage ->
278 { title = "404 - Not Found"
279 , body =
280 [ text "404 - Not Found"
281 , a [ href "/" ] [ text "Go back >" ]
282 ]
283 }
284
285 CatalogPage ->
286 { title = "Catalog"
287 , body = pageWrap model (Html.map CatalogMessage (Catalog.view model.catalogModel))
288 }
289
290 CartPage ->
291 { title = "Cart"
292 , body = pageWrap model (Html.map CartMessage (Cart.view model.cartModel))
293 }
294
295 ProductPage item ->
296 { title = "Product " ++ String.fromInt item
297 , body = pageWrap model (Html.map ProductMessage (Product.view model.productModel))
298 }
299
300
301viewHeader : Model -> Html Msg
302viewHeader model =
303 let
304 links =
305 [ ( "Home", "/" )
306 , ( "Catalog", "/catalog" )
307 , ( "Cart", "/cart" )
308 ]
309 in
310 div []
311 [ List.map
312 (\( name, loc ) ->
313 li []
314 [ a [ href loc ] [ text name ]
315 ]
316 )
317 links
318 ++ [ if model.loginModel.loginStatus /= Login.LoggedIn then
319 li [] [ a [ href "/login" ] [ text "Login" ] ]
320
321 else
322 button [ onClick LogoutPressed ] [ text "Logout" ]
323 ]
324 |> ul []
325 ]
326
327
328pageWrap : Model -> Html Msg -> List (Html Msg)
329pageWrap model page =
330 [ div []
331 [ viewHeader model
332 , page
333 ]
334 ]
335
336
337viewLink : String -> Html msg
338viewLink path =
339 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 @@
1module Product exposing (..)
2
3import Browser
4import Browser.Navigation as Nav
5import Html exposing (..)
6import Html.Attributes exposing (..)
7import Html.Events exposing (..)
8import Http
9import Json.Decode as D
10import Json.Encode as Encode
11import Url
12import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string)
13
14
15type SubmitStatus
16 = SubmitSuccess
17 | SubmitFail
18 | Submitting
19 | NotSubmitted
20
21
22type alias Product =
23 { id : Int
24 , name : String
25 , kind : Maybe String
26 , price : Float
27 , description : Maybe String
28 }
29
30
31emptyProduct =
32 Product -1 "" Nothing 0 Nothing
33
34
35type alias Rating =
36 { commentDate : String
37 , commentText : Maybe String
38 , customerName : String
39 , productName : String
40 , stars : Int
41 }
42
43
44type alias Model =
45 { pageStatus : Status
46 , listing : Product
47 , ratings : List Rating
48 , ratingStars : Int
49 , ratingText : String
50 , addRatingStatus : SubmitStatus
51 }
52
53
54type Status
55 = Loading
56 | Loaded
57 | NotLoaded
58
59
60type Msg
61 = ListingLoaded (Result Http.Error Product)
62 | RatingsLoaded (Result Http.Error (List Rating))
63 | FetchProduct Int
64 | FetchRatings Int
65 | AddRatingStars Int
66 | AddRatingComment String
67 | AddRatingPressed
68 | AddRatingSuccess (Result Http.Error ())
69 | AddRatingFail
70 | AddToCartSuccess (Result Http.Error ())
71 | AddToCartPressed
72
73
74init : Model
75init =
76 Model NotLoaded emptyProduct [] 0 "" NotSubmitted
77
78
79update : Msg -> Model -> ( Model, Cmd Msg )
80update msg model =
81 case msg of
82 ListingLoaded res ->
83 case res of
84 Ok s ->
85 ( { model | listing = s, pageStatus = Loaded }, Cmd.none )
86
87 Err e ->
88 let
89 _ =
90 Debug.log "error" e
91 in
92 ( { model | pageStatus = NotLoaded }, Cmd.none )
93
94 RatingsLoaded res ->
95 case res of
96 Ok s ->
97 ( { model | ratings = s, pageStatus = Loaded }, Cmd.none )
98
99 Err e ->
100 let
101 _ =
102 Debug.log "error" e
103 in
104 ( { model | pageStatus = NotLoaded }, Cmd.none )
105
106 FetchProduct id ->
107 ( { model | pageStatus = Loading }, fetchListing id )
108
109 FetchRatings id ->
110 ( { model | pageStatus = Loading }, fetchRatings id )
111
112 AddRatingStars i ->
113 ( { model | ratingStars = i }, Cmd.none )
114
115 AddRatingComment s ->
116 ( { model | ratingText = s }, Cmd.none )
117
118 AddRatingPressed ->
119 ( { model | addRatingStatus = Submitting }
120 , submitRating model
121 )
122
123 AddRatingSuccess res ->
124 case res of
125 Ok _ ->
126 ( { model | addRatingStatus = SubmitSuccess }, fetchRatings model.listing.id )
127
128 Err _ ->
129 ( { model | addRatingStatus = SubmitFail }, Cmd.none )
130
131 AddRatingFail ->
132 ( { model | addRatingStatus = SubmitFail }, Cmd.none )
133
134 AddToCartPressed ->
135 ( model, addToCart model )
136
137 AddToCartSuccess _ ->
138 ( model, Cmd.none )
139
140
141decodeProduct : D.Decoder Product
142decodeProduct =
143 D.map5 Product
144 (D.field "id" D.int)
145 (D.field "name" D.string)
146 (D.field "kind" (D.nullable D.string))
147 (D.field "price" D.float)
148 (D.field "description" (D.nullable D.string))
149
150
151decodeRating : D.Decoder Rating
152decodeRating =
153 D.map5 Rating
154 (D.field "comment_date" D.string)
155 (D.field "comment_text" (D.nullable D.string))
156 (D.field "customer_name" D.string)
157 (D.field "product_name" D.string)
158 (D.field "stars" D.int)
159
160
161decodeRatings : D.Decoder (List Rating)
162decodeRatings =
163 D.list decodeRating
164
165
166fetchListing : Int -> Cmd Msg
167fetchListing id =
168 let
169 _ =
170 Debug.log "err" <| "fetching listing " ++ String.fromInt id
171 in
172 Http.get
173 { url = "http://127.0.0.1:7878/product/" ++ String.fromInt id
174 , expect = Http.expectJson ListingLoaded decodeProduct
175 }
176
177
178fetchRatings : Int -> Cmd Msg
179fetchRatings id =
180 let
181 _ =
182 Debug.log "err" <| "fetching ratings " ++ String.fromInt id
183 in
184 Http.get
185 { url = "http://127.0.0.1:7878/product/reviews/" ++ String.fromInt id
186 , expect = Http.expectJson RatingsLoaded decodeRatings
187 }
188
189
190encodeRatingForm : Model -> Encode.Value
191encodeRatingForm model =
192 Encode.object
193 [ ( "product_id", Encode.int model.listing.id )
194 , ( "stars", Encode.int model.ratingStars )
195 , ( "comment_text", Encode.string model.ratingText )
196 ]
197
198
199submitRating : Model -> Cmd Msg
200submitRating model =
201 let
202 _ =
203 Debug.log "err" <| "submitting rating for" ++ String.fromInt model.listing.id
204 in
205 Http.riskyRequest
206 { method = "POST"
207 , headers = []
208 , url = "http://127.0.0.1:7878/rating/add"
209 , body = model |> encodeRatingForm |> Http.jsonBody
210 , expect = Http.expectWhatever AddRatingSuccess
211 , timeout = Nothing
212 , tracker = Nothing
213 }
214
215
216addToCart : Model -> Cmd Msg
217addToCart model =
218 let
219 _ =
220 Debug.log "err" <| "adding to cart: " ++ String.fromInt model.listing.id
221 in
222 Http.riskyRequest
223 { method = "POST"
224 , headers = []
225 , url = "http://127.0.0.1:7878/cart/add"
226 , body = Http.stringBody "applcation/json" <| String.fromInt <| model.listing.id
227 , expect = Http.expectWhatever AddToCartSuccess
228 , timeout = Nothing
229 , tracker = Nothing
230 }
231
232
233viewStatus : Status -> String
234viewStatus s =
235 case s of
236 Loading ->
237 "Loading"
238
239 Loaded ->
240 "Ready!"
241
242 NotLoaded ->
243 "Not loaded ..."
244
245
246viewProduct : Product -> Html Msg
247viewProduct p =
248 div []
249 [ text p.name
250 , text <| Maybe.withDefault "" p.kind
251 , text <| Maybe.withDefault "" p.description
252 , text <| String.fromFloat p.price
253 ]
254
255
256viewRating : Rating -> Html Msg
257viewRating r =
258 div []
259 [ text <| r.customerName ++ " posted on "
260 , text <| r.commentDate ++ " "
261 , text <| Maybe.withDefault "" r.commentText
262 , text <| " Stars: " ++ String.fromInt r.stars
263 ]
264
265
266viewInput : String -> String -> String -> (String -> msg) -> Html msg
267viewInput t p v toMsg =
268 input [ type_ t, placeholder p, value v, onInput toMsg ] []
269
270
271viewStars : Html Msg
272viewStars =
273 ul []
274 (List.map
275 (\i -> button [ onClick (AddRatingStars i) ] [ text <| String.fromInt i ])
276 [ 0, 1, 2, 3, 4, 5 ]
277 )
278
279
280view : Model -> Html Msg
281view model =
282 case model.pageStatus of
283 Loading ->
284 div [] [ text <| viewStatus Loading ]
285
286 _ ->
287 div []
288 [ div [] [ viewProduct model.listing ]
289 , ul [] (List.map viewRating model.ratings)
290 , div [] [ text "Add Rating: " ]
291 , div []
292 [ viewStars
293 , viewInput "text" "Enter Comment Text" model.ratingText AddRatingComment
294 , button [ onClick AddRatingPressed ] [ text "Submit Rating" ]
295 ]
296 , div []
297 [ button [ onClick AddToCartPressed ] [ text "Add To Cart" ]
298 ]
299 , div []
300 [ a [ href "/catalog" ] [ text "Back to catalog" ]
301 ]
302 ]
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 @@
1module Signup exposing (..)
2
3import Browser
4import Browser.Navigation as Nav
5import Html exposing (..)
6import Html.Attributes exposing (..)
7import Html.Events exposing (..)
8import Http
9import Json.Encode as Encode
10import Url
11import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string)
12
13
14type alias Model =
15 { username : String
16 , password : String
17 , phoneNumber : String
18 , emailId : String
19 , address : Maybe String
20 , status : Status
21 }
22
23
24type Status
25 = UsernameTaken
26 | InvalidPhone
27 | InvalidEmail
28 | CreatedSuccessfully
29 | CreatingUser
30 | Empty
31
32
33type Msg
34 = UserEntered String
35 | PassEntered String
36 | PhoneEntered String
37 | EmailEntered String
38 | AddressEntered String
39 | CreatePressed
40 | CreationSuccess (Result Http.Error ())
41 | UsernameExists (Result Http.Error String)
42 | CreationFail
43
44
45init : Model
46init =
47 Model "" "" "" "" Nothing Empty
48
49
50update : Msg -> Model -> ( Model, Cmd Msg )
51update msg model =
52 case msg of
53 UserEntered s ->
54 ( { model | username = s }
55 , Cmd.none
56 )
57
58 PassEntered s ->
59 ( { model | password = s }
60 , Cmd.none
61 )
62
63 PhoneEntered s ->
64 let
65 status =
66 if String.length s /= 10 || (List.all (not << Char.isDigit) <| String.toList s) then
67 InvalidPhone
68
69 else
70 Empty
71 in
72 ( { model | phoneNumber = s, status = status }
73 , Cmd.none
74 )
75
76 EmailEntered s ->
77 let
78 status =
79 if not <| String.contains "@" s then
80 InvalidEmail
81
82 else
83 Empty
84 in
85 ( { model | emailId = s, status = status }
86 , Cmd.none
87 )
88
89 AddressEntered s ->
90 ( { model | address = Just s }
91 , Cmd.none
92 )
93
94 CreatePressed ->
95 ( { model | status = CreatingUser }, checkExists model )
96
97 CreationSuccess res ->
98 case res of
99 Ok _ ->
100 ( { model | status = CreatedSuccessfully }, Cmd.none )
101
102 Err _ ->
103 ( model, Cmd.none )
104
105 CreationFail ->
106 ( init, Cmd.none )
107
108 UsernameExists res ->
109 case res of
110 Ok "true" ->
111 ( { model | status = UsernameTaken }, Cmd.none )
112
113 Ok "false" ->
114 let
115 _ =
116 Debug.log "signup" "Hit create user ..."
117 in
118 ( { model | status = CreatingUser }, createUser model )
119
120 _ ->
121 ( model, Cmd.none )
122
123
124encodeCreateUser : Model -> Encode.Value
125encodeCreateUser model =
126 Encode.object
127 [ ( "username", Encode.string model.username )
128 , ( "password", Encode.string model.password )
129 , ( "phone_number", Encode.string model.phoneNumber )
130 , ( "email_id", Encode.string model.emailId )
131 , ( "address", Encode.string <| Maybe.withDefault "" model.address )
132 ]
133
134
135checkExists : Model -> Cmd Msg
136checkExists model =
137 Http.post
138 { url = "http://127.0.0.1:7878/user/existing"
139 , body = Http.stringBody "application/json" model.username
140 , expect = Http.expectString UsernameExists
141 }
142
143
144createUser : Model -> Cmd Msg
145createUser model =
146 Http.riskyRequest
147 { method = "POST"
148 , headers = []
149 , url = "http://127.0.0.1:7878/user/new"
150 , body = model |> encodeCreateUser |> Http.jsonBody
151 , expect = Http.expectWhatever CreationSuccess
152 , timeout = Nothing
153 , tracker = Nothing
154 }
155
156
157viewStatus : Status -> String
158viewStatus s =
159 case s of
160 UsernameTaken ->
161 "This username is taken!"
162
163 InvalidPhone ->
164 "Invalid phone number!"
165
166 InvalidEmail ->
167 "Invalid email address!"
168
169 CreatedSuccessfully ->
170 "User created successfully"
171
172 CreatingUser ->
173 "Creating user ..."
174
175 Empty ->
176 ""
177
178
179viewInput : String -> String -> String -> (String -> msg) -> Html msg
180viewInput t p v toMsg =
181 input [ type_ t, placeholder p, value v, onInput toMsg ] []
182
183
184view : Model -> Html Msg
185view model =
186 div []
187 [ viewInput "text" "Enter Username" model.username UserEntered
188 , viewInput "password" "Password" model.password PassEntered
189 , viewInput "text" "Email" model.emailId EmailEntered
190 , viewInput "text" "Enter your Phone number" model.phoneNumber PhoneEntered
191 , viewInput "text" "Enter Shipping address" (Maybe.withDefault "" model.address) AddressEntered
192 , button [ onClick CreatePressed ] [ text "Create" ]
193 , text (viewStatus model.status)
194 ]