diff options
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/Catalog.elm | 132 | ||||
| -rw-r--r-- | frontend/src/Utils.elm | 15 |
2 files changed, 139 insertions, 8 deletions
diff --git a/frontend/src/Catalog.elm b/frontend/src/Catalog.elm index 80e5e38..6882c73 100644 --- a/frontend/src/Catalog.elm +++ b/frontend/src/Catalog.elm | |||
| @@ -8,8 +8,15 @@ import Html.Events exposing (..) | |||
| 8 | import Http | 8 | import Http |
| 9 | import Json.Decode as D | 9 | import Json.Decode as D |
| 10 | import Json.Encode as Encode | 10 | import Json.Encode as Encode |
| 11 | import Tuple exposing (..) | ||
| 11 | import Url | 12 | import Url |
| 12 | import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string) | 13 | import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string) |
| 14 | import Utils exposing (..) | ||
| 15 | |||
| 16 | |||
| 17 | type Order | ||
| 18 | = Rating | ||
| 19 | | Price | ||
| 13 | 20 | ||
| 14 | 21 | ||
| 15 | type alias Product = | 22 | type alias Product = |
| @@ -18,12 +25,25 @@ type alias Product = | |||
| 18 | , kind : Maybe String | 25 | , kind : Maybe String |
| 19 | , price : Float | 26 | , price : Float |
| 20 | , description : Maybe String | 27 | , description : Maybe String |
| 28 | , averageRating : Maybe Float | ||
| 21 | } | 29 | } |
| 22 | 30 | ||
| 23 | 31 | ||
| 32 | type alias Filters = | ||
| 33 | { price : ( Float, Float ) | ||
| 34 | , rating : ( Float, Float ) | ||
| 35 | } | ||
| 36 | |||
| 37 | |||
| 38 | defaultFilters : Filters | ||
| 39 | defaultFilters = | ||
| 40 | Filters ( -1, 10000 ) ( 0, 5 ) | ||
| 41 | |||
| 42 | |||
| 24 | type alias Model = | 43 | type alias Model = |
| 25 | { pageStatus : Status | 44 | { pageStatus : Status |
| 26 | , products : List Product | 45 | , products : List Product |
| 46 | , filters : Filters | ||
| 27 | } | 47 | } |
| 28 | 48 | ||
| 29 | 49 | ||
| @@ -36,11 +56,15 @@ type Status | |||
| 36 | type Msg | 56 | type Msg |
| 37 | = ProductsLoaded (Result Http.Error (List Product)) | 57 | = ProductsLoaded (Result Http.Error (List Product)) |
| 38 | | FetchProducts | 58 | | FetchProducts |
| 59 | | ChangePriceLower Float | ||
| 60 | | ChangePriceUpper Float | ||
| 61 | | ChangeRatingLower Float | ||
| 62 | | ChangeRatingUpper Float | ||
| 39 | 63 | ||
| 40 | 64 | ||
| 41 | init : Model | 65 | init : Model |
| 42 | init = | 66 | init = |
| 43 | Model NotLoaded [] | 67 | Model NotLoaded [] defaultFilters |
| 44 | 68 | ||
| 45 | 69 | ||
| 46 | update : Msg -> Model -> ( Model, Cmd Msg ) | 70 | update : Msg -> Model -> ( Model, Cmd Msg ) |
| @@ -61,15 +85,56 @@ update msg model = | |||
| 61 | FetchProducts -> | 85 | FetchProducts -> |
| 62 | ( { model | pageStatus = Loading }, fetchProducts ) | 86 | ( { model | pageStatus = Loading }, fetchProducts ) |
| 63 | 87 | ||
| 88 | ChangePriceLower v -> | ||
| 89 | let | ||
| 90 | fs = | ||
| 91 | model.filters | ||
| 92 | |||
| 93 | nfs = | ||
| 94 | { fs | price = mapFirst (always v) fs.price } | ||
| 95 | in | ||
| 96 | ( { model | filters = nfs }, Cmd.none ) | ||
| 97 | |||
| 98 | ChangePriceUpper v -> | ||
| 99 | let | ||
| 100 | fs = | ||
| 101 | model.filters | ||
| 102 | |||
| 103 | nfs = | ||
| 104 | { fs | price = mapSecond (always v) fs.price } | ||
| 105 | in | ||
| 106 | ( { model | filters = nfs }, Cmd.none ) | ||
| 107 | |||
| 108 | ChangeRatingLower v -> | ||
| 109 | let | ||
| 110 | fs = | ||
| 111 | model.filters | ||
| 112 | |||
| 113 | nfs = | ||
| 114 | { fs | rating = mapFirst (always v) fs.rating } | ||
| 115 | in | ||
| 116 | ( { model | filters = nfs }, Cmd.none ) | ||
| 117 | |||
| 118 | ChangeRatingUpper v -> | ||
| 119 | let | ||
| 120 | fs = | ||
| 121 | model.filters | ||
| 122 | |||
| 123 | nfs = | ||
| 124 | { fs | rating = mapSecond (always v) fs.rating } | ||
| 125 | in | ||
| 126 | ( { model | filters = nfs }, Cmd.none ) | ||
| 127 | |||
| 64 | 128 | ||
| 65 | decodeProduct : D.Decoder Product | 129 | decodeProduct : D.Decoder Product |
| 66 | decodeProduct = | 130 | decodeProduct = |
| 67 | D.map5 Product | 131 | D.map6 Product |
| 68 | (D.field "id" D.int) | 132 | (D.field "id" D.int) |
| 69 | (D.field "name" D.string) | 133 | (D.field "name" D.string) |
| 70 | (D.field "kind" (D.nullable D.string)) | 134 | (D.field "kind" (D.nullable D.string)) |
| 71 | (D.field "price" D.float) | 135 | (D.field "price" D.float) |
| 72 | (D.field "description" (D.nullable D.string)) | 136 | (D.field "description" (D.nullable D.string)) |
| 137 | (D.field "average_rating" (D.nullable D.float)) | ||
| 73 | 138 | ||
| 74 | 139 | ||
| 75 | decodeResponse : D.Decoder (List Product) | 140 | decodeResponse : D.Decoder (List Product) |
| @@ -105,14 +170,63 @@ viewStatus s = | |||
| 105 | viewProduct : Product -> Html Msg | 170 | viewProduct : Product -> Html Msg |
| 106 | viewProduct p = | 171 | viewProduct p = |
| 107 | div [] | 172 | div [] |
| 108 | [ text p.name | 173 | [ div [] [ text p.name ] |
| 109 | , text <| Maybe.withDefault "" p.kind | 174 | , div [] [ text <| Maybe.withDefault "" p.kind ] |
| 110 | , text <| Maybe.withDefault "" p.description | 175 | , div [] [ text <| Maybe.withDefault "" p.description ] |
| 111 | , text <| String.fromFloat p.price | 176 | , div [] [ text <| String.fromFloat p.price ] |
| 112 | , a [ href ("/product/" ++ String.fromInt p.id) ] [ text "View Product" ] | 177 | , case p.averageRating of |
| 178 | Just v -> | ||
| 179 | text <| "Avg Rating: " ++ String.fromFloat v | ||
| 180 | |||
| 181 | Nothing -> | ||
| 182 | text "No Ratings" | ||
| 183 | , div [] [ a [ href ("/product/" ++ String.fromInt p.id) ] [ text "View Product" ] ] | ||
| 113 | ] | 184 | ] |
| 114 | 185 | ||
| 115 | 186 | ||
| 187 | viewFilters : Model -> Html Msg | ||
| 188 | viewFilters model = | ||
| 189 | let | ||
| 190 | priceRange = | ||
| 191 | range 0 50000 5000 | ||
| 192 | |||
| 193 | ratingRange = | ||
| 194 | List.range 1 5 | ||
| 195 | |||
| 196 | viewRange = | ||
| 197 | List.map (\i -> option [] [ text <| String.fromInt i ]) | ||
| 198 | |||
| 199 | inp = | ||
| 200 | Maybe.withDefault 0 << String.toFloat | ||
| 201 | in | ||
| 202 | div [] | ||
| 203 | [ div [] | ||
| 204 | [ text "Price" | ||
| 205 | , select [ onInput (ChangePriceLower << inp) ] (viewRange priceRange) | ||
| 206 | , text "to" | ||
| 207 | , select [ onInput (ChangePriceUpper << inp) ] (viewRange priceRange) | ||
| 208 | ] | ||
| 209 | , div [] | ||
| 210 | [ text "Rating" | ||
| 211 | , select [ onInput (ChangeRatingLower << inp) ] (viewRange ratingRange) | ||
| 212 | , text "to" | ||
| 213 | , select [ onInput (ChangeRatingUpper << inp) ] (viewRange ratingRange) | ||
| 214 | ] | ||
| 215 | ] | ||
| 216 | |||
| 217 | |||
| 218 | filterProducts : Model -> List Product | ||
| 219 | filterProducts model = | ||
| 220 | model.products | ||
| 221 | |> List.filter (between model.filters.price << .price) | ||
| 222 | |> List.filter | ||
| 223 | (\p -> | ||
| 224 | p.averageRating | ||
| 225 | |> Maybe.withDefault 5.0 | ||
| 226 | |> between model.filters.rating | ||
| 227 | ) | ||
| 228 | |||
| 229 | |||
| 116 | view : Model -> Html Msg | 230 | view : Model -> Html Msg |
| 117 | view model = | 231 | view model = |
| 118 | case model.pageStatus of | 232 | case model.pageStatus of |
| @@ -121,5 +235,7 @@ view model = | |||
| 121 | 235 | ||
| 122 | _ -> | 236 | _ -> |
| 123 | div [] | 237 | div [] |
| 124 | [ ul [] (List.map viewProduct model.products) | 238 | [ div [] [ viewFilters model ] |
| 239 | , ul [] | ||
| 240 | (filterProducts model |> List.map viewProduct) | ||
| 125 | ] | 241 | ] |
diff --git a/frontend/src/Utils.elm b/frontend/src/Utils.elm new file mode 100644 index 0000000..825e4b7 --- /dev/null +++ b/frontend/src/Utils.elm | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | module Utils exposing (..) | ||
| 2 | |||
| 3 | |||
| 4 | between : ( Float, Float ) -> Float -> Bool | ||
| 5 | between ( l, u ) v = | ||
| 6 | v >= l && v <= u | ||
| 7 | |||
| 8 | |||
| 9 | range : Int -> Int -> Int -> List Int | ||
| 10 | range start stop step = | ||
| 11 | if start >= stop then | ||
| 12 | [] | ||
| 13 | |||
| 14 | else | ||
| 15 | start :: range (start + step) stop step | ||
