This commit is contained in:
Richard Feldman
2016-03-06 09:03:36 -08:00
parent 39bd2a08d1
commit 53bf3b7e26
12 changed files with 519 additions and 0 deletions

14
stages/9/README.md Normal file
View File

@@ -0,0 +1,14 @@
Stage 5
=======
To run tests:
```bash
npm test
```
To engage Auto-Rebuilding:
```bash
npm run watch
```

18
stages/9/elm-package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"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"
],
"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"
},
"elm-version": "0.16.0 <= v < 0.17.0"
}

25
stages/9/index.html Normal file
View File

@@ -0,0 +1,25 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>ElmHub</title>
<script type="text/javascript" src="elm.js"></script>
<!-- Uncomment the below line to enable elm-reactor support. -->
<!-- <script type="text/javascript" src="/_reactor/debug.js"></script> -->
<link rel="stylesheet" href="style.css">
</head>
<body>
</body>
<script type="text/javascript">
var app = Elm.fullscreen(Elm.Main, {});
// Uncomment this line and comment out the above to enable elm-reactor support.
// var app = Elm.fullscreenDebug("ElmHub", "Main.elm");
</script>
</html>

25
stages/9/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "elm-hub",
"version": "1.0.0",
"description": "Like GitHub, but for Elm stuff.",
"scripts": {
"build": "elm-make src/Main.elm --output elm.js",
"watch": "../../node_modules/.bin/elm-live src/Main.elm --open -- --output=elm.js",
"test": "node test.js",
"install": "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"
}
}

View File

@@ -0,0 +1,136 @@
module Component.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 Component.SearchResult exposing (ResultId)
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 Component.SearchResult.Model)
responseDecoder =
"items" := Json.Decode.list searchResultDecoder
searchResultDecoder : Decoder Component.SearchResult.Model
searchResultDecoder =
Json.Decode.object4
Component.SearchResult.Model
("id" := Json.Decode.int)
("full_name" := Json.Decode.string)
("stargazers_count" := Json.Decode.int)
(Json.Decode.succeed True)
type alias Model =
{ query : String
, results : List Component.SearchResult.Model
}
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 -> Component.SearchResult.Model -> Html
viewSearchResult address result =
Component.SearchResult.view
(Signal.forwardTo address (UpdateSearchResult result.id))
result
type Action
= Search
| SetQuery String
| SetResults (List Component.SearchResult.Model)
| UpdateSearchResult ResultId Component.SearchResult.Action
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 )
UpdateSearchResult id childAction ->
let
updateResult childModel =
if childModel.id == id then
let
( newChildModel, childEffects ) =
Component.SearchResult.update childAction childModel
in
( newChildModel
, Effects.map (UpdateSearchResult id) childEffects
)
else
( childModel, Effects.none )
( newResults, effects ) =
model.results
|> List.map updateResult
|> List.unzip
newModel =
{ model | results = newResults }
in
( newModel, Effects.batch effects )

View File

@@ -0,0 +1,57 @@
module Component.SearchResult (..) where
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Signal exposing (Address)
import Effects exposing (Effects)
type alias Model =
{ id : Int
, name : String
, stars : Int
, expanded : Bool
}
type alias ResultId =
Int
type Action
= Expand
| Collapse
update : Action -> Model -> ( Model, Effects Action )
update action model =
case action of
Expand ->
( { model | expanded = True }, Effects.none )
Collapse ->
( { model | expanded = False }, Effects.none )
view : Address Action -> Model -> Html
view address model =
li
[]
<| if model.expanded then
[ span [ class "star-count" ] [ text (toString model.stars) ]
, a
[ href ("https://github.com/" ++ model.name)
, class "result-name"
, target "_blank"
]
[ text model.name ]
, button
[ class "hide-result", onClick address Collapse ]
[ text "X" ]
]
else
[ button
[ class "expand-result", onClick address Expand ]
[ text "Show" ]
]

27
stages/9/src/Main.elm Normal file
View File

@@ -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

106
stages/9/style.css Normal file
View File

@@ -0,0 +1,106 @@
.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;
}
.expand-result {
padding: 4px 8px;
font-size: 16px;
color: white;
border: 1px solid #ccc;
background-color: rgb(96, 181, 204);
margin-left: 12px;
cursor: pointer;
}
.expand-result:hover {
color: rgb(96, 181, 204);
background-color: white;
}

10
stages/9/test.js Normal file
View File

@@ -0,0 +1,10 @@
var fs = require("fs");
var execSync = require("child_process").execSync;
var path = require("path");
var testDir = path.join(__dirname, "test");
var binPath = path.join(__dirname, "..", "..", "node_modules", ".bin");
var elmTestPath = path.join(binPath, "elm-test");
var elmMakePath = path.join(binPath, "elm-make");
execSync(elmMakePath + " TestRunner.elm --yes --output /dev/null", { cwd: testDir });
execSync(elmTestPath + " TestRunner.elm", { cwd: testDir, stdio: "inherit" });

View File

@@ -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

64
stages/9/test/Tests.elm Normal file
View File

@@ -0,0 +1,64 @@
module Tests (..) where
import ElmTest exposing (..)
import ElmHub exposing (responseDecoder)
import Json.Decode as Decode
import Json.Encode as Encode
import Check exposing (Claim, Evidence, check, claim, that, is, for)
import Check.Producer exposing (..)
import Check.Test exposing (evidenceToTest)
import String
import ElmHub exposing (..)
import Random
all : Test
all =
suite
"Decoding responses from GitHub"
[ test "they can decode empty responses"
<| let
emptyResponse =
"""{ "items": [] }"""
in
assertEqual
(Decode.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
(Decode.decodeString responseDecoder response)
(Ok
[ { id = 5, name = "foo", stars = 42 }
, { id = 3, name = "bar", stars = 77 }
]
)
, (claim "they can decode individual search results"
`that` (\( id, name, stars ) -> encodeAndDecode id name stars)
`is` (\( id, name, stars ) -> Ok (SearchResult id name stars))
`for` tuple3 ( int, string, int )
)
|> check 100 defaultSeed
|> evidenceToTest
]
encodeAndDecode : Int -> String -> Int -> Result String SearchResult
encodeAndDecode id name stars =
[ ( "id", Encode.int id )
, ( "full_name", Encode.string name )
, ( "stargazers_count", Encode.int stars )
]
|> Encode.object
|> Encode.encode 0
|> Decode.decodeString searchResultDecoder
defaultSeed =
Random.initialSeed 42

View File

@@ -0,0 +1,22 @@
{
"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"
],
"exposed-modules": [],
"dependencies": {
"NoRedInk/elm-check": "3.0.0 <= v < 4.0.0",
"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"
}