Update intro/part8

This commit is contained in:
Richard Feldman
2018-08-13 22:01:08 -04:00
parent 2978c6e193
commit c2b9a00446
4 changed files with 338 additions and 257 deletions

View File

@@ -2,11 +2,12 @@ module Article.Feed
exposing exposing
( Model ( Model
, Msg , Msg
, decoder
, init , init
, selectTag
, update , update
, viewArticles , viewArticles
, viewFeedSources , viewPagination
, viewTabs
) )
import Api import Api
@@ -16,12 +17,11 @@ import Article.Slug as ArticleSlug exposing (Slug)
import Article.Tag as Tag exposing (Tag) import Article.Tag as Tag exposing (Tag)
import Author import Author
import Avatar exposing (Avatar) import Avatar exposing (Avatar)
import Browser.Dom as Dom
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (attribute, class, classList, href, id, placeholder, src) import Html.Attributes exposing (attribute, class, classList, href, id, placeholder, src)
import Html.Events exposing (onClick) import Html.Events exposing (onClick)
import Http import Http
import HttpBuilder exposing (RequestBuilder, withExpect, withQueryParams) import HttpBuilder exposing (RequestBuilder)
import Json.Decode as Decode exposing (Decoder) import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (required) import Json.Decode.Pipeline exposing (required)
import Page import Page
@@ -32,6 +32,7 @@ import Session exposing (Session)
import Task exposing (Task) import Task exposing (Task)
import Time import Time
import Timestamp import Timestamp
import Url exposing (Url)
import Username exposing (Username) import Username exposing (Username)
import Viewer exposing (Viewer) import Viewer exposing (Viewer)
import Viewer.Cred as Cred exposing (Cred) import Viewer.Cred as Cred exposing (Cred)
@@ -59,37 +60,29 @@ overkill, so we use simpler APIs instead.
type Model type Model
= Model InternalModel = Model Internals
{-| This should not be exposed! We want to benefit from the guarantee that only {-| 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 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. a surprising state, we know exactly where to look: this module.
-} -}
type alias InternalModel = type alias Internals =
{ session : Session { session : Session
, errors : List String , errors : List String
, articles : PaginatedList (Article Preview) , articles : PaginatedList (Article Preview)
, sources : FeedSources
, isLoading : Bool , isLoading : Bool
} }
init : Session -> FeedSources -> Task Http.Error Model init : Session -> PaginatedList (Article Preview) -> Model
init session sources = init session articles =
let Model
fromArticles articles = { session = session
Model , errors = []
{ session = session , articles = articles
, errors = [] , isLoading = False
, articles = articles }
, sources = sources
, isLoading = False
}
in
FeedSources.selected sources
|> fetch (Session.cred session) 1
|> Task.map fromArticles
@@ -97,7 +90,7 @@ init session sources =
viewArticles : Time.Zone -> Model -> List (Html Msg) viewArticles : Time.Zone -> Model -> List (Html Msg)
viewArticles timeZone (Model { articles, sources, session }) = viewArticles timeZone (Model { articles, session, errors }) =
let let
maybeCred = maybeCred =
Session.cred session Session.cred session
@@ -105,14 +98,8 @@ viewArticles timeZone (Model { articles, sources, session }) =
articlesHtml = articlesHtml =
PaginatedList.values articles PaginatedList.values articles
|> List.map (viewPreview maybeCred timeZone) |> List.map (viewPreview maybeCred timeZone)
feedSource =
FeedSources.selected sources
pagination =
PaginatedList.view ClickedFeedPage articles (limit feedSource)
in in
List.append articlesHtml [ pagination ] Page.viewErrors ClickedDismissErrors errors :: articlesHtml
viewPreview : Maybe Cred -> Time.Zone -> Article Preview -> Html Msg viewPreview : Maybe Cred -> Time.Zone -> Article Preview -> Html Msg
@@ -167,85 +154,43 @@ viewPreview maybeCred timeZone article =
[ h1 [] [ text title ] [ h1 [] [ text title ]
, p [] [ text description ] , p [] [ text description ]
, span [] [ text "Read more..." ] , span [] [ text "Read more..." ]
, ul [ class "tag-list" ]
(List.map viewTag (Article.metadata article).tags)
] ]
] ]
viewFeedSources : Model -> Html Msg viewTabs :
viewFeedSources (Model { sources, isLoading, errors }) = List ( String, msg )
let -> ( String, msg )
errorsHtml = -> List ( String, msg )
Page.viewErrors ClickedDismissErrors errors -> Html msg
in viewTabs before selected after =
ul [ class "nav nav-pills outline-active" ] <| ul [ class "nav nav-pills outline-active" ] <|
List.concat List.concat
[ List.map (viewFeedSource False) (FeedSources.before sources) [ List.map (viewTab []) before
, [ viewFeedSource True (FeedSources.selected sources) ] , [ viewTab [ class "active" ] selected ]
, List.map (viewFeedSource False) (FeedSources.after sources) , List.map (viewTab []) after
, [ errorsHtml ]
] ]
viewFeedSource : Bool -> Source -> Html Msg viewTab : List (Attribute msg) -> ( String, msg ) -> Html msg
viewFeedSource isSelected source = viewTab attrs ( name, msg ) =
li [ class "nav-item" ] li [ class "nav-item" ]
[ a [ -- Note: The RealWorld CSS requires an href to work properly.
[ classList [ ( "nav-link", True ), ( "active", isSelected ) ] a (class "nav-link" :: onClick msg :: href "" :: attrs)
, onClick (ClickedFeedSource source) [ text name ]
-- The RealWorld CSS requires an href to work properly.
, href ""
]
[ text (sourceName source) ]
] ]
selectTag : Maybe Cred -> Tag -> Cmd Msg viewPagination : (Int -> msg) -> Model -> Html msg
selectTag maybeCred tag = viewPagination toMsg (Model feed) =
let PaginatedList.view toMsg feed.articles
source =
TagFeed tag
in
fetch maybeCred 1 source
|> Task.attempt (CompletedFeedLoad source)
sourceName : Source -> String viewTag : String -> Html msg
sourceName source = viewTag tagName =
case source of li [ class "tag-default tag-pill tag-outline" ] [ text tagName ]
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
@@ -256,10 +201,7 @@ type Msg
= ClickedDismissErrors = ClickedDismissErrors
| ClickedFavorite Cred Slug | ClickedFavorite Cred Slug
| ClickedUnfavorite Cred Slug | ClickedUnfavorite Cred Slug
| ClickedFeedPage Int
| ClickedFeedSource Source
| CompletedFavorite (Result Http.Error (Article Preview)) | CompletedFavorite (Result Http.Error (Article Preview))
| CompletedFeedLoad Source (Result Http.Error (PaginatedList (Article Preview)))
update : Maybe Cred -> Msg -> Model -> ( Model, Cmd Msg ) update : Maybe Cred -> Msg -> Model -> ( Model, Cmd Msg )
@@ -268,32 +210,6 @@ update maybeCred msg (Model model) =
ClickedDismissErrors -> ClickedDismissErrors ->
( Model { model | errors = [] }, Cmd.none ) ( 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 -> ClickedFavorite cred slug ->
fave Article.favorite cred slug model fave Article.favorite cred slug model
@@ -310,72 +226,6 @@ update maybeCred msg (Model model) =
, Cmd.none , 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 : Article a -> Article a -> Article a
replaceArticle newArticle oldArticle = replaceArticle newArticle oldArticle =
@@ -390,29 +240,24 @@ replaceArticle newArticle oldArticle =
-- SERIALIZATION -- SERIALIZATION
decoder : Maybe Cred -> Decoder (PaginatedList (Article Preview)) decoder : Maybe Cred -> Int -> Decoder (PaginatedList (Article Preview))
decoder maybeCred = decoder maybeCred resultsPerPage =
Decode.succeed PaginatedList.fromList Decode.succeed PaginatedList.fromList
|> required "articlesCount" Decode.int |> required "articlesCount" (pageCountDecoder resultsPerPage)
|> required "articles" (Decode.list (Article.previewDecoder maybeCred)) |> required "articles" (Decode.list (Article.previewDecoder maybeCred))
pageCountDecoder : Int -> Decoder Int
-- REQUEST pageCountDecoder resultsPerPage =
Decode.int
|> Decode.map (\total -> ceiling (toFloat total / toFloat resultsPerPage))
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
-- INTERNAL -- 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 = fave toRequest cred slug model =
( Model model ( Model model
, toRequest slug cred , toRequest slug cred

View File

@@ -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. {-| 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.Feed as Feed
import Article.FeedSources as FeedSources exposing (FeedSources, Source(..)) import Article.FeedSources as FeedSources exposing (FeedSources, Source(..))
import Article.Tag as Tag exposing (Tag) import Article.Tag as Tag exposing (Tag)
import Browser.Dom as Dom
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (attribute, class, classList, href, id, placeholder) import Html.Attributes exposing (attribute, class, classList, href, id, placeholder)
import Html.Events exposing (onClick) import Html.Events exposing (onClick)
import Http import Http
import HttpBuilder
import Loading import Loading
import Log import Log
import Page import Page
import PaginatedList exposing (PaginatedList)
import Session exposing (Session) import Session exposing (Session)
import Task exposing (Task) import Task exposing (Task)
import Time import Time
import Username exposing (Username)
import Viewer.Cred as Cred exposing (Cred) import Viewer.Cred as Cred exposing (Cred)
@@ -27,6 +32,8 @@ import Viewer.Cred as Cred exposing (Cred)
type alias Model = type alias Model =
{ session : Session { session : Session
, timeZone : Time.Zone , timeZone : Time.Zone
, feedTab : FeedTab
, feedPage : Int
-- Loaded independently from server -- Loaded independently from server
, tags : Status (List Tag) , tags : Status (List Tag)
@@ -41,28 +48,35 @@ type Status a
| Failed | Failed
type FeedTab
= YourFeed Cred
| GlobalFeed
| TagFeed Tag
init : Session -> ( Model, Cmd Msg ) init : Session -> ( Model, Cmd Msg )
init session = init session =
let let
feedSources = feedTab =
case Session.cred session of case Session.cred session of
Just cred -> Just cred ->
FeedSources.fromLists (YourFeed cred) [ GlobalFeed ] YourFeed cred
Nothing -> Nothing ->
FeedSources.fromLists GlobalFeed [] GlobalFeed
loadTags = loadTags =
Tag.list Http.toTask Tag.list
|> Http.toTask
in in
( { session = session ( { session = session
, timeZone = Time.utc , timeZone = Time.utc
, feedTab = feedTab
, feedPage = 1
, tags = Loading , tags = Loading
, feed = Loading , feed = Loading
} }
, Cmd.batch , Cmd.batch
[ Feed.init session feedSources [ fetchFeed session feedTab 1
|> Task.attempt CompletedFeedLoad |> Task.attempt CompletedFeedLoad
, Tag.list , Tag.list
|> Http.send CompletedTagsLoad |> Http.send CompletedTagsLoad
@@ -87,7 +101,17 @@ view model =
[ div [ class "col-md-9" ] <| [ div [ class "col-md-9" ] <|
case model.feed of case model.feed of
Loaded feed -> 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 -> Loading ->
[] []
@@ -130,11 +154,58 @@ viewBanner =
] ]
viewFeed : Time.Zone -> Feed.Model -> List (Html Msg)
viewFeed timeZone feed = -- TABS
div [ class "feed-toggle" ]
[ Feed.viewFeedSources feed |> Html.map GotFeedMsg ]
:: (Feed.viewArticles timeZone feed |> List.map (Html.map GotFeedMsg)) 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 viewTags : List Tag -> Html Msg
@@ -160,6 +231,8 @@ viewTag tagName =
type Msg type Msg
= ClickedTag Tag = ClickedTag Tag
| ClickedTab FeedTab
| ClickedFeedPage Int
| CompletedFeedLoad (Result Http.Error Feed.Model) | CompletedFeedLoad (Result Http.Error Feed.Model)
| CompletedTagsLoad (Result Http.Error (List Tag)) | CompletedTagsLoad (Result Http.Error (List Tag))
| GotTimeZone Time.Zone | GotTimeZone Time.Zone
@@ -171,12 +244,28 @@ type Msg
update : Msg -> Model -> ( Model, Cmd Msg ) update : Msg -> Model -> ( Model, Cmd Msg )
update msg model = update msg model =
case msg of case msg of
ClickedTag tagName -> ClickedTag tag ->
let let
subCmd = feedTab =
Feed.selectTag (Session.cred model.session) tagName TagFeed tag
in 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) -> CompletedFeedLoad (Ok feed) ->
( { model | feed = Loaded feed }, Cmd.none ) ( { 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 -- SUBSCRIPTIONS

View File

@@ -3,6 +3,8 @@ module Page.Profile exposing (Model, Msg, init, subscriptions, toSession, update
{-| An Author's profile. {-| An Author's profile.
-} -}
import Api
import Article exposing (Article, Preview)
import Article.Feed as Feed import Article.Feed as Feed
import Article.FeedSources as FeedSources exposing (FeedSources, Source(..)) import Article.FeedSources as FeedSources exposing (FeedSources, Source(..))
import Author exposing (Author(..), FollowedAuthor, UnfollowedAuthor) import Author exposing (Author(..), FollowedAuthor, UnfollowedAuthor)
@@ -10,9 +12,11 @@ import Avatar exposing (Avatar)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Http import Http
import HttpBuilder exposing (RequestBuilder)
import Loading import Loading
import Log import Log
import Page import Page
import PaginatedList exposing (PaginatedList)
import Profile exposing (Profile) import Profile exposing (Profile)
import Route import Route
import Session exposing (Session) import Session exposing (Session)
@@ -31,6 +35,8 @@ type alias Model =
{ session : Session { session : Session
, timeZone : Time.Zone , timeZone : Time.Zone
, errors : List String , errors : List String
, feedTab : FeedTab
, feedPage : Int
-- Loaded independently from server -- Loaded independently from server
, author : Status Author , author : Status Author
@@ -38,6 +44,11 @@ type alias Model =
} }
type FeedTab
= MyArticles
| FavoritedArticles
type Status a type Status a
= Loading Username = Loading Username
| LoadingSlowly Username | LoadingSlowly Username
@@ -54,6 +65,8 @@ init session username =
( { session = session ( { session = session
, timeZone = Time.utc , timeZone = Time.utc
, errors = [] , errors = []
, feedTab = defaultFeedTab
, feedPage = 1
, author = Loading username , author = Loading username
, feed = Loading username , feed = Loading username
} }
@@ -62,16 +75,68 @@ init session username =
|> Http.toTask |> Http.toTask
|> Task.mapError (Tuple.pair username) |> Task.mapError (Tuple.pair username)
|> Task.attempt CompletedAuthorLoad |> Task.attempt CompletedAuthorLoad
, defaultFeedSources username , fetchFeed session defaultFeedTab username 1
|> Feed.init session
|> Task.mapError (Tuple.pair username)
|> Task.attempt CompletedFeedLoad
, Task.perform GotTimeZone Time.here , Task.perform GotTimeZone Time.here
, Task.perform (\_ -> PassedSlowLoadThreshold) Loading.slowThreshold , 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 -- VIEW
@@ -145,7 +210,13 @@ view model =
, case model.feed of , case model.feed of
Loaded feed -> Loaded feed ->
div [ class "container" ] 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 _ -> Loading _ ->
text "" text ""
@@ -202,15 +273,27 @@ defaultTitle =
-- FEED -- TABS
viewFeed : Time.Zone -> Feed.Model -> Html Msg viewTabs : FeedTab -> Html Msg
viewFeed timeZone feed = viewTabs tab =
div [ class "col-xs-12 col-md-10 offset-md-1" ] <| case tab of
div [ class "articles-toggle" ] MyArticles ->
[ Feed.viewFeedSources feed |> Html.map GotFeedMsg ] Feed.viewTabs [] myArticles [ favoritedArticles ]
:: (Feed.viewArticles timeZone feed |> List.map (Html.map GotFeedMsg))
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 = ClickedDismissErrors
| ClickedFollow Cred UnfollowedAuthor | ClickedFollow Cred UnfollowedAuthor
| ClickedUnfollow Cred FollowedAuthor | ClickedUnfollow Cred FollowedAuthor
| ClickedTab FeedTab
| CompletedFollowChange (Result Http.Error Author) | CompletedFollowChange (Result Http.Error Author)
| CompletedAuthorLoad (Result ( Username, Http.Error ) Author) | CompletedAuthorLoad (Result ( Username, Http.Error ) Author)
| CompletedFeedLoad (Result ( Username, Http.Error ) Feed.Model) | CompletedFeedLoad (Result ( Username, Http.Error ) Feed.Model)
@@ -248,6 +332,11 @@ update msg model =
|> Http.send CompletedFollowChange |> Http.send CompletedFollowChange
) )
ClickedTab tab ->
( { model | feedTab = tab }
, fetchFeed model.session tab (currentUsername model) 1
)
CompletedFollowChange (Ok newAuthor) -> CompletedFollowChange (Ok newAuthor) ->
( { model | author = Loaded newAuthor } ( { model | author = Loaded newAuthor }
, Cmd.none , Cmd.none
@@ -335,12 +424,3 @@ subscriptions model =
toSession : Model -> Session toSession : Model -> Session
toSession model = toSession model =
model.session model.session
-- INTERNAL
defaultFeedSources : Username -> FeedSources
defaultFeedSources username =
FeedSources.fromLists (AuthorFeed username) [ FavoritedFeed username ]

View File

@@ -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 exposing (Html, a, li, text, ul)
import Html.Attributes exposing (class, classList, href) import Html.Attributes exposing (class, classList, href)
import Html.Events exposing (onClick) 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 } 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
view : (Int -> msg) -> PaginatedList a -> Int -> Html msg view : (Int -> msg) -> PaginatedList a -> Html msg
view toMsg list resultsPerPage = view toMsg (PaginatedList info) =
let let
totalPages =
ceiling (toFloat (total list) / toFloat resultsPerPage)
activePage =
page list
viewPageLink currentPage = viewPageLink currentPage =
pageLink toMsg currentPage (currentPage == activePage) pageLink toMsg currentPage (currentPage == info.page)
in in
if totalPages > 1 then if info.total > 1 then
List.range 1 totalPages List.range 1 info.total
|> List.map viewPageLink |> List.map viewPageLink
|> ul [ class "pagination" ] |> ul [ class "pagination" ]
@@ -96,3 +88,31 @@ pageLink toMsg targetPage isActive =
] ]
[ text (String.fromInt targetPage) ] [ 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 })