mirror of
https://github.com/dillonkearns/elm-pages.git
synced 2024-10-26 10:27:36 +03:00
Add hacker news example.
This commit is contained in:
parent
8800c8a16f
commit
afcfdb72dc
8
examples/hackernews/.gitignore
vendored
Normal file
8
examples/hackernews/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
elm-stuff/
|
||||
dist/
|
||||
.cache/
|
||||
.elm-pages/
|
||||
functions/render/
|
||||
functions/server-render/
|
||||
gen/
|
2
examples/hackernews/.nvmrc
Normal file
2
examples/hackernews/.nvmrc
Normal file
@ -0,0 +1,2 @@
|
||||
v17.2.0
|
||||
|
1
examples/hackernews/README.md
Normal file
1
examples/hackernews/README.md
Normal file
@ -0,0 +1 @@
|
||||
# README
|
312
examples/hackernews/adapter.mjs
Normal file
312
examples/hackernews/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("/")
|
||||
);
|
||||
}
|
27
examples/hackernews/app/Api.elm
Normal file
27
examples/hackernews/app/Api.elm
Normal file
@ -0,0 +1,27 @@
|
||||
module Api exposing (routes)
|
||||
|
||||
import ApiRoute exposing (ApiRoute)
|
||||
import DataSource exposing (DataSource)
|
||||
import Html exposing (Html)
|
||||
import Pages.Manifest as Manifest
|
||||
import Route exposing (Route)
|
||||
import Site
|
||||
|
||||
|
||||
routes :
|
||||
DataSource (List Route)
|
||||
-> (Html Never -> String)
|
||||
-> List (ApiRoute.ApiRoute ApiRoute.Response)
|
||||
routes getStaticRoutes htmlToString =
|
||||
[ DataSource.succeed manifest |> Manifest.generator Site.canonicalUrl
|
||||
]
|
||||
|
||||
|
||||
manifest : Manifest.Config
|
||||
manifest =
|
||||
Manifest.init
|
||||
{ name = "Hacker News Clone"
|
||||
, description = "elm-pages port of Hacker News"
|
||||
, startUrl = Route.Feed__ { feed = Nothing } |> Route.toPath
|
||||
, icons = []
|
||||
}
|
88
examples/hackernews/app/Effect.elm
Normal file
88
examples/hackernews/app/Effect.elm
Normal file
@ -0,0 +1,88 @@
|
||||
module Effect exposing (Effect(..), batch, fromCmd, map, none, perform)
|
||||
|
||||
import Browser.Navigation
|
||||
import Http
|
||||
import Url exposing (Url)
|
||||
|
||||
|
||||
type Effect msg
|
||||
= None
|
||||
| Cmd (Cmd msg)
|
||||
| Batch (List (Effect msg))
|
||||
| FetchRouteData
|
||||
{ body : Maybe { contentType : String, body : String }
|
||||
, path : Maybe String
|
||||
, toMsg : Result Http.Error Url -> 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)
|
||||
|
||||
FetchRouteData fetchInfo ->
|
||||
FetchRouteData
|
||||
{ body = fetchInfo.body
|
||||
, path = fetchInfo.path
|
||||
, toMsg = fetchInfo.toMsg >> fn
|
||||
}
|
||||
|
||||
|
||||
perform :
|
||||
{ fetchRouteData :
|
||||
{ body : Maybe { contentType : String, body : String }
|
||||
, path : Maybe String
|
||||
, toMsg : Result Http.Error Url -> 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)
|
||||
|
||||
FetchRouteData fetchInfo ->
|
||||
helpers.fetchRouteData
|
||||
{ body = fetchInfo.body
|
||||
, path = fetchInfo.path
|
||||
, toMsg = fetchInfo.toMsg
|
||||
}
|
84
examples/hackernews/app/ErrorPage.elm
Normal file
84
examples/hackernews/app/ErrorPage.elm
Normal file
@ -0,0 +1,84 @@
|
||||
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 Route
|
||||
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 "Page not found. Maybe try another URL?" ]
|
||||
, Html.div []
|
||||
[ Html.button
|
||||
[ onClick Increment
|
||||
]
|
||||
[ Html.text
|
||||
(model.count
|
||||
|> String.fromInt
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, title = "This is a NotFound Error"
|
||||
}
|
||||
|
||||
|
||||
statusCode : ErrorPage -> number
|
||||
statusCode error =
|
||||
case error of
|
||||
NotFound ->
|
||||
404
|
||||
|
||||
InternalError _ ->
|
||||
500
|
176
examples/hackernews/app/Route/Feed__.elm
Normal file
176
examples/hackernews/app/Route/Feed__.elm
Normal file
@ -0,0 +1,176 @@
|
||||
module Route.Feed__ exposing (Data, Model, Msg, route)
|
||||
|
||||
import DataSource exposing (DataSource)
|
||||
import DataSource.Http
|
||||
import Effect exposing (Effect)
|
||||
import ErrorPage exposing (ErrorPage)
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import Html
|
||||
import Html.Attributes as Attr
|
||||
import Json.Decode exposing (Decoder)
|
||||
import Json.Decode.Pipeline exposing (required)
|
||||
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 Story exposing (Story)
|
||||
import Url.Builder
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
type alias Model =
|
||||
{}
|
||||
|
||||
|
||||
type Msg
|
||||
= NoOp
|
||||
|
||||
|
||||
type alias RouteParams =
|
||||
{ feed : Maybe String }
|
||||
|
||||
|
||||
route : StatefulRoute RouteParams Data Model Msg
|
||||
route =
|
||||
RouteBuilder.serverRender
|
||||
{ head = head
|
||||
, data = data
|
||||
}
|
||||
|> RouteBuilder.buildWithLocalState
|
||||
{ view = view
|
||||
, update = update
|
||||
, subscriptions = subscriptions
|
||||
, init = init
|
||||
}
|
||||
|
||||
|
||||
init :
|
||||
Maybe PageUrl
|
||||
-> Shared.Model
|
||||
-> StaticPayload Data RouteParams
|
||||
-> ( Model, Effect Msg )
|
||||
init maybePageUrl sharedModel static =
|
||||
( {}, Effect.none )
|
||||
|
||||
|
||||
update :
|
||||
PageUrl
|
||||
-> Shared.Model
|
||||
-> StaticPayload Data 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
|
||||
|
||||
|
||||
pages : DataSource (List RouteParams)
|
||||
pages =
|
||||
DataSource.succeed []
|
||||
|
||||
|
||||
type alias Data =
|
||||
{ stories : List Story }
|
||||
|
||||
|
||||
data : RouteParams -> Request.Parser (DataSource (Response Data ErrorPage))
|
||||
data routeParams =
|
||||
Request.queryParam "page"
|
||||
|> Request.map
|
||||
(\maybePage ->
|
||||
let
|
||||
feed : String
|
||||
feed =
|
||||
--const type = Astro.params.stories || "top";
|
||||
case routeParams.feed |> Maybe.withDefault "top" of
|
||||
"top" ->
|
||||
"news"
|
||||
|
||||
"new" ->
|
||||
"newest"
|
||||
|
||||
"show" ->
|
||||
"show"
|
||||
|
||||
"ask" ->
|
||||
"ask"
|
||||
|
||||
"job" ->
|
||||
"jobs"
|
||||
|
||||
_ ->
|
||||
"not-found"
|
||||
|
||||
getStoriesUrl : String
|
||||
getStoriesUrl =
|
||||
Url.Builder.crossOrigin "https://node-hnapi.herokuapp.com"
|
||||
[ feed ]
|
||||
[ Url.Builder.string "page" (maybePage |> Maybe.withDefault "1")
|
||||
]
|
||||
|
||||
getStories : DataSource (List Story)
|
||||
getStories =
|
||||
DataSource.Http.get getStoriesUrl
|
||||
(Story.decoder |> Json.Decode.list)
|
||||
|
||||
--("https://node-hnapi.herokuapp.com/"
|
||||
-- ++ feed
|
||||
-- ++ "?page="
|
||||
--)
|
||||
--get(`https://node-hnapi.herokuapp.com/${l}?page=${page}`);
|
||||
in
|
||||
getStories |> DataSource.map (\stories -> Response.render { stories = stories })
|
||||
)
|
||||
|
||||
|
||||
head :
|
||||
StaticPayload Data 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 RouteParams
|
||||
-> View Msg
|
||||
view maybeUrl sharedModel model static =
|
||||
{ title = "News"
|
||||
, body =
|
||||
[ Html.main_
|
||||
[ Attr.class "news-list"
|
||||
]
|
||||
[ static.data.stories
|
||||
|> List.map Story.view
|
||||
|> Html.ul []
|
||||
|
||||
--, Html.text <| "Count: " ++ String.fromInt (static.data.stories |> List.length)
|
||||
]
|
||||
]
|
||||
}
|
149
examples/hackernews/app/Shared.elm
Normal file
149
examples/hackernews/app/Shared.elm
Normal file
@ -0,0 +1,149 @@
|
||||
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 [] (headerView :: pageView.body)
|
||||
, title = pageView.title
|
||||
}
|
||||
|
||||
|
||||
headerView : Html msg
|
||||
headerView =
|
||||
Html.header
|
||||
[ Attr.class "header"
|
||||
]
|
||||
[ Html.nav
|
||||
[ Attr.class "inner"
|
||||
]
|
||||
[ Html.a
|
||||
[ Attr.href "/"
|
||||
]
|
||||
[ Html.strong []
|
||||
[ Html.text "HN" ]
|
||||
]
|
||||
, Html.a
|
||||
[ Attr.href "/new"
|
||||
]
|
||||
[ Html.strong []
|
||||
[ Html.text "New" ]
|
||||
]
|
||||
, Html.a
|
||||
[ Attr.href "/show"
|
||||
]
|
||||
[ Html.strong []
|
||||
[ Html.text "Show" ]
|
||||
]
|
||||
, Html.a
|
||||
[ Attr.href "/ask"
|
||||
]
|
||||
[ Html.strong []
|
||||
[ Html.text "Ask" ]
|
||||
]
|
||||
, Html.a
|
||||
[ Attr.href "/job"
|
||||
]
|
||||
[ Html.strong []
|
||||
[ Html.text "Jobs" ]
|
||||
]
|
||||
, Html.a
|
||||
[ Attr.class "github"
|
||||
, Attr.href "https://github.com/dillonkearns/elm-pages"
|
||||
, Attr.target "_blank"
|
||||
, Attr.rel "noreferrer"
|
||||
]
|
||||
[ Html.text "Built with elm-pages" ]
|
||||
]
|
||||
]
|
23
examples/hackernews/app/Site.elm
Normal file
23
examples/hackernews/app/Site.elm
Normal file
@ -0,0 +1,23 @@
|
||||
module Site exposing (canonicalUrl, config)
|
||||
|
||||
import DataSource exposing (DataSource)
|
||||
import Head
|
||||
import SiteConfig exposing (SiteConfig)
|
||||
|
||||
|
||||
config : SiteConfig
|
||||
config =
|
||||
{ canonicalUrl = canonicalUrl
|
||||
, head = head
|
||||
}
|
||||
|
||||
|
||||
canonicalUrl : String
|
||||
canonicalUrl =
|
||||
"https://elm-pages.com"
|
||||
|
||||
|
||||
head : DataSource (List Head.Tag)
|
||||
head =
|
||||
[]
|
||||
|> DataSource.succeed
|
23
examples/hackernews/app/View.elm
Normal file
23
examples/hackernews/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/hackernews/elm-application.json
Normal file
8
examples/hackernews/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/hackernews/elm-pages.config.mjs
Normal file
8
examples/hackernews/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/hackernews/elm-tooling.json
Normal file
8
examples/hackernews/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"
|
||||
}
|
||||
}
|
67
examples/hackernews/elm.json
Normal file
67
examples/hackernews/elm.json
Normal file
@ -0,0 +1,67 @@
|
||||
{
|
||||
"type": "application",
|
||||
"source-directories": [
|
||||
"src",
|
||||
"app",
|
||||
"../../src",
|
||||
".elm-pages",
|
||||
"../../plugins",
|
||||
"gen"
|
||||
],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"MartinSStewart/elm-serialize": "1.2.5",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.1",
|
||||
"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": {}
|
||||
}
|
||||
}
|
6
examples/hackernews/index.js
Normal file
6
examples/hackernews/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
load: function (elmLoaded) {},
|
||||
flags: function () {
|
||||
return null;
|
||||
},
|
||||
};
|
13
examples/hackernews/netlify.toml
Normal file
13
examples/hackernews/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/hackernews/package-lock.json
generated
Normal file
6233
examples/hackernews/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
examples/hackernews/package.json
Normal file
24
examples/hackernews/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "elm-pages-example",
|
||||
"version": "1.0.0",
|
||||
"description": "Example site built with elm-pages.",
|
||||
"scripts": {
|
||||
"start": "elm-pages dev --debug",
|
||||
"serve": "npm run build && http-server ./dist -a localhost -p 3000 -c-1",
|
||||
"build": "elm-pages build --debug --keep-cache"
|
||||
},
|
||||
"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/hackernews/port-data-source.ts
Normal file
23
examples/hackernews/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/hackernews/public/favicon.ico
Normal file
BIN
examples/hackernews/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 450 B |
105
examples/hackernews/src/Story.elm
Normal file
105
examples/hackernews/src/Story.elm
Normal file
@ -0,0 +1,105 @@
|
||||
module Story exposing (..)
|
||||
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
import Json.Decode exposing (Decoder)
|
||||
import Json.Decode.Pipeline exposing (optional, required)
|
||||
|
||||
|
||||
type alias Story =
|
||||
{ title : String
|
||||
, points : Int
|
||||
, user : String
|
||||
, url : Maybe String
|
||||
, domain : String
|
||||
, time_ago : String
|
||||
, comments_count : Int
|
||||
, type_ : String
|
||||
}
|
||||
|
||||
|
||||
view : Story -> Html msg
|
||||
view story =
|
||||
Html.li
|
||||
[ Attr.class "news-item"
|
||||
]
|
||||
[ Html.span
|
||||
[ Attr.class "score"
|
||||
]
|
||||
[ Html.text (String.fromInt story.points) ]
|
||||
, Html.span
|
||||
[ Attr.class "title"
|
||||
]
|
||||
(case story.url of
|
||||
Just url ->
|
||||
[ Html.a
|
||||
[ Attr.href url
|
||||
, Attr.target "_blank"
|
||||
, Attr.rel "noreferrer"
|
||||
]
|
||||
[ Html.text story.title ]
|
||||
|
||||
{-
|
||||
{story.url && !story.url.startsWith("item?id=") ? (
|
||||
<>
|
||||
<a href={story.url} target="_blank" rel="noreferrer">
|
||||
{story.title}
|
||||
</a>
|
||||
<span class="host"> ({story.domain})</span>
|
||||
</>
|
||||
) : (
|
||||
<a href={`/item/${story.id}`}>{story.title}</a>
|
||||
)}
|
||||
-}
|
||||
, Html.span [ Attr.class "host" ] [ Html.text <| " (" ++ story.domain ++ ")" ]
|
||||
]
|
||||
|
||||
Nothing ->
|
||||
[ Html.a
|
||||
[-- TODO decode into custom type here? --Attr.href ("/item/" ++ story.id)
|
||||
]
|
||||
[]
|
||||
]
|
||||
)
|
||||
, Html.br []
|
||||
[]
|
||||
, Html.span
|
||||
[ Attr.class "meta"
|
||||
]
|
||||
[ Html.text "by "
|
||||
, Html.a [ Attr.href "TODO" ]
|
||||
[ Html.text story.user
|
||||
]
|
||||
, Html.text (" " ++ story.time_ago ++ " | ")
|
||||
, Html.a
|
||||
[-- TODO get story.id --Attr.href ("/stories/" ++ story.id)
|
||||
]
|
||||
[ if story.comments_count > 0 then
|
||||
Html.text (String.fromInt story.comments_count ++ " comments")
|
||||
|
||||
else
|
||||
Html.text "discuss"
|
||||
]
|
||||
]
|
||||
, if story.type_ /= "link" then
|
||||
Html.span
|
||||
[ Attr.class "label"
|
||||
]
|
||||
[ Html.text story.type_ ]
|
||||
|
||||
else
|
||||
Html.text ""
|
||||
]
|
||||
|
||||
|
||||
decoder : Decoder Story
|
||||
decoder =
|
||||
Json.Decode.succeed Story
|
||||
|> required "title" Json.Decode.string
|
||||
|> optional "points" Json.Decode.int 0
|
||||
|> optional "user" Json.Decode.string ""
|
||||
|> required "url" (Json.Decode.nullable Json.Decode.string)
|
||||
|> optional "domain" Json.Decode.string ""
|
||||
|> required "time_ago" Json.Decode.string
|
||||
|> required "comments_count" Json.Decode.int
|
||||
|> required "type" Json.Decode.string
|
329
examples/hackernews/style.css
Normal file
329
examples/hackernews/style.css
Normal file
@ -0,0 +1,329 @@
|
||||
/* Source: https://github.com/ryansolid/astro-solid-hackernews/blob/e6265ee3751dbd83a97f64c7de1e34af31a543ed/src/styles/global.css */
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
font-size: 15px;
|
||||
background-color: #f2f3f5;
|
||||
margin: 0;
|
||||
padding-top: 55px;
|
||||
color: #34495e;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #34495e;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #4b158a;
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
height: 55px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.header .inner {
|
||||
max-width: 800px;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
padding: 15px 5px;
|
||||
}
|
||||
|
||||
.header a {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 24px;
|
||||
transition: color 0.15s ease;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.075em;
|
||||
margin-right: 1.8em;
|
||||
}
|
||||
|
||||
.header a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header a.active {
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.header a:nth-child(6) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.header .github {
|
||||
color: #fff;
|
||||
font-size: 0.9em;
|
||||
margin: 0;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 24px;
|
||||
margin-right: 10px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.view {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.header .inner {
|
||||
padding: 15px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.header .inner {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.header a {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.header .github {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.news-view {
|
||||
padding-top: 45px;
|
||||
}
|
||||
|
||||
.news-list,
|
||||
.news-list-nav {
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.news-list-nav {
|
||||
padding: 15px 30px;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
top: 55px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 998;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.news-list-nav .page-link {
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
.news-list-nav .disabled {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.news-list {
|
||||
position: absolute;
|
||||
margin: 30px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.news-list ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.slide-left-enter,
|
||||
.slide-right-exit-to {
|
||||
opacity: 0;
|
||||
transform: translate(30px, 0);
|
||||
}
|
||||
|
||||
.slide-left-exit-to,
|
||||
.slide-right-enter {
|
||||
opacity: 0;
|
||||
transform: translate(-30px, 0);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.news-list {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.news-item {
|
||||
background-color: #fff;
|
||||
padding: 20px 30px 20px 80px;
|
||||
border-bottom: 1px solid #eee;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.news-item .score {
|
||||
color: #4b158a;
|
||||
font-size: 1.1em;
|
||||
font-weight: 700;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.news-item .host,
|
||||
.news-item .meta {
|
||||
font-size: 0.85em;
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
.news-item .host a,
|
||||
.news-item .meta a {
|
||||
color: #626262;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.news-item .host a:hover,
|
||||
.news-item .meta a:hover {
|
||||
color: #4b158a;
|
||||
}
|
||||
|
||||
.item-view-header {
|
||||
background-color: #fff;
|
||||
padding: 1.8em 2em 1em;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.item-view-header h1 {
|
||||
display: inline;
|
||||
font-size: 1.5em;
|
||||
margin: 0;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.item-view-header .host,
|
||||
.item-view-header .meta,
|
||||
.item-view-header .meta a {
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
.item-view-header .meta a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.item-view-comments {
|
||||
background-color: #fff;
|
||||
margin-top: 10px;
|
||||
padding: 0 2em 0.5em;
|
||||
}
|
||||
|
||||
.item-view-comments-header {
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
padding: 1em 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-view-comments-header .spinner {
|
||||
display: inline-block;
|
||||
margin: -15px 0;
|
||||
}
|
||||
|
||||
.comment-children {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.item-view-header h1 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-children .comment-children {
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
.comment {
|
||||
border-top: 1px solid #eee;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comment .by,
|
||||
.comment .text,
|
||||
.comment .toggle {
|
||||
font-size: 0.9em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.comment .by {
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
.comment .by a {
|
||||
color: #626262;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.comment .text {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.comment .text a:hover {
|
||||
color: #5f3392;
|
||||
}
|
||||
|
||||
.comment .text pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.comment .toggle {
|
||||
background-color: #fffbf2;
|
||||
padding: 0.3em 0.5em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.comment .toggle a {
|
||||
color: #626262;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.comment .toggle.open {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
margin-bottom: -0.5em;
|
||||
}
|
||||
|
||||
.user-view {
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
padding: 2em 3em;
|
||||
}
|
||||
|
||||
.user-view h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.user-view .meta {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.user-view .label {
|
||||
display: inline-block;
|
||||
min-width: 4em;
|
||||
}
|
||||
|
||||
.user-view .about {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.user-view .links a {
|
||||
text-decoration: underline;
|
||||
}
|
Loading…
Reference in New Issue
Block a user