├── .gitignore ├── LICENSE ├── README.md ├── TEACHING.md ├── advanced ├── README.md ├── part1 │ ├── README.md │ ├── elm.json │ └── src │ │ ├── Api.elm │ │ ├── Article.elm │ │ ├── Article │ │ ├── Body.elm │ │ ├── Comment.elm │ │ ├── Feed.elm │ │ ├── FeedSources.elm │ │ ├── Slug.elm │ │ └── Tag.elm │ │ ├── Asset.elm │ │ ├── Author.elm │ │ ├── Avatar.elm │ │ ├── CommentId.elm │ │ ├── Email.elm │ │ ├── Loading.elm │ │ ├── Log.elm │ │ ├── Main.elm │ │ ├── Page.elm │ │ ├── Page │ │ ├── Article.elm │ │ ├── Article │ │ │ └── Editor.elm │ │ ├── Blank.elm │ │ ├── Home.elm │ │ ├── Login.elm │ │ ├── NotFound.elm │ │ ├── Profile.elm │ │ ├── Register.elm │ │ └── Settings.elm │ │ ├── PaginatedList.elm │ │ ├── Profile.elm │ │ ├── Route.elm │ │ ├── Session.elm │ │ ├── Timestamp.elm │ │ ├── Username.elm │ │ ├── Viewer.elm │ │ └── Viewer │ │ └── Cred.elm ├── part2 │ ├── README.md │ ├── elm.json │ └── src │ │ ├── Api.elm │ │ ├── Article.elm │ │ ├── Article │ │ ├── Body.elm │ │ ├── Comment.elm │ │ ├── Feed.elm │ │ ├── FeedSources.elm │ │ ├── Slug.elm │ │ └── Tag.elm │ │ ├── Asset.elm │ │ ├── Author.elm │ │ ├── Avatar.elm │ │ ├── CommentId.elm │ │ ├── Email.elm │ │ ├── Loading.elm │ │ ├── Log.elm │ │ ├── Main.elm │ │ ├── Page.elm │ │ ├── Page │ │ ├── Article.elm │ │ ├── Article │ │ │ └── Editor.elm │ │ ├── Blank.elm │ │ ├── Home.elm │ │ ├── Login.elm │ │ ├── NotFound.elm │ │ ├── Profile.elm │ │ ├── Register.elm │ │ └── Settings.elm │ │ ├── PaginatedList.elm │ │ ├── Profile.elm │ │ ├── Route.elm │ │ ├── Session.elm │ │ ├── Timestamp.elm │ │ ├── Username.elm │ │ ├── Viewer.elm │ │ └── Viewer │ │ └── Cred.elm ├── part3 │ ├── README.md │ ├── elm.json │ └── src │ │ ├── Api.elm │ │ ├── Article.elm │ │ ├── Article │ │ ├── Body.elm │ │ ├── Comment.elm │ │ ├── Feed.elm │ │ ├── FeedSources.elm │ │ ├── Slug.elm │ │ └── Tag.elm │ │ ├── Asset.elm │ │ ├── Author.elm │ │ ├── Avatar.elm │ │ ├── CommentId.elm │ │ ├── Email.elm │ │ ├── Loading.elm │ │ ├── Log.elm │ │ ├── Main.elm │ │ ├── Page.elm │ │ ├── Page │ │ ├── Article.elm │ │ ├── Article │ │ │ └── Editor.elm │ │ ├── Blank.elm │ │ ├── Home.elm │ │ ├── Login.elm │ │ ├── NotFound.elm │ │ ├── Profile.elm │ │ ├── Register.elm │ │ └── Settings.elm │ │ ├── PaginatedList.elm │ │ ├── Profile.elm │ │ ├── Route.elm │ │ ├── Session.elm │ │ ├── Timestamp.elm │ │ ├── Username.elm │ │ ├── Viewer.elm │ │ └── Viewer │ │ └── Cred.elm ├── part4 │ ├── README.md │ ├── elm.json │ └── src │ │ ├── Api.elm │ │ ├── Article.elm │ │ ├── Article │ │ ├── Body.elm │ │ ├── Comment.elm │ │ ├── Feed.elm │ │ ├── FeedSources.elm │ │ ├── Slug.elm │ │ └── Tag.elm │ │ ├── Asset.elm │ │ ├── Author.elm │ │ ├── Avatar.elm │ │ ├── CommentId.elm │ │ ├── Email.elm │ │ ├── Loading.elm │ │ ├── Log.elm │ │ ├── Main.elm │ │ ├── Page.elm │ │ ├── Page │ │ ├── Article.elm │ │ ├── Article │ │ │ └── Editor.elm │ │ ├── Blank.elm │ │ ├── Home.elm │ │ ├── Login.elm │ │ ├── NotFound.elm │ │ ├── Profile.elm │ │ ├── Register.elm │ │ └── Settings.elm │ │ ├── PaginatedList.elm │ │ ├── Profile.elm │ │ ├── Route.elm │ │ ├── Session.elm │ │ ├── Timestamp.elm │ │ ├── Username.elm │ │ ├── Viewer.elm │ │ └── Viewer │ │ └── Cred.elm ├── part5 │ ├── README.md │ ├── assets │ │ ├── icons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── mstile-70x70.png │ │ │ └── safari-pinned-tab.svg │ │ ├── images │ │ │ └── error.jpg │ │ └── site.webmanifest │ ├── elm.json │ └── src │ │ ├── Api.elm │ │ ├── Article.elm │ │ ├── Article │ │ ├── Body.elm │ │ ├── Comment.elm │ │ ├── Feed.elm │ │ ├── FeedSources.elm │ │ ├── Slug.elm │ │ └── Tag.elm │ │ ├── Asset.elm │ │ ├── Author.elm │ │ ├── Avatar.elm │ │ ├── CommentId.elm │ │ ├── Email.elm │ │ ├── Loading.elm │ │ ├── Log.elm │ │ ├── Main.elm │ │ ├── Page.elm │ │ ├── Page │ │ ├── Article.elm │ │ ├── Article │ │ │ └── Editor.elm │ │ ├── Blank.elm │ │ ├── Home.elm │ │ ├── Login.elm │ │ ├── NotFound.elm │ │ ├── Profile.elm │ │ ├── Register.elm │ │ └── Settings.elm │ │ ├── PaginatedList.elm │ │ ├── Profile.elm │ │ ├── Route.elm │ │ ├── Session.elm │ │ ├── Timestamp.elm │ │ ├── Username.elm │ │ ├── Viewer.elm │ │ └── Viewer │ │ └── Cred.elm ├── part6 │ ├── README.md │ ├── elm.json │ └── src │ │ ├── Api.elm │ │ ├── Article.elm │ │ ├── Article │ │ ├── Body.elm │ │ ├── Comment.elm │ │ ├── Feed.elm │ │ ├── FeedSources.elm │ │ ├── Slug.elm │ │ └── Tag.elm │ │ ├── Asset.elm │ │ ├── Author.elm │ │ ├── Avatar.elm │ │ ├── CommentId.elm │ │ ├── Email.elm │ │ ├── Loading.elm │ │ ├── Log.elm │ │ ├── Main.elm │ │ ├── Page.elm │ │ ├── Page │ │ ├── Article.elm │ │ ├── Article │ │ │ └── Editor.elm │ │ ├── Blank.elm │ │ ├── Home.elm │ │ ├── Login.elm │ │ ├── NotFound.elm │ │ ├── Profile.elm │ │ ├── Register.elm │ │ └── Settings.elm │ │ ├── PaginatedList.elm │ │ ├── Profile.elm │ │ ├── Route.elm │ │ ├── Session.elm │ │ ├── Timestamp.elm │ │ ├── Username.elm │ │ ├── Viewer.elm │ │ └── Viewer │ │ └── Cred.elm ├── part7 │ ├── README.md │ ├── elm.json │ └── src │ │ ├── Api.elm │ │ ├── Article.elm │ │ ├── Article │ │ ├── Body.elm │ │ ├── Comment.elm │ │ ├── Feed.elm │ │ ├── FeedSources.elm │ │ ├── Slug.elm │ │ └── Tag.elm │ │ ├── Asset.elm │ │ ├── Author.elm │ │ ├── Avatar.elm │ │ ├── CommentId.elm │ │ ├── Email.elm │ │ ├── Loading.elm │ │ ├── Log.elm │ │ ├── Main.elm │ │ ├── Page.elm │ │ ├── Page │ │ ├── Article.elm │ │ ├── Article │ │ │ └── Editor.elm │ │ ├── Blank.elm │ │ ├── Home.elm │ │ ├── Login.elm │ │ ├── NotFound.elm │ │ ├── Profile.elm │ │ ├── Register.elm │ │ └── Settings.elm │ │ ├── PaginatedList.elm │ │ ├── Profile.elm │ │ ├── Route.elm │ │ ├── Session.elm │ │ ├── Timestamp.elm │ │ ├── Username.elm │ │ ├── Viewer.elm │ │ └── Viewer │ │ └── Cred.elm ├── part8 │ ├── README.md │ ├── assets │ │ ├── icons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── mstile-70x70.png │ │ │ └── safari-pinned-tab.svg │ │ ├── images │ │ │ └── error.jpg │ │ └── site.webmanifest │ ├── elm.json │ └── src │ │ ├── Api.elm │ │ ├── Article.elm │ │ ├── Article │ │ ├── Body.elm │ │ ├── Comment.elm │ │ ├── Feed.elm │ │ ├── FeedSources.elm │ │ ├── Slug.elm │ │ └── Tag.elm │ │ ├── Asset.elm │ │ ├── Author.elm │ │ ├── Avatar.elm │ │ ├── CommentId.elm │ │ ├── Email.elm │ │ ├── Loading.elm │ │ ├── Log.elm │ │ ├── Main.elm │ │ ├── Page.elm │ │ ├── Page │ │ ├── Article.elm │ │ ├── Article │ │ │ └── Editor.elm │ │ ├── Blank.elm │ │ ├── Home.elm │ │ ├── Login.elm │ │ ├── NotFound.elm │ │ ├── Profile.elm │ │ ├── Register.elm │ │ └── Settings.elm │ │ ├── PaginatedList.elm │ │ ├── Profile.elm │ │ ├── Route.elm │ │ ├── Session.elm │ │ ├── Timestamp.elm │ │ ├── Username.elm │ │ ├── Viewer.elm │ │ └── Viewer │ │ └── Cred.elm └── server │ ├── .gitignore │ ├── LICENSE │ ├── data │ ├── articles.db │ ├── comments.db │ ├── favorites.db │ ├── follows.db │ └── users.db │ ├── elm.json │ ├── logo.png │ ├── mixins │ └── db.mixin.js │ ├── moleculer.config.js │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── asset-manifest.json │ ├── assets │ │ └── images │ │ │ ├── error.jpg │ │ │ ├── loading.svg │ │ │ └── smiley-cyrus.jpg │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── fonts.css │ ├── images │ │ └── smiley-cyrus.jpg │ ├── index.html │ ├── main.css │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest │ ├── rw-logo.png │ ├── services │ ├── api.service.js │ ├── articles.service.js │ ├── comments.service.js │ ├── favorites.service.js │ ├── follows.service.js │ ├── metrics.service.js │ └── users.service.js │ └── src │ └── Main.elm └── intro ├── README.md ├── part1 ├── Main.elm ├── README.md ├── elm.json └── index.html ├── part2 ├── README.md ├── elm.json ├── index.html └── src │ └── Main.elm ├── part3 ├── README.md ├── elm.json ├── index.html └── src │ ├── Article.elm │ └── Main.elm ├── part4 ├── README.md ├── elm.json ├── index.html └── src │ ├── Article.elm │ └── Main.elm ├── part5 ├── .gitignore ├── .travis.yml ├── README.md ├── assets │ ├── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── mstile-70x70.png │ │ └── safari-pinned-tab.svg │ ├── images │ │ ├── error.jpg │ │ └── loading.svg │ └── site.webmanifest ├── elm.json └── src │ ├── Api.elm │ ├── Article.elm │ ├── Article │ ├── Body.elm │ ├── Comment.elm │ ├── Feed.elm │ ├── FeedSources.elm │ ├── Slug.elm │ └── Tag.elm │ ├── Asset.elm │ ├── Author.elm │ ├── Avatar.elm │ ├── CommentId.elm │ ├── Email.elm │ ├── Loading.elm │ ├── Log.elm │ ├── Main.elm │ ├── Page.elm │ ├── Page │ ├── Article.elm │ ├── Article │ │ └── Editor.elm │ ├── Blank.elm │ ├── Home.elm │ ├── Login.elm │ ├── NotFound.elm │ ├── Profile.elm │ ├── Register.elm │ └── Settings.elm │ ├── PaginatedList.elm │ ├── Profile.elm │ ├── Route.elm │ ├── Session.elm │ ├── Timestamp.elm │ ├── Username.elm │ ├── Viewer.elm │ └── Viewer │ └── Cred.elm ├── part6 ├── .gitignore ├── .travis.yml ├── README.md ├── assets │ ├── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── mstile-70x70.png │ │ └── safari-pinned-tab.svg │ ├── images │ │ ├── error.jpg │ │ └── loading.svg │ └── site.webmanifest ├── elm.json └── src │ ├── Api.elm │ ├── Article.elm │ ├── Article │ ├── Body.elm │ ├── Comment.elm │ ├── Feed.elm │ ├── FeedSources.elm │ ├── Slug.elm │ └── Tag.elm │ ├── Asset.elm │ ├── Author.elm │ ├── Avatar.elm │ ├── CommentId.elm │ ├── Email.elm │ ├── Loading.elm │ ├── Log.elm │ ├── Main.elm │ ├── Page.elm │ ├── Page │ ├── Article.elm │ ├── Article │ │ └── Editor.elm │ ├── Blank.elm │ ├── Home.elm │ ├── Login.elm │ ├── NotFound.elm │ ├── Profile.elm │ ├── Register.elm │ └── Settings.elm │ ├── PaginatedList.elm │ ├── Profile.elm │ ├── Route.elm │ ├── Session.elm │ ├── Timestamp.elm │ ├── Username.elm │ ├── Viewer.elm │ └── Viewer │ └── Cred.elm ├── part7 ├── .gitignore ├── .travis.yml ├── README.md ├── assets │ ├── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── mstile-70x70.png │ │ └── safari-pinned-tab.svg │ ├── images │ │ ├── error.jpg │ │ └── loading.svg │ └── site.webmanifest ├── elm.json └── src │ ├── Api.elm │ ├── Article.elm │ ├── Article │ ├── Body.elm │ ├── Comment.elm │ ├── Feed.elm │ ├── FeedSources.elm │ ├── Slug.elm │ └── Tag.elm │ ├── Asset.elm │ ├── Author.elm │ ├── Avatar.elm │ ├── CommentId.elm │ ├── Email.elm │ ├── Loading.elm │ ├── Log.elm │ ├── Main.elm │ ├── Page.elm │ ├── Page │ ├── Article.elm │ ├── Article │ │ └── Editor.elm │ ├── Blank.elm │ ├── Home.elm │ ├── Login.elm │ ├── NotFound.elm │ ├── Profile.elm │ ├── Register.elm │ └── Settings.elm │ ├── PaginatedList.elm │ ├── Profile.elm │ ├── Route.elm │ ├── Session.elm │ ├── Timestamp.elm │ ├── Username.elm │ ├── Viewer.elm │ └── Viewer │ └── Cred.elm ├── part8 ├── .gitignore ├── .travis.yml ├── README.md ├── assets │ ├── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── mstile-70x70.png │ │ └── safari-pinned-tab.svg │ ├── images │ │ ├── error.jpg │ │ └── loading.svg │ └── site.webmanifest ├── elm.json └── src │ ├── Api.elm │ ├── Article.elm │ ├── Article │ ├── Body.elm │ ├── Comment.elm │ ├── Feed.elm │ ├── FeedSources.elm │ ├── Slug.elm │ └── Tag.elm │ ├── Asset.elm │ ├── Author.elm │ ├── Avatar.elm │ ├── CommentId.elm │ ├── Email.elm │ ├── Loading.elm │ ├── Log.elm │ ├── Main.elm │ ├── Page.elm │ ├── Page │ ├── Article.elm │ ├── Article │ │ └── Editor.elm │ ├── Blank.elm │ ├── Home.elm │ ├── Login.elm │ ├── NotFound.elm │ ├── Profile.elm │ ├── Register.elm │ └── Settings.elm │ ├── PaginatedList.elm │ ├── Profile.elm │ ├── Route.elm │ ├── Session.elm │ ├── Timestamp.elm │ ├── Username.elm │ ├── Viewer.elm │ └── Viewer │ └── Cred.elm ├── part9 ├── .gitignore ├── .travis.yml ├── README.md ├── assets │ ├── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── mstile-70x70.png │ │ └── safari-pinned-tab.svg │ ├── images │ │ ├── error.jpg │ │ └── loading.svg │ └── site.webmanifest ├── elm.json └── src │ ├── Api.elm │ ├── Article.elm │ ├── Article │ ├── Body.elm │ ├── Comment.elm │ ├── Feed.elm │ ├── FeedSources.elm │ ├── Slug.elm │ └── Tag.elm │ ├── Asset.elm │ ├── Author.elm │ ├── Avatar.elm │ ├── CommentId.elm │ ├── Email.elm │ ├── Loading.elm │ ├── Log.elm │ ├── Main.elm │ ├── Page.elm │ ├── Page │ ├── Article.elm │ ├── Article │ │ └── Editor.elm │ ├── Blank.elm │ ├── Home.elm │ ├── Login.elm │ ├── NotFound.elm │ ├── Profile.elm │ ├── Register.elm │ └── Settings.elm │ ├── PaginatedList.elm │ ├── Profile.elm │ ├── Route.elm │ ├── Session.elm │ ├── Timestamp.elm │ ├── Username.elm │ ├── Viewer.elm │ └── Viewer │ └── Cred.elm └── server ├── .gitignore ├── LICENSE ├── data ├── articles.db ├── comments.db ├── favorites.db ├── follows.db └── users.db ├── elm.json ├── logo.png ├── mixins └── db.mixin.js ├── moleculer.config.js ├── package-lock.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── assets │ ├── images │ │ ├── error.jpg │ │ ├── loading.svg │ │ └── smiley-cyrus.jpg │ └── localForage.js ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts.css ├── images │ └── smiley-cyrus.jpg ├── index.html ├── main.css ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── mstile-70x70.png └── safari-pinned-tab.svg ├── rw-logo.png ├── services ├── api.service.js ├── articles.service.js ├── comments.service.js ├── favorites.service.js ├── follows.service.js ├── metrics.service.js └── users.service.js └── src └── Main.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | elm.js 3 | -------------------------------------------------------------------------------- /advanced/part1/README.md: -------------------------------------------------------------------------------- 1 | # Part 1 2 | 3 | To build everything, `cd` into this `part1/` directory and run: 4 | 5 | ```shell 6 | elm make src/Main.elm --output=../server/public/elm.js 7 | ``` 8 | 9 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 10 | 11 | ## Exercise 12 | 13 | Open `src/Viewer/Cred.elm` in your editor and resolve the TODOs there. 14 | -------------------------------------------------------------------------------- /advanced/part1/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /advanced/part1/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /advanced/part1/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /advanced/part1/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | 36 | -- SERIALIZATION 37 | 38 | 39 | decoder : Decoder Tag 40 | decoder = 41 | Decode.map Tag Decode.string 42 | -------------------------------------------------------------------------------- /advanced/part1/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /advanced/part1/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /advanced/part1/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /advanced/part1/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /advanced/part1/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /advanced/part1/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /advanced/part1/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /advanced/part1/src/Profile.elm: -------------------------------------------------------------------------------- 1 | module Profile exposing (Profile, avatar, bio, decoder) 2 | 3 | {-| A user's profile - potentially your own! 4 | 5 | Contrast with Cred, which is the currently signed-in user. 6 | 7 | -} 8 | 9 | import Api 10 | import Avatar exposing (Avatar) 11 | import Http 12 | import HttpBuilder exposing (RequestBuilder, withExpect) 13 | import Json.Decode as Decode exposing (Decoder) 14 | import Json.Decode.Pipeline exposing (required) 15 | import Username exposing (Username) 16 | import Viewer.Cred as Cred exposing (Cred) 17 | 18 | 19 | 20 | -- TYPES 21 | 22 | 23 | type Profile 24 | = Profile Internals 25 | 26 | 27 | type alias Internals = 28 | { bio : Maybe String 29 | , avatar : Avatar 30 | } 31 | 32 | 33 | 34 | -- INFO 35 | 36 | 37 | bio : Profile -> Maybe String 38 | bio (Profile info) = 39 | info.bio 40 | 41 | 42 | avatar : Profile -> Avatar 43 | avatar (Profile info) = 44 | info.avatar 45 | 46 | 47 | 48 | -- SERIALIZATION 49 | 50 | 51 | decoder : Decoder Profile 52 | decoder = 53 | Decode.succeed Internals 54 | |> required "bio" (Decode.nullable Decode.string) 55 | |> required "image" Avatar.decoder 56 | |> Decode.map Profile 57 | -------------------------------------------------------------------------------- /advanced/part1/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /advanced/part2/README.md: -------------------------------------------------------------------------------- 1 | # Part 2 2 | 3 | To build everything, `cd` into this `part2/` directory and run: 4 | 5 | ```shell 6 | elm make src/Main.elm --output=../server/public/elm.js 7 | ``` 8 | 9 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 10 | 11 | ## Exercise 12 | 13 | Open `src/Article.elm` in your editor and resolve the TODOs there. 14 | -------------------------------------------------------------------------------- /advanced/part2/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /advanced/part2/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /advanced/part2/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /advanced/part2/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString, validate) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | validate : String -> List String -> Bool 36 | validate str = 37 | String.split " " str 38 | |> List.map String.trim 39 | |> List.filter (not << String.isEmpty) 40 | |> (==) 41 | 42 | 43 | 44 | -- SERIALIZATION 45 | 46 | 47 | decoder : Decoder Tag 48 | decoder = 49 | Decode.map Tag Decode.string 50 | -------------------------------------------------------------------------------- /advanced/part2/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /advanced/part2/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /advanced/part2/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /advanced/part2/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /advanced/part2/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /advanced/part2/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /advanced/part2/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /advanced/part2/src/Profile.elm: -------------------------------------------------------------------------------- 1 | module Profile exposing (Profile, avatar, bio, decoder) 2 | 3 | {-| A user's profile - potentially your own! 4 | 5 | Contrast with Cred, which is the currently signed-in user. 6 | 7 | -} 8 | 9 | import Api 10 | import Avatar exposing (Avatar) 11 | import Http 12 | import HttpBuilder exposing (RequestBuilder, withExpect) 13 | import Json.Decode as Decode exposing (Decoder) 14 | import Json.Decode.Pipeline exposing (required) 15 | import Username exposing (Username) 16 | import Viewer.Cred as Cred exposing (Cred) 17 | 18 | 19 | 20 | -- TYPES 21 | 22 | 23 | type Profile 24 | = Profile Internals 25 | 26 | 27 | type alias Internals = 28 | { bio : Maybe String 29 | , avatar : Avatar 30 | } 31 | 32 | 33 | 34 | -- INFO 35 | 36 | 37 | bio : Profile -> Maybe String 38 | bio (Profile info) = 39 | info.bio 40 | 41 | 42 | avatar : Profile -> Avatar 43 | avatar (Profile info) = 44 | info.avatar 45 | 46 | 47 | 48 | -- SERIALIZATION 49 | 50 | 51 | decoder : Decoder Profile 52 | decoder = 53 | Decode.succeed Internals 54 | |> required "bio" (Decode.nullable Decode.string) 55 | |> required "image" Avatar.decoder 56 | |> Decode.map Profile 57 | -------------------------------------------------------------------------------- /advanced/part2/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /advanced/part3/README.md: -------------------------------------------------------------------------------- 1 | # Part 3 2 | 3 | To build everything, `cd` into this `part3/` directory and run: 4 | 5 | ```shell 6 | elm make src/Main.elm --output=../server/public/elm.js 7 | ``` 8 | 9 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 10 | 11 | ## Exercise 12 | 13 | Open `src/Page/Article.elm` in your editor and resolve the TODOs there. 14 | -------------------------------------------------------------------------------- /advanced/part3/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /advanced/part3/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /advanced/part3/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /advanced/part3/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString, validate) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | validate : String -> List String -> Bool 36 | validate str = 37 | String.split " " str 38 | |> List.map String.trim 39 | |> List.filter (not << String.isEmpty) 40 | |> (==) 41 | 42 | 43 | 44 | -- SERIALIZATION 45 | 46 | 47 | decoder : Decoder Tag 48 | decoder = 49 | Decode.map Tag Decode.string 50 | -------------------------------------------------------------------------------- /advanced/part3/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /advanced/part3/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /advanced/part3/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /advanced/part3/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /advanced/part3/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /advanced/part3/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /advanced/part3/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /advanced/part3/src/Profile.elm: -------------------------------------------------------------------------------- 1 | module Profile exposing (Profile, avatar, bio, decoder) 2 | 3 | {-| A user's profile - potentially your own! 4 | 5 | Contrast with Cred, which is the currently signed-in user. 6 | 7 | -} 8 | 9 | import Api 10 | import Avatar exposing (Avatar) 11 | import Http 12 | import HttpBuilder exposing (RequestBuilder, withExpect) 13 | import Json.Decode as Decode exposing (Decoder) 14 | import Json.Decode.Pipeline exposing (required) 15 | import Username exposing (Username) 16 | import Viewer.Cred as Cred exposing (Cred) 17 | 18 | 19 | 20 | -- TYPES 21 | 22 | 23 | type Profile 24 | = Profile Internals 25 | 26 | 27 | type alias Internals = 28 | { bio : Maybe String 29 | , avatar : Avatar 30 | } 31 | 32 | 33 | 34 | -- INFO 35 | 36 | 37 | bio : Profile -> Maybe String 38 | bio (Profile info) = 39 | info.bio 40 | 41 | 42 | avatar : Profile -> Avatar 43 | avatar (Profile info) = 44 | info.avatar 45 | 46 | 47 | 48 | -- SERIALIZATION 49 | 50 | 51 | decoder : Decoder Profile 52 | decoder = 53 | Decode.succeed Internals 54 | |> required "bio" (Decode.nullable Decode.string) 55 | |> required "image" Avatar.decoder 56 | |> Decode.map Profile 57 | -------------------------------------------------------------------------------- /advanced/part3/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /advanced/part4/README.md: -------------------------------------------------------------------------------- 1 | # Part 4 2 | 3 | To build everything, `cd` into this `part4/` directory and run: 4 | 5 | ```shell 6 | elm make src/Main.elm --output=../server/public/elm.js 7 | ``` 8 | 9 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 10 | 11 | ## Exercise 12 | 13 | Resolve the TODOs in these files: 14 | * `src/Page/Home.elm` 15 | * `src/Page/Settings.elm` 16 | * `src/Page/Profile.elm` 17 | -------------------------------------------------------------------------------- /advanced/part4/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /advanced/part4/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /advanced/part4/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /advanced/part4/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString, validate) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | validate : String -> List String -> Bool 36 | validate str = 37 | String.split " " str 38 | |> List.map String.trim 39 | |> List.filter (not << String.isEmpty) 40 | |> (==) 41 | 42 | 43 | 44 | -- SERIALIZATION 45 | 46 | 47 | decoder : Decoder Tag 48 | decoder = 49 | Decode.map Tag Decode.string 50 | -------------------------------------------------------------------------------- /advanced/part4/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /advanced/part4/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /advanced/part4/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /advanced/part4/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /advanced/part4/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /advanced/part4/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /advanced/part4/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /advanced/part4/src/Profile.elm: -------------------------------------------------------------------------------- 1 | module Profile exposing (Profile, avatar, bio, decoder) 2 | 3 | {-| A user's profile - potentially your own! 4 | 5 | Contrast with Cred, which is the currently signed-in user. 6 | 7 | -} 8 | 9 | import Api 10 | import Avatar exposing (Avatar) 11 | import Http 12 | import HttpBuilder exposing (RequestBuilder, withExpect) 13 | import Json.Decode as Decode exposing (Decoder) 14 | import Json.Decode.Pipeline exposing (required) 15 | import Username exposing (Username) 16 | import Viewer.Cred as Cred exposing (Cred) 17 | 18 | 19 | 20 | -- TYPES 21 | 22 | 23 | type Profile 24 | = Profile Internals 25 | 26 | 27 | type alias Internals = 28 | { bio : Maybe String 29 | , avatar : Avatar 30 | } 31 | 32 | 33 | 34 | -- INFO 35 | 36 | 37 | bio : Profile -> Maybe String 38 | bio (Profile info) = 39 | info.bio 40 | 41 | 42 | avatar : Profile -> Avatar 43 | avatar (Profile info) = 44 | info.avatar 45 | 46 | 47 | 48 | -- SERIALIZATION 49 | 50 | 51 | decoder : Decoder Profile 52 | decoder = 53 | Decode.succeed Internals 54 | |> required "bio" (Decode.nullable Decode.string) 55 | |> required "image" Avatar.decoder 56 | |> Decode.map Profile 57 | -------------------------------------------------------------------------------- /advanced/part4/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /advanced/part5/README.md: -------------------------------------------------------------------------------- 1 | # Part 5 2 | 3 | To build everything, `cd` into this `part5/` directory and run: 4 | 5 | ```shell 6 | elm make src/Main.elm --output=../server/public/elm.js 7 | ``` 8 | 9 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 10 | 11 | ## Exercise 12 | 13 | Resolve the TODOs in these files: 14 | 15 | 1. `src/Page/Profile.elm` 16 | 2. `src/Page/Home.elm` 17 | -------------------------------------------------------------------------------- /advanced/part5/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /advanced/part5/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /advanced/part5/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /advanced/part5/assets/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /advanced/part5/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /advanced/part5/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /advanced/part5/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/icons/favicon.ico -------------------------------------------------------------------------------- /advanced/part5/assets/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/icons/mstile-144x144.png -------------------------------------------------------------------------------- /advanced/part5/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /advanced/part5/assets/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/icons/mstile-310x150.png -------------------------------------------------------------------------------- /advanced/part5/assets/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/icons/mstile-310x310.png -------------------------------------------------------------------------------- /advanced/part5/assets/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/icons/mstile-70x70.png -------------------------------------------------------------------------------- /advanced/part5/assets/images/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part5/assets/images/error.jpg -------------------------------------------------------------------------------- /advanced/part5/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /advanced/part5/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /advanced/part5/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /advanced/part5/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /advanced/part5/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString, validate) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | validate : String -> List String -> Bool 36 | validate str = 37 | String.split " " str 38 | |> List.map String.trim 39 | |> List.filter (not << String.isEmpty) 40 | |> (==) 41 | 42 | 43 | 44 | -- SERIALIZATION 45 | 46 | 47 | decoder : Decoder Tag 48 | decoder = 49 | Decode.map Tag Decode.string 50 | -------------------------------------------------------------------------------- /advanced/part5/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /advanced/part5/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /advanced/part5/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /advanced/part5/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /advanced/part5/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /advanced/part5/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /advanced/part5/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /advanced/part5/src/Profile.elm: -------------------------------------------------------------------------------- 1 | module Profile exposing (Profile, avatar, bio, decoder) 2 | 3 | {-| A user's profile - potentially your own! 4 | 5 | Contrast with Cred, which is the currently signed-in user. 6 | 7 | -} 8 | 9 | import Api 10 | import Avatar exposing (Avatar) 11 | import Http 12 | import HttpBuilder exposing (RequestBuilder, withExpect) 13 | import Json.Decode as Decode exposing (Decoder) 14 | import Json.Decode.Pipeline exposing (required) 15 | import Username exposing (Username) 16 | import Viewer.Cred as Cred exposing (Cred) 17 | 18 | 19 | 20 | -- TYPES 21 | 22 | 23 | type Profile 24 | = Profile Internals 25 | 26 | 27 | type alias Internals = 28 | { bio : Maybe String 29 | , avatar : Avatar 30 | } 31 | 32 | 33 | 34 | -- INFO 35 | 36 | 37 | bio : Profile -> Maybe String 38 | bio (Profile info) = 39 | info.bio 40 | 41 | 42 | avatar : Profile -> Avatar 43 | avatar (Profile info) = 44 | info.avatar 45 | 46 | 47 | 48 | -- SERIALIZATION 49 | 50 | 51 | decoder : Decoder Profile 52 | decoder = 53 | Decode.succeed Internals 54 | |> required "bio" (Decode.nullable Decode.string) 55 | |> required "image" Avatar.decoder 56 | |> Decode.map Profile 57 | -------------------------------------------------------------------------------- /advanced/part5/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /advanced/part6/README.md: -------------------------------------------------------------------------------- 1 | # Part 6 2 | 3 | To build everything, `cd` into this `part6/` directory and run: 4 | 5 | ```shell 6 | elm make src/Main.elm --output=../server/public/elm.js 7 | ``` 8 | 9 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 10 | 11 | ## Exercise 12 | 13 | Open `src/Page/Home.elm` in your editor and resolve the TODOs there. 14 | -------------------------------------------------------------------------------- /advanced/part6/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /advanced/part6/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /advanced/part6/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /advanced/part6/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString, validate) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | validate : String -> List String -> Bool 36 | validate str = 37 | String.split " " str 38 | |> List.map String.trim 39 | |> List.filter (not << String.isEmpty) 40 | |> (==) 41 | 42 | 43 | 44 | -- SERIALIZATION 45 | 46 | 47 | decoder : Decoder Tag 48 | decoder = 49 | Decode.map Tag Decode.string 50 | -------------------------------------------------------------------------------- /advanced/part6/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /advanced/part6/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /advanced/part6/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /advanced/part6/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /advanced/part6/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /advanced/part6/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /advanced/part6/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /advanced/part6/src/Profile.elm: -------------------------------------------------------------------------------- 1 | module Profile exposing (Profile, avatar, bio, decoder) 2 | 3 | {-| A user's profile - potentially your own! 4 | 5 | Contrast with Cred, which is the currently signed-in user. 6 | 7 | -} 8 | 9 | import Api 10 | import Avatar exposing (Avatar) 11 | import Http 12 | import HttpBuilder exposing (RequestBuilder, withExpect) 13 | import Json.Decode as Decode exposing (Decoder) 14 | import Json.Decode.Pipeline exposing (required) 15 | import Username exposing (Username) 16 | import Viewer.Cred as Cred exposing (Cred) 17 | 18 | 19 | 20 | -- TYPES 21 | 22 | 23 | type Profile 24 | = Profile Internals 25 | 26 | 27 | type alias Internals = 28 | { bio : Maybe String 29 | , avatar : Avatar 30 | } 31 | 32 | 33 | 34 | -- INFO 35 | 36 | 37 | bio : Profile -> Maybe String 38 | bio (Profile info) = 39 | info.bio 40 | 41 | 42 | avatar : Profile -> Avatar 43 | avatar (Profile info) = 44 | info.avatar 45 | 46 | 47 | 48 | -- SERIALIZATION 49 | 50 | 51 | decoder : Decoder Profile 52 | decoder = 53 | Decode.succeed Internals 54 | |> required "bio" (Decode.nullable Decode.string) 55 | |> required "image" Avatar.decoder 56 | |> Decode.map Profile 57 | -------------------------------------------------------------------------------- /advanced/part6/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /advanced/part7/README.md: -------------------------------------------------------------------------------- 1 | # Part 7 2 | 3 | To build everything, `cd` into this `part7/` directory and run: 4 | 5 | ```shell 6 | elm make src/Main.elm --output=../server/public/elm.js 7 | ``` 8 | 9 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 10 | 11 | ## Exercise 12 | 13 | Resolve the TODOs in these files: 14 | 15 | - `src/Timestamp.elm` 16 | - `src/Author.elm` 17 | -------------------------------------------------------------------------------- /advanced/part7/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /advanced/part7/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /advanced/part7/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /advanced/part7/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString, validate) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | validate : String -> List String -> Bool 36 | validate str = 37 | String.split " " str 38 | |> List.map String.trim 39 | |> List.filter (not << String.isEmpty) 40 | |> (==) 41 | 42 | 43 | 44 | -- SERIALIZATION 45 | 46 | 47 | decoder : Decoder Tag 48 | decoder = 49 | Decode.map Tag Decode.string 50 | -------------------------------------------------------------------------------- /advanced/part7/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /advanced/part7/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /advanced/part7/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /advanced/part7/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /advanced/part7/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /advanced/part7/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /advanced/part7/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /advanced/part7/src/Profile.elm: -------------------------------------------------------------------------------- 1 | module Profile exposing (Profile, avatar, bio, decoder) 2 | 3 | {-| A user's profile - potentially your own! 4 | 5 | Contrast with Cred, which is the currently signed-in user. 6 | 7 | -} 8 | 9 | import Api 10 | import Avatar exposing (Avatar) 11 | import Http 12 | import HttpBuilder exposing (RequestBuilder, withExpect) 13 | import Json.Decode as Decode exposing (Decoder) 14 | import Json.Decode.Pipeline exposing (required) 15 | import Username exposing (Username) 16 | import Viewer.Cred as Cred exposing (Cred) 17 | 18 | 19 | 20 | -- TYPES 21 | 22 | 23 | type Profile 24 | = Profile Internals 25 | 26 | 27 | type alias Internals = 28 | { bio : Maybe String 29 | , avatar : Avatar 30 | } 31 | 32 | 33 | 34 | -- INFO 35 | 36 | 37 | bio : Profile -> Maybe String 38 | bio (Profile info) = 39 | info.bio 40 | 41 | 42 | avatar : Profile -> Avatar 43 | avatar (Profile info) = 44 | info.avatar 45 | 46 | 47 | 48 | -- SERIALIZATION 49 | 50 | 51 | decoder : Decoder Profile 52 | decoder = 53 | Decode.succeed Internals 54 | |> required "bio" (Decode.nullable Decode.string) 55 | |> required "image" Avatar.decoder 56 | |> Decode.map Profile 57 | -------------------------------------------------------------------------------- /advanced/part7/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /advanced/part8/README.md: -------------------------------------------------------------------------------- 1 | # Part 8 2 | 3 | To build everything, `cd` into this `part8/` directory and run: 4 | 5 | ```shell 6 | elm make src/Main.elm --output=../server/public/elm.js 7 | ``` 8 | 9 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 10 | 11 | ## Exercise 12 | 13 | Open `src/Route.elm` in your editor and resolve the TODOs there. 14 | -------------------------------------------------------------------------------- /advanced/part8/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /advanced/part8/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /advanced/part8/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /advanced/part8/assets/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /advanced/part8/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /advanced/part8/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /advanced/part8/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/icons/favicon.ico -------------------------------------------------------------------------------- /advanced/part8/assets/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/icons/mstile-144x144.png -------------------------------------------------------------------------------- /advanced/part8/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /advanced/part8/assets/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/icons/mstile-310x150.png -------------------------------------------------------------------------------- /advanced/part8/assets/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/icons/mstile-310x310.png -------------------------------------------------------------------------------- /advanced/part8/assets/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/icons/mstile-70x70.png -------------------------------------------------------------------------------- /advanced/part8/assets/images/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/part8/assets/images/error.jpg -------------------------------------------------------------------------------- /advanced/part8/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /advanced/part8/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /advanced/part8/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /advanced/part8/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /advanced/part8/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString, validate) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | validate : String -> List String -> Bool 36 | validate str = 37 | String.split " " str 38 | |> List.map String.trim 39 | |> List.filter (not << String.isEmpty) 40 | |> (==) 41 | 42 | 43 | 44 | -- SERIALIZATION 45 | 46 | 47 | decoder : Decoder Tag 48 | decoder = 49 | Decode.map Tag Decode.string 50 | -------------------------------------------------------------------------------- /advanced/part8/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /advanced/part8/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /advanced/part8/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /advanced/part8/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /advanced/part8/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /advanced/part8/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /advanced/part8/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /advanced/part8/src/Profile.elm: -------------------------------------------------------------------------------- 1 | module Profile exposing (Profile, avatar, bio, decoder) 2 | 3 | {-| A user's profile - potentially your own! 4 | 5 | Contrast with Cred, which is the currently signed-in user. 6 | 7 | -} 8 | 9 | import Api 10 | import Avatar exposing (Avatar) 11 | import Http 12 | import HttpBuilder exposing (RequestBuilder, withExpect) 13 | import Json.Decode as Decode exposing (Decoder) 14 | import Json.Decode.Pipeline exposing (required) 15 | import Username exposing (Username) 16 | import Viewer.Cred as Cred exposing (Cred) 17 | 18 | 19 | 20 | -- TYPES 21 | 22 | 23 | type Profile 24 | = Profile Internals 25 | 26 | 27 | type alias Internals = 28 | { bio : Maybe String 29 | , avatar : Avatar 30 | } 31 | 32 | 33 | 34 | -- INFO 35 | 36 | 37 | bio : Profile -> Maybe String 38 | bio (Profile info) = 39 | info.bio 40 | 41 | 42 | avatar : Profile -> Avatar 43 | avatar (Profile info) = 44 | info.avatar 45 | 46 | 47 | 48 | -- SERIALIZATION 49 | 50 | 51 | decoder : Decoder Profile 52 | decoder = 53 | Decode.succeed Internals 54 | |> required "bio" (Decode.nullable Decode.string) 55 | |> required "image" Avatar.decoder 56 | |> Decode.map Profile 57 | -------------------------------------------------------------------------------- /advanced/part8/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /advanced/server/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | coverage/ 4 | npm-debug.log 5 | stats.json 6 | yarn-error.log 7 | -------------------------------------------------------------------------------- /advanced/server/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ice Services 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /advanced/server/data/comments.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/data/comments.db -------------------------------------------------------------------------------- /advanced/server/data/favorites.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/data/favorites.db -------------------------------------------------------------------------------- /advanced/server/data/follows.db: -------------------------------------------------------------------------------- 1 | {"follow":"Z1YiwpVIz2GQQ13Q","user":"gsZdyqSGbQscoIU6","createdAt":{"$$date":1525550850919},"_id":"f8Yep9EguoPa8Rwn"} 2 | -------------------------------------------------------------------------------- /advanced/server/data/users.db: -------------------------------------------------------------------------------- 1 | {"username":"rtfeldman","email":"richard.t.feldman@gmail.com","password":"$2a$10$4WPM2GSk3pH8COW1UVAVEeu2OC7/CrCzjR91PcPZIw/Ycdvyg3Aqi","bio":"","image":null,"createdAt":{"$$date":1533459071196},"_id":"7r8Pdo5csPGElToj"} 2 | {"username":"SamSample","email":"sam@sample.com","password":"samsample","bio":"I'm the sample user for the workshop. Hi!","image":"https://user-images.githubusercontent.com/1094080/39663282-6459c64e-503e-11e8-8da8-a2af2c81d052.png","createdAt":{"$$date":1525523183044},"_id":"Z1YiwpVIz2GQQ13Q","updatedAt":{"$$date":1525523378471}} 3 | {"username":"wefjbsfdjkhdgrskjhsdfgjhb","email":"sdfgjhsgjhbdfgkjhsdf@sfgdhjfjhgdfgf.com","password":"$2a$10$gnFdddybmitDP.yKM3OjfubgZ1O3geK9N8LymFV4mZYaSGe9WYPby","bio":"","image":null,"createdAt":{"$$date":1525546056406},"_id":"gsZdyqSGbQscoIU6"} 4 | -------------------------------------------------------------------------------- /advanced/server/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0", 20 | "rtfeldman/elm-validate": "4.0.0" 21 | }, 22 | "indirect": { 23 | "elm/parser": "1.0.0", 24 | "elm/regex": "1.0.0", 25 | "elm/virtual-dom": "1.0.0" 26 | } 27 | }, 28 | "test-dependencies": { 29 | "direct": {}, 30 | "indirect": {} 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /advanced/server/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/logo.png -------------------------------------------------------------------------------- /advanced/server/mixins/db.mixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const mkdir = require("mkdirp").sync; 5 | 6 | const DbService = require("moleculer-db"); 7 | 8 | //process.env.MONGO_URI = "mongodb://localhost/conduit"; 9 | 10 | module.exports = function(collection) { 11 | if (process.env.MONGO_URI) { 12 | // Mongo adapter 13 | const MongoAdapter = require("moleculer-db-adapter-mongo"); 14 | 15 | return { 16 | mixins: [DbService], 17 | adapter: new MongoAdapter(process.env.MONGO_URI), 18 | collection 19 | }; 20 | } 21 | 22 | // --- NeDB fallback DB adapter 23 | 24 | // Create data folder 25 | mkdir(path.resolve("./data")); 26 | 27 | return { 28 | mixins: [DbService], 29 | adapter: new DbService.MemoryAdapter({ filename: `./data/${collection}.db` }) 30 | }; 31 | }; -------------------------------------------------------------------------------- /advanced/server/moleculer.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const os = require("os"); 4 | 5 | module.exports = { 6 | // It will be unique when scale up instances in Docker or on local computer 7 | nodeID: os.hostname().toLowerCase() + "-" + process.pid, 8 | 9 | logger: false, 10 | logLevel: "info", 11 | 12 | cacher: "memory", 13 | 14 | metrics: false 15 | }; 16 | -------------------------------------------------------------------------------- /advanced/server/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /advanced/server/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /advanced/server/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/apple-touch-icon.png -------------------------------------------------------------------------------- /advanced/server/public/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.js": "static/js/main.cd99036f.js", 3 | "main.js.map": "static/js/main.cd99036f.js.map" 4 | } -------------------------------------------------------------------------------- /advanced/server/public/assets/images/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/assets/images/error.jpg -------------------------------------------------------------------------------- /advanced/server/public/assets/images/smiley-cyrus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/assets/images/smiley-cyrus.jpg -------------------------------------------------------------------------------- /advanced/server/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /advanced/server/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/favicon-16x16.png -------------------------------------------------------------------------------- /advanced/server/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/favicon-32x32.png -------------------------------------------------------------------------------- /advanced/server/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/favicon.ico -------------------------------------------------------------------------------- /advanced/server/public/images/smiley-cyrus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/images/smiley-cyrus.jpg -------------------------------------------------------------------------------- /advanced/server/public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/mstile-144x144.png -------------------------------------------------------------------------------- /advanced/server/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/mstile-150x150.png -------------------------------------------------------------------------------- /advanced/server/public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/mstile-310x150.png -------------------------------------------------------------------------------- /advanced/server/public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/mstile-310x310.png -------------------------------------------------------------------------------- /advanced/server/public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/public/mstile-70x70.png -------------------------------------------------------------------------------- /advanced/server/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /advanced/server/rw-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/advanced/server/rw-logo.png -------------------------------------------------------------------------------- /intro/part1/README.md: -------------------------------------------------------------------------------- 1 | # Part 1 2 | 3 | To build everything, `cd` into the `part1/` directory and run: 4 | 5 | ```shell 6 | elm make Main.elm --output elm.js 7 | ``` 8 | 9 | This will compile your Main.elm file into `elm.js`, which gets loaded by 10 | `index.html`. 11 | 12 | Then open `index.html` in your browser. 13 | 14 | ## Exercise 15 | 16 | Open `Main.elm` in your editor and resolve the TODOs there. 17 | 18 | After you complete each one, re-run `elm make Main.elm --output elm.js` to 19 | recompile the `elm.js` file, then refresh the browser to see the results! 20 | -------------------------------------------------------------------------------- /intro/part1/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "." 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0", 10 | "elm/html": "1.0.0" 11 | }, 12 | "indirect": { 13 | "elm/json": "1.0.0", 14 | "elm/virtual-dom": "1.0.0" 15 | } 16 | }, 17 | "test-dependencies": { 18 | "direct": {}, 19 | "indirect": {} 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /intro/part1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Elm Workshop 6 | 7 | 8 | 9 | 10 |
11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /intro/part2/README.md: -------------------------------------------------------------------------------- 1 | # Part 2 2 | 3 | This is just like last time, except `Main.elm` is now in the `src/` directory. 4 | 5 | To build everything, `cd` into the `part2/` directory and run: 6 | 7 | ```shell 8 | elm make src/Main.elm --output elm.js 9 | ``` 10 | 11 | Then open `index.html` in your browser. 12 | 13 | ## Exercise 14 | 15 | Open `src/Main.elm` in your editor and resolve the TODOs there. 16 | -------------------------------------------------------------------------------- /intro/part2/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0", 10 | "elm/html": "1.0.0" 11 | }, 12 | "indirect": { 13 | "elm/json": "1.0.0", 14 | "elm/virtual-dom": "1.0.0" 15 | } 16 | }, 17 | "test-dependencies": { 18 | "direct": {}, 19 | "indirect": {} 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /intro/part2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Elm Workshop 6 | 7 | 8 | 9 | 10 |
11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /intro/part3/README.md: -------------------------------------------------------------------------------- 1 | # Part 3 2 | 3 | This is just like last time, except first we'll install the `elm/browser` package so we can create an interactive app in the browser, instead of static HTML. 4 | 5 | To install the package, `cd` into the `part3/` directory and run: 6 | 7 | ```shell 8 | elm install elm/browser 9 | ``` 10 | 11 | Then build everything the same way as last time: 12 | 13 | ```shell 14 | elm make src/Main.elm --output elm.js 15 | ``` 16 | 17 | Finally, open `index.html` in your browser. 18 | 19 | ## Exercise 20 | 21 | Open `src/Main.elm` in your editor and resolve the TODOs there. 22 | -------------------------------------------------------------------------------- /intro/part3/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0", 10 | "elm/html": "1.0.0" 11 | }, 12 | "indirect": { 13 | "elm/json": "1.0.0", 14 | "elm/time": "1.0.0", 15 | "elm/url": "1.0.0" 16 | } 17 | }, 18 | "test-dependencies": { 19 | "direct": {}, 20 | "indirect": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /intro/part3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Elm Workshop 6 | 7 | 8 | 9 | 10 |
11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /intro/part3/src/Article.elm: -------------------------------------------------------------------------------- 1 | module Article exposing (feed, tags) 2 | 3 | -- For now, this module only holds hardcoded data. 4 | -- 5 | -- In future exercises, it will read data from the server! 6 | 7 | 8 | tags = 9 | [ "elm" 10 | , "fun" 11 | , "programming" 12 | , "dragons" 13 | ] 14 | 15 | 16 | feed = 17 | [ { title = "Elm is fun!", description = "Elm", body = "I've really been enjoying it!", tags = [ "elm", "fun" ], slug = "elm-is-fun--zb6nba" } 18 | , { title = "Who says undefined isn't a function anyway?", description = "Functions", body = "Quite frankly I think undefined can be anything it wants to be,if it believes in itself.", slug = "who-says-undefined-isnt-a-function-anyway-t39ope", tags = [ "programming" ] } 19 | , { title = "This compiler is pretty neat", description = "Elm", body = "It tells me about problems in my code. How neat is that?", tags = [ "compilers", "elm" ], slug = "this-compiler-is-pretty-neat-9ycui8" } 20 | , { title = "Are dragons real?", description = "dragons", body = "Do Komodo Dragons count? I think they should. It's right there in the name!", tags = [ "dragons" ], slug = "are-dragons-real-467lsh" } 21 | ] 22 | -------------------------------------------------------------------------------- /intro/part4/README.md: -------------------------------------------------------------------------------- 1 | # Part 4 2 | 3 | Build everything the same way as last time: 4 | 5 | ```shell 6 | elm make src/Main.elm --output elm.js 7 | ``` 8 | 9 | No need to open `index.html` in your browser, since this time the exercise 10 | is only about compiling! 11 | 12 | ## Exercise 13 | 14 | Open `src/Main.elm` in your editor and resolve the TODOs there. 15 | -------------------------------------------------------------------------------- /intro/part4/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.0", 10 | "elm/core": "1.0.0", 11 | "elm/html": "1.0.0" 12 | }, 13 | "indirect": { 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm/virtual-dom": "1.0.0" 18 | } 19 | }, 20 | "test-dependencies": { 21 | "direct": {}, 22 | "indirect": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /intro/part4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Elm Workshop 6 | 7 | 8 | 9 | 10 |
11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /intro/part4/src/Article.elm: -------------------------------------------------------------------------------- 1 | module Article exposing (feed, tags) 2 | 3 | -- For now, this module only holds hardcoded data. 4 | -- 5 | -- In future exercises, it will read data from the server! 6 | 7 | 8 | tags = 9 | [ "elm" 10 | , "fun" 11 | , "programming" 12 | , "dragons" 13 | ] 14 | 15 | 16 | feed = 17 | [ { title = "Elm is fun!", description = "Elm", body = "I've really been enjoying it!", tags = [ "elm", "fun" ], slug = "elm-is-fun--zb6nba" } 18 | , { title = "Who says undefined isn't a function anyway?", description = "Functions", body = "Quite frankly I think undefined can be anything it wants to be,if it believes in itself.", slug = "who-says-undefined-isnt-a-function-anyway-t39ope", tags = [ "programming" ] } 19 | , { title = "This compiler is pretty neat", description = "Elm", body = "It tells me about problems in my code. How neat is that?", tags = [ "compilers", "elm" ], slug = "this-compiler-is-pretty-neat-9ycui8" } 20 | , { title = "Are dragons real?", description = "dragons", body = "Do Komodo Dragons count? I think they should. It's right there in the name!", tags = [ "dragons" ], slug = "are-dragons-real-467lsh" } 21 | ] 22 | -------------------------------------------------------------------------------- /intro/part5/.gitignore: -------------------------------------------------------------------------------- 1 | # elm-package generated files 2 | elm-stuff/ 3 | # elm-repl generated files 4 | repl-temp-* 5 | 6 | elm.js 7 | -------------------------------------------------------------------------------- /intro/part5/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /intro/part5/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /intro/part5/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /intro/part5/assets/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /intro/part5/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /intro/part5/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /intro/part5/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/icons/favicon.ico -------------------------------------------------------------------------------- /intro/part5/assets/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/icons/mstile-144x144.png -------------------------------------------------------------------------------- /intro/part5/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /intro/part5/assets/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/icons/mstile-310x150.png -------------------------------------------------------------------------------- /intro/part5/assets/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/icons/mstile-310x310.png -------------------------------------------------------------------------------- /intro/part5/assets/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/icons/mstile-70x70.png -------------------------------------------------------------------------------- /intro/part5/assets/images/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part5/assets/images/error.jpg -------------------------------------------------------------------------------- /intro/part5/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /intro/part5/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /intro/part5/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /intro/part5/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /intro/part5/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | 36 | -- SERIALIZATION 37 | 38 | 39 | decoder : Decoder Tag 40 | decoder = 41 | Decode.map Tag Decode.string 42 | -------------------------------------------------------------------------------- /intro/part5/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /intro/part5/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /intro/part5/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /intro/part5/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /intro/part5/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /intro/part5/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /intro/part5/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /intro/part5/src/Profile.elm: -------------------------------------------------------------------------------- 1 | module Profile exposing (Profile, avatar, bio, decoder) 2 | 3 | {-| A user's profile - potentially your own! 4 | 5 | Contrast with Cred, which is the currently signed-in user. 6 | 7 | -} 8 | 9 | import Api 10 | import Avatar exposing (Avatar) 11 | import Http 12 | import HttpBuilder exposing (RequestBuilder, withExpect) 13 | import Json.Decode as Decode exposing (Decoder) 14 | import Json.Decode.Pipeline exposing (required) 15 | import Username exposing (Username) 16 | import Viewer.Cred as Cred exposing (Cred) 17 | 18 | 19 | 20 | -- TYPES 21 | 22 | 23 | type Profile 24 | = Profile Internals 25 | 26 | 27 | type alias Internals = 28 | { bio : Maybe String 29 | , avatar : Avatar 30 | } 31 | 32 | 33 | 34 | -- INFO 35 | 36 | 37 | bio : Profile -> Maybe String 38 | bio (Profile info) = 39 | info.bio 40 | 41 | 42 | avatar : Profile -> Avatar 43 | avatar (Profile info) = 44 | info.avatar 45 | 46 | 47 | 48 | -- SERIALIZATION 49 | 50 | 51 | decoder : Decoder Profile 52 | decoder = 53 | Decode.succeed Internals 54 | |> required "bio" (Decode.nullable Decode.string) 55 | |> required "image" Avatar.decoder 56 | |> Decode.map Profile 57 | -------------------------------------------------------------------------------- /intro/part5/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /intro/part6/.gitignore: -------------------------------------------------------------------------------- 1 | # elm-package generated files 2 | elm-stuff/ 3 | # elm-repl generated files 4 | repl-temp-* 5 | 6 | elm.js 7 | -------------------------------------------------------------------------------- /intro/part6/README.md: -------------------------------------------------------------------------------- 1 | # Part 6 2 | 3 | Like last time, we'll be building `src/Main.elm`, but editing different files. 4 | 5 | To build everything, `cd` into the `part6/` directory and run: 6 | 7 | ```shell 8 | elm make src/Main.elm --output ../server/public/elm.js 9 | ``` 10 | 11 | Then open `http://localhost:3000` in your browser. 12 | 13 | ## Exercise 14 | 15 | There are two TODOs to resolve, in these files. 16 | 17 | 1. `src/Avatar.elm` - go for this one first! 18 | 2. `src/Page/Article/Editor.elm` 19 | -------------------------------------------------------------------------------- /intro/part6/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /intro/part6/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /intro/part6/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /intro/part6/assets/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /intro/part6/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /intro/part6/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /intro/part6/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/icons/favicon.ico -------------------------------------------------------------------------------- /intro/part6/assets/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/icons/mstile-144x144.png -------------------------------------------------------------------------------- /intro/part6/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /intro/part6/assets/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/icons/mstile-310x150.png -------------------------------------------------------------------------------- /intro/part6/assets/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/icons/mstile-310x310.png -------------------------------------------------------------------------------- /intro/part6/assets/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/icons/mstile-70x70.png -------------------------------------------------------------------------------- /intro/part6/assets/images/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part6/assets/images/error.jpg -------------------------------------------------------------------------------- /intro/part6/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /intro/part6/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /intro/part6/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /intro/part6/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /intro/part6/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString, validate) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | validate : String -> List String -> Bool 36 | validate str = 37 | String.split " " str 38 | |> List.map String.trim 39 | |> List.filter (not << String.isEmpty) 40 | |> (==) 41 | 42 | 43 | 44 | -- SERIALIZATION 45 | 46 | 47 | decoder : Decoder Tag 48 | decoder = 49 | Decode.map Tag Decode.string 50 | -------------------------------------------------------------------------------- /intro/part6/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /intro/part6/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /intro/part6/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /intro/part6/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /intro/part6/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /intro/part6/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /intro/part6/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /intro/part7/.gitignore: -------------------------------------------------------------------------------- 1 | # elm-package generated files 2 | elm-stuff/ 3 | # elm-repl generated files 4 | repl-temp-* 5 | 6 | elm.js 7 | -------------------------------------------------------------------------------- /intro/part7/README.md: -------------------------------------------------------------------------------- 1 | # Part 7 2 | 3 | Once again, we'll be building `src/Main.elm`, but editing different files. 4 | 5 | To build everything, `cd` into the `part7/` directory and run: 6 | 7 | ```shell 8 | elm make src/Main.elm --output ../server/public/elm.js 9 | ``` 10 | 11 | Then open `http://localhost:3000` in your browser. 12 | 13 | ## Exercise 14 | 15 | The articles in the feed don't quite look right! 16 | 17 | We'll fix them by editing `src/Article.elm` and resolving the TODO there. 18 | -------------------------------------------------------------------------------- /intro/part7/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /intro/part7/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /intro/part7/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /intro/part7/assets/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /intro/part7/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /intro/part7/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /intro/part7/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/icons/favicon.ico -------------------------------------------------------------------------------- /intro/part7/assets/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/icons/mstile-144x144.png -------------------------------------------------------------------------------- /intro/part7/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /intro/part7/assets/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/icons/mstile-310x150.png -------------------------------------------------------------------------------- /intro/part7/assets/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/icons/mstile-310x310.png -------------------------------------------------------------------------------- /intro/part7/assets/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/icons/mstile-70x70.png -------------------------------------------------------------------------------- /intro/part7/assets/images/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part7/assets/images/error.jpg -------------------------------------------------------------------------------- /intro/part7/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /intro/part7/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /intro/part7/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /intro/part7/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /intro/part7/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | 36 | -- SERIALIZATION 37 | 38 | 39 | decoder : Decoder Tag 40 | decoder = 41 | Decode.map Tag Decode.string 42 | -------------------------------------------------------------------------------- /intro/part7/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /intro/part7/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /intro/part7/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /intro/part7/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /intro/part7/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /intro/part7/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /intro/part7/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /intro/part7/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /intro/part8/.gitignore: -------------------------------------------------------------------------------- 1 | # elm-package generated files 2 | elm-stuff/ 3 | # elm-repl generated files 4 | repl-temp-* 5 | 6 | elm.js 7 | -------------------------------------------------------------------------------- /intro/part8/README.md: -------------------------------------------------------------------------------- 1 | # Part 8 2 | 3 | Once again, we'll be building `src/Main.elm`, but editing a different file. 4 | 5 | To build everything, `cd` into the `part8/` directory and run8 6 | 7 | ```shell 8 | elm make src/Main.elm --output ../server/public/elm.js 9 | ``` 10 | 11 | Then open `http://localhost:3000` in your browser. 12 | 13 | ## Exercise 14 | 15 | We need to make signup work again. Currently it doesn't actually send a HTTP request to the server. 16 | 17 | We'll fix this by editing `src/Page/Register.elm` and resolving the TODOs there. 18 | -------------------------------------------------------------------------------- /intro/part8/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /intro/part8/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /intro/part8/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /intro/part8/assets/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /intro/part8/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /intro/part8/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /intro/part8/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/icons/favicon.ico -------------------------------------------------------------------------------- /intro/part8/assets/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/icons/mstile-144x144.png -------------------------------------------------------------------------------- /intro/part8/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /intro/part8/assets/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/icons/mstile-310x150.png -------------------------------------------------------------------------------- /intro/part8/assets/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/icons/mstile-310x310.png -------------------------------------------------------------------------------- /intro/part8/assets/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/icons/mstile-70x70.png -------------------------------------------------------------------------------- /intro/part8/assets/images/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part8/assets/images/error.jpg -------------------------------------------------------------------------------- /intro/part8/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /intro/part8/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /intro/part8/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /intro/part8/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /intro/part8/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | 36 | -- SERIALIZATION 37 | 38 | 39 | decoder : Decoder Tag 40 | decoder = 41 | Decode.map Tag Decode.string 42 | -------------------------------------------------------------------------------- /intro/part8/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /intro/part8/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /intro/part8/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /intro/part8/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /intro/part8/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /intro/part8/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /intro/part8/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /intro/part8/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /intro/part9/.gitignore: -------------------------------------------------------------------------------- 1 | # elm-package generated files 2 | elm-stuff/ 3 | # elm-repl generated files 4 | repl-temp-* 5 | 6 | elm.js 7 | -------------------------------------------------------------------------------- /intro/part9/README.md: -------------------------------------------------------------------------------- 1 | # Part 9 2 | 3 | Once again, we'll be building `src/Main.elm`, but editing a different file. 4 | 5 | To build everything, `cd` into the `part9/` directory and run: 6 | 7 | ```shell 8 | elm make src/Main.elm --output ../server/public/elm.js 9 | ``` 10 | 11 | Then open `http://localhost:3000` in your browser. 12 | 13 | ## Exercise 14 | 15 | Look at `intro/server/public/index.html` to see how the localforage ports are 16 | hooked up on the JavaScript side. 17 | 18 | The exercise is to add the corresponding ports to `src/Session.elm`. See the 19 | TODOs in that file for more information! 20 | 21 | When you're done, you should once again be able to sign up for a new account. 22 | -------------------------------------------------------------------------------- /intro/part9/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /intro/part9/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /intro/part9/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /intro/part9/assets/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /intro/part9/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /intro/part9/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /intro/part9/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/icons/favicon.ico -------------------------------------------------------------------------------- /intro/part9/assets/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/icons/mstile-144x144.png -------------------------------------------------------------------------------- /intro/part9/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /intro/part9/assets/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/icons/mstile-310x150.png -------------------------------------------------------------------------------- /intro/part9/assets/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/icons/mstile-310x310.png -------------------------------------------------------------------------------- /intro/part9/assets/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/icons/mstile-70x70.png -------------------------------------------------------------------------------- /intro/part9/assets/images/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/part9/assets/images/error.jpg -------------------------------------------------------------------------------- /intro/part9/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /intro/part9/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0" 20 | }, 21 | "indirect": { 22 | "elm/parser": "1.0.0", 23 | "elm/regex": "1.0.0", 24 | "elm/virtual-dom": "1.0.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": { 29 | "elm-explorations/test": "1.0.0" 30 | }, 31 | "indirect": { 32 | "elm/random": "1.0.0" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /intro/part9/src/Article/Body.elm: -------------------------------------------------------------------------------- 1 | module Article.Body exposing (Body, MarkdownString, decoder, toHtml, toMarkdownString) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Markdown 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Body 13 | = Body MarkdownString 14 | 15 | 16 | {-| Internal use only. I want to remind myself that the string inside Body contains markdown. 17 | -} 18 | type alias MarkdownString = 19 | String 20 | 21 | 22 | 23 | -- CONVERSIONS 24 | 25 | 26 | toHtml : Body -> List (Attribute msg) -> Html msg 27 | toHtml (Body markdown) attributes = 28 | Markdown.toHtml attributes markdown 29 | 30 | 31 | toMarkdownString : Body -> MarkdownString 32 | toMarkdownString (Body markdown) = 33 | markdown 34 | 35 | 36 | decoder : Decoder Body 37 | decoder = 38 | Decode.map Body Decode.string 39 | -------------------------------------------------------------------------------- /intro/part9/src/Article/Slug.elm: -------------------------------------------------------------------------------- 1 | module Article.Slug exposing (Slug, decoder, toString, urlParser) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | import Url.Parser exposing (Parser) 5 | 6 | 7 | 8 | -- TYPES 9 | 10 | 11 | type Slug 12 | = Slug String 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | urlParser : Parser (Slug -> a) a 20 | urlParser = 21 | Url.Parser.custom "SLUG" (\str -> Just (Slug str)) 22 | 23 | 24 | decoder : Decoder Slug 25 | decoder = 26 | Decode.map Slug Decode.string 27 | 28 | 29 | 30 | -- TRANSFORM 31 | 32 | 33 | toString : Slug -> String 34 | toString (Slug str) = 35 | str 36 | -------------------------------------------------------------------------------- /intro/part9/src/Article/Tag.elm: -------------------------------------------------------------------------------- 1 | module Article.Tag exposing (Tag, list, toString) 2 | 3 | import Api 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | 7 | 8 | 9 | -- TYPES 10 | 11 | 12 | type Tag 13 | = Tag String 14 | 15 | 16 | 17 | -- TRANSFORM 18 | 19 | 20 | toString : Tag -> String 21 | toString (Tag slug) = 22 | slug 23 | 24 | 25 | 26 | -- LIST 27 | 28 | 29 | list : Http.Request (List Tag) 30 | list = 31 | Decode.field "tags" (Decode.list decoder) 32 | |> Http.get (Api.url [ "tags" ]) 33 | 34 | 35 | 36 | -- SERIALIZATION 37 | 38 | 39 | decoder : Decoder Tag 40 | decoder = 41 | Decode.map Tag Decode.string 42 | -------------------------------------------------------------------------------- /intro/part9/src/Asset.elm: -------------------------------------------------------------------------------- 1 | module Asset exposing (Image, defaultAvatar, error, loading, src) 2 | 3 | {-| Assets, such as images, videos, and audio. (We only have images for now.) 4 | 5 | We should never expose asset URLs directly; this module should be in charge of 6 | all of them. One source of truth! 7 | 8 | -} 9 | 10 | import Html exposing (Attribute, Html) 11 | import Html.Attributes as Attr 12 | 13 | 14 | type Image 15 | = Image String 16 | 17 | 18 | 19 | -- IMAGES 20 | 21 | 22 | error : Image 23 | error = 24 | image "error.jpg" 25 | 26 | 27 | loading : Image 28 | loading = 29 | image "loading.svg" 30 | 31 | 32 | defaultAvatar : Image 33 | defaultAvatar = 34 | image "smiley-cyrus.jpg" 35 | 36 | 37 | image : String -> Image 38 | image filename = 39 | Image ("/assets/images/" ++ filename) 40 | 41 | 42 | 43 | -- USING IMAGES 44 | 45 | 46 | src : Image -> Attribute msg 47 | src (Image url) = 48 | Attr.src url 49 | -------------------------------------------------------------------------------- /intro/part9/src/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Avatar exposing (Avatar, decoder, encode, src, toMaybeString) 2 | 3 | import Asset 4 | import Html exposing (Attribute) 5 | import Html.Attributes 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Encode as Encode exposing (Value) 8 | 9 | 10 | 11 | -- TYPES 12 | 13 | 14 | type Avatar 15 | = Avatar (Maybe String) 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | decoder : Decoder Avatar 23 | decoder = 24 | Decode.map Avatar (Decode.nullable Decode.string) 25 | 26 | 27 | 28 | -- TRANSFORM 29 | 30 | 31 | encode : Avatar -> Value 32 | encode (Avatar maybeUrl) = 33 | case maybeUrl of 34 | Just url -> 35 | Encode.string url 36 | 37 | Nothing -> 38 | Encode.null 39 | 40 | 41 | src : Avatar -> Attribute msg 42 | src (Avatar maybeUrl) = 43 | case maybeUrl of 44 | Nothing -> 45 | Asset.src Asset.defaultAvatar 46 | 47 | Just "" -> 48 | Asset.src Asset.defaultAvatar 49 | 50 | Just url -> 51 | Html.Attributes.src url 52 | 53 | 54 | toMaybeString : Avatar -> Maybe String 55 | toMaybeString (Avatar maybeUrl) = 56 | maybeUrl 57 | -------------------------------------------------------------------------------- /intro/part9/src/CommentId.elm: -------------------------------------------------------------------------------- 1 | module CommentId exposing (CommentId, decoder, toString) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | 7 | -- TYPES 8 | 9 | 10 | type CommentId 11 | = CommentId Int 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | decoder : Decoder CommentId 19 | decoder = 20 | Decode.map CommentId Decode.int 21 | 22 | 23 | 24 | -- TRANSFORM 25 | 26 | 27 | toString : CommentId -> String 28 | toString (CommentId id) = 29 | String.fromInt id 30 | -------------------------------------------------------------------------------- /intro/part9/src/Loading.elm: -------------------------------------------------------------------------------- 1 | module Loading exposing (error, icon, slowThreshold) 2 | 3 | {-| A loading spinner icon. 4 | -} 5 | 6 | import Asset 7 | import Html exposing (Attribute, Html) 8 | import Html.Attributes exposing (alt, height, src, width) 9 | import Process 10 | import Task exposing (Task) 11 | 12 | 13 | icon : Html msg 14 | icon = 15 | Html.img [ Asset.src Asset.loading, width 64, height 64, alt "Loading..." ] [] 16 | 17 | 18 | error : String -> Html msg 19 | error str = 20 | Html.text ("Error loading " ++ str ++ ".") 21 | 22 | 23 | slowThreshold : Task x () 24 | slowThreshold = 25 | Process.sleep 500 26 | -------------------------------------------------------------------------------- /intro/part9/src/Log.elm: -------------------------------------------------------------------------------- 1 | module Log exposing (error) 2 | 3 | {-| This is a placeholder API for how we might do logging through 4 | some service like (which is what we use at work). 5 | 6 | Whenever you see Log.error used in this code base, it means 7 | "Something unexpected happened. This is where we would log an 8 | error to our server with some diagnostic info so we could investigate 9 | what happened later." 10 | 11 | (Since this is outside the scope of the RealWorld spec, and is only 12 | a placeholder anyway, I didn't bother making this function accept actual 13 | diagnostic info, authentication tokens, etc.) 14 | 15 | -} 16 | 17 | 18 | error : Cmd msg 19 | error = 20 | Cmd.none 21 | -------------------------------------------------------------------------------- /intro/part9/src/Page/Blank.elm: -------------------------------------------------------------------------------- 1 | module Page.Blank exposing (view) 2 | 3 | import Html exposing (Html) 4 | 5 | 6 | view : { title : String, content : Html msg } 7 | view = 8 | { title = "" 9 | , content = Html.text "" 10 | } 11 | -------------------------------------------------------------------------------- /intro/part9/src/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Asset 4 | import Html exposing (Html, div, h1, img, main_, text) 5 | import Html.Attributes exposing (alt, class, id, src, tabindex) 6 | 7 | 8 | 9 | -- VIEW 10 | 11 | 12 | view : { title : String, content : Html msg } 13 | view = 14 | { title = "Page Not Found" 15 | , content = 16 | main_ [ id "content", class "container", tabindex -1 ] 17 | [ h1 [] [ text "Not Found" ] 18 | , div [ class "row" ] 19 | [ img [ Asset.src Asset.error ] [] ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /intro/part9/src/Username.elm: -------------------------------------------------------------------------------- 1 | module Username exposing (Username, decoder, encode, toHtml, toString, urlParser) 2 | 3 | import Html exposing (Html) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Json.Encode as Encode exposing (Value) 6 | import Url.Parser 7 | 8 | 9 | 10 | -- TYPES 11 | 12 | 13 | type Username 14 | = Username String 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | decoder : Decoder Username 22 | decoder = 23 | Decode.map Username Decode.string 24 | 25 | 26 | 27 | -- TRANSFORM 28 | 29 | 30 | encode : Username -> Value 31 | encode (Username username) = 32 | Encode.string username 33 | 34 | 35 | toString : Username -> String 36 | toString (Username username) = 37 | username 38 | 39 | 40 | urlParser : Url.Parser.Parser (Username -> a) a 41 | urlParser = 42 | Url.Parser.custom "USERNAME" (\str -> Just (Username str)) 43 | 44 | 45 | toHtml : Username -> Html msg 46 | toHtml (Username username) = 47 | Html.text username 48 | -------------------------------------------------------------------------------- /intro/server/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | coverage/ 4 | npm-debug.log 5 | stats.json 6 | yarn-error.log 7 | -------------------------------------------------------------------------------- /intro/server/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ice Services 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /intro/server/data/comments.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/data/comments.db -------------------------------------------------------------------------------- /intro/server/data/favorites.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/data/favorites.db -------------------------------------------------------------------------------- /intro/server/data/follows.db: -------------------------------------------------------------------------------- 1 | {"follow":"Z1YiwpVIz2GQQ13Q","user":"gsZdyqSGbQscoIU6","createdAt":{"$$date":1525550850919},"_id":"f8Yep9EguoPa8Rwn"} 2 | -------------------------------------------------------------------------------- /intro/server/data/users.db: -------------------------------------------------------------------------------- 1 | {"username":"rtfeldman","email":"richard.t.feldman@gmail.com","password":"$2a$10$4WPM2GSk3pH8COW1UVAVEeu2OC7/CrCzjR91PcPZIw/Ycdvyg3Aqi","bio":"","image":null,"createdAt":{"$$date":1533459071196},"_id":"7r8Pdo5csPGElToj"} 2 | {"username":"SamSample","email":"sam@sample.com","password":"samsample","bio":"I'm the sample user for the workshop. Hi!","image":"https://user-images.githubusercontent.com/1094080/39663282-6459c64e-503e-11e8-8da8-a2af2c81d052.png","createdAt":{"$$date":1525523183044},"_id":"Z1YiwpVIz2GQQ13Q","updatedAt":{"$$date":1525523378471}} 3 | {"username":"wefjbsfdjkhdgrskjhsdfgjhb","email":"sdfgjhsgjhbdfgkjhsdf@sfgdhjfjhgdfgf.com","password":"$2a$10$gnFdddybmitDP.yKM3OjfubgZ1O3geK9N8LymFV4mZYaSGe9WYPby","bio":"","image":null,"createdAt":{"$$date":1525546056406},"_id":"gsZdyqSGbQscoIU6"} 4 | -------------------------------------------------------------------------------- /intro/server/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm-explorations/markdown": "1.0.0", 18 | "lukewestby/elm-http-builder": "6.0.0", 19 | "rtfeldman/elm-iso8601-date-strings": "1.0.0", 20 | "rtfeldman/elm-validate": "4.0.0" 21 | }, 22 | "indirect": { 23 | "elm/parser": "1.0.0", 24 | "elm/regex": "1.0.0", 25 | "elm/virtual-dom": "1.0.0" 26 | } 27 | }, 28 | "test-dependencies": { 29 | "direct": {}, 30 | "indirect": {} 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /intro/server/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/logo.png -------------------------------------------------------------------------------- /intro/server/mixins/db.mixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const mkdir = require("mkdirp").sync; 5 | 6 | const DbService = require("moleculer-db"); 7 | 8 | //process.env.MONGO_URI = "mongodb://localhost/conduit"; 9 | 10 | module.exports = function(collection) { 11 | if (process.env.MONGO_URI) { 12 | // Mongo adapter 13 | const MongoAdapter = require("moleculer-db-adapter-mongo"); 14 | 15 | return { 16 | mixins: [DbService], 17 | adapter: new MongoAdapter(process.env.MONGO_URI), 18 | collection 19 | }; 20 | } 21 | 22 | // --- NeDB fallback DB adapter 23 | 24 | // Create data folder 25 | mkdir(path.resolve("./data")); 26 | 27 | return { 28 | mixins: [DbService], 29 | adapter: new DbService.MemoryAdapter({ filename: `./data/${collection}.db` }) 30 | }; 31 | }; -------------------------------------------------------------------------------- /intro/server/moleculer.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const os = require("os"); 4 | 5 | module.exports = { 6 | // It will be unique when scale up instances in Docker or on local computer 7 | nodeID: os.hostname().toLowerCase() + "-" + process.pid, 8 | 9 | logger: false, 10 | logLevel: "info", 11 | 12 | cacher: "memory", 13 | 14 | metrics: false 15 | }; 16 | -------------------------------------------------------------------------------- /intro/server/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /intro/server/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /intro/server/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/apple-touch-icon.png -------------------------------------------------------------------------------- /intro/server/public/assets/images/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/assets/images/error.jpg -------------------------------------------------------------------------------- /intro/server/public/assets/images/smiley-cyrus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/assets/images/smiley-cyrus.jpg -------------------------------------------------------------------------------- /intro/server/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /intro/server/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/favicon-16x16.png -------------------------------------------------------------------------------- /intro/server/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/favicon-32x32.png -------------------------------------------------------------------------------- /intro/server/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/favicon.ico -------------------------------------------------------------------------------- /intro/server/public/images/smiley-cyrus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/images/smiley-cyrus.jpg -------------------------------------------------------------------------------- /intro/server/public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/mstile-144x144.png -------------------------------------------------------------------------------- /intro/server/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/mstile-150x150.png -------------------------------------------------------------------------------- /intro/server/public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/mstile-310x150.png -------------------------------------------------------------------------------- /intro/server/public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/mstile-310x310.png -------------------------------------------------------------------------------- /intro/server/public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/public/mstile-70x70.png -------------------------------------------------------------------------------- /intro/server/rw-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/elm-0.19-workshop/dfe4cc02d9dde15fc4fe844349e3f4a84dd4f4da/intro/server/rw-logo.png --------------------------------------------------------------------------------