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 Html.Lazy exposing (lazy, lazy3)
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 =
{ minStars : Int
, minStarsError : Maybe String
, searchInDescription : Bool
, userFilter : String
}
type alias SearchResult =
{ id : Int
, name : String
, stars : Int
}
initialModel : Model
initialModel =
{ query = "tutorial"
, results = []
, errorMessage = Nothing
, options =
{ minStars = 0
, minStarsError = Nothing
, searchInDescription = True
, userFilter = ""
}
, tableState = Table.initialSort "Stars"
}
init : ( Model, Cmd Msg )
init =
( initialModel, githubSearch (getQueryString initialModel) )
subscriptions : Model -> Sub Msg
subscriptions _ =
githubResponse decodeResponse
viewOptions : SearchOptions -> Html OptionsMsg
viewOptions opts =
-- TODO add this line so we can tell whenever this function gets executed:
-- Debug.log "viewOptions was called" <|
div [ class "search-options" ]
[ div [ class "search-option" ]
[ label [ class "top-label" ] [ text "Minimum Stars" ]
, input
[ onBlurWithTargetValue SetMinStars
, defaultValue (toString opts.minStars)
]
[]
]
, div [ class "search-option" ]
[ label [ class "top-label" ] [ text "Owned by" ]
, input
[ type' "text"
, placeholder "Enter a username"
, defaultValue opts.userFilter
, onInput SetUserFilter
]
[]
]
, div [ class "search-option" ]
[ viewMinStarsError opts.minStarsError ]
, label [ class "search-option" ]
[ input [ type' "checkbox", checked opts.searchInDescription, onCheck SetSearchInDescription ] []
, text "Search in description"
]
]
viewMinStarsError : Maybe String -> Html msg
viewMinStarsError message =
case message of
Nothing ->
text " "
Just errorMessage ->
div [ class "stars-error" ] [ text errorMessage ]
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 )
onBlurWithTargetValue : (String -> msg) -> Attribute msg
onBlurWithTargetValue toMsg =
on "blur" (Json.Decode.map toMsg targetValue)
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
SetMinStars minStarsStr ->
case String.toInt minStarsStr of
Ok minStars ->
{ options | minStars = minStars, minStarsError = Nothing }
Err _ ->
{ options
| minStarsError =
Just "Please enter an integer!"
}
SetSearchInDescription searchInDescription ->
{ options | searchInDescription = searchInDescription }
SetUserFilter userFilter ->
{ options | userFilter = userFilter }
view : Model -> Html Msg
view model =
div [ class "content" ]
[ header []
[ h1 [] [ text "ElmHub" ]
, span [ class "tagline" ] [ text "Like GitHub, but for Elm things." ]
]
, div [ class "search" ]
-- TODO change this to (lazy viewOptions model.options)
-- and verify that it no longer shows the Debug.log message every
-- time you type in the input box. Instead, it should only show
-- once on page load, and then again when the options change.
[ 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
-- TODO add a lazy3 to wrap Table.view.
-- (We have no Debug.log for verification this time.)
, Table.view tableConfig model.tableState model.results
]
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 [ href ("https://github.com/" ++ result.name), target "_blank" ]
[ text result.name ]
, button [ class "hide-result", onClick (DeleteById result.id) ]
[ text "X" ]
]
type OptionsMsg
= SetMinStars String
| SetSearchInDescription Bool
| SetUserFilter String
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
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"
)
++ "+stars:>="
++ (toString model.options.minStars)
++ "+language:elm"
++ (if String.isEmpty model.options.userFilter then
""
else
"+user:" ++ model.options.userFilter
)