chore: remove web

This commit is contained in:
Dmitriy Pleshevskiy 2023-03-16 15:51:50 +03:00
parent 2234558d81
commit d0c777424f
Signed by: pleshevskiy
GPG Key ID: 79C4487B44403985
24 changed files with 0 additions and 593 deletions

14
web/.gitignore vendored
View File

@ -1,14 +0,0 @@
/*
!/makefile
!/*ignore
!/Dockerfile
!/*json
!/*.ts
!/*.nix
!/(domain|repo|uikit|comp|views|translates)/*.ts
!/styles/*.scss

View File

@ -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"]

View File

@ -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]),
]),
]);
}

View File

@ -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);
}

View File

@ -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",
}

View File

@ -1,6 +0,0 @@
{
"compilerOptions": {
"lib": ["deno.ns", "dom"]
},
"importMap": "./import_map.json"
}

View File

@ -1,4 +0,0 @@
export interface Ingredient {
readonly key: string;
readonly name: string;
}

View File

@ -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
}

View File

@ -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;
}

View File

@ -1,5 +0,0 @@
{
"imports": {
"ren/": "https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/1a67959b1a19598f8e95bf2ec5f7b1a4c3da4da6/ren/"
}
}

View File

@ -1,7 +0,0 @@
export function info(...args: unknown[]): void {
console.log("[INFO]", ...args);
}
export function debug(...args: unknown[]): void {
console.log("[DEBUG]", ...args);
}

View File

@ -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} .

View File

@ -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,
};
}

View File

@ -1,6 +0,0 @@
import { Lang } from "../../context.ts";
import { Ingredient } from "../../domain/ingredient/types.ts";
export interface IngredientRepo {
fetchIngredients(lang: Lang): Promise<Ingredient[]>;
}

View File

@ -1,4 +0,0 @@
export enum RestLang {
Rus = "Rus",
Eng = "Eng",
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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),
]);
}

View File

@ -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),
]);
}

View File

@ -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),
]),
]);
}

View File

@ -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),
]);
}

View File

@ -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),
]),
]);
}