diff --git a/stages/10/ElmHub.elm b/stages/10/ElmHub.elm new file mode 100644 index 0000000..e33490e --- /dev/null +++ b/stages/10/ElmHub.elm @@ -0,0 +1,124 @@ +module ElmHub (..) where + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Http +import Task exposing (Task) +import Effects exposing (Effects) +import Json.Decode exposing (Decoder, (:=)) +import Json.Encode +import Signal exposing (Address) +import Dict exposing (Dict) +import SearchResult + + +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" + + task = + Http.get responseDecoder url + |> Task.map SetResults + in + Task.onError task (\_ -> Task.succeed (SetResults [])) + + +responseDecoder : Decoder (List SearchResult.Model) +responseDecoder = + -- TODO use SearchResult's decoder + --Json.Decode.succeed [] + ("items" := Json.Decode.list SearchResult.decoder) + + +type alias Model = + { query : String + , results : Dict SearchResult.ResultId SearchResult.Model + } + + +initialModel : Model +initialModel = + { query = "tutorial" + , results = Dict.empty + } + + +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" ] + (viewSearchResults address model.results) + ] + + +viewSearchResults : Address Action -> Dict SearchResult.ResultId SearchResult.Model -> List Html +viewSearchResults address results = + results + |> Dict.values + |> List.sortBy (.stars >> negate) + |> filterResults + |> List.map (SearchResult.view address DeleteById) + + +filterResults : List SearchResult.Model -> List SearchResult.Model +filterResults results = + -- TODO filter out repos with 0 stars + -- using a case-expression rather than List.filter + results + + +onInput address wrap = + on "input" targetValue (\val -> Signal.message address (wrap val)) + + +defaultValue str = + property "defaultValue" (Json.Encode.string str) + + +type Action + = Search + | SetQuery String + | DeleteById SearchResult.ResultId + | SetResults (List SearchResult.Model) + + +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 + resultsById : Dict SearchResult.ResultId SearchResult.Model + resultsById = + results + |> List.map (\result -> ( result.id, result )) + |> Dict.fromList + in + ( { model | results = resultsById }, Effects.none ) + + DeleteById id -> + let + newModel = + { model | results = Dict.remove id model.results } + in + ( newModel, Effects.none ) diff --git a/stages/10/Main.elm b/stages/10/Main.elm index 5b8e88e..fa4bada 100644 --- a/stages/10/Main.elm +++ b/stages/10/Main.elm @@ -1,7 +1,7 @@ module Main (..) where import StartApp -import Component.ElmHub exposing (..) +import ElmHub exposing (..) import Effects exposing (Effects) import Task exposing (Task) import Html exposing (Html) diff --git a/stages/10/SearchResult.elm b/stages/10/SearchResult.elm new file mode 100644 index 0000000..95a843c --- /dev/null +++ b/stages/10/SearchResult.elm @@ -0,0 +1,50 @@ +module SearchResult (..) where + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Json.Decode exposing (Decoder, (:=)) +import Signal exposing (Address) +import Dict exposing (Dict) + + +type alias ResultId = + Int + + +type alias Model = + { id : ResultId + , name : String + , stars : Int + } + + +decoder : Decoder Model +decoder = + Json.Decode.object3 + Model + ("id" := Json.Decode.int) + ("full_name" := Json.Decode.string) + ("stargazers_count" := Json.Decode.int) + + +view : Address a -> (Int -> a) -> Model -> Html +view address delete result = + li + [] + [ span [ class "star-count" ] [ text (toString result.stars) ] + , a + [ href + ("https://github.com/" + ++ (Debug.log + "TODO we should not see this when typing in the search box!" + result.name + ) + ) + , target "_blank" + ] + [ text result.name ] + , button + [ class "hide-result", onClick address (delete result.id) ] + [ text "X" ] + ] diff --git a/stages/11/Component/ElmHub.elm b/stages/11/Component/ElmHub.elm index fb9237f..e825e6d 100644 --- a/stages/11/Component/ElmHub.elm +++ b/stages/11/Component/ElmHub.elm @@ -82,21 +82,16 @@ viewSearchResults address results = filterResults : List Component.SearchResult.Model -> List Component.SearchResult.Model -filterResults = - filterResultsHelp [] - - -filterResultsHelp : List Component.SearchResult.Model -> List Component.SearchResult.Model -> List Component.SearchResult.Model -filterResultsHelp output results = +filterResults results = case results of [] -> - output + [] first :: rest -> if first.stars > 0 then - filterResultsHelp (first :: output) rest + first :: (filterResults rest) else - filterResultsHelp output rest + filterResults rest onInput address wrap = @@ -111,7 +106,7 @@ viewSearchResult : Address Action -> Component.SearchResult.Model -> Html viewSearchResult address result = Component.SearchResult.view (Signal.forwardTo address (UpdateSearchResult result.id)) - result + (Debug.log "rendering result..." result) type Action diff --git a/stages/11/Component/SearchResult.elm b/stages/11/Component/SearchResult.elm index 67d7d37..4dedbd3 100644 --- a/stages/11/Component/SearchResult.elm +++ b/stages/11/Component/SearchResult.elm @@ -26,12 +26,8 @@ type Action update : Action -> Model -> ( Model, Effects Action ) update action model = - case action of - Expand -> - ( { model | expanded = True }, Effects.none ) - - Collapse -> - ( { model | expanded = False }, Effects.none ) + -- TODO make expand and collapse work + ( model, Effects.none ) view : Address Action -> Model -> Html @@ -44,11 +40,13 @@ view address model = [ href ("https://github.com/" ++ model.name), target "_blank" ] [ text model.name ] , button - [ class "hide-result", onClick address Collapse ] + -- TODO when the user clicks, send a Collapse action + [ class "hide-result" ] [ text "X" ] ] else [ button - [ class "expand-result", onClick address Expand ] + -- TODO when the user clicks, send an Expand action + [ class "expand-result" ] [ text "Show" ] ] diff --git a/stages/11/README.md b/stages/11/README.md index 60754c3..76fe73f 100644 --- a/stages/11/README.md +++ b/stages/11/README.md @@ -16,8 +16,10 @@ to fail; in that case, just run `elm package install` again.) elm live Main.elm --open -- --output=elm.js ``` -## Compiling CSS +## Running Tests ```bash -elm css css/Stylesheets.elm +cd test +elm package install +elm test TestRunner.elm ``` diff --git a/stages/11/elm-package.json b/stages/11/elm-package.json index ea197b6..5728b71 100644 --- a/stages/11/elm-package.json +++ b/stages/11/elm-package.json @@ -12,8 +12,7 @@ "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", - "rtfeldman/elm-css": "1.0.0 <= v < 2.0.0" + "evancz/start-app": "2.0.0 <= v < 3.0.0" }, "elm-version": "0.16.0 <= v < 0.17.0" } diff --git a/stages/11/style.css b/stages/11/style.css index 8b13789..64c74d7 100644 --- a/stages/11/style.css +++ b/stages/11/style.css @@ -1 +1,92 @@ +.content { + width: 960px; + margin: 0 auto; + padding: 30px; + font-family: Helvetica, Arial, serif; +} + +header { + position: relative; + padding: 6px 12px; + height: 36px; + 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; + margin-right: 16px; +} + +a { + color: rgb(96, 181, 204); + text-decoration: none; +} + +a: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/10/Component/ElmHub.elm b/stages/12/Component/ElmHub.elm similarity index 92% rename from stages/10/Component/ElmHub.elm rename to stages/12/Component/ElmHub.elm index e825e6d..fb9237f 100644 --- a/stages/10/Component/ElmHub.elm +++ b/stages/12/Component/ElmHub.elm @@ -82,16 +82,21 @@ viewSearchResults address results = filterResults : List Component.SearchResult.Model -> List Component.SearchResult.Model -filterResults results = +filterResults = + filterResultsHelp [] + + +filterResultsHelp : List Component.SearchResult.Model -> List Component.SearchResult.Model -> List Component.SearchResult.Model +filterResultsHelp output results = case results of [] -> - [] + output first :: rest -> if first.stars > 0 then - first :: (filterResults rest) + filterResultsHelp (first :: output) rest else - filterResults rest + filterResultsHelp output rest onInput address wrap = @@ -106,7 +111,7 @@ viewSearchResult : Address Action -> Component.SearchResult.Model -> Html viewSearchResult address result = Component.SearchResult.view (Signal.forwardTo address (UpdateSearchResult result.id)) - (Debug.log "rendering result..." result) + result type Action diff --git a/stages/10/Component/SearchResult.elm b/stages/12/Component/SearchResult.elm similarity index 75% rename from stages/10/Component/SearchResult.elm rename to stages/12/Component/SearchResult.elm index 4dedbd3..67d7d37 100644 --- a/stages/10/Component/SearchResult.elm +++ b/stages/12/Component/SearchResult.elm @@ -26,8 +26,12 @@ type Action update : Action -> Model -> ( Model, Effects Action ) update action model = - -- TODO make expand and collapse work - ( model, Effects.none ) + case action of + Expand -> + ( { model | expanded = True }, Effects.none ) + + Collapse -> + ( { model | expanded = False }, Effects.none ) view : Address Action -> Model -> Html @@ -40,13 +44,11 @@ view address model = [ href ("https://github.com/" ++ model.name), target "_blank" ] [ text model.name ] , button - -- TODO when the user clicks, send a Collapse action - [ class "hide-result" ] + [ class "hide-result", onClick address Collapse ] [ text "X" ] ] else [ button - -- TODO when the user clicks, send an Expand action - [ class "expand-result" ] + [ class "expand-result", onClick address Expand ] [ text "Show" ] ] diff --git a/stages/11/ElmHub.elm b/stages/12/ElmHub.elm similarity index 100% rename from stages/11/ElmHub.elm rename to stages/12/ElmHub.elm diff --git a/stages/12/Main.elm b/stages/12/Main.elm new file mode 100644 index 0000000..5b8e88e --- /dev/null +++ b/stages/12/Main.elm @@ -0,0 +1,27 @@ +module Main (..) where + +import StartApp +import Component.ElmHub exposing (..) +import Effects exposing (Effects) +import Task exposing (Task) +import Html exposing (Html) + + +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 diff --git a/stages/12/README.md b/stages/12/README.md new file mode 100644 index 0000000..e98b73b --- /dev/null +++ b/stages/12/README.md @@ -0,0 +1,23 @@ +Stage 12 +======== + +## Installation + +```bash +elm package install +``` + +(Answer `y` at the prompt. In rare cases a known issue can cause the download +to fail; in that case, just run `elm package install` again.) + +## Building + +```bash +elm live Main.elm --open -- --output=elm.js +``` + +## Compiling CSS + +```bash +elm css css/Stylesheets.elm +``` diff --git a/stages/11/Stylesheets.elm b/stages/12/Stylesheets.elm similarity index 100% rename from stages/11/Stylesheets.elm rename to stages/12/Stylesheets.elm diff --git a/stages/11/css/Stylesheets.elm b/stages/12/css/Stylesheets.elm similarity index 100% rename from stages/11/css/Stylesheets.elm rename to stages/12/css/Stylesheets.elm diff --git a/stages/12/elm-hub.png b/stages/12/elm-hub.png new file mode 100644 index 0000000..ba32816 Binary files /dev/null and b/stages/12/elm-hub.png differ diff --git a/stages/12/elm-package.json b/stages/12/elm-package.json new file mode 100644 index 0000000..ea197b6 --- /dev/null +++ b/stages/12/elm-package.json @@ -0,0 +1,19 @@ +{ + "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": [ + "." + ], + "exposed-modules": [], + "dependencies": { + "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", + "rtfeldman/elm-css": "1.0.0 <= v < 2.0.0" + }, + "elm-version": "0.16.0 <= v < 0.17.0" +} diff --git a/stages/12/index.html b/stages/12/index.html new file mode 100644 index 0000000..5db9b93 --- /dev/null +++ b/stages/12/index.html @@ -0,0 +1,26 @@ + + + + + + ElmHub + + + + + + + + + + + + + + + diff --git a/stages/12/style.css b/stages/12/style.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/stages/12/style.css @@ -0,0 +1 @@ + diff --git a/stages/12/test/TestRunner.elm b/stages/12/test/TestRunner.elm new file mode 100644 index 0000000..0baa6f2 --- /dev/null +++ b/stages/12/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/12/test/Tests.elm b/stages/12/test/Tests.elm new file mode 100644 index 0000000..7c06fe2 --- /dev/null +++ b/stages/12/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/12/test/elm-package.json b/stages/12/test/elm-package.json new file mode 100644 index 0000000..a440485 --- /dev/null +++ b/stages/12/test/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": [ + ".", + ".." + ], + "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/9/ElmHub.elm b/stages/9/ElmHub.elm index bab5aa0..486fb65 100644 --- a/stages/9/ElmHub.elm +++ b/stages/9/ElmHub.elm @@ -11,6 +11,7 @@ import Json.Decode exposing (Decoder, (:=)) import Json.Encode import Signal exposing (Address) import Dict exposing (Dict) +import SearchResult searchFeed : String -> Task x Action @@ -29,37 +30,18 @@ searchFeed query = Task.onError task (\_ -> Task.succeed (SetResults [])) -responseDecoder : Decoder (List SearchResult) +responseDecoder : Decoder (List SearchResult.Model) 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) + -- TODO use SearchResult's decoder + Json.Decode.succeed [] type alias Model = { query : String - , results : Dict ResultId SearchResult + , results : Dict SearchResult.ResultId SearchResult.Model } -type alias SearchResult = - { id : ResultId - , name : String - , stars : Int - } - - -type alias ResultId = - Int - - initialModel : Model initialModel = { query = "tutorial" @@ -84,20 +66,12 @@ view address model = ] -viewSearchResults : Address Action -> Dict ResultId SearchResult -> List Html +viewSearchResults : Address Action -> Dict SearchResult.ResultId SearchResult.Model -> List Html viewSearchResults address results = results |> Dict.values |> List.sortBy (.stars >> negate) - |> filterResults - |> List.map (viewSearchResult address) - - -filterResults : List SearchResult -> List SearchResult -filterResults results = - -- TODO filter out repos with 0 stars - -- using a case-expression rather than List.filter - [] + |> List.map (SearchResult.view address) onInput address wrap = @@ -108,33 +82,11 @@ 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/" - ++ (Debug.log "Viewing" result.name) - {- TODO we should no longer see this - console output when typing in the search box! - -} - ) - , target "_blank" - ] - [ text result.name ] - , button - [ class "hide-result", onClick address (DeleteById result.id) ] - [ text "X" ] - ] - - type Action = Search | SetQuery String - | DeleteById ResultId - | SetResults (List SearchResult) + | DeleteById SearchResult.ResultId + | SetResults (List SearchResult.Model) update : Action -> Model -> ( Model, Effects Action ) @@ -148,7 +100,7 @@ update action model = SetResults results -> let - resultsById : Dict ResultId SearchResult + resultsById : Dict SearchResult.ResultId SearchResult.Model resultsById = results |> List.map (\result -> ( result.id, result )) diff --git a/stages/9/SearchResult.elm b/stages/9/SearchResult.elm new file mode 100644 index 0000000..e93bd0c --- /dev/null +++ b/stages/9/SearchResult.elm @@ -0,0 +1,45 @@ +module SearchResult (..) where + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Json.Decode exposing (Decoder, (:=)) +import Signal exposing (Address) +import Dict exposing (Dict) + + +type alias ResultId = + Int + + +type alias Model = + { id : ResultId + , name : String + , stars : Int + } + + +decoder : Decoder Model +decoder = + Json.Decode.object3 + Model + ("id" := Json.Decode.int) + ("full_name" := Json.Decode.string) + ("stargazers_count" := Json.Decode.int) + + +view : Address a -> Model -> Html +view address result = + li + [] + [ span [ class "star-count" ] [ text (toString result.stars) ] + , a + [ href ("https://github.com/" ++ result.name) + , target "_blank" + ] + [ text result.name ] + , button + -- TODO onClick, send a delete action to the address + [ class "hide-result" ] + [ text "X" ] + ]