Implement chapter 4 - part1

- pull photo urls from the server
- provide styles.css and list ourselves
This commit is contained in:
Alexander Kobjolke 2023-12-11 21:02:12 +01:00
parent fe10054e44
commit e47f1d0449
6 changed files with 1475 additions and 204 deletions

1337
app.js

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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
View file

@ -0,0 +1 @@
1.jpeg,2.jpeg,3.jpeg,4.jpeg

View file

@ -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
View 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;
}