From 708b13cf1784cdc44987e760a199a6649809c71f Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sun, 22 May 2022 15:13:30 +0300 Subject: [PATCH] web: initial web structure --- web/comp/layout.ts | 17 ++++++++++ web/comp/page_layout.ts | 39 +++++++++++++++++++++++ web/context.ts | 3 ++ web/deno.json | 6 ++++ web/import_map.json | 5 +++ web/log.ts | 7 +++++ web/makefile | 35 +++++++++++++++++++++ web/server.ts | 67 ++++++++++++++++++++++++++++++++++++++++ web/uikit/typo.ts | 6 ++++ web/views/e404.ts | 15 +++++++++ web/views/home.ts | 13 ++++++++ web/views/ingredients.ts | 13 ++++++++ web/views/recipes.ts | 13 ++++++++ 13 files changed, 239 insertions(+) create mode 100644 web/comp/layout.ts create mode 100644 web/comp/page_layout.ts create mode 100644 web/context.ts create mode 100644 web/deno.json create mode 100644 web/import_map.json create mode 100644 web/log.ts create mode 100644 web/makefile create mode 100644 web/server.ts create mode 100644 web/uikit/typo.ts create mode 100644 web/views/e404.ts create mode 100644 web/views/home.ts create mode 100644 web/views/ingredients.ts create mode 100644 web/views/recipes.ts diff --git a/web/comp/layout.ts b/web/comp/layout.ts new file mode 100644 index 0000000..430dd77 --- /dev/null +++ b/web/comp/layout.ts @@ -0,0 +1,17 @@ +import { AnyNode, E, Elem } from "ren/node.ts"; + +export function Layout(page: AnyNode): Elem { + return E("html", { lang: "ru" }, [ + E("head", [], [ + E("meta", { charset: "utf-8" }), + E("meta", { + name: "viewport", + content: "width=device-width, initial-scale=1", + }), + E("title", [], "Recipes"), + ]), + E("body", [], [ + E("div", { id: "root" }, [page]), + ]), + ]); +} diff --git a/web/comp/page_layout.ts b/web/comp/page_layout.ts new file mode 100644 index 0000000..3830548 --- /dev/null +++ b/web/comp/page_layout.ts @@ -0,0 +1,39 @@ +import { AnyNode, Attrs, E, Elem } from "ren/node.ts"; +import { classNames } from "ren/attrs.ts"; +import { Context } from "../context.ts"; + +export function PageLayout(ctx: Context, children: AnyNode[]): Elem { + return E("div", { id: "main" }, [ + Header(ctx), + E("div", classNames("content"), children), + // Footer(), + ]); +} + +export function Header(ctx: Context): Elem { + return E("header", classNames("header"), [ + E("div", classNames("content-width"), [HeaderNav(ctx)]), + ]); +} + +export function HeaderNav(ctx: Context): Elem { + return E("nav", classNames("main-menu"), [ + Link(navLink("/", ctx), "Главная"), + Link(navLink("/recipes", ctx), "Рецепты"), + Link(navLink("/ingredients", ctx), "Ингредиенты"), + ]); +} + +export function Link(attrs: Attrs | Attrs[], text: string): Elem { + return E("a", attrs, text); +} + +function navLink(href: string, ctx?: Context): Attrs { + const attrs: Attrs = { href }; + if (ctx?.locPath === href) attrs["aria-current"] = "true"; + return attrs; +} + +export function Footer(): Elem { + return E("footer", classNames("footer"), "footer"); +} diff --git a/web/context.ts b/web/context.ts new file mode 100644 index 0000000..96a571b --- /dev/null +++ b/web/context.ts @@ -0,0 +1,3 @@ +export interface Context { + locPath: string; +} diff --git a/web/deno.json b/web/deno.json new file mode 100644 index 0000000..f76bf0a --- /dev/null +++ b/web/deno.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "lib": ["deno.ns", "dom"] + }, + "importMap": "./import_map.json" +} diff --git a/web/import_map.json b/web/import_map.json new file mode 100644 index 0000000..e47e129 --- /dev/null +++ b/web/import_map.json @@ -0,0 +1,5 @@ +{ + "imports": { + "ren/": "https://notabug.org/pleshevskiy/ren/raw/v2/ren/" + } +} diff --git a/web/log.ts b/web/log.ts new file mode 100644 index 0000000..7d17f05 --- /dev/null +++ b/web/log.ts @@ -0,0 +1,7 @@ +export function info(...args: unknown[]): void { + console.log("[INFO]", ...args); +} + +export function debug(...args: unknown[]): void { + console.log("[DEBUG]", ...args); +} diff --git a/web/makefile b/web/makefile new file mode 100644 index 0000000..909e4b1 --- /dev/null +++ b/web/makefile @@ -0,0 +1,35 @@ +PAR := $(MAKE) -j 128 +DOCKER_NAME := pleshevski +DOCKER_TAG := pleshevski + + +watch: + $(PAR) hr ts-w + +docker-restart: docker-stop docker-run + +docker-stop: + docker rm ${DOCKER_NAME} --force + +docker-run: + docker run -d --restart always -p 30000:30000 --name ${DOCKER_NAME} ${DOCKER_TAG} + +docker-build: + docker build -t ${DOCKER_TAG} . + +build: ts + +start: + npm run start + +hr: + deno run -A ~/sandbox/hr/server.ts target static + +ts: + npm run build + +ts-w: + NODE_ENV=develop npx tsc-watch --onSuccess "make start" + +clean: + rm -rf target diff --git a/web/server.ts b/web/server.ts new file mode 100644 index 0000000..cf4fbcd --- /dev/null +++ b/web/server.ts @@ -0,0 +1,67 @@ +import { StrRenderer } from "ren/str.ts"; +import { Layout } from "./comp/layout.ts"; +import { Context } from "./context.ts"; +import { E404Page } from "./views/e404.ts"; +import * as log from "./log.ts"; +import { HomePage } from "./views/home.ts"; +import { RecipesPage } from "./views/recipes.ts"; +import { IngredientsPage } from "./views/ingredients.ts"; + +if (import.meta.main) { + await main(); +} + +async function main() { + await startServer({ port: 33334 }); +} + +async function startServer(cfg: ServerConfig) { + const srv = Deno.listen({ hostname: "localhost", port: cfg.port }); + log.info(`Server listening at http://localhost:${cfg.port}`); + + for await (const conn of srv) { + serveHttp(conn); + } +} + +interface ServerConfig { + port: number; +} + +async function serveHttp(conn: Deno.Conn) { + const httpConn = Deno.serveHttp(conn); + + for await (const reqEvt of httpConn) { + const res = handleRequest(reqEvt.request); + reqEvt.respondWith(res); + } +} + +function handleRequest(req: Request): Response { + log.debug({ url: req.url }); + const ren = new StrRenderer(); + const ctx = createContextFromRequest(req); + + if (ctx.locPath === "/") { + return createHtmlResponse(ren.render(Layout(HomePage(ctx)))); + } else if (ctx.locPath === "/recipes") { + return createHtmlResponse(ren.render(Layout(RecipesPage(ctx)))); + } else if (ctx.locPath === "/ingredients") { + return createHtmlResponse(ren.render(Layout(IngredientsPage(ctx)))); + } else { + return createHtmlResponse(ren.render(Layout(E404Page(ctx))), 404); + } +} + +function createContextFromRequest(req: Request): Context { + return { + locPath: new URL(req.url).pathname, + }; +} + +function createHtmlResponse(body: string, status = 200): Response { + return new Response(body, { + status, + headers: { "content-type": "text/html" }, + }); +} diff --git a/web/uikit/typo.ts b/web/uikit/typo.ts new file mode 100644 index 0000000..5f95691 --- /dev/null +++ b/web/uikit/typo.ts @@ -0,0 +1,6 @@ +import { classNames } from "ren/attrs.ts"; +import { E, Elem } from "ren/node.ts"; + +export function H3(text: string): Elem { + return E("h3", classNames("font-h3"), text); +} diff --git a/web/views/e404.ts b/web/views/e404.ts new file mode 100644 index 0000000..94193f9 --- /dev/null +++ b/web/views/e404.ts @@ -0,0 +1,15 @@ +import { PageLayout } from "../comp/page_layout.ts"; +import { AnyNode, E } from "ren/node.ts"; +import { classNames } from "ren/attrs.ts"; +import { Context } from "../context.ts"; +import { H3 } from "../uikit/typo.ts"; + +export function E404Page(ctx: Context): AnyNode { + return PageLayout(ctx, [E404()]); +} + +export function E404(): AnyNode { + return E("div", classNames("content-width"), [ + H3("Страница не найдена"), + ]); +} diff --git a/web/views/home.ts b/web/views/home.ts new file mode 100644 index 0000000..c36e917 --- /dev/null +++ b/web/views/home.ts @@ -0,0 +1,13 @@ +import { PageLayout } from "../comp/page_layout.ts"; +import { AnyNode, E } from "ren/node.ts"; +import { classNames } from "ren/attrs.ts"; +import { Context } from "../context.ts"; +import { H3 } from "../uikit/typo.ts"; + +export function HomePage(ctx: Context): AnyNode { + return PageLayout(ctx, [ + E("div", classNames("content-width"), [ + H3("Главная"), + ]), + ]); +} diff --git a/web/views/ingredients.ts b/web/views/ingredients.ts new file mode 100644 index 0000000..fa06b92 --- /dev/null +++ b/web/views/ingredients.ts @@ -0,0 +1,13 @@ +import { PageLayout } from "../comp/page_layout.ts"; +import { AnyNode, E } from "ren/node.ts"; +import { classNames } from "ren/attrs.ts"; +import { Context } from "../context.ts"; +import { H3 } from "../uikit/typo.ts"; + +export function IngredientsPage(ctx: Context): AnyNode { + return PageLayout(ctx, [ + E("div", classNames("content-width"), [ + H3("Ингредиенты"), + ]), + ]); +} diff --git a/web/views/recipes.ts b/web/views/recipes.ts new file mode 100644 index 0000000..50cb9f4 --- /dev/null +++ b/web/views/recipes.ts @@ -0,0 +1,13 @@ +import { PageLayout } from "../comp/page_layout.ts"; +import { AnyNode, E } from "ren/node.ts"; +import { classNames } from "ren/attrs.ts"; +import { Context } from "../context.ts"; +import { H3 } from "../uikit/typo.ts"; + +export function RecipesPage(ctx: Context): AnyNode { + return PageLayout(ctx, [ + E("div", classNames("content-width"), [ + H3("Рецепты"), + ]), + ]); +}