diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d794bd4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +/* + +!/public +!/styles +!/translates +!/views + +!/*.ts +!/*.json + diff --git a/.gitignore b/.gitignore index 4644e4c..0262266 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,18 @@ /* -# editors -!/.vscode - -# ignores !/*ignore -# config !/*file -!/*.yml -!/*.json !/*.nix +!/*.yml + +!/*.json +!/*.ts + +!/public +!/styles +!/translates +!/views + +# misc +.DS_Store -# sources -!/src -!/static diff --git a/context.ts b/context.ts new file mode 100644 index 0000000..6b88679 --- /dev/null +++ b/context.ts @@ -0,0 +1,24 @@ +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", +} diff --git a/log.ts b/log.ts new file mode 100644 index 0000000..7d17f05 --- /dev/null +++ b/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/public/styles/main.css b/public/styles/main.css new file mode 100644 index 0000000..42f42dc --- /dev/null +++ b/public/styles/main.css @@ -0,0 +1,546 @@ +@charset "UTF-8"; +:root { + --default-color-black: #000000; + --default-color-black-0: hsla(0, 0%, 0%, 0); + --default-color-black-0x15: hsla(0, 0%, 0%, 0.15); + --default-color-black-0x6: hsla(0, 0%, 0%, 0.6); + --default-color-white: #FFFFFF; + --default-color-warning: #ffee58; + --default-color-error: #b00008; + --default-color-success: #417505; + --color-brand-blue: #1966df; + --color-graphite: #212121; + --color-warm-gray: #757575; + --color-pale: #B6B6B6; + --color-faded: #E0E0E0; + --max-width: 1440px; + --min-width: 320px; + --rad-std-half: 0.125rem; + --rad-std: 0.25rem; + --rad-std-x2: 0.5rem; + --rad-std-x3: 0.75rem; + --default-font-size: 16px; + --f-family: system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Roboto,Oxygen-Sans, Ubuntu, Cantarell, "Segoe UI", Verdana, sans-serif; + --f-wei-thin: 100; + --f-wei-reg: 400; + --f-wei-bold: 700; + --f-wei-black: 800; + --z-ind-background: -100; + --z-ind-backward: -1; + --z-ind-select: 50; + --z-ind-tooltip: 75; + --z-ind-high: 100; + --z-ind-overlay: 500; +} + +*, +::before, +::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + background-color: var(--default-color-white); + font-size: var(--default-font-size); + line-height: 1; + height: 100%; + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + color: var(--color-graphite); + font-weight: var(--f-wei-regular); + font-family: var(--f-family); + min-width: var(--min-width); + width: 100%; + height: 100%; +} + +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} + +details > summary { + cursor: pointer; + list-style: none; +} +details > summary::before, details > summary::-webkit-details-marker { + display: none; +} + +audio, canvas, progress, video { + display: inline-block; + vertical-align: baseline; +} + +audio:not([controls]) { + display: none; + height: 0; +} + +address, caption, cite, code, dfn, strong, th, var { + font-style: normal; + font-weight: var(--f-wei-regular); +} + +h1, h2, h3, h4, h5, h6 { + font-weight: var(--f-wei-bold); + line-height: 1; +} + +b, strong, optgroup { + font-weight: var(--f-wei-bold); +} + +dfn, em, i { + font-style: italic; +} + +iframe, abbr, acronym, img { + border: 0; + outline: 0; +} + +mark { + background: var(--default-color-warning); + color: var(--default-color-black); +} + +small { + font-size: 80%; +} + +sub, sup { + font-size: 80%; + vertical-align: baseline; + line-height: 0; + position: relative; +} + +sup { + top: -0.25em; +} + +sub { + bottom: -0.25em; +} + +q::before, q::after { + content: ""; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +code, kbd, pre, samp { + font-family: monospace; + font-size: 1em; +} + +pre { + overflow: auto; +} + +figure { + margin: 0; +} + +fieldset { + border: 1px solid var(--color-pale); + padding: 0.25rem 0.75rem; +} + +legend { + border: 0; + color: inherit; + display: table; + word-spacing: normal; + max-width: 100%; +} + +button, input, optgroup, select, textarea { + color: inherit; + font: inherit; +} + +button, input { + overflow: visible; +} + +button, select { + text-transform: none; +} + +input { + line-height: 1; +} + +button::-moz-focus-inner, +[type=button]::-moz-focus-inner, +[type=reset]::-moz-focus-inner, +[type=submit]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +button:-moz-focusring, +[type=button]:-moz-focusring, +[type=reset]:-moz-focusring, +[type=submit]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +input, input:focus, +button, textarea, +select, a:focus { + outline: 0; + border: 0; +} + +input::-webkit-input-placeholder, +input:-moz-placeholder, +textarea::-webkit-input-placeholder, +textarea:-moz-placeholder { + color: var(--color-pale); +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + height: auto; +} + +input[type=search] { + -webkit-appearance: textfield; + outline-offset: -2px; +} + +input[type=search]::-webkit-search-cancel-button, +input[type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + -webkit-appearance: button; + color: inherit; +} + +button, +input[type=button], +input[type=reset], +input[type=submit] { + -webkit-appearance: button; + cursor: pointer; +} + +button[disabled], input[disabled] { + cursor: no-drop; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + outline: 0; +} + +textarea { + overflow: auto; + resize: none; +} + +a { + cursor: pointer; + text-decoration: none; + color: inherit; +} +a:hover, a:focus { + color: var(--color-brand-blue); +} + +table { + border-collapse: collapse; + border-spacing: 0; + table-layout: fixed; +} + +ul, ol { + list-style: none; + margin: 0; + padding: 0; +} + +[hidden], template { + display: none; +} + +svg:not(:root) { + overflow: hidden; +} + +::selection { + background-color: var(--default-color-black); + color: var(--default-color-white); +} + +.footer { + border-top: 1px solid var(--color-faded); +} + +.maw-100p { + max-width: 100%; +} + +.maw-content, .content-width { + max-width: var(--max-content-width); +} + +.miw-content, #main { + min-width: var(--min-content-width); +} + +.w-100p, .responsive-typography ul li, .responsive-typography ol li, .responsive-typography ul, .responsive-typography ol, #root, #main, .content, .content-width { + width: 100%; +} + +.mah-100p { + max-height: 100%; +} + +.mah-100vh { + max-height: 100vh; +} + +.mih-100vh, #root { + min-height: 100vh; +} + +.mar-ha, .content-width { + margin-left: auto; + margin-right: auto; +} + +.pad-0x5, .main-menu > a { + padding: 0.5rem; +} + +.pad-h-1x25, .content-width { + padding-right: 1.25rem; + padding-left: 1.25rem; +} + +.pad-v-1, .header, .footer { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.gap-v-1x5 > :not([hidden]):not(.hidden-input) + :not([hidden]):not(.hidden-input), #main > :not([hidden]):not(.hidden-input) + :not([hidden]):not(.hidden-input) { + margin-top: 1.5rem; +} + +.gap-h-0x5 > :not(:last-child):not(:only-child) { + margin-right: 0.5rem; +} + +.gap-h-1 > :not(:last-child):not(:only-child), .main-menu > :not(:last-child):not(:only-child) { + margin-right: 1rem; +} + +.row-sta-sta, .main-menu { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; +} + +.row-sta-bet { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; +} + +.col-sta-sta { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; +} + +.col-str-sta, #root, #main { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; +} + +.flex-1, #main, .content { + -webkit-box-flex: 1 0; + -ms-flex: 1 0; + flex: 1 0; +} + +.responsive-typography h3 { + font-size: 24px; + margin: 1.5rem 0; +} +.responsive-typography > div, .responsive-typography p, .responsive-typography li { + font-size: 18px; + line-height: 1.5; +} +.responsive-typography ul li, .responsive-typography ol li { + position: relative; + 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, .responsive-typography ol > li::before { + background-color: var(--color-brand-blue); + border-radius: 50%; + width: 0.5rem; + height: 0.5rem; + margin-top: 0.5rem; + margin-left: 0.25rem; +} +.responsive-typography ul > * + *, .responsive-typography ol > * + * { + margin-top: 2rem; +} +.responsive-typography > div + div, .responsive-typography p + p { + margin-top: 1rem; +} +.responsive-typography li + li { + margin-top: 0.5rem; +} + +.anim, .main-menu > a, a, .anim::before, .main-menu > a::before, a::before, .anim::after, .main-menu > a::after, a::after { + transition: all 0.2s ease-in-out; +} + +.main-menu > a { + color: var(--color-brand-blue); + border-radius: 6px; + border: 1px solid var(--color-brand-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-brand-blue); +} + +/* dropdown + * Source: https://codepen.io/markcaron/pen/wdVmpB + * + * TODO: change styles + * */ +.dropdown { + position: relative; + display: inline-block; +} +.dropdown > input[type=checkbox] { + position: absolute; + left: -100vw; +} +.dropdown > label { + display: inline-block; + padding: 6px 15px; + color: #333; + line-height: 1.5em; + text-decoration: none; + border: 1px solid #8c8c8c; + cursor: pointer; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.dropdown > label:hover { + border-color: #333; +} +.dropdown > label:after { + content: "▲"; + font-size: 10px; + display: inline-block; + margin-left: 6px; + vertical-align: top; +} +.dropdown > ul { + position: absolute; + z-index: 999; + display: block; + left: -100vw; + bottom: calc(1.5em + 14px); + border: 1px solid #8c8c8c; + background: #fff; + padding: 6px 0; + margin: 0; + list-style: none; + width: 100%; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15); +} +.dropdown > ul a { + display: block; + padding: 6px 15px; + text-decoration: none; + color: #333; +} +.dropdown > ul a:hover, .dropdown > ul a:focus { + background: #ececec; +} +.dropdown > input[type=checkbox]:checked ~ ul { + left: 0; +} +.dropdown > input[type=checkbox]:checked + label:after { + content: "▼"; +} + +/*# sourceMappingURL=main.css.map */ diff --git a/public/styles/main.css.map b/public/styles/main.css.map new file mode 100644 index 0000000..a2f9c4f --- /dev/null +++ b/public/styles/main.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../../styles/base/reset.scss","../../styles/base/layout.scss","../../styles/atoms/sizes.scss","../../styles/atoms/white-spaces.scss","../../styles/mixins/white-spaces.scss","../../styles/atoms/flex.scss","../../styles/mixins/flex.scss","../../styles/atoms/typography.scss","../../styles/atoms/misc.scss","../../styles/uikit/main-menu.scss","../../styles/uikit/dropdown.scss"],"names":[],"mappings":";AAEA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;EAGE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAKF;EACE;EACA;;AAEA;EAEE;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAGF;AAAA;AAAA;AAAA;EAIE;;;AAGF;AAAA;AAAA;EAGE;EACA;;;AAGF;AAAA;AAAA;AAAA;EAIE;;;AAGF;AAAA;EAEE;;;AAGF;EACE;EACA;;;AAGF;AAAA;EAEE;;;AAGF;EACE;EACA;;;AAGF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAGF;EACE;;;AAGF;AAAA;EAEE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;AAKA;EACE;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;ACvSF;EAEE;;;ACTF;EAAe;;;AACf;EAAe;;;AACf;EAAe;;;AACf;EAAU;;;AAEV;EAAa;;;AACb;EAAa;;;AACb;EAAa;;;ACPb;ECQgB;EAAmB;;;ADNnC;ECQmB;;;ADPnB;ECWqB;EAEA;;;ADZrB;ECSqB;EAEA;;;AAInB;EAfmB;;;AAmBnB;EAlBmB;;;AAkBnB;EAlBmB;;;ACLrB;ECCE;EACA;EACA;EAWA;EACA;EACA;EACA;EA4CA;EACA;EACA;EAoCA;EACA;EACA;;;ADpGF;ECAE;EACA;EACA;EAWA;EACA;EACA;EACA;EAmEA;EACA;EACA;EAaA;EACA;EACA;;;ADnGF;ECDE;EACA;EACA;EAmBA;EACA;EACA;EACA;EAoCA;EACA;EACA;EAoCA;EACA;EACA;;;ADlGF;ECFE;EACA;EACA;EAmBA;EACA;EACA;EACA;EAoCA;EACA;EACA;EAsDA;EACA;EACA;;;ADlHF;ECuCE,kBDvCsB;ECwCtB,UDxCsB;ECyCtB,MDzCsB;;;AEHtB;EACE;EACA;;AAGF;EACE;EACA;;AAMA;EAGE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAIJ;EACE;;AAGF;EACE;;;ACjDJ;EACE;;;ACGA;EAIE;EACA;EACA;EACA;;AAEA;EAEE;EACA;;;ACfN;AAAA;AAAA;AAAA;AAAA;AAMA;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAMJ;EACE;;AAGF;EACE","file":"main.css"} \ No newline at end of file diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..c281d56 --- /dev/null +++ b/server.ts @@ -0,0 +1,177 @@ +import { StrRenderer } from "ren/str.ts"; +import * as log from "./log.ts"; +import rusTranslates from "./translates/rus.ts"; +import type { Translations } from "./translates/rus.ts"; +import { Context, getLangHref, Lang } from "./context.ts"; +import { E404Page } from "./views/pages/e404.ts"; +import { E500Page } from "./views/pages/e500.ts"; +import { AboutPage } from "./views/pages/about.ts"; +import { WorksPage } from "./views/pages/works.ts"; +import { Layout } from "./views/comp/layout.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 { + 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 === "/" || ctx.locPath === "/about") { + return createHtmlResponse(ren.render(AboutPage(ctx))); + } else if (ctx.locPath === "/works") { + return createHtmlResponse(ren.render(WorksPage(ctx))); + } 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) + .reduce( + (acc, [key, val]) => ({ + ...acc, + [key as keyof Translations]: val, + }), + { ...ctx.tr } as Translations, + ); + } catch (err) { + log.debug({ err }); + /* 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 { + 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 { + 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); +} diff --git a/styles/atoms/flex.scss b/styles/atoms/flex.scss new file mode 100644 index 0000000..968a8a1 --- /dev/null +++ b/styles/atoms/flex.scss @@ -0,0 +1,6 @@ +.row-sta-sta { @include flex-layout(row, sta, sta) } +.row-sta-bet { @include flex-layout(row, sta, bet) } +.col-sta-sta { @include flex-layout(col, sta, sta) } +.col-str-sta { @include flex-layout(col, str, sta) } + +.flex-1 { @include flex(1 0) } diff --git a/styles/atoms/misc.scss b/styles/atoms/misc.scss new file mode 100644 index 0000000..8b8987e --- /dev/null +++ b/styles/atoms/misc.scss @@ -0,0 +1,3 @@ +.anim, .anim::before, .anim::after { + transition: all 0.2s ease-in-out; +} diff --git a/styles/atoms/mod.scss b/styles/atoms/mod.scss new file mode 100644 index 0000000..172a120 --- /dev/null +++ b/styles/atoms/mod.scss @@ -0,0 +1,5 @@ +@import 'sizes'; +@import 'white-spaces'; +@import 'flex'; +@import 'typography'; +@import 'misc'; diff --git a/styles/atoms/sizes.scss b/styles/atoms/sizes.scss new file mode 100644 index 0000000..f8c5060 --- /dev/null +++ b/styles/atoms/sizes.scss @@ -0,0 +1,9 @@ +.maw-100p { max-width: 100% } +.maw-content { max-width: var(--max-content-width) } +.miw-content { min-width: var(--min-content-width) } +.w-100p { width: 100% } + +.mah-100p { max-height: 100% } +.mah-100vh { max-height: 100vh } +.mih-100vh { min-height: 100vh } + diff --git a/styles/atoms/typography.scss b/styles/atoms/typography.scss new file mode 100644 index 0000000..43f8fbd --- /dev/null +++ b/styles/atoms/typography.scss @@ -0,0 +1,57 @@ +.responsive-typography { + + h3 { + font-size: 24px; + margin: 1.5rem 0; + } + + > div, p, li { + font-size: 18px; + line-height: 1.5; + } + + ul, ol { + @extend .w-100p; + + li { + @extend .w-100p; + + position: relative; + min-height: 1.5rem; + padding-left: 1.5rem; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + } + } + + > li::before { + background-color: var(--color-brand-blue); + border-radius: 50%; + width: 0.5rem; + height: 0.5rem; + margin-top: 0.5rem; + margin-left: 0.25rem; + } + + > * + * { + margin-top: 2rem; + } + } + + > div + div, p + p { + margin-top: 1rem; + } + + li + li { + margin-top: 0.5rem; + } + +} + + + + diff --git a/styles/atoms/white-spaces.scss b/styles/atoms/white-spaces.scss new file mode 100644 index 0000000..be309bf --- /dev/null +++ b/styles/atoms/white-spaces.scss @@ -0,0 +1,11 @@ +.mar-ha { @include mar-ha } + +.pad-0x5 { @include pad(0.5) } +.pad-h-1x25 { @include pad-h(1.25) } +.pad-v-1 { @include pad-v(1) } + +.gap-v-1x5 { @include gap-v(1.5) } + +.gap-h-0x5 { @include gap-h(0.5) } +.gap-h-1 { @include gap-h(1) } + diff --git a/styles/base/layout.scss b/styles/base/layout.scss new file mode 100644 index 0000000..749785c --- /dev/null +++ b/styles/base/layout.scss @@ -0,0 +1,11 @@ +#root { @extend .col-str-sta, .w-100p, .mih-100vh } +#main { @extend .col-str-sta, .flex-1, .miw-content, .w-100p, .gap-v-1x5 } +.content { @extend .flex-1, .w-100p } +.content-width { @extend .maw-content, .w-100p, .mar-ha, .pad-h-1x25 } + +.header { @extend .pad-v-1 } + +.footer { + @extend .pad-v-1; + border-top: 1px solid var(--color-faded); +} diff --git a/styles/base/reset.scss b/styles/base/reset.scss new file mode 100644 index 0000000..2c801e4 --- /dev/null +++ b/styles/base/reset.scss @@ -0,0 +1,305 @@ +@use "sass:math"; + +:root { + // Default Colors + --default-color-black: #000000; + --default-color-black-0: hsla(0, 0%, 0%, 0); + --default-color-black-0x15: hsla(0, 0%, 0%, 0.15); + --default-color-black-0x6: hsla(0, 0%, 0%, 0.6); + --default-color-white: #FFFFFF; + --default-color-warning: #ffee58; + --default-color-error: #b00008; + --default-color-success: #417505; + // Project Colors + --color-brand-blue: #1966df; + --color-graphite: #212121; + --color-warm-gray: #757575; + --color-pale: #B6B6B6; + --color-faded: #E0E0E0; + // Layout + --max-width: #{$page-max-width + px}; + --min-width: #{$page-min-width + px}; + // Borders + --rad-std-half: #{math.div($radius, 2)}; + --rad-std: #{$radius}; + --rad-std-x2: #{$radius * 2}; + --rad-std-x3: #{$radius * 3}; + // Font + --default-font-size: 16px; + --f-family: system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Roboto,Oxygen-Sans, Ubuntu, Cantarell, "Segoe UI", Verdana, sans-serif; + --f-wei-thin: 100; + --f-wei-reg: 400; // Normal = Regular + --f-wei-bold: 700; + --f-wei-black: 800; // Extra Bold = Black + // Z-index + --z-ind-background: -100; + --z-ind-backward: -1; + --z-ind-select: 50; + --z-ind-tooltip: 75; + --z-ind-high: 100; + --z-ind-overlay: 500; +} + +*, +::before, +::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + background-color: var(--default-color-white); + font-size: var(--default-font-size); + line-height: 1; + height: 100%; + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + color: var(--color-graphite); + font-weight: var(--f-wei-regular); + font-family: var(--f-family); + min-width: var(--min-width); + width: 100%; + height: 100%; +} + +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} + +summary {} + +details > summary { + cursor: pointer; + list-style: none; + + &::before, + &::-webkit-details-marker { + display: none; + } +} + +audio, canvas, progress, video { + display: inline-block; + vertical-align: baseline; +} + +audio:not([controls]) { + display: none; + height: 0; +} + +address, caption, cite, code, dfn, strong, th, var { + font-style: normal; + font-weight: var(--f-wei-regular); +} + +h1, h2, h3, h4, h5, h6 { + font-weight: var(--f-wei-bold); + line-height: 1; +} + +b, strong, optgroup { + font-weight: var(--f-wei-bold); +} + +dfn, em, i { + font-style: italic; +} + +iframe, abbr, acronym, img { + border: 0; + outline: 0; +} + +mark { + background: var(--default-color-warning); + color: var(--default-color-black); +} + +small { + font-size: 80%; +} + +sub, sup { + font-size: 80%; + vertical-align: baseline; + line-height: 0; + position: relative; +} + +sup { + top: -0.25em; +} + +sub { + bottom: -0.25em; +} + +q::before, q::after { + content: ''; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +code, kbd, pre, samp { + font-family: monospace; + font-size: 1em; +} + +pre { + overflow: auto; +} + +figure { + margin: 0; +} + +fieldset { + border: 1px solid var(--color-pale); + padding: 0.25rem 0.75rem; +} + +legend { + border: 0; + color: inherit; + display: table; + word-spacing: normal; + max-width: 100%; +} + +button, input, optgroup, select, textarea { + color: inherit; + font: inherit; +} + +button, input { + overflow: visible; +} + +button, select { + text-transform: none; +} + +input { + line-height: 1; +} + +button::-moz-focus-inner, +[type='button']::-moz-focus-inner, +[type='reset']::-moz-focus-inner, +[type='submit']::-moz-focus-inner { + border-style: none; + padding: 0; +} + +button:-moz-focusring, +[type='button']:-moz-focusring, +[type='reset']:-moz-focusring, +[type='submit']:-moz-focusring { + outline: 1px dotted ButtonText; +} + +input, input:focus, +button, textarea, +select, a:focus { + outline: 0; + border: 0; +} + +input::-webkit-input-placeholder, +input:-moz-placeholder, +textarea::-webkit-input-placeholder, +textarea:-moz-placeholder { + color: var(--color-pale); +} + +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + height: auto; +} + +input[type='search'] { + -webkit-appearance: textfield; + outline-offset: -2px; +} + +input[type='search']::-webkit-search-cancel-button, +input[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + -webkit-appearance: button; + color: inherit; +} + +button, +input[type='button'], +input[type='reset'], +input[type='submit'] { + -webkit-appearance: button; + cursor: pointer; +} + +button[disabled], input[disabled] { + cursor: no-drop; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + outline: 0; +} + +textarea { + overflow: auto; + resize: none; +} + +a { + cursor: pointer; + text-decoration: none; + color: inherit; + + // TODO: move to other place + @extend .anim; + + &:hover, &:focus { + color: var(--color-brand-blue); + } +} + +table { + border-collapse: collapse; + border-spacing: 0; + table-layout: fixed; +} + +ul, ol { + list-style: none; + margin: 0; + padding: 0; +} + +[hidden], template { + display: none; +} + +svg:not(:root) { + overflow: hidden; +} + +::selection { + background-color: var(--default-color-black); + color: var(--default-color-white); +} + diff --git a/styles/main.scss b/styles/main.scss new file mode 100644 index 0000000..a539986 --- /dev/null +++ b/styles/main.scss @@ -0,0 +1,6 @@ +@import 'variables'; +@import 'mixins/mod'; +@import 'base/reset'; +@import 'base/layout'; +@import 'atoms/mod'; +@import 'uikit/mod'; diff --git a/styles/mixins/flex.scss b/styles/mixins/flex.scss new file mode 100644 index 0000000..0b8f923 --- /dev/null +++ b/styles/mixins/flex.scss @@ -0,0 +1,165 @@ +@mixin dis-flex { + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +@mixin dis-inl-flex { + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; +} + +@mixin flex-dir-row { + @include dis-flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; +} + +@mixin flex-dir-col { + @include dis-flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; +} + +@mixin flex-dir($val) { + @if $val == row {@include flex-dir-row;} + @else if $val == col {@include flex-dir-col;} + @else {@error 'unknown flex-direction: #{$val}';} +} + +@mixin flex-wrap($val: wrap) { + -ms-flex-wrap: $val; + flex-wrap: $val; +} + +@mixin flex-nowrap { + @include flex-wrap(nowrap); +} + +@mixin flex($val) { + -webkit-box-flex: $val; + -ms-flex: $val; + flex: $val; +} + +@mixin flex-shrink($val) { + -ms-flex-negative: $val; + flex-shrink: $val; +} + +@mixin ord($val) { + -webkit-box-ordinal-group: $val + 1; + -ms-flex-order: $val; + order: $val; +} + +@mixin flex-jus-sta { + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +@mixin flex-jus-cen { + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; +} + +@mixin flex-jus-end { + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +@mixin flex-jus-aro { + -ms-flex-pack: distribute; + justify-content: space-around; +} + +@mixin flex-jus-bet { + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; +} + +@mixin flex-jus($val) { + @if $val == sta {@include flex-jus-sta;} + @else if $val == cen {@include flex-jus-cen;} + @else if $val == end {@include flex-jus-end;} + @else if $val == aro {@include flex-jus-aro;} + @else if $val == bet {@include flex-jus-bet;} + @else {@error 'unknown flex-jus (justify-content) property: #{$val}';} +} + +@mixin flex-ali-sta { + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; +} + +@mixin flex-ali-cen { + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +@mixin flex-ali-end { + -webkit-box-align: end; + -ms-flex-align: end; + align-items: flex-end; +} + +@mixin flex-ali-str { + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; +} + +@mixin flex-ali($val) { + @if $val == sta {@include flex-ali-sta;} + @else if $val == cen {@include flex-ali-cen;} + @else if $val == end {@include flex-ali-end;} + @else if $val == str {@include flex-ali-str;} + @else {@error 'unknown flex-ali (align-items) property: #{$val}';} +} + +@mixin ali-self-sta { + -ms-flex-item-align: start; + align-self: flex-start; +} + +@mixin ali-self-cen { + -ms-flex-item-align: center; + -ms-grid-row-align: center; + align-self: center; +} + +@mixin ali-self-end { + -ms-flex-item-align: end; + align-self: flex-end; +} + +@mixin ali-self-str { + -ms-flex-item-align: stretch; + -ms-grid-row-align: stretch; + align-self: stretch; +} + +@mixin ali-self($val) { + @if $val == sta {@include ali-self-sta;} + @else if $val == cen {@include ali-self-cen;} + @else if $val == end {@include ali-self-end;} + @else if $val == str {@include ali-self-str;} + @else {@error 'unknown ali-self (align-self) property: #{$val}';} +} + +@mixin flex-layout($dir: none, $ali: none, $jus: none) { + @if $dir != none {@include flex-dir($dir);} + @if $jus != none {@include flex-jus($jus);} + @if $ali != none {@include flex-ali($ali);} +} diff --git a/styles/mixins/mod.scss b/styles/mixins/mod.scss new file mode 100644 index 0000000..276fb85 --- /dev/null +++ b/styles/mixins/mod.scss @@ -0,0 +1,2 @@ +@import 'flex'; +@import 'white-spaces'; diff --git a/styles/mixins/white-spaces.scss b/styles/mixins/white-spaces.scss new file mode 100644 index 0000000..fbfd577 --- /dev/null +++ b/styles/mixins/white-spaces.scss @@ -0,0 +1,29 @@ + +@mixin mar($val) { margin: #{$val}rem } +@mixin mar-v($val) { @include mar-t($val); @include mar-b($val) } +@mixin mar-h($val) { @include mar-r($val); @include mar-l($val) } +@mixin mar-t($val) { margin-top: #{$val}rem } +@mixin mar-r($val) { margin-right: #{$val}rem } +@mixin mar-b($val) { margin-bottom: #{$val}rem } +@mixin mar-l($val) { margin-left: #{$val}rem } +@mixin mar-ha { margin-left: auto; margin-right: auto } + +@mixin pad($val) { padding: #{$val}rem } +@mixin pad-v($val) { @include pad-t($val); @include pad-b($val) } +@mixin pad-h($val) { @include pad-r($val); @include pad-l($val) } +@mixin pad-t($val) { padding-top: #{$val}rem } +@mixin pad-r($val) { padding-right: #{$val}rem } +@mixin pad-b($val) { padding-bottom: #{$val}rem } +@mixin pad-l($val) { padding-left: #{$val}rem } + +@mixin gap-v($val) { + > :not([hidden]):not(.hidden-input) + :not([hidden]):not(.hidden-input) { @include mar-t($val) } +} + +@mixin gap-h($val) { + > :not(:last-child):not(:only-child) { @include mar-r($val) } +} + +@mixin gap-h-l($val) { + > :not([hidden]):not(.hidden-input) + :not([hidden]):not(.hidden-input) { @include mar-l($val) } +} diff --git a/styles/uikit/dropdown.scss b/styles/uikit/dropdown.scss new file mode 100644 index 0000000..4ce34b3 --- /dev/null +++ b/styles/uikit/dropdown.scss @@ -0,0 +1,83 @@ + +/* dropdown + * Source: https://codepen.io/markcaron/pen/wdVmpB + * + * TODO: change styles + * */ + +.dropdown { + position: relative; + display: inline-block; + + & > input[type="checkbox"] { + position: absolute; + left: -100vw; + } + + & > label { + display: inline-block; + padding: 6px 15px; + color: #333; + line-height: 1.5em; + text-decoration: none; + border: 1px solid #8c8c8c; + cursor: pointer; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + + &:hover { + border-color: #333; + } + + &:after { + content: "▲"; + font-size: 10px; + display: inline-block; + margin-left: 6px; + vertical-align: top; + } + } + + & > ul { + position: absolute; + z-index: 999; + display: block; + left: -100vw; + bottom: calc(1.5em + 14px); + border: 1px solid #8c8c8c; + background: #fff; + padding: 6px 0; + margin: 0; + list-style: none; + width: 100%; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0 3px 8px rgba(0,0,0,.15); + -moz-box-shadow: 0 3px 8px rgba(0,0,0,.15); + box-shadow: 0 3px 8px rgba(0,0,0,.15); + + a { + display: block; + padding: 6px 15px; + text-decoration: none; + color: #333; + + &:hover, &:focus { + background: #ececec; + } + } + } + + & > input[type="checkbox"]:checked { + & ~ ul { + left: 0; + } + + & + label:after { + content: "▼"; + } + } +} + diff --git a/styles/uikit/main-menu.scss b/styles/uikit/main-menu.scss new file mode 100644 index 0000000..d26d52b --- /dev/null +++ b/styles/uikit/main-menu.scss @@ -0,0 +1,21 @@ + +.main-menu { + @extend .row-sta-sta, .gap-h-1; + + > a { + @extend .pad-0x5, .anim; + + // TODO: move to atoms + color: var(--color-brand-blue); + border-radius: 6px; + border: 1px solid var(--color-brand-blue); + text-decoration: none; + + &:hover, + &[aria-current]:not([aria-current=""]) { + color: var(--default-color-white); + background-color: var(--color-brand-blue); + } + } +} + diff --git a/styles/uikit/mod.scss b/styles/uikit/mod.scss new file mode 100644 index 0000000..4de409d --- /dev/null +++ b/styles/uikit/mod.scss @@ -0,0 +1,2 @@ +@import 'main-menu'; +@import 'dropdown'; diff --git a/styles/variables.scss b/styles/variables.scss new file mode 100644 index 0000000..cc38d60 --- /dev/null +++ b/styles/variables.scss @@ -0,0 +1,9 @@ +@use "sass:math"; + +$radius: 0.25rem; +$col: 12; +$col-gutter: 24; +$page-max-width: 1440; +$page-min-width: 320; +$content-width: $page-max-width - ($col-gutter * 2); +$col-width: math.div($content-width - $col-gutter * $col - 1, $col); diff --git a/translates/eng.ts b/translates/eng.ts new file mode 100644 index 0000000..d5bd618 --- /dev/null +++ b/translates/eng.ts @@ -0,0 +1,33 @@ +import { Translations } from "./rus.ts"; + +export default { + About: "About", + Works: "Works", + My_latest_works: "My latest works", + Source_code: "Source code", + Page_not_found: "Page not found", + Internal_server_error: "Internal server error", + about: { + Hi: "Hi!", + My_name_is_Dmitriy_Pleshevskiy: "My name is Dmitriy Pleshevskiy.", + I_am_lead_software_developer_architect_team_leader_and_mentor: + "I'm a lead software developer, architect, team leader and mentor", + Open_source_projects_are_my_passion_I_invent_experiment_implement_and_improve_projects_in_my_spare_time: + "Open-source projects are my passion! I invest, exeriment, implement and improve projects in my spare time", + Besides_programming_I_love_to_cook_and_spend_time_with_my_beloved_family: + "Besides programming, I love to cook and spend time with my beloved family!", + Programming_languages: "Programming languages", + Prefer: "Prefer", + Extensive_experience: "Extensive experience", + Limited_experience: "Limited experience", + + Databases: "Databases", + + Creating_applications: "Creating applications", + Traditional: "Traditional", + Dynamic: "Dynamic", + Hybrid: "Hybrid", + Console: "Console", + Crossplatform: "Crossplatform", + }, +} as Translations; diff --git a/translates/rus.ts b/translates/rus.ts new file mode 100644 index 0000000..881f6f2 --- /dev/null +++ b/translates/rus.ts @@ -0,0 +1,35 @@ +export const rus = { + About: "Обо мне", + Works: "Работы", + My_latest_works: "Мои последние работы", + Source_code: "Исходный код", + Page_not_found: "Страница не найдена", + Internal_server_error: "Внутренняя ошибка сервера", + about: { + Hi: "Привет!", + My_name_is_Dmitriy_Pleshevskiy: "Меня зовут Дмитрий Плешевский.", + I_am_lead_software_developer_architect_team_leader_and_mentor: + "Я ведущий разработчик програмного обеспечения, архитектор, руководитель команды, а так же ментор.", + Open_source_projects_are_my_passion_I_invent_experiment_implement_and_improve_projects_in_my_spare_time: + "Open-source проекты – моя страсть! Придумываю, экспериментирую, воплощаю, улучшаю проекты в свое свободное время", + Besides_programming_I_love_to_cook_and_spend_time_with_my_beloved_family: + "Помимо программирования я люблю готовить и проводить время со своей любимой семьей!", + Programming_languages: "Языки программирования", + Prefer: "Предпочитаю", + Extensive_experience: "Огромный опыт", + Limited_experience: "Ограниченный опыт", + + Databases: "Базы данных", + + Creating_applications: "Создание приложений", + Traditional: "Традиционные", + Dynamic: "Динамичное", + Hybrid: "Гибридное", + Console: "Консольные", + Crossplatform: "Кроссплатформенные", + }, +}; + +export default rus; + +export type Translations = typeof rus; diff --git a/views/comp/layout.ts b/views/comp/layout.ts new file mode 100644 index 0000000..573e39f --- /dev/null +++ b/views/comp/layout.ts @@ -0,0 +1,19 @@ +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]), + ]), + ]); +} diff --git a/views/comp/page_layout.ts b/views/comp/page_layout.ts new file mode 100644 index 0000000..64460ee --- /dev/null +++ b/views/comp/page_layout.ts @@ -0,0 +1,59 @@ +import { AnyNode, Attrs, E, Elem } from "ren/node.ts"; +import { classNames } from "ren/attrs.ts"; +import { Context, getLangHref, iterLangs, Lang } from "../../context.ts"; +import { Link, RepoLink } from "../uikit/link.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.About, navLink("/", ctx)), + Link(ctx.tr.Works, navLink("/works", 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"), [ + RepoLink(ctx.tr.Source_code, "/pleshevskiy/recipes"), + ChangeLang(ctx), + ]), + ]); +} + +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 Link(lang, { "href": getLangHref(lang, ctx.locPath) }); +} diff --git a/views/pages/about.ts b/views/pages/about.ts new file mode 100644 index 0000000..b621cf8 --- /dev/null +++ b/views/pages/about.ts @@ -0,0 +1,49 @@ +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"; + +const p = E.bind(null, "p", []); +const ul = E.bind(null, "ul", []); +const li = E.bind(null, "li", []); + +export function AboutPage(ctx: Context): AnyNode { + return PageLayout(ctx, [ + E("div", classNames("content-width responsive-typography"), [ + p(ctx.tr.about.Hi), + p(ctx.tr.about.My_name_is_Dmitriy_Pleshevskiy), + p( + ctx.tr.about + .I_am_lead_software_developer_architect_team_leader_and_mentor, + ), + p( + ctx.tr.about + .Open_source_projects_are_my_passion_I_invent_experiment_implement_and_improve_projects_in_my_spare_time, + ), + p( + ctx.tr.about + .Besides_programming_I_love_to_cook_and_spend_time_with_my_beloved_family, + ), + + H3(ctx.tr.about.Programming_languages), + p(`${ctx.tr.about.Prefer}: Rust, TS, Bash`), + p(`${ctx.tr.about.Extensive_experience}: Rust, TS, JS, Python, Bash`), + p(`${ctx.tr.about.Limited_experience}: Haskell, Java, C#, C++`), + + H3(ctx.tr.about.Databases), + p(`${ctx.tr.about.Prefer}: Postgres`), + p(`${ctx.tr.about.Extensive_experience}: Postgres, MySQL, Sqlite, Mongo`), + + H3(ctx.tr.about.Creating_applications), + ul([ + li(`${ctx.tr.about.Traditional} (SSR + Forms)`), + li("API (REST/GraphQL/WebSocket/EventSource)"), + li(`${ctx.tr.about.Dynamic} (SPA)`), + li(`${ctx.tr.about.Hybrid} (SSR + SPA)`), + li(ctx.tr.about.Console), + li(ctx.tr.about.Crossplatform), + ]), + ]), + ]); +} diff --git a/views/pages/e404.ts b/views/pages/e404.ts new file mode 100644 index 0000000..56cdb1b --- /dev/null +++ b/views/pages/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(ctx)]); +} + +export function E404(ctx: Context): AnyNode { + return E("div", classNames("content-width gap-v-1x5"), [ + H3(ctx.tr.Page_not_found), + ]); +} diff --git a/views/pages/e500.ts b/views/pages/e500.ts new file mode 100644 index 0000000..3a45b91 --- /dev/null +++ b/views/pages/e500.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 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), + ]); +} diff --git a/views/pages/works.ts b/views/pages/works.ts new file mode 100644 index 0000000..817ff9b --- /dev/null +++ b/views/pages/works.ts @@ -0,0 +1,32 @@ +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"; + +const ul = E.bind(null, "ul", []); +const li = E.bind(null, "li", []); + +export function WorksPage(ctx: Context): AnyNode { + return PageLayout(ctx, [ + E("div", classNames("content-width gap-v-1x5 responsive-typography"), [ + H3(ctx.tr.My_latest_works), + ul([ + li([RepoLink("ren", "/pleshevskiy/ren")]), + li([RepoLink("hwt", "github.com/pleshevskiy/hwt")]), + li([RepoLink("sonic-channel", "github.com/pleshevskiy/sonic-channel")]), + li([RepoLink("migra", "github.com/pleshevskiy/migra")]), + li([RepoLink("itconfig", "/pleshevskiy/itconfig")]), + ]), + ]), + ]); +} + +export function RepoLink(name: string, repo: string): AnyNode { + const gitBase = new URL("https://git.pleshevski.ru"); + return E("a", { + target: "_blank", + href: new URL(repo, gitBase).toString(), + rel: "external nofollow noopener noreferrer", + }, name); +} diff --git a/views/uikit/link.ts b/views/uikit/link.ts new file mode 100644 index 0000000..1c3f7d1 --- /dev/null +++ b/views/uikit/link.ts @@ -0,0 +1,15 @@ +import { AnyNode, Attrs, E } from "ren/node.ts"; + +export function RepoLink(name: string, repo: string): AnyNode { + const gitBase = new URL("https://git.pleshevski.ru"); + + return Link(name, { + target: "_blank", + href: new URL(repo, gitBase).toString(), + rel: "external nofollow noopener noreferrer", + }); +} + +export function Link(text: string, attrs: Attrs | Attrs[]): AnyNode { + return E("a", attrs, text); +} diff --git a/views/uikit/typo.ts b/views/uikit/typo.ts new file mode 100644 index 0000000..5f95691 --- /dev/null +++ b/views/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); +}