Compare commits

..

21 Commits

Author SHA1 Message Date
Richard Feldman
66094700c4 Update README.md 2019-06-29 09:12:40 +02:00
Richard Feldman
d31b9dd661 Fix HINT for filter 2019-06-29 09:10:51 +02:00
Richard Feldman
b9e3cb8119 Update for public 0.19 release 2018-08-21 22:45:47 -04:00
Richard Feldman
354a707119 Solution for advanced/part8 2018-08-14 03:49:06 -04:00
Richard Feldman
3ec84cb051 Solution for advanced/part7 2018-08-14 03:48:43 -04:00
Richard Feldman
f744cad4f9 Solution for advanced/part6 2018-08-14 03:48:43 -04:00
Richard Feldman
46870e98bf Solution for advanced/part5 2018-08-14 03:48:43 -04:00
Richard Feldman
176b28ae6b Solution for advanced/part4 2018-08-14 03:48:43 -04:00
Richard Feldman
75a0c00828 Solution for advanced/part3 2018-08-14 03:48:43 -04:00
Richard Feldman
90db6e81b0 Solution for advanced/part2 2018-08-14 03:48:43 -04:00
Richard Feldman
9edc567a13 Solution for advanced/part1 2018-08-14 03:48:43 -04:00
Richard Feldman
34d3b7e6ec Solution for intro/part9 2018-08-14 03:48:43 -04:00
Richard Feldman
acea5cefa0 Solution for intro/part8 2018-08-14 03:48:43 -04:00
Richard Feldman
34b7e6e52e Solution for intro/part7 2018-08-14 03:48:43 -04:00
Richard Feldman
a28ef48931 Solution for intro/part6 2018-08-14 03:48:43 -04:00
Richard Feldman
633a1efbd9 Solution for intro/part5 2018-08-14 03:48:43 -04:00
Richard Feldman
412ddaa55f Solution to intro/part4 2018-08-14 03:48:43 -04:00
Richard Feldman
3e6c780a93 Solution to intro/part3 2018-08-14 03:48:43 -04:00
Richard Feldman
a329bd2f40 Solution to intro/part2 2018-08-14 03:48:43 -04:00
Richard Feldman
aed9d1aa5c Solution to intro/part1 2018-08-14 03:48:43 -04:00
Richard Feldman
da5af433f6 Update intro/part3 2018-08-14 03:48:42 -04:00
54 changed files with 3396 additions and 3619 deletions

View File

@@ -1,4 +1,4 @@
<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>
<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>
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 elm-format
npm install -g elm elm-test@elm0.19.0 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 ([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))
* [Introduction to Elm](https://frontendmasters.com/courses/intro-elm/) video course
* [Advanced Elm](https://frontendmasters.com/courses/advanced-elm/) video course

View File

@@ -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/1LM_W2BRs_ItT-SPDe70C10cbwhGNHGQlJ1fVnAdnRIY/edit?usp=sharing)
The [slides](https://docs.google.com/presentation/d/1sNx5k3_fHwJcgm9QEY1LsMH_TyF5SnnOSDKb8HvFsEU/edit?usp=sharing)
I use with the workshop include speaker notes. If youve seen me give this
workshop before, you may notice that Im not following these notes very closely.

View File

@@ -43,12 +43,7 @@ If things arent working, the instructor will be happy to help!
## Links
* [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
* [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.

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -11,18 +11,17 @@ import Username exposing (Username)
-- TYPES
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!
type Cred
= Cred Username String
💡 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
}
-- INFO
username : Cred -> Username
username (Cred uname _) =
uname
@@ -41,14 +40,14 @@ decoder =
encodeToken : Cred -> Value
encodeToken cred =
Encode.string cred.token
encodeToken (Cred _ token) =
Encode.string token
addHeader : Cred -> RequestBuilder a -> RequestBuilder a
addHeader cred builder =
addHeader (Cred _ token) builder =
builder
|> withHeader "authorization" ("Token " ++ cred.token)
|> withHeader "authorization" ("Token " ++ token)
addHeaderIfAvailable : Maybe Cred -> RequestBuilder a -> RequestBuilder a

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -159,8 +159,8 @@ slug (Article internals _) =
body : Article Full -> Body
body _ =
"👉 TODO make this return the article's body"
body (Article _ (Full bod)) =
bod
@@ -181,8 +181,8 @@ mapAuthor transform (Article info extras) =
fromPreview : Body -> Article Preview -> Article Full
fromPreview _ _ =
"👉 TODO convert from an Article Preview to an Article Full"
fromPreview bod (Article info _) =
Article info (Full bod)
@@ -200,7 +200,7 @@ fullDecoder : Maybe Cred -> Decoder (Article Full)
fullDecoder maybeCred =
Decode.succeed Article
|> 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

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -71,21 +71,16 @@ 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)
]
)

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -103,7 +103,7 @@ view model =
Loaded feed ->
[ div [ class "feed-toggle" ] <|
List.concat
[ [ viewTabs model ]
[ [ viewTabs model.session model.feedTab ]
, Feed.viewArticles model.timeZone feed
|> List.map (Html.map GotFeedMsg)
, [ Feed.viewPagination ClickedFeedPage feed ]
@@ -155,21 +155,16 @@ viewBanner =
-- TABS
{-| 👉 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
viewTabs : Session -> FeedTab -> Html Msg
viewTabs session feedTab =
case feedTab of
YourFeed cred ->
Feed.viewTabs [] (yourFeed cred) [ globalFeed ]
GlobalFeed ->
let
otherTabs =
case Session.cred model.session of
case Session.cred session of
Just cred ->
[ yourFeed cred ]
@@ -181,7 +176,7 @@ viewTabs model =
TagFeed tag ->
let
otherTabs =
case Session.cred model.session of
case Session.cred session of
Just cred ->
[ yourFeed cred, globalFeed ]

View File

@@ -61,9 +61,8 @@ init session username =
let
maybeCred =
Session.cred session
model =
{ session = session
in
( { session = session
, timeZone = Time.utc
, errors = []
, feedTab = defaultFeedTab
@@ -71,14 +70,12 @@ init session username =
, author = Loading username
, feed = Loading username
}
in
( model
, Cmd.batch
[ Author.fetch username maybeCred
|> Http.toTask
|> Task.mapError (Tuple.pair username)
|> Task.attempt CompletedAuthorLoad
, fetchFeed model defaultFeedTab 1
, fetchFeed session username defaultFeedTab 1
, Task.perform GotTimeZone Time.here
, Task.perform (\_ -> PassedSlowLoadThreshold) Loading.slowThreshold
]
@@ -110,19 +107,11 @@ defaultFeedTab =
-- HTTP
{-| 👉 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 =
fetchFeed : Session -> Username -> FeedTab -> Int -> Cmd Msg
fetchFeed session username feedTabs page =
let
username =
currentUsername model
maybeCred =
Session.cred model.session
Session.cred session
( extraParamName, extraParamVal ) =
case feedTabs of
@@ -138,7 +127,7 @@ fetchFeed model feedTabs page =
|> HttpBuilder.withQueryParam extraParamName extraParamVal
|> Cred.addHeaderIfAvailable maybeCred
|> PaginatedList.fromRequestBuilder articlesPerPage page
|> Task.map (Feed.init model.session)
|> Task.map (Feed.init session)
|> Task.mapError (Tuple.pair username)
|> Task.attempt CompletedFeedLoad
@@ -351,12 +340,12 @@ update msg model =
ClickedTab tab ->
( { model | feedTab = tab }
, fetchFeed model tab 1
, fetchFeed model.session (currentUsername model) tab 1
)
ClickedFeedPage page ->
( { model | feedPage = page }
, fetchFeed model model.feedTab page
, fetchFeed model.session (currentUsername model) model.feedTab page
)
CompletedFollowChange (Ok newAuthor) ->

View File

@@ -100,7 +100,7 @@ view : Model -> { title : String, content : Html Msg }
view model =
let
form =
viewForm model
viewForm (Session.cred model.session) model.form
in
{ title = "Settings"
, content =
@@ -124,16 +124,9 @@ view model =
}
{-| 👉 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
viewForm : Maybe Cred -> Form -> Html Msg
viewForm maybeCred form =
case maybeCred of
Nothing ->
text ""

View File

@@ -0,0 +1,15 @@
{
"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"
}

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -109,7 +109,7 @@ view model =
]
, Feed.viewArticles model.timeZone feed
|> 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

View File

@@ -218,7 +218,7 @@ view model =
[ [ viewTabs model.feedTab ]
, Feed.viewArticles model.timeZone feed
|> 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

View File

@@ -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.Attributes exposing (class, classList, href)
import Html.Events exposing (onClick)
@@ -87,3 +88,32 @@ 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) ]
]

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -155,46 +155,27 @@ 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 ]
GlobalFeed ->
[]
if isLoggedIn then
tabBar [ yourFeed ] globalFeed []
else
tabBar [] globalFeed []
TagFeed tagName ->
[]
if isLoggedIn then
tabBar [ yourFeed, globalFeed ] (tagFeed tagName) []
else
tabBar [ globalFeed ] (tagFeed tagName) []
tabBar :

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -206,17 +206,14 @@ toggleFollowButton txt extraClasses msgWhenClicked uname =
decoder : Maybe Cred -> Decoder Author
decoder maybeCred =
{- 👉 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 "..."
Decode.succeed Tuple.pair
|> 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!

View File

@@ -24,17 +24,18 @@ view timeZone timestamp =
-}
iso8601Decoder : Decoder Time.Posix
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
NOTE: You can disregard the (List DeadEnd) here. No need to use it to complete this exercise!
💡 HINT: Decode.andThen will be useful here.
-}
"..."
Err _ ->
Decode.fail ("Invalid ISO-8601 timestamp: " ++ str)

View File

@@ -0,0 +1,15 @@
{
"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"
}

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -34,14 +34,11 @@ parser =
, Parser.map Login (s "login")
, Parser.map Logout (s "logout")
, Parser.map Profile (s "profile" </> Username.urlParser)
-- 👉 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.
, 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")
]

View File

@@ -3,10 +3,10 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
"NoRedInk/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-date-strings": "1.0.0",
"rtfeldman/elm-iso8601": "1.0.1",
"rtfeldman/elm-validate": "4.0.0"
},
"indirect": {

File diff suppressed because it is too large Load Diff

View File

@@ -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#getting-started
If you haven't already, follow the [Getting Started instructions](https://github.com/rtfeldman/elm-0.19-workshop/blob/master/intro/README.md
) 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 terminal, `cd` into the `elm-0.19-workshop/intro/server/` directory again.
In the new termnal, `cd` into the `elm-0.19-workshop/intro/server/` directory again.
Then run this to build the Elm code for the first time:
@@ -43,9 +43,6 @@ If things arent 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!

View File

@@ -5,25 +5,11 @@ 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" ]
[ 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 =
div [ class "home-page" ]
[ div [] [ text "👉 TODO: Replace this <div> with the banner" ]
[ banner
, div [ class "container page" ]
[ div [ class "row" ]
[ div [ class "col-md-9" ] [ feed ]

View File

@@ -3,7 +3,7 @@
"source-directories": [
"."
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"elm/core": "1.0.0",

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"elm/core": "1.0.0",

View File

@@ -7,18 +7,13 @@ import Html.Attributes exposing (..)
viewTags tags =
let
renderedTags =
-- 👉 TODO: use `List.map` and `viewTag` to render the tags
[]
List.map viewTag tags
in
div [ class "tag-list" ] renderedTags
viewTag tagName =
{- 👉 TODO: render something like this:
<button class="tag-pill tag-default">tag name goes here</button>
-}
button [] []
button [ class "tag-pill tag-default" ] [ text tagName ]
main =
@@ -34,9 +29,7 @@ main =
, div [ class "col-md-3" ]
[ div [ class "sidebar" ]
[ p [] [ text "Popular Tags" ]
-- 👉 TODO: instead of passing [] to viewTags, pass the actual tags
, viewTags []
, viewTags tags
]
]
]

View File

@@ -3,16 +3,18 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"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/url": "1.0.0",
"elm/virtual-dom": "1.0.0"
}
},
"test-dependencies": {

View File

@@ -1,8 +1,5 @@
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
@@ -28,14 +25,10 @@ initialModel =
update msg model =
{- 👉 TODO: If `msg.description` is "ClickedTag", then
set the model's `selectedTag` field to be `msg.data`
if msg.description == "ClickedTag" then
{ model | selectedTag = msg.data }
💡 HINT: record update syntax looks like this:
{ model | foo = bar }
-}
else
model
@@ -45,16 +38,8 @@ 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 -> True)
List.filter (\article -> List.member model.selectedTag article.tags)
model.allArticles
feed =
@@ -104,17 +89,7 @@ viewTag selectedTagName tagName =
in
button
[ class ("tag-pill " ++ otherClass)
{- 👉 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`!
-}
, onClick { description = "ClickedTag", data = tagName }
]
[ text tagName ]

View File

@@ -0,0 +1,15 @@
{
"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"
}

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"elm/browser": "1.0.0",

View File

@@ -14,27 +14,20 @@ import Html.Events exposing (onClick)
type alias Model =
{ tags : List String
, selectedTag : String
{- 👉 TODO: change this `allArticles` annotation to the following:
allArticles : List Article
, allArticles : List Article
}
💡 HINT: You'll need to move the existing annotation to a `type alias`.
-}
, allArticles :
List
type alias Article =
{ title : String
, description : String
, body : String
, tags : List String
, slug : String
}
}
{-| 👉 TODO: Replace this comment with a type annotation for `initialModel`
-}
initialModel : Model
initialModel =
{ tags = Article.tags
, 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 =
if msg.description == "ClickedTag" then
{ model | selectedTag = msg.data }
@@ -66,8 +58,7 @@ update msg model =
-- VIEW
{-| 👉 TODO: Replace this comment with a type annotation for `view`
-}
view : Model -> Html Msg
view model =
let
articles =
@@ -93,8 +84,7 @@ view model =
]
{-| 👉 TODO: Replace this comment with a type annotation for `viewArticle`
-}
viewArticle : Article -> Html Msg
viewArticle article =
div [ class "article-preview" ]
[ h1 [] [ text article.title ]
@@ -103,8 +93,7 @@ viewArticle article =
]
{-| 👉 TODO: Replace this comment with a type annotation for `viewBanner`
-}
viewBanner : Html Msg
viewBanner =
div [ class "banner" ]
[ 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 =
let
otherClass =

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -86,11 +86,7 @@ viewForm form =
[ input
[ class "form-control form-control-lg"
, placeholder "Username"
{- 👉 TODO: when the user inputs a username, update it in the Model.
💡 HINT: Look at how the Email input below does this. 👇
-}
, onInput EnteredUsername
, value form.username
]
[]
@@ -141,6 +137,7 @@ type Msg
= SubmittedForm
| EnteredEmail String
| EnteredPassword String
| EnteredUsername String
| CompletedRegister (Result Http.Error Viewer)
| GotSession Session
@@ -154,6 +151,9 @@ 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 ->

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -40,12 +40,12 @@ src (Avatar maybeUrl) =
resolveAvatarUrl : Maybe String -> String
resolveAvatarUrl maybeUrl =
{- 👉 TODO #1 of 2: return the user's avatar from maybeUrl, if maybeUrl actually
contains one. If maybeUrl is Nothing, return this URL instead:
case maybeUrl of
Just url ->
url
https://static.productionready.io/images/smiley-cyrus.jpg
-}
""
Nothing ->
"https://static.productionready.io/images/smiley-cyrus.jpg"
encode : Avatar -> Value

View File

@@ -572,6 +572,7 @@ toTagList tagString =
-}
String.split " " tagString
|> List.map String.trim
|> List.filter (\str -> str /= "")
edit : Slug -> TrimmedForm -> Cred -> Http.Request (Article Full)

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -182,26 +182,12 @@ 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
|> hardcoded "(needs decoding!)"
|> hardcoded "(needs decoding!)"
|> hardcoded []
|> hardcoded False
|> hardcoded 0
|> required "description" string
|> required "title" string
|> required "tagList" (list string)
|> required "favorited" bool
|> required "favoritesCount" int
|> required "createdAt" Timestamp.iso8601Decoder

View File

@@ -12,6 +12,6 @@ Then open `http://localhost:3000` in your browser.
## Exercise
We need to make signup work again. Currently it doesn't actually send a HTTP request to the server.
We need to make login work. Currently it doesn't actually send a HTTP request to the server.
We'll fix this by editing `src/Page/Register.elm` and resolving the TODOs there.
We'll fix this by editing `src/Page/Login.elm` and resolving the TODOs there.

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",

View File

@@ -155,34 +155,13 @@ 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 =
Debug.todo "Call Http.post to represent a POST to /api/users"
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.none
Http.send CompletedRegister request
in
( { model | problems = [] }, cmd )

View File

@@ -1,8 +1,8 @@
# Part 9
# Part 8
Once again, we'll be building `src/Main.elm`, but editing a different file.
To build everything, `cd` into the `part9/` directory and run:
To build everything, `cd` into the `part8/` directory and run8
```shell
elm make src/Main.elm --output ../server/public/elm.js

View File

@@ -3,10 +3,10 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
"NoRedInk/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-date-strings": "1.0.0"
"rtfeldman/elm-iso8601": "1.0.1"
},
"indirect": {
"elm/parser": "1.0.0",

View File

@@ -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)
import Json.Decode as Decode exposing (Decoder, bool, int, list, string)
import Json.Decode.Pipeline exposing (custom, hardcoded, required)
import Json.Encode as Encode
import Markdown
@@ -83,47 +83,6 @@ 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
@@ -211,15 +170,39 @@ 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
|> required "description" (Decode.map (Maybe.withDefault "") (Decode.nullable Decode.string))
|> required "title" Decode.string
|> required "tagList" (Decode.list Decode.string)
|> hardcoded "(needs decoding!)"
|> hardcoded "(needs decoding!)"
|> hardcoded []
|> hardcoded False
|> hardcoded 0
|> required "createdAt" Timestamp.iso8601Decoder
|> required "favorited" Decode.bool
|> required "favoritesCount" Decode.int

View File

@@ -72,7 +72,7 @@ login newViewer =
Viewer.encode newViewer
|> Encode.encode 0
|> Just
|> sendSessionToJavaScript
|> storeSession
@@ -81,24 +81,10 @@ login newViewer =
logout : Cmd msg
logout =
sendSessionToJavaScript Nothing
storeSession Nothing
{-| 👉 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
port storeSession : Maybe String -> Cmd msg
@@ -107,24 +93,10 @@ sendSessionToJavaScript maybeAuthenticationToken =
changes : (Session -> msg) -> Nav.Key -> Sub msg
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
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
port onSessionChange : (Value -> msg) -> Sub msg
decode : Nav.Key -> Value -> Session

View File

@@ -3,10 +3,10 @@
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
"NoRedInk/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-date-strings": "1.0.0",
"rtfeldman/elm-iso8601": "1.0.1",
"rtfeldman/elm-validate": "4.0.0"
},
"indirect": {

File diff suppressed because it is too large Load Diff