update to new arch #2
58 changed files with 1912 additions and 3451 deletions
|
@ -1,12 +1,10 @@
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
!/tsconfig.json
|
!/public
|
||||||
|
!/styles
|
||||||
|
!/translates
|
||||||
|
!/views
|
||||||
|
|
||||||
# node modules
|
!/*.ts
|
||||||
!/package.json
|
!/*.json
|
||||||
!/package-lock.json
|
|
||||||
|
|
||||||
# sources
|
|
||||||
!/src
|
|
||||||
!/static
|
|
49
.drone.yml
Normal file
49
.drone.yml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build-docker-image
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: org_registry_username
|
||||||
|
password:
|
||||||
|
from_secret: org_registry_password
|
||||||
|
registry: ${ORG_REGISTRY}
|
||||||
|
repo: ${ORG_REGISTRY}/${DRONE_REPO}
|
||||||
|
tags:
|
||||||
|
- ${DRONE_COMMIT_SHA:0:8}
|
||||||
|
- ${DRONE_TARGET_BRANCH}
|
||||||
|
|
||||||
|
- name: deploy
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
image: registry.pleshevski.ru/drone_plugins/docker_stack
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
PLESHEVSKI_IMAGE: ${ORG_REGISTRY}/${DRONE_REPO}:${DRONE_COMMIT_SHA:0:8}
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: org_registry_username
|
||||||
|
password:
|
||||||
|
from_secret: org_registry_password
|
||||||
|
name: 'pleshevski'
|
||||||
|
|
||||||
|
image_pull_secrets:
|
||||||
|
- org_docker_config
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
host:
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
parser: "@typescript-eslint/parser"
|
|
||||||
env:
|
|
||||||
es6: true
|
|
||||||
node: true
|
|
||||||
parserOptions:
|
|
||||||
ecmaVersion: 2020
|
|
||||||
sourceType: "module"
|
|
||||||
extends:
|
|
||||||
- prettier
|
|
||||||
- plugin:prettier/recommended
|
|
||||||
- "plugin:@typescript-eslint/recommended"
|
|
||||||
rules:
|
|
||||||
"@typescript-eslint/no-unused-vars":
|
|
||||||
- error
|
|
||||||
- vars: all
|
|
||||||
args: after-used
|
|
||||||
argsIgnorePattern: ^_
|
|
||||||
varsIgnorePattern: ^_
|
|
||||||
ignoreRestSiblings: true
|
|
||||||
"@typescript-eslint/no-empty-interface": off
|
|
||||||
"@typescript-eslint/no-explicit-any": off
|
|
||||||
"@typescript-eslint/explicit-function-return-type":
|
|
||||||
- warn
|
|
||||||
- allowExpressions: false
|
|
||||||
allowTypedFunctionExpressions: true
|
|
||||||
allowHigherOrderFunctions: true
|
|
||||||
"@typescript-eslint/camelcase": off
|
|
||||||
"@typescript-eslint/no-use-before-define": off
|
|
21
.gitignore
vendored
21
.gitignore
vendored
|
@ -1,15 +1,18 @@
|
||||||
/*
|
/*
|
||||||
|
|
||||||
# editors
|
|
||||||
!/.vscode
|
|
||||||
|
|
||||||
# ignores
|
|
||||||
!/*ignore
|
!/*ignore
|
||||||
# config
|
|
||||||
!/*file
|
!/*file
|
||||||
|
!/*.nix
|
||||||
!/*.yml
|
!/*.yml
|
||||||
!/*.json
|
|
||||||
|
|
||||||
# sources
|
!/*.json
|
||||||
!/src
|
!/*.ts
|
||||||
!/static
|
|
||||||
|
!/public
|
||||||
|
!/styles
|
||||||
|
!/translates
|
||||||
|
!/views
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"editor.tabSize": 2
|
|
||||||
}
|
|
27
Dockerfile
27
Dockerfile
|
@ -1,24 +1,13 @@
|
||||||
FROM node:16.14.2-alpine3.14
|
FROM denoland/deno:alpine-1.22.1
|
||||||
|
|
||||||
|
EXPOSE 33334
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk update \
|
USER deno
|
||||||
&& apk upgrade \
|
|
||||||
&& apk add --no-cache bash git openssh
|
|
||||||
|
|
||||||
COPY package*.json ./
|
ADD . .
|
||||||
|
# Compile the main app so that it doesn't need to be compiled each startup/entry.
|
||||||
|
RUN deno cache server.ts
|
||||||
|
|
||||||
RUN npm install \
|
CMD ["run", "-A", "server.ts"]
|
||||||
&& apk del bash git openssh
|
|
||||||
|
|
||||||
COPY tsconfig.json ./
|
|
||||||
COPY src ./src
|
|
||||||
|
|
||||||
RUN npm run build \
|
|
||||||
&& npm prune --production
|
|
||||||
|
|
||||||
COPY static ./static/
|
|
||||||
|
|
||||||
EXPOSE 30000
|
|
||||||
|
|
||||||
CMD npm run start
|
|
||||||
|
|
24
context.ts
Normal file
24
context.ts
Normal 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",
|
||||||
|
}
|
6
deno.json
Normal file
6
deno.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["deno.ns", "dom"]
|
||||||
|
},
|
||||||
|
"importMap": "./import_map.json"
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ networks:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
site:
|
site:
|
||||||
image: pleshevski
|
image: $PLESHEVSKI_IMAGE
|
||||||
networks:
|
networks:
|
||||||
- rp_public
|
- rp_public
|
||||||
deploy:
|
deploy:
|
||||||
|
@ -27,5 +27,4 @@ services:
|
||||||
- traefik.http.routers.pleshevski_https.entrypoints=https
|
- traefik.http.routers.pleshevski_https.entrypoints=https
|
||||||
- traefik.http.routers.pleshevski_https.tls=true
|
- traefik.http.routers.pleshevski_https.tls=true
|
||||||
- traefik.http.routers.pleshevski_https.tls.certresolver=le
|
- traefik.http.routers.pleshevski_https.tls.certresolver=le
|
||||||
- traefik.http.services.pleshevski.loadbalancer.server.port=30000
|
- traefik.http.services.pleshevski.loadbalancer.server.port=33334
|
||||||
|
|
||||||
|
|
43
flake.lock
Normal file
43
flake.lock
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
22
flake.nix
Normal file
22
flake.nix
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
description = "Pleshevski personal site";
|
||||||
|
|
||||||
|
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; [
|
||||||
|
gnumake
|
||||||
|
nodePackages.sass
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in with utils.lib; eachSystem defaultSystems out;
|
||||||
|
|
||||||
|
}
|
5
import_map.json
Normal file
5
import_map.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"ren/": "https://git.pleshevski.ru/pleshevskiy/ren/raw/branch/main/ren/"
|
||||||
|
}
|
||||||
|
}
|
30
makefile
30
makefile
|
@ -1,10 +1,15 @@
|
||||||
PAR := $(MAKE) -j 128
|
PAR := $(MAKE) -j 128
|
||||||
DOCKER_NAME := pleshevski
|
DOCKER_NAME := recipes
|
||||||
DOCKER_TAG := pleshevski
|
DOCKER_TAG := recipes
|
||||||
|
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
$(PAR) hr ts-w
|
${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-restart: docker-stop docker-run
|
||||||
|
|
||||||
|
@ -16,20 +21,3 @@ docker-run:
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
docker build -t ${DOCKER_TAG} .
|
docker build -t ${DOCKER_TAG} .
|
||||||
|
|
||||||
build: ts
|
|
||||||
|
|
||||||
start:
|
|
||||||
npm run start
|
|
||||||
|
|
||||||
hr:
|
|
||||||
deno run -A ~/sandbox/hr/server.ts target static
|
|
||||||
|
|
||||||
ts:
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
ts-w:
|
|
||||||
NODE_ENV=develop npx tsc-watch --onSuccess "make start"
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf target
|
|
||||||
|
|
2849
package-lock.json
generated
2849
package-lock.json
generated
File diff suppressed because it is too large
Load diff
20
package.json
20
package.json
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"start": "node target/scripts/main.mjs"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^17.0.21",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
|
||||||
"@typescript-eslint/parser": "^5.14.0",
|
|
||||||
"eslint": "^8.10.0",
|
|
||||||
"eslint-config-prettier": "^8.5.0",
|
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
|
||||||
"prettier": "^2.5.1",
|
|
||||||
"tsc-watch": "^4.6.0",
|
|
||||||
"typescript": "^4.6.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ren": "github:pleshevskiy/ren"
|
|
||||||
}
|
|
||||||
}
|
|
546
public/styles/main.css
Normal file
546
public/styles/main.css
Normal 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 */
|
1
public/styles/main.css.map
Normal file
1
public/styles/main.css.map
Normal 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
177
server.ts
Normal 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);
|
||||||
|
}
|
|
@ -1,31 +0,0 @@
|
||||||
import { AnyNode, E, Ea, Elem } from "ren";
|
|
||||||
import { config } from "../config.mjs";
|
|
||||||
import { div } from "../utils.mjs";
|
|
||||||
|
|
||||||
export function Layout(page: AnyNode): Elem {
|
|
||||||
return Ea("html", { lang: "ru" }, [
|
|
||||||
E("head", [
|
|
||||||
Ea("meta", { charset: "utf-8" }),
|
|
||||||
Ea("meta", {
|
|
||||||
name: "viewport",
|
|
||||||
content: "width=device-width, initial-scale=1",
|
|
||||||
}),
|
|
||||||
Ea("link", {
|
|
||||||
rel: "stylesheet",
|
|
||||||
href: "/static/styles.css",
|
|
||||||
}),
|
|
||||||
E("title", "pleshevski"),
|
|
||||||
]),
|
|
||||||
E("body", [div({ id: "root" }, page), config.isDev && HotReloadScript()]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HotReloadScript(): Elem {
|
|
||||||
return E(
|
|
||||||
"script",
|
|
||||||
`const ws = new WebSocket("ws://localhost:30001");
|
|
||||||
ws.addEventListener("message", (m) => {
|
|
||||||
if (m.data === "RELOAD") location.reload();
|
|
||||||
});`
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { AnyNode, Ea, Elem } from "ren";
|
|
||||||
import { Context } from "../context.mjs";
|
|
||||||
import { div } from "../utils.mjs";
|
|
||||||
|
|
||||||
export function PageLayout(ctx: Context, ...children: AnyNode[]): Elem {
|
|
||||||
return Ea("div", { id: "main" }, [
|
|
||||||
Header(ctx.locPath),
|
|
||||||
Ea("div", { class: "content" }, children),
|
|
||||||
// Footer(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Header(locPath: string): Elem {
|
|
||||||
return Ea("header", { class: "header" }, [
|
|
||||||
div({ class: "content-width" }, HeaderNav(locPath)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HeaderNav(locPath: string): Elem {
|
|
||||||
return Ea("nav", { class: "main-menu" }, [
|
|
||||||
NavLink(locPath, "/", "Обо мне"),
|
|
||||||
NavLink(locPath, "/works", "Работы"),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NavLink(locPath: string, href: string, text: string): Elem {
|
|
||||||
const attrs = { href };
|
|
||||||
if (locPath === href) attrs["aria-current"] = "true";
|
|
||||||
return Ea("a", attrs, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Footer(): Elem {
|
|
||||||
return Ea("footer", { class: "footer" }, "footer");
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
export const config = createConfig();
|
|
||||||
|
|
||||||
export function createConfig(): Config {
|
|
||||||
return {
|
|
||||||
isDev: process.env.NODE_ENV === "develop",
|
|
||||||
server: { port: 30000 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
isDev: boolean;
|
|
||||||
server: ServerConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerConfig {
|
|
||||||
port: number;
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export interface Context {
|
|
||||||
locPath: string;
|
|
||||||
}
|
|
13
src/lang.mts
13
src/lang.mts
|
@ -1,13 +0,0 @@
|
||||||
export function isNil<T>(v: Nilable<T>): v is Nil {
|
|
||||||
return v == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Nullable<T> = T | null;
|
|
||||||
|
|
||||||
export type Nilable<T> = T | Nil;
|
|
||||||
|
|
||||||
export type Nil = null | undefined;
|
|
||||||
|
|
||||||
export function isBool(v: unknown): v is boolean {
|
|
||||||
return typeof v === "boolean";
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { createServer } from "./server.mjs";
|
|
||||||
|
|
||||||
main();
|
|
||||||
|
|
||||||
function main(): void {
|
|
||||||
createServer();
|
|
||||||
}
|
|
110
src/server.mts
110
src/server.mts
|
@ -1,110 +0,0 @@
|
||||||
import * as http from "http";
|
|
||||||
import * as fs from "fs/promises";
|
|
||||||
import * as path from "path";
|
|
||||||
import { Layout } from "./components/layout.mjs";
|
|
||||||
import { config } from "./config.mjs";
|
|
||||||
import { debug, info } from "./log.mjs";
|
|
||||||
import { StrRenderer } from "ren";
|
|
||||||
import { AboutPage } from "./views/about.mjs";
|
|
||||||
import { E404 } from "./views/e404.mjs";
|
|
||||||
import { WorksPage } from "./views/works.mjs";
|
|
||||||
import { Context } from "./context.mjs";
|
|
||||||
|
|
||||||
export function createServer(): void {
|
|
||||||
const server = http.createServer(handleHttpReq);
|
|
||||||
server.listen(config.server.port, () => {
|
|
||||||
info(
|
|
||||||
"[server]",
|
|
||||||
`Server listening at http://localhost:${config.server.port}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleHttpReq(
|
|
||||||
httpReq: http.IncomingMessage,
|
|
||||||
httpRes: http.ServerResponse
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const req = tryIntoAppServerRequest(httpReq);
|
|
||||||
|
|
||||||
debug("[server]", { req });
|
|
||||||
|
|
||||||
if (req.url.startsWith("/static")) {
|
|
||||||
const relFilePath = path.join(process.cwd(), req.url.slice(1));
|
|
||||||
const mimeType = mimeTypeByExt.get(path.extname(relFilePath));
|
|
||||||
|
|
||||||
const fileContent = await fs
|
|
||||||
.readFile(relFilePath, { encoding: "utf-8" })
|
|
||||||
.catch((_e) => null);
|
|
||||||
|
|
||||||
if (fileContent && mimeType) {
|
|
||||||
httpRes.writeHead(200, { "content-type": mimeType }).end(fileContent);
|
|
||||||
} else {
|
|
||||||
httpRes.writeHead(404).end("Not found");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const ctx: Context = { locPath: req.url };
|
|
||||||
const ren = new StrRenderer();
|
|
||||||
if (/^[/](?:about[/]?)?$/.test(req.url)) {
|
|
||||||
httpRes
|
|
||||||
.writeHead(200, { "content-type": "text/html" })
|
|
||||||
.end(ren.render(Layout(AboutPage(ctx))));
|
|
||||||
} else if (/^[/]works[/]?$/.test(req.url)) {
|
|
||||||
httpRes
|
|
||||||
.writeHead(200, { "content-type": "text/html" })
|
|
||||||
.end(ren.render(Layout(WorksPage(ctx))));
|
|
||||||
} else {
|
|
||||||
httpRes
|
|
||||||
.writeHead(404, { "content-type": "text/html" })
|
|
||||||
.end(ren.render(Layout(E404(ctx))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof InvalidServerRequest) {
|
|
||||||
httpRes.writeHead(400).end("Bad request");
|
|
||||||
} else if (err instanceof UnsupportedRestMethod) {
|
|
||||||
httpRes.writeHead(405).end("Method not allowed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mimeTypeByExt = new Map([[".css", "text/css"]]);
|
|
||||||
|
|
||||||
export function tryIntoAppServerRequest(
|
|
||||||
req: http.IncomingMessage
|
|
||||||
): ServerRequest {
|
|
||||||
if (!req.method || !req.url) throw new InvalidServerRequest();
|
|
||||||
|
|
||||||
return {
|
|
||||||
method: tryIntoAppRestMethod(req.method),
|
|
||||||
url: req.url,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerRequest {
|
|
||||||
method: RestMethod;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class InvalidServerRequest extends Error {}
|
|
||||||
|
|
||||||
export function tryIntoAppRestMethod(rest: string): RestMethod {
|
|
||||||
switch (rest.toUpperCase()) {
|
|
||||||
case "GET":
|
|
||||||
return RestMethod.Get;
|
|
||||||
default:
|
|
||||||
throw new UnsupportedRestMethod();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum RestMethod {
|
|
||||||
Get = "GET",
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UnsupportedRestMethod extends Error {}
|
|
||||||
|
|
||||||
export interface ServerResponse {
|
|
||||||
statusCode?: number;
|
|
||||||
headers?: Headers;
|
|
||||||
body: string;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { E, Ea, Et } from "ren";
|
|
||||||
|
|
||||||
export const p = Et.bind(null, "p");
|
|
||||||
export const h2 = Et.bind(null, "h2");
|
|
||||||
export const h3 = Et.bind(null, "h3");
|
|
||||||
|
|
||||||
export const div = Ea.bind(null, "div");
|
|
||||||
export const ul = E.bind(null, "ul");
|
|
||||||
export const li = E.bind(null, "li");
|
|
|
@ -1,50 +0,0 @@
|
||||||
import { PageLayout } from "../components/page_layout.mjs";
|
|
||||||
import { AnyNode } from "ren";
|
|
||||||
import { div, h3, li, p, ul } from "../utils.mjs";
|
|
||||||
import { Context } from "../context.mjs";
|
|
||||||
|
|
||||||
export function AboutPage(ctx: Context): AnyNode {
|
|
||||||
return PageLayout(
|
|
||||||
ctx,
|
|
||||||
div({ class: "content-width responsive-typography" }, [
|
|
||||||
div({}, [
|
|
||||||
p("Привет!"),
|
|
||||||
p("Меня зовут Дмитрий Плешевский."),
|
|
||||||
p(
|
|
||||||
"Я ведущий разработчик программного обеспечения, архитектор,",
|
|
||||||
"руководитель команды, а так же ментор."
|
|
||||||
),
|
|
||||||
p(
|
|
||||||
"Open-source проекты – моя страсть! Придумываю, экспериментирую,",
|
|
||||||
"воплощаю, улучшаю проекты в свое свободное время"
|
|
||||||
),
|
|
||||||
p(
|
|
||||||
"Помимо программирования я люблю готовить и проводить время со своей",
|
|
||||||
"любимой семьей!"
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
div({}, [
|
|
||||||
h3("Языки программирования"),
|
|
||||||
p("Предпочитаю: Rust, TS"),
|
|
||||||
p("Огромный опыт: Rust, TS, JS, Python"),
|
|
||||||
p("Ограниченный опыт: Haskell, Java, C#"),
|
|
||||||
]),
|
|
||||||
div({}, [
|
|
||||||
h3("Базы данных"),
|
|
||||||
p("Предпочитаю: Postgres"),
|
|
||||||
p("Огромный опыт: Postgres, MySQL, Sqlite, mongo"),
|
|
||||||
]),
|
|
||||||
div({}, [
|
|
||||||
h3("Создание приложений"),
|
|
||||||
ul([
|
|
||||||
li("Традиционное (SSR + Forms)"),
|
|
||||||
li("API (REST/GraphQL/WebSocket/EventSource)"),
|
|
||||||
li("Динамическое (SPA)"),
|
|
||||||
li("Гибридное (SSR + SPA)"),
|
|
||||||
li("Консольные"),
|
|
||||||
li("Кроссплатформенные"),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { PageLayout } from "../components/page_layout.mjs";
|
|
||||||
import { AnyNode, Ea } from "ren";
|
|
||||||
import { div } from "../utils.mjs";
|
|
||||||
import { Context } from "../context.mjs";
|
|
||||||
|
|
||||||
export function E404(ctx: Context): AnyNode {
|
|
||||||
return PageLayout(
|
|
||||||
ctx,
|
|
||||||
div(
|
|
||||||
{ class: "content-width" },
|
|
||||||
Ea("h3", { class: "font-h3" }, "Страница не найдена")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { PageLayout } from "../components/page_layout.mjs";
|
|
||||||
import { AnyNode, Ea } from "ren";
|
|
||||||
import { div, h3, li, ul } from "../utils.mjs";
|
|
||||||
import { Context } from "../context.mjs";
|
|
||||||
|
|
||||||
export function WorksPage(ctx: Context): AnyNode {
|
|
||||||
return PageLayout(
|
|
||||||
ctx,
|
|
||||||
div({ class: "content-width responsive-typography" }, [
|
|
||||||
h3("Одни из моих последних работ"),
|
|
||||||
ul([
|
|
||||||
li(Ea("a", { href: "https://github.com/pleshevskiy/ren" }, "ren")),
|
|
||||||
li(Ea("a", { href: "https://github.com/pleshevskiy/hwt" }, "hwt")),
|
|
||||||
li(
|
|
||||||
Ea(
|
|
||||||
"a",
|
|
||||||
{ href: "https://github.com/pleshevskiy/sonic-channel" },
|
|
||||||
"sonic-channel"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
li(Ea("a", { href: "https://github.com/pleshevskiy/migra" }, "migra")),
|
|
||||||
li(
|
|
||||||
Ea(
|
|
||||||
"a",
|
|
||||||
{ href: "https://github.com/pleshevskiy/itconfig-rs" },
|
|
||||||
"itconfig-rs"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
:root {
|
|
||||||
--min-width: 320px;
|
|
||||||
--max-width: 1024px;
|
|
||||||
|
|
||||||
--default-color-white: #ffffff;
|
|
||||||
--color-blue: #1966df;
|
|
||||||
|
|
||||||
--default-font-size: 16px;
|
|
||||||
--font-family: system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Roboto,Oxygen-Sans, Ubuntu, Cantarell, "Segoe UI", Verdana, sans-serif;
|
|
||||||
--font-weight-regular: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
*, *::before, *::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
background-color: var(--default-color-white);
|
|
||||||
font-size: var(--default-font-size);
|
|
||||||
line-height: 1;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
-moz-text-size-adjust: 100%;
|
|
||||||
-ms-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-weight: var(--font-weight-regular);
|
|
||||||
font-family: var(--font-family);
|
|
||||||
min-width: var(--min-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, ol {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
min-height: 100vh;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root,
|
|
||||||
#main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main,
|
|
||||||
.content {
|
|
||||||
flex: 1 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
padding-top: 1.5rem;
|
|
||||||
padding-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
// anim
|
|
||||||
.main-menu > a {
|
|
||||||
transition: all .2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-menu > a {
|
|
||||||
color: var(--color-blue);
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid var(--color-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-blue);
|
|
||||||
}
|
|
||||||
.main-menu > a:not(:last-child) {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-width {
|
|
||||||
width: 100%;
|
|
||||||
max-width: var(--max-width);
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
padding-left: 1.25rem;
|
|
||||||
padding-right: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive-typography h3 {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive-typography > div,
|
|
||||||
.responsive-typography p,
|
|
||||||
.responsive-typography li {
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive-typography ul,
|
|
||||||
.responsive-typography ol {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive-typography ul li,
|
|
||||||
.responsive-typography ol li {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
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 {
|
|
||||||
background-color: var(--color-blue);
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 0.5rem;
|
|
||||||
height: 0.5rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive-typography > * + * {
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive-typography > div + div,
|
|
||||||
.responsive-typography p + p {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
.responsive-typography li + li {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
6
styles/atoms/flex.scss
Normal file
6
styles/atoms/flex.scss
Normal 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
3
styles/atoms/misc.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.anim, .anim::before, .anim::after {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
5
styles/atoms/mod.scss
Normal file
5
styles/atoms/mod.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@import 'sizes';
|
||||||
|
@import 'white-spaces';
|
||||||
|
@import 'flex';
|
||||||
|
@import 'typography';
|
||||||
|
@import 'misc';
|
9
styles/atoms/sizes.scss
Normal file
9
styles/atoms/sizes.scss
Normal 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 }
|
||||||
|
|
57
styles/atoms/typography.scss
Normal file
57
styles/atoms/typography.scss
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
11
styles/atoms/white-spaces.scss
Normal file
11
styles/atoms/white-spaces.scss
Normal 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
11
styles/base/layout.scss
Normal 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
305
styles/base/reset.scss
Normal 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
6
styles/main.scss
Normal 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
165
styles/mixins/flex.scss
Normal 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
2
styles/mixins/mod.scss
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
@import 'flex';
|
||||||
|
@import 'white-spaces';
|
29
styles/mixins/white-spaces.scss
Normal file
29
styles/mixins/white-spaces.scss
Normal 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) }
|
||||||
|
}
|
83
styles/uikit/dropdown.scss
Normal file
83
styles/uikit/dropdown.scss
Normal 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: "▼";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
21
styles/uikit/main-menu.scss
Normal file
21
styles/uikit/main-menu.scss
Normal 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
2
styles/uikit/mod.scss
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
@import 'main-menu';
|
||||||
|
@import 'dropdown';
|
9
styles/variables.scss
Normal file
9
styles/variables.scss
Normal 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
33
translates/eng.ts
Normal 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
35
translates/rus.ts
Normal 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;
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "target/scripts",
|
|
||||||
"target": "esnext",
|
|
||||||
"module": "esnext",
|
|
||||||
"lib": ["dom", "esnext"],
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"rootDir": "src",
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"suppressImplicitAnyIndexErrors": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
"noImplicitAny": true
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.mts"
|
|
||||||
],
|
|
||||||
}
|
|
19
views/comp/layout.ts
Normal file
19
views/comp/layout.ts
Normal 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
59
views/comp/page_layout.ts
Normal 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
49
views/pages/about.ts
Normal 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
15
views/pages/e404.ts
Normal 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
15
views/pages/e500.ts
Normal 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
32
views/pages/works.ts
Normal 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
15
views/uikit/link.ts
Normal 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
6
views/uikit/typo.ts
Normal 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);
|
||||||
|
}
|
Loading…
Reference in a new issue