From a171aebb1f2c9a1ef9eac21319c137453f51300a Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 6 Sep 2016 20:43:33 -0700 Subject: [PATCH] Rename part14 to part12 --- part12/ElmHub.elm | 328 ++++++++++++++++++++++++++++ part12/ElmHubCss.elm | 148 +++++++++++++ part12/Main.elm | 14 ++ {part14 => part12}/README.md | 0 {part14 => part12}/Stylesheets.elm | 0 {part14 => part12}/elm-hub.png | Bin {part14 => part12}/elm-package.json | 4 - {part14 => part12}/github.js | 0 {part14 => part12}/index.html | 0 part14/ElmHub.elm | 146 ------------- part14/ElmHubCss.elm | 15 -- part14/Main.elm | 126 ----------- part14/Page.elm | 26 --- part14/Page/Home.elm | 204 ----------------- part14/Page/Repository.elm | 136 ------------ part14/SearchResult.elm | 63 ------ part14/style.css | 101 --------- 17 files changed, 490 insertions(+), 821 deletions(-) create mode 100644 part12/ElmHub.elm create mode 100644 part12/ElmHubCss.elm create mode 100644 part12/Main.elm rename {part14 => part12}/README.md (100%) rename {part14 => part12}/Stylesheets.elm (100%) rename {part14 => part12}/elm-hub.png (100%) rename {part14 => part12}/elm-package.json (73%) rename {part14 => part12}/github.js (100%) rename {part14 => part12}/index.html (100%) delete mode 100644 part14/ElmHub.elm delete mode 100644 part14/ElmHubCss.elm delete mode 100644 part14/Main.elm delete mode 100644 part14/Page.elm delete mode 100644 part14/Page/Home.elm delete mode 100644 part14/Page/Repository.elm delete mode 100644 part14/SearchResult.elm delete mode 100644 part14/style.css diff --git a/part12/ElmHub.elm b/part12/ElmHub.elm new file mode 100644 index 0000000..10d181d --- /dev/null +++ b/part12/ElmHub.elm @@ -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" + ) diff --git a/part12/ElmHubCss.elm b/part12/ElmHubCss.elm new file mode 100644 index 0000000..06a95f2 --- /dev/null +++ b/part12/ElmHubCss.elm @@ -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; + -- } + ] diff --git a/part12/Main.elm b/part12/Main.elm new file mode 100644 index 0000000..32ce3e1 --- /dev/null +++ b/part12/Main.elm @@ -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 + } diff --git a/part14/README.md b/part12/README.md similarity index 100% rename from part14/README.md rename to part12/README.md diff --git a/part14/Stylesheets.elm b/part12/Stylesheets.elm similarity index 100% rename from part14/Stylesheets.elm rename to part12/Stylesheets.elm diff --git a/part14/elm-hub.png b/part12/elm-hub.png similarity index 100% rename from part14/elm-hub.png rename to part12/elm-hub.png diff --git a/part14/elm-package.json b/part12/elm-package.json similarity index 73% rename from part14/elm-package.json rename to part12/elm-package.json index 985cddb..5a57200 100644 --- a/part14/elm-package.json +++ b/part12/elm-package.json @@ -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" diff --git a/part14/github.js b/part12/github.js similarity index 100% rename from part14/github.js rename to part12/github.js diff --git a/part14/index.html b/part12/index.html similarity index 100% rename from part14/index.html rename to part12/index.html diff --git a/part14/ElmHub.elm b/part14/ElmHub.elm deleted file mode 100644 index 3940987..0000000 --- a/part14/ElmHub.elm +++ /dev/null @@ -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 ] diff --git a/part14/ElmHubCss.elm b/part14/ElmHubCss.elm deleted file mode 100644 index 841de39..0000000 --- a/part14/ElmHubCss.elm +++ /dev/null @@ -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" ] - ] - ] diff --git a/part14/Main.elm b/part14/Main.elm deleted file mode 100644 index 7ac6308..0000000 --- a/part14/Main.elm +++ /dev/null @@ -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 ) diff --git a/part14/Page.elm b/part14/Page.elm deleted file mode 100644 index 0ed0b1f..0000000 --- a/part14/Page.elm +++ /dev/null @@ -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 diff --git a/part14/Page/Home.elm b/part14/Page/Home.elm deleted file mode 100644 index 232b06e..0000000 --- a/part14/Page/Home.elm +++ /dev/null @@ -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 diff --git a/part14/Page/Repository.elm b/part14/Page/Repository.elm deleted file mode 100644 index 04ca584..0000000 --- a/part14/Page/Repository.elm +++ /dev/null @@ -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 ) diff --git a/part14/SearchResult.elm b/part14/SearchResult.elm deleted file mode 100644 index 36f58df..0000000 --- a/part14/SearchResult.elm +++ /dev/null @@ -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" ] - ] diff --git a/part14/style.css b/part14/style.css deleted file mode 100644 index d030733..0000000 --- a/part14/style.css +++ /dev/null @@ -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; -}