diff options
Diffstat (limited to 'frontend/src')
-rw-r--r-- | frontend/src/Cart.elm | 164 | ||||
-rw-r--r-- | frontend/src/Catalog.elm | 125 | ||||
-rw-r--r-- | frontend/src/Login.elm | 119 | ||||
-rw-r--r-- | frontend/src/Main.elm | 339 | ||||
-rw-r--r-- | frontend/src/Product.elm | 302 | ||||
-rw-r--r-- | frontend/src/Signup.elm | 194 |
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 @@ | |||
1 | module Cart exposing (..) | ||
2 | |||
3 | import Browser | ||
4 | import Browser.Navigation as Nav | ||
5 | import Html exposing (..) | ||
6 | import Html.Attributes exposing (..) | ||
7 | import Html.Events exposing (..) | ||
8 | import Http | ||
9 | import Json.Decode as D | ||
10 | import Json.Encode as Encode | ||
11 | import Url | ||
12 | import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string) | ||
13 | |||
14 | |||
15 | type alias Product = | ||
16 | { id : Int | ||
17 | , name : String | ||
18 | , kind : Maybe String | ||
19 | , price : Float | ||
20 | , description : Maybe String | ||
21 | } | ||
22 | |||
23 | |||
24 | type alias Model = | ||
25 | { pageStatus : Status | ||
26 | , products : List Product | ||
27 | } | ||
28 | |||
29 | |||
30 | type Status | ||
31 | = Loading | ||
32 | | Loaded | ||
33 | | NotLoaded | ||
34 | |||
35 | |||
36 | type Msg | ||
37 | = CartLoaded (Result Http.Error (List Product)) | ||
38 | | FetchCartItems | ||
39 | | RemoveFromCart Int | ||
40 | | CartItemRemoved (Result Http.Error ()) | ||
41 | |||
42 | |||
43 | init : Model | ||
44 | init = | ||
45 | Model NotLoaded [] | ||
46 | |||
47 | |||
48 | update : Msg -> Model -> ( Model, Cmd Msg ) | ||
49 | update 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 | |||
73 | decodeProduct : D.Decoder Product | ||
74 | decodeProduct = | ||
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 | |||
83 | decodeResponse : D.Decoder (List Product) | ||
84 | decodeResponse = | ||
85 | D.list decodeProduct | ||
86 | |||
87 | |||
88 | removeProduct : Int -> Cmd Msg | ||
89 | removeProduct 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 | |||
105 | fetchCartItems : Cmd Msg | ||
106 | fetchCartItems = | ||
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 | |||
122 | viewStatus : Status -> String | ||
123 | viewStatus s = | ||
124 | case s of | ||
125 | Loading -> | ||
126 | "Loading" | ||
127 | |||
128 | Loaded -> | ||
129 | "Ready!" | ||
130 | |||
131 | NotLoaded -> | ||
132 | "Not loaded ..." | ||
133 | |||
134 | |||
135 | viewProduct : Product -> Html Msg | ||
136 | viewProduct 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 | |||
147 | view : Model -> Html Msg | ||
148 | view 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 @@ | |||
1 | module Catalog exposing (..) | ||
2 | |||
3 | import Browser | ||
4 | import Browser.Navigation as Nav | ||
5 | import Html exposing (..) | ||
6 | import Html.Attributes exposing (..) | ||
7 | import Html.Events exposing (..) | ||
8 | import Http | ||
9 | import Json.Decode as D | ||
10 | import Json.Encode as Encode | ||
11 | import Url | ||
12 | import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string) | ||
13 | |||
14 | |||
15 | type alias Product = | ||
16 | { id : Int | ||
17 | , name : String | ||
18 | , kind : Maybe String | ||
19 | , price : Float | ||
20 | , description : Maybe String | ||
21 | } | ||
22 | |||
23 | |||
24 | type alias Model = | ||
25 | { pageStatus : Status | ||
26 | , products : List Product | ||
27 | } | ||
28 | |||
29 | |||
30 | type Status | ||
31 | = Loading | ||
32 | | Loaded | ||
33 | | NotLoaded | ||
34 | |||
35 | |||
36 | type Msg | ||
37 | = ProductsLoaded (Result Http.Error (List Product)) | ||
38 | | FetchProducts | ||
39 | |||
40 | |||
41 | init : Model | ||
42 | init = | ||
43 | Model NotLoaded [] | ||
44 | |||
45 | |||
46 | update : Msg -> Model -> ( Model, Cmd Msg ) | ||
47 | update 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 | |||
65 | decodeProduct : D.Decoder Product | ||
66 | decodeProduct = | ||
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 | |||
75 | decodeResponse : D.Decoder (List Product) | ||
76 | decodeResponse = | ||
77 | D.list decodeProduct | ||
78 | |||
79 | |||
80 | fetchProducts : Cmd Msg | ||
81 | fetchProducts = | ||
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 | |||
92 | viewStatus : Status -> String | ||
93 | viewStatus s = | ||
94 | case s of | ||
95 | Loading -> | ||
96 | "Loading" | ||
97 | |||
98 | Loaded -> | ||
99 | "Ready!" | ||
100 | |||
101 | NotLoaded -> | ||
102 | "Not loaded ..." | ||
103 | |||
104 | |||
105 | viewProduct : Product -> Html Msg | ||
106 | viewProduct 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 | |||
116 | view : Model -> Html Msg | ||
117 | view 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 @@ | |||
1 | module Login exposing (..) | ||
2 | |||
3 | import Browser | ||
4 | import Browser.Navigation as Nav | ||
5 | import Html exposing (..) | ||
6 | import Html.Attributes exposing (..) | ||
7 | import Html.Events exposing (..) | ||
8 | import Http | ||
9 | import Json.Encode as Encode | ||
10 | import Url | ||
11 | import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string) | ||
12 | |||
13 | |||
14 | type alias Model = | ||
15 | { username : String | ||
16 | , password : String | ||
17 | , loginStatus : LoginStatus | ||
18 | } | ||
19 | |||
20 | |||
21 | type LoginStatus | ||
22 | = NotLoggedIn | ||
23 | | LoggedIn | ||
24 | | InvalidLogin | ||
25 | | LoggingIn | ||
26 | |||
27 | |||
28 | type Msg | ||
29 | = PassEntered String | ||
30 | | UserEntered String | ||
31 | | LoginPressed | ||
32 | | LoginSuccess (Result Http.Error ()) | ||
33 | | LoginFail | ||
34 | |||
35 | |||
36 | init : Model | ||
37 | init = | ||
38 | Model "" "" NotLoggedIn | ||
39 | |||
40 | |||
41 | update : Msg -> Model -> ( Model, Cmd Msg ) | ||
42 | update 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 | |||
69 | encodeLogin : Model -> Encode.Value | ||
70 | encodeLogin model = | ||
71 | Encode.object | ||
72 | [ ( "username", Encode.string model.username ) | ||
73 | , ( "password", Encode.string model.password ) | ||
74 | ] | ||
75 | |||
76 | |||
77 | tryLogin : Model -> Cmd Msg | ||
78 | tryLogin 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 | |||
90 | viewStatus : LoginStatus -> String | ||
91 | viewStatus 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 | |||
106 | viewInput : String -> String -> String -> (String -> msg) -> Html msg | ||
107 | viewInput t p v toMsg = | ||
108 | input [ type_ t, placeholder p, value v, onInput toMsg ] [] | ||
109 | |||
110 | |||
111 | view : Model -> Html Msg | ||
112 | view 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 @@ | |||
1 | module Main exposing (Model, Msg(..), init, main, subscriptions, update, view, viewLink) | ||
2 | |||
3 | import Browser | ||
4 | import Browser.Navigation as Nav | ||
5 | import Cart | ||
6 | import Catalog | ||
7 | import Html exposing (..) | ||
8 | import Html.Attributes exposing (..) | ||
9 | import Html.Events exposing (..) | ||
10 | import Http | ||
11 | import Json.Encode as Encode | ||
12 | import Login | ||
13 | import Product | ||
14 | import Signup | ||
15 | import Url | ||
16 | import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string) | ||
17 | |||
18 | |||
19 | |||
20 | -- MAIN | ||
21 | |||
22 | |||
23 | main : Program () Model Msg | ||
24 | main = | ||
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 | |||
39 | type Route | ||
40 | = LoginPage | ||
41 | | SignupPage | ||
42 | | HomePage | ||
43 | | CatalogPage | ||
44 | | CartPage | ||
45 | | ProductPage Int | ||
46 | | NotFoundPage | ||
47 | |||
48 | |||
49 | parseRoute : Parser (Route -> a) a | ||
50 | parseRoute = | ||
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 | |||
63 | type 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 | |||
75 | init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg ) | ||
76 | init 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 | |||
103 | type 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 | |||
115 | update : Msg -> Model -> ( Model, Cmd Msg ) | ||
116 | update 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 | |||
224 | tryLogout : Cmd Msg | ||
225 | tryLogout = | ||
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 | |||
241 | subscriptions : Model -> Sub Msg | ||
242 | subscriptions _ = | ||
243 | Sub.none | ||
244 | |||
245 | |||
246 | |||
247 | -- VIEW | ||
248 | |||
249 | |||
250 | view : Model -> Browser.Document Msg | ||
251 | view 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 | |||
301 | viewHeader : Model -> Html Msg | ||
302 | viewHeader 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 | |||
328 | pageWrap : Model -> Html Msg -> List (Html Msg) | ||
329 | pageWrap model page = | ||
330 | [ div [] | ||
331 | [ viewHeader model | ||
332 | , page | ||
333 | ] | ||
334 | ] | ||
335 | |||
336 | |||
337 | viewLink : String -> Html msg | ||
338 | viewLink 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 @@ | |||
1 | module Product exposing (..) | ||
2 | |||
3 | import Browser | ||
4 | import Browser.Navigation as Nav | ||
5 | import Html exposing (..) | ||
6 | import Html.Attributes exposing (..) | ||
7 | import Html.Events exposing (..) | ||
8 | import Http | ||
9 | import Json.Decode as D | ||
10 | import Json.Encode as Encode | ||
11 | import Url | ||
12 | import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string) | ||
13 | |||
14 | |||
15 | type SubmitStatus | ||
16 | = SubmitSuccess | ||
17 | | SubmitFail | ||
18 | | Submitting | ||
19 | | NotSubmitted | ||
20 | |||
21 | |||
22 | type alias Product = | ||
23 | { id : Int | ||
24 | , name : String | ||
25 | , kind : Maybe String | ||
26 | , price : Float | ||
27 | , description : Maybe String | ||
28 | } | ||
29 | |||
30 | |||
31 | emptyProduct = | ||
32 | Product -1 "" Nothing 0 Nothing | ||
33 | |||
34 | |||
35 | type alias Rating = | ||
36 | { commentDate : String | ||
37 | , commentText : Maybe String | ||
38 | , customerName : String | ||
39 | , productName : String | ||
40 | , stars : Int | ||
41 | } | ||
42 | |||
43 | |||
44 | type alias Model = | ||
45 | { pageStatus : Status | ||
46 | , listing : Product | ||
47 | , ratings : List Rating | ||
48 | , ratingStars : Int | ||
49 | , ratingText : String | ||
50 | , addRatingStatus : SubmitStatus | ||
51 | } | ||
52 | |||
53 | |||
54 | type Status | ||
55 | = Loading | ||
56 | | Loaded | ||
57 | | NotLoaded | ||
58 | |||
59 | |||
60 | type 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 | |||
74 | init : Model | ||
75 | init = | ||
76 | Model NotLoaded emptyProduct [] 0 "" NotSubmitted | ||
77 | |||
78 | |||
79 | update : Msg -> Model -> ( Model, Cmd Msg ) | ||
80 | update 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 | |||
141 | decodeProduct : D.Decoder Product | ||
142 | decodeProduct = | ||
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 | |||
151 | decodeRating : D.Decoder Rating | ||
152 | decodeRating = | ||
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 | |||
161 | decodeRatings : D.Decoder (List Rating) | ||
162 | decodeRatings = | ||
163 | D.list decodeRating | ||
164 | |||
165 | |||
166 | fetchListing : Int -> Cmd Msg | ||
167 | fetchListing 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 | |||
178 | fetchRatings : Int -> Cmd Msg | ||
179 | fetchRatings 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 | |||
190 | encodeRatingForm : Model -> Encode.Value | ||
191 | encodeRatingForm 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 | |||
199 | submitRating : Model -> Cmd Msg | ||
200 | submitRating 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 | |||
216 | addToCart : Model -> Cmd Msg | ||
217 | addToCart 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 | |||
233 | viewStatus : Status -> String | ||
234 | viewStatus s = | ||
235 | case s of | ||
236 | Loading -> | ||
237 | "Loading" | ||
238 | |||
239 | Loaded -> | ||
240 | "Ready!" | ||
241 | |||
242 | NotLoaded -> | ||
243 | "Not loaded ..." | ||
244 | |||
245 | |||
246 | viewProduct : Product -> Html Msg | ||
247 | viewProduct 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 | |||
256 | viewRating : Rating -> Html Msg | ||
257 | viewRating 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 | |||
266 | viewInput : String -> String -> String -> (String -> msg) -> Html msg | ||
267 | viewInput t p v toMsg = | ||
268 | input [ type_ t, placeholder p, value v, onInput toMsg ] [] | ||
269 | |||
270 | |||
271 | viewStars : Html Msg | ||
272 | viewStars = | ||
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 | |||
280 | view : Model -> Html Msg | ||
281 | view 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 @@ | |||
1 | module Signup exposing (..) | ||
2 | |||
3 | import Browser | ||
4 | import Browser.Navigation as Nav | ||
5 | import Html exposing (..) | ||
6 | import Html.Attributes exposing (..) | ||
7 | import Html.Events exposing (..) | ||
8 | import Http | ||
9 | import Json.Encode as Encode | ||
10 | import Url | ||
11 | import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string) | ||
12 | |||
13 | |||
14 | type alias Model = | ||
15 | { username : String | ||
16 | , password : String | ||
17 | , phoneNumber : String | ||
18 | , emailId : String | ||
19 | , address : Maybe String | ||
20 | , status : Status | ||
21 | } | ||
22 | |||
23 | |||
24 | type Status | ||
25 | = UsernameTaken | ||
26 | | InvalidPhone | ||
27 | | InvalidEmail | ||
28 | | CreatedSuccessfully | ||
29 | | CreatingUser | ||
30 | | Empty | ||
31 | |||
32 | |||
33 | type 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 | |||
45 | init : Model | ||
46 | init = | ||
47 | Model "" "" "" "" Nothing Empty | ||
48 | |||
49 | |||
50 | update : Msg -> Model -> ( Model, Cmd Msg ) | ||
51 | update 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 | |||
124 | encodeCreateUser : Model -> Encode.Value | ||
125 | encodeCreateUser 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 | |||
135 | checkExists : Model -> Cmd Msg | ||
136 | checkExists 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 | |||
144 | createUser : Model -> Cmd Msg | ||
145 | createUser 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 | |||
157 | viewStatus : Status -> String | ||
158 | viewStatus 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 | |||
179 | viewInput : String -> String -> String -> (String -> msg) -> Html msg | ||
180 | viewInput t p v toMsg = | ||
181 | input [ type_ t, placeholder p, value v, onInput toMsg ] [] | ||
182 | |||
183 | |||
184 | view : Model -> Html Msg | ||
185 | view 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 | ] | ||