Add starting point for trails demo.

This commit is contained in:
Dillon Kearns 2022-05-17 16:07:07 -07:00
parent c5ad2c5e40
commit 31265b4979
39 changed files with 8962 additions and 0 deletions

View File

@ -0,0 +1,150 @@
module Route.HelloForm exposing (ActionData, Data, Model, Msg, route)
import DataSource exposing (DataSource)
import Effect exposing (Effect)
import ErrorPage exposing (ErrorPage)
import Head
import Head.Seo as Seo
import Html
import Html.Attributes as Attr
import Pages.Msg
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Path exposing (Path)
import RouteBuilder exposing (StatefulRoute, StatelessRoute, StaticPayload)
import Server.Request as Request
import Server.Response as Response exposing (Response)
import Shared
import View exposing (View)
type alias Model =
{}
type Msg
= NoOp
type alias RouteParams =
{}
route : StatefulRoute RouteParams Data ActionData Model Msg
route =
RouteBuilder.serverRender
{ head = head
, data = data
, action = action
}
|> RouteBuilder.buildWithLocalState
{ view = view
, update = update
, subscriptions = subscriptions
, init = init
}
init :
Maybe PageUrl
-> Shared.Model
-> StaticPayload Data ActionData RouteParams
-> ( Model, Effect Msg )
init maybePageUrl sharedModel static =
( {}, Effect.none )
update :
PageUrl
-> Shared.Model
-> StaticPayload Data ActionData RouteParams
-> Msg
-> Model
-> ( Model, Effect Msg )
update pageUrl sharedModel static msg model =
case msg of
NoOp ->
( model, Effect.none )
subscriptions : Maybe PageUrl -> RouteParams -> Path -> Shared.Model -> Model -> Sub Msg
subscriptions maybePageUrl routeParams path sharedModel model =
Sub.none
type alias Data =
{}
type alias ActionData =
{}
data : RouteParams -> Request.Parser (DataSource (Response Data ErrorPage))
data routeParams =
Request.succeed (DataSource.succeed (Response.render Data))
action : RouteParams -> Request.Parser (DataSource (Response ActionData ErrorPage))
action routeParams =
Request.skip "No action."
--Request.expectFormPost
-- (\{ field } ->
-- Request.map
-- (\first ->
-- DataSource.succeed (Response.render {})
-- )
-- (field "first")
-- )
head :
StaticPayload Data ActionData RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = Pages.Url.external "TODO"
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
view :
Maybe PageUrl
-> Shared.Model
-> Model
-> StaticPayload Data ActionData RouteParams
-> View (Pages.Msg.Msg Msg)
view maybeUrl sharedModel model static =
{ title = "Placeholder"
, body =
[ Html.form
[ Attr.method "POST"
]
[ Html.label []
[ Html.text "First "
, Html.input
[ Attr.name "first"
]
[]
]
, Html.input
[ Attr.type_ "submit"
, Attr.value "Sign up"
]
[]
]
]
}

View File

@ -0,0 +1,166 @@
module Route.Search exposing (ActionData, Data, Model, Msg, route)
import DataSource exposing (DataSource)
import Effect exposing (Effect)
import ErrorPage exposing (ErrorPage)
import Head
import Head.Seo as Seo
import Html exposing (Html)
import Html.Attributes as Attr
import Pages.Msg
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Path exposing (Path)
import RouteBuilder exposing (StatefulRoute, StatelessRoute, StaticPayload)
import Server.Request as Request
import Server.Response as Response exposing (Response)
import Shared
import View exposing (View)
type alias Model =
{}
type Msg
= NoOp
type alias RouteParams =
{}
route : StatefulRoute RouteParams Data ActionData Model Msg
route =
RouteBuilder.serverRender
{ head = head
, data = data
, action = action
}
|> RouteBuilder.buildWithLocalState
{ view = view
, update = update
, subscriptions = subscriptions
, init = init
}
init :
Maybe PageUrl
-> Shared.Model
-> StaticPayload Data ActionData RouteParams
-> ( Model, Effect Msg )
init maybePageUrl sharedModel static =
( {}, Effect.none )
update :
PageUrl
-> Shared.Model
-> StaticPayload Data ActionData RouteParams
-> Msg
-> Model
-> ( Model, Effect Msg )
update pageUrl sharedModel static msg model =
case msg of
NoOp ->
( model, Effect.none )
subscriptions : Maybe PageUrl -> RouteParams -> Path -> Shared.Model -> Model -> Sub Msg
subscriptions maybePageUrl routeParams path sharedModel model =
Sub.none
type alias SearchResults =
{ query : String
, results : List String
}
type alias Data =
{ results : Maybe SearchResults
}
type alias ActionData =
{}
data : RouteParams -> Request.Parser (DataSource (Response Data ErrorPage))
data routeParams =
Request.oneOf
[ Request.expectForm
(\{ field, optionalField } ->
field "q"
|> Request.map
(\query ->
DataSource.succeed
(Response.render
{ results =
Just
{ query = query
, results = [ "Hello" ]
}
}
)
)
)
, Request.succeed (DataSource.succeed (Response.render { results = Nothing }))
]
action : RouteParams -> Request.Parser (DataSource (Response ActionData ErrorPage))
action routeParams =
Request.skip "No action."
head :
StaticPayload Data ActionData RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = Pages.Url.external "TODO"
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
view :
Maybe PageUrl
-> Shared.Model
-> Model
-> StaticPayload Data ActionData RouteParams
-> View (Pages.Msg.Msg Msg)
view maybeUrl sharedModel model static =
{ title = "Search"
, body =
[ Html.h2 [] [ Html.text "Search" ]
, Html.form []
[ Html.label []
[ Html.text "Query "
, Html.input [ Attr.name "q" ] []
]
, Html.input [ Attr.type_ "submit", Attr.value "Search" ] []
]
, static.data.results
|> Maybe.map resultsView
|> Maybe.withDefault (Html.div [] [])
]
}
resultsView : SearchResults -> Html msg
resultsView results =
Html.div []
[ Html.h2 [] [ Html.text <| "Results matching " ++ results.query ]
]

8
examples/trails/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
node_modules/
elm-stuff/
dist/
.cache/
.elm-pages/
functions/render/
functions/server-render/
gen/

2
examples/trails/.nvmrc Normal file
View File

@ -0,0 +1,2 @@
v17.2.0

View File

@ -0,0 +1 @@
# README

312
examples/trails/adapter.mjs Normal file
View File

@ -0,0 +1,312 @@
import fs from "fs";
export default async function run({
renderFunctionFilePath,
routePatterns,
apiRoutePatterns,
portsFilePath,
htmlTemplate,
}) {
console.log("Running adapter script");
ensureDirSync("functions/render");
ensureDirSync("functions/server-render");
fs.copyFileSync(
renderFunctionFilePath,
"./functions/render/elm-pages-cli.js"
);
fs.copyFileSync(
renderFunctionFilePath,
"./functions/server-render/elm-pages-cli.js"
);
fs.copyFileSync(portsFilePath, "./functions/render/port-data-source.mjs");
fs.copyFileSync(
portsFilePath,
"./functions/server-render/port-data-source.mjs"
);
fs.writeFileSync(
"./functions/render/index.js",
rendererCode(true, htmlTemplate)
);
fs.writeFileSync(
"./functions/server-render/index.js",
rendererCode(false, htmlTemplate)
);
// TODO rename functions/render to functions/fallback-render
// TODO prepend instead of writing file
const apiServerRoutes = apiRoutePatterns.filter(isServerSide);
ensureValidRoutePatternsForNetlify(apiServerRoutes);
// TODO filter apiRoutePatterns on is server side
// TODO need information on whether api route is odb or serverless
const apiRouteRedirects = apiServerRoutes
.map((apiRoute) => {
if (apiRoute.kind === "prerender-with-fallback") {
return `${apiPatternToRedirectPattern(
apiRoute.pathPattern
)} /.netlify/builders/render 200`;
} else if (apiRoute.kind === "serverless") {
return `${apiPatternToRedirectPattern(
apiRoute.pathPattern
)} /.netlify/functions/server-render 200`;
} else {
throw "Unhandled 2";
}
})
.join("\n");
const redirectsFile =
routePatterns
.filter(isServerSide)
.map((route) => {
if (route.kind === "prerender-with-fallback") {
return `${route.pathPattern} /.netlify/builders/render 200
${route.pathPattern}/content.dat /.netlify/builders/render 200`;
} else {
return `${route.pathPattern} /.netlify/functions/server-render 200
${route.pathPattern}/content.dat /.netlify/functions/server-render 200`;
}
})
.join("\n") +
"\n" +
apiRouteRedirects +
"\n";
fs.writeFileSync("dist/_redirects", redirectsFile);
}
function ensureValidRoutePatternsForNetlify(apiRoutePatterns) {
const invalidNetlifyRoutes = apiRoutePatterns.filter((apiRoute) =>
apiRoute.pathPattern.some(({ kind }) => kind === "hybrid")
);
if (invalidNetlifyRoutes.length > 0) {
throw (
"Invalid Netlify routes!\n" +
invalidNetlifyRoutes
.map((value) => JSON.stringify(value, null, 2))
.join(", ")
);
}
}
function isServerSide(route) {
return (
route.kind === "prerender-with-fallback" || route.kind === "serverless"
);
}
/**
* @param {boolean} isOnDemand
* @param {string} htmlTemplate
*/
function rendererCode(isOnDemand, htmlTemplate) {
return `const path = require("path");
const busboy = require("busboy");
const htmlTemplate = ${JSON.stringify(htmlTemplate)};
${
isOnDemand
? `const { builder } = require("@netlify/functions");
exports.handler = builder(render);`
: `
exports.handler = render;`
}
/**
* @param {import('aws-lambda').APIGatewayProxyEvent} event
* @param {any} context
*/
async function render(event, context) {
const requestTime = new Date();
console.log(JSON.stringify(event));
global.staticHttpCache = {};
const compiledElmPath = path.join(__dirname, "elm-pages-cli.js");
const compiledPortsFile = path.join(__dirname, "port-data-source.mjs");
const renderer = require("../../../../generator/src/render");
const preRenderHtml = require("../../../../generator/src/pre-render-html");
try {
const basePath = "/";
const mode = "build";
const addWatcher = () => {};
const renderResult = await renderer(
compiledPortsFile,
basePath,
require(compiledElmPath),
mode,
event.path,
await reqToJson(event, requestTime),
addWatcher,
false
);
console.log("@@@renderResult", JSON.stringify(renderResult, null, 2));
const statusCode = renderResult.is404 ? 404 : renderResult.statusCode;
if (renderResult.kind === "bytes") {
return {
body: Buffer.from(renderResult.contentDatPayload.buffer).toString("base64"),
isBase64Encoded: true,
headers: {
"Content-Type": "application/octet-stream",
"x-powered-by": "elm-pages",
...renderResult.headers,
},
statusCode,
};
} else if (renderResult.kind === "api-response") {
const serverResponse = renderResult.body;
return {
body: serverResponse.body,
multiValueHeaders: serverResponse.headers,
statusCode: serverResponse.statusCode,
isBase64Encoded: serverResponse.isBase64Encoded,
};
} else {
console.log('@rendering', preRenderHtml.replaceTemplate(htmlTemplate, renderResult.htmlString))
return {
body: preRenderHtml.replaceTemplate(htmlTemplate, renderResult.htmlString),
headers: {
"Content-Type": "text/html",
"x-powered-by": "elm-pages",
...renderResult.headers,
},
statusCode,
};
}
} catch (error) {
console.error(error);
return {
body: \`<body><h1>Error</h1><pre>\${error.toString()}</pre></body>\`,
statusCode: 500,
headers: {
"Content-Type": "text/html",
"x-powered-by": "elm-pages",
},
};
}
}
/**
* @param {import('aws-lambda').APIGatewayProxyEvent} req
* @param {Date} requestTime
* @returns {Promise<{ method: string; hostname: string; query: Record<string, string | undefined>; headers: Record<string, string>; host: string; pathname: string; port: number | null; protocol: string; rawUrl: string; }>}
*/
function reqToJson(req, requestTime) {
return new Promise((resolve, reject) => {
if (
req.httpMethod && req.httpMethod.toUpperCase() === "POST" &&
req.headers["content-type"] &&
req.headers["content-type"].includes("multipart/form-data") &&
req.body
) {
try {
console.log('@@@1');
const bb = busboy({
headers: req.headers,
});
let fields = {};
bb.on("file", (fieldname, file, info) => {
console.log('@@@2');
const { filename, encoding, mimeType } = info;
file.on("data", (data) => {
fields[fieldname] = {
filename,
mimeType,
body: data.toString(),
};
});
});
bb.on("field", (fieldName, value) => {
console.log("@@@field", fieldName, value);
fields[fieldName] = value;
});
// TODO skip parsing JSON and form data body if busboy doesn't run
bb.on("close", () => {
console.log('@@@3');
console.log("@@@close", fields);
resolve(toJsonHelper(req, requestTime, fields));
});
console.log('@@@4');
if (req.isBase64Encoded) {
bb.write(Buffer.from(req.body, 'base64').toString('utf8'));
} else {
bb.write(req.body);
}
} catch (error) {
console.error('@@@5', error);
resolve(toJsonHelper(req, requestTime, null));
}
} else {
console.log('@@@6');
resolve(toJsonHelper(req, requestTime, null));
}
});
}
/**
* @param {import('aws-lambda').APIGatewayProxyEvent} req
* @param {Date} requestTime
* @returns {{method: string; rawUrl: string; body: string?; headers: Record<string, string>; requestTime: number; multiPartFormData: unknown }}
*/
function toJsonHelper(req, requestTime, multiPartFormData) {
return {
method: req.httpMethod,
headers: req.headers,
rawUrl: req.rawUrl,
body: req.body,
requestTime: Math.round(requestTime.getTime()),
multiPartFormData: multiPartFormData,
};
}
`;
}
/**
* @param {fs.PathLike} dirpath
*/
function ensureDirSync(dirpath) {
try {
fs.mkdirSync(dirpath, { recursive: true });
} catch (err) {
if (err.code !== "EEXIST") throw err;
}
}
/** @typedef {{kind: 'dynamic'} | {kind: 'literal', value: string}} ApiSegment */
/**
* @param {ApiSegment[]} pathPattern
*/
function apiPatternToRedirectPattern(pathPattern) {
return (
"/" +
pathPattern
.map((segment, index) => {
switch (segment.kind) {
case "literal": {
return segment.value;
}
case "dynamic": {
return `:dynamic${index}`;
}
default: {
throw "Unhandled segment: " + JSON.stringify(segment);
}
}
})
.join("/")
);
}

262
examples/trails/app/Api.elm Normal file
View File

@ -0,0 +1,262 @@
module Api exposing (routes)
import ApiRoute exposing (ApiRoute)
import DataSource exposing (DataSource)
import DataSource.Http
import Html exposing (Html)
import Json.Decode
import Json.Encode
import MySession
import Pages.Manifest as Manifest
import Route exposing (Route)
import Server.Request
import Server.Response
import Server.Session as Session
import Site
routes :
DataSource (List Route)
-> (Html Never -> String)
-> List (ApiRoute.ApiRoute ApiRoute.Response)
routes getStaticRoutes htmlToString =
[ --nonHybridRoute
--, noArgs
redirectRoute
--, repoStars
--, repoStars2
, logout
, greet
, fileLength
, DataSource.succeed manifest |> Manifest.generator Site.canonicalUrl
]
greet : ApiRoute ApiRoute.Response
greet =
ApiRoute.succeed
(Server.Request.oneOf
[ Server.Request.expectFormPost
(\{ field, optionalField } ->
field "first"
)
, Server.Request.expectJsonBody (Json.Decode.field "first" Json.Decode.string)
, Server.Request.expectQueryParam "first"
, Server.Request.expectMultiPartFormPost
(\{ field, optionalField } ->
field "first"
)
]
|> Server.Request.map
(\firstName ->
Server.Response.plainText ("Hello " ++ firstName)
|> DataSource.succeed
)
)
|> ApiRoute.literal "api"
|> ApiRoute.slash
|> ApiRoute.literal "greet"
|> ApiRoute.serverRender
fileLength : ApiRoute ApiRoute.Response
fileLength =
ApiRoute.succeed
(Server.Request.expectMultiPartFormPost
(\{ field, optionalField, fileField } ->
fileField "file"
)
|> Server.Request.map
(\file ->
Server.Response.json
(Json.Encode.object
[ ( "File name: ", Json.Encode.string file.name )
, ( "Length", Json.Encode.int (String.length file.body) )
, ( "mime-type", Json.Encode.string file.mimeType )
, ( "First line"
, Json.Encode.string
(file.body
|> String.split "\n"
|> List.head
|> Maybe.withDefault ""
)
)
]
)
|> DataSource.succeed
)
)
|> ApiRoute.literal "api"
|> ApiRoute.slash
|> ApiRoute.literal "file"
|> ApiRoute.serverRender
redirectRoute : ApiRoute ApiRoute.Response
redirectRoute =
ApiRoute.succeed
(Server.Request.succeed
(DataSource.succeed
(Route.redirectTo Route.Index)
)
)
|> ApiRoute.literal "api"
|> ApiRoute.slash
|> ApiRoute.literal "redirect"
|> ApiRoute.serverRender
noArgs : ApiRoute ApiRoute.Response
noArgs =
ApiRoute.succeed
(Server.Request.succeed
(DataSource.Http.get
"https://api.github.com/repos/dillonkearns/elm-pages"
(Json.Decode.field "stargazers_count" Json.Decode.int)
|> DataSource.map
(\stars ->
Json.Encode.object
[ ( "repo", Json.Encode.string "elm-pages" )
, ( "stars", Json.Encode.int stars )
]
|> Server.Response.json
)
)
)
|> ApiRoute.literal "api"
|> ApiRoute.slash
|> ApiRoute.literal "stars"
|> ApiRoute.serverRender
nonHybridRoute =
ApiRoute.succeed
(\repoName ->
DataSource.Http.get
("https://api.github.com/repos/dillonkearns/" ++ repoName)
(Json.Decode.field "stargazers_count" Json.Decode.int)
|> DataSource.map
(\stars ->
Json.Encode.object
[ ( "repo", Json.Encode.string repoName )
, ( "stars", Json.Encode.int stars )
]
|> Json.Encode.encode 2
)
)
|> ApiRoute.literal "repo"
|> ApiRoute.slash
|> ApiRoute.capture
|> ApiRoute.preRender
(\route ->
DataSource.succeed
[ route "elm-graphql"
]
)
logout : ApiRoute ApiRoute.Response
logout =
ApiRoute.succeed
(MySession.withSession
(Server.Request.succeed ())
(\() sessionResult ->
DataSource.succeed
( Session.empty
, Route.redirectTo Route.Login
)
)
)
|> ApiRoute.literal "api"
|> ApiRoute.slash
|> ApiRoute.literal "logout"
|> ApiRoute.serverRender
repoStars : ApiRoute ApiRoute.Response
repoStars =
ApiRoute.succeed
(\repoName ->
Server.Request.succeed
(DataSource.Http.get
("https://api.github.com/repos/dillonkearns/" ++ repoName)
(Json.Decode.field "stargazers_count" Json.Decode.int)
|> DataSource.map
(\stars ->
Json.Encode.object
[ ( "repo", Json.Encode.string repoName )
, ( "stars", Json.Encode.int stars )
]
|> Server.Response.json
)
)
)
|> ApiRoute.literal "api"
|> ApiRoute.slash
|> ApiRoute.literal "repo"
|> ApiRoute.slash
|> ApiRoute.capture
--|> ApiRoute.literal ".json"
|> ApiRoute.serverRender
repoStars2 : ApiRoute ApiRoute.Response
repoStars2 =
ApiRoute.succeed
(\repoName ->
DataSource.Http.get
("https://api.github.com/repos/dillonkearns/" ++ repoName)
(Json.Decode.field "stargazers_count" Json.Decode.int)
|> DataSource.map
(\stars ->
Json.Encode.object
[ ( "repo", Json.Encode.string repoName )
, ( "stars", Json.Encode.int stars )
]
|> Server.Response.json
)
)
|> ApiRoute.literal "api2"
|> ApiRoute.slash
|> ApiRoute.literal "repo"
|> ApiRoute.slash
|> ApiRoute.capture
|> ApiRoute.preRenderWithFallback
(\route ->
DataSource.succeed
[ route "elm-graphql"
, route "elm-pages"
]
)
route1 =
ApiRoute.succeed
(\repoName ->
DataSource.Http.get
("https://api.github.com/repos/dillonkearns/" ++ repoName)
(Json.Decode.field "stargazers_count" Json.Decode.int)
|> DataSource.map
(\stars ->
Json.Encode.object
[ ( "repo", Json.Encode.string repoName )
, ( "stars", Json.Encode.int stars )
]
|> Json.Encode.encode 2
)
)
|> ApiRoute.literal "repo"
|> ApiRoute.slash
|> ApiRoute.capture
|> ApiRoute.literal ".json"
manifest : Manifest.Config
manifest =
Manifest.init
{ name = "Site Name"
, description = "Description"
, startUrl = Route.Index |> Route.toPath
, icons = []
}

View File

@ -0,0 +1,147 @@
module Effect exposing (Effect(..), batch, fromCmd, map, none, perform)
import Browser.Navigation
import Bytes exposing (Bytes)
import Bytes.Decode
import FormDecoder
import Http
import Json.Decode as Decode
import Pages.Fetcher
import Url exposing (Url)
type Effect msg
= None
| Cmd (Cmd msg)
| Batch (List (Effect msg))
| GetStargazers (Result Http.Error Int -> msg)
| FetchRouteData
{ body : Maybe { contentType : String, body : String }
, path : Maybe String
, toMsg : Result Http.Error Url -> msg
}
| Submit
{ values : FormDecoder.FormData
, path : Maybe (List String)
, method : Maybe String
, toMsg : Result Http.Error Url -> msg
}
| SubmitFetcher (Pages.Fetcher.Fetcher msg)
type alias RequestInfo =
{ contentType : String
, body : String
}
none : Effect msg
none =
None
batch : List (Effect msg) -> Effect msg
batch =
Batch
fromCmd : Cmd msg -> Effect msg
fromCmd =
Cmd
map : (a -> b) -> Effect a -> Effect b
map fn effect =
case effect of
None ->
None
Cmd cmd ->
Cmd (Cmd.map fn cmd)
Batch list ->
Batch (List.map (map fn) list)
GetStargazers toMsg ->
GetStargazers (toMsg >> fn)
FetchRouteData fetchInfo ->
FetchRouteData
{ body = fetchInfo.body
, path = fetchInfo.path
, toMsg = fetchInfo.toMsg >> fn
}
Submit fetchInfo ->
Submit
{ values = fetchInfo.values
, path = fetchInfo.path
, method = fetchInfo.method
, toMsg = fetchInfo.toMsg >> fn
}
SubmitFetcher fetcher ->
fetcher
|> Pages.Fetcher.map fn
|> SubmitFetcher
perform :
{ fetchRouteData :
{ body : Maybe { contentType : String, body : String }
, path : Maybe String
, toMsg : Result Http.Error Url -> pageMsg
}
-> Cmd msg
, submit :
{ values : FormDecoder.FormData
, encType : Maybe String
, method : Maybe String
, path : Maybe String
, toMsg : Result Http.Error Url -> pageMsg
}
-> Cmd msg
, runFetcher :
Pages.Fetcher.Fetcher pageMsg
-> Cmd msg
, fromPageMsg : pageMsg -> msg
, key : Browser.Navigation.Key
}
-> Effect pageMsg
-> Cmd msg
perform ({ fromPageMsg, key } as helpers) effect =
case effect of
None ->
Cmd.none
Cmd cmd ->
Cmd.map fromPageMsg cmd
Batch list ->
Cmd.batch (List.map (perform helpers) list)
GetStargazers toMsg ->
Http.get
{ url =
"https://api.github.com/repos/dillonkearns/elm-pages"
, expect = Http.expectJson (toMsg >> fromPageMsg) (Decode.field "stargazers_count" Decode.int)
}
FetchRouteData fetchInfo ->
helpers.fetchRouteData
{ body = fetchInfo.body
, path = fetchInfo.path
, toMsg = fetchInfo.toMsg
}
Submit record ->
helpers.submit
{ values = record.values
, path = Nothing --fetchInfo.path
, method = record.method
, encType = Nothing -- TODO
, toMsg = record.toMsg
}
SubmitFetcher record ->
helpers.runFetcher record

View File

@ -0,0 +1,74 @@
module ErrorPage exposing (ErrorPage(..), Model, Msg, head, init, internalError, notFound, statusCode, update, view)
import Effect exposing (Effect)
import Head
import Html exposing (Html)
import Html.Events exposing (onClick)
import View exposing (View)
type Msg
= Increment
type alias Model =
{ count : Int
}
init : ErrorPage -> ( Model, Effect Msg )
init errorPage =
( { count = 0 }
, Effect.none
)
update : ErrorPage -> Msg -> Model -> ( Model, Effect Msg )
update errorPage msg model =
case msg of
Increment ->
( { model | count = model.count + 1 }, Effect.none )
head : ErrorPage -> List Head.Tag
head errorPage =
[]
type ErrorPage
= NotFound
| InternalError String
notFound : ErrorPage
notFound =
NotFound
internalError : String -> ErrorPage
internalError =
InternalError
view : ErrorPage -> Model -> View Msg
view error model =
case error of
_ ->
{ body =
[ Html.div []
[ Html.p [] [ Html.text "Looks like you've wandered off the trail. Try finding a new path." ]
, Html.div [] []
]
]
, title = "This is a NotFound Error"
}
statusCode : ErrorPage -> number
statusCode error =
case error of
NotFound ->
404
InternalError _ ->
500

View File

@ -0,0 +1,83 @@
module Route.Index exposing (ActionData, Data, Model, Msg, route)
import DataSource exposing (DataSource)
import DataSource.Env as Env
import DataSource.Http
import Head
import Head.Seo as Seo
import Html exposing (..)
import Json.Decode as Decode exposing (Decoder)
import Json.Encode
import Pages.Msg
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Route
import RouteBuilder exposing (StatelessRoute, StaticPayload)
import Shared
import View exposing (View)
type alias Model =
{}
type alias Msg =
()
type alias RouteParams =
{}
route : StatelessRoute RouteParams Data ActionData
route =
RouteBuilder.single
{ head = head
, data = data
}
|> RouteBuilder.buildNoState { view = view }
type alias Data =
{}
type alias ActionData =
{}
data : DataSource Data
data =
DataSource.succeed {}
head :
StaticPayload Data RouteParams ActionData
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages Pokedex"
, image =
{ url = Pages.Url.external ""
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "This is a simple app to showcase server-rendering with elm-pages."
, locale = Nothing
, title = "Elm Pages Pokedex Example"
}
|> Seo.website
view :
Maybe PageUrl
-> Shared.Model
-> StaticPayload Data RouteParams ActionData
-> View (Pages.Msg.Msg Msg)
view maybeUrl sharedModel static =
{ title = "Pokedex"
, body =
[]
}

View File

@ -0,0 +1,147 @@
module Route.Login exposing (ActionData, Data, Model, Msg, route)
import DataSource exposing (DataSource)
import Dict exposing (Dict)
import ErrorPage exposing (ErrorPage)
import Head
import Head.Seo as Seo
import Html
import Html.Attributes as Attr
import MySession
import Pages.Msg
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Route
import RouteBuilder exposing (StatefulRoute, StatelessRoute, StaticPayload)
import Server.Request as Request
import Server.Response exposing (Response)
import Server.Session as Session
import Shared
import View exposing (View)
type alias Model =
{}
type alias Msg =
()
type alias RouteParams =
{}
route : StatelessRoute RouteParams Data ActionData
route =
RouteBuilder.serverRender
{ head = head
, data = data
, action = \_ -> Request.skip "No action."
}
|> RouteBuilder.buildNoState { view = view }
type alias Request =
{ cookies : Dict String String
, maybeFormData : Maybe (Dict String ( String, List String ))
}
data : RouteParams -> Request.Parser (DataSource (Response Data ErrorPage))
data routeParams =
Request.oneOf
[ MySession.withSession
(Request.expectFormPost (\{ field } -> field "name"))
(\name session ->
( session
|> Result.withDefault Nothing
|> Maybe.withDefault Session.empty
|> Session.insert "name" name
|> Session.withFlash "message" ("Welcome " ++ name ++ "!")
, Route.redirectTo Route.Index
)
|> DataSource.succeed
)
, MySession.withSession
(Request.succeed ())
(\() session ->
case session of
Ok (Just okSession) ->
( okSession
, okSession
|> Session.get "name"
|> Data
|> Server.Response.render
)
|> DataSource.succeed
_ ->
( Session.empty
, { username = Nothing }
|> Server.Response.render
)
|> DataSource.succeed
)
]
head :
StaticPayload Data ActionData RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = Pages.Url.external "TODO"
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
type alias Data =
{ username : Maybe String
}
type alias ActionData =
{}
view :
Maybe PageUrl
-> Shared.Model
-> StaticPayload Data ActionData RouteParams
-> View (Pages.Msg.Msg Msg)
view maybeUrl sharedModel static =
{ title = "Login"
, body =
[ Html.p []
[ Html.text
(case static.data.username of
Just username ->
"Hello " ++ username ++ "!"
Nothing ->
"You aren't logged in yet."
)
]
, Html.form
[ Attr.method "post"
, Attr.action "/login"
]
[ Html.label [] [ Html.input [ Attr.name "name", Attr.type_ "text" ] [] ]
, Html.button
[ Attr.type_ "submit"
]
[ Html.text "Login" ]
]
]
}

View File

@ -0,0 +1,166 @@
module Route.Search exposing (ActionData, Data, Model, Msg, route)
import DataSource exposing (DataSource)
import Effect exposing (Effect)
import ErrorPage exposing (ErrorPage)
import Head
import Head.Seo as Seo
import Html exposing (Html)
import Html.Attributes as Attr
import Pages.Msg
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Path exposing (Path)
import RouteBuilder exposing (StatefulRoute, StatelessRoute, StaticPayload)
import Server.Request as Request
import Server.Response as Response exposing (Response)
import Shared
import View exposing (View)
type alias Model =
{}
type Msg
= NoOp
type alias RouteParams =
{}
route : StatefulRoute RouteParams Data ActionData Model Msg
route =
RouteBuilder.serverRender
{ head = head
, data = data
, action = action
}
|> RouteBuilder.buildWithLocalState
{ view = view
, update = update
, subscriptions = subscriptions
, init = init
}
init :
Maybe PageUrl
-> Shared.Model
-> StaticPayload Data ActionData RouteParams
-> ( Model, Effect Msg )
init maybePageUrl sharedModel static =
( {}, Effect.none )
update :
PageUrl
-> Shared.Model
-> StaticPayload Data ActionData RouteParams
-> Msg
-> Model
-> ( Model, Effect Msg )
update pageUrl sharedModel static msg model =
case msg of
NoOp ->
( model, Effect.none )
subscriptions : Maybe PageUrl -> RouteParams -> Path -> Shared.Model -> Model -> Sub Msg
subscriptions maybePageUrl routeParams path sharedModel model =
Sub.none
type alias SearchResults =
{ query : String
, results : List String
}
type alias Data =
{ results : Maybe SearchResults
}
type alias ActionData =
{}
data : RouteParams -> Request.Parser (DataSource (Response Data ErrorPage))
data routeParams =
Request.oneOf
[ Request.expectForm
(\{ field, optionalField } ->
field "q"
|> Request.map
(\query ->
DataSource.succeed
(Response.render
{ results =
Just
{ query = query
, results = [ "Hello" ]
}
}
)
)
)
, Request.succeed (DataSource.succeed (Response.render { results = Nothing }))
]
action : RouteParams -> Request.Parser (DataSource (Response ActionData ErrorPage))
action routeParams =
Request.skip "No action."
head :
StaticPayload Data ActionData RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = Pages.Url.external "TODO"
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
view :
Maybe PageUrl
-> Shared.Model
-> Model
-> StaticPayload Data ActionData RouteParams
-> View (Pages.Msg.Msg Msg)
view maybeUrl sharedModel model static =
{ title = "Search"
, body =
[ Html.h2 [] [ Html.text "Search" ]
, Html.form []
[ Html.label []
[ Html.text "Query "
, Html.input [ Attr.name "q" ] []
]
, Html.input [ Attr.type_ "submit", Attr.value "Search" ] []
]
, static.data.results
|> Maybe.map resultsView
|> Maybe.withDefault (Html.div [] [])
]
}
resultsView : SearchResults -> Html msg
resultsView results =
Html.div []
[ Html.h2 [] [ Html.text <| "Results matching " ++ results.query ]
]

View File

@ -0,0 +1,226 @@
module Route.Signup exposing (ActionData, Data, Model, Msg, route)
import DataSource exposing (DataSource)
import Dict
import Effect exposing (Effect)
import ErrorPage exposing (ErrorPage)
import Head
import Head.Seo as Seo
import Html exposing (Html)
import Html.Attributes as Attr
import Http
import MySession
import Pages.Msg
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Path exposing (Path)
import Route
import RouteBuilder exposing (StatefulRoute, StatelessRoute, StaticPayload)
import Server.Request as Request
import Server.Response as Response exposing (Response)
import Server.Session as Session exposing (Session)
import Shared
import View exposing (View)
type alias Model =
{}
type Msg
= NoOp
| GotResponse (Result Http.Error ActionData)
type alias RouteParams =
{}
route : StatefulRoute RouteParams Data ActionData Model Msg
route =
RouteBuilder.serverRender
{ head = head
, data = data
, action = action
}
|> RouteBuilder.buildWithLocalState
{ view = view
, update = update
, subscriptions = subscriptions
, init = init
}
action : RouteParams -> Request.Parser (DataSource (Response ActionData ErrorPage))
action _ =
MySession.withSession
(Request.expectFormPost
(\{ field } ->
Request.map2 Tuple.pair
(field "first")
(field "email")
)
)
(\( first, email ) maybeSession ->
let
session : Session
session =
maybeSession |> Result.toMaybe |> Maybe.andThen identity |> Maybe.withDefault Session.empty
in
validate session
{ email = email
, first = first
}
|> DataSource.succeed
)
validate : Session -> { first : String, email : String } -> ( Session, Response ActionData ErrorPage )
validate session { first, email } =
if first /= "" && email /= "" then
( session
|> Session.withFlash "message" ("Success! You're all signed up " ++ first)
, Route.redirectTo Route.Signup
)
else
( session
, ValidationErrors
{ errors = [ "Cannot be blank?" ]
, fields =
[ ( "first", first )
, ( "email", email )
]
}
|> Response.render
)
init :
Maybe PageUrl
-> Shared.Model
-> StaticPayload Data ActionData RouteParams
-> ( Model, Effect Msg )
init maybePageUrl sharedModel static =
( {}
, Effect.none
)
update :
PageUrl
-> Shared.Model
-> StaticPayload Data ActionData RouteParams
-> Msg
-> Model
-> ( Model, Effect Msg )
update pageUrl sharedModel static msg model =
case msg of
NoOp ->
( model, Effect.none )
GotResponse result ->
let
_ =
Debug.log "GotResponse" result
in
( model, Effect.none )
subscriptions : Maybe PageUrl -> RouteParams -> Path -> Shared.Model -> Model -> Sub Msg
subscriptions maybePageUrl routeParams path sharedModel model =
Sub.none
type alias Data =
{ flashMessage : Maybe (Result String String)
}
type ActionData
= Success { email : String, first : String }
| ValidationErrors
{ errors : List String
, fields : List ( String, String )
}
data : RouteParams -> Request.Parser (DataSource (Response Data ErrorPage))
data routeParams =
MySession.withSession
(Request.succeed ())
(\() sessionResult ->
let
session : Session
session =
sessionResult |> Result.toMaybe |> Maybe.andThen identity |> Maybe.withDefault Session.empty
flashMessage : Maybe String
flashMessage =
session |> Session.get "message"
in
( Session.empty
, Response.render
{ flashMessage = flashMessage |> Maybe.map Ok }
)
|> DataSource.succeed
)
head :
StaticPayload Data ActionData RouteParams
-> List Head.Tag
head static =
[]
view :
Maybe PageUrl
-> Shared.Model
-> Model
-> StaticPayload Data ActionData RouteParams
-> View (Pages.Msg.Msg Msg)
view maybeUrl sharedModel model static =
{ title = "Signup"
, body =
[ Html.p []
[ case static.action of
Just (Success { email, first }) ->
Html.text <| "Hello " ++ first ++ "!"
Just (ValidationErrors { errors }) ->
errors
|> List.map (\error -> Html.li [] [ Html.text error ])
|> Html.ul []
_ ->
Html.text ""
]
, flashView static.data.flashMessage
, Html.form
[ Attr.method "POST"
]
[ Html.label [] [ Html.text "First", Html.input [ Attr.name "first" ] [] ]
, Html.label [] [ Html.text "Email", Html.input [ Attr.name "email" ] [] ]
, Html.input [ Attr.type_ "submit", Attr.value "Signup" ] []
]
]
}
flashView : Maybe (Result String String) -> Html msg
flashView message =
Html.p
[ Attr.style "background-color" "rgb(163 251 163)"
]
[ Html.text <|
case message of
Nothing ->
""
Just (Ok okMessage) ->
okMessage
Just (Err error) ->
"Something went wrong: " ++ error
]

View File

@ -0,0 +1,116 @@
module Shared exposing (Data, Model, Msg(..), SharedMsg(..), template)
import DataSource
import Effect exposing (Effect)
import Html exposing (Html)
import Html.Attributes as Attr
import Pages.Flags
import Pages.PageUrl exposing (PageUrl)
import Path exposing (Path)
import Route exposing (Route)
import SharedTemplate exposing (SharedTemplate)
import View exposing (View)
template : SharedTemplate Msg Model Data msg
template =
{ init = init
, update = update
, view = view
, data = data
, subscriptions = subscriptions
, onPageChange = Just OnPageChange
}
type Msg
= OnPageChange
{ path : Path
, query : Maybe String
, fragment : Maybe String
}
| SharedMsg SharedMsg
type alias Data =
()
type SharedMsg
= NoOp
type alias Model =
{ showMobileMenu : Bool
}
init :
Pages.Flags.Flags
->
Maybe
{ path :
{ path : Path
, query : Maybe String
, fragment : Maybe String
}
, metadata : route
, pageUrl : Maybe PageUrl
}
-> ( Model, Effect Msg )
init flags maybePagePath =
( { showMobileMenu = False }
, Effect.none
)
update : Msg -> Model -> ( Model, Effect Msg )
update msg model =
case msg of
OnPageChange _ ->
( { model | showMobileMenu = False }, Effect.none )
SharedMsg globalMsg ->
( model, Effect.none )
subscriptions : Path -> Model -> Sub Msg
subscriptions _ _ =
Sub.none
data : DataSource.DataSource Data
data =
DataSource.succeed ()
view :
Data
->
{ path : Path
, route : Maybe Route
}
-> Model
-> (Msg -> msg)
-> View msg
-> { body : Html msg, title : String }
view sharedData page model toMsg pageView =
{ body =
Html.div
[]
[ Html.nav
[ Attr.style "display" "flex"
, Attr.style "justify-content" "space-evenly"
]
[ Route.Index
|> Route.link
[]
[ Html.text "Home" ]
]
, Html.div
[ Attr.style "padding" "40px"
]
pageView.body
]
, title = pageView.title
}

View File

@ -0,0 +1,46 @@
module Site exposing (canonicalUrl, config)
import DataSource exposing (DataSource)
import Head
import Route exposing (Route)
import SiteConfig exposing (SiteConfig)
import Sitemap
type alias Data =
()
config : SiteConfig
config =
{ canonicalUrl = canonicalUrl
, head = head
}
canonicalUrl : String
canonicalUrl =
"https://elm-pages.com"
head : DataSource (List Head.Tag)
head =
[ Head.sitemapLink "/sitemap.xml"
]
|> DataSource.succeed
siteMap :
List (Maybe Route)
-> { path : List String, content : String }
siteMap allRoutes =
allRoutes
|> List.filterMap identity
|> List.map
(\route ->
{ path = Route.routeToPath route |> String.join "/"
, lastMod = Nothing
}
)
|> Sitemap.build { siteUrl = "https://elm-pages.com" }
|> (\sitemapXmlString -> { path = [ "sitemap.xml" ], content = sitemapXmlString })

View File

@ -0,0 +1,23 @@
module View exposing (View, map, placeholder)
import Html exposing (Html)
type alias View msg =
{ title : String
, body : List (Html msg)
}
map : (msg1 -> msg2) -> View msg1 -> View msg2
map fn doc =
{ title = doc.title
, body = List.map (Html.map fn) doc.body
}
placeholder : String -> View msg
placeholder moduleName =
{ title = "Placeholder - " ++ moduleName
, body = [ Html.text moduleName ]
}

View File

@ -0,0 +1,8 @@
{
"name": "dmy/elm-doc-preview",
"summary": "Offline documentation previewer",
"version": "5.0.0",
"exposed-modules": [
"Page"
]
}

View File

@ -0,0 +1,8 @@
import { defineConfig } from "vite";
import adapter from "./adapter.mjs";
export default {
vite: defineConfig({}),
adapter,
};

View File

@ -0,0 +1,8 @@
{
"tools": {
"elm": "0.19.1",
"elm-format": "0.8.4",
"elm-json": "0.2.10",
"elm-test-rs": "1.0.0"
}
}

66
examples/trails/elm.json Normal file
View File

@ -0,0 +1,66 @@
{
"type": "application",
"source-directories": [
"src",
"app",
"../../src",
".elm-pages",
"../../plugins",
"gen"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"MartinSStewart/elm-serialize": "1.2.5",
"avh4/elm-color": "1.0.0",
"danfishgold/base64-bytes": "1.1.0",
"danyx23/elm-mimetype": "4.0.1",
"dillonkearns/elm-bcp47-language-tag": "1.0.1",
"dillonkearns/elm-graphql": "5.0.9",
"dillonkearns/elm-markdown": "6.0.1",
"dillonkearns/elm-sitemap": "1.0.1",
"elm/browser": "1.0.2",
"elm/bytes": "1.0.8",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/parser": "1.1.0",
"elm/regex": "1.0.0",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.2",
"elm-community/dict-extra": "2.4.0",
"elm-community/list-extra": "8.3.0",
"jluckyiv/elm-utc-date-strings": "1.0.0",
"justinmimbs/date": "4.0.0",
"lamdera/codecs": "1.0.0",
"lamdera/core": "1.0.0",
"miniBill/elm-codec": "1.2.0",
"noahzgordon/elm-color-extra": "1.0.2",
"pablohirafuji/elm-syntax-highlight": "3.4.0",
"robinheghan/fnv1a": "1.0.0",
"robinheghan/murmur3": "1.0.0",
"rtfeldman/elm-css": "16.1.1",
"tripokey/elm-fuzzy": "5.2.1",
"turboMaCk/non-empty-list-alias": "1.2.0",
"vito/elm-ansi": "10.0.1",
"zwilias/json-decode-exploration": "6.0.0"
},
"indirect": {
"bburdette/toop": "1.0.1",
"billstclair/elm-xml-eeue56": "1.0.3",
"elm/file": "1.0.5",
"elm/random": "1.0.0",
"fredcy/elm-parseint": "2.0.1",
"j-maas/elm-ordered-containers": "1.0.0",
"lukewestby/elm-string-interpolate": "1.0.4",
"mgold/elm-nonempty-list": "4.2.0",
"rtfeldman/elm-hex": "1.0.0"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -0,0 +1,14 @@
exports.handler =
/**
* @param {import('aws-lambda').APIGatewayProxyEvent} event
* @param {any} context
*/
async function (event, context) {
return {
body: JSON.stringify(new Date().toTimeString()),
headers: {
"Content-Type": "application/json",
},
statusCode: 200,
};
};

6
examples/trails/index.js Normal file
View File

@ -0,0 +1,6 @@
export default {
load: function (elmLoaded) {},
flags: function () {
return null;
},
};

View File

@ -0,0 +1,13 @@
[build]
functions = "functions/"
publish = "dist/"
command = "mkdir bin && export PATH=\"/opt/build/repo/examples/pokedex/bin:$PATH\" && echo $PATH && curl https://static.lamdera.com/bin/linux/lamdera -o bin/lamdera && chmod a+x bin/lamdera && export ELM_HOME=\"$NETLIFY_BUILD_BASE/cache/elm\" && (cd ../../ && npm install --no-optional && npx --no-install elm-tooling install) && npm install && npm run generate:tailwind && npm run generate:graphql && npm run build && cp secret-note.txt functions/server-render/"
[dev]
command = "npm start"
targetPort = 1234
autoLaunch = true
framework = "#custom"
[functions]
included_files = ["content/**"]

6233
examples/trails/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"name": "elm-pages-example",
"version": "1.0.0",
"description": "Example site built with elm-pages.",
"scripts": {
"start": "elm-pages dev",
"serve": "npm run build && http-server ./dist -a localhost -p 3000 -c-1",
"build": "elm-pages build --debug --keep-cache",
"generate:tailwind": "elm-tailwind-modules --dir ./gen --tailwind-config tailwind.config.js",
"generate:graphql": "elm-graphql https://striking-mutt-82.hasura.app/v1/graphql --header 'x-hasura-admin-secret: $TRAILS_HASURA_SECRET' --output gen"
},
"author": "Dillon Kearns",
"license": "BSD-3",
"devDependencies": {
"@dillonkearns/elm-graphql": "^4.2.3",
"@netlify/functions": "^0.7.2",
"@tailwindcss/forms": "^0.3.4",
"busboy": "^1.1.0",
"elm-pages": "file:../..",
"elm-review": "^2.7.0",
"elm-tailwind-modules": "^0.3.2",
"elm-tooling": "^1.3.0",
"postcss": "^8.4.5",
"tailwindcss": "^2.2.19"
}
}

View File

@ -0,0 +1,23 @@
import kleur from "kleur";
kleur.enabled = true;
export async function environmentVariable(name) {
const result = process.env[name];
if (result) {
return result;
} else {
throw `No environment variable called ${kleur
.yellow()
.underline(name)}\n\nAvailable:\n\n${Object.keys(process.env)
.slice(0, 5)
.join("\n")}`;
}
}
export async function hello(name) {
return `147 ${name}!!`;
}
function waitFor(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

View File

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

View File

@ -0,0 +1,43 @@
pre.elmsh {
padding: 10px;
margin: 0;
text-align: left;
overflow: auto;
padding: 20px !important;
}
code.elmsh {
padding: 0;
}
code {
font-family: 'Roboto Mono' !important;
font-size: 20px !important;
line-height: 28px;
}
.elmsh-line:before {
/* content: attr(data-elmsh-lc); */
display: inline-block;
text-align: right;
width: 40px;
padding: 0 20px 0 0;
opacity: 0.3;
}
.elmsh {
color: #f8f8f2;
background: #000;
}
.elmsh-hl {background: #343434;}
.elmsh-add {background: #003800;}
.elmsh-del {background: #380000;}
.elmsh-comm {color: #75715e;}
.elmsh1 {color: #ae81ff;}
.elmsh2 {color: #e6db74;}
.elmsh3 {color: #66d9ef;}
.elmsh4 {color: #f92672;}
.elmsh5 {color: #a6e22e;}
.elmsh6 {color: #ae81ff;}
.elmsh7 {color: #fd971f;}

View File

@ -0,0 +1,65 @@
module Icon exposing (error, icon2, icon3)
import Html.Styled exposing (Html)
import Svg.Styled exposing (path, svg)
import Svg.Styled.Attributes as SvgAttr
import Tailwind.Utilities as Tw
error : Html msg
error =
svg
[ SvgAttr.css
[ Tw.h_5
, Tw.w_5
, Tw.text_red_500
]
, SvgAttr.viewBox "0 0 20 20"
, SvgAttr.fill "currentColor"
]
[ path
[ SvgAttr.fillRule "evenodd"
, SvgAttr.d "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
, SvgAttr.clipRule "evenodd"
]
[]
]
icon2 =
svg
[ SvgAttr.css
[ Tw.h_full
, Tw.w_full
, Tw.text_gray_300
]
, SvgAttr.fill "currentColor"
, SvgAttr.viewBox "0 0 24 24"
]
[ path
[ SvgAttr.d "M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"
]
[]
]
icon3 =
svg
[ SvgAttr.css
[ Tw.mx_auto
, Tw.h_12
, Tw.w_12
, Tw.text_gray_400
]
, SvgAttr.stroke "currentColor"
, SvgAttr.fill "none"
, SvgAttr.viewBox "0 0 48 48"
]
[ path
[ SvgAttr.d "M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
, SvgAttr.strokeWidth "2"
, SvgAttr.strokeLinecap "round"
, SvgAttr.strokeLinejoin "round"
]
[]
]

View File

@ -0,0 +1,233 @@
module MarkdownRenderer exposing (renderer)
import Html.Styled as Html
import Html.Styled.Attributes as Attr exposing (css)
import Markdown.Block as Block exposing (ListItem(..), Task(..))
import Markdown.Html
import Markdown.Renderer
import SyntaxHighlight
import Tailwind.Utilities as Tw
renderer : Markdown.Renderer.Renderer (Html.Html msg)
renderer =
{ heading = heading
, paragraph = Html.p []
, thematicBreak = Html.hr [] []
, text = Html.text
, strong = \content -> Html.strong [ css [ Tw.font_bold ] ] content
, emphasis = \content -> Html.em [ css [ Tw.italic ] ] content
, blockQuote = Html.blockquote []
, codeSpan =
\content ->
Html.code
[ css
[ Tw.font_semibold
, Tw.font_medium
]
]
[ Html.text content ]
--, codeSpan = code
, link =
\{ destination } body ->
Html.a
[ Attr.href destination
, css
[ Tw.underline
]
]
body
, hardLineBreak = Html.br [] []
, image =
\image ->
case image.title of
Just _ ->
Html.img [ Attr.src image.src, Attr.alt image.alt ] []
Nothing ->
Html.img [ Attr.src image.src, Attr.alt image.alt ] []
, unorderedList =
\items ->
Html.ul []
(items
|> List.map
(\item ->
case item of
Block.ListItem task children ->
let
checkbox =
case task of
Block.NoTask ->
Html.text ""
Block.IncompleteTask ->
Html.input
[ Attr.disabled True
, Attr.checked False
, Attr.type_ "checkbox"
]
[]
Block.CompletedTask ->
Html.input
[ Attr.disabled True
, Attr.checked True
, Attr.type_ "checkbox"
]
[]
in
Html.li [] (checkbox :: children)
)
)
, orderedList =
\startingIndex items ->
Html.ol
(case startingIndex of
1 ->
[ Attr.start startingIndex ]
_ ->
[]
)
(items
|> List.map
(\itemBlocks ->
Html.li []
itemBlocks
)
)
, html = Markdown.Html.oneOf []
, codeBlock = codeBlock
--\{ body, language } ->
-- let
-- classes =
-- -- Only the first word is used in the class
-- case Maybe.map String.words language of
-- Just (actualLanguage :: _) ->
-- [ Attr.class <| "language-" ++ actualLanguage ]
--
-- _ ->
-- []
-- in
-- Html.pre []
-- [ Html.code classes
-- [ Html.text body
-- ]
-- ]
, table = Html.table []
, tableHeader = Html.thead []
, tableBody = Html.tbody []
, tableRow = Html.tr []
, strikethrough =
\children -> Html.del [] children
, tableHeaderCell =
\maybeAlignment ->
let
attrs =
maybeAlignment
|> Maybe.map
(\alignment ->
case alignment of
Block.AlignLeft ->
"left"
Block.AlignCenter ->
"center"
Block.AlignRight ->
"right"
)
|> Maybe.map Attr.align
|> Maybe.map List.singleton
|> Maybe.withDefault []
in
Html.th attrs
, tableCell =
\maybeAlignment ->
let
attrs =
maybeAlignment
|> Maybe.map
(\alignment ->
case alignment of
Block.AlignLeft ->
"left"
Block.AlignCenter ->
"center"
Block.AlignRight ->
"right"
)
|> Maybe.map Attr.align
|> Maybe.map List.singleton
|> Maybe.withDefault []
in
Html.td attrs
}
rawTextToId : String -> String
rawTextToId rawText =
rawText
|> String.split " "
|> String.join "-"
|> String.toLower
heading : { level : Block.HeadingLevel, rawText : String, children : List (Html.Html msg) } -> Html.Html msg
heading { level, rawText, children } =
(case level of
Block.H1 ->
Html.h1
Block.H2 ->
Html.h2
Block.H3 ->
Html.h3
Block.H4 ->
Html.h4
Block.H5 ->
Html.h5
Block.H6 ->
Html.h6
)
[ Attr.id (rawTextToId rawText)
, Attr.attribute "name" (rawTextToId rawText)
, css
[ Tw.font_bold
, Tw.text_2xl
, Tw.mt_8
, Tw.mb_4
]
]
children
--code : String -> Element msg
--code snippet =
-- Element.el
-- [ Element.Background.color
-- (Element.rgba255 50 50 50 0.07)
-- , Element.Border.rounded 2
-- , Element.paddingXY 5 3
-- , Font.family [ Font.typeface "Roboto Mono", Font.monospace ]
-- ]
-- (Element.text snippet)
--
--
codeBlock : { body : String, language : Maybe String } -> Html.Html msg
codeBlock details =
SyntaxHighlight.elm details.body
|> Result.map (SyntaxHighlight.toBlockHtml (Just 1))
|> Result.map Html.fromUnstyled
|> Result.withDefault (Html.pre [] [ Html.code [] [ Html.text details.body ] ])

View File

@ -0,0 +1,85 @@
module MySession exposing (..)
import Codec
import DataSource exposing (DataSource)
import DataSource.Env as Env
import Route
import Server.Request exposing (Parser)
import Server.Response as Response exposing (Response)
import Server.Session as Session
withSession :
Parser request
-> (request -> Result () (Maybe Session.Session) -> DataSource ( Session.Session, Response data errorPage ))
-> Parser (DataSource (Response data errorPage))
withSession =
Session.withSession
{ name = "mysession"
, secrets = Env.expect "SESSION_SECRET" |> DataSource.map List.singleton
, sameSite = "lax"
}
withSessionOrRedirect :
Parser request
-> (request -> Maybe Session.Session -> DataSource ( Session.Session, Response data errorPage ))
-> Parser (DataSource (Response data errorPage))
withSessionOrRedirect handler toRequest =
Session.withSession
{ name = "mysession"
, secrets = Env.expect "SESSION_SECRET" |> DataSource.map List.singleton
, sameSite = "lax"
}
handler
(\request sessionResult ->
sessionResult
|> Result.map (toRequest request)
|> Result.withDefault
(DataSource.succeed
( Session.empty
, Route.redirectTo Route.Login
)
)
)
expectSessionOrRedirect :
(request -> Session.Session -> DataSource ( Session.Session, Response data errorPage ))
-> Parser request
-> Parser (DataSource (Response data errorPage))
expectSessionOrRedirect toRequest handler =
Session.withSession
{ name = "mysession"
, secrets = Env.expect "SESSION_SECRET" |> DataSource.map List.singleton
, sameSite = "lax"
}
handler
(\request sessionResult ->
sessionResult
|> Result.map (Maybe.map (toRequest request))
|> Result.withDefault Nothing
|> Maybe.withDefault
(DataSource.succeed
( Session.empty
, Route.redirectTo Route.Login
)
)
)
schema =
{ name = ( "name", Codec.string )
, message = ( "message", Codec.string )
, user =
( "user"
, Codec.object User
|> Codec.field "id" .id Codec.int
|> Codec.buildObject
)
}
type alias User =
{ id : Int
}

View File

@ -0,0 +1,70 @@
module Request.Fauna exposing (dataSource, mutationDataSource)
import DataSource exposing (DataSource)
import DataSource.Http
import Graphql.Document
import Graphql.Operation exposing (RootMutation, RootQuery)
import Graphql.SelectionSet exposing (SelectionSet)
import Json.Encode as Encode
dataSource : String -> SelectionSet value RootQuery -> DataSource value
dataSource timeStamp selectionSet =
DataSource.Http.request
{ url =
faunaUrl
-- for now, this timestamp invalidates the dev server cache
-- it would be helpful to have a way to mark a DataSource as uncached. Maybe only allow
-- from server-rendered pages?
++ "?time="
++ timeStamp
, method = "POST"
, headers = [ ( "authorization", faunaAuthValue ) ]
, body =
DataSource.Http.jsonBody
(Encode.object
[ ( "query"
, selectionSet
|> Graphql.Document.serializeQuery
|> Encode.string
)
]
)
}
(selectionSet
|> Graphql.Document.decoder
|> DataSource.Http.expectJson
)
mutationDataSource : String -> SelectionSet value RootMutation -> DataSource value
mutationDataSource timeStamp selectionSet =
DataSource.Http.request
{ url = faunaUrl ++ "?time=" ++ timeStamp
, method = "POST"
, headers = [ ( "authorization", faunaAuthValue ) ]
, body =
DataSource.Http.jsonBody
(Encode.object
[ ( "query"
, selectionSet
|> Graphql.Document.serializeMutation
|> Encode.string
)
]
)
}
(selectionSet
|> Graphql.Document.decoder
|> DataSource.Http.expectJson
)
faunaUrl : String
faunaUrl =
"https://graphql.us.fauna.com/graphql"
faunaAuthValue : String
faunaAuthValue =
"Bearer fnAEdqJ_JdAAST7wRrjZj7NKSw-vCfE9_W8RyshZ"

View File

@ -0,0 +1,5 @@
module Types exposing (..)
type alias Data =
List String

80
examples/trails/style.css Normal file
View File

@ -0,0 +1,80 @@
@import url("https://rsms.me/inter/inter.css");
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap");
body {
font-family: "Inter var" !important;
}
/*
input:invalid {
border: 2px dashed red;
}
input:valid {
border: 2px solid black;
}
*/
input[type=password], input[type=text], input[type=date], input[type=email] {
border-radius: 10px !important;
border-color: #ccc !important;
}
input[type=checkbox] {
border-radius: 4px !important;
border-color: #ccc !important;
}
main.color-app {
align-items: center;
display: flex;
justify-content: center;
min-height: 100vh;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 18px;
margin: 0;
padding: 0;
}
main.color-app {
background: rgba(0, 0, 0, 0.15);
border-radius: 0.8em;
padding: 0.8em;
}
main.color-app .content {
background: rgba(255, 255, 255, 0.9);
border-radius: 0.5em;
max-width: 90vw;
padding: 2rem 3rem;
width: 475px;
color: var(--selected-color);
}
main.color-app .content h1 {
margin: 0;
}
main.color-app .content ul {
list-style: none;
padding: 0;
}
main.color-app .content li {
margin-left: -1rem;
margin-right: -1rem;
padding: 1rem;
}
main.color-app .content li:nth-child(odd) {
background: rgb(100 10 80 / 0.1);
}
.timestamp {
font-size: 0.75rem;
}

View File

@ -0,0 +1,5 @@
module.exports = {
theme: {},
variants: [],
plugins: [require("@tailwindcss/forms")],
};