├── spotify ├── .gitignore ├── static │ └── interop.js ├── styles │ └── main.less ├── src │ ├── Album │ │ ├── Types.elm │ │ ├── State.elm │ │ ├── Rest.elm │ │ └── View.elm │ ├── App.elm │ ├── Types.elm │ ├── Events.elm │ ├── View │ │ └── Bootstrap.elm │ ├── Rest.elm │ ├── State.elm │ └── View.elm ├── test │ └── Test.elm ├── .dir-locals.el ├── elm-package.json └── README.md ├── .gitignore └── trees ├── src ├── Main.elm ├── Tree.elm ├── TreeView.elm └── TreeSolutions.elm ├── README.md └── elm-package.json /spotify/.gitignore: -------------------------------------------------------------------------------- 1 | /.shake 2 | /dist 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | elm.js 3 | index.html 4 | -------------------------------------------------------------------------------- /spotify/static/interop.js: -------------------------------------------------------------------------------- 1 | /*global Elm */ 2 | 3 | 4 | (function () { 5 | var app = Elm.App.fullscreen(); 6 | }()); 7 | -------------------------------------------------------------------------------- /trees/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | 3 | import Html exposing (..) 4 | import Tree 5 | import TreeView 6 | 7 | 8 | myTree = 9 | Tree.empty 10 | 11 | 12 | main : Html msg 13 | main = 14 | TreeView.draw 800 myTree 15 | -------------------------------------------------------------------------------- /spotify/styles/main.less: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px 0; 3 | } 4 | 5 | .panel { 6 | .panel-heading { 7 | height: 55px; 8 | overflow: hidden; 9 | } 10 | 11 | .panel-body { 12 | height: 300px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /spotify/src/Album/Types.elm: -------------------------------------------------------------------------------- 1 | module Album.Types exposing (..) 2 | 3 | 4 | type alias Image = 5 | String 6 | 7 | 8 | type alias Model = 9 | { id : String 10 | , name : String 11 | , images : List Image 12 | , showDetails : Bool 13 | } 14 | 15 | 16 | type Msg 17 | = ToggleDetails 18 | -------------------------------------------------------------------------------- /spotify/src/App.elm: -------------------------------------------------------------------------------- 1 | module App exposing (..) 2 | 3 | import Html.App 4 | import State 5 | import View 6 | 7 | 8 | main : Program Never 9 | main = 10 | Html.App.program 11 | { init = State.init 12 | , update = State.update 13 | , view = View.root 14 | , subscriptions = always Sub.none 15 | } 16 | -------------------------------------------------------------------------------- /spotify/src/Album/State.elm: -------------------------------------------------------------------------------- 1 | module Album.State exposing (..) 2 | 3 | import Album.Types exposing (..) 4 | 5 | 6 | update : Msg -> Model -> ( Model, Cmd Msg ) 7 | update msg model = 8 | case msg of 9 | ToggleDetails -> 10 | ( { model | showDetails = not model.showDetails } 11 | , Cmd.none 12 | ) 13 | -------------------------------------------------------------------------------- /spotify/test/Test.elm: -------------------------------------------------------------------------------- 1 | module Test exposing (main) 2 | 3 | {-| The main entry point for the tests. 4 | 5 | @docs main 6 | -} 7 | 8 | import ElmTest exposing (..) 9 | 10 | 11 | tests : Test 12 | tests = 13 | suite "All" [] 14 | 15 | 16 | {-| Run the test suite under node. 17 | -} 18 | main : Program Never 19 | main = 20 | runSuite tests 21 | -------------------------------------------------------------------------------- /spotify/src/Types.elm: -------------------------------------------------------------------------------- 1 | module Types exposing (..) 2 | 3 | import Album.Types as Album 4 | import Array exposing (Array) 5 | import Http exposing (Error) 6 | 7 | 8 | type alias Model = 9 | { query : String 10 | , results : Maybe (Result Error (Array Album.Model)) 11 | } 12 | 13 | 14 | type Msg 15 | = QueryChange String 16 | | Query 17 | | SpotifyResponse (Result Error (Array Album.Model)) 18 | | AlbumMsg Int Album.Msg 19 | -------------------------------------------------------------------------------- /trees/README.md: -------------------------------------------------------------------------------- 1 | # Trees 2 | 3 | ## Building & Developing 4 | 5 | ``` sh 6 | $ elm package install --yes 7 | $ elm reactor 8 | 9 | Then open http://localhost:8000/src/Main.elm. 10 | ``` 11 | 12 | ## Challenges 13 | 14 | See `src/Tree.elm`. The comments in that file will guide you through a 15 | series of challenges. 16 | 17 | As you solve each, you can view your solution in the reactor. 18 | 19 | If you get stuck, answers are in `src/TreeSolutions.elm`. 20 | -------------------------------------------------------------------------------- /spotify/src/Album/Rest.elm: -------------------------------------------------------------------------------- 1 | module Album.Rest exposing (..) 2 | 3 | import Album.Types exposing (..) 4 | import Json.Decode exposing (..) 5 | import Json.Decode.Pipeline exposing (..) 6 | 7 | 8 | decodeAlbum : Decoder Model 9 | decodeAlbum = 10 | decode Model 11 | |> required "id" string 12 | |> required "name" string 13 | |> required "images" (list decodeImage) 14 | |> hardcoded False 15 | 16 | 17 | decodeImage : Decoder Image 18 | decodeImage = 19 | "url" := string 20 | -------------------------------------------------------------------------------- /spotify/.dir-locals.el: -------------------------------------------------------------------------------- 1 | ;;; Directory Local Variables 2 | ;;; For more information see (info "(emacs) Directory Variables") 3 | 4 | ((nil (eval local-set-key 5 | (kbd "M-e") 6 | '(lambda () 7 | (interactive) 8 | (require 'magit) 9 | (let ((compile-command (format "cd %sspotify ; elm build" 10 | (magit-toplevel)))) 11 | (save-buffer) 12 | (compile compile-command)))))) 13 | -------------------------------------------------------------------------------- /trees/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "4.0.1 <= v < 5.0.0", 12 | "elm-lang/html": "1.0.0 <= v < 2.0.0", 13 | "evancz/elm-graphics": "1.0.0 <= v < 2.0.0" 14 | }, 15 | "elm-version": "0.17.0 <= v < 0.18.0" 16 | } -------------------------------------------------------------------------------- /spotify/src/Events.elm: -------------------------------------------------------------------------------- 1 | module Events exposing (onEnter) 2 | 3 | {-| Extensions to the Html.Events library. 4 | 5 | @docs onEnter 6 | -} 7 | 8 | import Html exposing (Attribute) 9 | import Html.Events exposing (..) 10 | import Json.Decode as Decode 11 | 12 | 13 | onEnter : msg -> Attribute msg 14 | onEnter msg = 15 | on "keydown" 16 | (Decode.customDecoder keyCode is13 17 | |> Decode.map (always msg) 18 | ) 19 | 20 | 21 | is13 : Int -> Result String () 22 | is13 code = 23 | if code == 13 then 24 | Ok () 25 | else 26 | Err "not the right key code" 27 | -------------------------------------------------------------------------------- /spotify/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0", 12 | "elm-community/elm-test": "1.1.0 <= v < 2.0.0", 13 | "elm-lang/core": "4.0.1 <= v < 5.0.0", 14 | "elm-lang/html": "1.0.0 <= v < 2.0.0", 15 | "evancz/elm-http": "3.0.1 <= v < 4.0.0", 16 | "krisajenkins/elm-dialog": "3.0.0 <= v < 4.0.0" 17 | }, 18 | "elm-version": "0.17.0 <= v < 0.18.0" 19 | } -------------------------------------------------------------------------------- /spotify/src/View/Bootstrap.elm: -------------------------------------------------------------------------------- 1 | module View.Bootstrap exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | 6 | 7 | container : List (Html a) -> Html a 8 | container = 9 | div [ class "container" ] 10 | 11 | 12 | row : List (Html a) -> Html a 13 | row = 14 | div [ class "row" ] 15 | 16 | 17 | bootstrap : Html a 18 | bootstrap = 19 | node "link" 20 | [ href "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 21 | , rel "stylesheet" 22 | ] 23 | [] 24 | 25 | 26 | panel : List (Html msg) -> List (Html msg) -> Html msg 27 | panel head body = 28 | div [ class "panel panel-info" ] 29 | [ div [ class "panel-heading" ] head 30 | , div [ class "panel-body" ] body 31 | ] 32 | -------------------------------------------------------------------------------- /spotify/src/Rest.elm: -------------------------------------------------------------------------------- 1 | module Rest exposing (..) 2 | 3 | import Album.Rest as Album 4 | import Album.Types as Album 5 | import Array exposing (Array) 6 | import Http exposing (Error) 7 | import Json.Decode exposing (..) 8 | import Task 9 | import Types exposing (..) 10 | 11 | 12 | search : String -> Cmd Msg 13 | search query = 14 | Http.get decodeAlbums (searchUrl query) 15 | |> Task.perform Err Ok 16 | |> Cmd.map SpotifyResponse 17 | 18 | 19 | searchUrl : String -> String 20 | searchUrl query = 21 | Http.url "https://api.spotify.com/v1/search" 22 | [ ( "q", query ) 23 | , ( "type", "album" ) 24 | , ( "market", "GB" ) 25 | , ( "limit", "50" ) 26 | ] 27 | 28 | 29 | decodeAlbums : Decoder (Array Album.Model) 30 | decodeAlbums = 31 | at [ "albums", "items" ] (array Album.decodeAlbum) 32 | -------------------------------------------------------------------------------- /spotify/src/Album/View.elm: -------------------------------------------------------------------------------- 1 | module Album.View exposing (..) 2 | 3 | import Album.Types exposing (..) 4 | import Dialog 5 | import Html exposing (..) 6 | import Html.Attributes exposing (..) 7 | import Html.Events exposing (..) 8 | import View.Bootstrap exposing (..) 9 | 10 | 11 | root : Model -> Html Msg 12 | root model = 13 | div [ onClick ToggleDetails ] 14 | [ panel [ text model.name ] 15 | [ case List.head model.images of 16 | Nothing -> 17 | span [] [ text "" ] 18 | 19 | Just image -> 20 | img [ class "img-responsive", src image ] 21 | [] 22 | ] 23 | ] 24 | 25 | 26 | showDialog : Model -> Maybe (Dialog.Config Msg) 27 | showDialog model = 28 | if model.showDetails then 29 | Just 30 | { closeMessage = Just ToggleDetails 31 | , header = Just (h2 [] [ text model.name ]) 32 | , body = Just (div [] [ code [] [ text (toString model) ] ]) 33 | , footer = Nothing 34 | } 35 | else 36 | Nothing 37 | -------------------------------------------------------------------------------- /spotify/src/State.elm: -------------------------------------------------------------------------------- 1 | module State exposing (..) 2 | 3 | import Album.State as Album 4 | import Array 5 | import Rest 6 | import Types exposing (..) 7 | 8 | 9 | init : ( Model, Cmd Msg ) 10 | init = 11 | ( { query = "" 12 | , results = Nothing 13 | } 14 | , Rest.search "frank zappa" 15 | ) 16 | 17 | 18 | update : Msg -> Model -> ( Model, Cmd Msg ) 19 | update action model = 20 | case action of 21 | QueryChange newQuery -> 22 | ( { model | query = newQuery } 23 | , Cmd.none 24 | ) 25 | 26 | Query -> 27 | ( model 28 | , Rest.search model.query 29 | ) 30 | 31 | SpotifyResponse response -> 32 | ( { model | results = Just response } 33 | , Cmd.none 34 | ) 35 | 36 | AlbumMsg index submsg -> 37 | case model.results of 38 | Just (Ok albums) -> 39 | case Array.get index albums of 40 | Nothing -> 41 | ( model, Cmd.none ) 42 | 43 | Just album -> 44 | let 45 | ( submodel, subcmd ) = 46 | Album.update submsg album 47 | in 48 | ( { model | results = Just (Ok (Array.set index submodel albums)) } 49 | , Cmd.map (AlbumMsg index) subcmd 50 | ) 51 | 52 | _ -> 53 | ( model, Cmd.none ) 54 | -------------------------------------------------------------------------------- /trees/src/Tree.elm: -------------------------------------------------------------------------------- 1 | module Tree exposing (..) 2 | 3 | 4 | type Tree a 5 | = Empty 6 | | Node a (Tree a) (Tree a) 7 | 8 | 9 | empty : Tree a 10 | empty = 11 | Empty 12 | 13 | 14 | 15 | -- singleton : a -> Tree a 16 | -- insert : comparable -> Tree comparable -> Tree comparable 17 | -- fromList : List comparable -> Tree comparable 18 | -- depth : Tree a -> Int 19 | -- map : (a -> b) -> Tree a -> Tree b 20 | {----------------------------------------------------------------- 21 | 22 | Exercises: 23 | 24 | (1) Sum all of the elements of a tree. 25 | 26 | sum : Tree number -> number 27 | 28 | (2) Flatten a tree into a list. 29 | 30 | flatten : Tree a -> List a 31 | 32 | (3) Check to see if an element is in a given tree. 33 | 34 | contains : a -> Tree a -> Bool 35 | 36 | (4) Write a general fold function that acts on trees. The fold 37 | function does not need to guarantee a particular order of 38 | traversal. 39 | 40 | fold : (a -> b -> b) -> b -> Tree a -> b 41 | 42 | (5) Use "fold" to do exercises 1-3 in one line each. The best 43 | readable versions I have come up have the following length 44 | in characters including spaces and function name: 45 | sum: 16 46 | flatten: 21 47 | contains: 45 48 | See if you can match or beat me! Don't forget about currying 49 | and partial application! 50 | 51 | (6) Can "fold" be used to implement "map" or "depth"? 52 | 53 | (7) Try experimenting with different ways to traverse a 54 | tree: pre-order, in-order, post-order, depth-first, etc. 55 | More info at: http://en.wikipedia.org/wiki/Tree_traversal 56 | 57 | -----------------------------------------------------------------} 58 | -------------------------------------------------------------------------------- /spotify/README.md: -------------------------------------------------------------------------------- 1 | # A Simple Spotify Client for Elm 2 | 3 | ## Building & Developing 4 | 5 | ``` sh 6 | $ elm package install --yes 7 | $ elm reactor 8 | 9 | Then open http://localhost:8000/src/Main.elm. 10 | ``` 11 | 12 | ## Challenges 13 | 14 | ### Learn About Rendering (Easy) 15 | 16 | The UI is a bit dull. Liven it up. 17 | 18 | Play around with `View.elm`. See how it creates HTML from the 19 | data-model, and how it handles attributes and children. We've pulling 20 | in Bootstrap 3, so you have those components available if you want 21 | them. 22 | 23 | ### Clean Up The Compiler Warnings (Medium) 24 | 25 | If you compile the app with `elm make src/Main.elm --warn`, you'll see 26 | we've got some work to do. Going through that list will teach you a 27 | lot about how Elm's types work, and hence how the app is structured. 28 | 29 | ### Learn About JSON-Handling (Harder) 30 | 31 | We need album art. Pull the album art links from Spotify's JSON 32 | response. 33 | 34 | Start by looking at the payload in the console, then change 35 | `Types.elm` and `Rest.elm` to extract it into the data-model. Lastly, 36 | change `View` to render it. 37 | 38 | For bonus points, put an HTML5 audio player in so the user can click 39 | for a preview of each track. 40 | 41 | ### Support More Query Types (Hard) 42 | 43 | The app is hard-coded to search for albums. If you look in `Rest.elm` 44 | you'll see why. Expand this so that the user has control over their 45 | search type. Start looking in `Types.elm` and expanding `model.query` 46 | to be a richer type. Then follow the compiler errors through until the 47 | rest of the app agrees with your new model. 48 | 49 | You'll need to read (the Spotify API docs)[https://developer.spotify.com/web-api/console/get-search-item/#complete] to know what's available. 50 | -------------------------------------------------------------------------------- /spotify/src/View.elm: -------------------------------------------------------------------------------- 1 | module View exposing (root) 2 | 3 | import Album.Types as Album 4 | import Album.View as Album 5 | import Array exposing (Array) 6 | import Dialog 7 | import Events exposing (onEnter) 8 | import Html exposing (..) 9 | import Html.App as Html 10 | import Html.Attributes exposing (..) 11 | import Html.Events exposing (..) 12 | import Types exposing (..) 13 | import View.Bootstrap exposing (..) 14 | 15 | 16 | root : Model -> Html Msg 17 | root model = 18 | div [] 19 | [ bootstrap 20 | , container 21 | [ inputForm model 22 | , case model.results of 23 | Nothing -> 24 | p [] [ text "Type a query." ] 25 | 26 | Just (Err err) -> 27 | div [ class "alert alert-danger" ] [ text (toString err) ] 28 | 29 | Just (Ok albums) -> 30 | albumsList albums 31 | ] 32 | , case model.results of 33 | Just (Ok albums) -> 34 | Dialog.view (showDialog albums) 35 | 36 | _ -> 37 | span [] [] 38 | ] 39 | 40 | 41 | inputForm : Model -> Html Msg 42 | inputForm model = 43 | input 44 | [ type' "text" 45 | , placeholder "Search for an album..." 46 | , value model.query 47 | , onInput QueryChange 48 | , onEnter Query 49 | ] 50 | [] 51 | 52 | 53 | albumsList : Array Album.Model -> Html Msg 54 | albumsList albums = 55 | let 56 | toTile index album = 57 | div [ class "col-xs-2 col-md-3" ] 58 | [ Album.root album 59 | |> Html.map (AlbumMsg index) 60 | ] 61 | in 62 | row 63 | (albums 64 | |> Array.indexedMap toTile 65 | |> Array.toList 66 | ) 67 | 68 | 69 | showDialog : Array Album.Model -> Maybe (Dialog.Config Msg) 70 | showDialog albums = 71 | let 72 | albumDialog : Int -> Album.Model -> Maybe (Dialog.Config Msg) 73 | albumDialog index album = 74 | Album.showDialog album 75 | |> Dialog.mapMaybe (AlbumMsg index) 76 | in 77 | albums 78 | |> Array.indexedMap albumDialog 79 | |> Array.toList 80 | |> Maybe.oneOf 81 | -------------------------------------------------------------------------------- /trees/src/TreeView.elm: -------------------------------------------------------------------------------- 1 | module TreeView exposing (..) 2 | 3 | import Collage exposing (..) 4 | import Color exposing (..) 5 | import Element exposing (..) 6 | import Html 7 | import Text 8 | import Tree exposing (..) 9 | 10 | 11 | draw : Float -> Tree a -> Html.Html msg 12 | draw viewWidth tree = 13 | let 14 | ( treeForm, depth ) = 15 | treeToForm tree 16 | 17 | treeWidth = 18 | 30 * 2 ^ depth * 2 19 | 20 | scaleFactor = 21 | if treeWidth > viewWidth - 20 then 22 | viewWidth / treeWidth 23 | else 24 | 1 25 | 26 | treeHeight = 27 | 30 * depth * scaleFactor 28 | in 29 | toHtml 30 | <| collage (round viewWidth) 31 | (round treeHeight + 40) 32 | [ treeForm 33 | |> scale scaleFactor 34 | |> move ( 0, treeHeight / 2 ) 35 | ] 36 | 37 | 38 | treeToForm : Tree a -> ( Form, Float ) 39 | treeToForm tree = 40 | case tree of 41 | Empty -> 42 | ( filled orange (oval 10 10) 43 | , 0 44 | ) 45 | 46 | Node value left right -> 47 | let 48 | ( leftForm, leftDepth ) = 49 | treeToForm left 50 | 51 | ( rightForm, rightDepth ) = 52 | treeToForm right 53 | 54 | leftPoint = 55 | ( -30 * 2 ^ leftDepth, -30 ) 56 | 57 | rightPoint = 58 | ( 30 * 2 ^ rightDepth, -30 ) 59 | 60 | treeForm = 61 | group 62 | [ segment ( 0, 0 ) leftPoint 63 | |> traced (solid black) 64 | , segment ( 0, 0 ) rightPoint 65 | |> traced (solid black) 66 | , oval 20 20 67 | |> filled blue 68 | , toString value 69 | |> Text.fromString 70 | |> Text.color white 71 | |> text 72 | , move leftPoint leftForm 73 | , move rightPoint rightForm 74 | ] 75 | in 76 | ( treeForm, 1 + max leftDepth rightDepth ) 77 | -------------------------------------------------------------------------------- /trees/src/TreeSolutions.elm: -------------------------------------------------------------------------------- 1 | module TreeSolutions exposing (..) 2 | 3 | {--HEY DON'T CHEAT!!!! 4 | 5 | This is for checking your answers. 6 | 7 | --} 8 | 9 | 10 | type Tree a 11 | = Empty 12 | | Node a (Tree a) (Tree a) 13 | 14 | 15 | empty : Tree a 16 | empty = 17 | Empty 18 | 19 | 20 | singleton : a -> Tree a 21 | singleton value = 22 | Node value Empty Empty 23 | 24 | 25 | insert : comparable -> Tree comparable -> Tree comparable 26 | insert newValue tree = 27 | case tree of 28 | Empty -> 29 | singleton newValue 30 | 31 | Node value left right -> 32 | if value == newValue then 33 | tree 34 | else if value < newValue then 35 | Node value (insert newValue left) right 36 | else 37 | Node value left (insert newValue right) 38 | 39 | 40 | fromList : List comparable -> Tree comparable 41 | fromList list = 42 | List.foldl insert empty list 43 | 44 | 45 | depth : Tree a -> Int 46 | depth tree = 47 | case tree of 48 | Empty -> 49 | 0 50 | 51 | Node _ left right -> 52 | 1 + max (depth left) (depth right) 53 | 54 | 55 | map : (a -> b) -> Tree a -> Tree b 56 | map func tree = 57 | case tree of 58 | Empty -> 59 | Empty 60 | 61 | Node value left right -> 62 | Node (func value) (map func left) (map func right) 63 | 64 | 65 | sum : Tree number -> number 66 | sum tree = 67 | case tree of 68 | Empty -> 69 | 0 70 | 71 | Node value left right -> 72 | value + sum left + sum right 73 | 74 | 75 | flatten : Tree a -> List a 76 | flatten tree = 77 | case tree of 78 | Empty -> 79 | [] 80 | 81 | Node value left right -> 82 | flatten left ++ [ value ] ++ flatten right 83 | 84 | 85 | contains : a -> Tree a -> Bool 86 | contains val tree = 87 | case tree of 88 | Empty -> 89 | False 90 | 91 | Node value left right -> 92 | if value == val then 93 | True 94 | else if val < value then 95 | contains val left 96 | else 97 | contains val right 98 | 99 | 100 | fold : (a -> b -> b) -> b -> Tree a -> b 101 | fold func accumulator tree = 102 | case tree of 103 | Empty -> 104 | accumulator 105 | 106 | Node value left right -> 107 | fold func (func value (fold func accumulator left)) right 108 | 109 | 110 | 111 | {--CODE GOLF 112 | 113 | sum = fold (+) 0 114 | flatten = fold (::) [] 115 | contains x = fold (\y b -> b || x == y) False 116 | contains x = fold (||) False << map ((==) x) 117 | 118 | --} 119 | {--(6) Can "fold" be used to implement "map" or "depth"? 120 | 121 | No. 122 | 123 | --} 124 | --------------------------------------------------------------------------------