From 9f3d21bfbf0c3e00b96bc7eb526b7f13517f4bf4 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 5 Mar 2016 08:01:14 -0800 Subject: [PATCH] Add lots more content. --- stages/1/.gitignore | 2 + stages/1/README.md | 22 +++++ stages/1/elm-package.json | 21 +++++ stages/1/index.html | 25 ++++++ stages/1/package.json | 25 ++++++ stages/1/src/ElmHub.elm | 160 ++++++++++++++++++++++++++++++++++ stages/1/style.css | 91 +++++++++++++++++++ stages/1/test/TestRunner.elm | 15 ++++ stages/1/test/Tests.elm | 35 ++++++++ stages/2/.gitignore | 2 + stages/2/README.md | 22 +++++ stages/2/elm-package.json | 21 +++++ stages/2/index.html | 25 ++++++ stages/2/package.json | 25 ++++++ stages/2/src/ElmHub.elm | 160 ++++++++++++++++++++++++++++++++++ stages/2/style.css | 91 +++++++++++++++++++ stages/2/test/TestRunner.elm | 15 ++++ stages/2/test/Tests.elm | 35 ++++++++ stages/3/src/ElmHub.elm | 93 +++++++------------- stages/3/style.css | 17 ++++ stages/4/.gitignore | 2 + stages/4/README.md | 22 +++++ stages/4/elm-package.json | 21 +++++ stages/4/index.html | 25 ++++++ stages/4/package.json | 25 ++++++ stages/4/src/ElmHub.elm | 163 +++++++++++++++++++++++++++++++++++ stages/4/style.css | 91 +++++++++++++++++++ stages/4/test/TestRunner.elm | 15 ++++ stages/4/test/Tests.elm | 35 ++++++++ stages/5/.gitignore | 2 + stages/5/README.md | 22 +++++ stages/5/elm-package.json | 21 +++++ stages/5/index.html | 25 ++++++ stages/5/package.json | 25 ++++++ stages/5/src/ElmHub.elm | 163 +++++++++++++++++++++++++++++++++++ stages/5/style.css | 91 +++++++++++++++++++ stages/5/test/TestRunner.elm | 15 ++++ stages/5/test/Tests.elm | 35 ++++++++ 38 files changed, 1640 insertions(+), 60 deletions(-) create mode 100644 stages/1/.gitignore create mode 100644 stages/1/README.md create mode 100644 stages/1/elm-package.json create mode 100644 stages/1/index.html create mode 100644 stages/1/package.json create mode 100644 stages/1/src/ElmHub.elm create mode 100644 stages/1/style.css create mode 100644 stages/1/test/TestRunner.elm create mode 100644 stages/1/test/Tests.elm create mode 100644 stages/2/.gitignore create mode 100644 stages/2/README.md create mode 100644 stages/2/elm-package.json create mode 100644 stages/2/index.html create mode 100644 stages/2/package.json create mode 100644 stages/2/src/ElmHub.elm create mode 100644 stages/2/style.css create mode 100644 stages/2/test/TestRunner.elm create mode 100644 stages/2/test/Tests.elm create mode 100644 stages/4/.gitignore create mode 100644 stages/4/README.md create mode 100644 stages/4/elm-package.json create mode 100644 stages/4/index.html create mode 100644 stages/4/package.json create mode 100644 stages/4/src/ElmHub.elm create mode 100644 stages/4/style.css create mode 100644 stages/4/test/TestRunner.elm create mode 100644 stages/4/test/Tests.elm create mode 100644 stages/5/.gitignore create mode 100644 stages/5/README.md create mode 100644 stages/5/elm-package.json create mode 100644 stages/5/index.html create mode 100644 stages/5/package.json create mode 100644 stages/5/src/ElmHub.elm create mode 100644 stages/5/style.css create mode 100644 stages/5/test/TestRunner.elm create mode 100644 stages/5/test/Tests.elm diff --git a/stages/1/.gitignore b/stages/1/.gitignore new file mode 100644 index 0000000..3bd52a1 --- /dev/null +++ b/stages/1/.gitignore @@ -0,0 +1,2 @@ +elm-stuff +elm.js diff --git a/stages/1/README.md b/stages/1/README.md new file mode 100644 index 0000000..45dfaf0 --- /dev/null +++ b/stages/1/README.md @@ -0,0 +1,22 @@ +Stage 3 +======= + +## Install the Project + +Run this at the terminal: + +```bash +npm install +``` + +## Run Tests + +```bash +npm test +``` + +## Engage Auto-Rebuilding + +```bash +npm run watch +``` diff --git a/stages/1/elm-package.json b/stages/1/elm-package.json new file mode 100644 index 0000000..c5b0298 --- /dev/null +++ b/stages/1/elm-package.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0", + "summary": "Like GitHub, but for Elm stuff.", + "repository": "https://github.com/rtfeldman/elm-workshop.git", + "license": "BSD-3-Clause", + "source-directories": [ + "src", + "test" + ], + "exposed-modules": [], + "dependencies": { + "deadfoxygrandpa/elm-test": "3.1.1 <= v < 4.0.0", + "elm-lang/core": "3.0.0 <= v < 4.0.0", + "evancz/elm-effects": "2.0.0 <= v < 3.0.0", + "evancz/elm-html": "4.0.0 <= v < 5.0.0", + "evancz/elm-http": "3.0.0 <= v < 4.0.0", + "evancz/start-app": "2.0.0 <= v < 3.0.0", + "laszlopandy/elm-console": "1.0.3 <= v < 2.0.0" + }, + "elm-version": "0.16.0 <= v < 0.17.0" +} diff --git a/stages/1/index.html b/stages/1/index.html new file mode 100644 index 0000000..f3d5dad --- /dev/null +++ b/stages/1/index.html @@ -0,0 +1,25 @@ + + + + + + ElmHub + + + + + + + + + + + + + + diff --git a/stages/1/package.json b/stages/1/package.json new file mode 100644 index 0000000..66fe54f --- /dev/null +++ b/stages/1/package.json @@ -0,0 +1,25 @@ +{ + "name": "elm-hub", + "version": "1.0.0", + "description": "Like GitHub, but for Elm stuff.", + "scripts": { + "build": "elm-make src/ElmHub.elm --output elm.js", + "watch": "elm-live src/ElmHub.elm --open -- --output=elm.js", + "test": "elm-test test/TestRunner.elm", + "install": "elm-package install --yes && npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rtfeldman/elm-workshop.git" + }, + "author": "Richard Feldman", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/rtfeldman/elm-workshop/issues" + }, + "homepage": "https://github.com/rtfeldman/elm-workshop#readme", + "devDependencies": { + "elm-live": "2.0.4", + "elm-test": "0.16.1-alpha3" + } +} diff --git a/stages/1/src/ElmHub.elm b/stages/1/src/ElmHub.elm new file mode 100644 index 0000000..3e98377 --- /dev/null +++ b/stages/1/src/ElmHub.elm @@ -0,0 +1,160 @@ +module ElmHub (..) where + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import StartApp +import Http +import Task exposing (Task) +import Effects exposing (Effects) +import Json.Decode exposing (Decoder, (:=)) +import Json.Encode +import Signal exposing (Address) + + +main : Signal Html +main = + app.html + + +app : StartApp.App Model +app = + StartApp.start + { view = view + , update = update + , init = ( initialModel, Effects.task (searchFeed initialModel.query) ) + , inputs = [] + } + + +port tasks : Signal (Task Effects.Never ()) +port tasks = + app.tasks + + +searchFeed : String -> Task x Action +searchFeed query = + let + -- See https://developer.github.com/v3/search/#example for how to customize! + url = + "https://api.github.com/search/repositories?q=" + ++ query + ++ "+language:elm&sort=stars&order=desc" + + task = + Http.get responseDecoder url + |> Task.map SetResults + in + Task.onError task (\_ -> Task.succeed (SetResults [])) + + +responseDecoder : Decoder (List SearchResult) +responseDecoder = + "items" := Json.Decode.list searchResultDecoder + + +searchResultDecoder : Decoder SearchResult +searchResultDecoder = + Json.Decode.object3 + SearchResult + ("id" := Json.Decode.int) + ("full_name" := Json.Decode.string) + ("stargazers_count" := Json.Decode.int) + + +type alias Model = + { query : String + , results : List SearchResult + } + + +type alias SearchResult = + { id : ResultId + , name : String + , stars : Int + } + + +type alias ResultId = + Int + + +initialModel : Model +initialModel = + { query = "tutorial" + , results = [] + } + + +view : Address Action -> Model -> Html +view address model = + div + [ class "content" ] + [ header + [] + [ h1 [] [ text "ElmHub" ] + , span [ class "tagline" ] [ text "“Like GitHub, but for Elm things.”" ] + ] + , input [ class "search-query", onInput address SetQuery, defaultValue model.query ] [] + , button [ class "search-button", onClick address Search ] [ text "Search" ] + , ul + [ class "results" ] + (List.map viewSearchResult model.results) + ] + + +onInput address wrap = + on "input" targetValue (\val -> Signal.message address (wrap val)) + + +defaultValue str = + property "defaultValue" (Json.Encode.string str) + + +viewSearchResult : SearchResult -> Html +viewSearchResult result = + li + [] + [ span [ class "star-count" ] [ text (toString result.stars) ] + , a + [ href ("https://github.com/" ++ result.name) + , class "result-name" + , target "_blank" + ] + [ text result.name ] + ] + + +type Action + = Search + | SetQuery String + | HideById ResultId + | SetResults (List SearchResult) + + +update : Action -> Model -> ( Model, Effects Action ) +update action model = + case action of + Search -> + ( model, Effects.task (searchFeed (Debug.log "searching for" model.query)) ) + + SetQuery query -> + ( { model | query = query }, Effects.none ) + + SetResults results -> + let + newModel = + { model | results = results } + in + ( newModel, Effects.none ) + + HideById idToHide -> + let + newResults = + model.results + |> List.filter (\{ id } -> id /= idToHide) + + newModel = + { model | results = newResults } + in + ( newModel, Effects.none ) diff --git a/stages/1/style.css b/stages/1/style.css new file mode 100644 index 0000000..8ee7f3b --- /dev/null +++ b/stages/1/style.css @@ -0,0 +1,91 @@ + +.content { + width: 960px; + margin: 0 auto; + padding: 30px; + font-family: Helvetica, Arial, serif; +} + +header { + position: relative; + padding: 6px 12px; + background-color: rgb(96, 181, 204); +} + +h1 { + color: white; + font-weight: normal; + margin: 0; +} + +.tagline { + color: #eee; + position: absolute; + right: 16px; + top: 12px; + font-size: 24px; + font-style: italic; +} + +.results { + list-style-image: url('http://img-cache.cdn.gaiaonline.com/76bd5c99d8f2236e9d3672510e933fdf/http://i278.photobucket.com/albums/kk81/d3m3nt3dpr3p/Tiny-Star-Icon.png'); + list-style-position: inside; + padding: 0; +} + +.results li { + font-size: 18px; + margin-bottom: 16px; +} + +.star-count { + font-weight: bold; +} + +.result-name { + color: rgb(96, 181, 204); + margin-left: 16px; + text-decoration: none; +} + +.result-name:hover{ + text-decoration: underline; +} + +.search-query { + padding: 8px; + font-size: 24px; + margin-bottom: 18px; + margin-top: 36px; +} + +.search-button { + padding: 8px 16px; + font-size: 24px; + color: white; + border: 1px solid #ccc; + background-color: rgb(96, 181, 204); + margin-left: 12px +} + +.search-button:hover { + color: rgb(96, 181, 204); + background-color: white; +} + +.hide-result { + background-color: transparent; + border: 0; + font-weight: bold; + font-size: 18px; + margin-left: 18px; + cursor: pointer; +} + +.hide-result:hover { + color: rgb(96, 181, 204); +} + +button:focus, input:focus { + outline: none; +} diff --git a/stages/1/test/TestRunner.elm b/stages/1/test/TestRunner.elm new file mode 100644 index 0000000..0baa6f2 --- /dev/null +++ b/stages/1/test/TestRunner.elm @@ -0,0 +1,15 @@ +module Main where + +import Signal exposing (Signal) + +import ElmTest exposing (consoleRunner) +import Console exposing (IO, run) +import Task + +import Tests + +console : IO () +console = consoleRunner Tests.all + +port runner : Signal (Task.Task x ()) +port runner = run console diff --git a/stages/1/test/Tests.elm b/stages/1/test/Tests.elm new file mode 100644 index 0000000..7c06fe2 --- /dev/null +++ b/stages/1/test/Tests.elm @@ -0,0 +1,35 @@ +module Tests (..) where + +import ElmTest exposing (..) +import ElmHub exposing (responseDecoder) +import Json.Decode exposing (decodeString) + + +all : Test +all = + suite + "Decoding responses from GitHub" + [ test "they can decode empty responses" + <| let + emptyResponse = + """{ "items": [] }""" + in + assertEqual + (decodeString responseDecoder emptyResponse) + (Ok []) + , test "they can decode responses with results in them" + <| let + response = + """{ "items": [ + { "id": 5, "full_name": "foo", "stargazers_count": 42 }, + { "id": 3, "full_name": "bar", "stargazers_count": 77 } + ] }""" + in + assertEqual + (decodeString responseDecoder response) + (Ok + [ { id = 5, name = "foo", stars = 42 } + , { id = 3, name = "bar", stars = 77 } + ] + ) + ] diff --git a/stages/2/.gitignore b/stages/2/.gitignore new file mode 100644 index 0000000..3bd52a1 --- /dev/null +++ b/stages/2/.gitignore @@ -0,0 +1,2 @@ +elm-stuff +elm.js diff --git a/stages/2/README.md b/stages/2/README.md new file mode 100644 index 0000000..45dfaf0 --- /dev/null +++ b/stages/2/README.md @@ -0,0 +1,22 @@ +Stage 3 +======= + +## Install the Project + +Run this at the terminal: + +```bash +npm install +``` + +## Run Tests + +```bash +npm test +``` + +## Engage Auto-Rebuilding + +```bash +npm run watch +``` diff --git a/stages/2/elm-package.json b/stages/2/elm-package.json new file mode 100644 index 0000000..c5b0298 --- /dev/null +++ b/stages/2/elm-package.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0", + "summary": "Like GitHub, but for Elm stuff.", + "repository": "https://github.com/rtfeldman/elm-workshop.git", + "license": "BSD-3-Clause", + "source-directories": [ + "src", + "test" + ], + "exposed-modules": [], + "dependencies": { + "deadfoxygrandpa/elm-test": "3.1.1 <= v < 4.0.0", + "elm-lang/core": "3.0.0 <= v < 4.0.0", + "evancz/elm-effects": "2.0.0 <= v < 3.0.0", + "evancz/elm-html": "4.0.0 <= v < 5.0.0", + "evancz/elm-http": "3.0.0 <= v < 4.0.0", + "evancz/start-app": "2.0.0 <= v < 3.0.0", + "laszlopandy/elm-console": "1.0.3 <= v < 2.0.0" + }, + "elm-version": "0.16.0 <= v < 0.17.0" +} diff --git a/stages/2/index.html b/stages/2/index.html new file mode 100644 index 0000000..f3d5dad --- /dev/null +++ b/stages/2/index.html @@ -0,0 +1,25 @@ + + + + + + ElmHub + + + + + + + + + + + + + + diff --git a/stages/2/package.json b/stages/2/package.json new file mode 100644 index 0000000..66fe54f --- /dev/null +++ b/stages/2/package.json @@ -0,0 +1,25 @@ +{ + "name": "elm-hub", + "version": "1.0.0", + "description": "Like GitHub, but for Elm stuff.", + "scripts": { + "build": "elm-make src/ElmHub.elm --output elm.js", + "watch": "elm-live src/ElmHub.elm --open -- --output=elm.js", + "test": "elm-test test/TestRunner.elm", + "install": "elm-package install --yes && npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rtfeldman/elm-workshop.git" + }, + "author": "Richard Feldman", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/rtfeldman/elm-workshop/issues" + }, + "homepage": "https://github.com/rtfeldman/elm-workshop#readme", + "devDependencies": { + "elm-live": "2.0.4", + "elm-test": "0.16.1-alpha3" + } +} diff --git a/stages/2/src/ElmHub.elm b/stages/2/src/ElmHub.elm new file mode 100644 index 0000000..3e98377 --- /dev/null +++ b/stages/2/src/ElmHub.elm @@ -0,0 +1,160 @@ +module ElmHub (..) where + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import StartApp +import Http +import Task exposing (Task) +import Effects exposing (Effects) +import Json.Decode exposing (Decoder, (:=)) +import Json.Encode +import Signal exposing (Address) + + +main : Signal Html +main = + app.html + + +app : StartApp.App Model +app = + StartApp.start + { view = view + , update = update + , init = ( initialModel, Effects.task (searchFeed initialModel.query) ) + , inputs = [] + } + + +port tasks : Signal (Task Effects.Never ()) +port tasks = + app.tasks + + +searchFeed : String -> Task x Action +searchFeed query = + let + -- See https://developer.github.com/v3/search/#example for how to customize! + url = + "https://api.github.com/search/repositories?q=" + ++ query + ++ "+language:elm&sort=stars&order=desc" + + task = + Http.get responseDecoder url + |> Task.map SetResults + in + Task.onError task (\_ -> Task.succeed (SetResults [])) + + +responseDecoder : Decoder (List SearchResult) +responseDecoder = + "items" := Json.Decode.list searchResultDecoder + + +searchResultDecoder : Decoder SearchResult +searchResultDecoder = + Json.Decode.object3 + SearchResult + ("id" := Json.Decode.int) + ("full_name" := Json.Decode.string) + ("stargazers_count" := Json.Decode.int) + + +type alias Model = + { query : String + , results : List SearchResult + } + + +type alias SearchResult = + { id : ResultId + , name : String + , stars : Int + } + + +type alias ResultId = + Int + + +initialModel : Model +initialModel = + { query = "tutorial" + , results = [] + } + + +view : Address Action -> Model -> Html +view address model = + div + [ class "content" ] + [ header + [] + [ h1 [] [ text "ElmHub" ] + , span [ class "tagline" ] [ text "“Like GitHub, but for Elm things.”" ] + ] + , input [ class "search-query", onInput address SetQuery, defaultValue model.query ] [] + , button [ class "search-button", onClick address Search ] [ text "Search" ] + , ul + [ class "results" ] + (List.map viewSearchResult model.results) + ] + + +onInput address wrap = + on "input" targetValue (\val -> Signal.message address (wrap val)) + + +defaultValue str = + property "defaultValue" (Json.Encode.string str) + + +viewSearchResult : SearchResult -> Html +viewSearchResult result = + li + [] + [ span [ class "star-count" ] [ text (toString result.stars) ] + , a + [ href ("https://github.com/" ++ result.name) + , class "result-name" + , target "_blank" + ] + [ text result.name ] + ] + + +type Action + = Search + | SetQuery String + | HideById ResultId + | SetResults (List SearchResult) + + +update : Action -> Model -> ( Model, Effects Action ) +update action model = + case action of + Search -> + ( model, Effects.task (searchFeed (Debug.log "searching for" model.query)) ) + + SetQuery query -> + ( { model | query = query }, Effects.none ) + + SetResults results -> + let + newModel = + { model | results = results } + in + ( newModel, Effects.none ) + + HideById idToHide -> + let + newResults = + model.results + |> List.filter (\{ id } -> id /= idToHide) + + newModel = + { model | results = newResults } + in + ( newModel, Effects.none ) diff --git a/stages/2/style.css b/stages/2/style.css new file mode 100644 index 0000000..8ee7f3b --- /dev/null +++ b/stages/2/style.css @@ -0,0 +1,91 @@ + +.content { + width: 960px; + margin: 0 auto; + padding: 30px; + font-family: Helvetica, Arial, serif; +} + +header { + position: relative; + padding: 6px 12px; + background-color: rgb(96, 181, 204); +} + +h1 { + color: white; + font-weight: normal; + margin: 0; +} + +.tagline { + color: #eee; + position: absolute; + right: 16px; + top: 12px; + font-size: 24px; + font-style: italic; +} + +.results { + list-style-image: url('http://img-cache.cdn.gaiaonline.com/76bd5c99d8f2236e9d3672510e933fdf/http://i278.photobucket.com/albums/kk81/d3m3nt3dpr3p/Tiny-Star-Icon.png'); + list-style-position: inside; + padding: 0; +} + +.results li { + font-size: 18px; + margin-bottom: 16px; +} + +.star-count { + font-weight: bold; +} + +.result-name { + color: rgb(96, 181, 204); + margin-left: 16px; + text-decoration: none; +} + +.result-name:hover{ + text-decoration: underline; +} + +.search-query { + padding: 8px; + font-size: 24px; + margin-bottom: 18px; + margin-top: 36px; +} + +.search-button { + padding: 8px 16px; + font-size: 24px; + color: white; + border: 1px solid #ccc; + background-color: rgb(96, 181, 204); + margin-left: 12px +} + +.search-button:hover { + color: rgb(96, 181, 204); + background-color: white; +} + +.hide-result { + background-color: transparent; + border: 0; + font-weight: bold; + font-size: 18px; + margin-left: 18px; + cursor: pointer; +} + +.hide-result:hover { + color: rgb(96, 181, 204); +} + +button:focus, input:focus { + outline: none; +} diff --git a/stages/2/test/TestRunner.elm b/stages/2/test/TestRunner.elm new file mode 100644 index 0000000..0baa6f2 --- /dev/null +++ b/stages/2/test/TestRunner.elm @@ -0,0 +1,15 @@ +module Main where + +import Signal exposing (Signal) + +import ElmTest exposing (consoleRunner) +import Console exposing (IO, run) +import Task + +import Tests + +console : IO () +console = consoleRunner Tests.all + +port runner : Signal (Task.Task x ()) +port runner = run console diff --git a/stages/2/test/Tests.elm b/stages/2/test/Tests.elm new file mode 100644 index 0000000..7c06fe2 --- /dev/null +++ b/stages/2/test/Tests.elm @@ -0,0 +1,35 @@ +module Tests (..) where + +import ElmTest exposing (..) +import ElmHub exposing (responseDecoder) +import Json.Decode exposing (decodeString) + + +all : Test +all = + suite + "Decoding responses from GitHub" + [ test "they can decode empty responses" + <| let + emptyResponse = + """{ "items": [] }""" + in + assertEqual + (decodeString responseDecoder emptyResponse) + (Ok []) + , test "they can decode responses with results in them" + <| let + response = + """{ "items": [ + { "id": 5, "full_name": "foo", "stargazers_count": 42 }, + { "id": 3, "full_name": "bar", "stargazers_count": 77 } + ] }""" + in + assertEqual + (decodeString responseDecoder response) + (Ok + [ { id = 5, name = "foo", stars = 42 } + , { id = 3, name = "bar", stars = 77 } + ] + ) + ] diff --git a/stages/3/src/ElmHub.elm b/stages/3/src/ElmHub.elm index 3e98377..b7c6ab3 100644 --- a/stages/3/src/ElmHub.elm +++ b/stages/3/src/ElmHub.elm @@ -22,46 +22,11 @@ app = StartApp.start { view = view , update = update - , init = ( initialModel, Effects.task (searchFeed initialModel.query) ) + , init = ( initialModel, Effects.none ) , inputs = [] } -port tasks : Signal (Task Effects.Never ()) -port tasks = - app.tasks - - -searchFeed : String -> Task x Action -searchFeed query = - let - -- See https://developer.github.com/v3/search/#example for how to customize! - url = - "https://api.github.com/search/repositories?q=" - ++ query - ++ "+language:elm&sort=stars&order=desc" - - task = - Http.get responseDecoder url - |> Task.map SetResults - in - Task.onError task (\_ -> Task.succeed (SetResults [])) - - -responseDecoder : Decoder (List SearchResult) -responseDecoder = - "items" := Json.Decode.list searchResultDecoder - - -searchResultDecoder : Decoder SearchResult -searchResultDecoder = - Json.Decode.object3 - SearchResult - ("id" := Json.Decode.int) - ("full_name" := Json.Decode.string) - ("stargazers_count" := Json.Decode.int) - - type alias Model = { query : String , results : List SearchResult @@ -82,7 +47,28 @@ type alias ResultId = initialModel : Model initialModel = { query = "tutorial" - , results = [] + , results = + [ { id = 1 + , name = "TheSeamau5/elm-checkerboardgrid-tutorial" + , stars = 66 + } + , { id = 2 + , name = "grzegorzbalcerek/elm-by-example" + , stars = 41 + } + , { id = 3 + , name = "sporto/elm-tutorial-app" + , stars = 35 + } + , { id = 4 + , name = "jvoigtlaender/Elm-Tutorium" + , stars = 10 + } + , { id = 5 + , name = "sporto/elm-tutorial-assets" + , stars = 7 + } + ] } @@ -95,11 +81,9 @@ view address model = [ h1 [] [ text "ElmHub" ] , span [ class "tagline" ] [ text "“Like GitHub, but for Elm things.”" ] ] - , input [ class "search-query", onInput address SetQuery, defaultValue model.query ] [] - , button [ class "search-button", onClick address Search ] [ text "Search" ] , ul [ class "results" ] - (List.map viewSearchResult model.results) + (List.map (viewSearchResult address) model.results) ] @@ -111,8 +95,8 @@ defaultValue str = property "defaultValue" (Json.Encode.string str) -viewSearchResult : SearchResult -> Html -viewSearchResult result = +viewSearchResult : Address Action -> SearchResult -> Html +viewSearchResult address result = li [] [ span [ class "star-count" ] [ text (toString result.stars) ] @@ -122,39 +106,28 @@ viewSearchResult result = , target "_blank" ] [ text result.name ] + , button + -- TODO add an onClick handler that sends a HideById action + [ class "hide-result" ] + [ text "X" ] ] type Action - = Search - | SetQuery String + = SetQuery String | HideById ResultId - | SetResults (List SearchResult) update : Action -> Model -> ( Model, Effects Action ) update action model = case action of - Search -> - ( model, Effects.task (searchFeed (Debug.log "searching for" model.query)) ) - SetQuery query -> ( { model | query = query }, Effects.none ) - SetResults results -> - let - newModel = - { model | results = results } - in - ( newModel, Effects.none ) - HideById idToHide -> let - newResults = - model.results - |> List.filter (\{ id } -> id /= idToHide) - + -- TODO build a new model without the given ID present anymore. newModel = - { model | results = newResults } + model in ( newModel, Effects.none ) diff --git a/stages/3/style.css b/stages/3/style.css index 7f19ca9..8ee7f3b 100644 --- a/stages/3/style.css +++ b/stages/3/style.css @@ -72,3 +72,20 @@ h1 { color: rgb(96, 181, 204); background-color: white; } + +.hide-result { + background-color: transparent; + border: 0; + font-weight: bold; + font-size: 18px; + margin-left: 18px; + cursor: pointer; +} + +.hide-result:hover { + color: rgb(96, 181, 204); +} + +button:focus, input:focus { + outline: none; +} diff --git a/stages/4/.gitignore b/stages/4/.gitignore new file mode 100644 index 0000000..3bd52a1 --- /dev/null +++ b/stages/4/.gitignore @@ -0,0 +1,2 @@ +elm-stuff +elm.js diff --git a/stages/4/README.md b/stages/4/README.md new file mode 100644 index 0000000..45dfaf0 --- /dev/null +++ b/stages/4/README.md @@ -0,0 +1,22 @@ +Stage 3 +======= + +## Install the Project + +Run this at the terminal: + +```bash +npm install +``` + +## Run Tests + +```bash +npm test +``` + +## Engage Auto-Rebuilding + +```bash +npm run watch +``` diff --git a/stages/4/elm-package.json b/stages/4/elm-package.json new file mode 100644 index 0000000..c5b0298 --- /dev/null +++ b/stages/4/elm-package.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0", + "summary": "Like GitHub, but for Elm stuff.", + "repository": "https://github.com/rtfeldman/elm-workshop.git", + "license": "BSD-3-Clause", + "source-directories": [ + "src", + "test" + ], + "exposed-modules": [], + "dependencies": { + "deadfoxygrandpa/elm-test": "3.1.1 <= v < 4.0.0", + "elm-lang/core": "3.0.0 <= v < 4.0.0", + "evancz/elm-effects": "2.0.0 <= v < 3.0.0", + "evancz/elm-html": "4.0.0 <= v < 5.0.0", + "evancz/elm-http": "3.0.0 <= v < 4.0.0", + "evancz/start-app": "2.0.0 <= v < 3.0.0", + "laszlopandy/elm-console": "1.0.3 <= v < 2.0.0" + }, + "elm-version": "0.16.0 <= v < 0.17.0" +} diff --git a/stages/4/index.html b/stages/4/index.html new file mode 100644 index 0000000..f3d5dad --- /dev/null +++ b/stages/4/index.html @@ -0,0 +1,25 @@ + + + + + + ElmHub + + + + + + + + + + + + + + diff --git a/stages/4/package.json b/stages/4/package.json new file mode 100644 index 0000000..66fe54f --- /dev/null +++ b/stages/4/package.json @@ -0,0 +1,25 @@ +{ + "name": "elm-hub", + "version": "1.0.0", + "description": "Like GitHub, but for Elm stuff.", + "scripts": { + "build": "elm-make src/ElmHub.elm --output elm.js", + "watch": "elm-live src/ElmHub.elm --open -- --output=elm.js", + "test": "elm-test test/TestRunner.elm", + "install": "elm-package install --yes && npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rtfeldman/elm-workshop.git" + }, + "author": "Richard Feldman", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/rtfeldman/elm-workshop/issues" + }, + "homepage": "https://github.com/rtfeldman/elm-workshop#readme", + "devDependencies": { + "elm-live": "2.0.4", + "elm-test": "0.16.1-alpha3" + } +} diff --git a/stages/4/src/ElmHub.elm b/stages/4/src/ElmHub.elm new file mode 100644 index 0000000..ff7decd --- /dev/null +++ b/stages/4/src/ElmHub.elm @@ -0,0 +1,163 @@ +module ElmHub (..) where + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import StartApp +import Http +import Task exposing (Task) +import Effects exposing (Effects) +import Json.Decode exposing (Decoder, (:=)) +import Json.Encode +import Signal exposing (Address) + + +main : Signal Html +main = + app.html + + +app : StartApp.App Model +app = + StartApp.start + { view = view + , update = update + , init = ( initialModel, Effects.task (searchFeed initialModel.query) ) + , inputs = [] + } + + +port tasks : Signal (Task Effects.Never ()) +port tasks = + app.tasks + + +searchFeed : String -> Task x Action +searchFeed query = + let + -- See https://developer.github.com/v3/search/#example for how to customize! + url = + "https://api.github.com/search/repositories?q=" + ++ query + ++ "+language:elm&sort=stars&order=desc" + + task = + Http.get responseDecoder url + |> Task.map SetResults + in + Task.onError task (\_ -> Task.succeed (SetResults [])) + + +responseDecoder : Decoder (List SearchResult) +responseDecoder = + "items" := Json.Decode.list searchResultDecoder + + +searchResultDecoder : Decoder SearchResult +searchResultDecoder = + Json.Decode.object3 + SearchResult + ("TODO what field goes here?" := Json.Decode.int) + ("TODO what field goes here?" := Json.Decode.string) + ("TODO what field goes here?" := Json.Decode.int) + + +type alias Model = + { query : String + , results : List SearchResult + } + + +type alias SearchResult = + { id : ResultId + , name : String + , stars : Int + } + + +type alias ResultId = + Int + + +initialModel : Model +initialModel = + { query = "tutorial" + , results = [] + } + + +view : Address Action -> Model -> Html +view address model = + div + [ class "content" ] + [ header + [] + [ h1 [] [ text "ElmHub" ] + , span [ class "tagline" ] [ text "“Like GitHub, but for Elm things.”" ] + ] + , input [ class "search-query", onInput address SetQuery, defaultValue model.query ] [] + , button [ class "search-button", onClick address Search ] [ text "Search" ] + , ul + [ class "results" ] + (List.map (viewSearchResult address) model.results) + ] + + +onInput address wrap = + on "input" targetValue (\val -> Signal.message address (wrap val)) + + +defaultValue str = + property "defaultValue" (Json.Encode.string str) + + +viewSearchResult : Address Action -> SearchResult -> Html +viewSearchResult address result = + li + [] + [ span [ class "star-count" ] [ text (toString result.stars) ] + , a + [ href ("https://github.com/" ++ result.name) + , class "result-name" + , target "_blank" + ] + [ text result.name ] + , button + [ class "hide-result", onClick address (HideById result.id) ] + [ text "X" ] + ] + + +type Action + = Search + | SetQuery String + | HideById ResultId + | SetResults (List SearchResult) + + +update : Action -> Model -> ( Model, Effects Action ) +update action model = + case action of + Search -> + ( model, Effects.task (searchFeed model.query) ) + + SetQuery query -> + ( { model | query = query }, Effects.none ) + + SetResults results -> + let + newModel = + { model | results = results } + in + ( newModel, Effects.none ) + + HideById idToHide -> + let + newResults = + model.results + |> List.filter (\{ id } -> id /= idToHide) + + newModel = + { model | results = newResults } + in + ( newModel, Effects.none ) diff --git a/stages/4/style.css b/stages/4/style.css new file mode 100644 index 0000000..8ee7f3b --- /dev/null +++ b/stages/4/style.css @@ -0,0 +1,91 @@ + +.content { + width: 960px; + margin: 0 auto; + padding: 30px; + font-family: Helvetica, Arial, serif; +} + +header { + position: relative; + padding: 6px 12px; + background-color: rgb(96, 181, 204); +} + +h1 { + color: white; + font-weight: normal; + margin: 0; +} + +.tagline { + color: #eee; + position: absolute; + right: 16px; + top: 12px; + font-size: 24px; + font-style: italic; +} + +.results { + list-style-image: url('http://img-cache.cdn.gaiaonline.com/76bd5c99d8f2236e9d3672510e933fdf/http://i278.photobucket.com/albums/kk81/d3m3nt3dpr3p/Tiny-Star-Icon.png'); + list-style-position: inside; + padding: 0; +} + +.results li { + font-size: 18px; + margin-bottom: 16px; +} + +.star-count { + font-weight: bold; +} + +.result-name { + color: rgb(96, 181, 204); + margin-left: 16px; + text-decoration: none; +} + +.result-name:hover{ + text-decoration: underline; +} + +.search-query { + padding: 8px; + font-size: 24px; + margin-bottom: 18px; + margin-top: 36px; +} + +.search-button { + padding: 8px 16px; + font-size: 24px; + color: white; + border: 1px solid #ccc; + background-color: rgb(96, 181, 204); + margin-left: 12px +} + +.search-button:hover { + color: rgb(96, 181, 204); + background-color: white; +} + +.hide-result { + background-color: transparent; + border: 0; + font-weight: bold; + font-size: 18px; + margin-left: 18px; + cursor: pointer; +} + +.hide-result:hover { + color: rgb(96, 181, 204); +} + +button:focus, input:focus { + outline: none; +} diff --git a/stages/4/test/TestRunner.elm b/stages/4/test/TestRunner.elm new file mode 100644 index 0000000..0baa6f2 --- /dev/null +++ b/stages/4/test/TestRunner.elm @@ -0,0 +1,15 @@ +module Main where + +import Signal exposing (Signal) + +import ElmTest exposing (consoleRunner) +import Console exposing (IO, run) +import Task + +import Tests + +console : IO () +console = consoleRunner Tests.all + +port runner : Signal (Task.Task x ()) +port runner = run console diff --git a/stages/4/test/Tests.elm b/stages/4/test/Tests.elm new file mode 100644 index 0000000..36bc327 --- /dev/null +++ b/stages/4/test/Tests.elm @@ -0,0 +1,35 @@ +module Tests (..) where + +import ElmTest exposing (..) +import ElmHub exposing (responseDecoder) +import Json.Decode exposing (decodeString) + + +all : Test +all = + suite + "Decoding responses from GitHub" + [ test "they can decode empty responses" + <| let + emptyResponse = + """{ "items": [] }""" + in + assertEqual + (decodeString responseDecoder emptyResponse) + ({- TODO: put the expected result here instead -}) + , test "they can decode responses with results in them" + <| let + response = + """{ + ... json goes here ... + }""" + + expected = + [ { id = 5, name = "foo", stars = 42 } + , { id = 3, name = "bar", stars = 77 } + ] + in + assertEqual + (decodeString responseDecoder response) + ({- TODO: put the expected result here instead -}) + ] diff --git a/stages/5/.gitignore b/stages/5/.gitignore new file mode 100644 index 0000000..3bd52a1 --- /dev/null +++ b/stages/5/.gitignore @@ -0,0 +1,2 @@ +elm-stuff +elm.js diff --git a/stages/5/README.md b/stages/5/README.md new file mode 100644 index 0000000..45dfaf0 --- /dev/null +++ b/stages/5/README.md @@ -0,0 +1,22 @@ +Stage 3 +======= + +## Install the Project + +Run this at the terminal: + +```bash +npm install +``` + +## Run Tests + +```bash +npm test +``` + +## Engage Auto-Rebuilding + +```bash +npm run watch +``` diff --git a/stages/5/elm-package.json b/stages/5/elm-package.json new file mode 100644 index 0000000..c5b0298 --- /dev/null +++ b/stages/5/elm-package.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0", + "summary": "Like GitHub, but for Elm stuff.", + "repository": "https://github.com/rtfeldman/elm-workshop.git", + "license": "BSD-3-Clause", + "source-directories": [ + "src", + "test" + ], + "exposed-modules": [], + "dependencies": { + "deadfoxygrandpa/elm-test": "3.1.1 <= v < 4.0.0", + "elm-lang/core": "3.0.0 <= v < 4.0.0", + "evancz/elm-effects": "2.0.0 <= v < 3.0.0", + "evancz/elm-html": "4.0.0 <= v < 5.0.0", + "evancz/elm-http": "3.0.0 <= v < 4.0.0", + "evancz/start-app": "2.0.0 <= v < 3.0.0", + "laszlopandy/elm-console": "1.0.3 <= v < 2.0.0" + }, + "elm-version": "0.16.0 <= v < 0.17.0" +} diff --git a/stages/5/index.html b/stages/5/index.html new file mode 100644 index 0000000..f3d5dad --- /dev/null +++ b/stages/5/index.html @@ -0,0 +1,25 @@ + + + + + + ElmHub + + + + + + + + + + + + + + diff --git a/stages/5/package.json b/stages/5/package.json new file mode 100644 index 0000000..66fe54f --- /dev/null +++ b/stages/5/package.json @@ -0,0 +1,25 @@ +{ + "name": "elm-hub", + "version": "1.0.0", + "description": "Like GitHub, but for Elm stuff.", + "scripts": { + "build": "elm-make src/ElmHub.elm --output elm.js", + "watch": "elm-live src/ElmHub.elm --open -- --output=elm.js", + "test": "elm-test test/TestRunner.elm", + "install": "elm-package install --yes && npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rtfeldman/elm-workshop.git" + }, + "author": "Richard Feldman", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/rtfeldman/elm-workshop/issues" + }, + "homepage": "https://github.com/rtfeldman/elm-workshop#readme", + "devDependencies": { + "elm-live": "2.0.4", + "elm-test": "0.16.1-alpha3" + } +} diff --git a/stages/5/src/ElmHub.elm b/stages/5/src/ElmHub.elm new file mode 100644 index 0000000..ec98511 --- /dev/null +++ b/stages/5/src/ElmHub.elm @@ -0,0 +1,163 @@ +module ElmHub (..) where + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import StartApp +import Http +import Task exposing (Task) +import Effects exposing (Effects) +import Json.Decode exposing (Decoder, (:=)) +import Json.Encode +import Signal exposing (Address) + + +main : Signal Html +main = + app.html + + +app : StartApp.App Model +app = + StartApp.start + { view = view + , update = update + , init = ( initialModel, Effects.task (searchFeed initialModel.query) ) + , inputs = [] + } + + +port tasks : Signal (Task Effects.Never ()) +port tasks = + app.tasks + + +searchFeed : String -> Task x Action +searchFeed query = + let + -- See https://developer.github.com/v3/search/#example for how to customize! + url = + "https://api.github.com/search/repositories?q=" + ++ query + ++ "+language:elm&sort=stars&order=desc" + + task = + Http.get responseDecoder url + |> Task.map SetResults + in + Task.onError task (\_ -> Task.succeed (SetResults [])) + + +responseDecoder : Decoder (List SearchResult) +responseDecoder = + "items" := Json.Decode.list searchResultDecoder + + +searchResultDecoder : Decoder SearchResult +searchResultDecoder = + Json.Decode.object3 + SearchResult + ("id" := Json.Decode.int) + ("full_name" := Json.Decode.string) + ("stargazers_count" := Json.Decode.int) + + +type alias Model = + { query : String + , results : List SearchResult + } + + +type alias SearchResult = + { id : ResultId + , name : String + , stars : Int + } + + +type alias ResultId = + Int + + +initialModel : Model +initialModel = + { query = "tutorial" + , results = [] + } + + +view : Address Action -> Model -> Html +view address model = + div + [ class "content" ] + [ header + [] + [ h1 [] [ text "ElmHub" ] + , span [ class "tagline" ] [ text "“Like GitHub, but for Elm things.”" ] + ] + , input [ class "search-query", onInput address SetQuery, defaultValue model.query ] [] + , button [ class "search-button", onClick address Search ] [ text "Search" ] + , ul + [ class "results" ] + (List.map (viewSearchResult address) model.results) + ] + + +onInput address wrap = + on "input" targetValue (\val -> Signal.message address (wrap val)) + + +defaultValue str = + property "defaultValue" (Json.Encode.string str) + + +viewSearchResult : Address Action -> SearchResult -> Html +viewSearchResult address result = + li + [] + [ span [ class "star-count" ] [ text (toString result.stars) ] + , a + [ href ("https://github.com/" ++ result.name) + , class "result-name" + , target "_blank" + ] + [ text result.name ] + , button + [ class "hide-result", onClick address (HideById result.id) ] + [ text "X" ] + ] + + +type Action + = Search + | SetQuery String + | HideById ResultId + | SetResults (List SearchResult) + + +update : Action -> Model -> ( Model, Effects Action ) +update action model = + case action of + Search -> + ( model, Effects.task (searchFeed model.query) ) + + SetQuery query -> + ( { model | query = query }, Effects.none ) + + SetResults results -> + let + newModel = + { model | results = results } + in + ( newModel, Effects.none ) + + HideById idToHide -> + let + newResults = + model.results + |> List.filter (\{ id } -> id /= idToHide) + + newModel = + { model | results = newResults } + in + ( newModel, Effects.none ) diff --git a/stages/5/style.css b/stages/5/style.css new file mode 100644 index 0000000..8ee7f3b --- /dev/null +++ b/stages/5/style.css @@ -0,0 +1,91 @@ + +.content { + width: 960px; + margin: 0 auto; + padding: 30px; + font-family: Helvetica, Arial, serif; +} + +header { + position: relative; + padding: 6px 12px; + background-color: rgb(96, 181, 204); +} + +h1 { + color: white; + font-weight: normal; + margin: 0; +} + +.tagline { + color: #eee; + position: absolute; + right: 16px; + top: 12px; + font-size: 24px; + font-style: italic; +} + +.results { + list-style-image: url('http://img-cache.cdn.gaiaonline.com/76bd5c99d8f2236e9d3672510e933fdf/http://i278.photobucket.com/albums/kk81/d3m3nt3dpr3p/Tiny-Star-Icon.png'); + list-style-position: inside; + padding: 0; +} + +.results li { + font-size: 18px; + margin-bottom: 16px; +} + +.star-count { + font-weight: bold; +} + +.result-name { + color: rgb(96, 181, 204); + margin-left: 16px; + text-decoration: none; +} + +.result-name:hover{ + text-decoration: underline; +} + +.search-query { + padding: 8px; + font-size: 24px; + margin-bottom: 18px; + margin-top: 36px; +} + +.search-button { + padding: 8px 16px; + font-size: 24px; + color: white; + border: 1px solid #ccc; + background-color: rgb(96, 181, 204); + margin-left: 12px +} + +.search-button:hover { + color: rgb(96, 181, 204); + background-color: white; +} + +.hide-result { + background-color: transparent; + border: 0; + font-weight: bold; + font-size: 18px; + margin-left: 18px; + cursor: pointer; +} + +.hide-result:hover { + color: rgb(96, 181, 204); +} + +button:focus, input:focus { + outline: none; +} diff --git a/stages/5/test/TestRunner.elm b/stages/5/test/TestRunner.elm new file mode 100644 index 0000000..0baa6f2 --- /dev/null +++ b/stages/5/test/TestRunner.elm @@ -0,0 +1,15 @@ +module Main where + +import Signal exposing (Signal) + +import ElmTest exposing (consoleRunner) +import Console exposing (IO, run) +import Task + +import Tests + +console : IO () +console = consoleRunner Tests.all + +port runner : Signal (Task.Task x ()) +port runner = run console diff --git a/stages/5/test/Tests.elm b/stages/5/test/Tests.elm new file mode 100644 index 0000000..7c06fe2 --- /dev/null +++ b/stages/5/test/Tests.elm @@ -0,0 +1,35 @@ +module Tests (..) where + +import ElmTest exposing (..) +import ElmHub exposing (responseDecoder) +import Json.Decode exposing (decodeString) + + +all : Test +all = + suite + "Decoding responses from GitHub" + [ test "they can decode empty responses" + <| let + emptyResponse = + """{ "items": [] }""" + in + assertEqual + (decodeString responseDecoder emptyResponse) + (Ok []) + , test "they can decode responses with results in them" + <| let + response = + """{ "items": [ + { "id": 5, "full_name": "foo", "stargazers_count": 42 }, + { "id": 3, "full_name": "bar", "stargazers_count": 77 } + ] }""" + in + assertEqual + (decodeString responseDecoder response) + (Ok + [ { id = 5, name = "foo", stars = 42 } + , { id = 3, name = "bar", stars = 77 } + ] + ) + ]