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