Move things around some more.

This commit is contained in:
Richard Feldman
2016-03-27 09:21:16 -07:00
parent 3df83cd5dd
commit 2c096b9732
24 changed files with 522 additions and 92 deletions

124
stages/10/ElmHub.elm Normal file
View File

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

View File

@@ -1,7 +1,7 @@
module Main (..) where module Main (..) where
import StartApp import StartApp
import Component.ElmHub exposing (..) import ElmHub exposing (..)
import Effects exposing (Effects) import Effects exposing (Effects)
import Task exposing (Task) import Task exposing (Task)
import Html exposing (Html) import Html exposing (Html)

View File

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

View File

@@ -82,21 +82,16 @@ viewSearchResults address results =
filterResults : List Component.SearchResult.Model -> List Component.SearchResult.Model filterResults : List Component.SearchResult.Model -> List Component.SearchResult.Model
filterResults = filterResults results =
filterResultsHelp []
filterResultsHelp : List Component.SearchResult.Model -> List Component.SearchResult.Model -> List Component.SearchResult.Model
filterResultsHelp output results =
case results of case results of
[] -> [] ->
output []
first :: rest -> first :: rest ->
if first.stars > 0 then if first.stars > 0 then
filterResultsHelp (first :: output) rest first :: (filterResults rest)
else else
filterResultsHelp output rest filterResults rest
onInput address wrap = onInput address wrap =
@@ -111,7 +106,7 @@ viewSearchResult : Address Action -> Component.SearchResult.Model -> Html
viewSearchResult address result = viewSearchResult address result =
Component.SearchResult.view Component.SearchResult.view
(Signal.forwardTo address (UpdateSearchResult result.id)) (Signal.forwardTo address (UpdateSearchResult result.id))
result (Debug.log "rendering result..." result)
type Action type Action

View File

@@ -26,12 +26,8 @@ type Action
update : Action -> Model -> ( Model, Effects Action ) update : Action -> Model -> ( Model, Effects Action )
update action model = update action model =
case action of -- TODO make expand and collapse work
Expand -> ( model, Effects.none )
( { model | expanded = True }, Effects.none )
Collapse ->
( { model | expanded = False }, Effects.none )
view : Address Action -> Model -> Html view : Address Action -> Model -> Html
@@ -44,11 +40,13 @@ view address model =
[ href ("https://github.com/" ++ model.name), target "_blank" ] [ href ("https://github.com/" ++ model.name), target "_blank" ]
[ text model.name ] [ text model.name ]
, button , button
[ class "hide-result", onClick address Collapse ] -- TODO when the user clicks, send a Collapse action
[ class "hide-result" ]
[ text "X" ] [ text "X" ]
] ]
else else
[ button [ button
[ class "expand-result", onClick address Expand ] -- TODO when the user clicks, send an Expand action
[ class "expand-result" ]
[ text "Show" ] [ text "Show" ]
] ]

View File

@@ -16,8 +16,10 @@ to fail; in that case, just run `elm package install` again.)
elm live Main.elm --open -- --output=elm.js elm live Main.elm --open -- --output=elm.js
``` ```
## Compiling CSS ## Running Tests
```bash ```bash
elm css css/Stylesheets.elm cd test
elm package install
elm test TestRunner.elm
``` ```

View File

@@ -12,8 +12,7 @@
"evancz/elm-effects": "2.0.0 <= v < 3.0.0", "evancz/elm-effects": "2.0.0 <= v < 3.0.0",
"evancz/elm-html": "4.0.0 <= v < 5.0.0", "evancz/elm-html": "4.0.0 <= v < 5.0.0",
"evancz/elm-http": "3.0.0 <= v < 4.0.0", "evancz/elm-http": "3.0.0 <= v < 4.0.0",
"evancz/start-app": "2.0.0 <= v < 3.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" "elm-version": "0.16.0 <= v < 0.17.0"
} }

View File

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

View File

@@ -82,16 +82,21 @@ viewSearchResults address results =
filterResults : List Component.SearchResult.Model -> List Component.SearchResult.Model 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 case results of
[] -> [] ->
[] output
first :: rest -> first :: rest ->
if first.stars > 0 then if first.stars > 0 then
first :: (filterResults rest) filterResultsHelp (first :: output) rest
else else
filterResults rest filterResultsHelp output rest
onInput address wrap = onInput address wrap =
@@ -106,7 +111,7 @@ viewSearchResult : Address Action -> Component.SearchResult.Model -> Html
viewSearchResult address result = viewSearchResult address result =
Component.SearchResult.view Component.SearchResult.view
(Signal.forwardTo address (UpdateSearchResult result.id)) (Signal.forwardTo address (UpdateSearchResult result.id))
(Debug.log "rendering result..." result) result
type Action type Action

View File

@@ -26,8 +26,12 @@ type Action
update : Action -> Model -> ( Model, Effects Action ) update : Action -> Model -> ( Model, Effects Action )
update action model = update action model =
-- TODO make expand and collapse work case action of
( model, Effects.none ) Expand ->
( { model | expanded = True }, Effects.none )
Collapse ->
( { model | expanded = False }, Effects.none )
view : Address Action -> Model -> Html view : Address Action -> Model -> Html
@@ -40,13 +44,11 @@ view address model =
[ href ("https://github.com/" ++ model.name), target "_blank" ] [ href ("https://github.com/" ++ model.name), target "_blank" ]
[ text model.name ] [ text model.name ]
, button , button
-- TODO when the user clicks, send a Collapse action [ class "hide-result", onClick address Collapse ]
[ class "hide-result" ]
[ text "X" ] [ text "X" ]
] ]
else else
[ button [ button
-- TODO when the user clicks, send an Expand action [ class "expand-result", onClick address Expand ]
[ class "expand-result" ]
[ text "Show" ] [ text "Show" ]
] ]

27
stages/12/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

23
stages/12/README.md Normal file
View File

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

BIN
stages/12/elm-hub.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

26
stages/12/index.html Normal file
View File

@@ -0,0 +1,26 @@
<!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">
<link rel="icon" type="image/png" href="elm-hub.png">
</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>

1
stages/12/style.css Normal file
View File

@@ -0,0 +1 @@

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

35
stages/12/test/Tests.elm Normal file
View File

@@ -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 }
]
)
]

View File

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

View File

@@ -11,6 +11,7 @@ import Json.Decode exposing (Decoder, (:=))
import Json.Encode import Json.Encode
import Signal exposing (Address) import Signal exposing (Address)
import Dict exposing (Dict) import Dict exposing (Dict)
import SearchResult
searchFeed : String -> Task x Action searchFeed : String -> Task x Action
@@ -29,37 +30,18 @@ searchFeed query =
Task.onError task (\_ -> Task.succeed (SetResults [])) Task.onError task (\_ -> Task.succeed (SetResults []))
responseDecoder : Decoder (List SearchResult) responseDecoder : Decoder (List SearchResult.Model)
responseDecoder = responseDecoder =
"items" := Json.Decode.list searchResultDecoder -- TODO use SearchResult's decoder
Json.Decode.succeed []
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 = type alias Model =
{ query : String { 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 : Model
initialModel = initialModel =
{ query = "tutorial" { 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 = viewSearchResults address results =
results results
|> Dict.values |> Dict.values
|> List.sortBy (.stars >> negate) |> List.sortBy (.stars >> negate)
|> filterResults |> List.map (SearchResult.view address)
|> 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
[]
onInput address wrap = onInput address wrap =
@@ -108,33 +82,11 @@ defaultValue str =
property "defaultValue" (Json.Encode.string 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 type Action
= Search = Search
| SetQuery String | SetQuery String
| DeleteById ResultId | DeleteById SearchResult.ResultId
| SetResults (List SearchResult) | SetResults (List SearchResult.Model)
update : Action -> Model -> ( Model, Effects Action ) update : Action -> Model -> ( Model, Effects Action )
@@ -148,7 +100,7 @@ update action model =
SetResults results -> SetResults results ->
let let
resultsById : Dict ResultId SearchResult resultsById : Dict SearchResult.ResultId SearchResult.Model
resultsById = resultsById =
results results
|> List.map (\result -> ( result.id, result )) |> List.map (\result -> ( result.id, result ))

45
stages/9/SearchResult.elm Normal file
View File

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