Compare commits

...

2 commits

Author SHA1 Message Date
68ece9f36c
remove old site 2024-07-24 20:40:05 +03:00
753116020a
refac: use vuepress 2024-07-24 20:39:34 +03:00
87 changed files with 5473 additions and 2100 deletions

View file

@ -1,13 +0,0 @@
/*
!/data
!/public
!/styles
!/translates
!/views
!/uikit
!/modules
!/*.ts
!/*.json

21
.gitignore vendored
View file

@ -1,6 +1,17 @@
# misc
.DS_Store
# editors
.idea/
.vscode/
*.swp
# direnv
/.envrc
/.direnv/
.direnv
.envrc
# nodejs
node_modules/
# nix
/result
# custom
.DS_Store
# vuepress
/docs/.vuepress/.cache
/docs/.vuepress/.temp
/docs/.vuepress/dist

View file

@ -1,19 +0,0 @@
when:
branch: main
steps:
- name: build-docker-image
image: plugins/docker
settings:
repo: ${ORG_REGISTRY}/${CI_REPO}
tags:
- "${CI_COMMIT_BRANCH}"
- "${CI_COMMIT_SHA:0:8}"
- name: deploy
image: ${ORG_REGISTRY}/drone_plugins/docker_stack
pull: true
environment:
PLESHEVSKI_IMAGE: ${ORG_REGISTRY}/${CI_REPO}:${CI_COMMIT_SHA:0:8}
settings:
name: pleshevski

View file

@ -1,13 +0,0 @@
FROM denoland/deno:alpine-1.22.1
EXPOSE 33334
WORKDIR /app
USER deno
ADD . .
# Compile the main app so that it doesn't need to be compiled each startup/entry.
RUN deno cache server.ts
CMD ["run", "-A", "server.ts"]

View file

@ -1,13 +1,16 @@
PAR := $(MAKE) -j 128
watch:
${PAR} deno-w sass-w
deno-w:
deno run -A --watch server.ts
sass-w:
sass -w styles/main.scss public/styles/main.css
PREFIX ?= dist
help:
cat makefile
cat Makefile
build:
pnpm run docs:build --dest $(PREFIX)
install: build
dev:
pnpm run docs:dev
deps:
pnpm install

View file

@ -1,25 +0,0 @@
import { Translations } from "./translates/rus.ts";
export interface Context {
title?: string;
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}`;
}
export function iterLangs(): Lang[] {
return [Lang.Eng, Lang.Rus];
}
export enum Lang {
Rus = "rus",
Eng = "eng",
}

View file

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

View file

@ -1,13 +0,0 @@
{
"version": "2",
"remote": {
"https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/257079305813c4c0a71c16a89164c20bbce1a5e2/core/node.ts": "0857215f4ddbc5ef661af1eef8010376914373b9040d11ac777923651f59db08",
"https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/257079305813c4c0a71c16a89164c20bbce1a5e2/core/utils.ts": "136b4b594befe6f1157741932aaea86f00f5ca6b1f7ee3e84059e9140a87e82d",
"https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/257079305813c4c0a71c16a89164c20bbce1a5e2/par/md.ts": "0d4264ee133a883372664ac4b5e2dcb66a41a10a914de089e96b4ebb6757c641",
"https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/257079305813c4c0a71c16a89164c20bbce1a5e2/par/types.ts": "f114cafde896121b9db754ffb5f2778edb3d414c78e3782ce1fb50a2ec8e2708",
"https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/257079305813c4c0a71c16a89164c20bbce1a5e2/ren/attrs.ts": "a8f118423567bc64fd53ffd472bba4417df7de4df726c122c2ae8f37e921329a",
"https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/257079305813c4c0a71c16a89164c20bbce1a5e2/ren/html_str.ts": "6052fa5b65ae0d8b2d5f6cb82d365e8b9a01f6070d55015db8386175386b8cca",
"https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/257079305813c4c0a71c16a89164c20bbce1a5e2/ren/node.ts": "85d81ee3adc506f7ae90db31ed52336034766430ec8a8d6ae5d9951206f989fc",
"https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/257079305813c4c0a71c16a89164c20bbce1a5e2/ren/types.ts": "f1f561397a8326ddfcfefb00c3c9ea7e4e5467021fa89795628daa656d90727b"
}
}

43
dist/404.html vendored Normal file
View file

@ -0,0 +1,43 @@
<!doctype html>
<html lang="ru-RU">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="generator" content="VuePress 2.0.0-rc.14" />
<style>
:root {
--c-bg: #fff;
}
html.dark {
--c-bg: #22272e;
}
html,
body {
background-color: var(--c-bg);
}
</style>
<script>
const userMode = localStorage.getItem('vuepress-color-scheme')
const systemDarkMode =
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
if (userMode === 'light') {
document.documentElement.dataset.theme = 'light'
} else if (userMode === 'dark' || systemDarkMode) {
document.documentElement.classList.toggle('dark', true)
document.documentElement.dataset.theme = 'dark'
}
</script>
<meta property="og:url" content="https://pleshevski.ru/404.html"><meta property="og:site_name" content="Дмитрий Плешевский"><meta property="og:description" content="404 Not Found"><meta property="og:type" content="website"><meta property="og:locale" content="ru-RU"><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","name":"","description":"404 Not Found"}</script><title>Дмитрий Плешевский</title><meta name="description" content="404 Not Found">
<link rel="preload" href="/assets/style-BnNdFOI8.css" as="style"><link rel="stylesheet" href="/assets/style-BnNdFOI8.css">
<link rel="modulepreload" href="/assets/app-Dw1tezwH.js"><link rel="modulepreload" href="/assets/404.html-C-XD0uTd.js">
<link rel="prefetch" href="/assets/index.html-Bgp5oohT.js" as="script"><link rel="prefetch" href="/assets/works.html-sG2zmfnZ.js" as="script"><link rel="prefetch" href="/assets/index.html-Bmcvetlf.js" as="script"><link rel="prefetch" href="/assets/works.html-DIDCwbnz.js" as="script">
</head>
<body>
<div id="app"><!--[--><div class="vp-theme-container" data-v-99131a8e><main class="page" data-v-99131a8e><div class="theme-default-content" data-v-99131a8e><h1 data-v-99131a8e>404</h1><blockquote data-v-99131a8e>Мы потеряли страницу...</blockquote><a class="route-link" href="/" data-v-99131a8e>Вернуться на главную</a></div></main></div><!--[--><!----><!--]--><!--]--></div>
<script type="module" src="/assets/app-Dw1tezwH.js" defer></script>
</body>
</html>

1
dist/assets/404.html-C-XD0uTd.js vendored Normal file
View file

@ -0,0 +1 @@
import{_ as t,c as e,o,a as n}from"./app-Dw1tezwH.js";const r={},a=n("p",null,"404 Not Found",-1),c=[a];function s(p,l){return o(),e("div",null,c)}const d=t(r,[["render",s],["__file","404.html.vue"]]),m=JSON.parse('{"path":"/404.html","title":"","lang":"ru-RU","frontmatter":{"layout":"NotFound","description":"404 Not Found","head":[["meta",{"property":"og:url","content":"https://pleshevski.ru/404.html"}],["meta",{"property":"og:site_name","content":"Дмитрий Плешевский"}],["meta",{"property":"og:description","content":"404 Not Found"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"ru-RU"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"\\",\\"description\\":\\"404 Not Found\\"}"]]},"headers":[],"git":{},"autoDesc":true,"filePathRelative":null}');export{d as comp,m as data};

32
dist/assets/app-Dw1tezwH.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/index.html-Bgp5oohT.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/index.html-Bmcvetlf.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/style-BnNdFOI8.css vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/works.html-DIDCwbnz.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/works.html-sG2zmfnZ.js vendored Normal file

File diff suppressed because one or more lines are too long

43
dist/eng/index.html vendored Normal file

File diff suppressed because one or more lines are too long

43
dist/eng/works.html vendored Normal file

File diff suppressed because one or more lines are too long

43
dist/index.html vendored Normal file

File diff suppressed because one or more lines are too long

5
dist/robots.txt vendored Normal file
View file

@ -0,0 +1,5 @@
User-agent:*
Disallow:
Sitemap: https://pleshevski.ru/sitemap.xml

17
dist/rus/index.html vendored Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<meta http-equiv="refresh" content="0; url=/index.html">
<link rel="canonical" href="/index.html">
<title>Redirecting...</title>
<script>
const anchor = window.location.hash.substring(1);
location.href = `/index.html${anchor? `#${anchor}`: ""}`;
</script>
</head>
<body>
<p>Redirecting...</p>
</body>
</html>

17
dist/rus/works.html vendored Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<meta http-equiv="refresh" content="0; url=/works.html">
<link rel="canonical" href="/works.html">
<title>Redirecting...</title>
<script>
const anchor = window.location.hash.substring(1);
location.href = `/works.html${anchor? `#${anchor}`: ""}`;
</script>
</head>
<body>
<p>Redirecting...</p>
</body>
</html>

3
dist/sitemap.xml vendored Normal file
View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/sitemap.xsl"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>https://pleshevski.ru/</loc><lastmod>2024-07-24T14:15:05.000Z</lastmod><changefreq>daily</changefreq><xhtml:link rel="alternate" hreflang="ru-RU" href="https://pleshevski.ru/"/><xhtml:link rel="alternate" hreflang="en-US" href="https://pleshevski.ru/eng/"/></url><url><loc>https://pleshevski.ru/works.html</loc><lastmod>2024-07-24T14:15:05.000Z</lastmod><changefreq>daily</changefreq><xhtml:link rel="alternate" hreflang="ru-RU" href="https://pleshevski.ru/works.html"/><xhtml:link rel="alternate" hreflang="en-US" href="https://pleshevski.ru/eng/works.html"/></url><url><loc>https://pleshevski.ru/eng/</loc><lastmod>2024-07-24T14:15:05.000Z</lastmod><changefreq>daily</changefreq><xhtml:link rel="alternate" hreflang="ru-RU" href="https://pleshevski.ru/"/><xhtml:link rel="alternate" hreflang="en-US" href="https://pleshevski.ru/eng/"/></url><url><loc>https://pleshevski.ru/eng/works.html</loc><lastmod>2024-07-24T14:15:05.000Z</lastmod><changefreq>daily</changefreq><xhtml:link rel="alternate" hreflang="ru-RU" href="https://pleshevski.ru/works.html"/><xhtml:link rel="alternate" hreflang="en-US" href="https://pleshevski.ru/eng/works.html"/></url></urlset>

207
dist/sitemap.xsl vendored Normal file
View file

@ -0,0 +1,207 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:html="http://www.w3.org/TR/REC-html40"
xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>XML Sitemap</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0;" />
<style>
:root {
--bg-color: #f8f8f8;
--bg-color-secondary: #fff;
--text-color: #2c3e50;
--border-color: #eaecef;
--brand-color: #3eaf7c;
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #0d1117;
--bg-color-secondary: #161b22;
--text-color: #ccc;
--border-color: #30363d;
}
}
html,
body {
margin: 0;
padding: 0;
background: var(--bg-color);
}
html {
font-size: 14px;
}
body {
min-height: 100vh;
color: var(--text-color);
text-align: center;
}
#content {
max-width: 960px;
margin: 0 auto;
}
h1 {
margin-top: 1rem;
font-size: 2rem;
}
@media (max-width: 419px) {
h1 {
font-size: 1.5rem;
}
}
a {
color: var(--text-color);
font-weight: 500;
overflow-wrap: break-word;
}
table {
width: 100%;
border-radius: 8px;
border-collapse: collapse;
text-align: center;
overflow: hidden;
}
@media (max-width: 419px) {
table {
border-radius: 0;
}
}
th {
min-width: 56px;
padding: 0.6em 1em;
background-color: var(--brand-color);
color: var(--bg-color);
font-weight: bold;
font-size: 16px;
}
@media (max-width: 719px) {
th {
font-size: 14px;
}
}
th:first-child {
text-align: start;
}
tr:nth-child(odd) {
background: var(--bg-color-secondary);
}
tr:hover {
background-color: #e8e8e8;
}
@media (prefers-color-scheme: dark) {
tr:hover {
background-color: #333;
}
}
td {
padding: 0.6em 1em;
}
@media (max-width: 719px) {
td {
font-size: 12px;
}
}
td:first-child {
text-align: start;
}
footer {
margin-top: 10px;
padding: 4px;
color: #888;
font-size: 12px;
text-align: center;
}
</style>
</head>
<body>
<div id="content">
<h1>Sitemap</h1>
<table>
<thead>
<tr>
<th>
<xsl:value-of select="concat('URL (', count(sitemap:urlset/sitemap:url), ')')" />
</th>
<th>Priority</th>
<th>Change Frequency</th>
<th>Last Updated Time</th>
</tr>
</thead>
<tbody>
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'" />
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'" />
<xsl:for-each select="sitemap:urlset/sitemap:url">
<tr>
<td>
<xsl:variable name="itemURL">
<xsl:value-of select="sitemap:loc" />
</xsl:variable>
<a href="{$itemURL}" target="_blank">
<xsl:value-of select="sitemap:loc" />
</a>
</td>
<td>
<xsl:choose>
<xsl:when test="sitemap:priority">
<xsl:value-of select="concat(sitemap:priority*100,'%a')" />
</xsl:when>
<xsl:otherwise>
<xsl:text>0.5</xsl:text>
</xsl:otherwise>
</xsl:choose>
</td>
<td>
<xsl:choose>
<xsl:when test="sitemap:changefreq">
<xsl:value-of select="concat(translate(substring(sitemap:changefreq, 1, 1),concat($lower, $upper),concat($upper, $lower)),substring(sitemap:changefreq, 2))" />
</xsl:when>
<xsl:otherwise>
<xsl:text>-</xsl:text>
</xsl:otherwise>
</xsl:choose>
</td>
<td>
<xsl:value-of select="concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)))" />
</td>
</tr>
</xsl:for-each>
</tbody>
</table>
</div>
<footer>
Generatd by <a href="https://ecosystem.vuejs.press/plugins/sitemap/">@vuepress/plugin-sitemap</a>
</footer>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

43
dist/works.html vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,29 +0,0 @@
version: "3.8"
networks:
traefik_public:
external: true
services:
site:
image: $PLESHEVSKI_IMAGE
networks:
- traefik_public
deploy:
replicas: 1
endpoint_mode: vip
update_config:
order: start-first
rollback_config:
order: start-first
labels:
- traefik.enable=true
- traefik.constraint-label=magenta_public
- traefik.http.routers.to_pleshevski_site.rule=Host(`pleshevski.ru`)
- traefik.http.routers.to_pleshevski_site.entrypoints=https
- traefik.http.routers.to_pleshevski_site.tls=true
- traefik.http.routers.to_pleshevski_site.tls.certresolver=le
- traefik.http.services.pleshevski_site.loadbalancer.server.port=33334
placement:
constraints:
- node.role == worker

6
docs/.vuepress/client.ts Normal file
View file

@ -0,0 +1,6 @@
import { defineClientConfig } from "vuepress/client";
import WorksPage from "./pages/WorksPage.vue";
export default defineClientConfig({
layouts: { WorksPage },
});

84
docs/.vuepress/config.ts Normal file
View file

@ -0,0 +1,84 @@
import { defineUserConfig } from "vuepress";
import { viteBundler } from "@vuepress/bundler-vite";
import { mkMyTheme } from "./theme.js";
import { redirectPlugin } from "@vuepress/plugin-redirect";
export default defineUserConfig({
lang: "ru-RU",
title: "Дмитрий Плешевский",
description: " ",
locales: {
"/": {
lang: "ru-RU",
title: "Дмитрий Плешевский",
},
"/eng/": {
lang: "en-US",
title: "Dmitriy Pleshevskiy",
},
},
theme: mkMyTheme({
hostname: "https://pleshevski.ru",
repo: "https://git.pleshevski.ru/pleshevskiy/pleshevski.ru",
locales: {
"/": {
selectLanguageName: "Русский",
navbar: ["/", "/works"],
notFound: [
"Верните страницу!",
"Мы потеряли страницу...",
"Вы заблокированы в лабиринте.",
"Искали кота?",
"Страница украдена.",
"Ошиблись в параллельной вселенной.",
],
backToHome: "Вернуться на главную",
worksTable: {
name: "Название",
description: "Описание",
role: "Роль",
technologies: "Технологии",
start: "Начало",
statusOrEnd: "Статус/Окончание",
},
},
"/eng/": {
selectLanguageName: "English",
navbar: ["/eng/", "/eng/works"],
notFound: [
"Return the page!",
"We lost a page...",
"You're blocked in a labyrinth.",
"Looking for a cat?",
"The page has been stolen.",
"Wrong turn in parallel universe.",
],
backToHome: "Back to home",
worksTable: {
name: "Name",
description: "Description",
role: "Role",
technologies: "Technologies",
start: "Start",
statusOrEnd: "Status/End",
},
},
},
}),
plugins: [
redirectPlugin({
config: {
"/rus/": "/",
"/rus/works.html": "/works.html",
},
}),
],
bundler: viteBundler(),
});

View file

@ -1,3 +1,5 @@
export type NonEmptyArray<T> = [T, ...T[]];
export function renderDate(date: Date): string {
return date.toLocaleDateString(undefined, {
year: "numeric",

View file

@ -0,0 +1,44 @@
<script lang="ts">
export default {
name: 'WorksPage',
computed: {
tableTheme() {
return this.$themeLocale.worksTable ?? {};
},
},
}
</script>
<script lang="ts" setup>
import { CHRONOLOGICAL_WORKS } from '../data.ts'
import { renderDate } from '../../../global.ts'
import { work as w } from '../domain'
</script>
<template>
<table>
<thead>
<tr>
<th>{{ tableTheme.name }}</th>
<th>{{ tableTheme.description }}</th>
<th>{{ tableTheme.role }}</th>
<th>{{ tableTheme.technologies }}</th>
<th>{{ tableTheme.start }}</th>
<th>{{ tableTheme.statusOrEnd }}</th>
</tr>
</thead>
<tbody>
<tr v-for="work in CHRONOLOGICAL_WORKS">
<td><a rel="nofollow noopener" :href="w.getExternalLink(work)">{{ work.name }}</a></td>
<td>{{ work.description }}</td>
<td v-html="work.roles.join(', ')"></td>
<td>{{ work.technologies.join(', ') }}</td>
<td>{{ renderDate(work.startDate) }}</td>
<td>{{ work.endDate ? renderDate(work.endDate) : work.status }}</td>
</tr>
</tbody>
</table>
</template>

View file

@ -0,0 +1 @@
export { default as ChronologicalWorksTable } from "./ChronologicalWorksTable.vue";

View file

@ -1,7 +1,7 @@
import { Role } from "./domain/Role.ts";
import { Status } from "./domain/Status.ts";
import { Technology } from "./domain/Technology.ts";
import { Work } from "./domain/Work.ts";
import type { Work } from "./domain/Work.ts";
export const CHRONOLOGICAL_WORKS: Work[] = [
{
@ -19,10 +19,7 @@ export const CHRONOLOGICAL_WORKS: Work[] = [
url: "/pleshevskiy/mindustry-tools",
description: "Tools for the Mindustry game",
roles: [Role.Author],
technologies: [
Technology.Nix,
Technology.Godot,
],
technologies: [Technology.Nix, Technology.Godot],
startDate: new Date("2024-01-07"),
status: Status.PassivelyMaintained,
},
@ -135,11 +132,7 @@ export const CHRONOLOGICAL_WORKS: Work[] = [
url: "/drone_plugins/docker_stack",
description: "Deploy to production using `docker stack deploy`",
roles: [Role.Author],
technologies: [
Technology.Docker,
Technology.Drone,
Technology.Woodpecker,
],
technologies: [Technology.Docker, Technology.Drone, Technology.Woodpecker],
startDate: new Date("2022-06-06"),
status: Status.PassivelyMaintained,
},

View file

@ -1,4 +1,4 @@
import { NonEmptyArray } from "../../../global.ts";
import type { NonEmptyArray } from "../../../global.ts";
import { Role } from "./Role.ts";
import { Status } from "./Status.ts";
import { Technology } from "./Technology.ts";

View file

@ -0,0 +1 @@
export * from "./ChronologicalWorksTable";

View file

@ -0,0 +1,42 @@
<script setup>
// See: https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/theme-default/layouts/Layout.vue
import ParentLayout from '@vuepress/theme-default/layouts/Layout.vue'
import { ChronologicalWorksTable } from "../modules/work";
</script>
<template>
<ParentLayout>
<template #page-bottom>
<div class="works-table-wrapper">
<ChronologicalWorksTable />
</div>
</template>
</ParentLayout>
</template>
<style lang="css" scoped>
.works-table-wrapper {
margin: 0 auto;
padding: 0 2rem;
max-width: 1224px;
box-sizing: border-box
}
.works-table-wrapper :deep(table) {
width: 100%;
}
</style>
<style lang="css">
:root {
--content-width: 960px;
}
.vp-page .theme-default-content {
padding-bottom: 0 !important;
}
.vp-page-meta {
display: none;
}
</style>

10
docs/.vuepress/theme.ts Normal file
View file

@ -0,0 +1,10 @@
import { defaultTheme } from '@vuepress/theme-default'
export const mkMyTheme = (options) => {
// returns a theme object
return {
name: 'my-theme',
extends: defaultTheme(options),
}
}

View file

@ -1,6 +1,10 @@
###
---
home: true
title: Resume
heroText: null
---
Always up-to-date link to [resume](https://pleshevski.ru/eng/).
Always up-to-date link to [resume](/eng/).
### Overview

View file

@ -1,3 +1,9 @@
---
title: Works
layout: WorksPage
sidebar: false
---
### Highlighted working experience
#### Binary Management
@ -8,17 +14,17 @@
Development of a project management tool for interior designers
- Development of the GraphQL API (Node.JS, Apollo, PostgreSQL, Redis, BullMQ).
Moved database triggers to business logic. Wrote integration tests on 70% api.
Moved database triggers to business logic. Wrote integration tests on 70% api.
- Development of the frontend (React, Antd). Formed uikit, shared components,
redesigned the page generation gathering. Completely changed work with API on
the frontend. Introduced the practice of writing integration tests using
cypress
redesigned the page generation gathering. Completely changed work with API on
the frontend. Introduced the practice of writing integration tests using
cypress
- Completely ported the project to TypeScript. I have formed isolated modules of
the system.
the system.
- As a team leader, I brought the critical chain method, the buffer method, and
the planning method to the project from the end. Helped the team get into a
rhythm to make releases each week in small batches. A couple of times I also
prepared an individual development plan for team members.
the planning method to the project from the end. Helped the team get into a
rhythm to make releases each week in small batches. A couple of times I also
prepared an individual development plan for team members.
#### Master Progress
@ -27,11 +33,11 @@ Development of a project management tool for interior designers
Development web infrastructure of the educational center Master Progress
- Development of [the main site](https://masterprogress.ru) (Python, Flask).
- Development of [Student's cabinet](https://cabinet.masterprogress.ru) (Python,
Flask, TypeScript, React).
- Development of [a tool for rosmintrud](https://rosmintrud.masterprogress.ru)
(Deno, Vue, Typescript)
- [The main site](https://masterprogress.ru) (Python, Flask).
- [Student's cabinet](https://cabinet.masterprogress.ru) (Python,
Flask, TypeScript, React).
- [A tool for rosmintrud](https://rosmintrud.masterprogress.ru)
(Deno, Vue, Typescript)
- Created a complete infrastructure on Woodpecker CI and Docker swarm.
#### Core Spirit
@ -39,13 +45,13 @@ Development web infrastructure of the educational center Master Progress
- Dates: August 2018 - May 2020
- Role: Lead Fullstack Developer
Development of Social platform focusing on human and planetary enhancement
Development of Social platform focusing on human and planetary enhancement.
- Development of the REST API (Node.JS, Express, PostgreSQL) for main site and
backoffice.
- Development of an auto poster to various social networks and messengers
(Facebook, LinkedIn, Twitter, Telegram).
- Development of a neural network for automatic categorization of articles.
- REST API (Node.JS, Express, PostgreSQL) for main site and
backoffice.
- Auto poster to various social networks and messengers
(Facebook, LinkedIn, Twitter, Telegram).
- Neural network for automatic categorization of articles.
#### MERLION
@ -58,7 +64,8 @@ completed:
- optimize the creation of promotional pages (PHP, JavaScript)
- support main traditional site <https://citilink.ru> (PHP, JavaScript)
- development of parsing to monitor products for changes in price,
quantity/availability in stock, rating and other fields based on data from 55
websites (Node.JS, Express)
quantity/availability in stock, rating and other fields based on data from 55
websites (Node.JS, Express)
- work with neural networks for matching of goods
- development face recognition apps for Android (Java)

View file

@ -1,6 +1,10 @@
###
---
home: true
title: Резюме
heroText: null
---
Всегда актуальная ссылка на [резюме](https://pleshevski.ru/rus/).
Всегда актуальная ссылка на [резюме](/).
### Общие сведения
@ -63,7 +67,7 @@ Backend (Node.JS)
Frontend
- React (твёрдый 8-летний опыт)
- VueJS (предпочитаю, твёрдый 3-летний опыт)
- VueJS (предпочитаю, твёрдый 4-летний опыт)
- Cypress (предпочитаю, твёрдый 3-летний опыт)
- JQuery
- Antd / Antdv

View file

@ -1,6 +1,12 @@
### Выделенный опыт работы
---
title: Работы
layout: WorksPage
sidebar: false
---
#### Binary Management
## Выделенный опыт работы
### Binary Management
- Даты: Август 2018 по настоящее время
- Роли: Lead Fullstack Developer, Team Lead, Architect
@ -8,47 +14,47 @@
Разработка инструмента управления проектами для дизайнеров интерьера
- Разработка GraphQL API (Node.JS, Apollo, PostgreSQL, Redis, BullMQ). Перенес
триггеры базы данных в бизнес-логику. Написал интеграционные тесты на 70% api.
триггеры базы данных в бизнес-логику. Написал интеграционные тесты на 70% api.
- Разработка фронтенда (React, Antd). Сформировал uikit и общие компоненты,
оптимизировал сложные и нагруженные компоненты. Полностью изменил работу с API
на фронтенде. Внедрил практику написания интеграционных тестов с помощью
cypress.
оптимизировал сложные и нагруженные компоненты. Полностью изменил работу с API
на фронтенде. Внедрил практику написания интеграционных тестов с помощью
cypress.
- Полностью перенес проект на TypeScript. Сформировал изолированные модули
системы.
системы.
- Как руководитель команды, я привнес в проект метод критической цепи, метод
буфера и метод планирования с конца. Помог команде войти в ритм, чтобы
выпускать релизы каждую неделю небольшими партиями. Я также несколько раз
составлял индивидуальный план развития для членов команды.
буфера и метод планирования с конца. Помог команде войти в ритм, чтобы
выпускать релизы каждую неделю небольшими партиями. Я также несколько раз
составлял индивидуальный план развития для членов команды.
#### Master Progress
### Master Progress
- Даты: Май 2018 - по настоящее время (Пассивная поддержка)
- Роль: Tech Lead
Разработка веб-инфраструктуры образовательного центра Мастер Прогресс
- Разработка [главного сайта](https://masterprogress.ru) (Python, Flask).
- Разработка [кабинета студента](https://cabinet.masterprogress.ru) (Python,
Flask, TypeScript, React).
- Разработка
[инструмента для работы с rosmintrud](https://rosmintrud.masterprogress.ru)
(Deno, Vue, Typescript)
- [Главного сайта](https://masterprogress.ru) (Python, Flask).
- [Кабинета студента](https://cabinet.masterprogress.ru) (Python, Flask,
TypeScript, React).
- [Инструмента для работы с rosmintrud](https://rosmintrud.masterprogress.ru)
(Deno, Vue, Typescript)
- Создана полная инфраструктура на Woodpecker CI и Docker swarm.
#### Core Spirit
### Core Spirit
- Даты: Август 2018 - May 2020
- Роль: Lead Fullstack Developer
Разработка социальной платформы, сфокусированной на улучшении человека и планеты
Разработка социальной платформы, сфокусированной на улучшении человека и
планеты.
- Разработка REST API (Node.JS, Express, PostgreSQL) для основного сайта и
бэк-офиса.
- Разработка автопостера в различные социальные сети и мессенджеры (Facebook,
LinkedIn, Twitter, Telegram).
- Разработка нейронной сети для автоматической категоризации статей.
- REST API (Node.JS, Express, PostgreSQL) для основного сайта и
бэк-офиса.
- Автопостер в различные социальные сети и мессенджеры (Facebook,
LinkedIn, Twitter, Telegram).
- Создал нейронную сеть для автоматической категоризации статей.
#### MERLION
### MERLION
- Dates: March 2016 May 2018
- Role: Senior Fullstack developer
@ -57,9 +63,13 @@
- Оптимизация создания рекламных страниц (PHP, JavaScript)
- Поддержка основного традиционного сайта <https://citilink.ru> (PHP,
JavaScript)
JavaScript)
- Разработка парсинга для мониторинга товаров на предмет изменения цены,
количества/наличия на складе, рейтинга и других полей на основе данных с 55+
сайтов (Node.js, Express)
количества/наличия на складе, рейтинга и других полей на основе данных с 55+
сайтов (Node.js, Express)
- Работа с нейронными сетями для подбора товаров
- Разработка приложений для распознавания лиц для Android (Java)
## Хронология
В списке перечислены только публичные проекты.

View file

@ -1,12 +1,30 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1654593855,
"narHash": "sha256-c+SyXvj7THre87OyIdZfRVR+HhI/g1ZDrQ3VUtTuHkU=",
"lastModified": 1721562059,
"narHash": "sha256-Tybxt65eyOARf285hMHIJ2uul8SULjFZbT9ZaEeUnP8=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "033bd4fa9a8fbe0c68a88e925d9a884161044b25",
"rev": "68c9ed8bbed9dfce253cc91560bf9043297ef2fe",
"type": "github"
},
"original": {
@ -18,22 +36,22 @@
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"utils": "utils"
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"utils": {
"systems": {
"locked": {
"lastModified": 1653893745,
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}

View file

@ -3,20 +3,44 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
utils.url = "github:numtide/flake-utils";
flake-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
];
outputs = { self, nixpkgs, flake-utils }:
let
out = system:
let
inherit (builtins) substring;
pkgs = import nixpkgs { inherit system; };
version = "0.0.1+${substring 0 8 self.lastModifiedDate}_${self.shortRev or "dirty"}";
in
{
packages.default = with pkgs; stdenv.mkDerivation (finalAttrs: {
pname = "pleshevski_site";
inherit version;
src = ./.;
dontBuild = true;
installPhase = ''
mkdir -p $out
cp -r dist/* $out
'';
});
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_22
gnumake
pnpm
nodePackages.typescript-language-server # typescript
nodePackages.vscode-langservers-extracted # html, css, json, eslint
];
};
};
};
in with utils.lib; eachSystem defaultSystems out;
in
flake-utils.lib.eachDefaultSystem out;
}

View file

@ -1 +0,0 @@
export type NonEmptyArray<T> = [T, ...T[]];

View file

@ -1,6 +0,0 @@
{
"imports": {
"par/": "https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/257079305813c4c0a71c16a89164c20bbce1a5e2/par/",
"ren/": "https://git.pleshevski.ru/pleshevskiy/paren/raw/commit/257079305813c4c0a71c16a89164c20bbce1a5e2/ren/"
}
}

14
log.ts
View file

@ -1,14 +0,0 @@
export function error(...args: unknown[]): void {
console.log("[ERROR]", ...args);
}
export function info(...args: unknown[]): void {
console.log("[INFO]", ...args);
}
export function debug(...args: unknown[]): void {
// choose better name for this env
if (Deno.env.get("DEBUG") === "1") {
console.log("[DEBUG]", ...args);
}
}

View file

@ -1,54 +0,0 @@
import { AnyNode, E } from "ren/node.ts";
import { WorkLink } from "../mod.ts";
import { renderDate } from "../../../render.ts";
import { CHRONOLOGICAL_WORKS } from "../data.ts";
import { RoleList } from "./RoleList.ts";
import { TechnologyList } from "./TechnologyList.ts";
export type ChronologicalWorksTableTranslations = Readonly<
Record<
| "Name"
| "Description"
| "Role"
| "Technologies"
| "Start"
| "Status_or_End",
string
>
>;
const tr = E.bind(null, "tr", []);
const td = E.bind(null, "td", []);
const th = E.bind(null, "th", []);
export const ChronologicalWorksTable = (
i18n: ChronologicalWorksTableTranslations,
): AnyNode =>
E("table", [], [
E("thead", [], [
tr([
th(i18n.Name),
th(i18n.Description),
th(i18n.Role),
th(i18n.Technologies),
th(i18n.Start),
th(i18n.Status_or_End),
]),
]),
E(
"tbody",
[],
CHRONOLOGICAL_WORKS.map((work) =>
tr([
td([WorkLink(work)]),
td(work.description),
td([RoleList(work.roles)]),
td([TechnologyList(work.technologies)]),
td(renderDate(work.startDate)),
td(
work.endDate ? renderDate(work.endDate) : work.status,
),
])
),
),
]);

View file

@ -1,6 +0,0 @@
import { NonEmptyArray } from "../../../global.ts";
import { Role } from "../domain/mod.ts";
import { AnyNode, TextNode } from "ren/node.ts";
export const RoleList: (roles: NonEmptyArray<Role>) => AnyNode = (roles) =>
new TextNode(roles.join(", "));

View file

@ -1 +0,0 @@
export { ChronologicalWorksTable } from "./ChronologicalWorksTable.ts";

View file

@ -1,6 +0,0 @@
import { AnyNode } from "ren/node.ts";
import { Work, work as w } from "./domain/mod.ts";
import { Link } from "../../uikit/link.ts";
export const WorkLink: (work: Work) => AnyNode = (work) =>
Link(work.name, { href: w.getExternalLink(work) });

View file

@ -1 +0,0 @@
export { WorkLink } from "./WorkLink.ts";

36
package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "pleshevskiy",
"version": "0.0.1",
"description": "Dmitriy Pleshevskiy",
"license": "MIT",
"type": "module",
"scripts": {
"docs:build": "vuepress build docs",
"docs:clean-dev": "vuepress dev docs --clean-cache",
"docs:dev": "vuepress dev docs",
"docs:update-package": "pnpm dlx vp-update"
},
"devDependencies": {
"@vuepress/bundler-vite": "^2.0.0-rc.7",
"@vuepress/plugin-blog": "^2.0.0-rc.11",
"@vuepress/plugin-redirect": "2.0.0-rc.8",
"@vuepress/theme-default": "^2.0.0-rc.11",
"vue": "^3.4.0",
"vuepress": "^2.0.0-rc.7",
"@rushstack/eslint-patch": "^1.1.4",
"@types/node": "^20.14.2",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/language-server": "^2.0.19",
"@vue/tsconfig": "^0.5.1",
"@vue/typescript-plugin": "^2.0.19",
"eslint": "<9.0.0",
"eslint-plugin-cypress": "^3.3.0",
"eslint-plugin-vue": "^9.14.1",
"prettier": ">=3.0.0",
"typescript": "~5.4.5",
"vue-tsc": "^1.6.0"
}
}

4554
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,570 +0,0 @@
@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-brand-faded-blue: #f5f5ff;
--color-graphite: #212121;
--color-warm-gray: #757575;
--color-pale: #b6b6b6;
--color-faded: #e0e0e0;
--max-content-width: 1440px;
--min-content-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-content-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: var(--color-brand-blue);
border-bottom: dashed 1px var(--color-brand-blue);
}
a:hover, a:focus {
border-bottom-style: solid;
}
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, .responsive-typography th, .responsive-typography td {
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 p + ul,
.responsive-typography ul + p,
.responsive-typography p + p {
margin-top: 1rem;
}
.responsive-typography li + li {
margin-top: 0.5rem;
}
.responsive-typography table {
table-layout: fixed;
border-collapse: collapse;
line-height: 1.5;
}
.responsive-typography thead {
background-color: var(--color-brand-faded-blue);
}
.responsive-typography tbody tr {
border-top: solid 1px var(--color-pale);
}
.responsive-typography th, .responsive-typography td {
text-align: initial;
}
.responsive-typography th:not(:first-of-type), .responsive-typography td:not(:first-of-type) {
border-left: solid 1px var(--color-pale);
}
.responsive-typography td:nth-child(n+3) {
color: var(--color-warm-gray);
}
.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

@ -1 +0,0 @@
{"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;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;EACA;;AAKA;EACE;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;ACzSF;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;AAAA;AAAA;EAGE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;EAEE;;AAEA;EACE;;AAIJ;EACE;;;AC9EJ;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"}

246
server.ts
View file

@ -1,246 +0,0 @@
import { MarkdownParser } from "par/md.ts";
import { HtmlStrRenderer } from "ren/html_str.ts";
import * as log from "./log.ts";
import rusTranslates from "./translates/rus.ts";
import type { Translations } from "./translates/rus.ts";
import { Context, getLangHref, getLangUrlPrefix, Lang } from "./context.ts";
import { E404Page } from "./views/pages/e404.ts";
import { E500Page } from "./views/pages/e500.ts";
import { WorksPage } from "./views/pages/works.ts";
import { Layout } from "./views/comp/layout.ts";
import { ContentPage } from "./views/pages/content.ts";
if (import.meta.main) {
await main();
}
async function main() {
await startServer({ port: 33334 });
}
async function startServer(cfg: ServerConfig) {
const srv = Deno.listen({ hostname: "0.0.0.0", 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 restCtx = createRestContextFromRequest(req);
try {
const res = await tryCreateFileResponse(restCtx.locPath);
return res;
} catch (_) {
if (restCtx.lang == null && restCtx.newLang) {
return new Response(null, {
status: 301,
headers: {
location: getLangUrlPrefix(restCtx.newLang) + restCtx.locPath,
},
});
}
const ctx = intoAppContext(restCtx);
if (restCtx.lang !== Lang.Rus) {
await loadAndUpdateTranslations(ctx);
}
log.debug({ context: restCtx });
const par = new MarkdownParser();
const ren = new HtmlStrRenderer({
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 (restCtx.locPath === "/" || restCtx.locPath === "/about") {
const res = par.parse(
await readMarkdownFile("data/about", ctx.lang),
);
return createHtmlResponse(ren.render(ContentPage(ctx, res)));
} else if (restCtx.locPath === "/works") {
const res = par.parse(
await readMarkdownFile("data/works", ctx.lang),
);
return createHtmlResponse(ren.render(WorksPage(ctx, res)));
} else {
return createHtmlResponse(ren.render(E404Page(ctx)), 404);
}
} catch (e) {
log.error(e);
return createHtmlResponse(ren.render(E500Page(ctx)), 500);
}
}
}
async function readMarkdownFile(dirPath: string, lang: Lang): Promise<string> {
return await Deno.readTextFile(`${dirPath}/${lang}.md`)
.catch((_) => Deno.readTextFile(`${dirPath}/${Lang.Rus}.md`));
}
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 intoAppContext(restCtx: RestContext): Context {
return {
locPath: restCtx.locPath,
lang: restCtx.lang || Lang.Rus,
tr: restCtx.tr,
};
}
function createRestContextFromRequest(req: Request): RestContext {
log.debug(req.headers);
const locUrl = new URL(req.url);
const lang = tryIntoAppLangFromUrl(locUrl);
return {
lang,
newLang: getPreferRequestLang(req.headers) ?? Lang.Rus,
locPath: stripPrefix(`/${lang}`, locUrl.pathname),
tr: rusTranslates,
};
}
interface RestContext {
locPath: string;
lang: Lang | null;
newLang: Lang;
tr: Translations;
}
function getPreferRequestLang(headers: Headers): Lang | null {
const acceptLanguageHeader = headers.get("accept-language");
if (!acceptLanguageHeader) return null;
const acceptLanguages = acceptLanguageHeader
.split(/,\s*/)
.map((part) => part.split(";q=")[0])
.map(tryIntoAppLangFromAcceptLangCode)
.filter((lang): lang is Lang => !!lang);
return acceptLanguages[0] ?? null;
}
function tryIntoAppLangFromAcceptLangCode(lang: string): Lang | null {
return lang === "*"
? Lang.Rus
: lang.startsWith("en")
? Lang.Eng
: lang.startsWith("ru")
? Lang.Rus
: null;
}
function tryIntoAppLangFromUrl(url: URL): Lang | null {
return url.pathname.startsWith("/eng/")
? Lang.Eng
: url.pathname.startsWith("/rus/")
? Lang.Rus
: null;
}
function stripPrefix(prefix: string, val: string): string {
return val.startsWith(prefix) ? val.slice(prefix.length) : val;
}
function createHtmlResponse(body: string, status = 200): Response {
return new Response(body, {
status,
headers: getContentTypeHeader("html"),
});
}
async function tryCreateFileResponse(urlPath: string): Promise<Response> {
const filePath = extractFilePath(urlPath);
log.debug({ filePath });
if (!filePath) throw new SkipFile();
const content = await Deno.readTextFile(filePath).catch(() => {
throw new SkipFile();
});
return createFileResponse(content, getFileExt(filePath));
}
class SkipFile extends Error {}
function createFileResponse(content: string, fileExt: string): Response {
return new Response(content, {
headers: getContentTypeHeader(fileExt),
});
}
function extractFilePath(urlPath: string): string | null {
const relPath = urlPath.slice(1);
if (relPath.startsWith("styles/")) {
return `public/${relPath}`;
}
return null;
}
function getContentTypeHeader(fileExt: string): Record<string, string> {
return { "content-type": getContentTypeByExt(fileExt) };
}
function getContentTypeByExt(fileExt: string): string {
switch (fileExt) {
case "html":
return "text/html";
case "css":
return "text/css";
default:
return "text/plain";
}
}
function getFileExt(filePath: string): string {
return filePath.slice((filePath.lastIndexOf(".") - 1 >>> 0) + 2);
}

View file

@ -1,6 +0,0 @@
.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) }

View file

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

View file

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

View file

@ -1,9 +0,0 @@
.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

@ -1,86 +0,0 @@
.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;
}
}
p + ul,
ul + p,
p + p {
margin-top: 1rem;
}
li + li {
margin-top: 0.5rem;
}
table {
table-layout: fixed;
border-collapse: collapse;
line-height: 1.5;
}
thead {
background-color: var(--color-brand-faded-blue);
}
tbody tr {
border-top: solid 1px var(--color-pale);
}
th, td {
@extend .pad-0x5;
text-align: initial;
&:not(:first-of-type) {
border-left: solid 1px var(--color-pale);
}
}
td:nth-child(n+3) {
color: var(--color-warm-gray)
}
}

View file

@ -1,11 +0,0 @@
.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) }

View file

@ -1,11 +0,0 @@
#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);
}

View file

@ -1,307 +0,0 @@
@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-brand-faded-blue: #f5f5ff;
--color-graphite: #212121;
--color-warm-gray: #757575;
--color-pale: #b6b6b6;
--color-faded: #e0e0e0;
// Layout
--max-content-width: #{$page-max-width + px};
--min-content-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-content-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: var(--color-brand-blue);
border-bottom: dashed 1px var(--color-brand-blue);
// TODO: move to other place
@extend .anim;
&:hover, &:focus {
border-bottom-style: solid;
}
}
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);
}

View file

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

View file

@ -1,165 +0,0 @@
@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);}
}

View file

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

View file

@ -1,29 +0,0 @@
@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

@ -1,83 +0,0 @@
/* 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

@ -1,21 +0,0 @@
.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);
}
}
}

View file

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

View file

@ -1,9 +0,0 @@
@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);

View file

@ -1,16 +0,0 @@
import { Translations } from "./rus.ts";
export default {
About: "About",
Works: "Works",
Chronological: "Chronological",
Source_code: "Source code",
Page_not_found: "Page not found",
Internal_server_error: "Internal server error",
Name: "Name",
Description: "Description",
Role: "Role",
Technologies: "Technologies",
Start: "Start",
Status_or_End: "Status/End",
} as Translations;

View file

@ -1,18 +0,0 @@
export const rus = {
About: "Обо мне",
Works: "Работы",
Chronological: "Хронология",
Source_code: "Исходный код",
Page_not_found: "Страница не найдена",
Internal_server_error: "Внутренняя ошибка сервера",
Name: "Название",
Description: "Описание",
Role: "Роль",
Technologies: "Технологии",
Start: "Начало",
Status_or_End: "Статус/Окончание",
};
export default rus;
export type Translations = typeof rus;

11
tsconfig.json Normal file
View file

@ -0,0 +1,11 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "docs/**/*", "docs/**/*.vue"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./docs/*"]
}
}
}

View file

@ -1,20 +0,0 @@
import { AnyNode, Attrs, E } from "ren/node.ts";
export function Link(
text: string,
sourceAttrs: Attrs | Attrs[],
): AnyNode {
const attrs = Array.isArray(sourceAttrs) ? sourceAttrs : [sourceAttrs];
const isExternal = attrs.some((attr) =>
typeof attr.href === "string" && attr.href?.startsWith("http")
);
if (isExternal) {
attrs.push({
target: "_blank",
rel: "external nofollow noopener noreferrer",
});
}
return E("a", attrs, text);
}

View file

@ -1,6 +0,0 @@
import { classNames } from "ren/attrs.ts";
import { E, Elem } from "ren/node.ts";
export function H3(text: string): Elem {
return E("h3", classNames("font-h3"), text);
}

View file

@ -1,19 +0,0 @@
import { AnyNode, E } from "ren/node.ts";
import { Context } from "../../context.ts";
export function Layout(ctx: Context, page: AnyNode): AnyNode {
return E("html", { lang: ctx.lang }, [
E("head", [], [
E("meta", { charset: "utf-8" }),
E("meta", {
name: "viewport",
content: "width=device-width, initial-scale=1",
}),
E("link", { rel: "stylesheet", href: "/styles/main.css" }),
E("title", [], ctx.title ?? "Pleshevski"),
]),
E("body", [], [
E("div", { id: "root" }, [page]),
]),
]);
}

View file

@ -1,72 +0,0 @@
import { AnyNode, Attrs, E, Elem } from "ren/node.ts";
import { classNames } from "ren/attrs.ts";
import { Context, getLangHref, iterLangs, Lang } from "../../context.ts";
import { Link } from "../../uikit/link.ts";
import { renderDate } from "../../render.ts";
const SITE_UPDATED_AT = new Date();
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"), [
E("div", classNames("gap-v-1x5"), [
E("div", [], [
E("b", [], "Updated At:"),
renderDate(SITE_UPDATED_AT),
]),
E("div", [], [
Link(ctx.tr.Source_code, {
href: "https://git.pleshevski.ru/pleshevskiy/pleshevski.ru",
}),
]),
]),
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) });
}

View file

@ -1,12 +0,0 @@
import { PageLayout } from "../comp/page_layout.ts";
import { AnyNode, E } from "ren/node.ts";
import { classNames } from "ren/attrs.ts";
import { Context } from "../../context.ts";
export function ContentPage(ctx: Context, content: AnyNode): AnyNode {
ctx.title = "About | Pleshevski";
return PageLayout(ctx, [
E("div", classNames("content-width responsive-typography"), [content]),
]);
}

View file

@ -1,17 +0,0 @@
import { PageLayout } from "../comp/page_layout.ts";
import { AnyNode, E } from "ren/node.ts";
import { classNames } from "ren/attrs.ts";
import { Context } from "../../context.ts";
import { H3 } from "../../uikit/typo.ts";
export function E404Page(ctx: Context): AnyNode {
ctx.title = "Not Found - 404 | Pleshevski";
return PageLayout(ctx, [E404(ctx)]);
}
export function E404(ctx: Context): AnyNode {
return E("div", classNames("content-width gap-v-1x5"), [
H3(ctx.tr.Page_not_found),
]);
}

View file

@ -1,17 +0,0 @@
import { PageLayout } from "../comp/page_layout.ts";
import { AnyNode, E } from "ren/node.ts";
import { classNames } from "ren/attrs.ts";
import { Context } from "../../context.ts";
import { H3 } from "../../uikit/typo.ts";
export function E500Page(ctx: Context): AnyNode {
ctx.title = "Internal Server Error - 500 | Pleshevski";
return PageLayout(ctx, [E500(ctx)]);
}
export function E500(ctx: Context): AnyNode {
return E("div", classNames("content-width gap-v-1x5"), [
H3(ctx.tr.Internal_server_error),
]);
}

View file

@ -1,18 +0,0 @@
import { PageLayout } from "../comp/page_layout.ts";
import { AnyNode, E } from "ren/node.ts";
import { classNames } from "ren/attrs.ts";
import { Context } from "../../context.ts";
import { ChronologicalWorksTable } from "../../modules/work/ChronologicalWorksTable/mod.ts";
import { H3 } from "../../uikit/typo.ts";
export function WorksPage(ctx: Context, content: AnyNode): AnyNode {
ctx.title = "Works | Pleshevski";
return PageLayout(ctx, [
E("div", classNames("content-width gap-v-1x5 responsive-typography"), [
content,
H3(ctx.tr.Chronological),
ChronologicalWorksTable(ctx.tr),
]),
]);
}