module Page.Profile exposing (Model, Msg, init, subscriptions, toSession, update, view) {-| 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) import Task exposing (Task) import Time import Username exposing (Username) import Viewer exposing (Viewer) import Viewer.Cred as Cred exposing (Cred) -- MODEL type alias Model = { session : Session , timeZone : Time.Zone , errors : List String , feedTab : FeedTab , feedPage : Int -- Loaded independently from server , author : Status Author , feed : Status Feed.Model } type FeedTab = MyArticles | FavoritedArticles type Status a = Loading Username | LoadingSlowly Username | Loaded a | Failed Username init : Session -> Username -> ( Model, Cmd Msg ) init session username = let maybeCred = Session.cred session in ( { session = session , timeZone = Time.utc , errors = [] , feedTab = defaultFeedTab , feedPage = 1 , author = Loading username , feed = Loading username } , Cmd.batch [ Author.fetch username maybeCred |> Http.toTask |> Task.mapError (Tuple.pair username) |> Task.attempt CompletedAuthorLoad , 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 view : Model -> { title : String, content : Html Msg } view model = let title = case model.author of Loaded (IsViewer _ _) -> myProfileTitle Loaded ((IsFollowing followedAuthor) as author) -> titleForOther (Author.username author) Loaded ((IsNotFollowing unfollowedAuthor) as author) -> titleForOther (Author.username author) Loading username -> titleForMe (Session.cred model.session) username LoadingSlowly username -> titleForMe (Session.cred model.session) username Failed username -> titleForMe (Session.cred model.session) username in { title = title , content = case model.author of Loaded author -> let profile = Author.profile author username = Author.username author followButton = case Session.cred model.session of Just cred -> case author of IsViewer _ _ -> -- We can't follow ourselves! text "" IsFollowing followedAuthor -> Author.unfollowButton ClickedUnfollow cred followedAuthor IsNotFollowing unfollowedAuthor -> Author.followButton ClickedFollow cred unfollowedAuthor Nothing -> -- We can't follow if we're logged out text "" in div [ class "profile-page" ] [ Page.viewErrors ClickedDismissErrors model.errors , div [ class "user-info" ] [ div [ class "container" ] [ div [ class "row" ] [ div [ class "col-xs-12 col-md-10 offset-md-1" ] [ img [ class "user-img", Avatar.src (Profile.avatar profile) ] [] , h4 [] [ Username.toHtml username ] , p [] [ text (Maybe.withDefault "" (Profile.bio profile)) ] , followButton ] ] ] ] , case model.feed of Loaded feed -> div [ class "container" ] [ 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) , [ viewPagination (Feed.articles feed) ] ] ] ] ] Loading _ -> text "" LoadingSlowly _ -> Loading.icon Failed _ -> Loading.error "feed" ] Loading _ -> text "" LoadingSlowly _ -> Loading.icon Failed _ -> Loading.error "profile" } -- 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 titleForOther : Username -> String titleForOther otherUsername = "Profile — " ++ Username.toString otherUsername titleForMe : Maybe Cred -> Username -> String titleForMe maybeCred username = case maybeCred of Just cred -> if username == Cred.username cred then myProfileTitle else defaultTitle Nothing -> defaultTitle myProfileTitle : String myProfileTitle = "My Profile" defaultTitle : String defaultTitle = "Profile" -- TABS 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 ) -- UPDATE 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) | GotTimeZone Time.Zone | GotFeedMsg Feed.Msg | GotSession Session | PassedSlowLoadThreshold update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of ClickedDismissErrors -> ( { model | errors = [] }, Cmd.none ) ClickedUnfollow cred followedAuthor -> ( model , Author.requestUnfollow followedAuthor cred |> Http.send CompletedFollowChange ) ClickedFollow cred unfollowedAuthor -> ( model , Author.requestFollow unfollowedAuthor cred |> 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 ) CompletedFollowChange (Err error) -> ( model , Log.error ) CompletedAuthorLoad (Ok author) -> ( { model | author = Loaded author }, Cmd.none ) CompletedAuthorLoad (Err ( username, err )) -> ( { model | author = Failed username } , Log.error ) CompletedFeedLoad (Ok feed) -> ( { model | feed = Loaded feed } , Cmd.none ) CompletedFeedLoad (Err ( username, err )) -> ( { model | feed = Failed username } , Log.error ) GotFeedMsg subMsg -> case model.feed of Loaded feed -> let ( newFeed, subCmd ) = Feed.update (Session.cred model.session) subMsg feed in ( { model | feed = Loaded newFeed } , Cmd.map GotFeedMsg subCmd ) Loading _ -> ( model, Log.error ) LoadingSlowly _ -> ( model, Log.error ) Failed _ -> ( model, Log.error ) GotTimeZone tz -> ( { model | timeZone = tz }, Cmd.none ) GotSession session -> ( { model | session = session } , Route.replaceUrl (Session.navKey session) Route.Home ) PassedSlowLoadThreshold -> let -- If any data is still Loading, change it to LoadingSlowly -- so `view` knows to render a spinner. feed = case model.feed of Loading username -> LoadingSlowly username other -> other in ( { model | feed = feed }, Cmd.none ) -- SUBSCRIPTIONS subscriptions : Model -> Sub Msg subscriptions model = Session.changes GotSession (Session.navKey model.session) -- EXPORT toSession : Model -> Session toSession model = model.session