From d06b6427e5be41d7814e43a025c9b48e9fa9ac96 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 13 Aug 2018 22:19:38 -0400 Subject: [PATCH] Update advanced/part5 --- advanced/part5/src/Article/Feed.elm | 304 ++++++--------------------- advanced/part5/src/Page/Home.elm | 210 ++++++++++++++++-- advanced/part5/src/Page/Profile.elm | 174 +++++++++++++-- advanced/part5/src/PaginatedList.elm | 34 ++- 4 files changed, 437 insertions(+), 285 deletions(-) diff --git a/advanced/part5/src/Article/Feed.elm b/advanced/part5/src/Article/Feed.elm index 2050db6..5fe6c04 100644 --- a/advanced/part5/src/Article/Feed.elm +++ b/advanced/part5/src/Article/Feed.elm @@ -2,11 +2,13 @@ module Article.Feed exposing ( Model , Msg + , articles + , decoder , init - , selectTag , update , viewArticles - , viewFeedSources + , viewPagination + , viewTabs ) import Api @@ -16,12 +18,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 +33,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 +61,40 @@ 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 articleList = + Model + { session = session + , errors = [] + , articles = articleList + , isLoading = False + } + + + +-- INFO + + +{-| NOTE: This is only exposed to simplify this exercise. Normally it is not exposed! +-} +articles : Model -> PaginatedList (Article Preview) +articles (Model info) = + info.articles @@ -97,61 +102,16 @@ init session sources = viewArticles : Time.Zone -> Model -> List (Html Msg) -viewArticles timeZone (Model { articles, sources, session }) = +viewArticles timeZone (Model info) = let maybeCred = - Session.cred session + Session.cred info.session articlesHtml = - PaginatedList.values articles + PaginatedList.values info.articles |> List.map (viewPreview maybeCred timeZone) - - feedSource = - FeedSources.selected sources - - pagination = - viewPaginatedList articles (limit feedSource) in - List.append articlesHtml [ pagination ] - - -{-| 👉 TODO Move this logic into PaginatedList.view and make it reusable, -so we can use it on other pages too! -💡 HINT: Make `PaginatedList.view` return `Html msg` instead of `Html Msg`. (The function will need to accept an extra argument for this to work.) --} -viewPaginatedList : PaginatedList a -> Int -> Html Msg -viewPaginatedList paginatedList resultsPerPage = - let - totalPages = - ceiling (toFloat (PaginatedList.total paginatedList) / toFloat resultsPerPage) - - activePage = - PaginatedList.page paginatedList - - viewPageLink currentPage = - pageLink currentPage (currentPage == activePage) - in - if totalPages > 1 then - List.range 1 totalPages - |> List.map viewPageLink - |> ul [ class "pagination" ] - - else - Html.text "" - - -pageLink : Int -> Bool -> Html Msg -pageLink targetPage isActive = - li [ classList [ ( "page-item", True ), ( "active", isActive ) ] ] - [ a - [ class "page-link" - , onClick (ClickedFeedPage targetPage) - - -- The RealWorld CSS requires an href to work properly. - , href "" - ] - [ text (String.fromInt targetPage) ] - ] + Page.viewErrors ClickedDismissErrors info.errors :: articlesHtml viewPreview : Maybe Cred -> Time.Zone -> Article Preview -> Html Msg @@ -206,85 +166,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 ] @@ -295,10 +213,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 ) @@ -307,32 +222,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 @@ -349,72 +238,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 = @@ -429,29 +252,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/part5/src/Page/Home.elm b/advanced/part5/src/Page/Home.elm index a4dea6d..7bedc9a 100644 --- a/advanced/part5/src/Page/Home.elm +++ b/advanced/part5/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, page, total) 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) + , [ viewPagination (Feed.articles feed) ] + ] + ] Loading -> [] @@ -130,11 +154,98 @@ 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)) + +-- PAGINATION + + +{-| 👉 TODO: Relocate `viewPagination` into `PaginatedList.view` and make it reusable, +then refactor both Page.Home and Page.Profile to use it! + +💡 HINT: Make `PaginatedList.view` return `Html msg` instead of `Html Msg`. +(You'll need to introduce at least one extra argument for this to work.) + +-} +viewPagination : PaginatedList (Article Preview) -> Html Msg +viewPagination list = + let + viewPageLink currentPage = + pageLink currentPage (currentPage == page list) + in + if total list > 1 then + List.range 1 (total list) + |> List.map viewPageLink + |> ul [ class "pagination" ] + + else + Html.text "" + + +pageLink : Int -> Bool -> Html Msg +pageLink targetPage isActive = + li [ classList [ ( "page-item", True ), ( "active", isActive ) ] ] + [ a + [ class "page-link" + , onClick (ClickedFeedPage targetPage) + + -- The RealWorld CSS requires an href to work properly. + , href "" + ] + [ text (String.fromInt targetPage) ] + ] + + + +-- 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 +271,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 +284,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 +371,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/part5/src/Page/Profile.elm b/advanced/part5/src/Page/Profile.elm index af86830..9c7eaca 100644 --- a/advanced/part5/src/Page/Profile.elm +++ b/advanced/part5/src/Page/Profile.elm @@ -3,16 +3,21 @@ 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) import Avatar exposing (Avatar) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick) import Http +import HttpBuilder exposing (RequestBuilder) import Loading import Log import Page +import PaginatedList exposing (PaginatedList, page, total) import Profile exposing (Profile) import Route import Session exposing (Session) @@ -31,6 +36,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 +45,11 @@ type alias Model = } +type FeedTab + = MyArticles + | FavoritedArticles + + type Status a = Loading Username | LoadingSlowly Username @@ -54,6 +66,8 @@ init session username = ( { session = session , timeZone = Time.utc , errors = [] + , feedTab = defaultFeedTab + , feedPage = 1 , author = Loading username , feed = Loading username } @@ -62,16 +76,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 +211,18 @@ 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" ] <| + List.concat + [ [ viewTabs model.feedTab ] + , Feed.viewArticles model.timeZone feed + |> List.map (Html.map GotFeedMsg) + , [ Feed.viewPagination ClickedFeedPage feed ] + ] + ] + ] + ] Loading _ -> text "" @@ -169,6 +246,46 @@ view model = +-- PAGINATION + + +{-| 👉 TODO: Relocate `viewPagination` into `PaginatedList.view` and make it reusable, +then refactor both Page.Home and Page.Profile to use it! + +💡 HINT: Make `PaginatedList.view` return `Html msg` instead of `Html Msg`. +(You'll need to introduce at least one extra argument for this to work.) + +-} +viewPagination : PaginatedList (Article Preview) -> Html Msg +viewPagination list = + let + viewPageLink currentPage = + pageLink currentPage (currentPage == page list) + in + if total list > 1 then + List.range 1 (total list) + |> List.map viewPageLink + |> ul [ class "pagination" ] + + else + Html.text "" + + +pageLink : Int -> Bool -> Html Msg +pageLink targetPage isActive = + li [ classList [ ( "page-item", True ), ( "active", isActive ) ] ] + [ a + [ class "page-link" + , onClick (ClickedFeedPage targetPage) + + -- The RealWorld CSS requires an href to work properly. + , href "" + ] + [ text (String.fromInt targetPage) ] + ] + + + -- PAGE TITLE @@ -202,15 +319,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 +350,8 @@ type Msg = ClickedDismissErrors | ClickedFollow Cred UnfollowedAuthor | ClickedUnfollow Cred FollowedAuthor + | ClickedTab FeedTab + | ClickedFeedPage Int | CompletedFollowChange (Result Http.Error Author) | CompletedAuthorLoad (Result ( Username, Http.Error ) Author) | CompletedFeedLoad (Result ( Username, Http.Error ) Feed.Model) @@ -248,6 +379,16 @@ update msg model = |> Http.send CompletedFollowChange ) + ClickedTab tab -> + ( { model | feedTab = tab } + , fetchFeed model.session tab (currentUsername model) 1 + ) + + ClickedFeedPage page -> + ( { model | feedPage = page } + , fetchFeed model.session model.feedTab (currentUsername model) page + ) + CompletedFollowChange (Ok newAuthor) -> ( { model | author = Loaded newAuthor } , Cmd.none @@ -335,12 +476,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/part5/src/PaginatedList.elm b/advanced/part5/src/PaginatedList.elm index 6fd5911..7c2c1e4 100644 --- a/advanced/part5/src/PaginatedList.elm +++ b/advanced/part5/src/PaginatedList.elm @@ -1,8 +1,11 @@ -module PaginatedList exposing (PaginatedList, fromList, map, mapPage, page, total, values) +module PaginatedList exposing (PaginatedList, fromList, fromRequestBuilder, map, page, total, values) 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,9 +57,32 @@ 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 } + +-- 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 })