chore: remove web
This commit is contained in:
parent
2234558d81
commit
d0c777424f
|
@ -1,14 +0,0 @@
|
||||||
/*
|
|
||||||
|
|
||||||
!/makefile
|
|
||||||
!/*ignore
|
|
||||||
|
|
||||||
!/Dockerfile
|
|
||||||
|
|
||||||
!/*json
|
|
||||||
!/*.ts
|
|
||||||
!/*.nix
|
|
||||||
|
|
||||||
!/(domain|repo|uikit|comp|views|translates)/*.ts
|
|
||||||
!/styles/*.scss
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
FROM denoland/deno:alpine-1.22.1
|
|
||||||
|
|
||||||
EXPOSE 33334
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
USER deno
|
|
||||||
|
|
||||||
ADD . .
|
|
||||||
# Compile the main app so that it doesn't need to be compiled each startup/entry.
|
|
||||||
RUN deno cache server.ts
|
|
||||||
|
|
||||||
CMD ["run", "-A", "server.ts"]
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { AnyNode, E } from "ren/node.ts";
|
|
||||||
import { Context } from "../context.ts";
|
|
||||||
|
|
||||||
export function Layout(ctx: Context, page: AnyNode): AnyNode {
|
|
||||||
return E("html", { lang: ctx.lang }, [
|
|
||||||
E("head", [], [
|
|
||||||
E("meta", { charset: "utf-8" }),
|
|
||||||
E("meta", {
|
|
||||||
name: "viewport",
|
|
||||||
content: "width=device-width, initial-scale=1",
|
|
||||||
}),
|
|
||||||
E("link", { rel: "stylesheet", href: "/styles/main.css" }),
|
|
||||||
E("title", [], "Recipes"),
|
|
||||||
]),
|
|
||||||
E("body", [], [
|
|
||||||
E("div", { id: "root" }, [page]),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
import { AnyNode, Attrs, E, Elem } from "ren/node.ts";
|
|
||||||
import { classNames } from "ren/attrs.ts";
|
|
||||||
import { Context, getLangHref, iterLangs, Lang } from "../context.ts";
|
|
||||||
|
|
||||||
export function PageLayout(ctx: Context, children: AnyNode[]): Elem {
|
|
||||||
return E("div", { id: "main" }, [
|
|
||||||
Header(ctx),
|
|
||||||
E("div", classNames("content"), children),
|
|
||||||
Footer(ctx),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Header(ctx: Context): AnyNode {
|
|
||||||
return E("header", classNames("header gap-v-1x5"), [
|
|
||||||
E("div", classNames("content-width"), [HeaderNav(ctx)]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HeaderNav(ctx: Context): AnyNode {
|
|
||||||
return E("nav", classNames("main-menu"), [
|
|
||||||
Link(ctx.tr.Home, navLink("/", ctx)),
|
|
||||||
Link(ctx.tr.Recipes, navLink("/recipes", ctx)),
|
|
||||||
Link(ctx.tr.Ingredients, navLink("/ingredients", ctx)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function navLink(lhref: string, ctx?: Context): Attrs {
|
|
||||||
const attrs: Attrs = { lhref };
|
|
||||||
if (ctx?.locPath === lhref) attrs["aria-current"] = "true";
|
|
||||||
return attrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Footer(ctx: Context): AnyNode {
|
|
||||||
return E("footer", classNames("footer"), [
|
|
||||||
E("div", classNames("content-width row-sta-bet"), [
|
|
||||||
Link(ctx.tr.Source_code, {
|
|
||||||
target: "_blank",
|
|
||||||
href: "https://notabug.org/pleshevskiy/recipes",
|
|
||||||
rel: "external nofollow noopener noreferrer",
|
|
||||||
}),
|
|
||||||
ChangeLang(ctx),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Link(text: string, attrs: Attrs | Attrs[]): AnyNode {
|
|
||||||
return E("a", attrs, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChangeLang(ctx: Context): AnyNode {
|
|
||||||
const dropdownId = "change_langs";
|
|
||||||
return E("div", classNames("dropdown"), [
|
|
||||||
E("input", { id: dropdownId, type: "checkbox" }),
|
|
||||||
E("label", { for: dropdownId }, ctx.lang),
|
|
||||||
E(
|
|
||||||
"ul",
|
|
||||||
[],
|
|
||||||
iterLangs().filter((l) => l !== ctx.lang).map((l) =>
|
|
||||||
ChangeLangBtn(ctx, l)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChangeLangBtn(ctx: Context, lang: Lang): AnyNode {
|
|
||||||
return E("a", { "href": getLangHref(lang, ctx.locPath) }, lang);
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { Translations } from "./translates/rus.ts";
|
|
||||||
|
|
||||||
export interface Context {
|
|
||||||
locPath: string;
|
|
||||||
lang: Lang;
|
|
||||||
tr: Translations;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLangHref(lang: Lang, url: string): string {
|
|
||||||
return getLangUrlPrefix(lang) + url;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLangUrlPrefix(lang: Lang): string {
|
|
||||||
return lang === Lang.Rus ? "" : `/${lang}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function iterLangs(): Lang[] {
|
|
||||||
return [Lang.Eng, Lang.Rus];
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Lang {
|
|
||||||
Rus = "rus",
|
|
||||||
Eng = "eng",
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["deno.ns", "dom"]
|
|
||||||
},
|
|
||||||
"importMap": "./import_map.json"
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export interface Ingredient {
|
|
||||||
readonly key: string;
|
|
||||||
readonly name: string;
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1654593855,
|
|
||||||
"narHash": "sha256-c+SyXvj7THre87OyIdZfRVR+HhI/g1ZDrQ3VUtTuHkU=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "033bd4fa9a8fbe0c68a88e925d9a884161044b25",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"utils": "utils"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"utils": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1653893745,
|
|
||||||
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
description = "Recipes web";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
|
||||||
utils.url = "github:numtide/flake-utils";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = {self, nixpkgs, utils}:
|
|
||||||
let out = system:
|
|
||||||
let pkgs = nixpkgs.legacyPackages."${system}";
|
|
||||||
in {
|
|
||||||
devShell = pkgs.mkShell {
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
nodePackages.sass
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in with utils.lib; eachSystem defaultSystems out;
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"imports": {
|
|
||||||
"ren/": "https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/1a67959b1a19598f8e95bf2ec5f7b1a4c3da4da6/ren/"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export function info(...args: unknown[]): void {
|
|
||||||
console.log("[INFO]", ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function debug(...args: unknown[]): void {
|
|
||||||
console.log("[DEBUG]", ...args);
|
|
||||||
}
|
|
23
web/makefile
23
web/makefile
|
@ -1,23 +0,0 @@
|
||||||
PAR := $(MAKE) -j 128
|
|
||||||
DOCKER_NAME := recipes
|
|
||||||
DOCKER_TAG := recipes
|
|
||||||
|
|
||||||
watch:
|
|
||||||
${PAR} deno-w sass-w
|
|
||||||
|
|
||||||
deno-w:
|
|
||||||
deno run -A --watch server.ts
|
|
||||||
|
|
||||||
sass-w:
|
|
||||||
sass -w styles/main.scss public/styles/main.css
|
|
||||||
|
|
||||||
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} .
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { Lang } from "../../context.ts";
|
|
||||||
import { Ingredient } from "../../domain/ingredient/types.ts";
|
|
||||||
import { RestLang } from "../misc_types.ts";
|
|
||||||
import { IngredientRepo } from "./types.ts";
|
|
||||||
|
|
||||||
export class RestIngredientRepo implements IngredientRepo {
|
|
||||||
async fetchIngredients(lang: Lang): Promise<Ingredient[]> {
|
|
||||||
const url = new URL("http://localhost:33333/api/ingredients");
|
|
||||||
url.searchParams.set("lang", lang);
|
|
||||||
|
|
||||||
const res = await fetch(
|
|
||||||
url.toString(),
|
|
||||||
{ headers: { "content-type": "application/json" } },
|
|
||||||
);
|
|
||||||
|
|
||||||
const restIngrs: RestIngredient[] = await res.json();
|
|
||||||
|
|
||||||
return restIngrs.map(intoAppIngredient).sort((a, b) =>
|
|
||||||
a.name.localeCompare(b.name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RestIngredient {
|
|
||||||
readonly key: string;
|
|
||||||
readonly lang: RestLang;
|
|
||||||
readonly name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function intoAppIngredient(rest: RestIngredient): Ingredient {
|
|
||||||
return {
|
|
||||||
key: rest.key,
|
|
||||||
name: rest.name,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { Lang } from "../../context.ts";
|
|
||||||
import { Ingredient } from "../../domain/ingredient/types.ts";
|
|
||||||
|
|
||||||
export interface IngredientRepo {
|
|
||||||
fetchIngredients(lang: Lang): Promise<Ingredient[]>;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export enum RestLang {
|
|
||||||
Rus = "Rus",
|
|
||||||
Eng = "Eng",
|
|
||||||
}
|
|
183
web/server.ts
183
web/server.ts
|
@ -1,183 +0,0 @@
|
||||||
import { StrRenderer } from "ren/str.ts";
|
|
||||||
import { Layout } from "./comp/layout.ts";
|
|
||||||
import { Context, getLangHref, Lang } 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";
|
|
||||||
import { RestIngredientRepo } from "./repo/ingredient/rest.ts";
|
|
||||||
import rusTranslates from "./translates/rus.ts";
|
|
||||||
import type { Translations } from "./translates/rus.ts";
|
|
||||||
import { E500Page } from "./views/e500.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 = await handleRequest(reqEvt.request);
|
|
||||||
reqEvt.respondWith(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRequest(req: Request): Promise<Response> {
|
|
||||||
log.info({ method: req.method, url: req.url });
|
|
||||||
|
|
||||||
if (req.method === "GET") {
|
|
||||||
return await handleGet(req);
|
|
||||||
} else {
|
|
||||||
return new Response("Method Not Allowed", { status: 405 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleGet(req: Request) {
|
|
||||||
const ctx = createContextFromRequest(req);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await tryCreateFileResponse(ctx.locPath);
|
|
||||||
return res;
|
|
||||||
} catch (_) {
|
|
||||||
if (ctx.lang !== Lang.Rus) {
|
|
||||||
await loadAndUpdateTranslations(ctx);
|
|
||||||
}
|
|
||||||
log.debug({ context: ctx });
|
|
||||||
|
|
||||||
const ren = new StrRenderer({
|
|
||||||
wrapNode: Layout.bind(null, ctx),
|
|
||||||
onVisitAttr: ([key, value]) => {
|
|
||||||
if (key === "lhref" && typeof value === "string") {
|
|
||||||
return ["href", getLangHref(ctx.lang, value)];
|
|
||||||
} else {
|
|
||||||
return [key, value];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
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") {
|
|
||||||
const repo = new RestIngredientRepo();
|
|
||||||
const ingredients = await repo.fetchIngredients(ctx.lang);
|
|
||||||
|
|
||||||
return createHtmlResponse(
|
|
||||||
ren.render(IngredientsPage(ctx, { ingredients })),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return createHtmlResponse(ren.render(E404Page(ctx)), 404);
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
return createHtmlResponse(ren.render(E500Page(ctx)), 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAndUpdateTranslations(ctx: Context) {
|
|
||||||
try {
|
|
||||||
const translates = await import(`./translates/${ctx.lang}.ts`);
|
|
||||||
ctx.tr = Object.entries(translates.default as Partial<Translations>)
|
|
||||||
.reduce(
|
|
||||||
(acc, [key, val]) => ({
|
|
||||||
...acc,
|
|
||||||
[key as keyof Translations]: val,
|
|
||||||
}),
|
|
||||||
{ ...ctx.tr } as Translations,
|
|
||||||
);
|
|
||||||
} catch (_e) { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
function createContextFromRequest(req: Request): Context {
|
|
||||||
const locUrl = new URL(req.url);
|
|
||||||
const lang = langFromUrl(locUrl);
|
|
||||||
|
|
||||||
return {
|
|
||||||
lang,
|
|
||||||
locPath: stripPrefix(`/${lang}`, locUrl.pathname),
|
|
||||||
tr: rusTranslates,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function langFromUrl(url: URL): Lang {
|
|
||||||
return url.pathname.startsWith("/eng/") ? Lang.Eng : Lang.Rus;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripPrefix(prefix: string, val: string): string {
|
|
||||||
return val.startsWith(prefix) ? val.slice(prefix.length) : val;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createHtmlResponse(body: string, status = 200): Response {
|
|
||||||
return new Response(body, {
|
|
||||||
status,
|
|
||||||
headers: getContentTypeHeader("html"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function tryCreateFileResponse(urlPath: string): Promise<Response> {
|
|
||||||
const filePath = extractFilePath(urlPath);
|
|
||||||
log.debug({ filePath });
|
|
||||||
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 {
|
|
||||||
return new Response(content, {
|
|
||||||
headers: getContentTypeHeader(fileExt),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractFilePath(urlPath: string): string | null {
|
|
||||||
const relPath = urlPath.slice(1);
|
|
||||||
if (relPath.startsWith("styles/")) {
|
|
||||||
return `public/${relPath}`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getContentTypeHeader(fileExt: string): Record<string, string> {
|
|
||||||
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(".") - 1 >>> 0) + 2);
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { Translations } from "./rus.ts";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
Home: "Home",
|
|
||||||
Recipes: "Recipes",
|
|
||||||
Ingredients: "Ingredients",
|
|
||||||
Source_code: "Source code",
|
|
||||||
Page_not_found: "Page not found",
|
|
||||||
Internal_server_error: "Internal server error",
|
|
||||||
} as Translations;
|
|
|
@ -1,12 +0,0 @@
|
||||||
export const rus = {
|
|
||||||
Home: "Главная",
|
|
||||||
Recipes: "Рецепты",
|
|
||||||
Ingredients: "Ингредиенты",
|
|
||||||
Source_code: "Исходный код",
|
|
||||||
Page_not_found: "Страница не найдена",
|
|
||||||
Internal_server_error: "Внутренняя ошибка сервера",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default rus;
|
|
||||||
|
|
||||||
export type Translations = typeof rus;
|
|
|
@ -1,6 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
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(ctx)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function E404(ctx: Context): AnyNode {
|
|
||||||
return E("div", classNames("content-width gap-v-1x5"), [
|
|
||||||
H3(ctx.tr.Page_not_found),
|
|
||||||
]);
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
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 E500Page(ctx: Context): AnyNode {
|
|
||||||
return PageLayout(ctx, [E500(ctx)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function E500(ctx: Context): AnyNode {
|
|
||||||
return E("div", classNames("content-width gap-v-1x5"), [
|
|
||||||
H3(ctx.tr.Internal_server_error),
|
|
||||||
]);
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
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 gap-v-1x5"), [
|
|
||||||
H3(ctx.tr.Home),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
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";
|
|
||||||
import { Ingredient } from "../domain/ingredient/types.ts";
|
|
||||||
|
|
||||||
interface IngredientsPageData {
|
|
||||||
ingredients: Ingredient[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IngredientsPage(
|
|
||||||
ctx: Context,
|
|
||||||
data: IngredientsPageData,
|
|
||||||
): AnyNode {
|
|
||||||
return PageLayout(ctx, [
|
|
||||||
E("div", classNames("content-width gap-v-1x5"), [
|
|
||||||
H3(ctx.tr.Ingredients),
|
|
||||||
IngredientList(data.ingredients),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IngredientList(ingrs: Ingredient[]): AnyNode {
|
|
||||||
return E("div", classNames("responsive-typography"), [
|
|
||||||
E("ul", [], ingrs.map(IngredientItem)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IngredientItem(ingr: Ingredient): AnyNode {
|
|
||||||
return E("li", [], [
|
|
||||||
ingr.name,
|
|
||||||
// E("a", { href: `/ingredients/${ingr.key}` }, ingr.name),
|
|
||||||
]);
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
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 gap-v-1x5"), [
|
|
||||||
H3(ctx.tr.Recipes),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
Loading…
Reference in New Issue