Implement chapter 4 - part1
- pull photo urls from the server - provide styles.css and list ourselves
This commit is contained in:
parent
fe10054e44
commit
e47f1d0449
6 changed files with 1475 additions and 204 deletions
3
elm.json
3
elm.json
|
|
@ -9,9 +9,12 @@
|
||||||
"elm/browser": "1.0.2",
|
"elm/browser": "1.0.2",
|
||||||
"elm/core": "1.0.5",
|
"elm/core": "1.0.5",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
|
"elm/http": "2.0.0",
|
||||||
"elm/random": "1.0.0"
|
"elm/random": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
|
"elm/bytes": "1.0.8",
|
||||||
|
"elm/file": "1.0.5",
|
||||||
"elm/json": "1.1.3",
|
"elm/json": "1.1.3",
|
||||||
"elm/time": "1.0.0",
|
"elm/time": "1.0.0",
|
||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="http://elm-in-action.com/styles.css" />
|
<link rel="stylesheet" href="styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
1
list
Normal file
1
list
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
1.jpeg,2.jpeg,3.jpeg,4.jpeg
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
module PhotoGroove exposing (main)
|
module PhotoGroove exposing (main)
|
||||||
|
|
||||||
import Array exposing (Array)
|
|
||||||
import Browser
|
import Browser
|
||||||
import Html exposing (Html, button, div, h1, h3, img, input, label, text)
|
import Html exposing (Html, button, div, h1, h3, img, input, label, text)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events exposing (onClick)
|
import Html.Events exposing (onClick)
|
||||||
|
import Http
|
||||||
import Random
|
import Random
|
||||||
|
|
||||||
|
|
||||||
|
type Status
|
||||||
|
= Loading
|
||||||
|
| Loaded (List Photo) String
|
||||||
|
| Errored String
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
{ photos : List Photo
|
{ status : Status
|
||||||
, selectedUrl : String
|
|
||||||
, chosenSize : ThumbnailSize
|
, chosenSize : ThumbnailSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,51 +32,47 @@ type ThumbnailSize
|
||||||
|
|
||||||
initialModel : Model
|
initialModel : Model
|
||||||
initialModel =
|
initialModel =
|
||||||
{ photos =
|
{ status = Loading
|
||||||
[ { url = "1.jpeg" }
|
|
||||||
, { url = "2.jpeg" }
|
|
||||||
, { url = "3.jpeg" }
|
|
||||||
]
|
|
||||||
, selectedUrl = "1.jpeg"
|
|
||||||
, chosenSize = Large
|
, chosenSize = Large
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getPhotoUrl : Int -> String
|
initialCommand : Cmd Message
|
||||||
getPhotoUrl id =
|
initialCommand =
|
||||||
case Array.get id photoArray of
|
Http.get
|
||||||
Just photo ->
|
{ url = "list"
|
||||||
photo.url
|
, expect = Http.expectString GotPhotos
|
||||||
|
}
|
||||||
Nothing ->
|
|
||||||
""
|
|
||||||
|
|
||||||
|
|
||||||
photoArray : Array Photo
|
|
||||||
photoArray =
|
|
||||||
Array.fromList initialModel.photos
|
|
||||||
|
|
||||||
|
|
||||||
randomPhotoPicker : Random.Generator Int
|
|
||||||
randomPhotoPicker =
|
|
||||||
Random.int 0 (Array.length photoArray - 1)
|
|
||||||
|
|
||||||
|
|
||||||
view : Model -> Html Message
|
view : Model -> Html Message
|
||||||
view model =
|
view model =
|
||||||
div [ class "content" ]
|
div [ class "content" ] <|
|
||||||
[ h1 [] [ text "Photo Groove" ]
|
case model.status of
|
||||||
, button [ onClick ClickedSurpriseMe ] [ text "Surprise me!" ]
|
Loading ->
|
||||||
, h3 [] [ text "Thumbnail Size:" ]
|
[]
|
||||||
, div [ id "choose-size" ]
|
|
||||||
(List.map viewSizeChooser [ Small, Medium, Large ])
|
Loaded photos selected ->
|
||||||
, div
|
viewLoaded photos selected model.chosenSize
|
||||||
[ id "thumbnails"
|
|
||||||
, class (sizeToClass model.chosenSize)
|
Errored error ->
|
||||||
]
|
[ text ("Error: " ++ error) ]
|
||||||
(List.map (viewThumbnail model.selectedUrl) model.photos)
|
|
||||||
, img [ class "large", src (urlPrefix ++ "large/" ++ model.selectedUrl) ] []
|
|
||||||
|
viewLoaded : List Photo -> String -> ThumbnailSize -> List (Html Message)
|
||||||
|
viewLoaded photos selected size =
|
||||||
|
[ h1 [] [ text "Photo Groove" ]
|
||||||
|
, button [ onClick ClickedSurpriseMe ] [ text "Surprise me!" ]
|
||||||
|
, h3 [] [ text "Thumbnail Size:" ]
|
||||||
|
, div [ id "choose-size" ]
|
||||||
|
(List.map viewSizeChooser [ Small, Medium, Large ])
|
||||||
|
, div
|
||||||
|
[ id "thumbnails"
|
||||||
|
, class (sizeToClass size)
|
||||||
]
|
]
|
||||||
|
(List.map (viewThumbnail selected) photos)
|
||||||
|
, img [ class "large", src (urlPrefix ++ "large/" ++ selected) ] []
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
viewThumbnail : String -> Photo -> Html Message
|
viewThumbnail : String -> Photo -> Html Message
|
||||||
|
|
@ -127,23 +128,84 @@ type Message
|
||||||
= ClickedThumbnail String
|
= ClickedThumbnail String
|
||||||
| ClickedSurpriseMe
|
| ClickedSurpriseMe
|
||||||
| ClickedSize ThumbnailSize
|
| ClickedSize ThumbnailSize
|
||||||
| GotSelectedIndex Int
|
| GotRandomPhoto Photo
|
||||||
|
| GotPhotos (Result Http.Error String)
|
||||||
|
|
||||||
|
|
||||||
update : Message -> Model -> ( Model, Cmd Message )
|
update : Message -> Model -> ( Model, Cmd Message )
|
||||||
update msg model =
|
update msg model =
|
||||||
case msg of
|
case msg of
|
||||||
ClickedThumbnail thumb ->
|
ClickedThumbnail thumb ->
|
||||||
( { model | selectedUrl = thumb }, Cmd.none )
|
( { model | status = selectUrl thumb model.status }, Cmd.none )
|
||||||
|
|
||||||
ClickedSurpriseMe ->
|
ClickedSurpriseMe ->
|
||||||
( model, Random.generate GotSelectedIndex randomPhotoPicker )
|
case model.status of
|
||||||
|
Loaded (photo :: morePhotos) _ ->
|
||||||
|
Random.uniform photo morePhotos
|
||||||
|
|> Random.generate GotRandomPhoto
|
||||||
|
|> Tuple.pair model
|
||||||
|
|
||||||
|
Loaded [] _ ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Loading ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Errored _ ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
ClickedSize size ->
|
ClickedSize size ->
|
||||||
( { model | chosenSize = size }, Cmd.none )
|
( { model | chosenSize = size }, Cmd.none )
|
||||||
|
|
||||||
GotSelectedIndex idx ->
|
GotRandomPhoto photo ->
|
||||||
( { model | selectedUrl = getPhotoUrl idx }, Cmd.none )
|
( { model | status = selectUrl photo.url model.status }, Cmd.none )
|
||||||
|
|
||||||
|
GotPhotos (Ok str) ->
|
||||||
|
case String.split "," str of
|
||||||
|
(x :: _) as urls ->
|
||||||
|
let
|
||||||
|
photos =
|
||||||
|
List.map Photo urls
|
||||||
|
in
|
||||||
|
( { model | status = Loaded photos x }, Cmd.none )
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
( { model | status = Errored ("No photos: " ++ str) }, Cmd.none )
|
||||||
|
|
||||||
|
GotPhotos (Err httpError) ->
|
||||||
|
( { model | status = Errored <| ("Failed to load photos: " ++ httpErrorToString httpError) }, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
selectUrl : String -> Status -> Status
|
||||||
|
selectUrl url status =
|
||||||
|
case status of
|
||||||
|
Loading ->
|
||||||
|
status
|
||||||
|
|
||||||
|
Loaded photos _ ->
|
||||||
|
Loaded photos url
|
||||||
|
|
||||||
|
Errored _ ->
|
||||||
|
status
|
||||||
|
|
||||||
|
|
||||||
|
httpErrorToString : Http.Error -> String
|
||||||
|
httpErrorToString err =
|
||||||
|
case err of
|
||||||
|
Http.BadUrl msg ->
|
||||||
|
"bad URL: " ++ msg
|
||||||
|
|
||||||
|
Http.Timeout ->
|
||||||
|
"timeout"
|
||||||
|
|
||||||
|
Http.NetworkError ->
|
||||||
|
"network error"
|
||||||
|
|
||||||
|
Http.BadStatus status ->
|
||||||
|
"bad status: " ++ String.fromInt status
|
||||||
|
|
||||||
|
Http.BadBody msg ->
|
||||||
|
"bad body: " ++ msg
|
||||||
|
|
||||||
|
|
||||||
urlPrefix : String
|
urlPrefix : String
|
||||||
|
|
@ -154,7 +216,7 @@ urlPrefix =
|
||||||
main : Program () Model Message
|
main : Program () Model Message
|
||||||
main =
|
main =
|
||||||
Browser.element
|
Browser.element
|
||||||
{ init = \_ -> ( initialModel, Cmd.none )
|
{ init = \_ -> ( initialModel, initialCommand )
|
||||||
, subscriptions = \_ -> Sub.none
|
, subscriptions = \_ -> Sub.none
|
||||||
, view = view
|
, view = view
|
||||||
, update = update
|
, update = update
|
||||||
|
|
|
||||||
184
styles.css
Normal file
184
styles.css
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
body { font-family: Verdana; background-color: rgb(44, 44, 44); color: rgb(250, 250, 250); }
|
||||||
|
img, canvas { border: 1px solid white; margin: 5px; }
|
||||||
|
.large { width: 500px; float: right; }
|
||||||
|
.selected { margin: 0; border: 6px solid #60b5cc; }
|
||||||
|
.content { margin: 40px auto; width: 960px; }
|
||||||
|
#thumbnails { width: 440px; float: left; clear: both; }
|
||||||
|
#thumbnails.small img { width: 50px; }
|
||||||
|
#thumbnails.med img { width: 100px; }
|
||||||
|
#thumbnails.large img { width: 200px; }
|
||||||
|
#choose-size { float: left; margin-left: 20px; }
|
||||||
|
#choose-size > span { display: inline-block; margin: 0 10px; }
|
||||||
|
#activate-groove { float: right; margin-right: 20px; margin-top: 15px; }
|
||||||
|
|
||||||
|
h1 { color: #60b5cc; margin-bottom: 0; }
|
||||||
|
h3 { float: left; margin: 0 }
|
||||||
|
label { vertical-align: bottom; }
|
||||||
|
button {
|
||||||
|
float: right; background-color: #60b5cc; border: 0; color: rgb(44, 44, 44);
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
#thumbnails, img.large, #choose-size, h3 { margin-top: 20px; }
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
width: 318px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-slider {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-slider label {
|
||||||
|
width: 70px;
|
||||||
|
display: inline-block;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
range-slider {
|
||||||
|
width: 120px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-right: 15px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
range-slider .jsr {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
range-slider .jsr_rail-outer {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
range-slider .jsr_rail {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
range-slider .jsr_label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
range-slider .jsr_slider:focus::before {
|
||||||
|
background: rgb(96, 181, 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity {
|
||||||
|
position: absolute;
|
||||||
|
top: 40px;
|
||||||
|
right: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder { margin-left: 30px; }
|
||||||
|
.folder > label {
|
||||||
|
background-color: #555;
|
||||||
|
margin: 6px 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder > label:hover {
|
||||||
|
background-color: #60b5cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder.expanded label::before {
|
||||||
|
content: "▸";
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder.collapsed label::before {
|
||||||
|
content: "▾";
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin-left: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo:hover {
|
||||||
|
background-color: #60b5cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folders, .selected-photo {
|
||||||
|
float: left;
|
||||||
|
min-height: 400px;
|
||||||
|
width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-photo h3 {
|
||||||
|
margin-top: 60px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #60b5cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-photo img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-photo {
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content::after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-photos {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-photo {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-photo:hover {
|
||||||
|
float: left;
|
||||||
|
border-color: #60b5cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-photo {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
list-style: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li, nav h1 {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 5px 15px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .active {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
color: #bbb;
|
||||||
|
margin: 20px;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue