This commit is contained in:
Sergey Yarkov 2022-08-09 09:14:14 +03:00
parent fbe085d9b8
commit 5908df1500
56 changed files with 1557 additions and 1371 deletions

View File

@ -4,7 +4,7 @@ import { toString } from "hast-util-to-string";
import { escape } from "html-escaper"; import { escape } from "html-escaper";
import solid from "@astrojs/solid-js"; import solid from "@astrojs/solid-js";
import image from "@astrojs/image"; import image from "@astrojs/image";
import compress from "astro-compress"; // import compress from "astro-compress";
import mdx from "@astrojs/mdx"; import mdx from "@astrojs/mdx";
const createAnchorLabel = (heading) => { const createAnchorLabel = (heading) => {
@ -16,15 +16,7 @@ const createAnchorLabel = (heading) => {
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: "https://yarkov.tech", site: "https://yarkov.tech",
integrations: [ integrations: [solid(), image(), mdx()],
solid(),
compress({
img: false,
html: false,
}),
image(),
mdx(),
],
markdown: { markdown: {
syntaxHighlight: "prism", syntaxHighlight: "prism",
remarkPlugins: ["remark-gfm", "remark-smartypants"], remarkPlugins: ["remark-gfm", "remark-smartypants"],

2395
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,11 +19,12 @@
}, },
"devDependencies": { "devDependencies": {
"@astrojs/image": "^0.3.2", "@astrojs/image": "^0.3.2",
"@astrojs/mdx": "^0.7.0", "@astrojs/mdx": "^0.8.0",
"@astrojs/solid-js": "^0.4.1", "@astrojs/solid-js": "^0.4.1",
"@typescript-eslint/eslint-plugin": "^5.32.0", "@types/mdx": "^2.0.2",
"@typescript-eslint/parser": "^5.32.0", "@typescript-eslint/eslint-plugin": "^5.33.0",
"astro": "^1.0.0-rc.7", "@typescript-eslint/parser": "^5.33.0",
"astro": "^1.0.0-rc.8",
"astro-compress": "^0.0.15", "astro-compress": "^0.0.15",
"astro-icon": "^0.7.3", "astro-icon": "^0.7.3",
"eslint": "^8.21.0", "eslint": "^8.21.0",
@ -32,7 +33,7 @@
"hastscript": "^7.0.2", "hastscript": "^7.0.2",
"html-escaper": "^3.0.3", "html-escaper": "^3.0.3",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prettier-plugin-astro": "^0.3.0", "prettier-plugin-astro": "^0.4.1",
"rehype-autolink-headings": "^6.1.1", "rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1", "rehype-slug": "^5.0.1",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",

View File

@ -6,17 +6,17 @@
*/ */
window.theme = (() => { window.theme = (() => {
const local = localStorage.getItem('theme'); const local = localStorage.getItem("theme");
if (local && (local === 'dark' || local === 'light')) return local; if (local && (local === "dark" || local === "light")) return local;
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
return isDark ? 'dark' : 'light'; return isDark ? "dark" : "light";
})(); })();
document.documentElement.setAttribute('data-theme', window.theme); document.documentElement.setAttribute("data-theme", window.theme);
/* Sync with OS theme */ /* Sync with OS theme */
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches: isDark }) => { window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", ({ matches: isDark }) => {
window.theme = isDark ? 'dark' : 'light'; window.theme = isDark ? "dark" : "light";
localStorage.setItem('theme', window.theme); localStorage.setItem("theme", window.theme);
document.documentElement.setAttribute('data-theme', window.theme); document.documentElement.setAttribute("data-theme", window.theme);
}); });

View File

@ -7,30 +7,30 @@
if (window.theme) { if (window.theme) {
/* the {theme} in the path will be dynamically changed */ /* the {theme} in the path will be dynamically changed */
const PATH = `/assets/css/code-highlight-{theme}.min.css`; const PATH = `/assets/css/code-highlight-{theme}.min.css`;
const $head = document.getElementsByTagName('head')[0]; const $head = document.getElementsByTagName("head")[0];
const getPath = (theme) => PATH.replace(`{theme}`, theme); const getPath = (theme) => PATH.replace(`{theme}`, theme);
const createLink = (theme, media) => { const createLink = (theme, media) => {
const $link = document.createElement('link'); const $link = document.createElement("link");
$link.rel = 'stylesheet'; $link.rel = "stylesheet";
$link.type = 'text/css'; $link.type = "text/css";
$link.href = getPath(theme); $link.href = getPath(theme);
$link.media = media; $link.media = media;
return $link; return $link;
}; };
/* init links to load them first */ /* init links to load them first */
const $light = createLink('light', 'none'); const $light = createLink("light", "none");
const $dark = createLink('dark', 'none'); const $dark = createLink("dark", "none");
const toggleMedia = (theme) => { const toggleMedia = (theme) => {
switch (theme) { switch (theme) {
case 'light': case "light":
$light.media = 'all'; $light.media = "all";
$dark.media = 'none'; $dark.media = "none";
break; break;
case 'dark': case "dark":
$light.media = 'none'; $light.media = "none";
$dark.media = 'all'; $dark.media = "all";
break; break;
default: default:
break; break;
@ -41,8 +41,8 @@ if (window.theme) {
$head.appendChild($light); $head.appendChild($light);
$head.appendChild($dark); $head.appendChild($dark);
window.addEventListener('onthemetoggled', ({ detail: newTheme }) => toggleMedia(newTheme)); window.addEventListener("onthemetoggled", ({ detail: newTheme }) => toggleMedia(newTheme));
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches: isDark }) => { window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", ({ matches: isDark }) => {
return isDark ? toggleMedia('dark') : toggleMedia('light'); return isDark ? toggleMedia("dark") : toggleMedia("light");
}); });
} }

View File

@ -1 +1 @@
export { default } from './ArticleContent.astro'; export { default } from "./ArticleContent.astro";

View File

@ -15,7 +15,7 @@
} }
& p { & p {
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
font-family: monospace; font-family: var(--font-family-monospace);
color: var(--font-color-gray); color: var(--font-color-gray);
text-transform: uppercase; text-transform: uppercase;
text-align: right; text-align: right;

View File

@ -1 +1 @@
export * from './ArticleItem'; export * from "./ArticleItem";

View File

@ -1 +1 @@
export { default } from './ArticlePost.astro'; export { default } from "./ArticlePost.astro";

View File

@ -1,12 +1,12 @@
import { Component } from 'solid-js'; import { Component } from "solid-js";
import { createSignal, createEffect, For } from 'solid-js'; import { createSignal, createEffect, For } from "solid-js";
import { search, selectedTags } from '@stores/searchStore'; import { search, selectedTags } from "@stores/searchStore";
import { findArticlesBySearch, findArticlesByTags, sortArtcilesByDate } from '@utils/articles'; import { findArticlesBySearch, findArticlesByTags, sortArtcilesByDate } from "@utils/articles";
import { ArticleItem } from '@components/ArticleItem'; import { ArticleItem } from "@components/ArticleItem";
import SearchInput from '@components/SearchInput'; import SearchInput from "@components/SearchInput";
import TagsList from '@components/TagsList'; import TagsList from "@components/TagsList";
import './ArticlesList.scss'; import "./ArticlesList.scss";
type ArticlesListProps = { type ArticlesListProps = {
data: { data: {
@ -69,7 +69,7 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
<div class="articles-list"> <div class="articles-list">
{isEmpty() ? ( {isEmpty() ? (
<div class="articles-list__empty"> <div class="articles-list__empty">
<p>{props.emptyPlaceholder || 'No articles found...'}</p> <p>{props.emptyPlaceholder || "No articles found..."}</p>
</div> </div>
) : ( ) : (
<For <For

View File

@ -1 +1 @@
export { default } from './ArticlesList'; export { default } from "./ArticlesList";

View File

@ -1,5 +1,5 @@
--- ---
import { SITE_METADATA } from '@root/config'; import { SITE_METADATA } from "@root/config";
export interface Props { export interface Props {
title?: string | undefined; title?: string | undefined;
@ -11,7 +11,8 @@ const { title = SITE_METADATA.title, description } = Astro.props as Props;
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono&display=swap" rel="stylesheet"
/>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@ -21,5 +22,7 @@ const { title = SITE_METADATA.title, description } = Astro.props as Props;
<meta name="title" content={title} /> <meta name="title" content={title} />
<meta name="description" content={description || SITE_METADATA.description} /> <meta name="description" content={description || SITE_METADATA.description} />
<meta name="theme-color" content={SITE_METADATA.themeColor} /> <meta name="theme-color" content={SITE_METADATA.themeColor} />
<script is:inline src="/assets/js/detect-theme.js" /> <script is:inline src="/assets/js/detect-theme.js">
</script>
<slot /> <slot />

View File

@ -1 +1 @@
export { default } from './BaseLayout.astro'; export { default } from "./BaseLayout.astro";

View File

@ -1 +1 @@
export { default } from './Button'; export { default } from "./Button";

View File

@ -1 +1 @@
export { default } from './ContactForm'; export { default } from "./ContactForm";

View File

@ -1 +1 @@
export { default } from './ContactsList.astro'; export { default } from "./ContactsList.astro";

View File

@ -1 +1 @@
export { default } from './Counter'; export { default } from "./Counter";

View File

@ -1 +1 @@
export { default } from './Footer.astro'; export { default } from "./Footer.astro";

View File

@ -1,46 +1,46 @@
--- ---
import './Header.scss'; import { Icon } from "astro-icon";
import { SITE_METADATA } from '@root/config'; import { SITE_METADATA } from "@root/config";
import ThemeSwitcher from '@components/ThemeSwticher'; import ThemeSwitcher from "@components/ThemeSwticher";
import { Icon } from 'astro-icon'; import { removeSlashes } from "@utils/string";
import { removeSlashes } from '@utils/string' import NavToggle from "./NavToggle";
import NavToggle from './NavToggle'; import "./Header.scss";
const { name, nav, contacts } = SITE_METADATA; const { name, nav, contacts } = SITE_METADATA;
const { pathname } = Astro.canonicalURL; const { pathname } = Astro.url;
--- ---
<header class='header'> <header class="header">
<div class='container'> <div class="container">
<div class='header__logo'> <div class="header__logo">
<a href='/'>{name}</a> <a href="/">{name}</a>
</div> </div>
<nav class='header__nav'> <nav class="header__nav">
<ul class='header__nav-links'> <ul class="header__nav-links">
{nav.map((link) => ( {nav.map((link) => (
<li> <li>
<a class={removeSlashes(pathname)===removeSlashes(link.url) && 'active' } href={link.url}> <a class={removeSlashes(pathname) === removeSlashes(link.url) && "active"} href={link.url}>
{link.name} {link.name}
</a> </a>
</li> </li>
))} ))}
</ul> </ul>
<div class='header__nav-contacts'> <div class="header__nav-contacts">
<a href={'mailto:' + contacts.email}> <a href={"mailto:" + contacts.email}>
Почта Почта
<Icon name='email' width='18' height='18' /> <Icon name="email" width="18" height="18" />
</a> </a>
<a href={contacts.githubUrl}> <a href={contacts.githubUrl}>
Github Github
<Icon name='github_filled' width='18' height='18' /> <Icon name="github_filled" width="18" height="18" />
</a> </a>
<a href={contacts.telegramUrl}> <a href={contacts.telegramUrl}>
Телеграм Телеграм
<Icon name='telegram' width='18' height='18' /> <Icon name="telegram" width="18" height="18" />
</a> </a>
</div> </div>
<ThemeSwitcher client:visible /> <ThemeSwitcher client:visible />
</nav> </nav>
<NavToggle client:visible /> <NavToggle client:visible />
</div> </div>
</header> </header>

View File

@ -1,7 +1,7 @@
import type { Component } from 'solid-js'; import type { Component } from "solid-js";
import { createSignal, createEffect } from 'solid-js'; import { createSignal, createEffect } from "solid-js";
import './NavToggle.scss'; import "./NavToggle.scss";
const MenuIcon: Component = () => ( const MenuIcon: Component = () => (
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="24px" height="24px"> <svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="24px" height="24px">
@ -24,11 +24,11 @@ const NavToggle: Component = () => {
const body = document.body; const body = document.body;
if (isNavShown()) { if (isNavShown()) {
body.setAttribute('data-is-mobile-open', 'true'); body.setAttribute("data-is-mobile-open", "true");
return; return;
} }
body.removeAttribute('data-is-mobile-open'); body.removeAttribute("data-is-mobile-open");
}); });
return ( return (

View File

@ -1 +1 @@
export { default } from './NavToggle'; export { default } from "./NavToggle";

View File

@ -1 +1 @@
export { default } from './Header.astro'; export { default } from "./Header.astro";

View File

@ -1,5 +1,5 @@
--- ---
import './PageHeading.scss'; import "./PageHeading.scss";
export interface Props { export interface Props {
heading: string; heading: string;
@ -9,8 +9,8 @@ export interface Props {
const { heading, description } = Astro.props as Props; const { heading, description } = Astro.props as Props;
--- ---
<div class='page-heading'> <div class="page-heading">
<h1 style={!description && 'margin-bottom: 2.5rem;'}> <h1 style={!description && "margin-bottom: 2.5rem;"}>
{heading} {heading}
</h1> </h1>
{description && <p>{description}</p>} {description && <p>{description}</p>}

View File

@ -1 +1 @@
export { default } from './PageHeading.astro'; export { default } from "./PageHeading.astro";

View File

@ -1 +1 @@
export { default } from './ProjectItem.astro'; export { default } from "./ProjectItem.astro";

View File

@ -1 +1 @@
export { default } from './ProjectsList.astro'; export { default } from "./ProjectsList.astro";

View File

@ -1,7 +1,7 @@
--- ---
import './RecentArticles.scss'; import "./RecentArticles.scss";
import { ArticleItem } from '@components/ArticleItem'; import { ArticleItem } from "@components/ArticleItem";
import { sortArtcilesByDate } from '@utils/articles'; import { sortArtcilesByDate } from "@utils/articles";
export interface Props { export interface Props {
articles: ArticleType[]; articles: ArticleType[];
@ -13,13 +13,13 @@ const { articles, length = 5 } = Astro.props as Props;
const recent = sortArtcilesByDate(articles); const recent = sortArtcilesByDate(articles);
--- ---
<section class='recent'> <section class="recent">
<div class='recent-heading'> <div class="recent-heading">
<h2>недавние публикации</h2> <h2>недавние публикации</h2>
<a href='/articles'>смотреть все</a> <a href="/articles">смотреть все</a>
</div> </div>
<div class='recent-list'> <div class="recent-list">
{recent.length !== 0 ? ( {recent.length !== 0 ? (
recent.slice(0, length).map((article) => <ArticleItem data={article} />) recent.slice(0, length).map((article) => <ArticleItem data={article} />)
) : ( ) : (

View File

@ -1 +1 @@
export { default } from './RecentArticles.astro'; export { default } from "./RecentArticles.astro";

View File

@ -1,9 +1,9 @@
import type { Component, JSX } from 'solid-js'; import type { Component, JSX } from "solid-js";
import { createEffect } from 'solid-js'; import { createEffect } from "solid-js";
import { search, setSearch } from '@stores/searchStore'; import { search, setSearch } from "@stores/searchStore";
import * as utils from '@utils/history'; import * as utils from "@utils/history";
import './SearchInput.scss'; import "./SearchInput.scss";
type SearchInputProps = { type SearchInputProps = {
count: number; count: number;
@ -15,10 +15,10 @@ const SearchInput: Component<SearchInputProps> = (props) => {
* Reset state and delete `search` param from url * Reset state and delete `search` param from url
*/ */
const resetSearch = () => { const resetSearch = () => {
const url = utils.deleteParamFromUrl('search'); const url = utils.deleteParamFromUrl("search");
setSearch(''); setSearch("");
history.replaceState(null, '', url); history.replaceState(null, "", url);
}; };
/** /**
@ -32,9 +32,9 @@ const SearchInput: Component<SearchInputProps> = (props) => {
return; return;
} }
const url = utils.setParamToUrl('search', value.trim()); const url = utils.setParamToUrl("search", value.trim());
history.replaceState(null, '', `${location.pathname}?${url}`); history.replaceState(null, "", `${location.pathname}?${url}`);
setSearch(value); setSearch(value);
}; };
@ -42,7 +42,7 @@ const SearchInput: Component<SearchInputProps> = (props) => {
* Set the signal state by reading the `search` parameter from the URL. * Set the signal state by reading the `search` parameter from the URL.
*/ */
createEffect(() => { createEffect(() => {
const param = new URLSearchParams(location.search).get('search'); const param = new URLSearchParams(location.search).get("search");
if (!param) { if (!param) {
resetSearch(); resetSearch();
@ -63,7 +63,7 @@ const SearchInput: Component<SearchInputProps> = (props) => {
value={search()} value={search()}
type="text" type="text"
role="search" role="search"
placeholder={props.placeholder || 'Search article by title or description...'} placeholder={props.placeholder || "Search article by title or description..."}
autocapitalize="off" autocapitalize="off"
autocomplete="off" autocomplete="off"
/> />

View File

@ -1 +1 @@
export { default } from './SearchInput'; export { default } from "./SearchInput";

View File

@ -1 +1 @@
export { default } from './SkillsBlock.astro'; export { default } from "./SkillsBlock.astro";

View File

@ -1 +1 @@
export { default } from './SkillsList.astro'; export { default } from "./SkillsList.astro";

View File

@ -1,7 +1,7 @@
--- ---
import './Splash.scss'; import "./Splash.scss";
--- ---
<section class='splash'> <section class="splash">
<slot></slot> <slot />
</section> </section>

View File

@ -1,7 +1,7 @@
--- ---
import './SplashHeading.scss'; import "./SplashHeading.scss";
--- ---
<header class='splash-heading'> <header class="splash-heading">
<slot></slot> <slot />
</header> </header>

View File

@ -1,7 +1,7 @@
--- ---
import './SplashLinks.scss'; import "./SplashLinks.scss";
--- ---
<nav class='splash-links'> <nav class="splash-links">
<slot></slot> <slot />
</nav> </nav>

View File

@ -1,4 +1,4 @@
export { default as Splash } from './Splash.astro'; export { default as Splash } from "./Splash.astro";
export { default as SplashHeading } from './SplashHeading.astro'; export { default as SplashHeading } from "./SplashHeading.astro";
export { default as SplashLinks } from './SplashLinks.astro'; export { default as SplashLinks } from "./SplashLinks.astro";
export { default as Link } from './Link.astro'; export { default as Link } from "./Link.astro";

View File

@ -1,8 +1,8 @@
import type { Component, JSX } from 'solid-js'; import type { Component, JSX } from "solid-js";
import { For, createEffect } from 'solid-js'; import { For, createEffect } from "solid-js";
import { selectedTags, setSelectedTags } from '@stores/searchStore'; import { selectedTags, setSelectedTags } from "@stores/searchStore";
import * as utils from '@utils/history'; import * as utils from "@utils/history";
type TagsListProps = { type TagsListProps = {
data: string[]; data: string[];
@ -15,14 +15,14 @@ const TagsList: Component<TagsListProps> = (props) => {
* Reset signal state and `tags` property in history state * Reset signal state and `tags` property in history state
*/ */
const resetSelected = () => { const resetSelected = () => {
const url = utils.deleteParamFromUrl('tags'); const url = utils.deleteParamFromUrl("tags");
setSelectedTags([]); setSelectedTags([]);
history.replaceState(null, '', url); history.replaceState(null, "", url);
}; };
const onSelect: JSX.EventHandler<HTMLLIElement, PointerEvent> = (e) => { const onSelect: JSX.EventHandler<HTMLLIElement, PointerEvent> = (e) => {
const tag = e.target.getAttribute('data-tag'); const tag = e.target.getAttribute("data-tag");
/** /**
* Delete tag from selected tags if already selected * Delete tag from selected tags if already selected
@ -30,7 +30,7 @@ const TagsList: Component<TagsListProps> = (props) => {
*/ */
if (isTagSelected(tag)) { if (isTagSelected(tag)) {
const updated = selectedTags().filter((selectedTag) => selectedTag !== tag); const updated = selectedTags().filter((selectedTag) => selectedTag !== tag);
const url = utils.setParamToUrl('tags', updated.join(',')); const url = utils.setParamToUrl("tags", updated.join(","));
if (updated.length === 0) { if (updated.length === 0) {
resetSelected(); resetSelected();
@ -38,7 +38,7 @@ const TagsList: Component<TagsListProps> = (props) => {
} }
setSelectedTags(updated); setSelectedTags(updated);
history.replaceState(null, '', `${location.pathname}?${url}`); history.replaceState(null, "", `${location.pathname}?${url}`);
return; return;
} }
@ -46,14 +46,14 @@ const TagsList: Component<TagsListProps> = (props) => {
* Add selected tag to state and update url * Add selected tag to state and update url
*/ */
const updated = [...selectedTags(), tag]; const updated = [...selectedTags(), tag];
const url = utils.setParamToUrl('tags', updated.join(',')); const url = utils.setParamToUrl("tags", updated.join(","));
setSelectedTags(updated); setSelectedTags(updated);
history.replaceState(null, '', `${location.pathname}?${url}`); history.replaceState(null, "", `${location.pathname}?${url}`);
}; };
createEffect(() => { createEffect(() => {
let param: string | string[] = new URLSearchParams(location.search).get('tags'); let param: string | string[] = new URLSearchParams(location.search).get("tags");
if (!param) { if (!param) {
resetSelected(); resetSelected();
@ -61,7 +61,7 @@ const TagsList: Component<TagsListProps> = (props) => {
} }
/* to array */ /* to array */
param = param.split(','); param = param.split(",");
if (param.length === 0) { if (param.length === 0) {
resetSelected(); resetSelected();

View File

@ -1 +1 @@
export { default } from './TagsList'; export { default } from "./TagsList";

View File

@ -1,7 +1,7 @@
import type { Component, JSX } from 'solid-js'; import type { Component, JSX } from "solid-js";
import { createSignal, onMount, Switch, Match } from 'solid-js'; import { createSignal, onMount, Switch, Match } from "solid-js";
import './ThemeSwitcher.scss'; import "./ThemeSwitcher.scss";
const MoonIcon: Component = () => ( const MoonIcon: Component = () => (
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -22,19 +22,19 @@ const SunIcon: Component = () => (
); );
const ThemeSwitcher: Component = () => { const ThemeSwitcher: Component = () => {
const [theme, setTheme] = createSignal<ThemeModeType>('light'); const [theme, setTheme] = createSignal<ThemeModeType>("light");
const [isReady, setIsReady] = createSignal<boolean>(false); const [isReady, setIsReady] = createSignal<boolean>(false);
const toggleTheme: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => { const toggleTheme: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => {
if (!window.theme) return; if (!window.theme) return;
const newTheme = theme() === 'light' ? 'dark' : 'light'; const newTheme = theme() === "light" ? "dark" : "light";
const toggleEvent = new CustomEvent('onthemetoggled', { detail: newTheme }); const toggleEvent = new CustomEvent("onthemetoggled", { detail: newTheme });
setTheme(newTheme); setTheme(newTheme);
window.dispatchEvent(toggleEvent); window.dispatchEvent(toggleEvent);
localStorage.setItem('theme', newTheme); localStorage.setItem("theme", newTheme);
document.documentElement.setAttribute('data-theme', newTheme); document.documentElement.setAttribute("data-theme", newTheme);
}; };
onMount(() => { onMount(() => {
@ -42,20 +42,20 @@ const ThemeSwitcher: Component = () => {
if (window.theme) { if (window.theme) {
setTheme(window.theme); setTheme(window.theme);
/* Update theme state on changing OS theme */ /* Update theme state on changing OS theme */
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches: isDark }) => { window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", ({ matches: isDark }) => {
const theme = isDark ? 'dark' : 'light'; const theme = isDark ? "dark" : "light";
setTheme(theme); setTheme(theme);
}); });
} }
}); });
return ( return (
<button onClick={toggleTheme} class={isReady() ? 'theme-switcher ready' : 'theme-switcher'} aria-label={theme()}> <button onClick={toggleTheme} class={isReady() ? "theme-switcher ready" : "theme-switcher"} aria-label={theme()}>
<Switch <Switch
children={ children={
<> <>
<Match when={theme() === 'light'} children={<MoonIcon />} /> <Match when={theme() === "light"} children={<MoonIcon />} />
<Match when={theme() === 'dark'} children={<SunIcon />} /> <Match when={theme() === "dark"} children={<SunIcon />} />
</> </>
} }
/> />

View File

@ -1 +1 @@
export { default } from './ThemeSwitcher'; export { default } from "./ThemeSwitcher";

View File

@ -1,132 +1,132 @@
export const SITE_METADATA: SiteMetadataType = { export const SITE_METADATA: SiteMetadataType = {
title: 'Sergey Yarkov', title: "Sergey Yarkov",
name: 'yarkov.tech', name: "yarkov.tech",
author: 'Sergey Yarkov', author: "Sergey Yarkov",
description: description:
'Здесь я публикую статьи и туториалы на различные IT темы, а также рассказываю о своих проектах которые я сделал за последние несколько лет.', "Здесь я публикую статьи и туториалы на различные IT темы, а также рассказываю о своих проектах которые я сделал за последние несколько лет.",
themeColor: '#1e2327', themeColor: "#1e2327",
contacts: { contacts: {
email: 'serzh.yarkov@gmail.com', email: "serzh.yarkov@gmail.com",
githubUrl: 'https://github.com/sergeyyarkov', githubUrl: "https://github.com/sergeyyarkov",
telegramUrl: 'https://t.me/sergeyyarkov', telegramUrl: "https://t.me/sergeyyarkov",
}, },
nav: [ nav: [
{ name: 'Статьи', url: '/articles' }, { name: "Статьи", url: "/articles" },
{ name: 'Проекты', url: '/projects' }, { name: "Проекты", url: "/projects" },
{ name: 'Обо мне', url: '/about' }, { name: "Обо мне", url: "/about" },
{ name: 'Контакты', url: '/contacts' }, { name: "Контакты", url: "/contacts" },
], ],
projects: [ projects: [
{ {
icon: 'book', icon: "book",
name: 'yarkov.tech', name: "yarkov.tech",
year: '2022', year: "2022",
description: 'Мой персональный веб-сайт где находятся информация обо мне и список различных статей, туториалов...', description: "Мой персональный веб-сайт где находятся информация обо мне и список различных статей, туториалов...",
sourceUrl: 'https://github.com/sergeyyarkov/yarkov.tech', sourceUrl: "https://github.com/sergeyyarkov/yarkov.tech",
demoUrl: 'https://yarkov.tech', demoUrl: "https://yarkov.tech",
articleUrl: undefined, articleUrl: undefined,
}, },
{ {
icon: 'triangle_filled', icon: "triangle_filled",
name: 'devthread', name: "devthread",
year: '2021', year: "2021",
description: 'Статический веб-сайт для публикации различных статей, разработанный при помощи Gatsby.js и NetlifyCMS', description: "Статический веб-сайт для публикации различных статей, разработанный при помощи Gatsby.js и NetlifyCMS",
sourceUrl: 'https://github.com/sergeyyarkov/devthread.ru', sourceUrl: "https://github.com/sergeyyarkov/devthread.ru",
demoUrl: 'https://devthread.netlify.app', demoUrl: "https://devthread.netlify.app",
articleUrl: undefined, articleUrl: undefined,
}, },
{ {
icon: 'round-school', icon: "round-school",
name: 'educt', name: "educt",
year: '2022', year: "2022",
description: 'Система управления обучением (LMS) для публикации ваших курсов, лекций, уроков', description: "Система управления обучением (LMS) для публикации ваших курсов, лекций, уроков",
sourceUrl: 'https://github.com/sergeyyarkov/educt', sourceUrl: "https://github.com/sergeyyarkov/educt",
demoUrl: 'https://client-educt-lms.netlify.app', demoUrl: "https://client-educt-lms.netlify.app",
articleUrl: undefined, articleUrl: undefined,
}, },
{ {
icon: 'round-shop-two', icon: "round-shop-two",
name: 'digital-store', name: "digital-store",
year: '2019', year: "2019",
description: 'Настраиваемый интернет-магазин с панелью управления для продажи цифровых товаров', description: "Настраиваемый интернет-магазин с панелью управления для продажи цифровых товаров",
sourceUrl: 'https://github.com/sergeyyarkov/digital-store/tree/digital-store-docker', sourceUrl: "https://github.com/sergeyyarkov/digital-store/tree/digital-store-docker",
demoUrl: 'https://digital-store.herokuapp.com', demoUrl: "https://digital-store.herokuapp.com",
articleUrl: undefined, articleUrl: undefined,
}, },
{ {
icon: 'chat', icon: "chat",
name: 'react-chat-app', name: "react-chat-app",
year: '2021', year: "2021",
description: 'Клиент-серверное веб приложение чата разработанное при помощи Node.js и Socket.io', description: "Клиент-серверное веб приложение чата разработанное при помощи Node.js и Socket.io",
sourceUrl: 'https://github.com/sergeyyarkov/react-chat-app', sourceUrl: "https://github.com/sergeyyarkov/react-chat-app",
demoUrl: undefined, demoUrl: undefined,
articleUrl: undefined, articleUrl: undefined,
}, },
{ {
icon: 'nodejs', icon: "nodejs",
name: 'node-docker-example', name: "node-docker-example",
year: '2022', year: "2022",
description: 'Пример использования Docker для развертывания Node.js приложения', description: "Пример использования Docker для развертывания Node.js приложения",
sourceUrl: 'https://github.com/sergeyyarkov/node-docker-example', sourceUrl: "https://github.com/sergeyyarkov/node-docker-example",
demoUrl: 'https://website-simple-blog.herokuapp.com', demoUrl: "https://website-simple-blog.herokuapp.com",
articleUrl: undefined, articleUrl: undefined,
}, },
{ {
icon: 'chip', icon: "chip",
name: 'memory-led-game', name: "memory-led-game",
year: '2022', year: "2022",
description: 'Аналог популярной игры “Саймон говорит” на AVR микроконтроллере', description: "Аналог популярной игры “Саймон говорит” на AVR микроконтроллере",
sourceUrl: 'https://github.com/sergeyyarkov/attiny24a_memory-led-game', sourceUrl: "https://github.com/sergeyyarkov/attiny24a_memory-led-game",
demoUrl: undefined, demoUrl: undefined,
articleUrl: undefined, articleUrl: undefined,
}, },
], ],
skills: [ skills: [
{ {
title: 'Программирование', title: "Программирование",
list: [ list: [
{ icon: 'nodejs', name: 'Javascript/Typescript' }, { icon: "nodejs", name: "Javascript/Typescript" },
{ icon: 'language-c', name: 'C Language' }, { icon: "language-c", name: "C Language" },
{ icon: 'mysql', name: 'SQL' }, { icon: "mysql", name: "SQL" },
{ icon: 'graphql', name: 'GraphQL' }, { icon: "graphql", name: "GraphQL" },
{ icon: 'avr', name: 'AVR instruction set' }, { icon: "avr", name: "AVR instruction set" },
], ],
}, },
{ {
title: 'Фреймворки', title: "Фреймворки",
list: [ list: [
{ icon: 'adonisjs', name: 'Adonis.js' }, { icon: "adonisjs", name: "Adonis.js" },
{ icon: 'expressjs', name: 'Express.js' }, { icon: "expressjs", name: "Express.js" },
{ icon: 'socket-io', name: 'Socket.io' }, { icon: "socket-io", name: "Socket.io" },
{ icon: 'astro_filled', name: 'Astro' }, { icon: "astro_filled", name: "Astro" },
{ icon: 'reactjs', name: 'React.js' }, { icon: "reactjs", name: "React.js" },
{ icon: 'gatsbyjs', name: 'Gatsby.js' }, { icon: "gatsbyjs", name: "Gatsby.js" },
], ],
}, },
{ {
title: 'Инструменты', title: "Инструменты",
list: [ list: [
{ icon: 'nodejs', name: 'Node.js' }, { icon: "nodejs", name: "Node.js" },
{ icon: 'docker', name: 'Docker' }, { icon: "docker", name: "Docker" },
{ icon: 'git-branch', name: 'Git/gitflow' }, { icon: "git-branch", name: "Git/gitflow" },
{ icon: 'sass', name: 'SASS' }, { icon: "sass", name: "SASS" },
{ icon: 'webpack', name: 'Webpack' }, { icon: "webpack", name: "Webpack" },
{ icon: 'vite', name: 'Vite.js' }, { icon: "vite", name: "Vite.js" },
], ],
}, },
{ {
title: 'Базы данных', title: "Базы данных",
list: [ list: [
{ icon: 'postgresql', name: 'PostgreSQL' }, { icon: "postgresql", name: "PostgreSQL" },
{ icon: 'mongodb', name: 'MongoDB' }, { icon: "mongodb", name: "MongoDB" },
{ icon: 'redis', name: 'Redis' }, { icon: "redis", name: "Redis" },
], ],
}, },
{ {
title: 'Языки', title: "Языки",
list: [ list: [
{ icon: undefined, name: 'Русский (родной)' }, { icon: undefined, name: "Русский (родной)" },
{ icon: undefined, name: 'Английский (B1)' }, { icon: undefined, name: "Английский (B1)" },
], ],
}, },
], ],

View File

@ -1,5 +1,5 @@
--- ---
import NotFound from '@templates/NotFound.astro'; import NotFound from "@templates/NotFound.astro";
--- ---
<NotFound /> <NotFound />

View File

@ -1,5 +1,5 @@
--- ---
import About from '@templates/About.astro'; import About from "@templates/About.astro";
--- ---
<About /> <About />

View File

@ -1,5 +1,5 @@
--- ---
import Articles from '@templates/Articles.astro'; import Articles from "@templates/Articles.astro";
--- ---
<Articles /> <Articles />

View File

@ -1,5 +1,5 @@
--- ---
import Contacts from '@templates/Contacts.astro'; import Contacts from "@templates/Contacts.astro";
--- ---
<Contacts /> <Contacts />

View File

@ -1,5 +1,5 @@
--- ---
import Home from '@templates/Home.astro'; import Home from "@templates/Home.astro";
--- ---
<Home /> <Home />

View File

@ -1,5 +1,5 @@
--- ---
import Projects from '@templates/Projects.astro'; import Projects from "@templates/Projects.astro";
--- ---
<Projects /> <Projects />

View File

@ -1 +1 @@
export * from './searchStore'; export * from "./searchStore";

View File

@ -1,4 +1,4 @@
import { createSignal } from 'solid-js'; import { createSignal } from "solid-js";
export const [search, setSearch] = createSignal<string>(''); export const [search, setSearch] = createSignal<string>("");
export const [selectedTags, setSelectedTags] = createSignal<string[]>([]); export const [selectedTags, setSelectedTags] = createSignal<string[]>([]);

View File

@ -5,6 +5,7 @@
/* Fonts */ /* Fonts */
--font-fallback: -apple-system, BlinkMacSystemFont, Segoe UI, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; --font-fallback: -apple-system, BlinkMacSystemFont, Segoe UI, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
--font-family-base: "Rubik", var(--font-fallback); --font-family-base: "Rubik", var(--font-fallback);
--font-family-monospace: "IBM Plex Mono", Menlo, monospace;
--font-size-xs: 0.75rem; --font-size-xs: 0.75rem;
--font-size-sm: 0.875rem; --font-size-sm: 0.875rem;
--font-size-md: 1rem; --font-size-md: 1rem;

View File

@ -1,10 +1,10 @@
--- ---
import * as utils from '@utils/index'; import * as utils from "@utils/index";
import PageLayout from '@layouts/default.astro'; import PageLayout from "@layouts/default.astro";
import RecentArticles from '@components/RecentArticles'; import RecentArticles from "@components/RecentArticles";
import { Splash, SplashHeading, SplashLinks, Link } from '@components/Splash'; import { Splash, SplashHeading, SplashLinks, Link } from "@components/Splash";
const data = await Astro.glob<ArticleType>('../../content/articles/**/*.mdx'); const data = await Astro.glob<ArticleType>("../../content/articles/**/*.mdx");
const articles = utils.transformArticles(data); const articles = utils.transformArticles(data);
--- ---

View File

@ -1,4 +1,4 @@
import { MarkdownInstance } from 'astro'; import { MarkdownInstance } from "astro";
export const transformArticles = (articles: MarkdownInstance<ArticleType>[]): ArticleType[] => export const transformArticles = (articles: MarkdownInstance<ArticleType>[]): ArticleType[] =>
articles.map((article) => ({ articles.map((article) => ({

View File

@ -1,6 +1,6 @@
export const DateFormatter = new Intl.DateTimeFormat(undefined, { export const DateFormatter = new Intl.DateTimeFormat(undefined, {
timeZone: 'UTC', timeZone: "UTC",
month: 'short', month: "short",
year: 'numeric', year: "numeric",
day: 'numeric', day: "numeric",
}); });

View File

@ -2,14 +2,14 @@ export const extendHistoryState = (state: object, url?: URL | string | undefined
const { history } = window; const { history } = window;
const updated = Object.assign({}, history.state, state); const updated = Object.assign({}, history.state, state);
history.replaceState(updated, '', url); history.replaceState(updated, "", url);
}; };
export const deleteParamFromUrl = (param: string): string => { export const deleteParamFromUrl = (param: string): string => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
params.delete(param); params.delete(param);
const url = !params.toString() ? window.location.pathname : '?' + params.toString(); const url = !params.toString() ? window.location.pathname : "?" + params.toString();
return url; return url;
}; };

View File

@ -1,3 +1,3 @@
export * from './articles'; export * from "./articles";
export * from './date'; export * from "./date";
export * from './history'; export * from "./history";

View File

@ -5,7 +5,7 @@
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"types": ["vite/client", "@astrojs/image/client", "mdx"], "types": ["vite/client", "@astrojs/image/client", "@types/mdx"],
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": ".",
"jsx": "preserve", "jsx": "preserve",