update to new arch #2

Merged
pleshevskiy merged 3 commits from v2 into main 2022-06-08 23:24:20 +03:00
33 changed files with 1779 additions and 10 deletions
Showing only changes of commit d501fb900c - Show all commits

10
.dockerignore Normal file
View file

@ -0,0 +1,10 @@
/*
!/public
!/styles
!/translates
!/views
!/*.ts
!/*.json

22
.gitignore vendored
View file

@ -1,16 +1,18 @@
/* /*
# editors
!/.vscode
# ignores
!/*ignore !/*ignore
# config
!/*file !/*file
!/*.yml
!/*.json
!/*.nix !/*.nix
!/*.yml
!/*.json
!/*.ts
!/public
!/styles
!/translates
!/views
# misc
.DS_Store
# sources
!/src
!/static

24
context.ts Normal file
View file

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

7
log.ts Normal file
View file

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

546
public/styles/main.css Normal file
View file

@ -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 */

View file

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

177
server.ts Normal file
View file

@ -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<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 === "/" || 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<Translations>)
.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<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);
}

6
styles/atoms/flex.scss Normal file
View file

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

3
styles/atoms/misc.scss Normal file
View file

@ -0,0 +1,3 @@
.anim, .anim::before, .anim::after {
transition: all 0.2s ease-in-out;
}

5
styles/atoms/mod.scss Normal file
View file

@ -0,0 +1,5 @@
@import 'sizes';
@import 'white-spaces';
@import 'flex';
@import 'typography';
@import 'misc';

9
styles/atoms/sizes.scss Normal file
View file

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

View file

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

View file

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

11
styles/base/layout.scss Normal file
View file

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

305
styles/base/reset.scss Normal file
View file

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

6
styles/main.scss Normal file
View file

@ -0,0 +1,6 @@
@import 'variables';
@import 'mixins/mod';
@import 'base/reset';
@import 'base/layout';
@import 'atoms/mod';
@import 'uikit/mod';

165
styles/mixins/flex.scss Normal file
View file

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

2
styles/mixins/mod.scss Normal file
View file

@ -0,0 +1,2 @@
@import 'flex';
@import 'white-spaces';

View file

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

View file

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

View file

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

2
styles/uikit/mod.scss Normal file
View file

@ -0,0 +1,2 @@
@import 'main-menu';
@import 'dropdown';

9
styles/variables.scss Normal file
View file

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

33
translates/eng.ts Normal file
View file

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

35
translates/rus.ts Normal file
View file

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

19
views/comp/layout.ts Normal file
View file

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

59
views/comp/page_layout.ts Normal file
View file

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

49
views/pages/about.ts Normal file
View file

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

15
views/pages/e404.ts Normal file
View file

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

15
views/pages/e500.ts Normal file
View file

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

32
views/pages/works.ts Normal file
View file

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

15
views/uikit/link.ts Normal file
View file

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

6
views/uikit/typo.ts Normal file
View file

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