Copy over and modify starter from elm-pages.com site.
|
@ -0,0 +1,4 @@
|
|||
node_modules/
|
||||
elm-stuff/
|
||||
dist/
|
||||
.cache/
|
|
@ -0,0 +1,11 @@
|
|||
# elm-pages-starter
|
||||
|
||||
This is an example repo to get you up and running with `elm-pages`.
|
||||
|
||||
The entrypoint file is `index.js`. That file imports `src/Main.elm`. The `content` folder is turned into your static pages. The rest is mostly determined by logic in the Elm code! Learn more with the resources below.
|
||||
|
||||
## Learn more about `elm-pages`
|
||||
|
||||
- Documentation site: https://elm-pages.com
|
||||
- [Elm Package docs](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/)
|
||||
- [`elm-pages` blog](https://elm-pages.com/blog)
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
{
|
||||
"type": "blog",
|
||||
"author": "Dillon Kearns",
|
||||
"title": "A Draft Blog Post",
|
||||
"description": "I'm not quite ready to share this post with the world",
|
||||
"image": "/images/article-covers/mountains.jpg",
|
||||
"draft": true,
|
||||
"published": "2019-09-21",
|
||||
}
|
||||
---
|
||||
|
||||
This blog post is a draft! Check out `Index.elm` to see how it's being skipped in the `/blog/` listing page.
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
{
|
||||
"type": "blog",
|
||||
"author": "Dillon Kearns",
|
||||
"title": "Hello `elm-pages`! 🚀",
|
||||
"description": "Here's an intro for my blog post to get you interested in reading more...",
|
||||
"image": "/images/article-covers/hello.jpg",
|
||||
"published": "2019-09-21",
|
||||
}
|
||||
---
|
||||
|
||||
Welcome to my blog! It was built with `elm-pages`!
|
||||
|
||||
|
||||
```elm
|
||||
plus : number -> number -> number
|
||||
plus m n =
|
||||
m + n
|
||||
```
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: elm-pages blog
|
||||
type: blog-index
|
||||
---
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: elm-pages-starter - a simple blog starter
|
||||
type: page
|
||||
---
|
||||
|
||||
This is an example repo to get you up and running with `elm-pages`.
|
||||
|
||||
The entrypoint file is `index.js`. That file imports `src/Main.elm`. The `content` folder is turned into your static pages. The rest is mostly determined by logic in the Elm code! Learn more with the resources below.
|
||||
|
||||
## Learn more about `elm-pages`
|
||||
|
||||
- Documentation site: https://elm-pages.com
|
||||
- [Elm Package docs](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/)
|
||||
- [`elm-pages` blog](https://elm-pages.com/blog)
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"type": "application",
|
||||
"source-directories": [
|
||||
"src",
|
||||
"gen"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"avh4/elm-color": "1.0.0",
|
||||
"dillonkearns/elm-pages": "1.0.0",
|
||||
"elm/browser": "1.0.1",
|
||||
"elm/core": "1.0.2",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/http": "2.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/parser": "1.1.0",
|
||||
"elm/svg": "1.0.1",
|
||||
"elm/url": "1.0.0",
|
||||
"elm-community/list-extra": "8.2.2",
|
||||
"elm-community/result-extra": "2.2.1",
|
||||
"elm-community/string-extra": "4.0.1",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"justinmimbs/date": "3.1.2",
|
||||
"lukewestby/elm-string-interpolate": "1.0.4",
|
||||
"mdgriffith/elm-markup": "3.0.1",
|
||||
"mdgriffith/elm-ui": "1.1.5",
|
||||
"noahzgordon/elm-color-extra": "1.0.2",
|
||||
"rtfeldman/elm-hex": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/bytes": "1.0.8",
|
||||
"elm/file": "1.0.5",
|
||||
"elm/regex": "1.0.0",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.2",
|
||||
"fredcy/elm-parseint": "2.0.1"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
"direct": {
|
||||
"elm-explorations/test": "1.2.2"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/random": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
port module Pages exposing (PathKey, allPages, allImages, application, images, isValidRoute, pages)
|
||||
|
||||
import Color exposing (Color)
|
||||
import Head
|
||||
import Html exposing (Html)
|
||||
import Json.Decode
|
||||
import Json.Encode
|
||||
import Mark
|
||||
import Pages.Platform
|
||||
import Pages.ContentCache exposing (Page)
|
||||
import Pages.Manifest exposing (DisplayMode, Orientation)
|
||||
import Pages.Manifest.Category as Category exposing (Category)
|
||||
import Url.Parser as Url exposing ((</>), s)
|
||||
import Pages.Document as Document
|
||||
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Pages.Directory as Directory exposing (Directory)
|
||||
|
||||
|
||||
type PathKey
|
||||
= PathKey
|
||||
|
||||
|
||||
buildImage : List String -> ImagePath PathKey
|
||||
buildImage path =
|
||||
ImagePath.build PathKey ("images" :: path)
|
||||
|
||||
|
||||
|
||||
buildPage : List String -> PagePath PathKey
|
||||
buildPage path =
|
||||
PagePath.build PathKey path
|
||||
|
||||
|
||||
directoryWithIndex : List String -> Directory PathKey Directory.WithIndex
|
||||
directoryWithIndex path =
|
||||
Directory.withIndex PathKey allPages path
|
||||
|
||||
|
||||
directoryWithoutIndex : List String -> Directory PathKey Directory.WithoutIndex
|
||||
directoryWithoutIndex path =
|
||||
Directory.withoutIndex PathKey allPages path
|
||||
|
||||
|
||||
port toJsPort : Json.Encode.Value -> Cmd msg
|
||||
|
||||
|
||||
application :
|
||||
{ init : ( userModel, Cmd userMsg )
|
||||
, update : userMsg -> userModel -> ( userModel, Cmd userMsg )
|
||||
, subscriptions : userModel -> Sub userMsg
|
||||
, view : userModel -> List ( PagePath PathKey, metadata ) -> Page metadata view PathKey -> { title : String, body : Html userMsg }
|
||||
, head : metadata -> List (Head.Tag PathKey)
|
||||
, documents : List ( String, Document.DocumentHandler metadata view )
|
||||
, manifest : Pages.Manifest.Config PathKey
|
||||
, canonicalSiteUrl : String
|
||||
}
|
||||
-> Pages.Platform.Program userModel userMsg metadata view
|
||||
application config =
|
||||
Pages.Platform.application
|
||||
{ init = config.init
|
||||
, view = config.view
|
||||
, update = config.update
|
||||
, subscriptions = config.subscriptions
|
||||
, document = Document.fromList config.documents
|
||||
, content = content
|
||||
, toJsPort = toJsPort
|
||||
, head = config.head
|
||||
, manifest = config.manifest
|
||||
, canonicalSiteUrl = config.canonicalSiteUrl
|
||||
, pathKey = PathKey
|
||||
}
|
||||
|
||||
|
||||
|
||||
allPages : List (PagePath PathKey)
|
||||
allPages =
|
||||
[ (buildPage [ "blog", "draft" ])
|
||||
, (buildPage [ "blog", "hello" ])
|
||||
, (buildPage [ "blog" ])
|
||||
, (buildPage [ ])
|
||||
]
|
||||
|
||||
pages =
|
||||
{ blog =
|
||||
{ draft = (buildPage [ "blog", "draft" ])
|
||||
, hello = (buildPage [ "blog", "hello" ])
|
||||
, index = (buildPage [ "blog" ])
|
||||
, directory = directoryWithIndex ["blog"]
|
||||
}
|
||||
, index = (buildPage [ ])
|
||||
, directory = directoryWithIndex []
|
||||
}
|
||||
|
||||
images =
|
||||
{ articleCovers =
|
||||
{ hello = (buildImage [ "article-covers", "hello.jpg" ])
|
||||
, mountains = (buildImage [ "article-covers", "mountains.jpg" ])
|
||||
, directory = directoryWithoutIndex ["articleCovers"]
|
||||
}
|
||||
, author =
|
||||
{ dillon = (buildImage [ "author", "dillon.jpg" ])
|
||||
, directory = directoryWithoutIndex ["author"]
|
||||
}
|
||||
, elmLogo = (buildImage [ "elm-logo.svg" ])
|
||||
, github = (buildImage [ "github.svg" ])
|
||||
, iconPng = (buildImage [ "icon-png.png" ])
|
||||
, icon = (buildImage [ "icon.svg" ])
|
||||
, directory = directoryWithoutIndex []
|
||||
}
|
||||
|
||||
allImages : List (ImagePath PathKey)
|
||||
allImages =
|
||||
[(buildImage [ "article-covers", "hello.jpg" ])
|
||||
, (buildImage [ "article-covers", "mountains.jpg" ])
|
||||
, (buildImage [ "author", "dillon.jpg" ])
|
||||
, (buildImage [ "elm-logo.svg" ])
|
||||
, (buildImage [ "github.svg" ])
|
||||
, (buildImage [ "icon-png.png" ])
|
||||
, (buildImage [ "icon.svg" ])
|
||||
]
|
||||
|
||||
|
||||
isValidRoute : String -> Result String ()
|
||||
isValidRoute route =
|
||||
let
|
||||
validRoutes =
|
||||
List.map PagePath.toString allPages
|
||||
in
|
||||
if
|
||||
(route |> String.startsWith "http://")
|
||||
|| (route |> String.startsWith "https://")
|
||||
|| (route |> String.startsWith "#")
|
||||
|| (validRoutes |> List.member route)
|
||||
then
|
||||
Ok ()
|
||||
|
||||
else
|
||||
("Valid routes:\n"
|
||||
++ String.join "\n\n" validRoutes
|
||||
)
|
||||
|> Err
|
||||
|
||||
|
||||
content : List ( List String, { extension: String, frontMatter : String, body : Maybe String } )
|
||||
content =
|
||||
[
|
||||
( ["blog", "draft"]
|
||||
, { frontMatter = """{"type":"blog","author":"Dillon Kearns","title":"A Draft Blog Post","description":"I'm not quite ready to share this post with the world","image":"/images/article-covers/mountains.jpg","draft":true,"published":"2019-09-21"}
|
||||
""" , body = Nothing
|
||||
, extension = "md"
|
||||
} )
|
||||
,
|
||||
( ["blog", "hello"]
|
||||
, { frontMatter = """{"type":"blog","author":"Dillon Kearns","title":"Hello `elm-pages`! 🚀","description":"Here's an intro for my blog post to get you interested in reading more...","image":"/images/article-covers/hello.jpg","published":"2019-09-21"}
|
||||
""" , body = Nothing
|
||||
, extension = "md"
|
||||
} )
|
||||
,
|
||||
( ["blog"]
|
||||
, { frontMatter = """{"title":"elm-pages blog","type":"blog-index"}
|
||||
""" , body = Nothing
|
||||
, extension = "md"
|
||||
} )
|
||||
,
|
||||
( []
|
||||
, { frontMatter = """{"title":"elm-pages-starter - a simple blog starter","type":"page"}
|
||||
""" , body = Nothing
|
||||
, extension = "md"
|
||||
} )
|
||||
|
||||
]
|
|
@ -0,0 +1,459 @@
|
|||
module Pages.ContentCache exposing
|
||||
( ContentCache
|
||||
, Entry(..)
|
||||
, Page
|
||||
, Path
|
||||
, errorView
|
||||
, extractMetadata
|
||||
, init
|
||||
, lazyLoad
|
||||
, lookup
|
||||
, lookupMetadata
|
||||
, pagesWithErrors
|
||||
, pathForUrl
|
||||
, routesForCache
|
||||
, update
|
||||
)
|
||||
|
||||
import Dict exposing (Dict)
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
import Http
|
||||
import Json.Decode
|
||||
import Mark
|
||||
import Mark.Error
|
||||
import Pages.Document as Document exposing (Document)
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Result.Extra
|
||||
import Task exposing (Task)
|
||||
import Url exposing (Url)
|
||||
import Url.Builder
|
||||
|
||||
|
||||
type alias Content =
|
||||
List ( List String, { extension : String, frontMatter : String, body : Maybe String } )
|
||||
|
||||
|
||||
type alias ContentCache metadata view =
|
||||
Result Errors (Dict Path (Entry metadata view))
|
||||
|
||||
|
||||
type alias Errors =
|
||||
Dict Path String
|
||||
|
||||
|
||||
type alias ContentCacheInner metadata view =
|
||||
Dict Path (Entry metadata view)
|
||||
|
||||
|
||||
type Entry metadata view
|
||||
= NeedContent String metadata
|
||||
| Unparsed String metadata String
|
||||
-- TODO need to have an UnparsedMarkup entry type so the right parser is applied
|
||||
| Parsed metadata (Result ParseError view)
|
||||
|
||||
|
||||
type alias ParseError =
|
||||
String
|
||||
|
||||
|
||||
type alias Path =
|
||||
List String
|
||||
|
||||
|
||||
extractMetadata : pathKey -> ContentCacheInner metadata view -> List ( PagePath pathKey, metadata )
|
||||
extractMetadata pathKey cache =
|
||||
cache
|
||||
|> Dict.toList
|
||||
|> List.map (\( path, entry ) -> ( PagePath.build pathKey path, getMetadata entry ))
|
||||
|
||||
|
||||
getMetadata : Entry metadata view -> metadata
|
||||
getMetadata entry =
|
||||
case entry of
|
||||
NeedContent extension metadata ->
|
||||
metadata
|
||||
|
||||
Unparsed extension metadata _ ->
|
||||
metadata
|
||||
|
||||
Parsed metadata _ ->
|
||||
metadata
|
||||
|
||||
|
||||
pagesWithErrors : ContentCache metadata view -> Maybe (Dict (List String) String)
|
||||
pagesWithErrors cache =
|
||||
cache
|
||||
|> Result.map
|
||||
(\okCache ->
|
||||
okCache
|
||||
|> Dict.toList
|
||||
|> List.filterMap
|
||||
(\( path, value ) ->
|
||||
case value of
|
||||
Parsed metadata (Err parseError) ->
|
||||
Just ( path, parseError )
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
)
|
||||
)
|
||||
|> Result.map
|
||||
(\errors ->
|
||||
case errors of
|
||||
[] ->
|
||||
Nothing
|
||||
|
||||
_ ->
|
||||
errors
|
||||
|> Dict.fromList
|
||||
|> Just
|
||||
)
|
||||
|> Result.withDefault Nothing
|
||||
|
||||
|
||||
init :
|
||||
Document metadata view
|
||||
-> Content
|
||||
-> ContentCache metadata view
|
||||
init document content =
|
||||
parseMetadata document content
|
||||
|> List.map
|
||||
(\tuple ->
|
||||
Tuple.mapSecond
|
||||
(\result ->
|
||||
result
|
||||
|> Result.mapError (\error -> ( Tuple.first tuple, error ))
|
||||
)
|
||||
tuple
|
||||
)
|
||||
|> combineTupleResults
|
||||
|> Result.mapError Dict.fromList
|
||||
|> Result.map Dict.fromList
|
||||
|
||||
|
||||
parseMetadata :
|
||||
Document metadata view
|
||||
-> List ( List String, { extension : String, frontMatter : String, body : Maybe String } )
|
||||
-> List ( List String, Result String (Entry metadata view) )
|
||||
parseMetadata document content =
|
||||
content
|
||||
|> List.map
|
||||
(Tuple.mapSecond
|
||||
(\{ frontMatter, extension, body } ->
|
||||
let
|
||||
maybeDocumentEntry =
|
||||
Document.get extension document
|
||||
in
|
||||
case maybeDocumentEntry of
|
||||
Just documentEntry ->
|
||||
frontMatter
|
||||
|> documentEntry.frontmatterParser
|
||||
|> Result.map
|
||||
(\metadata ->
|
||||
case body of
|
||||
Just presentBody ->
|
||||
Parsed metadata
|
||||
(parseContent extension presentBody document)
|
||||
|
||||
Nothing ->
|
||||
NeedContent extension metadata
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
Err ("Could not find extension '" ++ extension ++ "'")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
parseContent :
|
||||
String
|
||||
-> String
|
||||
-> Document metadata view
|
||||
-> Result String view
|
||||
parseContent extension body document =
|
||||
let
|
||||
maybeDocumentEntry =
|
||||
Document.get extension document
|
||||
in
|
||||
case maybeDocumentEntry of
|
||||
Just documentEntry ->
|
||||
documentEntry.contentParser body
|
||||
|
||||
Nothing ->
|
||||
Err ("Could not find extension '" ++ extension ++ "'")
|
||||
|
||||
|
||||
errorView : Errors -> Html msg
|
||||
errorView errors =
|
||||
errors
|
||||
|> Dict.toList
|
||||
|> List.map errorEntryView
|
||||
|> Html.div
|
||||
[ Attr.style "padding" "20px 100px"
|
||||
]
|
||||
|
||||
|
||||
errorEntryView : ( Path, String ) -> Html msg
|
||||
errorEntryView ( path, error ) =
|
||||
Html.div []
|
||||
[ Html.h2 []
|
||||
[ Html.text ("/" ++ (path |> String.join "/"))
|
||||
]
|
||||
, Html.p [] [ Html.text "I couldn't parse the frontmatter in this page. I ran into this error with your JSON decoder:" ]
|
||||
, Html.pre [] [ Html.text error ]
|
||||
]
|
||||
|
||||
|
||||
routes : List ( List String, anything ) -> List String
|
||||
routes record =
|
||||
record
|
||||
|> List.map Tuple.first
|
||||
|> List.map (String.join "/")
|
||||
|> List.map (\route -> "/" ++ route)
|
||||
|
||||
|
||||
routesForCache : ContentCache metadata view -> List String
|
||||
routesForCache cacheResult =
|
||||
case cacheResult of
|
||||
Ok cache ->
|
||||
cache
|
||||
|> Dict.toList
|
||||
|> routes
|
||||
|
||||
Err _ ->
|
||||
[]
|
||||
|
||||
|
||||
type alias Page metadata view pathKey =
|
||||
{ metadata : metadata
|
||||
, path : PagePath pathKey
|
||||
, view : view
|
||||
}
|
||||
|
||||
|
||||
renderErrors : ( List String, List Mark.Error.Error ) -> Html msg
|
||||
renderErrors ( path, errors ) =
|
||||
Html.div []
|
||||
[ Html.text (path |> String.join "/")
|
||||
, errors
|
||||
|> List.map (Mark.Error.toHtml Mark.Error.Light)
|
||||
|> Html.div []
|
||||
]
|
||||
|
||||
|
||||
combineTupleResults :
|
||||
List ( List String, Result error success )
|
||||
-> Result (List error) (List ( List String, success ))
|
||||
combineTupleResults input =
|
||||
input
|
||||
|> List.map
|
||||
(\( path, result ) ->
|
||||
result
|
||||
|> Result.map (\success -> ( path, success ))
|
||||
)
|
||||
|> combine
|
||||
|
||||
|
||||
combine : List (Result error ( List String, success )) -> Result (List error) (List ( List String, success ))
|
||||
combine list =
|
||||
list
|
||||
|> List.foldr resultFolder (Ok [])
|
||||
|
||||
|
||||
resultFolder : Result error a -> Result (List error) (List a) -> Result (List error) (List a)
|
||||
resultFolder current soFarResult =
|
||||
case soFarResult of
|
||||
Ok soFarOk ->
|
||||
case current of
|
||||
Ok currentOk ->
|
||||
currentOk
|
||||
:: soFarOk
|
||||
|> Ok
|
||||
|
||||
Err error ->
|
||||
Err [ error ]
|
||||
|
||||
Err soFarErr ->
|
||||
case current of
|
||||
Ok currentOk ->
|
||||
Err soFarErr
|
||||
|
||||
Err error ->
|
||||
error
|
||||
:: soFarErr
|
||||
|> Err
|
||||
|
||||
|
||||
{-| Get from the Cache... if it's not already parsed, it will
|
||||
parse it before returning it and store the parsed version in the Cache
|
||||
-}
|
||||
lazyLoad :
|
||||
Document metadata view
|
||||
-> Url
|
||||
-> ContentCache metadata view
|
||||
-> Task Http.Error (ContentCache metadata view)
|
||||
lazyLoad document url cacheResult =
|
||||
case cacheResult of
|
||||
Err _ ->
|
||||
Task.succeed cacheResult
|
||||
|
||||
Ok cache ->
|
||||
case Dict.get (pathForUrl url) cache of
|
||||
Just entry ->
|
||||
case entry of
|
||||
NeedContent extension _ ->
|
||||
httpTask url
|
||||
|> Task.map
|
||||
(\downloadedContent ->
|
||||
update cacheResult
|
||||
(\thing ->
|
||||
parseContent extension thing document
|
||||
)
|
||||
url
|
||||
downloadedContent
|
||||
)
|
||||
|
||||
Unparsed extension metadata content ->
|
||||
update cacheResult
|
||||
(\thing ->
|
||||
parseContent extension thing document
|
||||
)
|
||||
url
|
||||
content
|
||||
|> Task.succeed
|
||||
|
||||
Parsed _ _ ->
|
||||
Task.succeed cacheResult
|
||||
|
||||
Nothing ->
|
||||
Task.succeed cacheResult
|
||||
|
||||
|
||||
httpTask url =
|
||||
Http.task
|
||||
{ method = "GET"
|
||||
, headers = []
|
||||
, url =
|
||||
Url.Builder.absolute
|
||||
((url.path |> String.split "/" |> List.filter (not << String.isEmpty))
|
||||
++ [ "content.txt"
|
||||
]
|
||||
)
|
||||
[]
|
||||
, body = Http.emptyBody
|
||||
, resolver =
|
||||
Http.stringResolver
|
||||
(\response ->
|
||||
case response of
|
||||
Http.BadUrl_ url_ ->
|
||||
Err (Http.BadUrl url_)
|
||||
|
||||
Http.Timeout_ ->
|
||||
Err Http.Timeout
|
||||
|
||||
Http.NetworkError_ ->
|
||||
Err Http.NetworkError
|
||||
|
||||
Http.BadStatus_ metadata body ->
|
||||
Err (Http.BadStatus metadata.statusCode)
|
||||
|
||||
Http.GoodStatus_ metadata body ->
|
||||
Ok body
|
||||
)
|
||||
, timeout = Nothing
|
||||
}
|
||||
|
||||
|
||||
update :
|
||||
ContentCache metadata view
|
||||
-> (String -> Result ParseError view)
|
||||
-> Url
|
||||
-> String
|
||||
-> ContentCache metadata view
|
||||
update cacheResult renderer url rawContent =
|
||||
case cacheResult of
|
||||
Ok cache ->
|
||||
Dict.update (pathForUrl url)
|
||||
(\entry ->
|
||||
case entry of
|
||||
Just (Parsed metadata view) ->
|
||||
entry
|
||||
|
||||
Just (Unparsed extension metadata content) ->
|
||||
Parsed metadata (renderer content)
|
||||
|> Just
|
||||
|
||||
Just (NeedContent extension metadata) ->
|
||||
Parsed metadata (renderer rawContent)
|
||||
|> Just
|
||||
|
||||
Nothing ->
|
||||
-- TODO this should never happen
|
||||
Nothing
|
||||
)
|
||||
cache
|
||||
|> Ok
|
||||
|
||||
Err error ->
|
||||
-- TODO update this ever???
|
||||
-- Should this be something other than the raw HTML, or just concat the error HTML?
|
||||
Err error
|
||||
|
||||
|
||||
pathForUrl : Url -> Path
|
||||
pathForUrl url =
|
||||
url.path
|
||||
|> dropTrailingSlash
|
||||
|> String.split "/"
|
||||
|> List.drop 1
|
||||
|
||||
|
||||
lookup :
|
||||
pathKey
|
||||
-> ContentCache metadata view
|
||||
-> Url
|
||||
-> Maybe ( PagePath pathKey, Entry metadata view )
|
||||
lookup pathKey content url =
|
||||
case content of
|
||||
Ok dict ->
|
||||
let
|
||||
path =
|
||||
pathForUrl url
|
||||
in
|
||||
Dict.get path dict
|
||||
|> Maybe.map
|
||||
(\entry ->
|
||||
( PagePath.build pathKey path, entry )
|
||||
)
|
||||
|
||||
Err _ ->
|
||||
Nothing
|
||||
|
||||
|
||||
lookupMetadata :
|
||||
ContentCache metadata view
|
||||
-> Url
|
||||
-> Maybe metadata
|
||||
lookupMetadata content url =
|
||||
lookup () content url
|
||||
|> Maybe.map
|
||||
(\( pagePath, entry ) ->
|
||||
case entry of
|
||||
NeedContent _ metadata ->
|
||||
metadata
|
||||
|
||||
Unparsed _ metadata _ ->
|
||||
metadata
|
||||
|
||||
Parsed metadata _ ->
|
||||
metadata
|
||||
)
|
||||
|
||||
|
||||
dropTrailingSlash path =
|
||||
if path |> String.endsWith "/" then
|
||||
String.dropRight 1 path
|
||||
|
||||
else
|
||||
path
|
|
@ -0,0 +1,415 @@
|
|||
module Pages.Platform exposing (Flags, Model, Msg, Page, Parser, Program, application, cliApplication)
|
||||
|
||||
import Browser
|
||||
import Browser.Navigation
|
||||
import Dict exposing (Dict)
|
||||
import Head
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes
|
||||
import Http
|
||||
import Json.Decode
|
||||
import Json.Encode
|
||||
import List.Extra
|
||||
import Mark
|
||||
import Pages.ContentCache as ContentCache exposing (ContentCache)
|
||||
import Pages.Document
|
||||
import Pages.Manifest as Manifest
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Result.Extra
|
||||
import Task exposing (Task)
|
||||
import Url exposing (Url)
|
||||
|
||||
|
||||
dropTrailingSlash path =
|
||||
if path |> String.endsWith "/" then
|
||||
String.dropRight 1 path
|
||||
|
||||
else
|
||||
path
|
||||
|
||||
|
||||
type alias Page metadata view pathKey =
|
||||
{ metadata : metadata
|
||||
, path : PagePath pathKey
|
||||
, view : view
|
||||
}
|
||||
|
||||
|
||||
type alias Content =
|
||||
List ( List String, { extension : String, frontMatter : String, body : Maybe String } )
|
||||
|
||||
|
||||
type alias Program userModel userMsg metadata view =
|
||||
Platform.Program Flags (Model userModel userMsg metadata view) (Msg userMsg metadata view)
|
||||
|
||||
|
||||
mainView :
|
||||
pathKey
|
||||
-> (userModel -> List ( PagePath pathKey, metadata ) -> Page metadata view pathKey -> { title : String, body : Html userMsg })
|
||||
-> ModelDetails userModel metadata view
|
||||
-> { title : String, body : Html userMsg }
|
||||
mainView pathKey pageView model =
|
||||
case model.contentCache of
|
||||
Ok site ->
|
||||
pageViewOrError pathKey pageView model model.contentCache
|
||||
|
||||
-- TODO these lookup helpers should not need it to be a Result
|
||||
Err errors ->
|
||||
{ title = "Error parsing"
|
||||
, body = ContentCache.errorView errors
|
||||
}
|
||||
|
||||
|
||||
pageViewOrError :
|
||||
pathKey
|
||||
-> (userModel -> List ( PagePath pathKey, metadata ) -> Page metadata view pathKey -> { title : String, body : Html userMsg })
|
||||
-> ModelDetails userModel metadata view
|
||||
-> ContentCache metadata view
|
||||
-> { title : String, body : Html userMsg }
|
||||
pageViewOrError pathKey pageView model cache =
|
||||
case ContentCache.lookup pathKey cache model.url of
|
||||
Just ( pagePath, entry ) ->
|
||||
case entry of
|
||||
ContentCache.Parsed metadata viewResult ->
|
||||
case viewResult of
|
||||
Ok viewList ->
|
||||
pageView model.userModel
|
||||
(cache
|
||||
|> Result.map (ContentCache.extractMetadata pathKey)
|
||||
|> Result.withDefault []
|
||||
-- TODO handle error better
|
||||
)
|
||||
{ metadata = metadata
|
||||
, path = pagePath
|
||||
, view = viewList
|
||||
}
|
||||
|
||||
Err error ->
|
||||
{ title = "Parsing error"
|
||||
, body = Html.text error
|
||||
}
|
||||
|
||||
ContentCache.NeedContent extension _ ->
|
||||
{ title = "", body = Html.text "" }
|
||||
|
||||
ContentCache.Unparsed extension _ _ ->
|
||||
{ title = "", body = Html.text "" }
|
||||
|
||||
Nothing ->
|
||||
{ title = "Page not found"
|
||||
, body =
|
||||
Html.div []
|
||||
[ Html.text "Page not found. Valid routes:\n\n"
|
||||
, cache
|
||||
|> ContentCache.routesForCache
|
||||
|> String.join ", "
|
||||
|> Html.text
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
view :
|
||||
pathKey
|
||||
-> Content
|
||||
-> (userModel -> List ( PagePath pathKey, metadata ) -> Page metadata view pathKey -> { title : String, body : Html userMsg })
|
||||
-> ModelDetails userModel metadata view
|
||||
-> Browser.Document (Msg userMsg metadata view)
|
||||
view pathKey content pageView model =
|
||||
let
|
||||
{ title, body } =
|
||||
mainView pathKey pageView model
|
||||
in
|
||||
{ title = title
|
||||
, body =
|
||||
[ Html.div
|
||||
[ Html.Attributes.attribute "data-url" (Url.toString model.url)
|
||||
]
|
||||
[ body
|
||||
|> Html.map UserMsg
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
encodeHeads : String -> String -> List (Head.Tag pathKey) -> Json.Encode.Value
|
||||
encodeHeads canonicalSiteUrl currentPagePath head =
|
||||
Json.Encode.list (Head.toJson canonicalSiteUrl currentPagePath) head
|
||||
|
||||
|
||||
type alias Flags =
|
||||
{}
|
||||
|
||||
|
||||
combineTupleResults :
|
||||
List ( List String, Result error success )
|
||||
-> Result error (List ( List String, success ))
|
||||
combineTupleResults input =
|
||||
input
|
||||
|> List.map
|
||||
(\( path, result ) ->
|
||||
result
|
||||
|> Result.map (\success -> ( path, success ))
|
||||
)
|
||||
|> Result.Extra.combine
|
||||
|
||||
|
||||
init :
|
||||
pathKey
|
||||
-> String
|
||||
-> Pages.Document.Document metadata view
|
||||
-> (Json.Encode.Value -> Cmd (Msg userMsg metadata view))
|
||||
-> (metadata -> List (Head.Tag pathKey))
|
||||
-> Content
|
||||
-> ( userModel, Cmd userMsg )
|
||||
-> Flags
|
||||
-> Url
|
||||
-> Browser.Navigation.Key
|
||||
-> ( ModelDetails userModel metadata view, Cmd (Msg userMsg metadata view) )
|
||||
init pathKey canonicalSiteUrl document toJsPort head content initUserModel flags url key =
|
||||
let
|
||||
( userModel, userCmd ) =
|
||||
initUserModel
|
||||
|
||||
contentCache =
|
||||
ContentCache.init document content
|
||||
in
|
||||
case contentCache of
|
||||
Ok okCache ->
|
||||
( { key = key
|
||||
, url = url
|
||||
, userModel = userModel
|
||||
, contentCache = contentCache
|
||||
}
|
||||
, Cmd.batch
|
||||
([ ContentCache.lookupMetadata (Ok okCache) url
|
||||
|> Maybe.map head
|
||||
|> Maybe.map (encodeHeads canonicalSiteUrl url.path)
|
||||
|> Maybe.map toJsPort
|
||||
, userCmd |> Cmd.map UserMsg |> Just
|
||||
, contentCache
|
||||
|> ContentCache.lazyLoad document url
|
||||
|> Task.attempt UpdateCache
|
||||
|> Just
|
||||
]
|
||||
|> List.filterMap identity
|
||||
)
|
||||
)
|
||||
|
||||
Err _ ->
|
||||
( { key = key
|
||||
, url = url
|
||||
, userModel = userModel
|
||||
, contentCache = contentCache
|
||||
}
|
||||
, Cmd.batch
|
||||
[ userCmd |> Cmd.map UserMsg
|
||||
]
|
||||
-- TODO handle errors better
|
||||
)
|
||||
|
||||
|
||||
type Msg userMsg metadata view
|
||||
= LinkClicked Browser.UrlRequest
|
||||
| UrlChanged Url.Url
|
||||
| UserMsg userMsg
|
||||
| UpdateCache (Result Http.Error (ContentCache metadata view))
|
||||
| UpdateCacheAndUrl Url (Result Http.Error (ContentCache metadata view))
|
||||
|
||||
|
||||
type Model userModel userMsg metadata view
|
||||
= Model (ModelDetails userModel metadata view)
|
||||
| CliModel
|
||||
|
||||
|
||||
type alias ModelDetails userModel metadata view =
|
||||
{ key : Browser.Navigation.Key
|
||||
, url : Url.Url
|
||||
, contentCache : ContentCache metadata view
|
||||
, userModel : userModel
|
||||
}
|
||||
|
||||
|
||||
update :
|
||||
(Json.Encode.Value -> Cmd (Msg userMsg metadata view))
|
||||
-> Pages.Document.Document metadata view
|
||||
-> (userMsg -> userModel -> ( userModel, Cmd userMsg ))
|
||||
-> Msg userMsg metadata view
|
||||
-> ModelDetails userModel metadata view
|
||||
-> ( ModelDetails userModel metadata view, Cmd (Msg userMsg metadata view) )
|
||||
update toJsPort document userUpdate msg model =
|
||||
case msg of
|
||||
LinkClicked urlRequest ->
|
||||
case urlRequest of
|
||||
Browser.Internal url ->
|
||||
let
|
||||
navigatingToSamePage =
|
||||
url.path == model.url.path
|
||||
in
|
||||
if navigatingToSamePage then
|
||||
-- this is a workaround for an issue with anchor fragment navigation
|
||||
-- see https://github.com/elm/browser/issues/39
|
||||
( model, Browser.Navigation.load (Url.toString url) )
|
||||
|
||||
else
|
||||
( model, Browser.Navigation.pushUrl model.key (Url.toString url) )
|
||||
|
||||
Browser.External href ->
|
||||
( model, Browser.Navigation.load href )
|
||||
|
||||
UrlChanged url ->
|
||||
( model
|
||||
, model.contentCache
|
||||
|> ContentCache.lazyLoad document url
|
||||
|> Task.attempt (UpdateCacheAndUrl url)
|
||||
)
|
||||
|
||||
UserMsg userMsg ->
|
||||
let
|
||||
( userModel, userCmd ) =
|
||||
userUpdate userMsg model.userModel
|
||||
in
|
||||
( { model | userModel = userModel }, userCmd |> Cmd.map UserMsg )
|
||||
|
||||
UpdateCache cacheUpdateResult ->
|
||||
case cacheUpdateResult of
|
||||
-- TODO can there be race conditions here? Might need to set something in the model
|
||||
-- to keep track of the last url change
|
||||
Ok updatedCache ->
|
||||
( { model | contentCache = updatedCache }, Cmd.none )
|
||||
|
||||
Err _ ->
|
||||
-- TODO handle error
|
||||
( model, Cmd.none )
|
||||
|
||||
UpdateCacheAndUrl url cacheUpdateResult ->
|
||||
case cacheUpdateResult of
|
||||
-- TODO can there be race conditions here? Might need to set something in the model
|
||||
-- to keep track of the last url change
|
||||
Ok updatedCache ->
|
||||
( { model | url = url, contentCache = updatedCache }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Err _ ->
|
||||
-- TODO handle error
|
||||
( { model | url = url }, Cmd.none )
|
||||
|
||||
|
||||
type alias Parser metadata view =
|
||||
Dict String String
|
||||
-> List String
|
||||
-> List ( List String, metadata )
|
||||
-> Mark.Document view
|
||||
|
||||
|
||||
application :
|
||||
{ init : ( userModel, Cmd userMsg )
|
||||
, update : userMsg -> userModel -> ( userModel, Cmd userMsg )
|
||||
, subscriptions : userModel -> Sub userMsg
|
||||
, view : userModel -> List ( PagePath pathKey, metadata ) -> Page metadata view pathKey -> { title : String, body : Html userMsg }
|
||||
, document : Pages.Document.Document metadata view
|
||||
, content : Content
|
||||
, toJsPort : Json.Encode.Value -> Cmd (Msg userMsg metadata view)
|
||||
, head : metadata -> List (Head.Tag pathKey)
|
||||
, manifest : Manifest.Config pathKey
|
||||
, canonicalSiteUrl : String
|
||||
, pathKey : pathKey
|
||||
}
|
||||
-> Program userModel userMsg metadata view
|
||||
application config =
|
||||
Browser.application
|
||||
{ init =
|
||||
\flags url key ->
|
||||
init config.pathKey config.canonicalSiteUrl config.document config.toJsPort config.head config.content config.init flags url key
|
||||
|> Tuple.mapFirst Model
|
||||
, view =
|
||||
\outerModel ->
|
||||
case outerModel of
|
||||
Model model ->
|
||||
view config.pathKey config.content config.view model
|
||||
|
||||
CliModel ->
|
||||
{ title = "Error"
|
||||
, body = [ Html.text "Unexpected state" ]
|
||||
}
|
||||
, update =
|
||||
\msg outerModel ->
|
||||
case outerModel of
|
||||
Model model ->
|
||||
update config.toJsPort config.document config.update msg model |> Tuple.mapFirst Model
|
||||
|
||||
CliModel ->
|
||||
( outerModel, Cmd.none )
|
||||
, subscriptions =
|
||||
\outerModel ->
|
||||
case outerModel of
|
||||
Model model ->
|
||||
config.subscriptions model.userModel
|
||||
|> Sub.map UserMsg
|
||||
|
||||
CliModel ->
|
||||
Sub.none
|
||||
, onUrlChange = UrlChanged
|
||||
, onUrlRequest = LinkClicked
|
||||
}
|
||||
|
||||
|
||||
cliApplication :
|
||||
{ init : ( userModel, Cmd userMsg )
|
||||
, update : userMsg -> userModel -> ( userModel, Cmd userMsg )
|
||||
, subscriptions : userModel -> Sub userMsg
|
||||
, view : userModel -> List ( PagePath pathKey, metadata ) -> Page metadata view pathKey -> { title : String, body : Html userMsg }
|
||||
, document : Pages.Document.Document metadata view
|
||||
, content : Content
|
||||
, toJsPort : Json.Encode.Value -> Cmd (Msg userMsg metadata view)
|
||||
, head : metadata -> List (Head.Tag pathKey)
|
||||
, manifest : Manifest.Config pathKey
|
||||
, canonicalSiteUrl : String
|
||||
, pathKey : pathKey
|
||||
}
|
||||
-> Program userModel userMsg metadata view
|
||||
cliApplication config =
|
||||
let
|
||||
contentCache =
|
||||
ContentCache.init config.document config.content
|
||||
in
|
||||
Platform.worker
|
||||
{ init =
|
||||
\flags ->
|
||||
( CliModel
|
||||
, case contentCache of
|
||||
Ok _ ->
|
||||
case contentCache |> ContentCache.pagesWithErrors of
|
||||
Just pageErrors ->
|
||||
config.toJsPort
|
||||
(Json.Encode.object
|
||||
[ ( "errors", encodeErrors pageErrors )
|
||||
, ( "manifest", Manifest.toJson config.manifest )
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
config.toJsPort
|
||||
(Json.Encode.object
|
||||
[ ( "manifest", Manifest.toJson config.manifest )
|
||||
]
|
||||
)
|
||||
|
||||
Err error ->
|
||||
config.toJsPort
|
||||
(Json.Encode.object
|
||||
[ ( "errors", encodeErrors error )
|
||||
, ( "manifest", Manifest.toJson config.manifest )
|
||||
]
|
||||
)
|
||||
)
|
||||
, update = \msg model -> ( model, Cmd.none )
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
|
||||
|
||||
encodeErrors errors =
|
||||
errors
|
||||
|> Json.Encode.dict
|
||||
(\path -> "/" ++ String.join "/" path)
|
||||
(\errorsForPath -> Json.Encode.string errorsForPath)
|
After Width: | Height: | Size: 307 KiB |
After Width: | Height: | Size: 293 KiB |
After Width: | Height: | Size: 2.4 MiB |
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 323.141 322.95" enable-background="new 0 0 323.141 322.95" xml:space="preserve">
|
||||
<g>
|
||||
<polygon
|
||||
fill="#F0AD00"
|
||||
points="161.649,152.782 231.514,82.916 91.783,82.916"/>
|
||||
|
||||
<polygon
|
||||
fill="#7FD13B"
|
||||
points="8.867,0 79.241,70.375 232.213,70.375 161.838,0"/>
|
||||
|
||||
<rect
|
||||
fill="#7FD13B"
|
||||
x="192.99"
|
||||
y="107.392"
|
||||
transform="matrix(0.7071 0.7071 -0.7071 0.7071 186.4727 -127.2386)"
|
||||
width="107.676"
|
||||
height="108.167"/>
|
||||
|
||||
<polygon
|
||||
fill="#60B5CC"
|
||||
points="323.298,143.724 323.298,0 179.573,0"/>
|
||||
|
||||
<polygon
|
||||
fill="#5A6378"
|
||||
points="152.781,161.649 0,8.868 0,314.432"/>
|
||||
|
||||
<polygon
|
||||
fill="#F0AD00"
|
||||
points="255.522,246.655 323.298,314.432 323.298,178.879"/>
|
||||
|
||||
<polygon
|
||||
fill="#60B5CC"
|
||||
points="161.649,170.517 8.869,323.298 314.43,323.298"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub icon</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
After Width: | Height: | Size: 827 B |
After Width: | Height: | Size: 976 B |
|
@ -0,0 +1,2 @@
|
|||
<svg version="1.1" viewBox="251.0485 144.52063 56.114286 74.5" width="50px" height="74.5"><defs><linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="10%" style="stop-color:rgba(1.96%,45.88%,90.2%,1);stop-opacity:1"></stop><stop offset="100%" style="stop-color:rgba(0%,94.9%,37.65%,1);stop-opacity:1"></stop></linearGradient></defs><metadata></metadata><g id="Canvas_11" stroke="none" fill="url(#grad1)" stroke-opacity="1" fill-opacity="1" stroke-dasharray="none"><g id="Canvas_11: Layer 1"><g id="Group_38"><g id="Graphic_32"><path d="M 252.5485 146.02063 L 252.5485 217.52063 L 305.66277 217.52063 L 305.66277 161.68254 L 290.00087 146.02063 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"></path></g><g id="Line_34"><line x1="266.07286" y1="182.8279" x2="290.75465" y2="183.00997" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_35"><line x1="266.07286" y1="191.84156" x2="290.75465" y2="192.02363" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_36"><line x1="266.07286" y1="200.85522" x2="290.75465" y2="201.0373" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_37"><line x1="266.07286" y1="164.80058" x2="278.3874" y2="164.94049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,11 @@
|
|||
import hljs from "highlight.js";
|
||||
import "highlight.js/styles/github.css";
|
||||
import "./style.css";
|
||||
// @ts-ignore
|
||||
window.hljs = hljs;
|
||||
const { Elm } = require("./src/Main.elm");
|
||||
const pagesInit = require("elm-pages");
|
||||
|
||||
pagesInit({
|
||||
mainElmModule: Elm.Main
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
[build]
|
||||
publish = "./dist/"
|
||||
command = "npm run build"
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "elm-pages-example",
|
||||
"version": "1.0.0",
|
||||
"description": "Example site built with elm-pages.",
|
||||
"scripts": {
|
||||
"start": "elm-pages develop",
|
||||
"serve": "npm run build && http-server ./dist -a localhost -p 3000 -c-1",
|
||||
"build": "elm-pages build"
|
||||
},
|
||||
"author": "Dillon Kearns",
|
||||
"license": "BSD-3",
|
||||
"dependencies": {
|
||||
"highlight.js": "^9.15.10",
|
||||
"node-sass": "^4.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"elm": "^0.19.0-no-deps",
|
||||
"elm-pages": "1.0.35",
|
||||
"http-server": "^0.11.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
module Data.Author exposing (Author, all, decoder, view)
|
||||
|
||||
import Element exposing (Element)
|
||||
import Html.Attributes as Attr
|
||||
import Json.Decode as Decode exposing (Decoder)
|
||||
import List.Extra
|
||||
import Pages
|
||||
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
||||
|
||||
|
||||
type alias Author =
|
||||
{ name : String
|
||||
, avatar : ImagePath Pages.PathKey
|
||||
, bio : String
|
||||
}
|
||||
|
||||
|
||||
all : List Author
|
||||
all =
|
||||
[ { name = "Dillon Kearns"
|
||||
, avatar = Pages.images.author.dillon
|
||||
, bio = "Elm developer and educator. Founder of Incremental Elm Consulting."
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
decoder : Decoder Author
|
||||
decoder =
|
||||
Decode.string
|
||||
|> Decode.andThen
|
||||
(\lookupName ->
|
||||
case List.Extra.find (\currentAuthor -> currentAuthor.name == lookupName) all of
|
||||
Just author ->
|
||||
Decode.succeed author
|
||||
|
||||
Nothing ->
|
||||
Decode.fail ("Couldn't find author with name " ++ lookupName ++ ". Options are " ++ String.join ", " (List.map .name all))
|
||||
)
|
||||
|
||||
|
||||
view : List (Element.Attribute msg) -> Author -> Element msg
|
||||
view attributes author =
|
||||
Element.image
|
||||
(Element.width (Element.px 70)
|
||||
:: Element.htmlAttribute (Attr.class "avatar")
|
||||
:: attributes
|
||||
)
|
||||
{ src = ImagePath.toString author.avatar, description = author.name }
|
|
@ -0,0 +1,91 @@
|
|||
module DocumentSvg exposing (view)
|
||||
|
||||
import Color
|
||||
import Element exposing (Element)
|
||||
import Svg exposing (..)
|
||||
import Svg.Attributes exposing (..)
|
||||
|
||||
|
||||
strokeColor =
|
||||
-- "url(#grad1)"
|
||||
"black"
|
||||
|
||||
|
||||
pageTextColor =
|
||||
"black"
|
||||
|
||||
|
||||
fillColor =
|
||||
"url(#grad1)"
|
||||
|
||||
|
||||
|
||||
-- "none"
|
||||
|
||||
|
||||
fillGradient =
|
||||
gradient
|
||||
(Color.rgb255 5 117 230)
|
||||
(Color.rgb255 0 242 96)
|
||||
|
||||
|
||||
|
||||
-- (Color.rgb255 252 0 255)
|
||||
-- (Color.rgb255 0 219 222)
|
||||
-- (Color.rgb255 255 93 194)
|
||||
-- (Color.rgb255 255 150 250)
|
||||
|
||||
|
||||
gradient color1 color2 =
|
||||
linearGradient [ id "grad1", x1 "0%", y1 "0%", x2 "100%", y2 "0%" ]
|
||||
[ stop
|
||||
[ offset "10%"
|
||||
, Svg.Attributes.style ("stop-color:" ++ Color.toCssString color1 ++ ";stop-opacity:1")
|
||||
]
|
||||
[]
|
||||
, stop [ offset "100%", Svg.Attributes.style ("stop-color:" ++ Color.toCssString color2 ++ ";stop-opacity:1") ] []
|
||||
]
|
||||
|
||||
|
||||
view : Element msg
|
||||
view =
|
||||
svg
|
||||
[ version "1.1"
|
||||
, viewBox "251.0485 144.52063 56.114286 74.5"
|
||||
, width "56.114286"
|
||||
, height "74.5"
|
||||
, Svg.Attributes.width "30px"
|
||||
]
|
||||
[ defs []
|
||||
[ fillGradient ]
|
||||
, metadata [] []
|
||||
, g
|
||||
[ id "Canvas_11"
|
||||
, stroke "none"
|
||||
, fill fillColor
|
||||
, strokeOpacity "1"
|
||||
, fillOpacity "1"
|
||||
, strokeDasharray "none"
|
||||
]
|
||||
[ g [ id "Canvas_11: Layer 1" ]
|
||||
[ g [ id "Group_38" ]
|
||||
[ g [ id "Graphic_32" ]
|
||||
[ Svg.path
|
||||
[ d "M 252.5485 146.02063 L 252.5485 217.52063 L 305.66277 217.52063 L 305.66277 161.68254 L 290.00087 146.02063 Z"
|
||||
, stroke strokeColor
|
||||
, strokeLinecap "round"
|
||||
, strokeLinejoin "round"
|
||||
, strokeWidth "3"
|
||||
]
|
||||
[]
|
||||
]
|
||||
, g [ id "Line_34" ] [ line [ x1 "266.07286", y1 "182.8279", x2 "290.75465", y2 "183.00997", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
, g [ id "Line_35" ] [ line [ x1 "266.07286", y1 "191.84156", x2 "290.75465", y2 "192.02363", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
, g [ id "Line_36" ] [ line [ x1 "266.07286", y1 "200.85522", x2 "290.75465", y2 "201.0373", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
, g [ id "Line_37" ] [ line [ x1 "266.07286", y1 "164.80058", x2 "278.3874", y2 "164.94049", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|> Element.html
|
||||
|> Element.el []
|
|
@ -0,0 +1,121 @@
|
|||
module Index exposing (view)
|
||||
|
||||
import Data.Author
|
||||
import Date
|
||||
import Element exposing (Element)
|
||||
import Element.Border
|
||||
import Element.Font
|
||||
import Metadata exposing (Metadata)
|
||||
import Pages
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Pages.Platform exposing (Page)
|
||||
|
||||
|
||||
view :
|
||||
List ( PagePath Pages.PathKey, Metadata )
|
||||
-> Element msg
|
||||
view posts =
|
||||
Element.column [ Element.spacing 20 ]
|
||||
(posts
|
||||
|> List.filterMap
|
||||
(\( path, metadata ) ->
|
||||
case metadata of
|
||||
Metadata.Page meta ->
|
||||
Nothing
|
||||
|
||||
Metadata.Author _ ->
|
||||
Nothing
|
||||
|
||||
Metadata.Article meta ->
|
||||
if meta.draft then
|
||||
Nothing
|
||||
|
||||
else
|
||||
Just ( path, meta )
|
||||
|
||||
Metadata.BlogIndex ->
|
||||
Nothing
|
||||
)
|
||||
|> List.map postSummary
|
||||
)
|
||||
|
||||
|
||||
postSummary :
|
||||
( PagePath Pages.PathKey, Metadata.ArticleMetadata )
|
||||
-> Element msg
|
||||
postSummary ( postPath, post ) =
|
||||
articleIndex post
|
||||
|> linkToPost postPath
|
||||
|
||||
|
||||
linkToPost : PagePath Pages.PathKey -> Element msg -> Element msg
|
||||
linkToPost postPath content =
|
||||
Element.link [ Element.width Element.fill ]
|
||||
{ url = PagePath.toString postPath, label = content }
|
||||
|
||||
|
||||
title : String -> Element msg
|
||||
title text =
|
||||
[ Element.text text ]
|
||||
|> Element.paragraph
|
||||
[ Element.Font.size 36
|
||||
, Element.Font.center
|
||||
, Element.Font.family [ Element.Font.typeface "Raleway" ]
|
||||
, Element.Font.semiBold
|
||||
, Element.padding 16
|
||||
]
|
||||
|
||||
|
||||
articleIndex : Metadata.ArticleMetadata -> Element msg
|
||||
articleIndex metadata =
|
||||
Element.el
|
||||
[ Element.centerX
|
||||
, Element.width (Element.maximum 800 Element.fill)
|
||||
, Element.padding 40
|
||||
, Element.spacing 10
|
||||
, Element.Border.width 1
|
||||
, Element.Border.color (Element.rgba255 0 0 0 0.1)
|
||||
, Element.mouseOver
|
||||
[ Element.Border.color (Element.rgba255 0 0 0 1)
|
||||
]
|
||||
]
|
||||
(postPreview metadata)
|
||||
|
||||
|
||||
readMoreLink =
|
||||
Element.text "Continue reading >>"
|
||||
|> Element.el
|
||||
[ Element.centerX
|
||||
, Element.Font.size 18
|
||||
, Element.alpha 0.6
|
||||
, Element.mouseOver [ Element.alpha 1 ]
|
||||
, Element.Font.underline
|
||||
, Element.Font.center
|
||||
]
|
||||
|
||||
|
||||
postPreview : Metadata.ArticleMetadata -> Element msg
|
||||
postPreview post =
|
||||
Element.textColumn
|
||||
[ Element.centerX
|
||||
, Element.width Element.fill
|
||||
, Element.spacing 30
|
||||
, Element.Font.size 18
|
||||
]
|
||||
[ title post.title
|
||||
, Element.row [ Element.spacing 10, Element.centerX ]
|
||||
[ Data.Author.view [ Element.width (Element.px 40) ] post.author
|
||||
, Element.text post.author.name
|
||||
, Element.text "•"
|
||||
, Element.text (post.published |> Date.format "MMMM ddd, yyyy")
|
||||
]
|
||||
, post.description
|
||||
|> Element.text
|
||||
|> List.singleton
|
||||
|> Element.paragraph
|
||||
[ Element.Font.size 22
|
||||
, Element.Font.center
|
||||
, Element.Font.family [ Element.Font.typeface "Raleway" ]
|
||||
]
|
||||
, readMoreLink
|
||||
]
|
|
@ -0,0 +1,419 @@
|
|||
module Main exposing (main)
|
||||
|
||||
import Color
|
||||
import Data.Author as Author
|
||||
import Date
|
||||
import DocumentSvg
|
||||
import Element exposing (Element)
|
||||
import Element.Background
|
||||
import Element.Border
|
||||
import Element.Font as Font
|
||||
import Element.Region
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
import Index
|
||||
import Json.Decode
|
||||
import Markdown
|
||||
import Metadata exposing (Metadata)
|
||||
import Pages exposing (images, pages)
|
||||
import Pages.Directory as Directory exposing (Directory)
|
||||
import Pages.Document
|
||||
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
||||
import Pages.Manifest as Manifest
|
||||
import Pages.Manifest.Category
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Pages.Platform exposing (Page)
|
||||
import Palette
|
||||
|
||||
|
||||
manifest : Manifest.Config Pages.PathKey
|
||||
manifest =
|
||||
{ backgroundColor = Just Color.white
|
||||
, categories = [ Pages.Manifest.Category.education ]
|
||||
, displayMode = Manifest.Standalone
|
||||
, orientation = Manifest.Portrait
|
||||
, description = "elm-pages - A statically typed site generator."
|
||||
, iarcRatingId = Nothing
|
||||
, name = "elm-pages docs"
|
||||
, themeColor = Just Color.white
|
||||
, startUrl = pages.index
|
||||
, shortName = Just "elm-pages"
|
||||
, sourceIcon = images.iconPng
|
||||
}
|
||||
|
||||
|
||||
type alias Rendered =
|
||||
Element Msg
|
||||
|
||||
|
||||
|
||||
-- the intellij-elm plugin doesn't support type aliases for Programs so we need to use this line
|
||||
-- main : Platform.Program Pages.Platform.Flags (Pages.Platform.Model Model Msg Metadata Rendered) (Pages.Platform.Msg Msg Metadata Rendered)
|
||||
|
||||
|
||||
main : Pages.Platform.Program Model Msg Metadata Rendered
|
||||
main =
|
||||
Pages.application
|
||||
{ init = init
|
||||
, view = view
|
||||
, update = update
|
||||
, subscriptions = subscriptions
|
||||
, documents = [ markdownDocument ]
|
||||
, head = head
|
||||
, manifest = manifest
|
||||
, canonicalSiteUrl = canonicalSiteUrl
|
||||
}
|
||||
|
||||
|
||||
markdownDocument : ( String, Pages.Document.DocumentHandler Metadata Rendered )
|
||||
markdownDocument =
|
||||
Pages.Document.parser
|
||||
{ extension = "md"
|
||||
, metadata = Metadata.decoder
|
||||
, body =
|
||||
\markdownBody ->
|
||||
Html.div [] [ Markdown.toHtml [] markdownBody ]
|
||||
|> Element.html
|
||||
|> Ok
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
{}
|
||||
|
||||
|
||||
init : ( Model, Cmd Msg )
|
||||
init =
|
||||
( Model, Cmd.none )
|
||||
|
||||
|
||||
type alias Msg =
|
||||
()
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
() ->
|
||||
( model, Cmd.none )
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions _ =
|
||||
Sub.none
|
||||
|
||||
|
||||
view : Model -> List ( PagePath Pages.PathKey, Metadata ) -> Page Metadata Rendered Pages.PathKey -> { title : String, body : Html Msg }
|
||||
view model siteMetadata page =
|
||||
let
|
||||
{ title, body } =
|
||||
pageView model siteMetadata page
|
||||
in
|
||||
{ title = title
|
||||
, body =
|
||||
body
|
||||
|> Element.layout
|
||||
[ Element.width Element.fill
|
||||
, Font.size 20
|
||||
, Font.family [ Font.typeface "Roboto" ]
|
||||
, Font.color (Element.rgba255 0 0 0 0.8)
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
pageView : Model -> List ( PagePath Pages.PathKey, Metadata ) -> Page Metadata Rendered Pages.PathKey -> { title : String, body : Element Msg }
|
||||
pageView model siteMetadata page =
|
||||
case page.metadata of
|
||||
Metadata.Page metadata ->
|
||||
{ title = metadata.title
|
||||
, body =
|
||||
[ header page.path
|
||||
, Element.column
|
||||
[ Element.padding 50
|
||||
, Element.spacing 60
|
||||
, Element.Region.mainContent
|
||||
]
|
||||
[ page.view
|
||||
]
|
||||
]
|
||||
|> Element.textColumn
|
||||
[ Element.width Element.fill
|
||||
]
|
||||
}
|
||||
|
||||
Metadata.Article metadata ->
|
||||
{ title = metadata.title
|
||||
, body =
|
||||
Element.column [ Element.width Element.fill ]
|
||||
[ header page.path
|
||||
, Element.column
|
||||
[ Element.padding 30
|
||||
, Element.spacing 40
|
||||
, Element.Region.mainContent
|
||||
, Element.width (Element.fill |> Element.maximum 800)
|
||||
, Element.centerX
|
||||
]
|
||||
(Element.column [ Element.spacing 10 ]
|
||||
[ Element.row [ Element.spacing 10 ]
|
||||
[ Author.view [] metadata.author
|
||||
, Element.column [ Element.spacing 10, Element.width Element.fill ]
|
||||
[ Element.paragraph [ Font.bold, Font.size 24 ]
|
||||
[ Element.text metadata.author.name
|
||||
]
|
||||
, Element.paragraph [ Font.size 16 ]
|
||||
[ Element.text metadata.author.bio ]
|
||||
]
|
||||
]
|
||||
]
|
||||
:: (publishedDateView metadata |> Element.el [ Font.size 16, Font.color (Element.rgba255 0 0 0 0.6) ])
|
||||
:: Palette.blogHeading metadata.title
|
||||
:: articleImageView metadata.image
|
||||
:: [ page.view ]
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
Metadata.Author author ->
|
||||
{ title = author.name
|
||||
, body =
|
||||
Element.column
|
||||
[ Element.width Element.fill
|
||||
]
|
||||
[ header page.path
|
||||
, Element.column
|
||||
[ Element.padding 30
|
||||
, Element.spacing 20
|
||||
, Element.Region.mainContent
|
||||
, Element.width (Element.fill |> Element.maximum 800)
|
||||
, Element.centerX
|
||||
]
|
||||
[ Palette.blogHeading author.name
|
||||
, Author.view [] author
|
||||
, Element.paragraph [ Element.centerX, Font.center ] [ page.view ]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
Metadata.BlogIndex ->
|
||||
{ title = "elm-pages blog"
|
||||
, body =
|
||||
Element.column [ Element.width Element.fill ]
|
||||
[ header page.path
|
||||
, Element.column [ Element.padding 20, Element.centerX ] [ Index.view siteMetadata ]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
articleImageView : ImagePath Pages.PathKey -> Element msg
|
||||
articleImageView articleImage =
|
||||
Element.image [ Element.width Element.fill ]
|
||||
{ src = ImagePath.toString articleImage
|
||||
, description = "Article cover photo"
|
||||
}
|
||||
|
||||
|
||||
header : PagePath Pages.PathKey -> Element msg
|
||||
header currentPath =
|
||||
Element.column [ Element.width Element.fill ]
|
||||
[ Element.el
|
||||
[ Element.height (Element.px 4)
|
||||
, Element.width Element.fill
|
||||
, Element.Background.gradient
|
||||
{ angle = 0.2
|
||||
, steps =
|
||||
[ Element.rgb255 0 242 96
|
||||
, Element.rgb255 5 117 230
|
||||
]
|
||||
}
|
||||
]
|
||||
Element.none
|
||||
, Element.row
|
||||
[ Element.paddingXY 25 4
|
||||
, Element.spaceEvenly
|
||||
, Element.width Element.fill
|
||||
, Element.Region.navigation
|
||||
, Element.Border.widthEach { bottom = 1, left = 0, right = 0, top = 0 }
|
||||
, Element.Border.color (Element.rgba255 40 80 40 0.4)
|
||||
]
|
||||
[ Element.link []
|
||||
{ url = "/"
|
||||
, label =
|
||||
Element.row [ Font.size 30, Element.spacing 16 ]
|
||||
[ DocumentSvg.view
|
||||
, Element.text "elm-pages-starter"
|
||||
]
|
||||
}
|
||||
, Element.row [ Element.spacing 15 ]
|
||||
[ elmDocsLink
|
||||
, githubRepoLink
|
||||
, highlightableLink currentPath pages.blog.directory "Blog"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
highlightableLink :
|
||||
PagePath Pages.PathKey
|
||||
-> Directory Pages.PathKey Directory.WithIndex
|
||||
-> String
|
||||
-> Element msg
|
||||
highlightableLink currentPath linkDirectory displayName =
|
||||
let
|
||||
isHighlighted =
|
||||
currentPath |> Directory.includes linkDirectory
|
||||
in
|
||||
Element.link
|
||||
(if isHighlighted then
|
||||
[ Font.underline
|
||||
, Font.color Palette.color.primary
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
)
|
||||
{ url = linkDirectory |> Directory.indexPath |> PagePath.toString
|
||||
, label = Element.text displayName
|
||||
}
|
||||
|
||||
|
||||
{-| <https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards>
|
||||
<https://htmlhead.dev>
|
||||
<https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names>
|
||||
<https://ogp.me/>
|
||||
-}
|
||||
head : Metadata -> List (Head.Tag Pages.PathKey)
|
||||
head metadata =
|
||||
case metadata of
|
||||
Metadata.Page meta ->
|
||||
Seo.summaryLarge
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = images.iconPng
|
||||
, alt = "elm-pages logo"
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = siteTagline
|
||||
, locale = Nothing
|
||||
, title = meta.title
|
||||
}
|
||||
|> Seo.website
|
||||
|
||||
Metadata.Article meta ->
|
||||
Seo.summaryLarge
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = meta.image
|
||||
, alt = meta.description
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = meta.description
|
||||
, locale = Nothing
|
||||
, title = meta.title
|
||||
}
|
||||
|> Seo.article
|
||||
{ tags = []
|
||||
, section = Nothing
|
||||
, publishedTime = Just (Date.toIsoString meta.published)
|
||||
, modifiedTime = Nothing
|
||||
, expirationTime = Nothing
|
||||
}
|
||||
|
||||
Metadata.Author meta ->
|
||||
let
|
||||
( firstName, lastName ) =
|
||||
case meta.name |> String.split " " of
|
||||
[ first, last ] ->
|
||||
( first, last )
|
||||
|
||||
[ first, middle, last ] ->
|
||||
( first ++ " " ++ middle, last )
|
||||
|
||||
[] ->
|
||||
( "", "" )
|
||||
|
||||
_ ->
|
||||
( meta.name, "" )
|
||||
in
|
||||
Seo.summary
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = meta.avatar
|
||||
, alt = meta.name ++ "'s elm-pages articles."
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = meta.bio
|
||||
, locale = Nothing
|
||||
, title = meta.name ++ "'s elm-pages articles."
|
||||
}
|
||||
|> Seo.profile
|
||||
{ firstName = firstName
|
||||
, lastName = lastName
|
||||
, username = Nothing
|
||||
}
|
||||
|
||||
Metadata.BlogIndex ->
|
||||
Seo.summaryLarge
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = images.iconPng
|
||||
, alt = "elm-pages logo"
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = siteTagline
|
||||
, locale = Nothing
|
||||
, title = "elm-pages blog"
|
||||
}
|
||||
|> Seo.website
|
||||
|
||||
|
||||
canonicalSiteUrl : String
|
||||
canonicalSiteUrl =
|
||||
"https://elm-pages.com"
|
||||
|
||||
|
||||
siteTagline : String
|
||||
siteTagline =
|
||||
"A statically typed site generator - elm-pages"
|
||||
|
||||
|
||||
publishedDateView metadata =
|
||||
Element.text
|
||||
(metadata.published
|
||||
|> Date.format "MMMM ddd, yyyy"
|
||||
)
|
||||
|
||||
|
||||
githubRepoLink : Element msg
|
||||
githubRepoLink =
|
||||
Element.newTabLink []
|
||||
{ url = "https://github.com/dillonkearns/elm-pages"
|
||||
, label =
|
||||
Element.image
|
||||
[ Element.width (Element.px 22)
|
||||
, Font.color Palette.color.primary
|
||||
]
|
||||
{ src = ImagePath.toString Pages.images.github, description = "Github repo" }
|
||||
}
|
||||
|
||||
|
||||
elmDocsLink : Element msg
|
||||
elmDocsLink =
|
||||
Element.newTabLink []
|
||||
{ url = "https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/"
|
||||
, label =
|
||||
Element.image
|
||||
[ Element.width (Element.px 22)
|
||||
, Font.color Palette.color.primary
|
||||
]
|
||||
{ src = ImagePath.toString Pages.images.elmLogo, description = "Elm Package Docs" }
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
module Metadata exposing (ArticleMetadata, Metadata(..), PageMetadata, decoder)
|
||||
|
||||
import Data.Author
|
||||
import Date exposing (Date)
|
||||
import Dict exposing (Dict)
|
||||
import Element exposing (Element)
|
||||
import Json.Decode as Decode exposing (Decoder)
|
||||
import List.Extra
|
||||
import Pages
|
||||
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
||||
|
||||
|
||||
type Metadata
|
||||
= Page PageMetadata
|
||||
| Article ArticleMetadata
|
||||
| Author Data.Author.Author
|
||||
| BlogIndex
|
||||
|
||||
|
||||
type alias ArticleMetadata =
|
||||
{ title : String
|
||||
, description : String
|
||||
, published : Date
|
||||
, author : Data.Author.Author
|
||||
, image : ImagePath Pages.PathKey
|
||||
, draft : Bool
|
||||
}
|
||||
|
||||
|
||||
type alias PageMetadata =
|
||||
{ title : String }
|
||||
|
||||
|
||||
decoder =
|
||||
Decode.field "type" Decode.string
|
||||
|> Decode.andThen
|
||||
(\pageType ->
|
||||
case pageType of
|
||||
"page" ->
|
||||
Decode.field "title" Decode.string
|
||||
|> Decode.map (\title -> Page { title = title })
|
||||
|
||||
"blog-index" ->
|
||||
Decode.succeed BlogIndex
|
||||
|
||||
"author" ->
|
||||
Decode.map3 Data.Author.Author
|
||||
(Decode.field "name" Decode.string)
|
||||
(Decode.field "avatar" imageDecoder)
|
||||
(Decode.field "bio" Decode.string)
|
||||
|> Decode.map Author
|
||||
|
||||
"blog" ->
|
||||
Decode.map6 ArticleMetadata
|
||||
(Decode.field "title" Decode.string)
|
||||
(Decode.field "description" Decode.string)
|
||||
(Decode.field "published"
|
||||
(Decode.string
|
||||
|> Decode.andThen
|
||||
(\isoString ->
|
||||
case Date.fromIsoString isoString of
|
||||
Ok date ->
|
||||
Decode.succeed date
|
||||
|
||||
Err error ->
|
||||
Decode.fail error
|
||||
)
|
||||
)
|
||||
)
|
||||
(Decode.field "author" Data.Author.decoder)
|
||||
(Decode.field "image" imageDecoder)
|
||||
(Decode.field "draft" Decode.bool
|
||||
|> Decode.maybe
|
||||
|> Decode.map (Maybe.withDefault False)
|
||||
)
|
||||
|> Decode.map Article
|
||||
|
||||
_ ->
|
||||
Decode.fail <| "Unexpected page type " ++ pageType
|
||||
)
|
||||
|
||||
|
||||
imageDecoder : Decoder (ImagePath Pages.PathKey)
|
||||
imageDecoder =
|
||||
Decode.string
|
||||
|> Decode.andThen
|
||||
(\imageAssetPath ->
|
||||
case findMatchingImage imageAssetPath of
|
||||
Nothing ->
|
||||
Decode.fail "Couldn't find image."
|
||||
|
||||
Just imagePath ->
|
||||
Decode.succeed imagePath
|
||||
)
|
||||
|
||||
|
||||
findMatchingImage : String -> Maybe (ImagePath Pages.PathKey)
|
||||
findMatchingImage imageAssetPath =
|
||||
Pages.allImages
|
||||
|> List.Extra.find
|
||||
(\image ->
|
||||
ImagePath.toString image
|
||||
== imageAssetPath
|
||||
)
|
|
@ -0,0 +1,44 @@
|
|||
module Palette exposing (blogHeading, color, heading)
|
||||
|
||||
import Element exposing (Element)
|
||||
import Element.Font as Font
|
||||
import Element.Region
|
||||
|
||||
|
||||
color =
|
||||
{ primary = Element.rgb255 5 117 230
|
||||
, secondary = Element.rgb255 0 242 96
|
||||
}
|
||||
|
||||
|
||||
heading : Int -> List (Element msg) -> Element msg
|
||||
heading level content =
|
||||
Element.paragraph
|
||||
([ Font.bold
|
||||
, Font.family [ Font.typeface "Raleway" ]
|
||||
, Element.Region.heading level
|
||||
]
|
||||
++ (case level of
|
||||
1 ->
|
||||
[ Font.size 36 ]
|
||||
|
||||
2 ->
|
||||
[ Font.size 24 ]
|
||||
|
||||
_ ->
|
||||
[ Font.size 20 ]
|
||||
)
|
||||
)
|
||||
content
|
||||
|
||||
|
||||
blogHeading : String -> Element msg
|
||||
blogHeading title =
|
||||
Element.paragraph
|
||||
[ Font.bold
|
||||
, Font.family [ Font.typeface "Raleway" ]
|
||||
, Element.Region.heading 1
|
||||
, Font.size 36
|
||||
, Font.center
|
||||
]
|
||||
[ Element.text title ]
|