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/core": "1.0.5",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/http": "2.0.0",
|
||||
"elm/random": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/bytes": "1.0.8",
|
||||
"elm/file": "1.0.5",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="http://elm-in-action.com/styles.css" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<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)
|
||||
|
||||
import Array exposing (Array)
|
||||
import Browser
|
||||
import Html exposing (Html, button, div, h1, h3, img, input, label, text)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick)
|
||||
import Http
|
||||
import Random
|
||||
|
||||
|
||||
type Status
|
||||
= Loading
|
||||
| Loaded (List Photo) String
|
||||
| Errored String
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ photos : List Photo
|
||||
, selectedUrl : String
|
||||
{ status : Status
|
||||
, chosenSize : ThumbnailSize
|
||||
}
|
||||
|
||||
|
|
@ -27,51 +32,47 @@ type ThumbnailSize
|
|||
|
||||
initialModel : Model
|
||||
initialModel =
|
||||
{ photos =
|
||||
[ { url = "1.jpeg" }
|
||||
, { url = "2.jpeg" }
|
||||
, { url = "3.jpeg" }
|
||||
]
|
||||
, selectedUrl = "1.jpeg"
|
||||
{ status = Loading
|
||||
, chosenSize = Large
|
||||
}
|
||||
|
||||
|
||||
getPhotoUrl : Int -> String
|
||||
getPhotoUrl id =
|
||||
case Array.get id photoArray of
|
||||
Just photo ->
|
||||
photo.url
|
||||
|
||||
Nothing ->
|
||||
""
|
||||
|
||||
|
||||
photoArray : Array Photo
|
||||
photoArray =
|
||||
Array.fromList initialModel.photos
|
||||
|
||||
|
||||
randomPhotoPicker : Random.Generator Int
|
||||
randomPhotoPicker =
|
||||
Random.int 0 (Array.length photoArray - 1)
|
||||
initialCommand : Cmd Message
|
||||
initialCommand =
|
||||
Http.get
|
||||
{ url = "list"
|
||||
, expect = Http.expectString GotPhotos
|
||||
}
|
||||
|
||||
|
||||
view : Model -> Html Message
|
||||
view model =
|
||||
div [ class "content" ]
|
||||
[ 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 model.chosenSize)
|
||||
]
|
||||
(List.map (viewThumbnail model.selectedUrl) model.photos)
|
||||
, img [ class "large", src (urlPrefix ++ "large/" ++ model.selectedUrl) ] []
|
||||
div [ class "content" ] <|
|
||||
case model.status of
|
||||
Loading ->
|
||||
[]
|
||||
|
||||
Loaded photos selected ->
|
||||
viewLoaded photos selected model.chosenSize
|
||||
|
||||
Errored error ->
|
||||
[ text ("Error: " ++ error) ]
|
||||
|
||||
|
||||
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
|
||||
|
|
@ -127,23 +128,84 @@ type Message
|
|||
= ClickedThumbnail String
|
||||
| ClickedSurpriseMe
|
||||
| ClickedSize ThumbnailSize
|
||||
| GotSelectedIndex Int
|
||||
| GotRandomPhoto Photo
|
||||
| GotPhotos (Result Http.Error String)
|
||||
|
||||
|
||||
update : Message -> Model -> ( Model, Cmd Message )
|
||||
update msg model =
|
||||
case msg of
|
||||
ClickedThumbnail thumb ->
|
||||
( { model | selectedUrl = thumb }, Cmd.none )
|
||||
( { model | status = selectUrl thumb model.status }, Cmd.none )
|
||||
|
||||
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 ->
|
||||
( { model | chosenSize = size }, Cmd.none )
|
||||
|
||||
GotSelectedIndex idx ->
|
||||
( { model | selectedUrl = getPhotoUrl idx }, Cmd.none )
|
||||
GotRandomPhoto photo ->
|
||||
( { 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
|
||||
|
|
@ -154,7 +216,7 @@ urlPrefix =
|
|||
main : Program () Model Message
|
||||
main =
|
||||
Browser.element
|
||||
{ init = \_ -> ( initialModel, Cmd.none )
|
||||
{ init = \_ -> ( initialModel, initialCommand )
|
||||
, subscriptions = \_ -> Sub.none
|
||||
, view = view
|
||||
, 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