Compare commits
37 Commits
solutions-
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6710d3bb72 | ||
|
|
dfe4cc02d9 | ||
|
|
331813a90b | ||
|
|
f6d6dc5ffc | ||
|
|
c1401c637f | ||
|
|
d5d25f90f6 | ||
|
|
98901d3743 | ||
|
|
c9287a157c | ||
|
|
b2089add0d | ||
|
|
04ed226507 | ||
|
|
da827954e5 | ||
|
|
965184ca09 | ||
|
|
1bcb581666 | ||
|
|
b5697d0765 | ||
|
|
e5124265f8 | ||
|
|
81b51ab973 | ||
|
|
3c6e9aeb07 | ||
|
|
6142e8d685 | ||
|
|
b2b9cc5b76 | ||
|
|
ab31fd08c0 | ||
|
|
3f9b3381c2 | ||
|
|
af7ed944f1 | ||
|
|
4035369af2 | ||
|
|
850d4e6ff0 | ||
|
|
5b9cdfb3b3 | ||
|
|
2bd5aa8141 | ||
|
|
0a9fadcf87 | ||
|
|
4fcaad042a | ||
|
|
bb38d331b4 | ||
|
|
0efe239ae4 | ||
|
|
69f3c478fb | ||
|
|
0288b3fcb7 | ||
|
|
33713b59b8 | ||
|
|
6c04669ec5 | ||
|
|
50aa6ee0d6 | ||
|
|
e7b7d0bdc3 | ||
|
|
9288011f22 |
10
README.md
10
README.md
@@ -1,4 +1,4 @@
|
||||
<i>This workshop is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. The `server/` directories use [`moleculer-node-realworld-example`](https://github.com/gothinkster/moleculer-node-realworld-example-app), which has its own license. The JavaScript interop example uses [`localForage`](https://github.com/localForage/localForage), which is (c) 2013-2017 Mozilla, under the Apache License 2.0. The rest of the code is a variation on [`elm-spa-example`](https://github.com/rtfeldman/elm-spa-example/), an [MIT-licensed](https://github.com/rtfeldman/elm-spa-example/blob/master/LICENSE) implementation of the [`realworld`](https://github.com/gothinkster/realworld) front-end. Many thanks to the authors of these projects!</i>
|
||||
<i>This workshop, as well as the slides that go with it (linked below), are all licensed under a <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. The `server/` directories use [`moleculer-node-realworld-example`](https://github.com/gothinkster/moleculer-node-realworld-example-app), which has its own license. The JavaScript interop example uses [`localForage`](https://github.com/localForage/localForage), which is (c) 2013-2017 Mozilla, under the Apache License 2.0. The rest of the code is a variation on [`elm-spa-example`](https://github.com/rtfeldman/elm-spa-example/), an [MIT-licensed](https://github.com/rtfeldman/elm-spa-example/blob/master/LICENSE) implementation of the [`realworld`](https://github.com/gothinkster/realworld) front-end. Many thanks to the authors of these projects!</i>
|
||||
|
||||
Getting Started
|
||||
===============
|
||||
@@ -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`).
|
||||
|
||||
```shell
|
||||
npm install -g elm elm-test@elm0.19.0 elm-format
|
||||
npm install -g elm elm-test elm-format
|
||||
```
|
||||
|
||||
5. Clone this repository
|
||||
@@ -29,9 +29,9 @@ 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!
|
||||
|
||||
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
|
||||
* [Introduction to Elm](https://frontendmasters.com/courses/intro-elm/) video course ([slides](https://docs.google.com/presentation/d/1LM_W2BRs_ItT-SPDe70C10cbwhGNHGQlJ1fVnAdnRIY/edit?usp=sharing)
|
||||
* [Advanced Elm](https://frontendmasters.com/courses/advanced-elm/) video course ([slides](https://docs.google.com/presentation/d/1aFZBXs9kzlZww2JN6iDmrYiQaxKlCAz6a5zpt882GHk/edit?usp=sharing))
|
||||
|
||||
@@ -15,7 +15,7 @@ some info about it that may be helpful in teaching this workshop.
|
||||
|
||||
## Slides
|
||||
|
||||
The [slides](https://docs.google.com/presentation/d/1sNx5k3_fHwJcgm9QEY1LsMH_TyF5SnnOSDKb8HvFsEU/edit?usp=sharing)
|
||||
The [slides](https://docs.google.com/presentation/d/1LM_W2BRs_ItT-SPDe70C10cbwhGNHGQlJ1fVnAdnRIY/edit?usp=sharing)
|
||||
I use with the workshop include speaker notes. If you’ve seen me give this
|
||||
workshop before, you may notice that I’m not following these notes very closely.
|
||||
|
||||
|
||||
@@ -43,7 +43,12 @@ If things aren’t working, the instructor will be happy to help!
|
||||
|
||||
## Links
|
||||
|
||||
* [Elm in Action](https://www.manning.com/books/elm-in-action?a_aid=elm_in_action&a_bid=b15edc5c), a book by [Richard Feldman](https://twitter.com/rtfeldman), creator of this workshop
|
||||
* [Official Elm Guide](https://guide.elm-lang.org/) by [Evan Czaplicki](https://twitter.com/czaplic), creator of Elm
|
||||
* [Elm Slack](http://elmlang.herokuapp.com/) - amazingly helpful chat community. People in [the `#beginners` channel](https://elmlang.slack.com/messages/C192T0Q1E/) are happy to answer questions!
|
||||
* [Elm Discourse](https://discourse.elm-lang.org/) - for longer-form discussions.
|
||||
* [The solutions to these exercises](https://github.com/rtfeldman/elm-0.19-workshop/tree/solutions/advanced)
|
||||
* [Slides for the Frontend Masters workshop that goes with this repo](https://docs.google.com/presentation/d/1aFZBXs9kzlZww2JN6iDmrYiQaxKlCAz6a5zpt882GHk/edit?usp=sharing)
|
||||
* [Advanced Elm Video Course](https://frontendmasters.com/courses/advanced-elm/) that goes with this repo
|
||||
* [The Life of a File](https://www.youtube.com/watch?v=XpDsk374LDE) - Evan Czaplicki
|
||||
* [The Importance of Ports](https://www.youtube.com/watch?v=P3pL85n9_5s) - Murphy Randle
|
||||
* [Working with Maybe](https://www.youtube.com/watch?v=43eM4kNbb6c) - Joël Quenneville
|
||||
* [Making Impossible States Impossible](https://www.youtube.com/watch?v=IcgmSRJHu_8) - Richard Feldman
|
||||
* [Scaling Elm Apps](https://www.youtube.com/watch?v=DoA4Txr4GUs) - Richard Feldman
|
||||
* [Make Data Structures](https://www.youtube.com/watch?v=x1FU3e0sT1I) - Richard Feldman
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
@@ -32,4 +32,4 @@
|
||||
"elm/random": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,17 +11,18 @@ import Username exposing (Username)
|
||||
-- TYPES
|
||||
|
||||
|
||||
type Cred
|
||||
= Cred Username String
|
||||
type alias Cred =
|
||||
{- 👉 TODO: Make Cred an opaque type, then fix the resulting compiler errors.
|
||||
Afterwards, it should no longer be possible for any other module to access
|
||||
this `token` value directly!
|
||||
|
||||
|
||||
|
||||
-- INFO
|
||||
|
||||
|
||||
username : Cred -> Username
|
||||
username (Cred uname _) =
|
||||
uname
|
||||
💡 HINT: Other modules still depend on being able to access the
|
||||
`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
|
||||
, token : String
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -40,14 +41,14 @@ decoder =
|
||||
|
||||
|
||||
encodeToken : Cred -> Value
|
||||
encodeToken (Cred _ token) =
|
||||
Encode.string token
|
||||
encodeToken cred =
|
||||
Encode.string cred.token
|
||||
|
||||
|
||||
addHeader : Cred -> RequestBuilder a -> RequestBuilder a
|
||||
addHeader (Cred _ token) builder =
|
||||
addHeader cred builder =
|
||||
builder
|
||||
|> withHeader "authorization" ("Token " ++ token)
|
||||
|> withHeader "authorization" ("Token " ++ cred.token)
|
||||
|
||||
|
||||
addHeaderIfAvailable : Maybe Cred -> RequestBuilder a -> RequestBuilder a
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
@@ -32,4 +32,4 @@
|
||||
"elm/random": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,8 +159,8 @@ slug (Article internals _) =
|
||||
|
||||
|
||||
body : Article Full -> Body
|
||||
body (Article _ (Full bod)) =
|
||||
bod
|
||||
body _ =
|
||||
"👉 TODO make this return the article's body"
|
||||
|
||||
|
||||
|
||||
@@ -181,8 +181,8 @@ mapAuthor transform (Article info extras) =
|
||||
|
||||
|
||||
fromPreview : Body -> Article Preview -> Article Full
|
||||
fromPreview bod (Article info _) =
|
||||
Article info (Full bod)
|
||||
fromPreview _ _ =
|
||||
"👉 TODO convert from an Article Preview to an Article Full"
|
||||
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ fullDecoder : Maybe Cred -> Decoder (Article Full)
|
||||
fullDecoder maybeCred =
|
||||
Decode.succeed Article
|
||||
|> custom (internalsDecoder maybeCred)
|
||||
|> required "body" (Decode.map Full Body.decoder)
|
||||
|> required "body" "👉 TODO use `Body.decoder` (which is a `Decoder Body`) to decode the body into this Article Full"
|
||||
|
||||
|
||||
internalsDecoder : Maybe Cred -> Decoder Internals
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
@@ -32,4 +32,4 @@
|
||||
"elm/random": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,16 +71,21 @@ init session slug =
|
||||
, article = Loading
|
||||
}
|
||||
, 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://package.elm-lang.org/packages/elm/core/latest/Task#attempt
|
||||
https://package.elm-lang.org/packages/elm/core/latest/Task#perform
|
||||
-}
|
||||
[ Article.fetch maybeCred slug
|
||||
|> Http.toTask
|
||||
|> Task.attempt CompletedLoadArticle
|
||||
, Comment.list maybeCred slug
|
||||
|> Http.toTask
|
||||
|> Task.attempt CompletedLoadComments
|
||||
, Time.here
|
||||
|> Task.perform GotTimeZone
|
||||
, Loading.slowThreshold
|
||||
|> Task.perform (\_ -> PassedSlowLoadThreshold)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
@@ -32,4 +32,4 @@
|
||||
"elm/random": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ view model =
|
||||
Loaded feed ->
|
||||
[ div [ class "feed-toggle" ] <|
|
||||
List.concat
|
||||
[ [ viewTabs model.session model.feedTab ]
|
||||
[ [ viewTabs model ]
|
||||
, Feed.viewArticles model.timeZone feed
|
||||
|> List.map (Html.map GotFeedMsg)
|
||||
, [ Feed.viewPagination ClickedFeedPage feed ]
|
||||
@@ -155,16 +155,21 @@ viewBanner =
|
||||
-- TABS
|
||||
|
||||
|
||||
viewTabs : Session -> FeedTab -> Html Msg
|
||||
viewTabs session feedTab =
|
||||
case feedTab of
|
||||
{-| 👉 TODO: refactor this to accept narrower types than the entire Model.
|
||||
|
||||
💡 HINT: It may end up with multiple arguments!
|
||||
|
||||
-}
|
||||
viewTabs : Model -> Html Msg
|
||||
viewTabs model =
|
||||
case model.feedTab of
|
||||
YourFeed cred ->
|
||||
Feed.viewTabs [] (yourFeed cred) [ globalFeed ]
|
||||
|
||||
GlobalFeed ->
|
||||
let
|
||||
otherTabs =
|
||||
case Session.cred session of
|
||||
case Session.cred model.session of
|
||||
Just cred ->
|
||||
[ yourFeed cred ]
|
||||
|
||||
@@ -176,7 +181,7 @@ viewTabs session feedTab =
|
||||
TagFeed tag ->
|
||||
let
|
||||
otherTabs =
|
||||
case Session.cred session of
|
||||
case Session.cred model.session of
|
||||
Just cred ->
|
||||
[ yourFeed cred, globalFeed ]
|
||||
|
||||
|
||||
@@ -61,21 +61,24 @@ init session username =
|
||||
let
|
||||
maybeCred =
|
||||
Session.cred session
|
||||
|
||||
model =
|
||||
{ session = session
|
||||
, timeZone = Time.utc
|
||||
, errors = []
|
||||
, feedTab = defaultFeedTab
|
||||
, feedPage = 1
|
||||
, author = Loading username
|
||||
, feed = Loading username
|
||||
}
|
||||
in
|
||||
( { session = session
|
||||
, timeZone = Time.utc
|
||||
, errors = []
|
||||
, feedTab = defaultFeedTab
|
||||
, feedPage = 1
|
||||
, author = Loading username
|
||||
, feed = Loading username
|
||||
}
|
||||
( model
|
||||
, Cmd.batch
|
||||
[ Author.fetch username maybeCred
|
||||
|> Http.toTask
|
||||
|> Task.mapError (Tuple.pair username)
|
||||
|> Task.attempt CompletedAuthorLoad
|
||||
, fetchFeed session username defaultFeedTab 1
|
||||
, fetchFeed model defaultFeedTab 1
|
||||
, Task.perform GotTimeZone Time.here
|
||||
, Task.perform (\_ -> PassedSlowLoadThreshold) Loading.slowThreshold
|
||||
]
|
||||
@@ -107,11 +110,19 @@ defaultFeedTab =
|
||||
-- HTTP
|
||||
|
||||
|
||||
fetchFeed : Session -> Username -> FeedTab -> Int -> Cmd Msg
|
||||
fetchFeed session username feedTabs page =
|
||||
{-| 👉 TODO: refactor this to accept narrower types than the entire Model.
|
||||
|
||||
💡 HINT: It may end up with multiple arguments!
|
||||
|
||||
-}
|
||||
fetchFeed : Model -> FeedTab -> Int -> Cmd Msg
|
||||
fetchFeed model feedTabs page =
|
||||
let
|
||||
username =
|
||||
currentUsername model
|
||||
|
||||
maybeCred =
|
||||
Session.cred session
|
||||
Session.cred model.session
|
||||
|
||||
( extraParamName, extraParamVal ) =
|
||||
case feedTabs of
|
||||
@@ -127,7 +138,7 @@ fetchFeed session username feedTabs page =
|
||||
|> HttpBuilder.withQueryParam extraParamName extraParamVal
|
||||
|> Cred.addHeaderIfAvailable maybeCred
|
||||
|> PaginatedList.fromRequestBuilder articlesPerPage page
|
||||
|> Task.map (Feed.init session)
|
||||
|> Task.map (Feed.init model.session)
|
||||
|> Task.mapError (Tuple.pair username)
|
||||
|> Task.attempt CompletedFeedLoad
|
||||
|
||||
@@ -340,12 +351,12 @@ update msg model =
|
||||
|
||||
ClickedTab tab ->
|
||||
( { model | feedTab = tab }
|
||||
, fetchFeed model.session (currentUsername model) tab 1
|
||||
, fetchFeed model tab 1
|
||||
)
|
||||
|
||||
ClickedFeedPage page ->
|
||||
( { model | feedPage = page }
|
||||
, fetchFeed model.session (currentUsername model) model.feedTab page
|
||||
, fetchFeed model model.feedTab page
|
||||
)
|
||||
|
||||
CompletedFollowChange (Ok newAuthor) ->
|
||||
|
||||
@@ -100,7 +100,7 @@ view : Model -> { title : String, content : Html Msg }
|
||||
view model =
|
||||
let
|
||||
form =
|
||||
viewForm (Session.cred model.session) model.form
|
||||
viewForm model
|
||||
in
|
||||
{ title = "Settings"
|
||||
, content =
|
||||
@@ -124,9 +124,16 @@ view model =
|
||||
}
|
||||
|
||||
|
||||
viewForm : Maybe Cred -> Form -> Html Msg
|
||||
viewForm maybeCred form =
|
||||
case maybeCred of
|
||||
{-| 👉 TODO refactor this to accept narrower types than the entire Model.
|
||||
💡 HINT: It may end up with multiple arguments!
|
||||
-}
|
||||
viewForm : Model -> Html Msg
|
||||
viewForm model =
|
||||
let
|
||||
form =
|
||||
model.form
|
||||
in
|
||||
case Session.cred model.session of
|
||||
Nothing ->
|
||||
text ""
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"summary": "helpful summary of your project, less than 80 characters",
|
||||
"repository": "https://github.com/user/project.git",
|
||||
"license": "BSD3",
|
||||
"source-directories": [
|
||||
"."
|
||||
],
|
||||
"exposed-modules": [],
|
||||
"dependencies": {
|
||||
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
||||
},
|
||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
@@ -32,4 +32,4 @@
|
||||
"elm/random": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ view model =
|
||||
]
|
||||
, Feed.viewArticles model.timeZone feed
|
||||
|> List.map (Html.map GotFeedMsg)
|
||||
, [ PaginatedList.view ClickedFeedPage (Feed.articles feed) ]
|
||||
, [ viewPagination (Feed.articles feed) ]
|
||||
]
|
||||
]
|
||||
|
||||
@@ -155,6 +155,46 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ view model =
|
||||
[ [ viewTabs model.feedTab ]
|
||||
, Feed.viewArticles model.timeZone feed
|
||||
|> List.map (Html.map GotFeedMsg)
|
||||
, [ PaginatedList.view ClickedFeedPage (Feed.articles feed) ]
|
||||
, [ viewPagination (Feed.articles feed) ]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -246,6 +246,46 @@ view model =
|
||||
|
||||
|
||||
|
||||
-- PAGINATION
|
||||
|
||||
|
||||
{-| 👉 TODO: Relocate `viewPagination` into `PaginatedList.view` and make it reusable,
|
||||
then refactor both Page.Home and Page.Profile to use it!
|
||||
|
||||
💡 HINT: Make `PaginatedList.view` return `Html msg` instead of `Html Msg`.
|
||||
(You'll need to introduce at least one extra argument for this to work.)
|
||||
|
||||
-}
|
||||
viewPagination : PaginatedList (Article Preview) -> Html Msg
|
||||
viewPagination list =
|
||||
let
|
||||
viewPageLink currentPage =
|
||||
pageLink currentPage (currentPage == page list)
|
||||
in
|
||||
if total list > 1 then
|
||||
List.range 1 (total list)
|
||||
|> List.map viewPageLink
|
||||
|> ul [ class "pagination" ]
|
||||
|
||||
else
|
||||
Html.text ""
|
||||
|
||||
|
||||
pageLink : Int -> Bool -> Html Msg
|
||||
pageLink targetPage isActive =
|
||||
li [ classList [ ( "page-item", True ), ( "active", isActive ) ] ]
|
||||
[ a
|
||||
[ class "page-link"
|
||||
, onClick (ClickedFeedPage targetPage)
|
||||
|
||||
-- The RealWorld CSS requires an href to work properly.
|
||||
, href ""
|
||||
]
|
||||
[ text (String.fromInt targetPage) ]
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- PAGE TITLE
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module PaginatedList exposing (PaginatedList, fromList, fromRequestBuilder, map, page, total, values, view)
|
||||
module PaginatedList exposing (PaginatedList, fromList, fromRequestBuilder, map, page, total, values)
|
||||
|
||||
import Article exposing (Article, Preview)
|
||||
import Html exposing (Html, a, li, text, ul)
|
||||
import Html.Attributes exposing (class, classList, href)
|
||||
import Html.Events exposing (onClick)
|
||||
@@ -88,32 +87,3 @@ fromRequestBuilder resultsPerPage pageNumber builder =
|
||||
|
||||
|
||||
-- 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) ]
|
||||
]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
@@ -32,4 +32,4 @@
|
||||
"elm/random": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,27 +155,46 @@ viewBanner =
|
||||
-- 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 isLoggedIn activeTab =
|
||||
ul [ class "nav nav-pills outline-active" ] <|
|
||||
List.singleton <|
|
||||
case activeTab of
|
||||
YourFeed ->
|
||||
tabBar [] yourFeed [ globalFeed ]
|
||||
case activeTab of
|
||||
YourFeed ->
|
||||
[]
|
||||
|
||||
GlobalFeed ->
|
||||
if isLoggedIn then
|
||||
tabBar [ yourFeed ] globalFeed []
|
||||
GlobalFeed ->
|
||||
[]
|
||||
|
||||
else
|
||||
tabBar [] globalFeed []
|
||||
|
||||
TagFeed tagName ->
|
||||
if isLoggedIn then
|
||||
tabBar [ yourFeed, globalFeed ] (tagFeed tagName) []
|
||||
|
||||
else
|
||||
tabBar [ globalFeed ] (tagFeed tagName) []
|
||||
TagFeed tagName ->
|
||||
[]
|
||||
|
||||
|
||||
tabBar :
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
@@ -32,4 +32,4 @@
|
||||
"elm/random": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,14 +206,17 @@ toggleFollowButton txt extraClasses msgWhenClicked uname =
|
||||
|
||||
decoder : Maybe Cred -> Decoder Author
|
||||
decoder maybeCred =
|
||||
Decode.succeed Tuple.pair
|
||||
{- 👉 TODO: Use this `Profile` and `Username` to decode an `Author`!
|
||||
|
||||
💡 HINT: `decoderHelp` will help here, but slightly altering its type may make things easier...
|
||||
-}
|
||||
Decode.succeed "..."
|
||||
|> custom Profile.decoder
|
||||
|> required "username" Username.decoder
|
||||
|> Decode.andThen (decoderHelp maybeCred)
|
||||
|
||||
|
||||
decoderHelp : Maybe Cred -> ( Profile, Username ) -> Decoder Author
|
||||
decoderHelp maybeCred ( prof, uname ) =
|
||||
decoderHelp : Maybe Cred -> Profile -> Username -> Decoder Author
|
||||
decoderHelp maybeCred prof uname =
|
||||
case maybeCred of
|
||||
Nothing ->
|
||||
-- If you're logged out, you can't be following anyone!
|
||||
|
||||
@@ -24,18 +24,17 @@ view timeZone timestamp =
|
||||
-}
|
||||
iso8601Decoder : Decoder Time.Posix
|
||||
iso8601Decoder =
|
||||
Decode.string
|
||||
|> Decode.andThen decoderHelp
|
||||
{- 👉 TODO: Use the following function to decode this Time.Posix value:
|
||||
|
||||
|
||||
decoderHelp : String -> Decoder Time.Posix
|
||||
decoderHelp str =
|
||||
case Iso8601.toTime str of
|
||||
Ok time ->
|
||||
Decode.succeed time
|
||||
Iso8601.toTime : String -> Result (List DeadEnd) Time.Posix
|
||||
|
||||
Err _ ->
|
||||
Decode.fail ("Invalid ISO-8601 timestamp: " ++ str)
|
||||
|
||||
❕ NOTE: You can disregard the (List DeadEnd) here. No need to use it to complete this exercise!
|
||||
|
||||
💡 HINT: Decode.andThen will be useful here.
|
||||
-}
|
||||
"..."
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"summary": "helpful summary of your project, less than 80 characters",
|
||||
"repository": "https://github.com/user/project.git",
|
||||
"license": "BSD3",
|
||||
"source-directories": [
|
||||
"."
|
||||
],
|
||||
"exposed-modules": [],
|
||||
"dependencies": {
|
||||
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
||||
},
|
||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
@@ -32,4 +32,4 @@
|
||||
"elm/random": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,14 @@ parser =
|
||||
, Parser.map Login (s "login")
|
||||
, Parser.map Logout (s "logout")
|
||||
, Parser.map Profile (s "profile" </> Username.urlParser)
|
||||
, Parser.map Settings (s "settings")
|
||||
, Parser.map Register (s "settings")
|
||||
, Parser.map Article (s "article" </> Slug.urlParser)
|
||||
, Parser.map EditArticle (s "editor" </> Slug.urlParser)
|
||||
, Parser.map NewArticle (s "editor")
|
||||
|
||||
-- 👉 TODO /settings → Settings
|
||||
-- 👉 TODO /register → Register
|
||||
-- 👉 TODO /article/[slug] → Article [slug]
|
||||
-- 👉 TODO /editor → NewArticle
|
||||
-- 👉 TODO /editor/[slug] → EditArticle [slug]
|
||||
--
|
||||
-- 💡 HINT: Article and EditArticle work similarly to how Profile works.
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1",
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0",
|
||||
"rtfeldman/elm-validate": "4.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
|
||||
3241
advanced/server/package-lock.json
generated
3241
advanced/server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"jsonwebtoken": "8.1.0",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"lodash": "4.17.4",
|
||||
"moleculer": "^0.11.0",
|
||||
"moleculer-db": "0.7.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Introduction to Elm Workshop
|
||||
============================
|
||||
|
||||
If you haven't already, follow the [Getting Started instructions](https://github.com/rtfeldman/elm-0.19-workshop/blob/master/intro/README.md
|
||||
If you haven't already, follow the [Getting Started instructions](https://github.com/rtfeldman/elm-0.19-workshop#getting-started
|
||||
) at the root of this repository, then continue here!
|
||||
|
||||
## Start the server
|
||||
@@ -24,7 +24,7 @@ of the workshop, so **don't close it** until the workshop is over!
|
||||
|
||||
Leave the existing terminal running, and open a **second** terminal.
|
||||
|
||||
In the new termnal, `cd` into the `elm-0.19-workshop/intro/server/` directory again.
|
||||
In the new terminal, `cd` into the `elm-0.19-workshop/intro/server/` directory again.
|
||||
|
||||
Then run this to build the Elm code for the first time:
|
||||
|
||||
@@ -43,6 +43,9 @@ If things aren’t working, the instructor will be happy to help!
|
||||
|
||||
## Links
|
||||
|
||||
* [The solutions to these exercises](https://github.com/rtfeldman/elm-0.19-workshop/tree/solutions/intro)
|
||||
* [Slides for the Frontend Masters video course that goes with this repo](https://docs.google.com/presentation/d/1LM_W2BRs_ItT-SPDe70C10cbwhGNHGQlJ1fVnAdnRIY/edit?usp=sharing)
|
||||
* [Introduction to Elm Video Course](https://frontendmasters.com/courses/intro-elm/) that goes along with this repo.
|
||||
* [Elm in Action](https://www.manning.com/books/elm-in-action?a_aid=elm_in_action&a_bid=b15edc5c), a book by [Richard Feldman](https://twitter.com/rtfeldman), creator of this workshop
|
||||
* [Official Elm Guide](https://guide.elm-lang.org/) by [Evan Czaplicki](https://twitter.com/czaplic), creator of Elm
|
||||
* [Elm Slack](http://elmlang.herokuapp.com/) - amazingly helpful chat community. People in [the `#beginners` channel](https://elmlang.slack.com/messages/C192T0Q1E/) are happy to answer questions!
|
||||
|
||||
@@ -5,11 +5,25 @@ import Html.Attributes exposing (..)
|
||||
|
||||
|
||||
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="container"> 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 "container" ]
|
||||
[ h1 [ class "logo-font" ] [ text "conduit" ]
|
||||
, p [] [ text "A place to share your knowledge." ]
|
||||
]
|
||||
[ text "👉 TODO: Put the <h1> here instead of this text, then add the <p> right after the <h1>" ]
|
||||
]
|
||||
|
||||
|
||||
@@ -19,7 +33,7 @@ feed =
|
||||
|
||||
main =
|
||||
div [ class "home-page" ]
|
||||
[ banner
|
||||
[ div [] [ text "👉 TODO: Replace this <div> with the banner" ]
|
||||
, div [ class "container page" ]
|
||||
[ div [ class "row" ]
|
||||
[ div [ class "col-md-9" ] [ feed ]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"."
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"elm/core": "1.0.0",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"elm/core": "1.0.0",
|
||||
|
||||
@@ -7,13 +7,18 @@ import Html.Attributes exposing (..)
|
||||
viewTags tags =
|
||||
let
|
||||
renderedTags =
|
||||
List.map viewTag tags
|
||||
-- 👉 TODO: use `List.map` and `viewTag` to render the tags
|
||||
[]
|
||||
in
|
||||
div [ class "tag-list" ] renderedTags
|
||||
|
||||
|
||||
viewTag tagName =
|
||||
button [ class "tag-pill tag-default" ] [ text tagName ]
|
||||
{- 👉 TODO: render something like this:
|
||||
|
||||
<button class="tag-pill tag-default">tag name goes here</button>
|
||||
-}
|
||||
button [] []
|
||||
|
||||
|
||||
main =
|
||||
@@ -29,7 +34,9 @@ main =
|
||||
, div [ class "col-md-3" ]
|
||||
[ div [ class "sidebar" ]
|
||||
[ p [] [ text "Popular Tags" ]
|
||||
, viewTags tags
|
||||
|
||||
-- 👉 TODO: instead of passing [] to viewTags, pass the actual tags
|
||||
, viewTags []
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
@@ -3,18 +3,16 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/json": "1.0.0",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.0"
|
||||
"elm/url": "1.0.0"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
module Main exposing (main)
|
||||
|
||||
-- NOTE: Make sure to follow the instructions in the README for part3
|
||||
-- to install the elm/browser package before running elm make!
|
||||
--
|
||||
-- FYI: 👇 You can see our new `Article` module in `src/Article.elm`
|
||||
|
||||
import Article
|
||||
@@ -25,11 +28,15 @@ initialModel =
|
||||
|
||||
|
||||
update msg model =
|
||||
if msg.description == "ClickedTag" then
|
||||
{ model | selectedTag = msg.data }
|
||||
{- 👉 TODO: If `msg.description` is "ClickedTag", then
|
||||
set the model's `selectedTag` field to be `msg.data`
|
||||
|
||||
else
|
||||
model
|
||||
💡 HINT: record update syntax looks like this:
|
||||
|
||||
{ model | foo = bar }
|
||||
|
||||
-}
|
||||
model
|
||||
|
||||
|
||||
|
||||
@@ -38,8 +45,16 @@ update msg model =
|
||||
|
||||
view model =
|
||||
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 =
|
||||
List.filter (\article -> List.member model.selectedTag article.tags)
|
||||
List.filter (\article -> True)
|
||||
model.allArticles
|
||||
|
||||
feed =
|
||||
@@ -89,7 +104,17 @@ viewTag selectedTagName tagName =
|
||||
in
|
||||
button
|
||||
[ 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 ]
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"summary": "helpful summary of your project, less than 80 characters",
|
||||
"repository": "https://github.com/user/project.git",
|
||||
"license": "BSD3",
|
||||
"source-directories": [
|
||||
"."
|
||||
],
|
||||
"exposed-modules": [],
|
||||
"dependencies": {
|
||||
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
||||
},
|
||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"elm/browser": "1.0.0",
|
||||
@@ -21,4 +21,4 @@
|
||||
"direct": {},
|
||||
"indirect": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,20 +14,27 @@ import Html.Events exposing (onClick)
|
||||
type alias Model =
|
||||
{ tags : List 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`.
|
||||
-}
|
||||
, allArticles :
|
||||
List
|
||||
{ title : String
|
||||
, description : String
|
||||
, body : String
|
||||
, tags : List String
|
||||
, slug : String
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type alias Article =
|
||||
{ title : String
|
||||
, description : String
|
||||
, body : String
|
||||
, tags : List String
|
||||
, slug : String
|
||||
}
|
||||
|
||||
|
||||
initialModel : Model
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `initialModel`
|
||||
-}
|
||||
initialModel =
|
||||
{ tags = Article.tags
|
||||
, selectedTag = "elm"
|
||||
@@ -45,7 +52,8 @@ type alias Msg =
|
||||
}
|
||||
|
||||
|
||||
update : Msg -> Model -> Model
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `update`
|
||||
-}
|
||||
update msg model =
|
||||
if msg.description == "ClickedTag" then
|
||||
{ model | selectedTag = msg.data }
|
||||
@@ -58,7 +66,8 @@ update msg model =
|
||||
-- VIEW
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `view`
|
||||
-}
|
||||
view model =
|
||||
let
|
||||
articles =
|
||||
@@ -84,7 +93,8 @@ view model =
|
||||
]
|
||||
|
||||
|
||||
viewArticle : Article -> Html Msg
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `viewArticle`
|
||||
-}
|
||||
viewArticle article =
|
||||
div [ class "article-preview" ]
|
||||
[ h1 [] [ text article.title ]
|
||||
@@ -93,7 +103,8 @@ viewArticle article =
|
||||
]
|
||||
|
||||
|
||||
viewBanner : Html Msg
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `viewBanner`
|
||||
-}
|
||||
viewBanner =
|
||||
div [ class "banner" ]
|
||||
[ div [ class "container" ]
|
||||
@@ -103,7 +114,8 @@ viewBanner =
|
||||
]
|
||||
|
||||
|
||||
viewTag : String -> String -> Html Msg
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `viewTag`
|
||||
-}
|
||||
viewTag selectedTagName tagName =
|
||||
let
|
||||
otherClass =
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
@@ -32,4 +32,4 @@
|
||||
"elm/random": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,11 @@ viewForm form =
|
||||
[ input
|
||||
[ class "form-control form-control-lg"
|
||||
, 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
|
||||
]
|
||||
[]
|
||||
@@ -137,7 +141,6 @@ type Msg
|
||||
= SubmittedForm
|
||||
| EnteredEmail String
|
||||
| EnteredPassword String
|
||||
| EnteredUsername String
|
||||
| CompletedRegister (Result Http.Error Viewer)
|
||||
| GotSession Session
|
||||
|
||||
@@ -151,9 +154,6 @@ update msg model =
|
||||
EnteredPassword password ->
|
||||
updateForm (\form -> { form | password = password }) model
|
||||
|
||||
EnteredUsername username ->
|
||||
updateForm (\form -> { form | username = username }) model
|
||||
|
||||
SubmittedForm ->
|
||||
case validate model.form of
|
||||
Ok validForm ->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
|
||||
@@ -40,12 +40,12 @@ src (Avatar maybeUrl) =
|
||||
|
||||
resolveAvatarUrl : Maybe String -> String
|
||||
resolveAvatarUrl maybeUrl =
|
||||
case maybeUrl of
|
||||
Just url ->
|
||||
url
|
||||
{- 👉 TODO #1 of 2: return the user's avatar from maybeUrl, if maybeUrl actually
|
||||
contains one. If maybeUrl is Nothing, return this URL instead:
|
||||
|
||||
Nothing ->
|
||||
"https://static.productionready.io/images/smiley-cyrus.jpg"
|
||||
https://static.productionready.io/images/smiley-cyrus.jpg
|
||||
-}
|
||||
""
|
||||
|
||||
|
||||
encode : Avatar -> Value
|
||||
|
||||
@@ -572,7 +572,6 @@ toTagList tagString =
|
||||
-}
|
||||
String.split " " tagString
|
||||
|> List.map String.trim
|
||||
|> List.filter (\str -> str /= "")
|
||||
|
||||
|
||||
edit : Slug -> TrimmedForm -> Cred -> Http.Request (Article Full)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
|
||||
@@ -182,12 +182,26 @@ type alias Metadata =
|
||||
|
||||
metadataDecoder : Decoder Metadata
|
||||
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
|
||||
|> required "description" string
|
||||
|> required "title" string
|
||||
|> required "tagList" (list string)
|
||||
|> required "favorited" bool
|
||||
|> required "favoritesCount" int
|
||||
|> hardcoded "(needs decoding!)"
|
||||
|> hardcoded "(needs decoding!)"
|
||||
|> hardcoded []
|
||||
|> hardcoded False
|
||||
|> hardcoded 0
|
||||
|> required "createdAt" Timestamp.iso8601Decoder
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@ Then open `http://localhost:3000` in your browser.
|
||||
|
||||
## Exercise
|
||||
|
||||
We need to make login work. Currently it doesn't actually send a HTTP request to the server.
|
||||
We need to make signup work again. Currently it doesn't actually send a HTTP request to the server.
|
||||
|
||||
We'll fix this by editing `src/Page/Login.elm` and resolving the TODOs there.
|
||||
We'll fix this by editing `src/Page/Register.elm` and resolving the TODOs there.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
|
||||
@@ -155,13 +155,34 @@ update msg model =
|
||||
responseDecoder =
|
||||
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.post "/api/users" requestBody responseDecoder
|
||||
Debug.todo "Call Http.post to represent a POST to /api/users"
|
||||
|
||||
{- 👉 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 =
|
||||
Http.send CompletedRegister request
|
||||
Cmd.none
|
||||
in
|
||||
( { model | problems = [] }, cmd )
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Part 8
|
||||
# Part 9
|
||||
|
||||
Once again, we'll be building `src/Main.elm`, but editing a different file.
|
||||
|
||||
To build everything, `cd` into the `part8/` directory and run8
|
||||
To build everything, `cd` into the `part9/` directory and run:
|
||||
|
||||
```shell
|
||||
elm make src/Main.elm --output ../server/public/elm.js
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -40,7 +40,7 @@ import Html.Attributes exposing (class)
|
||||
import Html.Events exposing (stopPropagationOn)
|
||||
import Http
|
||||
import HttpBuilder exposing (RequestBuilder, withBody, withExpect, withQueryParams)
|
||||
import Json.Decode as Decode exposing (Decoder, bool, int, list, string)
|
||||
import Json.Decode as Decode exposing (Decoder)
|
||||
import Json.Decode.Pipeline exposing (custom, hardcoded, required)
|
||||
import Json.Encode as Encode
|
||||
import Markdown
|
||||
@@ -83,6 +83,47 @@ type Article a
|
||||
= Article Internals a
|
||||
|
||||
|
||||
{-| Metadata about the article - its title, description, and so on.
|
||||
|
||||
Importantly, this module's public API exposes a way to read this metadata, but
|
||||
not to alter it. This is read-only information!
|
||||
|
||||
If we find ourselves using any particular piece of metadata often,
|
||||
for example `title`, we could expose a convenience function like this:
|
||||
|
||||
Article.title : Article a -> String
|
||||
|
||||
If you like, it's totally reasonable to expose a function like that for every one
|
||||
of these fields!
|
||||
|
||||
(Okay, to be completely honest, exposing one function per field is how I prefer
|
||||
to do it, and that's how I originally wrote this module. However, I'm aware that
|
||||
this code base has become a common reference point for beginners, and I think it
|
||||
is _extremely important_ that slapping some "getters and setters" on a record
|
||||
does not become a habit for anyone who is getting started with Elm. The whole
|
||||
point of making the Article type opaque is to create guarantees through
|
||||
_selectively choosing boundaries_ around it. If you aren't selective about
|
||||
where those boundaries are, and instead expose a "getter and setter" for every
|
||||
field in the record, the result is an API with no more guarantees than if you'd
|
||||
exposed the entire record directly! It is so important to me that beginners not
|
||||
fall into the terrible "getters and setters" trap that I've exposed this
|
||||
Metadata record instead of exposing a single function for each of its fields,
|
||||
as I did originally. This record is not a bad way to do it, by any means,
|
||||
but if this seems at odds with <https://youtu.be/x1FU3e0sT1I> - now you know why!
|
||||
See commit c2640ae3abd60262cdaafe6adee3f41d84cd85c3 for how it looked before.
|
||||
)
|
||||
|
||||
-}
|
||||
type alias Metadata =
|
||||
{ description : String
|
||||
, title : String
|
||||
, tags : List String
|
||||
, createdAt : Time.Posix
|
||||
, favorited : Bool
|
||||
, favoritesCount : Int
|
||||
}
|
||||
|
||||
|
||||
type alias Internals =
|
||||
{ slug : Slug
|
||||
, author : Author
|
||||
@@ -170,39 +211,15 @@ internalsDecoder maybeCred =
|
||||
|> custom metadataDecoder
|
||||
|
||||
|
||||
type alias Metadata =
|
||||
{ description : String
|
||||
, title : String
|
||||
, tags : List String
|
||||
, favorited : Bool
|
||||
, favoritesCount : Int
|
||||
, createdAt : Time.Posix
|
||||
}
|
||||
|
||||
|
||||
metadataDecoder : Decoder Metadata
|
||||
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
|
||||
|> hardcoded "(needs decoding!)"
|
||||
|> hardcoded "(needs decoding!)"
|
||||
|> hardcoded []
|
||||
|> hardcoded False
|
||||
|> hardcoded 0
|
||||
|> required "description" (Decode.map (Maybe.withDefault "") (Decode.nullable Decode.string))
|
||||
|> required "title" Decode.string
|
||||
|> required "tagList" (Decode.list Decode.string)
|
||||
|> required "createdAt" Timestamp.iso8601Decoder
|
||||
|> required "favorited" Decode.bool
|
||||
|> required "favoritesCount" Decode.int
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ login newViewer =
|
||||
Viewer.encode newViewer
|
||||
|> Encode.encode 0
|
||||
|> Just
|
||||
|> storeSession
|
||||
|> sendSessionToJavaScript
|
||||
|
||||
|
||||
|
||||
@@ -81,10 +81,24 @@ login newViewer =
|
||||
|
||||
logout : Cmd msg
|
||||
logout =
|
||||
storeSession Nothing
|
||||
sendSessionToJavaScript Nothing
|
||||
|
||||
|
||||
port storeSession : Maybe String -> Cmd msg
|
||||
{-| 👉 TODO 1 of 2: Replace this do-nothing function with a port that sends the
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -93,10 +107,24 @@ port storeSession : Maybe String -> Cmd msg
|
||||
|
||||
changes : (Session -> msg) -> Nav.Key -> Sub msg
|
||||
changes toMsg key =
|
||||
onSessionChange (\val -> toMsg (decode key val))
|
||||
receiveSessionFromJavaScript (\val -> toMsg (decode key val))
|
||||
|
||||
|
||||
port onSessionChange : (Value -> msg) -> Sub msg
|
||||
{-| 👉 TODO 2 of 2: Replace this do-nothing function with a port that receives the
|
||||
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
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1",
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0",
|
||||
"rtfeldman/elm-validate": "4.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
|
||||
3134
intro/server/package-lock.json
generated
3134
intro/server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user