Rename part14 to part12

This commit is contained in:
Richard Feldman
2016-09-06 20:43:33 -07:00
parent ce5f0d0b60
commit a171aebb1f
17 changed files with 490 additions and 821 deletions

328
part12/ElmHub.elm Normal file
View File

@@ -0,0 +1,328 @@
port module ElmHub exposing (..)
import Html exposing (..)
import Html.Attributes exposing (class, target, href, defaultValue, type', checked, placeholder, value)
import Html.Events exposing (..)
import Html.App as Html
import Auth
import Json.Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (..)
import String
import Table
responseDecoder : Decoder (List SearchResult)
responseDecoder =
Json.Decode.at [ "items" ] (Json.Decode.list searchResultDecoder)
searchResultDecoder : Decoder SearchResult
searchResultDecoder =
decode SearchResult
|> required "id" Json.Decode.int
|> required "full_name" Json.Decode.string
|> required "stargazers_count" Json.Decode.int
type alias Model =
{ query : String
, results : List SearchResult
, errorMessage : Maybe String
, options : SearchOptions
, tableState : Table.State
}
type alias SearchOptions =
{ sort : String
, ascending : Bool
, searchInDescription : Bool
, userFilter : String
}
type alias SearchResult =
{ id : Int
, name : String
, stars : Int
}
initialModel : Model
initialModel =
{ query = "tutorial"
, results = []
, errorMessage = Nothing
, options =
{ sort = "stars"
, ascending = False
, searchInDescription = True
, userFilter = ""
}
, tableState = Table.initialSort "Stars"
}
init : ( Model, Cmd Msg )
init =
( initialModel, githubSearch (getQueryString initialModel) )
subscriptions : Model -> Sub Msg
subscriptions _ =
githubResponse decodeResponse
type Msg
= Search
| Options OptionsMsg
| SetQuery String
| DeleteById Int
| HandleSearchResponse (List SearchResult)
| HandleSearchError (Maybe String)
| SetTableState Table.State
| DoNothing
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Options optionsMsg ->
( { model | options = updateOptions optionsMsg model.options }, Cmd.none )
Search ->
( model, githubSearch (getQueryString model) )
SetQuery query ->
( { model | query = query }, Cmd.none )
HandleSearchResponse results ->
( { model | results = results }, Cmd.none )
HandleSearchError error ->
( { model | errorMessage = error }, Cmd.none )
DeleteById idToHide ->
let
newResults =
model.results
|> List.filter (\{ id } -> id /= idToHide)
newModel =
{ model | results = newResults }
in
( newModel, Cmd.none )
SetTableState tableState ->
( { model | tableState = tableState }, Cmd.none )
DoNothing ->
( model, Cmd.none )
tableConfig : Table.Config SearchResult Msg
tableConfig =
Table.config
{ toId = .id >> toString
, toMsg = SetTableState
, columns = [ starsColumn, nameColumn ]
}
starsColumn : Table.Column SearchResult Msg
starsColumn =
Table.veryCustomColumn
{ name = "Stars"
, viewData = viewStars
, sorter = Table.increasingOrDecreasingBy (negate << .stars)
}
nameColumn : Table.Column SearchResult Msg
nameColumn =
Table.veryCustomColumn
{ name = "Name"
, viewData = viewSearchResult
, sorter = Table.increasingOrDecreasingBy .name
}
updateOptions : OptionsMsg -> SearchOptions -> SearchOptions
updateOptions optionsMsg options =
case optionsMsg of
SetSort sort ->
{ options | sort = sort }
SetAscending ascending ->
{ options | ascending = ascending }
SetSearchInDescription searchInDescription ->
{ options | searchInDescription = searchInDescription }
SetUserFilter userFilter ->
{ options | userFilter = userFilter }
view : Model -> Html Msg
view model =
let
currentTableState : Table.State
currentTableState =
model.tableState
in
div [ class "content" ]
[ header []
[ h1 [] [ text "ElmHub" ]
, span [ class "tagline" ] [ text "Like GitHub, but for Elm things." ]
]
, div [ class "search" ]
[ Html.map Options (viewOptions model.options)
, div [ class "search-input" ]
[ input [ class "search-query", onInput SetQuery, defaultValue model.query ] []
, button [ class "search-button", onClick Search ] [ text "Search" ]
]
]
, viewErrorMessage model.errorMessage
, Table.view tableConfig currentTableState model.results
]
viewErrorMessage : Maybe String -> Html a
viewErrorMessage errorMessage =
case errorMessage of
Just message ->
div [ class "error" ] [ text message ]
Nothing ->
text ""
viewSearchResult : SearchResult -> Html Msg
viewSearchResult result =
li []
[ span [ class "star-count" ] [ text (toString result.stars) ]
, a [ href ("https://github.com/" ++ result.name), target "_blank" ]
[ text result.name ]
, button [ class "hide-result", onClick (DeleteById result.id) ]
[ text "X" ]
]
type OptionsMsg
= SetSort String
| SetAscending Bool
| SetSearchInDescription Bool
| SetUserFilter String
viewOptions : SearchOptions -> Html OptionsMsg
viewOptions opts =
div [ class "search-options" ]
[ div [ class "search-option" ]
[ label [ class "top-label" ] [ text "Sort by" ]
, select [ onChange SetSort, value opts.sort ]
[ option [ value "stars" ] [ text "Stars" ]
, option [ value "forks" ] [ text "Forks" ]
, option [ value "updated" ] [ text "Updated" ]
]
]
, div [ class "search-option" ]
[ label [ class "top-label" ] [ text "Owned by" ]
, input
[ type' "text"
, placeholder "Enter a username"
, defaultValue opts.userFilter
, onInput SetUserFilter
]
[]
]
, label [ class "search-option" ]
[ input [ type' "checkbox", checked opts.ascending, onCheck SetAscending ] []
, text "Sort ascending"
]
, label [ class "search-option" ]
[ input [ type' "checkbox", checked opts.searchInDescription, onCheck SetSearchInDescription ] []
, text "Search in description"
]
]
decodeGithubResponse : Json.Decode.Value -> Msg
decodeGithubResponse value =
case Json.Decode.decodeValue responseDecoder value of
Ok results ->
HandleSearchResponse results
Err err ->
HandleSearchError (Just err)
onChange : (String -> msg) -> Attribute msg
onChange toMsg =
on "change" (Json.Decode.map toMsg Html.Events.targetValue)
decodeResponse : Json.Decode.Value -> Msg
decodeResponse json =
case Json.Decode.decodeValue responseDecoder json of
Err err ->
HandleSearchError (Just err)
Ok results ->
HandleSearchResponse results
port githubSearch : String -> Cmd msg
port githubResponse : (Json.Decode.Value -> msg) -> Sub msg
{-| NOTE: The following is not part of the exercise, but is food for thought if
you have extra time.
There are several opportunities to improve this getQueryString implementation.
A nice refactor of this would not change the type annotation! It would still be:
getQueryString : Model -> String
Try identifying patterns and writing helper functions which are responsible for
handling those patterns. Then have this function call them. Things to consider:
* There's pattern of adding "+foo:bar" - could we write a helper function for this?
* In one case, if the "bar" in "+foo:bar" is empty, we want to return "" instead
of "+foo:" - is this always true? Should our helper function always do that?
* We also join query parameters together with "=" and "&" a lot. Can we give
that pattern a similar treatment? Should we also take "?" into account?
If you have time, give this refactor a shot and see how it turns out!
Writing something out the long way like this, and then refactoring to something
nicer, is generally the preferred way to go about building things in Elm.
-}
getQueryString : Model -> String
getQueryString model =
-- See https://developer.github.com/v3/search/#example for how to customize!
"access_token="
++ Auth.token
++ "&q="
++ model.query
++ (if model.options.searchInDescription then
"+in:name,description"
else
"+in:name"
)
++ "+language:elm"
++ (if String.isEmpty model.options.userFilter then
""
else
"+user:" ++ model.options.userFilter
)
++ "&sort="
++ model.options.sort
++ "&order="
++ (if model.options.ascending then
"asc"
else
"desc"
)

148
part12/ElmHubCss.elm Normal file
View File

@@ -0,0 +1,148 @@
module ElmHubCss exposing (..)
import Css exposing (..)
css : Stylesheet
css =
stylesheet
[ ((.) "content")
[ width (px 960)
, margin2 zero auto
, padding (px 30)
, fontFamilies [ "Helvetica", "Arial", "serif" ]
]
-- TODO convert these remaining styles to use elm-css.
--
-- 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;
-- }
--
-- .error {
-- background-color: #FF9632;
-- padding: 20px;
-- box-sizing: border-box;
-- overflow-x: auto;
-- font-family: monospace;
-- font-size: 18px;
-- }
--
-- .search-input {
-- display: block;
-- float: left;
-- width: 50%;
-- }
--
-- .search-options {
-- position: relative;
-- float: right;
-- width: 50%;
-- box-sizing: border-box;
-- padding: 20px;
-- }
--
-- .search-option {
-- display: block;
-- float: left;
-- width: 50%;
-- box-sizing: border-box;
-- }
--
-- .search-option input[type="text"] {
-- padding: 5px;
-- box-sizing: border-box;
-- width: 90%;
-- }
--
-- .search:after {
-- content: "";
-- display: table;
-- clear: both;
-- }
--
-- .top-label {
-- display: block;
-- color: #555;
-- }
]

14
part12/Main.elm Normal file
View File

@@ -0,0 +1,14 @@
module Main exposing (main)
import ElmHub
import Html.App as Html
main : Program Never
main =
Html.program
{ view = ElmHub.view
, update = ElmHub.update
, init = ElmHub.init
, subscriptions = ElmHub.subscriptions
}

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -9,14 +9,10 @@
],
"exposed-modules": [],
"dependencies": {
"Fresheyeball/elm-tuple-extra": "2.1.0 <= v < 3.0.0",
"NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0",
"elm-lang/core": "4.0.1 <= v < 5.0.0",
"elm-lang/html": "1.0.0 <= v < 2.0.0",
"elm-lang/navigation": "1.0.0 <= v < 2.0.0",
"evancz/elm-http": "3.0.1 <= v < 4.0.0",
"evancz/elm-sortable-table": "1.0.0 <= v < 2.0.0",
"evancz/url-parser": "1.0.0 <= v < 2.0.0",
"rtfeldman/elm-css": "5.0.0 <= v < 6.0.0"
},
"elm-version": "0.17.0 <= v < 0.18.0"

View File

@@ -1,146 +0,0 @@
module ElmHub exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Html.App as Html
import Http
import Auth
import Task exposing (Task)
import Json.Decode exposing (Decoder)
import Dict exposing (Dict)
import SearchResult
searchFeed : String -> Cmd Msg
searchFeed query =
let
url =
"https://api.github.com/search/repositories?access_token="
++ Auth.token
++ "&q="
++ query
++ "+language:elm&sort=stars&order=desc"
in
Task.perform HandleSearchError HandleSearchResponse (Http.get responseDecoder url)
responseDecoder : Decoder (List SearchResult.Model)
responseDecoder =
Json.Decode.at [ "items" ] (Json.Decode.list SearchResult.decoder)
type alias Model =
{ query : String
, results : Dict Int SearchResult.Model
, errorMessage : Maybe String
}
initialModel : Model
initialModel =
{ query = "tutorial"
, results = Dict.empty
, errorMessage = Nothing
}
view : Model -> Html Msg
view model =
div [ class "content" ]
[ header []
[ h1 [] [ text "ElmHub" ]
, span [ class "tagline" ] [ text "Like GitHub, but for Elm things." ]
]
, input [ class "search-query", onInput SetQuery, defaultValue model.query ] []
, button [ class "search-button", onClick Search ] [ text "Search" ]
, viewErrorMessage model.errorMessage
, ul [ class "results" ] (viewSearchResults model.results)
]
viewErrorMessage : Maybe String -> Html a
viewErrorMessage errorMessage =
case errorMessage of
Just message ->
div [ class "error" ] [ text message ]
Nothing ->
text ""
viewSearchResults : Dict Int SearchResult.Model -> List (Html Msg)
viewSearchResults results =
results
|> Dict.values
|> List.sortBy (.stars >> negate)
|> filterResults
|> List.map viewSearchResult
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
viewSearchResult : SearchResult.Model -> Html Msg
viewSearchResult result =
result
|> SearchResult.view
|> Html.App.map (UpdateSearchResult result.id)
type Msg
= Search
| SetQuery String
| UpdateSearchResult Int SearchResult.Msg
| HandleSearchResponse (List SearchResult.Model)
| HandleSearchError Http.Error
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Search ->
model ! [ searchFeed model.query ]
SetQuery query ->
{ model | query = query, errorMessage = Nothing } ! []
HandleSearchError error ->
case error of
Http.UnexpectedPayload str ->
{ model | errorMessage = Just str } ! []
_ ->
{ model | errorMessage = Just "Error loading search results" } ! []
HandleSearchResponse results ->
let
resultsById : Dict Int SearchResult.Model
resultsById =
results
|> List.map (\result -> ( result.id, result ))
|> Dict.fromList
in
{ model | results = resultsById } ! []
UpdateSearchResult id childMsg ->
case Dict.get id model.results of
Nothing ->
model ! []
Just childModel ->
let
( newChildModel, childCmd ) =
SearchResult.update childMsg childModel
cmd =
Cmd.map (UpdateSearchResult id) childCmd
newResults =
Dict.insert id newChildModel model.results
in
{ model | results = newResults } ! [ cmd ]

View File

@@ -1,15 +0,0 @@
module ElmHubCss exposing (..)
import Css exposing (..)
css : Stylesheet
css =
stylesheet
[ ((.) "content")
[ width (px 960)
, margin2 zero auto
, padding (px 30)
, fontFamilies [ "Helvetica", "Arial", "serif" ]
]
]

View File

@@ -1,126 +0,0 @@
module Main exposing (..)
import Page.Home
import Page.Repository
import Navigation
import Page exposing (Page(..))
import Tuple2
import Html exposing (Html, div, h1, header, text, span)
import Html.Attributes exposing (class)
import Html.App as Html
type Model
= Home Page.Home.Model
| Repository Page.Repository.Model
| NotFound
type Msg
= HomeMsg Page.Home.Msg
| RepositoryMsg Page.Repository.Msg
main : Program Never
main =
Navigation.program (Navigation.makeParser Page.parser)
{ init = init
, subscriptions = subscriptions
, view = view
, update = update
, urlUpdate = urlUpdate
}
subscriptions : Model -> Sub Msg
subscriptions model =
case model of
Home pageModel ->
Page.Home.subscriptions pageModel
|> Sub.map HomeMsg
Repository pageModel ->
-- Repository has no subscriptions, so there's nothing to translate!
Sub.none
NotFound ->
-- NotFound has no subscriptions, so there's nothing to translate!
Sub.none
init : Result String Page -> ( Model, Cmd Msg )
init result =
case result of
Ok (Page.Home) ->
Page.Home.init
|> Tuple2.mapEach Home (Cmd.map HomeMsg)
Ok (Page.Repository repoOwner repoName) ->
Page.Repository.init repoOwner repoName
|> Tuple2.mapEach Repository (Cmd.map RepositoryMsg)
Ok (Page.NotFound) ->
( NotFound, Cmd.none )
Err err ->
( NotFound, Cmd.none )
view : Model -> Html Msg
view model =
withHeader <|
case model of
Home pageModel ->
Page.Home.view pageModel
|> Html.map HomeMsg
Repository pageModel ->
Page.Repository.view pageModel
|> Html.map RepositoryMsg
NotFound ->
h1 [] [ text "Page Not Found" ]
withHeader : Html msg -> Html msg
withHeader innerContent =
div [ class "content" ]
[ header []
[ h1 [] [ text "ElmHub" ]
, span [ class "tagline" ] [ text "Like GitHub, but for Elm things." ]
]
, innerContent
]
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case ( msg, model ) of
( HomeMsg pageMsg, Home pageModel ) ->
Page.Home.update pageMsg pageModel
|> Tuple2.mapEach Home (Cmd.map HomeMsg)
( RepositoryMsg pageMsg, Repository pageModel ) ->
Page.Repository.update pageMsg pageModel
|> Tuple2.mapEach Repository (Cmd.map RepositoryMsg)
_ ->
( model, Cmd.none )
urlUpdate : Result String Page -> Model -> ( Model, Cmd Msg )
urlUpdate result model =
case result of
Ok (Page.Home) ->
Page.Home.init
|> Tuple2.mapEach Home (Cmd.map HomeMsg)
Ok (Page.Repository repoOwner repoName) ->
Page.Repository.init repoOwner repoName
|> Tuple2.mapEach Repository (Cmd.map RepositoryMsg)
Ok (Page.NotFound) ->
( NotFound, Cmd.none )
Err err ->
( NotFound, Cmd.none )

View File

@@ -1,26 +0,0 @@
module Page exposing (..)
import Navigation
import UrlParser exposing (Parser, (</>), format, int, s, string)
import String
type Page
= Home
| Repository String String
| NotFound
pageParser : Parser (Page -> a) a
pageParser =
UrlParser.oneOf
[ format Home (s "")
, format Repository (s "repositories" </> string </> string)
]
parser : Navigation.Location -> Result String Page
parser location =
location.pathname
|> String.dropLeft 1
|> UrlParser.parse identity pageParser

View File

@@ -1,204 +0,0 @@
port module Page.Home exposing (..)
import Html exposing (..)
import Html.Attributes exposing (class, target, href, property, defaultValue)
import Html.Events exposing (..)
import Auth
import Json.Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (decode, required)
import Navigation
import Table
type alias SearchResult =
{ id : Int
, name : String
, stars : Int
}
searchResultDecoder : Decoder SearchResult
searchResultDecoder =
decode SearchResult
|> required "id" Json.Decode.int
|> required "full_name" Json.Decode.string
|> required "stargazers_count" Json.Decode.int
getQueryString : String -> String
getQueryString query =
-- See https://developer.github.com/v3/search/#example for how to customize!
"access_token="
++ Auth.token
++ "&q="
++ query
++ "+language:elm&sort=stars&order=desc"
responseDecoder : Decoder (List SearchResult)
responseDecoder =
Json.Decode.at [ "items" ] (Json.Decode.list searchResultDecoder)
type alias Model =
{ query : String
, results : List SearchResult
, errorMessage : Maybe String
, tableState : Table.State
}
initialQuery : String
initialQuery =
"tutorial"
init : ( Model, Cmd Msg )
init =
( { query = initialQuery
, results = []
, errorMessage = Nothing
, tableState = Table.initialSort "Stars"
}
, githubSearch (getQueryString initialQuery)
)
view : Model -> Html Msg
view model =
div [ class "home-container" ]
[ input [ class "search-query", onInput SetQuery, defaultValue model.query ] []
, button [ class "search-button", onClick Search ] [ text "Search" ]
, viewErrorMessage model.errorMessage
, Table.view tableConfig model.tableState model.results
]
tableConfig : Table.Config SearchResult Msg
tableConfig =
Table.config
{ toId = .id >> toString
, toMsg = SetTableState
, columns = [ starsColumn, nameColumn ]
}
starsColumn : Table.Column SearchResult Msg
starsColumn =
Table.veryCustomColumn
{ name = "Stars"
, viewData = viewStars
, sorter = Table.increasingOrDecreasingBy (negate << .stars)
}
nameColumn : Table.Column SearchResult Msg
nameColumn =
Table.veryCustomColumn
{ name = "Name"
, viewData = viewSearchResult
, sorter = Table.increasingOrDecreasingBy .name
}
viewErrorMessage : Maybe String -> Html a
viewErrorMessage errorMessage =
case errorMessage of
Just message ->
div [ class "error" ] [ text message ]
Nothing ->
text ""
viewStars : SearchResult -> Table.HtmlDetails Msg
viewStars result =
Table.HtmlDetails []
[ span [ class "star-count" ] [ text (toString result.stars) ] ]
viewSearchResult : SearchResult -> Table.HtmlDetails Msg
viewSearchResult result =
Table.HtmlDetails []
[ a [ onClick (Visit ("/repositories/" ++ result.name)) ] [ text result.name ]
, button [ class "hide-result", onClick (DeleteById result.id) ]
[ text "X" ]
]
type Msg
= Search
| Visit String
| SetQuery String
| DeleteById Int
| HandleSearchResponse (List SearchResult)
| HandleSearchError (Maybe String)
| SetTableState Table.State
| DoNothing
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Visit url ->
( model, Navigation.newUrl url )
Search ->
( model, githubSearch (getQueryString model.query) )
SetQuery query ->
( { model | query = query }, Cmd.none )
HandleSearchResponse results ->
( { model | results = results }, Cmd.none )
HandleSearchError error ->
( { model | errorMessage = error }, Cmd.none )
DeleteById idToHide ->
let
newResults =
model.results
|> List.filter (\{ id } -> id /= idToHide)
newModel =
{ model | results = newResults }
in
( newModel, Cmd.none )
SetTableState newState ->
( { model | tableState = newState }, Cmd.none )
DoNothing ->
( model, Cmd.none )
decodeGithubResponse : Json.Decode.Value -> Msg
decodeGithubResponse value =
case Json.Decode.decodeValue responseDecoder value of
Ok results ->
HandleSearchResponse results
Err err ->
HandleSearchError (Just err)
decodeResponse : Json.Decode.Value -> Msg
decodeResponse json =
case Json.Decode.decodeValue responseDecoder json of
Err err ->
HandleSearchError (Just err)
Ok results ->
HandleSearchResponse results
subscriptions : Model -> Sub Msg
subscriptions _ =
githubResponse decodeResponse
port githubSearch : String -> Cmd msg
port githubResponse : (Json.Decode.Value -> msg) -> Sub msg

View File

@@ -1,136 +0,0 @@
module Page.Repository exposing (..)
import Html exposing (..)
import Html.Attributes exposing (class, target, href, property, defaultValue, src)
import Auth
import Http
import Task
import Json.Decode exposing (Decoder, int, string, list)
import Json.Decode.Pipeline exposing (decode, required)
type alias Model =
{ repoOwner : String
, repoName : String
, repository : Maybe Repository
}
type alias Repository =
{ id : Int
, issues : Int
, forks : Int
, watchers : Int
, owner : User
, description : String
}
type alias User =
{ id : Int
, username : String
, avatarUrl : String
, profileUrl : String
}
userDecoder : Decoder User
userDecoder =
decode User
|> required "id" int
|> required "login" string
|> required "avatar_url" string
|> required "url" string
repoDecoder : Decoder Repository
repoDecoder =
decode Repository
|> required "id" int
|> required "open_issues_count" int
|> required "forks" int
|> required "watchers" int
|> required "owner" userDecoder
|> required "description" string
init : String -> String -> ( Model, Cmd Msg )
init repoOwner repoName =
( { repoOwner = repoOwner
, repoName = repoName
, repository = Nothing
}
, getRepoInfo repoOwner repoName
)
view : Model -> Html Msg
view model =
let
ownerUrl =
"https://github.com/" ++ model.repoOwner
repoUrl =
ownerUrl ++ "/" ++ model.repoName
details =
model.repository
|> Maybe.map viewDetails
|> Maybe.withDefault (text "")
in
div []
[ h2 []
[ a [ href repoUrl ] [ text model.repoName ] ]
, details
]
viewDetails : Repository -> Html Msg
viewDetails repo =
div []
[ p [ class "repo-description" ] [ text repo.description ]
, h3 []
[ a [ href repo.owner.profileUrl ]
[ img [ class "profile-photo", src repo.owner.avatarUrl ] []
, text repo.owner.username
]
]
, table []
[ tbody []
[ tr [] [ th [] [ text "issues" ], td [] [ text (toString repo.issues) ] ]
, tr [] [ th [] [ text "forks" ], td [] [ text (toString repo.forks) ] ]
, tr [] [ th [] [ text "watchers" ], td [] [ text (toString repo.watchers) ] ]
]
]
]
type Msg
= HandleRepoError Http.Error
| HandleRepoResponse Repository
getRepoInfo : String -> String -> Cmd Msg
getRepoInfo repoOwner repoName =
let
url =
"https://api.github.com/repos/"
++ repoOwner
++ "/"
++ repoName
++ "?access_token="
++ Auth.token
|> Debug.log "getRepoInfo"
in
Http.get repoDecoder url
|> Task.perform HandleRepoError HandleRepoResponse
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
HandleRepoError err ->
( model, Cmd.none )
HandleRepoResponse repository ->
( { model | repository = Just repository }, Cmd.none )

View File

@@ -1,63 +0,0 @@
module SearchResult exposing (..)
import Html exposing (..)
import Html.Attributes exposing (class, target, href, property, defaultValue)
import Html.Events exposing (..)
import Json.Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (..)
type alias Model =
{ id : Int
, name : String
, stars : Int
, expanded : Bool
}
type Msg
= Expand
| Collapse
decoder : Decoder Model
decoder =
decode Model
|> required "id" Json.Decode.int
|> required "full_name" Json.Decode.string
|> required "stargazers_count" Json.Decode.int
|> hardcoded True
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Expand ->
{ model | expanded = True } ! []
Collapse ->
{ model | expanded = False } ! []
view : Model -> Html Msg
view model =
li [] <|
if model.expanded then
[ span [ class "star-count" ] [ text (toString model.stars) ]
, a
[ href
("https://github.com/"
++ (Debug.log "TODO we should not see this when typing in the search box!"
model.name
)
)
, target "_blank"
]
[ text model.name ]
, button [ class "hide-result", onClick Collapse ]
[ text "X" ]
]
else
[ button [ class "expand-result", onClick Expand ]
[ text "Show" ]
]

View File

@@ -1,101 +0,0 @@
.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;
}
.error {
background-color: #FF9632;
padding: 20px;
box-sizing: border-box;
overflow-x: auto;
font-family: monospace;
font-size: 18px;
}