From 5dcf50c64f124ffe4c9205c836354d5e5fb641f6 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 13 Aug 2018 22:01:57 -0400 Subject: [PATCH] Update advanced/part1 --- advanced/part1/src/Article/Feed.elm | 249 +++++---------------------- advanced/part1/src/Page/Home.elm | 170 ++++++++++++++++-- advanced/part1/src/Page/Profile.elm | 124 ++++++++++--- advanced/part1/src/PaginatedList.elm | 54 ++++-- 4 files changed, 339 insertions(+), 258 deletions(-) diff --git a/advanced/part1/src/Article/Feed.elm b/advanced/part1/src/Article/Feed.elm index a5a482a..e24c39a 100644 --- a/advanced/part1/src/Article/Feed.elm +++ b/advanced/part1/src/Article/Feed.elm @@ -2,11 +2,12 @@ module Article.Feed exposing ( Model , Msg + , decoder , init - , selectTag , update , viewArticles - , viewFeedSources + , viewPagination + , viewTabs ) import Api @@ -16,12 +17,11 @@ import Article.Slug as ArticleSlug exposing (Slug) import Article.Tag as Tag exposing (Tag) import Author import Avatar exposing (Avatar) -import Browser.Dom as Dom import Html exposing (..) import Html.Attributes exposing (attribute, class, classList, href, id, placeholder, src) import Html.Events exposing (onClick) import Http -import HttpBuilder exposing (RequestBuilder, withExpect, withQueryParams) +import HttpBuilder exposing (RequestBuilder) import Json.Decode as Decode exposing (Decoder) import Json.Decode.Pipeline exposing (required) import Page @@ -32,6 +32,7 @@ import Session exposing (Session) import Task exposing (Task) import Time import Timestamp +import Url exposing (Url) import Username exposing (Username) import Viewer exposing (Viewer) import Viewer.Cred as Cred exposing (Cred) @@ -59,37 +60,29 @@ overkill, so we use simpler APIs instead. type Model - = Model InternalModel + = Model Internals {-| This should not be exposed! We want to benefit from the guarantee that only this module can create or alter this model. This way if it ever ends up in a surprising state, we know exactly where to look: this module. -} -type alias InternalModel = +type alias Internals = { session : Session , errors : List String , articles : PaginatedList (Article Preview) - , sources : FeedSources , isLoading : Bool } -init : Session -> FeedSources -> Task Http.Error Model -init session sources = - let - fromArticles articles = - Model - { session = session - , errors = [] - , articles = articles - , sources = sources - , isLoading = False - } - in - FeedSources.selected sources - |> fetch (Session.cred session) 1 - |> Task.map fromArticles +init : Session -> PaginatedList (Article Preview) -> Model +init session articles = + Model + { session = session + , errors = [] + , articles = articles + , isLoading = False + } @@ -97,7 +90,7 @@ init session sources = viewArticles : Time.Zone -> Model -> List (Html Msg) -viewArticles timeZone (Model { articles, sources, session }) = +viewArticles timeZone (Model { articles, session, errors }) = let maybeCred = Session.cred session @@ -105,14 +98,8 @@ viewArticles timeZone (Model { articles, sources, session }) = articlesHtml = PaginatedList.values articles |> List.map (viewPreview maybeCred timeZone) - - feedSource = - FeedSources.selected sources - - pagination = - PaginatedList.view ClickedFeedPage articles (limit feedSource) in - List.append articlesHtml [ pagination ] + Page.viewErrors ClickedDismissErrors errors :: articlesHtml viewPreview : Maybe Cred -> Time.Zone -> Article Preview -> Html Msg @@ -167,85 +154,43 @@ viewPreview maybeCred timeZone article = [ h1 [] [ text title ] , p [] [ text description ] , span [] [ text "Read more..." ] + , ul [ class "tag-list" ] + (List.map viewTag (Article.metadata article).tags) ] ] -viewFeedSources : Model -> Html Msg -viewFeedSources (Model { sources, isLoading, errors }) = - let - errorsHtml = - Page.viewErrors ClickedDismissErrors errors - in +viewTabs : + List ( String, msg ) + -> ( String, msg ) + -> List ( String, msg ) + -> Html msg +viewTabs before selected after = ul [ class "nav nav-pills outline-active" ] <| List.concat - [ List.map (viewFeedSource False) (FeedSources.before sources) - , [ viewFeedSource True (FeedSources.selected sources) ] - , List.map (viewFeedSource False) (FeedSources.after sources) - , [ errorsHtml ] + [ List.map (viewTab []) before + , [ viewTab [ class "active" ] selected ] + , List.map (viewTab []) after ] -viewFeedSource : Bool -> Source -> Html Msg -viewFeedSource isSelected source = +viewTab : List (Attribute msg) -> ( String, msg ) -> Html msg +viewTab attrs ( name, msg ) = li [ class "nav-item" ] - [ a - [ classList [ ( "nav-link", True ), ( "active", isSelected ) ] - , onClick (ClickedFeedSource source) - - -- The RealWorld CSS requires an href to work properly. - , href "" - ] - [ text (sourceName source) ] + [ -- Note: The RealWorld CSS requires an href to work properly. + a (class "nav-link" :: onClick msg :: href "" :: attrs) + [ text name ] ] -selectTag : Maybe Cred -> Tag -> Cmd Msg -selectTag maybeCred tag = - let - source = - TagFeed tag - in - fetch maybeCred 1 source - |> Task.attempt (CompletedFeedLoad source) +viewPagination : (Int -> msg) -> Model -> Html msg +viewPagination toMsg (Model feed) = + PaginatedList.view toMsg feed.articles -sourceName : Source -> String -sourceName source = - case source of - YourFeed _ -> - "Your Feed" - - GlobalFeed -> - "Global Feed" - - TagFeed tagName -> - "#" ++ Tag.toString tagName - - FavoritedFeed username -> - "Favorited Articles" - - AuthorFeed username -> - "My Articles" - - -limit : Source -> Int -limit feedSource = - case feedSource of - YourFeed _ -> - 10 - - GlobalFeed -> - 10 - - TagFeed tagName -> - 10 - - FavoritedFeed username -> - 5 - - AuthorFeed username -> - 5 +viewTag : String -> Html msg +viewTag tagName = + li [ class "tag-default tag-pill tag-outline" ] [ text tagName ] @@ -256,10 +201,7 @@ type Msg = ClickedDismissErrors | ClickedFavorite Cred Slug | ClickedUnfavorite Cred Slug - | ClickedFeedPage Int - | ClickedFeedSource Source | CompletedFavorite (Result Http.Error (Article Preview)) - | CompletedFeedLoad Source (Result Http.Error (PaginatedList (Article Preview))) update : Maybe Cred -> Msg -> Model -> ( Model, Cmd Msg ) @@ -268,32 +210,6 @@ update maybeCred msg (Model model) = ClickedDismissErrors -> ( Model { model | errors = [] }, Cmd.none ) - ClickedFeedSource source -> - ( Model { model | isLoading = True } - , source - |> fetch maybeCred 1 - |> Task.attempt (CompletedFeedLoad source) - ) - - CompletedFeedLoad source (Ok articles) -> - ( Model - { model - | articles = articles - , sources = FeedSources.select source model.sources - , isLoading = False - } - , Cmd.none - ) - - CompletedFeedLoad _ (Err error) -> - ( Model - { model - | errors = Api.addServerError model.errors - , isLoading = False - } - , Cmd.none - ) - ClickedFavorite cred slug -> fave Article.favorite cred slug model @@ -310,72 +226,6 @@ update maybeCred msg (Model model) = , Cmd.none ) - ClickedFeedPage page -> - let - source = - FeedSources.selected model.sources - in - ( Model model - , fetch maybeCred page source - |> Task.andThen (\articles -> Task.map (\_ -> articles) scrollToTop) - |> Task.attempt (CompletedFeedLoad source) - ) - - -scrollToTop : Task x () -scrollToTop = - Dom.setViewport 0 0 - -- It's not worth showing the user anything special if scrolling fails. - -- If anything, we'd log this to an error recording service. - |> Task.onError (\_ -> Task.succeed ()) - - -fetch : Maybe Cred -> Int -> Source -> Task Http.Error (PaginatedList (Article Preview)) -fetch maybeCred page feedSource = - let - articlesPerPage = - limit feedSource - - offset = - (page - 1) * articlesPerPage - - params = - [ ( "limit", String.fromInt articlesPerPage ) - , ( "offset", String.fromInt offset ) - ] - in - Task.map (PaginatedList.mapPage (\_ -> page)) <| - case feedSource of - YourFeed cred -> - params - |> buildFromQueryParams (Just cred) (Api.url [ "articles", "feed" ]) - |> Cred.addHeader cred - |> HttpBuilder.toRequest - |> Http.toTask - - GlobalFeed -> - list maybeCred params - - TagFeed tagName -> - list maybeCred (( "tag", Tag.toString tagName ) :: params) - - FavoritedFeed username -> - list maybeCred (( "favorited", Username.toString username ) :: params) - - AuthorFeed username -> - list maybeCred (( "author", Username.toString username ) :: params) - - -list : - Maybe Cred - -> List ( String, String ) - -> Task Http.Error (PaginatedList (Article Preview)) -list maybeCred params = - buildFromQueryParams maybeCred (Api.url [ "articles" ]) params - |> Cred.addHeaderIfAvailable maybeCred - |> HttpBuilder.toRequest - |> Http.toTask - replaceArticle : Article a -> Article a -> Article a replaceArticle newArticle oldArticle = @@ -390,29 +240,24 @@ replaceArticle newArticle oldArticle = -- SERIALIZATION -decoder : Maybe Cred -> Decoder (PaginatedList (Article Preview)) -decoder maybeCred = +decoder : Maybe Cred -> Int -> Decoder (PaginatedList (Article Preview)) +decoder maybeCred resultsPerPage = Decode.succeed PaginatedList.fromList - |> required "articlesCount" Decode.int + |> required "articlesCount" (pageCountDecoder resultsPerPage) |> required "articles" (Decode.list (Article.previewDecoder maybeCred)) - --- REQUEST - - -buildFromQueryParams : Maybe Cred -> String -> List ( String, String ) -> RequestBuilder (PaginatedList (Article Preview)) -buildFromQueryParams maybeCred url queryParams = - HttpBuilder.get url - |> withExpect (Http.expectJson (decoder maybeCred)) - |> withQueryParams queryParams +pageCountDecoder : Int -> Decoder Int +pageCountDecoder resultsPerPage = + Decode.int + |> Decode.map (\total -> ceiling (toFloat total / toFloat resultsPerPage)) -- INTERNAL -fave : (Slug -> Cred -> Http.Request (Article Preview)) -> Cred -> Slug -> InternalModel -> ( Model, Cmd Msg ) +fave : (Slug -> Cred -> Http.Request (Article Preview)) -> Cred -> Slug -> Internals -> ( Model, Cmd Msg ) fave toRequest cred slug model = ( Model model , toRequest slug cred diff --git a/advanced/part1/src/Page/Home.elm b/advanced/part1/src/Page/Home.elm index a4dea6d..a1e6569 100644 --- a/advanced/part1/src/Page/Home.elm +++ b/advanced/part1/src/Page/Home.elm @@ -3,20 +3,25 @@ module Page.Home exposing (Model, Msg, init, subscriptions, toSession, update, v {-| The homepage. You can get here via either the / or /#/ routes. -} -import Article +import Api +import Article exposing (Article, Preview) import Article.Feed as Feed import Article.FeedSources as FeedSources exposing (FeedSources, Source(..)) import Article.Tag as Tag exposing (Tag) +import Browser.Dom as Dom import Html exposing (..) import Html.Attributes exposing (attribute, class, classList, href, id, placeholder) import Html.Events exposing (onClick) import Http +import HttpBuilder import Loading import Log import Page +import PaginatedList exposing (PaginatedList) import Session exposing (Session) import Task exposing (Task) import Time +import Username exposing (Username) import Viewer.Cred as Cred exposing (Cred) @@ -27,6 +32,8 @@ import Viewer.Cred as Cred exposing (Cred) type alias Model = { session : Session , timeZone : Time.Zone + , feedTab : FeedTab + , feedPage : Int -- Loaded independently from server , tags : Status (List Tag) @@ -41,28 +48,35 @@ type Status a | Failed +type FeedTab + = YourFeed Cred + | GlobalFeed + | TagFeed Tag + + init : Session -> ( Model, Cmd Msg ) init session = let - feedSources = + feedTab = case Session.cred session of Just cred -> - FeedSources.fromLists (YourFeed cred) [ GlobalFeed ] + YourFeed cred Nothing -> - FeedSources.fromLists GlobalFeed [] + GlobalFeed loadTags = - Tag.list - |> Http.toTask + Http.toTask Tag.list in ( { session = session , timeZone = Time.utc + , feedTab = feedTab + , feedPage = 1 , tags = Loading , feed = Loading } , Cmd.batch - [ Feed.init session feedSources + [ fetchFeed session feedTab 1 |> Task.attempt CompletedFeedLoad , Tag.list |> Http.send CompletedTagsLoad @@ -87,7 +101,17 @@ view model = [ div [ class "col-md-9" ] <| case model.feed of Loaded feed -> - viewFeed model.timeZone feed + [ div [ class "feed-toggle" ] <| + List.concat + [ [ viewTabs + (Session.cred model.session) + model.feedTab + ] + , Feed.viewArticles model.timeZone feed + |> List.map (Html.map GotFeedMsg) + , [ Feed.viewPagination ClickedFeedPage feed ] + ] + ] Loading -> [] @@ -130,11 +154,58 @@ viewBanner = ] -viewFeed : Time.Zone -> Feed.Model -> List (Html Msg) -viewFeed timeZone feed = - div [ class "feed-toggle" ] - [ Feed.viewFeedSources feed |> Html.map GotFeedMsg ] - :: (Feed.viewArticles timeZone feed |> List.map (Html.map GotFeedMsg)) + +-- TABS + + +viewTabs : Maybe Cred -> FeedTab -> Html Msg +viewTabs maybeCred tab = + case tab of + YourFeed cred -> + Feed.viewTabs [] (yourFeed cred) [ globalFeed ] + + GlobalFeed -> + let + otherTabs = + case maybeCred of + Just cred -> + [ yourFeed cred ] + + Nothing -> + [] + in + Feed.viewTabs otherTabs globalFeed [] + + TagFeed tag -> + let + otherTabs = + case maybeCred of + Just cred -> + [ yourFeed cred, globalFeed ] + + Nothing -> + [ globalFeed ] + in + Feed.viewTabs otherTabs (tagFeed tag) [] + + +yourFeed : Cred -> ( String, Msg ) +yourFeed cred = + ( "Your Feed", ClickedTab (YourFeed cred) ) + + +globalFeed : ( String, Msg ) +globalFeed = + ( "Global Feed", ClickedTab GlobalFeed ) + + +tagFeed : Tag -> ( String, Msg ) +tagFeed tag = + ( "#" ++ Tag.toString tag, ClickedTab (TagFeed tag) ) + + + +-- TAGS viewTags : List Tag -> Html Msg @@ -160,6 +231,8 @@ viewTag tagName = type Msg = ClickedTag Tag + | ClickedTab FeedTab + | ClickedFeedPage Int | CompletedFeedLoad (Result Http.Error Feed.Model) | CompletedTagsLoad (Result Http.Error (List Tag)) | GotTimeZone Time.Zone @@ -171,12 +244,28 @@ type Msg update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of - ClickedTag tagName -> + ClickedTag tag -> let - subCmd = - Feed.selectTag (Session.cred model.session) tagName + feedTab = + TagFeed tag in - ( model, Cmd.map GotFeedMsg subCmd ) + ( { model | feedTab = feedTab } + , fetchFeed model.session feedTab 1 + |> Task.attempt CompletedFeedLoad + ) + + ClickedTab tab -> + ( { model | feedTab = tab } + , fetchFeed model.session tab 1 + |> Task.attempt CompletedFeedLoad + ) + + ClickedFeedPage page -> + ( { model | feedPage = page } + , fetchFeed model.session model.feedTab page + |> Task.andThen (\feed -> Task.map (\_ -> feed) scrollToTop) + |> Task.attempt CompletedFeedLoad + ) CompletedFeedLoad (Ok feed) -> ( { model | feed = Loaded feed }, Cmd.none ) @@ -242,6 +331,53 @@ update msg model = +-- HTTP + + +fetchFeed : Session -> FeedTab -> Int -> Task Http.Error Feed.Model +fetchFeed session feedTabs page = + let + maybeCred = + Session.cred session + + builder = + case feedTabs of + YourFeed cred -> + Api.url [ "articles", "feed" ] + |> HttpBuilder.get + |> Cred.addHeader cred + + GlobalFeed -> + Api.url [ "articles" ] + |> HttpBuilder.get + |> Cred.addHeaderIfAvailable maybeCred + + TagFeed tag -> + Api.url [ "articles" ] + |> HttpBuilder.get + |> Cred.addHeaderIfAvailable maybeCred + |> HttpBuilder.withQueryParam "tag" (Tag.toString tag) + in + builder + |> HttpBuilder.withExpect (Http.expectJson (Feed.decoder maybeCred articlesPerPage)) + |> PaginatedList.fromRequestBuilder articlesPerPage page + |> Task.map (Feed.init session) + + +articlesPerPage : Int +articlesPerPage = + 10 + + +scrollToTop : Task x () +scrollToTop = + Dom.setViewport 0 0 + -- It's not worth showing the user anything special if scrolling fails. + -- If anything, we'd log this to an error recording service. + |> Task.onError (\_ -> Task.succeed ()) + + + -- SUBSCRIPTIONS diff --git a/advanced/part1/src/Page/Profile.elm b/advanced/part1/src/Page/Profile.elm index 49735ae..30d0b46 100644 --- a/advanced/part1/src/Page/Profile.elm +++ b/advanced/part1/src/Page/Profile.elm @@ -3,6 +3,8 @@ module Page.Profile exposing (Model, Msg, init, subscriptions, toSession, update {-| An Author's profile. -} +import Api +import Article exposing (Article, Preview) import Article.Feed as Feed import Article.FeedSources as FeedSources exposing (FeedSources, Source(..)) import Author exposing (Author(..), FollowedAuthor, UnfollowedAuthor) @@ -10,9 +12,11 @@ import Avatar exposing (Avatar) import Html exposing (..) import Html.Attributes exposing (..) import Http +import HttpBuilder exposing (RequestBuilder) import Loading import Log import Page +import PaginatedList exposing (PaginatedList) import Profile exposing (Profile) import Route import Session exposing (Session) @@ -31,6 +35,8 @@ type alias Model = { session : Session , timeZone : Time.Zone , errors : List String + , feedTab : FeedTab + , feedPage : Int -- Loaded independently from server , author : Status Author @@ -38,6 +44,11 @@ type alias Model = } +type FeedTab + = MyArticles + | FavoritedArticles + + type Status a = Loading Username | LoadingSlowly Username @@ -54,6 +65,8 @@ init session username = ( { session = session , timeZone = Time.utc , errors = [] + , feedTab = defaultFeedTab + , feedPage = 1 , author = Loading username , feed = Loading username } @@ -62,16 +75,68 @@ init session username = |> Http.toTask |> Task.mapError (Tuple.pair username) |> Task.attempt CompletedAuthorLoad - , defaultFeedSources username - |> Feed.init session - |> Task.mapError (Tuple.pair username) - |> Task.attempt CompletedFeedLoad + , fetchFeed session defaultFeedTab username 1 , Task.perform GotTimeZone Time.here , Task.perform (\_ -> PassedSlowLoadThreshold) Loading.slowThreshold ] ) +currentUsername : Model -> Username +currentUsername model = + case model.author of + Loading username -> + username + + LoadingSlowly username -> + username + + Loaded author -> + Author.username author + + Failed username -> + username + + +defaultFeedTab : FeedTab +defaultFeedTab = + MyArticles + + + +-- HTTP + + +fetchFeed : Session -> FeedTab -> Username -> Int -> Cmd Msg +fetchFeed session feedTabs username page = + let + maybeCred = + Session.cred session + + ( extraParamName, extraParamVal ) = + case feedTabs of + MyArticles -> + ( "author", Username.toString username ) + + FavoritedArticles -> + ( "favorited", Username.toString username ) + in + Api.url [ "articles" ] + |> HttpBuilder.get + |> HttpBuilder.withExpect (Http.expectJson (Feed.decoder maybeCred articlesPerPage)) + |> HttpBuilder.withQueryParam extraParamName extraParamVal + |> Cred.addHeaderIfAvailable maybeCred + |> PaginatedList.fromRequestBuilder articlesPerPage page + |> Task.map (Feed.init session) + |> Task.mapError (Tuple.pair username) + |> Task.attempt CompletedFeedLoad + + +articlesPerPage : Int +articlesPerPage = + 5 + + -- VIEW @@ -145,7 +210,13 @@ view model = , case model.feed of Loaded feed -> div [ class "container" ] - [ div [ class "row" ] [ viewFeed model.timeZone feed ] ] + [ div [ class "row" ] + [ div [ class "col-xs-12 col-md-10 offset-md-1" ] <| + div [ class "articles-toggle" ] + [ viewTabs model.feedTab ] + :: (Feed.viewArticles model.timeZone feed |> List.map (Html.map GotFeedMsg)) + ] + ] Loading _ -> text "" @@ -181,7 +252,7 @@ titleForMe : Maybe Cred -> Username -> String titleForMe maybeCred username = case maybeCred of Just cred -> - if username == cred.username then + if username == Cred.username cred then myProfileTitle else @@ -202,15 +273,27 @@ defaultTitle = --- FEED +-- TABS -viewFeed : Time.Zone -> Feed.Model -> Html Msg -viewFeed timeZone feed = - div [ class "col-xs-12 col-md-10 offset-md-1" ] <| - div [ class "articles-toggle" ] - [ Feed.viewFeedSources feed |> Html.map GotFeedMsg ] - :: (Feed.viewArticles timeZone feed |> List.map (Html.map GotFeedMsg)) +viewTabs : FeedTab -> Html Msg +viewTabs tab = + case tab of + MyArticles -> + Feed.viewTabs [] myArticles [ favoritedArticles ] + + FavoritedArticles -> + Feed.viewTabs [ myArticles ] favoritedArticles [] + + +myArticles : ( String, Msg ) +myArticles = + ( "My Articles", ClickedTab MyArticles ) + + +favoritedArticles : ( String, Msg ) +favoritedArticles = + ( "Favorited Articles", ClickedTab FavoritedArticles ) @@ -221,6 +304,7 @@ type Msg = ClickedDismissErrors | ClickedFollow Cred UnfollowedAuthor | ClickedUnfollow Cred FollowedAuthor + | ClickedTab FeedTab | CompletedFollowChange (Result Http.Error Author) | CompletedAuthorLoad (Result ( Username, Http.Error ) Author) | CompletedFeedLoad (Result ( Username, Http.Error ) Feed.Model) @@ -248,6 +332,11 @@ update msg model = |> Http.send CompletedFollowChange ) + ClickedTab tab -> + ( { model | feedTab = tab } + , fetchFeed model.session tab (currentUsername model) 1 + ) + CompletedFollowChange (Ok newAuthor) -> ( { model | author = Loaded newAuthor } , Cmd.none @@ -335,12 +424,3 @@ subscriptions model = toSession : Model -> Session toSession model = model.session - - - --- INTERNAL - - -defaultFeedSources : Username -> FeedSources -defaultFeedSources username = - FeedSources.fromLists (AuthorFeed username) [ FavoritedFeed username ] diff --git a/advanced/part1/src/PaginatedList.elm b/advanced/part1/src/PaginatedList.elm index 9e73e71..2a4aba7 100644 --- a/advanced/part1/src/PaginatedList.elm +++ b/advanced/part1/src/PaginatedList.elm @@ -1,8 +1,11 @@ -module PaginatedList exposing (PaginatedList, fromList, map, mapPage, page, total, values, view) +module PaginatedList exposing (PaginatedList, fromList, fromRequestBuilder, map, page, total, values, view) import Html exposing (Html, a, li, text, ul) import Html.Attributes exposing (class, classList, href) import Html.Events exposing (onClick) +import Http +import HttpBuilder exposing (RequestBuilder) +import Task exposing (Task) @@ -54,29 +57,18 @@ map transform (PaginatedList info) = PaginatedList { info | values = List.map transform info.values } -mapPage : (Int -> Int) -> PaginatedList a -> PaginatedList a -mapPage transform (PaginatedList info) = - PaginatedList { info | page = transform info.page } - - -- VIEW -view : (Int -> msg) -> PaginatedList a -> Int -> Html msg -view toMsg list resultsPerPage = +view : (Int -> msg) -> PaginatedList a -> Html msg +view toMsg (PaginatedList info) = let - totalPages = - ceiling (toFloat (total list) / toFloat resultsPerPage) - - activePage = - page list - viewPageLink currentPage = - pageLink toMsg currentPage (currentPage == activePage) + pageLink toMsg currentPage (currentPage == info.page) in - if totalPages > 1 then - List.range 1 totalPages + if info.total > 1 then + List.range 1 info.total |> List.map viewPageLink |> ul [ class "pagination" ] @@ -96,3 +88,31 @@ pageLink toMsg targetPage isActive = ] [ text (String.fromInt targetPage) ] ] + + + +-- HTTP + + +{-| I considered accepting a record here so I don't mess up the argument order. +-} +fromRequestBuilder : + Int + -> Int + -> RequestBuilder (PaginatedList a) + -> Task Http.Error (PaginatedList a) +fromRequestBuilder resultsPerPage pageNumber builder = + let + offset = + (pageNumber - 1) * resultsPerPage + + params = + [ ( "limit", String.fromInt resultsPerPage ) + , ( "offset", String.fromInt offset ) + ] + in + builder + |> HttpBuilder.withQueryParams params + |> HttpBuilder.toRequest + |> Http.toTask + |> Task.map (\(PaginatedList info) -> PaginatedList { info | page = pageNumber })