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

1323
app.js

File diff suppressed because it is too large Load diff

View file

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

View file

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

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

View file

@ -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,39 +32,35 @@ 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" ]
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:" ]
@ -67,10 +68,10 @@ view model =
(List.map viewSizeChooser [ Small, Medium, Large ])
, div
[ id "thumbnails"
, class (sizeToClass model.chosenSize)
, class (sizeToClass size)
]
(List.map (viewThumbnail model.selectedUrl) model.photos)
, img [ class "large", src (urlPrefix ++ "large/" ++ model.selectedUrl) ] []
(List.map (viewThumbnail selected) photos)
, img [ class "large", src (urlPrefix ++ "large/" ++ selected) ] []
]
@ -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
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;
}