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