Files
elm-0.19-workshop/advanced/part2/src/Article/Feed.elm
2018-08-13 22:02:14 -04:00

267 lines
7.3 KiB
Elm

module Article.Feed
exposing
( Model
, Msg
, decoder
, init
, update
, viewArticles
, viewPagination
, viewTabs
)
import Api
import Article exposing (Article, Preview)
import Article.FeedSources as FeedSources exposing (FeedSources, Source(..))
import Article.Slug as ArticleSlug exposing (Slug)
import Article.Tag as Tag exposing (Tag)
import Author
import Avatar exposing (Avatar)
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)
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (required)
import Page
import PaginatedList exposing (PaginatedList)
import Profile
import Route exposing (Route)
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)
{-| NOTE: This module has its own Model, view, and update. This is not normal!
If you find yourself doing this often, please watch <https://www.youtube.com/watch?v=DoA4Txr4GUs>
This is the reusable Article Feed that appears on both the Home page as well as
on the Profile page. There's a lot of logic here, so it's more convenient to use
the heavyweight approach of giving this its own Model, view, and update.
This means callers must use Html.map and Cmd.map to use this thing, but in
this case that's totally worth it because of the amount of logic wrapped up
in this thing.
For every other reusable view in this application, this API would be totally
overkill, so we use simpler APIs instead.
-}
-- MODEL
type Model
= 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 Internals =
{ session : Session
, errors : List String
, articles : PaginatedList (Article Preview)
, isLoading : Bool
}
init : Session -> PaginatedList (Article Preview) -> Model
init session articles =
Model
{ session = session
, errors = []
, articles = articles
, isLoading = False
}
-- VIEW
viewArticles : Time.Zone -> Model -> List (Html Msg)
viewArticles timeZone (Model { articles, session, errors }) =
let
maybeCred =
Session.cred session
articlesHtml =
PaginatedList.values articles
|> List.map (viewPreview maybeCred timeZone)
in
Page.viewErrors ClickedDismissErrors errors :: articlesHtml
viewPreview : Maybe Cred -> Time.Zone -> Article Preview -> Html Msg
viewPreview maybeCred timeZone article =
let
slug =
Article.slug article
{ title, description, createdAt } =
Article.metadata article
author =
Article.author article
profile =
Author.profile author
username =
Author.username author
faveButton =
case maybeCred of
Just cred ->
let
{ favoritesCount, favorited } =
Article.metadata article
viewButton =
if favorited then
Article.unfavoriteButton cred (ClickedUnfavorite cred slug)
else
Article.favoriteButton cred (ClickedFavorite cred slug)
in
viewButton [ class "pull-xs-right" ]
[ text (" " ++ String.fromInt favoritesCount) ]
Nothing ->
text ""
in
div [ class "article-preview" ]
[ div [ class "article-meta" ]
[ a [ Route.href (Route.Profile username) ]
[ img [ Avatar.src (Profile.avatar profile) ] [] ]
, div [ class "info" ]
[ Author.view username
, Timestamp.view timeZone createdAt
]
, faveButton
]
, a [ class "preview-link", Route.href (Route.Article (Article.slug article)) ]
[ h1 [] [ text title ]
, p [] [ text description ]
, span [] [ text "Read more..." ]
, ul [ class "tag-list" ]
(List.map viewTag (Article.metadata article).tags)
]
]
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 (viewTab []) before
, [ viewTab [ class "active" ] selected ]
, List.map (viewTab []) after
]
viewTab : List (Attribute msg) -> ( String, msg ) -> Html msg
viewTab attrs ( name, msg ) =
li [ class "nav-item" ]
[ -- Note: The RealWorld CSS requires an href to work properly.
a (class "nav-link" :: onClick msg :: href "" :: attrs)
[ text name ]
]
viewPagination : (Int -> msg) -> Model -> Html msg
viewPagination toMsg (Model feed) =
PaginatedList.view toMsg feed.articles
viewTag : String -> Html msg
viewTag tagName =
li [ class "tag-default tag-pill tag-outline" ] [ text tagName ]
-- UPDATE
type Msg
= ClickedDismissErrors
| ClickedFavorite Cred Slug
| ClickedUnfavorite Cred Slug
| CompletedFavorite (Result Http.Error (Article Preview))
update : Maybe Cred -> Msg -> Model -> ( Model, Cmd Msg )
update maybeCred msg (Model model) =
case msg of
ClickedDismissErrors ->
( Model { model | errors = [] }, Cmd.none )
ClickedFavorite cred slug ->
fave Article.favorite cred slug model
ClickedUnfavorite cred slug ->
fave Article.unfavorite cred slug model
CompletedFavorite (Ok article) ->
( Model { model | articles = PaginatedList.map (replaceArticle article) model.articles }
, Cmd.none
)
CompletedFavorite (Err error) ->
( Model { model | errors = Api.addServerError model.errors }
, Cmd.none
)
replaceArticle : Article a -> Article a -> Article a
replaceArticle newArticle oldArticle =
if Article.slug newArticle == Article.slug oldArticle then
newArticle
else
oldArticle
-- SERIALIZATION
decoder : Maybe Cred -> Int -> Decoder (PaginatedList (Article Preview))
decoder maybeCred resultsPerPage =
Decode.succeed PaginatedList.fromList
|> required "articlesCount" (pageCountDecoder resultsPerPage)
|> required "articles" (Decode.list (Article.previewDecoder maybeCred))
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 -> Internals -> ( Model, Cmd Msg )
fave toRequest cred slug model =
( Model model
, toRequest slug cred
|> Http.toTask
|> Task.attempt CompletedFavorite
)