diff --git a/web/comp/layout.ts b/web/comp/layout.ts index 38618d3..72838fb 100644 --- a/web/comp/layout.ts +++ b/web/comp/layout.ts @@ -8,6 +8,7 @@ export function Layout(page: AnyNode): AnyNode { name: "viewport", content: "width=device-width, initial-scale=1", }), + E("link", { rel: "stylesheet", href: "/static/styles.css" }), E("title", [], "Recipes"), ]), E("body", [], [ diff --git a/web/server.ts b/web/server.ts index 5fda0ca..fe1ef29 100644 --- a/web/server.ts +++ b/web/server.ts @@ -32,24 +32,30 @@ async function serveHttp(conn: Deno.Conn) { const httpConn = Deno.serveHttp(conn); for await (const reqEvt of httpConn) { - const res = handleRequest(reqEvt.request); + const res = await handleRequest(reqEvt.request); reqEvt.respondWith(res); } } -function handleRequest(req: Request): Response { +async function handleRequest(req: Request): Promise { log.debug({ url: req.url }); - const ren = new StrRenderer({ wrapNode: Layout }); const ctx = createContextFromRequest(req); - if (ctx.locPath === "/") { - return createHtmlResponse(ren.render(HomePage(ctx))); - } else if (ctx.locPath === "/recipes") { - return createHtmlResponse(ren.render(RecipesPage(ctx))); - } else if (ctx.locPath === "/ingredients") { - return createHtmlResponse(ren.render(IngredientsPage(ctx))); - } else { - return createHtmlResponse(ren.render(E404Page(ctx)), 404); + try { + const res = await tryCreateFileResponse(ctx.locPath); + return res; + } catch (_) { + const ren = new StrRenderer({ wrapNode: Layout }); + + if (ctx.locPath === "/") { + return createHtmlResponse(ren.render(HomePage(ctx))); + } else if (ctx.locPath === "/recipes") { + return createHtmlResponse(ren.render(RecipesPage(ctx))); + } else if (ctx.locPath === "/ingredients") { + return createHtmlResponse(ren.render(IngredientsPage(ctx))); + } else { + return createHtmlResponse(ren.render(E404Page(ctx)), 404); + } } } @@ -62,6 +68,52 @@ function createContextFromRequest(req: Request): Context { function createHtmlResponse(body: string, status = 200): Response { return new Response(body, { status, - headers: { "content-type": "text/html" }, + headers: getContentTypeHeader("html"), }); } + +async function tryCreateFileResponse(urlPath: string): Promise { + const filePath = extractFilePath(urlPath); + if (!filePath) throw new SkipFile(); + + const content = await Deno.readTextFile(filePath).catch(() => { + throw new SkipFile(); + }); + + return createFileResponse(content, getFileExt(filePath)); +} + +class SkipFile extends Error {} + +function createFileResponse(content: string, fileExt: string): Response { + log.debug(fileExt); + return new Response(content, { + headers: getContentTypeHeader(fileExt), + }); +} + +function extractFilePath(urlPath: string): string | null { + if (urlPath.startsWith("/static")) { + return urlPath.slice(1); + } + return null; +} + +function getContentTypeHeader(fileExt: string): Record { + return { "content-type": getContentTypeByExt(fileExt) }; +} + +function getContentTypeByExt(fileExt: string): string { + switch (fileExt) { + case "html": + return "text/html"; + case "css": + return "text/css"; + default: + return "text/plain"; + } +} + +function getFileExt(filePath: string): string { + return filePath.slice((filePath.lastIndexOf(".") >>> 0) + 1); +} diff --git a/web/static/styles.css b/web/static/styles.css new file mode 100644 index 0000000..7f916ea --- /dev/null +++ b/web/static/styles.css @@ -0,0 +1,150 @@ +:root { + --min-width: 320px; + --max-width: 1024px; + + --default-color-white: #ffffff; + --color-blue: #1966df; + + --default-font-size: 16px; + --font-family: system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Roboto,Oxygen-Sans, Ubuntu, Cantarell, "Segoe UI", Verdana, sans-serif; + --font-weight-regular: 400; +} + +*, *::before, *::after { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html { + background-color: var(--default-color-white); + font-size: var(--default-font-size); + line-height: 1; + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + font-weight: var(--font-weight-regular); + font-family: var(--font-family); + min-width: var(--min-width); +} + +html, body { + height: 100%; + width: 100%; +} + +ul, ol { + list-style: none; +} + +#root { + min-height: 100vh; + align-items: stretch; +} + +#root, +#main { + display: flex; + flex-direction: column; +} + +#main, +.content { + flex: 1 0; +} + +.header { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} + +.main-menu { + display: flex; + flex-direction: row; +} + +// anim +.main-menu > a { + transition: all .2s ease-in-out; +} + +.main-menu > a { + color: var(--color-blue); + padding: 0.5rem; + border-radius: 6px; + border: 1px solid var(--color-blue); + text-decoration: none; +} +.main-menu > a:hover, +.main-menu > a[aria-current]:not([aria-current=""]) { + color: var(--default-color-white); + background-color: var(--color-blue); +} +.main-menu > a:not(:last-child) { + margin-right: 1rem; +} + +.content-width { + width: 100%; + max-width: var(--max-width); + margin-left: auto; + margin-right: auto; + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.responsive-typography h3 { + font-size: 24px; +} + +.responsive-typography > div, +.responsive-typography p, +.responsive-typography li { + font-size: 18px; + line-height: 1.5; +} + +.responsive-typography ul, +.responsive-typography ol { + width: 100%; +} + +.responsive-typography ul li, +.responsive-typography ol li { + position: relative; + width: 100%; + min-height: 1.5rem; + padding-left: 1.5rem; +} + +.responsive-typography ul li::before, +.responsive-typography ol li::before { + content: ''; + position: absolute; + top: 0; + left: 0; +} + +.responsive-typography ul > li::before { + background-color: var(--color-blue); + border-radius: 50%; + width: 0.5rem; + height: 0.5rem; + margin-top: 0.5rem; + margin-left: 0.25rem; +} + +.responsive-typography > * + * { + margin-top: 2rem; +} + +.responsive-typography > div + div, +.responsive-typography p + p { + margin-top: 1rem; +} +.responsive-typography li + li { + margin-top: 0.5rem; +}