mirror of
https://github.com/dillonkearns/elm-pages-v3-beta.git
synced 2024-11-27 21:29:55 +03:00
Add starting point for trails demo.
This commit is contained in:
parent
c5ad2c5e40
commit
31265b4979
150
examples/pokedex/app/Route/HelloForm.elm
Normal file
150
examples/pokedex/app/Route/HelloForm.elm
Normal 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"
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
}
|
166
examples/pokedex/app/Route/Search.elm
Normal file
166
examples/pokedex/app/Route/Search.elm
Normal 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
8
examples/trails/.gitignore
vendored
Normal 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
2
examples/trails/.nvmrc
Normal file
@ -0,0 +1,2 @@
|
||||
v17.2.0
|
||||
|
1
examples/trails/README.md
Normal file
1
examples/trails/README.md
Normal file
@ -0,0 +1 @@
|
||||
# README
|
312
examples/trails/adapter.mjs
Normal file
312
examples/trails/adapter.mjs
Normal 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
262
examples/trails/app/Api.elm
Normal 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 = []
|
||||
}
|
147
examples/trails/app/Effect.elm
Normal file
147
examples/trails/app/Effect.elm
Normal 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
|
74
examples/trails/app/ErrorPage.elm
Normal file
74
examples/trails/app/ErrorPage.elm
Normal 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
|
83
examples/trails/app/Route/Index.elm
Normal file
83
examples/trails/app/Route/Index.elm
Normal 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 =
|
||||
[]
|
||||
}
|
147
examples/trails/app/Route/Login.elm
Normal file
147
examples/trails/app/Route/Login.elm
Normal 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" ]
|
||||
]
|
||||
]
|
||||
}
|
166
examples/trails/app/Route/Search.elm
Normal file
166
examples/trails/app/Route/Search.elm
Normal 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 ]
|
||||
]
|
226
examples/trails/app/Route/Signup.elm
Normal file
226
examples/trails/app/Route/Signup.elm
Normal 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
|
||||
]
|
116
examples/trails/app/Shared.elm
Normal file
116
examples/trails/app/Shared.elm
Normal 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
|
||||
}
|
46
examples/trails/app/Site.elm
Normal file
46
examples/trails/app/Site.elm
Normal 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 })
|
23
examples/trails/app/View.elm
Normal file
23
examples/trails/app/View.elm
Normal 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 ]
|
||||
}
|
8
examples/trails/elm-application.json
Normal file
8
examples/trails/elm-application.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "dmy/elm-doc-preview",
|
||||
"summary": "Offline documentation previewer",
|
||||
"version": "5.0.0",
|
||||
"exposed-modules": [
|
||||
"Page"
|
||||
]
|
||||
}
|
8
examples/trails/elm-pages.config.mjs
Normal file
8
examples/trails/elm-pages.config.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
import adapter from "./adapter.mjs";
|
||||
|
||||
export default {
|
||||
vite: defineConfig({}),
|
||||
adapter,
|
||||
};
|
8
examples/trails/elm-tooling.json
Normal file
8
examples/trails/elm-tooling.json
Normal 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
66
examples/trails/elm.json
Normal 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": {}
|
||||
}
|
||||
}
|
14
examples/trails/functions/time.js
Normal file
14
examples/trails/functions/time.js
Normal 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
6
examples/trails/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
load: function (elmLoaded) {},
|
||||
flags: function () {
|
||||
return null;
|
||||
},
|
||||
};
|
13
examples/trails/netlify.toml
Normal file
13
examples/trails/netlify.toml
Normal 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
6233
examples/trails/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
examples/trails/package.json
Normal file
26
examples/trails/package.json
Normal 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"
|
||||
}
|
||||
}
|
23
examples/trails/port-data-source.ts
Normal file
23
examples/trails/port-data-source.ts
Normal 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));
|
||||
}
|
BIN
examples/trails/public/favicon.ico
Normal file
BIN
examples/trails/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 450 B |
39
examples/trails/public/images/elm-logo.svg
Normal file
39
examples/trails/public/images/elm-logo.svg
Normal 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 |
1
examples/trails/public/images/github.svg
Normal file
1
examples/trails/public/images/github.svg
Normal 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 |
BIN
examples/trails/public/images/icon-png.png
Normal file
BIN
examples/trails/public/images/icon-png.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 976 B |
2
examples/trails/public/images/icon.svg
Normal file
2
examples/trails/public/images/icon.svg
Normal 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 |
43
examples/trails/public/syntax.css
Normal file
43
examples/trails/public/syntax.css
Normal 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;}
|
||||
|
65
examples/trails/src/Icon.elm
Normal file
65
examples/trails/src/Icon.elm
Normal 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"
|
||||
]
|
||||
[]
|
||||
]
|
233
examples/trails/src/MarkdownRenderer.elm
Normal file
233
examples/trails/src/MarkdownRenderer.elm
Normal 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 ] ])
|
85
examples/trails/src/MySession.elm
Normal file
85
examples/trails/src/MySession.elm
Normal 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
|
||||
}
|
70
examples/trails/src/Request/Fauna.elm
Normal file
70
examples/trails/src/Request/Fauna.elm
Normal 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"
|
5
examples/trails/src/Types.elm
Normal file
5
examples/trails/src/Types.elm
Normal file
@ -0,0 +1,5 @@
|
||||
module Types exposing (..)
|
||||
|
||||
|
||||
type alias Data =
|
||||
List String
|
80
examples/trails/style.css
Normal file
80
examples/trails/style.css
Normal 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;
|
||||
}
|
5
examples/trails/tailwind.config.js
Normal file
5
examples/trails/tailwind.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
theme: {},
|
||||
variants: [],
|
||||
plugins: [require("@tailwindcss/forms")],
|
||||
};
|
Loading…
Reference in New Issue
Block a user