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
@ -27,10 +33,10 @@ 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,
- [The main site](https://masterprogress.ru) (Python, Flask).
- [Student's cabinet](https://cabinet.masterprogress.ru) (Python,
Flask, TypeScript, React).
- Development of [a tool for rosmintrud](https://rosmintrud.masterprogress.ru)
- [A tool for rosmintrud](https://rosmintrud.masterprogress.ru)
(Deno, Vue, Typescript)
- Created a complete infrastructure on Woodpecker CI and Docker swarm.
@ -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
- REST API (Node.JS, Express, PostgreSQL) for main site and
backoffice.
- Development of an auto poster to various social networks and messengers
- Auto poster to various social networks and messengers
(Facebook, LinkedIn, Twitter, Telegram).
- Development of a neural network for automatic categorization of articles.
- Neural network for automatic categorization of articles.
#### MERLION
@ -62,3 +68,4 @@ completed:
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
@ -20,35 +26,35 @@
выпускать релизы каждую неделю небольшими партиями. Я также несколько раз
составлял индивидуальный план развития для членов команды.
#### 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)
- [Главного сайта](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) для основного сайта и
- REST API (Node.JS, Express, PostgreSQL) для основного сайта и
бэк-офиса.
- Разработка автопостера в различные социальные сети и мессенджеры (Facebook,
- Автопостер в различные социальные сети и мессенджеры (Facebook,
LinkedIn, Twitter, Telegram).
- Разработка нейронной сети для автоматической категоризации статей.
- Создал нейронную сеть для автоматической категоризации статей.
#### MERLION
### MERLION
- Dates: March 2016 May 2018
- Role: Senior Fullstack developer
@ -63,3 +69,7 @@
сайтов (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 {
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
nodePackages.sass
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),
]),
]);
}