Compare commits
21 Commits
master
...
solutions-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66094700c4 | ||
|
|
d31b9dd661 | ||
|
|
b9e3cb8119 | ||
|
|
354a707119 | ||
|
|
3ec84cb051 | ||
|
|
f744cad4f9 | ||
|
|
46870e98bf | ||
|
|
176b28ae6b | ||
|
|
75a0c00828 | ||
|
|
90db6e81b0 | ||
|
|
9edc567a13 | ||
|
|
34d3b7e6ec | ||
|
|
acea5cefa0 | ||
|
|
34b7e6e52e | ||
|
|
a28ef48931 | ||
|
|
633a1efbd9 | ||
|
|
412ddaa55f | ||
|
|
3e6c780a93 | ||
|
|
a329bd2f40 | ||
|
|
aed9d1aa5c | ||
|
|
da5af433f6 |
10
README.md
10
README.md
@@ -14,7 +14,7 @@ Getting Started
|
|||||||
> **Note:** Make sure not to run this command with `sudo`! If it gives you an `EACCESS` error, apply [**this fix**](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-two-change-npms-default-directory) and then re-run the command (still without `sudo`).
|
> **Note:** Make sure not to run this command with `sudo`! If it gives you an `EACCESS` error, apply [**this fix**](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-two-change-npms-default-directory) and then re-run the command (still without `sudo`).
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm install -g elm-test@beta elm-format@rc
|
npm install -g elm elm-test@elm0.19.0 elm-format
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Clone this repository
|
5. Clone this repository
|
||||||
@@ -27,3 +27,11 @@ cd elm-0.19-workshop
|
|||||||
```
|
```
|
||||||
|
|
||||||
6. Continue with either the [`intro`](https://github.com/rtfeldman/elm-0.19-workshop/blob/master/intro/README.md) or [`advanced`](https://github.com/rtfeldman/elm-0.19-workshop/blob/master/advanced/README.md) instructions, depending on which workshop you're doing!
|
6. Continue with either the [`intro`](https://github.com/rtfeldman/elm-0.19-workshop/blob/master/intro/README.md) or [`advanced`](https://github.com/rtfeldman/elm-0.19-workshop/blob/master/advanced/README.md) instructions, depending on which workshop you're doing!
|
||||||
|
|
||||||
|
Video Course of this Workshop
|
||||||
|
=======================
|
||||||
|
|
||||||
|
I recorded full-length videos for [Frontend Masters](https://frontendmasters.com/), in which I teach both of these workshops start to finish:
|
||||||
|
|
||||||
|
* [Introduction to Elm](https://frontendmasters.com/courses/intro-elm/) video course
|
||||||
|
* [Advanced Elm](https://frontendmasters.com/courses/advanced-elm/) video course
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ import Username exposing (Username)
|
|||||||
-- TYPES
|
-- TYPES
|
||||||
|
|
||||||
|
|
||||||
type alias Cred =
|
type Cred
|
||||||
-- 👉 TODO make Cred an opaque type, then fix the resulting compiler errors.
|
= Cred Username String
|
||||||
-- Afterwards, it should no longer be possible for any other module to access
|
|
||||||
-- this `token` vale directly!
|
|
||||||
--
|
|
||||||
-- 💡 HINT: Other modules still depend on being able to access the
|
-- INFO
|
||||||
-- `username` value. Expand this module's API to expose a new way for them
|
|
||||||
-- to access the `username` without also giving them access to `token`.
|
|
||||||
{ username : Username
|
username : Cred -> Username
|
||||||
, token : String
|
username (Cred uname _) =
|
||||||
}
|
uname
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -40,14 +40,14 @@ decoder =
|
|||||||
|
|
||||||
|
|
||||||
encodeToken : Cred -> Value
|
encodeToken : Cred -> Value
|
||||||
encodeToken cred =
|
encodeToken (Cred _ token) =
|
||||||
Encode.string cred.token
|
Encode.string token
|
||||||
|
|
||||||
|
|
||||||
addHeader : Cred -> RequestBuilder a -> RequestBuilder a
|
addHeader : Cred -> RequestBuilder a -> RequestBuilder a
|
||||||
addHeader cred builder =
|
addHeader (Cred _ token) builder =
|
||||||
builder
|
builder
|
||||||
|> withHeader "authorization" ("Token " ++ cred.token)
|
|> withHeader "authorization" ("Token " ++ token)
|
||||||
|
|
||||||
|
|
||||||
addHeaderIfAvailable : Maybe Cred -> RequestBuilder a -> RequestBuilder a
|
addHeaderIfAvailable : Maybe Cred -> RequestBuilder a -> RequestBuilder a
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ slug (Article internals _) =
|
|||||||
|
|
||||||
|
|
||||||
body : Article Full -> Body
|
body : Article Full -> Body
|
||||||
body _ =
|
body (Article _ (Full bod)) =
|
||||||
"👉 TODO make this return the article's body"
|
bod
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -181,8 +181,8 @@ mapAuthor transform (Article info extras) =
|
|||||||
|
|
||||||
|
|
||||||
fromPreview : Body -> Article Preview -> Article Full
|
fromPreview : Body -> Article Preview -> Article Full
|
||||||
fromPreview _ _ =
|
fromPreview bod (Article info _) =
|
||||||
"👉 TODO convert from an Article Preview to an Article Full"
|
Article info (Full bod)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ fullDecoder : Maybe Cred -> Decoder (Article Full)
|
|||||||
fullDecoder maybeCred =
|
fullDecoder maybeCred =
|
||||||
Decode.succeed Article
|
Decode.succeed Article
|
||||||
|> custom (internalsDecoder maybeCred)
|
|> custom (internalsDecoder maybeCred)
|
||||||
|> required "body" "👉 TODO use `Body.decoder` (which is a `Decoder Body`) to decode the body into this Article Full"
|
|> required "body" (Decode.map Full Body.decoder)
|
||||||
|
|
||||||
|
|
||||||
internalsDecoder : Maybe Cred -> Decoder Internals
|
internalsDecoder : Maybe Cred -> Decoder Internals
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -71,21 +71,16 @@ init session slug =
|
|||||||
, article = Loading
|
, article = Loading
|
||||||
}
|
}
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
{- 👉 TODO: Oops! These are all `Task` values, not `Cmd` values!
|
|
||||||
|
|
||||||
Use `|> Task.attempt` and `|> Task.perform` to make this compile.
|
|
||||||
|
|
||||||
Relevant docs:
|
|
||||||
|
|
||||||
https://alpha.elm-lang.org/packages/elm/core/latest/Task#attempt
|
|
||||||
https://alpha.elm-lang.org/packages/elm/core/latest/Task#perform
|
|
||||||
-}
|
|
||||||
[ Article.fetch maybeCred slug
|
[ Article.fetch maybeCred slug
|
||||||
|> Http.toTask
|
|> Http.toTask
|
||||||
|
|> Task.attempt CompletedLoadArticle
|
||||||
, Comment.list maybeCred slug
|
, Comment.list maybeCred slug
|
||||||
|> Http.toTask
|
|> Http.toTask
|
||||||
|
|> Task.attempt CompletedLoadComments
|
||||||
, Time.here
|
, Time.here
|
||||||
|
|> Task.perform GotTimeZone
|
||||||
, Loading.slowThreshold
|
, Loading.slowThreshold
|
||||||
|
|> Task.perform (\_ -> PassedSlowLoadThreshold)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ view model =
|
|||||||
Loaded feed ->
|
Loaded feed ->
|
||||||
[ div [ class "feed-toggle" ] <|
|
[ div [ class "feed-toggle" ] <|
|
||||||
List.concat
|
List.concat
|
||||||
[ [ viewTabs model ]
|
[ [ viewTabs model.session model.feedTab ]
|
||||||
, Feed.viewArticles model.timeZone feed
|
, Feed.viewArticles model.timeZone feed
|
||||||
|> List.map (Html.map GotFeedMsg)
|
|> List.map (Html.map GotFeedMsg)
|
||||||
, [ Feed.viewPagination ClickedFeedPage feed ]
|
, [ Feed.viewPagination ClickedFeedPage feed ]
|
||||||
@@ -155,21 +155,16 @@ viewBanner =
|
|||||||
-- TABS
|
-- TABS
|
||||||
|
|
||||||
|
|
||||||
{-| 👉 TODO: refactor this to accept narrower types than the entire Model.
|
viewTabs : Session -> FeedTab -> Html Msg
|
||||||
|
viewTabs session feedTab =
|
||||||
💡 HINT: It may end up with multiple arguments!
|
case feedTab of
|
||||||
|
|
||||||
-}
|
|
||||||
viewTabs : Model -> Html Msg
|
|
||||||
viewTabs model =
|
|
||||||
case model.feedTab of
|
|
||||||
YourFeed cred ->
|
YourFeed cred ->
|
||||||
Feed.viewTabs [] (yourFeed cred) [ globalFeed ]
|
Feed.viewTabs [] (yourFeed cred) [ globalFeed ]
|
||||||
|
|
||||||
GlobalFeed ->
|
GlobalFeed ->
|
||||||
let
|
let
|
||||||
otherTabs =
|
otherTabs =
|
||||||
case Session.cred model.session of
|
case Session.cred session of
|
||||||
Just cred ->
|
Just cred ->
|
||||||
[ yourFeed cred ]
|
[ yourFeed cred ]
|
||||||
|
|
||||||
@@ -181,7 +176,7 @@ viewTabs model =
|
|||||||
TagFeed tag ->
|
TagFeed tag ->
|
||||||
let
|
let
|
||||||
otherTabs =
|
otherTabs =
|
||||||
case Session.cred model.session of
|
case Session.cred session of
|
||||||
Just cred ->
|
Just cred ->
|
||||||
[ yourFeed cred, globalFeed ]
|
[ yourFeed cred, globalFeed ]
|
||||||
|
|
||||||
|
|||||||
@@ -61,9 +61,8 @@ init session username =
|
|||||||
let
|
let
|
||||||
maybeCred =
|
maybeCred =
|
||||||
Session.cred session
|
Session.cred session
|
||||||
|
in
|
||||||
model =
|
( { session = session
|
||||||
{ session = session
|
|
||||||
, timeZone = Time.utc
|
, timeZone = Time.utc
|
||||||
, errors = []
|
, errors = []
|
||||||
, feedTab = defaultFeedTab
|
, feedTab = defaultFeedTab
|
||||||
@@ -71,14 +70,12 @@ init session username =
|
|||||||
, author = Loading username
|
, author = Loading username
|
||||||
, feed = Loading username
|
, feed = Loading username
|
||||||
}
|
}
|
||||||
in
|
|
||||||
( model
|
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ Author.fetch username maybeCred
|
[ Author.fetch username maybeCred
|
||||||
|> Http.toTask
|
|> Http.toTask
|
||||||
|> Task.mapError (Tuple.pair username)
|
|> Task.mapError (Tuple.pair username)
|
||||||
|> Task.attempt CompletedAuthorLoad
|
|> Task.attempt CompletedAuthorLoad
|
||||||
, fetchFeed model defaultFeedTab 1
|
, fetchFeed session username defaultFeedTab 1
|
||||||
, Task.perform GotTimeZone Time.here
|
, Task.perform GotTimeZone Time.here
|
||||||
, Task.perform (\_ -> PassedSlowLoadThreshold) Loading.slowThreshold
|
, Task.perform (\_ -> PassedSlowLoadThreshold) Loading.slowThreshold
|
||||||
]
|
]
|
||||||
@@ -110,19 +107,11 @@ defaultFeedTab =
|
|||||||
-- HTTP
|
-- HTTP
|
||||||
|
|
||||||
|
|
||||||
{-| 👉 TODO: refactor this to accept narrower types than the entire Model.
|
fetchFeed : Session -> Username -> FeedTab -> Int -> Cmd Msg
|
||||||
|
fetchFeed session username feedTabs page =
|
||||||
💡 HINT: It may end up with multiple arguments!
|
|
||||||
|
|
||||||
-}
|
|
||||||
fetchFeed : Model -> FeedTab -> Int -> Cmd Msg
|
|
||||||
fetchFeed model feedTabs page =
|
|
||||||
let
|
let
|
||||||
username =
|
|
||||||
currentUsername model
|
|
||||||
|
|
||||||
maybeCred =
|
maybeCred =
|
||||||
Session.cred model.session
|
Session.cred session
|
||||||
|
|
||||||
( extraParamName, extraParamVal ) =
|
( extraParamName, extraParamVal ) =
|
||||||
case feedTabs of
|
case feedTabs of
|
||||||
@@ -138,7 +127,7 @@ fetchFeed model feedTabs page =
|
|||||||
|> HttpBuilder.withQueryParam extraParamName extraParamVal
|
|> HttpBuilder.withQueryParam extraParamName extraParamVal
|
||||||
|> Cred.addHeaderIfAvailable maybeCred
|
|> Cred.addHeaderIfAvailable maybeCred
|
||||||
|> PaginatedList.fromRequestBuilder articlesPerPage page
|
|> PaginatedList.fromRequestBuilder articlesPerPage page
|
||||||
|> Task.map (Feed.init model.session)
|
|> Task.map (Feed.init session)
|
||||||
|> Task.mapError (Tuple.pair username)
|
|> Task.mapError (Tuple.pair username)
|
||||||
|> Task.attempt CompletedFeedLoad
|
|> Task.attempt CompletedFeedLoad
|
||||||
|
|
||||||
@@ -351,12 +340,12 @@ update msg model =
|
|||||||
|
|
||||||
ClickedTab tab ->
|
ClickedTab tab ->
|
||||||
( { model | feedTab = tab }
|
( { model | feedTab = tab }
|
||||||
, fetchFeed model tab 1
|
, fetchFeed model.session (currentUsername model) tab 1
|
||||||
)
|
)
|
||||||
|
|
||||||
ClickedFeedPage page ->
|
ClickedFeedPage page ->
|
||||||
( { model | feedPage = page }
|
( { model | feedPage = page }
|
||||||
, fetchFeed model model.feedTab page
|
, fetchFeed model.session (currentUsername model) model.feedTab page
|
||||||
)
|
)
|
||||||
|
|
||||||
CompletedFollowChange (Ok newAuthor) ->
|
CompletedFollowChange (Ok newAuthor) ->
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ view : Model -> { title : String, content : Html Msg }
|
|||||||
view model =
|
view model =
|
||||||
let
|
let
|
||||||
form =
|
form =
|
||||||
viewForm model
|
viewForm (Session.cred model.session) model.form
|
||||||
in
|
in
|
||||||
{ title = "Settings"
|
{ title = "Settings"
|
||||||
, content =
|
, content =
|
||||||
@@ -124,16 +124,9 @@ view model =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| 👉 TODO refactor this to accept narrower types than the entire Model.
|
viewForm : Maybe Cred -> Form -> Html Msg
|
||||||
💡 HINT: It may end up with multiple arguments!
|
viewForm maybeCred form =
|
||||||
-}
|
case maybeCred of
|
||||||
viewForm : Model -> Html Msg
|
|
||||||
viewForm model =
|
|
||||||
let
|
|
||||||
form =
|
|
||||||
model.form
|
|
||||||
in
|
|
||||||
case Session.cred model.session of
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
text ""
|
text ""
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ view model =
|
|||||||
]
|
]
|
||||||
, Feed.viewArticles model.timeZone feed
|
, Feed.viewArticles model.timeZone feed
|
||||||
|> List.map (Html.map GotFeedMsg)
|
|> List.map (Html.map GotFeedMsg)
|
||||||
, [ viewPagination (Feed.articles feed) ]
|
, [ PaginatedList.view ClickedFeedPage (Feed.articles feed) ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -155,46 +155,6 @@ viewBanner =
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- 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
|
-- TABS
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ view model =
|
|||||||
[ [ viewTabs model.feedTab ]
|
[ [ viewTabs model.feedTab ]
|
||||||
, Feed.viewArticles model.timeZone feed
|
, Feed.viewArticles model.timeZone feed
|
||||||
|> List.map (Html.map GotFeedMsg)
|
|> List.map (Html.map GotFeedMsg)
|
||||||
, [ viewPagination (Feed.articles feed) ]
|
, [ PaginatedList.view ClickedFeedPage (Feed.articles feed) ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -246,46 +246,6 @@ 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
|
-- PAGE TITLE
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
module PaginatedList exposing (PaginatedList, fromList, fromRequestBuilder, map, page, total, values)
|
module PaginatedList exposing (PaginatedList, fromList, fromRequestBuilder, map, page, total, values, view)
|
||||||
|
|
||||||
|
import Article exposing (Article, Preview)
|
||||||
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)
|
||||||
@@ -87,3 +88,32 @@ fromRequestBuilder resultsPerPage pageNumber builder =
|
|||||||
|
|
||||||
|
|
||||||
-- VIEW
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
|
view : (Int -> msg) -> PaginatedList (Article Preview) -> Html msg
|
||||||
|
view toMsg list =
|
||||||
|
let
|
||||||
|
viewPageLink currentPage =
|
||||||
|
pageLink toMsg 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 -> msg) -> Int -> Bool -> Html msg
|
||||||
|
pageLink toMsg targetPage isActive =
|
||||||
|
li [ classList [ ( "page-item", True ), ( "active", isActive ) ] ]
|
||||||
|
[ a
|
||||||
|
[ class "page-link"
|
||||||
|
, onClick (toMsg targetPage)
|
||||||
|
|
||||||
|
-- The RealWorld CSS requires an href to work properly.
|
||||||
|
, href ""
|
||||||
|
]
|
||||||
|
[ text (String.fromInt targetPage) ]
|
||||||
|
]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -155,46 +155,27 @@ viewBanner =
|
|||||||
-- TABS
|
-- TABS
|
||||||
|
|
||||||
|
|
||||||
{-| TODO: Have viewTabs render all the tabs, using `activeTab` as the
|
|
||||||
single source of truth for their state.
|
|
||||||
|
|
||||||
The specification for how the tabs work is:
|
|
||||||
|
|
||||||
1. If the user is logged in, render `yourFeed` as the first tab. Examples:
|
|
||||||
|
|
||||||
"Your Feed" "Global Feed"
|
|
||||||
"Your Feed" "Global Feed" "#dragons"
|
|
||||||
|
|
||||||
2. If the user is NOT logged in, do not render `yourFeed` at all. Examples:
|
|
||||||
|
|
||||||
"Global Feed"
|
|
||||||
"Global Feed" "#dragons"
|
|
||||||
|
|
||||||
3. If the active tab is a `TagFeed`, render that tab last. Show the tag it contains with a "#" in front.
|
|
||||||
|
|
||||||
"Global Feed" "#dragons"
|
|
||||||
"Your Feed" "Global Feed" "#dragons"
|
|
||||||
|
|
||||||
3. If the active tab is NOT a `TagFeed`, do not render a tag tab at all.
|
|
||||||
|
|
||||||
"Your Feed" "Global Feed"
|
|
||||||
"Global Feed"
|
|
||||||
|
|
||||||
💡 HINT: The 4 declarations after `viewTabs` may be helpful!
|
|
||||||
|
|
||||||
-}
|
|
||||||
viewTabs : Bool -> FeedTab -> Html Msg
|
viewTabs : Bool -> FeedTab -> Html Msg
|
||||||
viewTabs isLoggedIn activeTab =
|
viewTabs isLoggedIn activeTab =
|
||||||
ul [ class "nav nav-pills outline-active" ] <|
|
ul [ class "nav nav-pills outline-active" ] <|
|
||||||
|
List.singleton <|
|
||||||
case activeTab of
|
case activeTab of
|
||||||
YourFeed ->
|
YourFeed ->
|
||||||
[]
|
tabBar [] yourFeed [ globalFeed ]
|
||||||
|
|
||||||
GlobalFeed ->
|
GlobalFeed ->
|
||||||
[]
|
if isLoggedIn then
|
||||||
|
tabBar [ yourFeed ] globalFeed []
|
||||||
|
|
||||||
|
else
|
||||||
|
tabBar [] globalFeed []
|
||||||
|
|
||||||
TagFeed tagName ->
|
TagFeed tagName ->
|
||||||
[]
|
if isLoggedIn then
|
||||||
|
tabBar [ yourFeed, globalFeed ] (tagFeed tagName) []
|
||||||
|
|
||||||
|
else
|
||||||
|
tabBar [ globalFeed ] (tagFeed tagName) []
|
||||||
|
|
||||||
|
|
||||||
tabBar :
|
tabBar :
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -206,17 +206,14 @@ toggleFollowButton txt extraClasses msgWhenClicked uname =
|
|||||||
|
|
||||||
decoder : Maybe Cred -> Decoder Author
|
decoder : Maybe Cred -> Decoder Author
|
||||||
decoder maybeCred =
|
decoder maybeCred =
|
||||||
{- 👉 TODO: Use this `Profile` and `Username` to decode an `Author`!
|
Decode.succeed Tuple.pair
|
||||||
|
|
||||||
💡 HINT: `decoderHelp` will help here, but slightly altering its type may make things easier...
|
|
||||||
-}
|
|
||||||
Decode.succeed "..."
|
|
||||||
|> custom Profile.decoder
|
|> custom Profile.decoder
|
||||||
|> required "username" Username.decoder
|
|> required "username" Username.decoder
|
||||||
|
|> Decode.andThen (decoderHelp maybeCred)
|
||||||
|
|
||||||
|
|
||||||
decoderHelp : Maybe Cred -> Profile -> Username -> Decoder Author
|
decoderHelp : Maybe Cred -> ( Profile, Username ) -> Decoder Author
|
||||||
decoderHelp maybeCred prof uname =
|
decoderHelp maybeCred ( prof, uname ) =
|
||||||
case maybeCred of
|
case maybeCred of
|
||||||
Nothing ->
|
Nothing ->
|
||||||
-- If you're logged out, you can't be following anyone!
|
-- If you're logged out, you can't be following anyone!
|
||||||
|
|||||||
@@ -24,17 +24,18 @@ view timeZone timestamp =
|
|||||||
-}
|
-}
|
||||||
iso8601Decoder : Decoder Time.Posix
|
iso8601Decoder : Decoder Time.Posix
|
||||||
iso8601Decoder =
|
iso8601Decoder =
|
||||||
{- 👉 TODO: Use the following function to decode this Time.Posix value:
|
Decode.string
|
||||||
|
|> Decode.andThen decoderHelp
|
||||||
|
|
||||||
|
|
||||||
Iso8601.toTime : String -> Result (List DeadEnd) Time.Posix
|
decoderHelp : String -> Decoder Time.Posix
|
||||||
|
decoderHelp str =
|
||||||
|
case Iso8601.toTime str of
|
||||||
|
Ok time ->
|
||||||
|
Decode.succeed time
|
||||||
|
|
||||||
|
Err _ ->
|
||||||
❕ NOTE: You can disregard the (List DeadEnd) here. No need to use it to complete this exercise!
|
Decode.fail ("Invalid ISO-8601 timestamp: " ++ str)
|
||||||
|
|
||||||
💡 HINT: Decode.andThen will be useful here.
|
|
||||||
-}
|
|
||||||
"..."
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -34,14 +34,11 @@ parser =
|
|||||||
, Parser.map Login (s "login")
|
, Parser.map Login (s "login")
|
||||||
, Parser.map Logout (s "logout")
|
, Parser.map Logout (s "logout")
|
||||||
, Parser.map Profile (s "profile" </> Username.urlParser)
|
, Parser.map Profile (s "profile" </> Username.urlParser)
|
||||||
|
, Parser.map Settings (s "settings")
|
||||||
-- 👉 TODO /settings → Settings
|
, Parser.map Register (s "settings")
|
||||||
-- 👉 TODO /register → Register
|
, Parser.map Article (s "article" </> Slug.urlParser)
|
||||||
-- 👉 TODO /article/[slug] → Article [slug]
|
, Parser.map EditArticle (s "editor" </> Slug.urlParser)
|
||||||
-- 👉 TODO /editor → NewArticle
|
, Parser.map NewArticle (s "editor")
|
||||||
-- 👉 TODO /editor/[slug] → EditArticle [slug]
|
|
||||||
--
|
|
||||||
-- 💡 HINT: Article and EditArticle work similarly to how Profile works.
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,25 +5,11 @@ import Html.Attributes exposing (..)
|
|||||||
|
|
||||||
|
|
||||||
banner =
|
banner =
|
||||||
{- 👉 TODO: Add a logo and tagline to this banner, so its structure becomes:
|
|
||||||
|
|
||||||
<div class="banner">
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<h1 class="logo-font">conduit</h1>
|
|
||||||
|
|
||||||
<p>A place to share your knowledge.</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
💡 HINT 1: the <div class="row"> above is an element with 2 child nodes.
|
|
||||||
|
|
||||||
💡 HINT 2: the <div class="feed-toggle"> below is an element with text.
|
|
||||||
-}
|
|
||||||
div [ class "banner" ]
|
div [ class "banner" ]
|
||||||
[ div [ class "container" ]
|
[ div [ class "container" ]
|
||||||
[ text "👉 TODO: Put the <h1> here instead of this text, then add the <p> right after the <h1>" ]
|
[ h1 [ class "logo-font" ] [ text "conduit" ]
|
||||||
|
, p [] [ text "A place to share your knowledge." ]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -33,7 +19,7 @@ feed =
|
|||||||
|
|
||||||
main =
|
main =
|
||||||
div [ class "home-page" ]
|
div [ class "home-page" ]
|
||||||
[ div [] [ text "👉 TODO: Replace this <div> with the banner" ]
|
[ banner
|
||||||
, div [ class "container page" ]
|
, div [ class "container page" ]
|
||||||
[ div [ class "row" ]
|
[ div [ class "row" ]
|
||||||
[ div [ class "col-md-9" ] [ feed ]
|
[ div [ class "col-md-9" ] [ feed ]
|
||||||
|
|||||||
@@ -7,18 +7,13 @@ import Html.Attributes exposing (..)
|
|||||||
viewTags tags =
|
viewTags tags =
|
||||||
let
|
let
|
||||||
renderedTags =
|
renderedTags =
|
||||||
-- 👉 TODO: use `List.map` and `viewTag` to render the tags
|
List.map viewTag tags
|
||||||
[]
|
|
||||||
in
|
in
|
||||||
div [ class "tag-list" ] renderedTags
|
div [ class "tag-list" ] renderedTags
|
||||||
|
|
||||||
|
|
||||||
viewTag tagName =
|
viewTag tagName =
|
||||||
{- 👉 TODO: render something like this:
|
button [ class "tag-pill tag-default" ] [ text tagName ]
|
||||||
|
|
||||||
<button class="tag-pill tag-default">tag name goes here</button>
|
|
||||||
-}
|
|
||||||
button [] []
|
|
||||||
|
|
||||||
|
|
||||||
main =
|
main =
|
||||||
@@ -34,9 +29,7 @@ main =
|
|||||||
, div [ class "col-md-3" ]
|
, div [ class "col-md-3" ]
|
||||||
[ div [ class "sidebar" ]
|
[ div [ class "sidebar" ]
|
||||||
[ p [] [ text "Popular Tags" ]
|
[ p [] [ text "Popular Tags" ]
|
||||||
|
, viewTags tags
|
||||||
-- 👉 TODO: instead of passing [] to viewTags, pass the actual tags
|
|
||||||
, viewTags []
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,15 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0"
|
"elm/html": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/json": "1.0.0",
|
"elm/json": "1.0.0",
|
||||||
"elm/time": "1.0.0",
|
"elm/time": "1.0.0",
|
||||||
"elm/url": "1.0.0"
|
"elm/url": "1.0.0",
|
||||||
|
"elm/virtual-dom": "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test-dependencies": {
|
"test-dependencies": {
|
||||||
|
|||||||
@@ -25,14 +25,10 @@ initialModel =
|
|||||||
|
|
||||||
|
|
||||||
update msg model =
|
update msg model =
|
||||||
{- 👉 TODO: If `msg.description` is "ClickedTag", then
|
if msg.description == "ClickedTag" then
|
||||||
set the model's `selectedTag` field to be `msg.data`
|
{ model | selectedTag = msg.data }
|
||||||
|
|
||||||
💡 HINT: record update syntax looks like this:
|
else
|
||||||
|
|
||||||
{ model | foo = bar }
|
|
||||||
|
|
||||||
-}
|
|
||||||
model
|
model
|
||||||
|
|
||||||
|
|
||||||
@@ -42,16 +38,8 @@ update msg model =
|
|||||||
|
|
||||||
view model =
|
view model =
|
||||||
let
|
let
|
||||||
{- 👉 TODO: Filter the articles down to only the ones
|
|
||||||
that include the currently selected tag.
|
|
||||||
|
|
||||||
💡 HINT: Replace `True` below with something involving
|
|
||||||
`List.member`, `article.tags`, and `model.selectedTag`
|
|
||||||
|
|
||||||
Docs for List.member: http://package.elm-lang.org/packages/elm-lang/core/latest/List#member
|
|
||||||
-}
|
|
||||||
articles =
|
articles =
|
||||||
List.filter (\article -> True)
|
List.filter (\article -> List.member model.selectedTag article.tags)
|
||||||
model.allArticles
|
model.allArticles
|
||||||
|
|
||||||
feed =
|
feed =
|
||||||
@@ -101,17 +89,7 @@ viewTag selectedTagName tagName =
|
|||||||
in
|
in
|
||||||
button
|
button
|
||||||
[ class ("tag-pill " ++ otherClass)
|
[ class ("tag-pill " ++ otherClass)
|
||||||
|
, onClick { description = "ClickedTag", data = tagName }
|
||||||
{- 👉 TODO: Add an `onClick` handler which sends a msg
|
|
||||||
that our `update` function above will use
|
|
||||||
to set the currently selected tag to `tagName`.
|
|
||||||
|
|
||||||
💡 HINT: It should look something like this:
|
|
||||||
|
|
||||||
, onClick { description = … , data = … }
|
|
||||||
|
|
||||||
👆 Don't forget to add a comma before `onClick`!
|
|
||||||
-}
|
|
||||||
]
|
]
|
||||||
[ text tagName ]
|
[ text tagName ]
|
||||||
|
|
||||||
|
|||||||
@@ -14,27 +14,20 @@ import Html.Events exposing (onClick)
|
|||||||
type alias Model =
|
type alias Model =
|
||||||
{ tags : List String
|
{ tags : List String
|
||||||
, selectedTag : String
|
, selectedTag : String
|
||||||
|
, allArticles : List Article
|
||||||
{- 👉 TODO: change this `allArticles` annotation to the following:
|
}
|
||||||
|
|
||||||
allArticles : List Article
|
|
||||||
|
|
||||||
|
|
||||||
💡 HINT: You'll need to move the existing annotation to a `type alias`.
|
type alias Article =
|
||||||
-}
|
|
||||||
, allArticles :
|
|
||||||
List
|
|
||||||
{ title : String
|
{ title : String
|
||||||
, description : String
|
, description : String
|
||||||
, body : String
|
, body : String
|
||||||
, tags : List String
|
, tags : List String
|
||||||
, slug : String
|
, slug : String
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| 👉 TODO: Replace this comment with a type annotation for `initialModel`
|
initialModel : Model
|
||||||
-}
|
|
||||||
initialModel =
|
initialModel =
|
||||||
{ tags = Article.tags
|
{ tags = Article.tags
|
||||||
, selectedTag = "elm"
|
, selectedTag = "elm"
|
||||||
@@ -52,8 +45,7 @@ type alias Msg =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| 👉 TODO: Replace this comment with a type annotation for `update`
|
update : Msg -> Model -> Model
|
||||||
-}
|
|
||||||
update msg model =
|
update msg model =
|
||||||
if msg.description == "ClickedTag" then
|
if msg.description == "ClickedTag" then
|
||||||
{ model | selectedTag = msg.data }
|
{ model | selectedTag = msg.data }
|
||||||
@@ -66,8 +58,7 @@ update msg model =
|
|||||||
-- VIEW
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
{-| 👉 TODO: Replace this comment with a type annotation for `view`
|
view : Model -> Html Msg
|
||||||
-}
|
|
||||||
view model =
|
view model =
|
||||||
let
|
let
|
||||||
articles =
|
articles =
|
||||||
@@ -93,8 +84,7 @@ view model =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
{-| 👉 TODO: Replace this comment with a type annotation for `view`
|
viewArticle : Article -> Html Msg
|
||||||
-}
|
|
||||||
viewArticle article =
|
viewArticle article =
|
||||||
div [ class "article-preview" ]
|
div [ class "article-preview" ]
|
||||||
[ h1 [] [ text article.title ]
|
[ h1 [] [ text article.title ]
|
||||||
@@ -103,8 +93,7 @@ viewArticle article =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
{-| 👉 TODO: Replace this comment with a type annotation for `viewBanner`
|
viewBanner : Html Msg
|
||||||
-}
|
|
||||||
viewBanner =
|
viewBanner =
|
||||||
div [ class "banner" ]
|
div [ class "banner" ]
|
||||||
[ div [ class "container" ]
|
[ div [ class "container" ]
|
||||||
@@ -114,8 +103,7 @@ viewBanner =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
{-| 👉 TODO: Replace this comment with a type annotation for `viewTag`
|
viewTag : String -> String -> Html Msg
|
||||||
-}
|
|
||||||
viewTag selectedTagName tagName =
|
viewTag selectedTagName tagName =
|
||||||
let
|
let
|
||||||
otherClass =
|
otherClass =
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -86,11 +86,7 @@ viewForm form =
|
|||||||
[ input
|
[ input
|
||||||
[ class "form-control form-control-lg"
|
[ class "form-control form-control-lg"
|
||||||
, placeholder "Username"
|
, placeholder "Username"
|
||||||
|
, onInput EnteredUsername
|
||||||
{- 👉 TODO: when the user inputs a username, update it in the Model.
|
|
||||||
|
|
||||||
💡 HINT: Look at how the Email input below does this. 👇
|
|
||||||
-}
|
|
||||||
, value form.username
|
, value form.username
|
||||||
]
|
]
|
||||||
[]
|
[]
|
||||||
@@ -141,6 +137,7 @@ type Msg
|
|||||||
= SubmittedForm
|
= SubmittedForm
|
||||||
| EnteredEmail String
|
| EnteredEmail String
|
||||||
| EnteredPassword String
|
| EnteredPassword String
|
||||||
|
| EnteredUsername String
|
||||||
| CompletedRegister (Result Http.Error Viewer)
|
| CompletedRegister (Result Http.Error Viewer)
|
||||||
| GotSession Session
|
| GotSession Session
|
||||||
|
|
||||||
@@ -154,6 +151,9 @@ update msg model =
|
|||||||
EnteredPassword password ->
|
EnteredPassword password ->
|
||||||
updateForm (\form -> { form | password = password }) model
|
updateForm (\form -> { form | password = password }) model
|
||||||
|
|
||||||
|
EnteredUsername username ->
|
||||||
|
updateForm (\form -> { form | username = username }) model
|
||||||
|
|
||||||
SubmittedForm ->
|
SubmittedForm ->
|
||||||
case validate model.form of
|
case validate model.form of
|
||||||
Ok validForm ->
|
Ok validForm ->
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -40,12 +40,12 @@ src (Avatar maybeUrl) =
|
|||||||
|
|
||||||
resolveAvatarUrl : Maybe String -> String
|
resolveAvatarUrl : Maybe String -> String
|
||||||
resolveAvatarUrl maybeUrl =
|
resolveAvatarUrl maybeUrl =
|
||||||
{- 👉 TODO #1 of 2: return the user's avatar from maybeUrl, if maybeUrl actually
|
case maybeUrl of
|
||||||
contains one. If maybeUrl is Nothing, return this URL instead:
|
Just url ->
|
||||||
|
url
|
||||||
|
|
||||||
https://static.productionready.io/images/smiley-cyrus.jpg
|
Nothing ->
|
||||||
-}
|
"https://static.productionready.io/images/smiley-cyrus.jpg"
|
||||||
""
|
|
||||||
|
|
||||||
|
|
||||||
encode : Avatar -> Value
|
encode : Avatar -> Value
|
||||||
|
|||||||
@@ -566,12 +566,13 @@ toTagList tagString =
|
|||||||
will result in an error! If it has been fixed, saving will work and the
|
will result in an error! If it has been fixed, saving will work and the
|
||||||
tags will be accepted.
|
tags will be accepted.
|
||||||
|
|
||||||
💡 HINT: Here's how to remove all the "foo" strings from a list of strings:
|
💡 HINT: Here's how to keep only the "foo" strings in a list of strings:
|
||||||
|
|
||||||
List.filter (\str -> str == "foo") listOfStrings
|
List.filter (\str -> str == "foo") listOfStrings
|
||||||
-}
|
-}
|
||||||
String.split " " tagString
|
String.split " " tagString
|
||||||
|> List.map String.trim
|
|> List.map String.trim
|
||||||
|
|> List.filter (\str -> str /= "")
|
||||||
|
|
||||||
|
|
||||||
edit : Slug -> TrimmedForm -> Cred -> Http.Request (Article Full)
|
edit : Slug -> TrimmedForm -> Cred -> Http.Request (Article Full)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -182,26 +182,12 @@ type alias Metadata =
|
|||||||
|
|
||||||
metadataDecoder : Decoder Metadata
|
metadataDecoder : Decoder Metadata
|
||||||
metadataDecoder =
|
metadataDecoder =
|
||||||
{- 👉 TODO: replace the calls to `hardcoded` with calls to `required`
|
|
||||||
in order to decode these fields:
|
|
||||||
|
|
||||||
--- "description" -------> description : String
|
|
||||||
--- "title" -------------> title : String
|
|
||||||
--- "tagList" -----------> tags : List String
|
|
||||||
--- "favorited" ---------> favorited : Bool
|
|
||||||
--- "favoritesCount" ----> favoritesCount : Int
|
|
||||||
|
|
||||||
Once this is done, the articles in the feed should look normal again.
|
|
||||||
|
|
||||||
💡 HINT: Order matters! These must be decoded in the same order
|
|
||||||
as the order of the fields in `type alias Metadata` above. ☝️
|
|
||||||
-}
|
|
||||||
Decode.succeed Metadata
|
Decode.succeed Metadata
|
||||||
|> hardcoded "(needs decoding!)"
|
|> required "description" string
|
||||||
|> hardcoded "(needs decoding!)"
|
|> required "title" string
|
||||||
|> hardcoded []
|
|> required "tagList" (list string)
|
||||||
|> hardcoded False
|
|> required "favorited" bool
|
||||||
|> hardcoded 0
|
|> required "favoritesCount" int
|
||||||
|> required "createdAt" Timestamp.iso8601Decoder
|
|> required "createdAt" Timestamp.iso8601Decoder
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"elm-version": "0.19.0",
|
"elm-version": "0.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
"elm/browser": "1.0.0",
|
"elm/browser": "1.0.0",
|
||||||
"elm/core": "1.0.0",
|
"elm/core": "1.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"lukewestby/elm-http-builder": "6.0.0",
|
"lukewestby/elm-http-builder": "6.0.0",
|
||||||
"rtfeldman/elm-iso8601": "1.0.1"
|
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/parser": "1.0.0",
|
"elm/parser": "1.0.0",
|
||||||
|
|||||||
@@ -155,34 +155,13 @@ update msg model =
|
|||||||
responseDecoder =
|
responseDecoder =
|
||||||
Decode.field "user" Viewer.decoder
|
Decode.field "user" Viewer.decoder
|
||||||
|
|
||||||
{- 👉 TODO: Create a Http.Request value that represents
|
|
||||||
a POST request to "/api/users"
|
|
||||||
|
|
||||||
💡 HINT 1: Documentation for `Http.post` is here:
|
|
||||||
|
|
||||||
http://package.elm-lang.org/packages/elm-lang/http/1.0.0/Http#post
|
|
||||||
|
|
||||||
💡 HINT 2: Look at the values defined above in this
|
|
||||||
let-expression. What are their types? What are the types the
|
|
||||||
`Http.post` function is looking for?
|
|
||||||
-}
|
|
||||||
request : Http.Request Viewer
|
request : Http.Request Viewer
|
||||||
request =
|
request =
|
||||||
Debug.todo "Call Http.post to represent a POST to /api/users/login"
|
Http.post "/api/users" requestBody responseDecoder
|
||||||
|
|
||||||
{- 👉 TODO: Use Http.send to turn the request we just defined
|
|
||||||
into a Cmd for `update` to execute.
|
|
||||||
|
|
||||||
💡 HINT 1: Documentation for `Http.send` is here:
|
|
||||||
|
|
||||||
http://package.elm-lang.org/packages/elm-lang/http/1.0.0/Http#send
|
|
||||||
|
|
||||||
💡 HINT 2: The `CompletedRegister` variant defined in `type Msg`
|
|
||||||
will be useful here!
|
|
||||||
-}
|
|
||||||
cmd : Cmd Msg
|
cmd : Cmd Msg
|
||||||
cmd =
|
cmd =
|
||||||
Cmd.none
|
Http.send CompletedRegister request
|
||||||
in
|
in
|
||||||
( { model | problems = [] }, cmd )
|
( { model | problems = [] }, cmd )
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ login newViewer =
|
|||||||
Viewer.encode newViewer
|
Viewer.encode newViewer
|
||||||
|> Encode.encode 0
|
|> Encode.encode 0
|
||||||
|> Just
|
|> Just
|
||||||
|> sendSessionToJavaScript
|
|> storeSession
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -81,24 +81,10 @@ login newViewer =
|
|||||||
|
|
||||||
logout : Cmd msg
|
logout : Cmd msg
|
||||||
logout =
|
logout =
|
||||||
sendSessionToJavaScript Nothing
|
storeSession Nothing
|
||||||
|
|
||||||
|
|
||||||
{-| 👉 TODO 1 of 2: Replace this do-nothing function with a port that sends the
|
port storeSession : Maybe String -> Cmd msg
|
||||||
authentication token to JavaScript.
|
|
||||||
|
|
||||||
💡 HINT 1: When you convert it to a port, the port's name _must_ match
|
|
||||||
the name JavaScript expects in `intro/server/public/index.html`.
|
|
||||||
That name is not `sendSessionToJavaScript`, so you will need to
|
|
||||||
rename it to match what JS expects!
|
|
||||||
|
|
||||||
💡 HINT 2: After you rename it, some code in this file will break because
|
|
||||||
it was depending on the old name. Follow the compiler errors to fix them!
|
|
||||||
|
|
||||||
-}
|
|
||||||
sendSessionToJavaScript : Maybe String -> Cmd msg
|
|
||||||
sendSessionToJavaScript maybeAuthenticationToken =
|
|
||||||
Cmd.none
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -107,24 +93,10 @@ sendSessionToJavaScript maybeAuthenticationToken =
|
|||||||
|
|
||||||
changes : (Session -> msg) -> Nav.Key -> Sub msg
|
changes : (Session -> msg) -> Nav.Key -> Sub msg
|
||||||
changes toMsg key =
|
changes toMsg key =
|
||||||
receiveSessionFromJavaScript (\val -> toMsg (decode key val))
|
onSessionChange (\val -> toMsg (decode key val))
|
||||||
|
|
||||||
|
|
||||||
{-| 👉 TODO 2 of 2: Replace this do-nothing function with a port that receives the
|
port onSessionChange : (Value -> msg) -> Sub msg
|
||||||
authentication token from JavaScript.
|
|
||||||
|
|
||||||
💡 HINT 1: When you convert it to a port, the port's name _must_ match
|
|
||||||
the name JavaScript expects in `intro/server/public/index.html`.
|
|
||||||
That name is not `receiveSessionFromJavaScript`, so you will need to
|
|
||||||
rename it to match what JS expects!
|
|
||||||
|
|
||||||
💡 HINT 2: After you rename it, some code in this file will break because
|
|
||||||
it was depending on the old name. Follow the compiler errors to fix them!
|
|
||||||
|
|
||||||
-}
|
|
||||||
receiveSessionFromJavaScript : (Value -> msg) -> Sub msg
|
|
||||||
receiveSessionFromJavaScript toMsg =
|
|
||||||
Sub.none
|
|
||||||
|
|
||||||
|
|
||||||
decode : Nav.Key -> Value -> Session
|
decode : Nav.Key -> Value -> Session
|
||||||
|
|||||||
Reference in New Issue
Block a user