mirror of
https://github.com/sergeyyarkov/yarkov.tech.git
synced 2026-10-11 10:29:21 +03:00
feat: setup i18n for static pages
This commit is contained in:
parent
dac7b1c155
commit
8552126506
@ -17,6 +17,7 @@ const createAnchorLabel = (heading) => {
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: "https://yarkov.tech",
|
site: "https://yarkov.tech",
|
||||||
integrations: [solid(), image(), mdx()],
|
integrations: [solid(), image(), mdx()],
|
||||||
|
trailingSlash: "always",
|
||||||
markdown: {
|
markdown: {
|
||||||
syntaxHighlight: "prism",
|
syntaxHighlight: "prism",
|
||||||
remarkPlugins: ["remark-gfm", "remark-smartypants"],
|
remarkPlugins: ["remark-gfm", "remark-smartypants"],
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
import type { Component } from "solid-js";
|
import type { Component } from "solid-js";
|
||||||
import { DateFormatter } from "@utils/date";
|
import { createDateFormatter } from "@utils/date";
|
||||||
import "./ArticleItem.scss";
|
import "./ArticleItem.scss";
|
||||||
|
|
||||||
export type ArticleItemProps = {
|
export type ArticleItemProps = {
|
||||||
data: ArticleType;
|
data: ArticleType;
|
||||||
|
lang: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ArticleItem: Component<ArticleItemProps> = (props) => {
|
export const ArticleItem: Component<ArticleItemProps> = (props) => {
|
||||||
|
// eslint-disable-next-line solid/reactivity
|
||||||
|
const DateFormatter = createDateFormatter(props.lang);
|
||||||
return (
|
return (
|
||||||
<article class="article-item">
|
<article class="article-item">
|
||||||
<a href={`/blog/${new Date(props.data.published_at).toLocaleDateString("en-CA")}/${props.data.slug}`}>
|
<a href={`/blog/${new Date(props.data.published_at).toLocaleDateString("en-CA")}/${props.data.slug}/`}>
|
||||||
<h3>{props.data.title}</h3>
|
<h3>{props.data.title}</h3>
|
||||||
<p>
|
<p>
|
||||||
<time datetime={props.data.published_at.toString()}>{DateFormatter.format(new Date(props.data.published_at))}</time>
|
<time datetime={props.data.published_at.toString()}>{DateFormatter.format(new Date(props.data.published_at))}</time>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
import { DateFormatter } from "@root/utils";
|
import { createDateFormatter, getLanguageFromURL } from "@root/utils";
|
||||||
import { Image } from "@astrojs/image/components";
|
import { Image } from "@astrojs/image/components";
|
||||||
import "./ArticlePost.scss";
|
import "./ArticlePost.scss";
|
||||||
|
|
||||||
@ -9,6 +9,10 @@ export type Props = {
|
|||||||
|
|
||||||
const { data } = Astro.props as Props;
|
const { data } = Astro.props as Props;
|
||||||
const { title, tags, author, published_at } = data;
|
const { title, tags, author, published_at } = data;
|
||||||
|
// const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
|
// console.log(lang);
|
||||||
|
|
||||||
|
const DateFormatter = createDateFormatter("ru");
|
||||||
---
|
---
|
||||||
|
|
||||||
<article class="article">
|
<article class="article">
|
||||||
@ -18,7 +22,7 @@ const { title, tags, author, published_at } = data;
|
|||||||
{
|
{
|
||||||
tags.map((tag) => (
|
tags.map((tag) => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/articles?tags=${tag.toLocaleLowerCase()}`}>{`${tag}${tags.at(-1) === tag ? "" : ","}`}</a>
|
<a href={`/articles/?tags=${tag.toLocaleLowerCase()}`}>{`${tag}${tags.at(-1) === tag ? "" : ","}`}</a>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,12 +8,18 @@ import SearchInput from "@components/SearchInput";
|
|||||||
import TagsList from "@components/TagsList";
|
import TagsList from "@components/TagsList";
|
||||||
import "./ArticlesList.scss";
|
import "./ArticlesList.scss";
|
||||||
|
|
||||||
|
type UiStringsType = {
|
||||||
|
"input.search": string;
|
||||||
|
"articles.empty": string;
|
||||||
|
};
|
||||||
|
|
||||||
type ArticlesListProps = {
|
type ArticlesListProps = {
|
||||||
data: {
|
data: {
|
||||||
articles: ArticleBlockType;
|
articles: ArticleBlockType;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
emptyPlaceholder?: string | undefined;
|
lang: string;
|
||||||
|
i18n: UiStringsType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ArticlesList: Component<ArticlesListProps> = (props) => {
|
const ArticlesList: Component<ArticlesListProps> = (props) => {
|
||||||
@ -64,12 +70,12 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchInput count={getLength()} placeholder="поиск по названию или описанию..." />
|
<SearchInput count={getLength()} placeholder={props.i18n["input.search"]} />
|
||||||
<TagsList data={props.data.tags} />
|
<TagsList data={props.data.tags} />
|
||||||
<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.i18n["articles.empty"]}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<For
|
<For
|
||||||
@ -78,7 +84,7 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div class="articles-list__wrapper">
|
<div class="articles-list__wrapper">
|
||||||
<h2>{year}</h2>
|
<h2>{year}</h2>
|
||||||
<For each={sortArtcilesByDate(articles()[year])} children={(article) => <ArticleItem data={article} />} />
|
<For each={sortArtcilesByDate(articles()[year])} children={(article) => <ArticleItem lang={props.lang} data={article} />} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
---
|
---
|
||||||
import { SITE_METADATA } from "@root/config";
|
import { SITE_METADATA } from "@root/config";
|
||||||
|
import { useTranslation } from "@i18n/utils";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
title?: string | undefined;
|
title?: string | undefined;
|
||||||
description?: string | undefined;
|
description?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const t = useTranslation(Astro);
|
||||||
const { title = SITE_METADATA.title, description } = Astro.props as Props;
|
const { title = SITE_METADATA.title, description } = Astro.props as Props;
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -18,7 +20,7 @@ const { title = SITE_METADATA.title, description } = Astro.props as Props;
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta name="title" content={title} />
|
<meta name="title" content={title} />
|
||||||
<meta name="description" content={description || SITE_METADATA.description} />
|
<meta name="description" content={description || t("site.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>
|
<script is:inline src="/assets/js/detect-theme.js"></script>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@ -3,22 +3,35 @@ import type { Component, JSX } from "solid-js";
|
|||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { useForm } from "@hooks/index";
|
import { useForm } from "@hooks/index";
|
||||||
import { encode } from "@root/utils/url";
|
import { encode } from "@utils/url";
|
||||||
import Button from "@components/Button";
|
import Button from "@components/Button";
|
||||||
import { validateEmail } from "./validations";
|
import validations from "./validations";
|
||||||
import Submitted from "./Submitted";
|
import Submitted from "./Submitted";
|
||||||
import Error from "./Error";
|
import Error from "./Error";
|
||||||
import "./ContactForm.scss";
|
import "./ContactForm.scss";
|
||||||
|
|
||||||
|
type UiStringsType = {
|
||||||
|
"form.contactSuccess": string;
|
||||||
|
"form.contactError": string;
|
||||||
|
"input.nameField": string;
|
||||||
|
"input.subjectField": string;
|
||||||
|
"input.messageField": string;
|
||||||
|
"btn.send": string;
|
||||||
|
"btn.sending": string;
|
||||||
|
"error.invalidEmail": string;
|
||||||
|
};
|
||||||
|
|
||||||
const ErrorMessage: Component<{ message: string }> = (props) => <span class="error-message">{props.message}</span>;
|
const ErrorMessage: Component<{ message: string }> = (props) => <span class="error-message">{props.message}</span>;
|
||||||
|
|
||||||
const ContactForm: Component = () => {
|
const ContactForm: Component<{ i18n: UiStringsType }> = (props) => {
|
||||||
const { formSubmit, validate, errors } = useForm({ errorClass: "error-input" });
|
const { formSubmit, validate, errors } = useForm({ errorClass: "error-input" });
|
||||||
const [isSubmitted, setIsSubmitted] = createSignal<boolean>(false);
|
const [isSubmitted, setIsSubmitted] = createSignal<boolean>(false);
|
||||||
const [isLoading, setIsLoading] = createSignal<boolean>(false);
|
const [isLoading, setIsLoading] = createSignal<boolean>(false);
|
||||||
const [isError, setIsError] = createSignal<boolean>(false);
|
const [isError, setIsError] = createSignal<boolean>(false);
|
||||||
const [state, setState] = createStore<ContactFieldsType>({ name: "", email: "", subject: "", message: "", "bot-field": "" });
|
const [state, setState] = createStore<ContactFieldsType>({ name: "", email: "", subject: "", message: "", "bot-field": "" });
|
||||||
|
|
||||||
|
const { validateEmail } = validations;
|
||||||
|
|
||||||
const reset = (form: HTMLFormElement) => {
|
const reset = (form: HTMLFormElement) => {
|
||||||
for (const key in state) {
|
for (const key in state) {
|
||||||
form.elements[key].value = "";
|
form.elements[key].value = "";
|
||||||
@ -63,13 +76,22 @@ const ContactForm: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div>
|
<div>
|
||||||
<input onInput={onChange} use:validate name="name" placeholder="Ваше имя" type="text" maxLength="20" autocomplete="off" required />
|
<input
|
||||||
|
onInput={onChange}
|
||||||
|
use:validate
|
||||||
|
name="name"
|
||||||
|
placeholder={props.i18n["input.nameField"]}
|
||||||
|
type="text"
|
||||||
|
maxLength="20"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
{errors.name && <ErrorMessage message={errors.name} />}
|
{errors.name && <ErrorMessage message={errors.name} />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
onInput={onChange}
|
onInput={onChange}
|
||||||
use:validate={[validateEmail]}
|
use:validate={[validateEmail(props.i18n["error.invalidEmail"])]}
|
||||||
name="email"
|
name="email"
|
||||||
placeholder="example@email.com"
|
placeholder="example@email.com"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@ -84,7 +106,7 @@ const ContactForm: Component = () => {
|
|||||||
onInput={onChange}
|
onInput={onChange}
|
||||||
use:validate
|
use:validate
|
||||||
name="subject"
|
name="subject"
|
||||||
placeholder="Тема сообщения"
|
placeholder={props.i18n["input.subjectField"]}
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
@ -94,7 +116,7 @@ const ContactForm: Component = () => {
|
|||||||
onInput={onChange}
|
onInput={onChange}
|
||||||
use:validate
|
use:validate
|
||||||
name="message"
|
name="message"
|
||||||
placeholder="Ваше сообщение..."
|
placeholder={props.i18n["input.messageField"]}
|
||||||
id="message"
|
id="message"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
cols={30}
|
cols={30}
|
||||||
@ -104,12 +126,12 @@ const ContactForm: Component = () => {
|
|||||||
{errors.message && <ErrorMessage message={errors.message} />}
|
{errors.message && <ErrorMessage message={errors.message} />}
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
{!isError() && !isSubmitted() && (
|
{!isError() && !isSubmitted() && (
|
||||||
<Button isLoading={isLoading()} loadingText="Отправка..." type="submit">
|
<Button isLoading={isLoading()} loadingText={props.i18n["btn.sending"]} type="submit">
|
||||||
Отправить
|
{props.i18n["btn.send"]}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!isError() && isSubmitted() && <Submitted />}
|
{!isError() && isSubmitted() && <Submitted text={props.i18n["form.contactSuccess"]} />}
|
||||||
{isError() && isSubmitted() && <Error />}
|
{isError() && isSubmitted() && <Error text={props.i18n["form.contactError"]} />}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { Component } from "solid-js";
|
import type { Component } from "solid-js";
|
||||||
|
|
||||||
const Submitted: Component = () => {
|
const Submitted: Component<{ text: string }> = (props) => {
|
||||||
return <div class="form-result">❌ При отправке произошла ошибка. Повторите запрос позже.</div>;
|
return <div class="form-result">❌ {props.text}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Submitted;
|
export default Submitted;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { Component } from "solid-js";
|
import type { Component } from "solid-js";
|
||||||
|
|
||||||
const Submitted: Component = () => {
|
const Submitted: Component<{ text: string }> = (props) => {
|
||||||
return <div class="form-result">✔️ Сообщение было успешно отправлено!</div>;
|
return <div class="form-result">✔️ {props.text}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Submitted;
|
export default Submitted;
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
import { isEmail } from "@utils/string";
|
import { isEmail } from "@utils/string";
|
||||||
|
|
||||||
export const validateEmail = (field: HTMLInputElement) => (isEmail(field.value) ? false : "Заполните поле в формате e-mail.");
|
export default {
|
||||||
|
validateEmail: (error: string) => {
|
||||||
|
return (field: HTMLInputElement) => (isEmail(field.value) ? false : error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
import { Icon } from "astro-icon";
|
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 { removeSlashes } from "@utils/string";
|
import UIString from "@components/UIString";
|
||||||
import NavToggle from "./NavToggle";
|
import NavToggle from "./NavToggle";
|
||||||
import "./Header.scss";
|
import "./Header.scss";
|
||||||
|
import TranslatedLink from "@components/TranslatedLink";
|
||||||
|
|
||||||
const { name, nav, contacts } = SITE_METADATA;
|
const { name, nav, contacts } = SITE_METADATA;
|
||||||
const { pathname } = Astro.url;
|
const { pathname } = Astro.url;
|
||||||
@ -13,23 +14,25 @@ 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>
|
<TranslatedLink to={"/"}>
|
||||||
|
{name}
|
||||||
|
</TranslatedLink>
|
||||||
</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}>
|
<TranslatedLink class={pathname.includes(link.url) && "active"} to={link.url}>
|
||||||
{link.name}
|
<UIString key={link.key} />
|
||||||
</a>
|
</TranslatedLink>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="header__nav-contacts">
|
<div class="header__nav-contacts">
|
||||||
<a href={"mailto:" + contacts.email}>
|
<a href={"mailto:" + contacts.email}>
|
||||||
Почта
|
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}>
|
||||||
@ -37,11 +40,14 @@ const { pathname } = Astro.url;
|
|||||||
<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}>
|
||||||
Телеграм
|
Telegram
|
||||||
<Icon name="telegram" width="18" height="18" />
|
<Icon name="telegram" width="18" height="18" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<ThemeSwitcher client:visible />
|
<div class="flex header__nav-settings">
|
||||||
|
<Icon name="translate" width="21" height="21" />
|
||||||
|
<ThemeSwitcher client:visible />
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<NavToggle client:visible />
|
<NavToggle client:visible />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
background-color: var(--background-header-color);
|
background-color: var(--background-header-color);
|
||||||
padding-top: 15px;
|
padding-top: 13.5px;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 13.5px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
& .container {
|
& .container {
|
||||||
@ -53,7 +53,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: 10px;
|
column-gap: 10px;
|
||||||
margin-right: 40px;
|
margin-right: 30px;
|
||||||
& a {
|
& a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
@ -62,6 +62,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&-settings {
|
||||||
|
column-gap: 13.5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
---
|
---
|
||||||
import { Icon } from 'astro-icon';
|
import { FALLBACK_LANGUAGE } from "@root/config";
|
||||||
import './ProjectItem.scss';
|
import { getLanguageFromURL } from "@root/utils";
|
||||||
|
import { Icon } from "astro-icon";
|
||||||
|
import UIString from "../UIString";
|
||||||
|
import "./ProjectItem.scss";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
data: ProjectType;
|
data: ProjectType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = Astro.props as Props;
|
const { data } = Astro.props as Props;
|
||||||
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
const { icon, name, year, description, articleUrl, sourceUrl, demoUrl } = data;
|
const { icon, name, year, description, articleUrl, sourceUrl, demoUrl } = data;
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -19,25 +23,31 @@ const { icon, name, year, description, articleUrl, sourceUrl, demoUrl } = data;
|
|||||||
<span>{year}</span>
|
<span>{year}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-item__description">
|
<div class="project-item__description">
|
||||||
<p>{description}</p>
|
<p>{description[lang] || description[FALLBACK_LANGUAGE]}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-item__links">
|
<div class="project-item__links">
|
||||||
{articleUrl && (
|
{
|
||||||
<a class="btn-link" href={articleUrl} target="_blank">
|
articleUrl && (
|
||||||
Статья
|
<a class="btn-link" href={articleUrl} target="_blank">
|
||||||
</a>
|
<UIString key={"btn.article"} />
|
||||||
)}
|
</a>
|
||||||
{sourceUrl && (
|
)
|
||||||
<a class="btn-link" href={sourceUrl} target="_blank">
|
}
|
||||||
Исходник
|
{
|
||||||
<Icon name="link-out" width="15" height="15" />
|
sourceUrl && (
|
||||||
</a>
|
<a class="btn-link" href={sourceUrl} target="_blank">
|
||||||
)}
|
<UIString key={"btn.sourceCode"} />
|
||||||
{demoUrl && (
|
<Icon name="link-out" width="15" height="15" />
|
||||||
<a class="btn-link" href={demoUrl} target="_blank">
|
</a>
|
||||||
Демо
|
)
|
||||||
<Icon name="link-out" width="15" height="15" />
|
}
|
||||||
</a>
|
{
|
||||||
)}
|
demoUrl && (
|
||||||
|
<a class="btn-link" href={demoUrl} target="_blank">
|
||||||
|
<UIString key={"btn.demo"} />
|
||||||
|
<Icon name="link-out" width="15" height="15" />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
|
---
|
||||||
|
import UIString from "@components/UIString";
|
||||||
|
---
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>Пока что здесь ничего нет...</p>
|
<p><UIString key={"recentArticles.empty"} /></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
---
|
---
|
||||||
import { ArticleItem } from "@components/ArticleItem";
|
import { ArticleItem } from "@components/ArticleItem";
|
||||||
|
import UIString from "@components/UIString";
|
||||||
|
import { getLanguageFromURL } from "@utils/url";
|
||||||
import { sortArtcilesByDate } from "@utils/articles";
|
import { sortArtcilesByDate } from "@utils/articles";
|
||||||
import EmptyList from "./EmptyList.astro";
|
import EmptyList from "./EmptyList.astro";
|
||||||
|
import TranslatedLink from "@components/TranslatedLink";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
articles: ArticleType[];
|
articles: ArticleType[];
|
||||||
@ -10,18 +13,21 @@ export interface Props {
|
|||||||
|
|
||||||
const { articles, length = 5 } = Astro.props as Props;
|
const { articles, length = 5 } = Astro.props as Props;
|
||||||
|
|
||||||
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
const recent = sortArtcilesByDate(articles);
|
const recent = sortArtcilesByDate(articles);
|
||||||
const isEmpty = recent.length === 0;
|
const isEmpty = recent.length === 0;
|
||||||
---
|
---
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div>
|
<div>
|
||||||
<h2>недавние публикации</h2>
|
<h2><UIString key={"recentArticles.heading"} /></h2>
|
||||||
<a href="/articles">смотреть все</a>
|
<TranslatedLink to="/articles/">
|
||||||
|
<UIString key={"recentArticles.all"} />
|
||||||
|
</TranslatedLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{!isEmpty ? recent.slice(0, length).map((article) => <ArticleItem data={article} />) : <EmptyList />}
|
{!isEmpty ? recent.slice(0, length).map((article) => <ArticleItem lang={lang} data={article} />) : <EmptyList />}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,33 @@
|
|||||||
---
|
---
|
||||||
import { Icon } from 'astro-icon';
|
import { FALLBACK_LANGUAGE } from "@root/config";
|
||||||
import './SkillsBlock.scss';
|
import { getLanguageFromURL } from "@root/utils";
|
||||||
|
import { Icon } from "astro-icon";
|
||||||
|
import "./SkillsBlock.scss";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
data: SkillsType;
|
data: SkillsType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
const {
|
const {
|
||||||
data: { title, list },
|
data: { title, list },
|
||||||
} = Astro.props as Props;
|
} = Astro.props as Props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class='skills-block'>
|
<div class="skills-block">
|
||||||
<div>
|
<div>
|
||||||
<h3>{title}</h3>
|
<h3>{title[lang] || title[FALLBACK_LANGUAGE]}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
{list.map((entry) => (
|
{
|
||||||
<li>
|
list.map((entry) => (
|
||||||
{entry.icon && <Icon name={entry.icon} width="20" height="20" />}
|
<li>
|
||||||
{entry.name}
|
{entry.icon && <Icon name={entry.icon} width="20" height="20" />}
|
||||||
</li>
|
{entry.name}
|
||||||
))}
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { Icon } from "astro-icon";
|
import { Icon } from "astro-icon";
|
||||||
|
import TranslatedLink from "@components/TranslatedLink";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
icon: string;
|
icon: string;
|
||||||
@ -11,9 +12,9 @@ const { icon, href } = Astro.props as Props;
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Icon name={icon} width="18" height="18" />
|
<Icon name={icon} width="18" height="18" />
|
||||||
<a href={href}>
|
<TranslatedLink to={href}>
|
||||||
<slot />
|
<slot />
|
||||||
</a>
|
</TranslatedLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
14
src/components/TranslatedLink/TranslatedLink.astro
Normal file
14
src/components/TranslatedLink/TranslatedLink.astro
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
import { FALLBACK_LANGUAGE } from "@root/config";
|
||||||
|
import { getLanguageFromURL } from "@root/utils";
|
||||||
|
|
||||||
|
export interface Props extends astroHTML.JSX.AnchorHTMLAttributes {
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
|
const { to, ...attrs } = Astro.props as Props;
|
||||||
|
const href = lang === FALLBACK_LANGUAGE ? to : `/${lang}${to}`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a href={href} {...attrs}><slot /></a>
|
||||||
1
src/components/TranslatedLink/index.ts
Normal file
1
src/components/TranslatedLink/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./TranslatedLink.astro";
|
||||||
9
src/components/UIString/UIString.astro
Normal file
9
src/components/UIString/UIString.astro
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
import { useTranslation } from "@i18n/utils";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
key: UIDictionaryKeys;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
{useTranslation(Astro)(Astro.props.key)}
|
||||||
1
src/components/UIString/index.ts
Normal file
1
src/components/UIString/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./UIString.astro";
|
||||||
@ -1,9 +1,9 @@
|
|||||||
|
export const FALLBACK_LANGUAGE = "ru";
|
||||||
|
|
||||||
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:
|
|
||||||
"Здесь я публикую статьи и туториалы на различные IT темы, а также рассказываю о своих проектах которые я сделал за последние несколько лет.",
|
|
||||||
themeColor: "#1e2327",
|
themeColor: "#1e2327",
|
||||||
contacts: {
|
contacts: {
|
||||||
email: "serzh.yarkov@gmail.com",
|
email: "serzh.yarkov@gmail.com",
|
||||||
@ -11,17 +11,20 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
telegramUrl: "https://t.me/sergeyyarkov",
|
telegramUrl: "https://t.me/sergeyyarkov",
|
||||||
},
|
},
|
||||||
nav: [
|
nav: [
|
||||||
{ name: "Статьи", url: "/articles" },
|
{ key: "navigation.articles", url: "/articles/" },
|
||||||
{ name: "Проекты", url: "/projects" },
|
{ key: "navigation.projects", url: "/projects/" },
|
||||||
{ name: "Обо мне", url: "/about" },
|
{ key: "navigation.about", url: "/about/" },
|
||||||
{ name: "Контакты", url: "/contacts" },
|
{ key: "navigation.contacts", url: "/contacts/" },
|
||||||
],
|
],
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
icon: "book",
|
icon: "book",
|
||||||
name: "yarkov.tech",
|
name: "yarkov.tech",
|
||||||
year: "2022",
|
year: "2022",
|
||||||
description: "Мой персональный веб-сайт где находятся информация обо мне и список различных статей, туториалов...",
|
description: {
|
||||||
|
ru: "Мой персональный веб-сайт где находятся информация обо мне и список различных статей, туториалов...",
|
||||||
|
en: "My personal website where you can find information about me and a list of various articles, tutorials...",
|
||||||
|
},
|
||||||
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,
|
||||||
@ -30,7 +33,10 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
icon: "triangle_filled",
|
icon: "triangle_filled",
|
||||||
name: "devthread",
|
name: "devthread",
|
||||||
year: "2021",
|
year: "2021",
|
||||||
description: "Статический веб-сайт для публикации различных статей, разработанный при помощи Gatsby.js и NetlifyCMS",
|
description: {
|
||||||
|
ru: "Статический веб-сайт для публикации различных статей, разработанный при помощи Gatsby.js и NetlifyCMS",
|
||||||
|
en: "Static website for publishing various articles, developed with Gatsby.js and 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,
|
||||||
@ -39,7 +45,10 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
icon: "round-school",
|
icon: "round-school",
|
||||||
name: "educt",
|
name: "educt",
|
||||||
year: "2022",
|
year: "2022",
|
||||||
description: "Система управления обучением (LMS) для публикации ваших курсов, лекций, уроков",
|
description: {
|
||||||
|
ru: "Система управления обучением (LMS) для публикации ваших курсов, лекций, уроков",
|
||||||
|
en: "Learning Management System (LMS) to publish your courses, lectures, lessons",
|
||||||
|
},
|
||||||
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,
|
||||||
@ -48,7 +57,10 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
icon: "round-shop-two",
|
icon: "round-shop-two",
|
||||||
name: "digital-store",
|
name: "digital-store",
|
||||||
year: "2019",
|
year: "2019",
|
||||||
description: "Настраиваемый интернет-магазин с панелью управления для продажи цифровых товаров",
|
description: {
|
||||||
|
ru: "Настраиваемый интернет-магазин с панелью управления для продажи цифровых товаров",
|
||||||
|
en: "Customizable online store with control panel for selling digital goods",
|
||||||
|
},
|
||||||
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,
|
||||||
@ -57,7 +69,10 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
icon: "chat",
|
icon: "chat",
|
||||||
name: "react-chat-app",
|
name: "react-chat-app",
|
||||||
year: "2021",
|
year: "2021",
|
||||||
description: "Клиент-серверное веб приложение чата разработанное при помощи Node.js и Socket.io",
|
description: {
|
||||||
|
ru: "Клиент-серверное веб приложение чата разработанное при помощи Node.js и Socket.io",
|
||||||
|
en: "Client-server web chat application developed with Node.js and 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,
|
||||||
@ -66,7 +81,10 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
icon: "nodejs",
|
icon: "nodejs",
|
||||||
name: "node-docker-example",
|
name: "node-docker-example",
|
||||||
year: "2022",
|
year: "2022",
|
||||||
description: "Пример использования Docker для развертывания Node.js приложения",
|
description: {
|
||||||
|
ru: "Пример использования Docker для развертывания Node.js приложения",
|
||||||
|
en: "An example of using Docker to deploy a Node.js application",
|
||||||
|
},
|
||||||
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,
|
||||||
@ -75,7 +93,10 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
icon: "chip",
|
icon: "chip",
|
||||||
name: "memory-led-game",
|
name: "memory-led-game",
|
||||||
year: "2022",
|
year: "2022",
|
||||||
description: "Аналог популярной игры “Саймон говорит” на AVR микроконтроллере",
|
description: {
|
||||||
|
ru: 'Аналог популярной игры "Саймон говорит" на AVR микроконтроллере',
|
||||||
|
en: 'Analogue of the popular game "Simon says" on the AVR microcontroller',
|
||||||
|
},
|
||||||
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,
|
||||||
@ -83,7 +104,10 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
],
|
],
|
||||||
skills: [
|
skills: [
|
||||||
{
|
{
|
||||||
title: "Программирование",
|
title: {
|
||||||
|
ru: "Программирование",
|
||||||
|
en: "Programming",
|
||||||
|
},
|
||||||
list: [
|
list: [
|
||||||
{ icon: "nodejs", name: "Javascript/Typescript" },
|
{ icon: "nodejs", name: "Javascript/Typescript" },
|
||||||
{ icon: "language-c", name: "C Language" },
|
{ icon: "language-c", name: "C Language" },
|
||||||
@ -93,7 +117,10 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Фреймворки",
|
title: {
|
||||||
|
ru: "Фреймворки",
|
||||||
|
en: "Frameworks",
|
||||||
|
},
|
||||||
list: [
|
list: [
|
||||||
{ icon: "adonisjs", name: "Adonis.js" },
|
{ icon: "adonisjs", name: "Adonis.js" },
|
||||||
{ icon: "expressjs", name: "Express.js" },
|
{ icon: "expressjs", name: "Express.js" },
|
||||||
@ -104,7 +131,10 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Инструменты",
|
title: {
|
||||||
|
ru: "Инструменты",
|
||||||
|
en: "Tools",
|
||||||
|
},
|
||||||
list: [
|
list: [
|
||||||
{ icon: "nodejs", name: "Node.js" },
|
{ icon: "nodejs", name: "Node.js" },
|
||||||
{ icon: "docker", name: "Docker" },
|
{ icon: "docker", name: "Docker" },
|
||||||
@ -115,7 +145,10 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Базы данных",
|
title: {
|
||||||
|
ru: "Базы данных",
|
||||||
|
en: "Databases",
|
||||||
|
},
|
||||||
list: [
|
list: [
|
||||||
{ icon: "postgresql", name: "PostgreSQL" },
|
{ icon: "postgresql", name: "PostgreSQL" },
|
||||||
{ icon: "mongodb", name: "MongoDB" },
|
{ icon: "mongodb", name: "MongoDB" },
|
||||||
@ -123,11 +156,11 @@ export const SITE_METADATA: SiteMetadataType = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Языки",
|
title: {
|
||||||
list: [
|
ru: "Языки",
|
||||||
{ icon: undefined, name: "Русский (родной)" },
|
en: "Languages",
|
||||||
{ icon: undefined, name: "Английский (B1)" },
|
},
|
||||||
],
|
list: [{ name: "Russian (native)" }, { name: "English (B1)" }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,18 @@
|
|||||||
export default {
|
export default {
|
||||||
"site.title": "yarkov.tech",
|
"site.title": "yarkov.tech",
|
||||||
|
"site.description":
|
||||||
|
"Here I publish articles and tutorials on various IT topics, as well as talk about my projects that I have done over the past few years.",
|
||||||
|
"articles.empty": "No articles found...",
|
||||||
|
"recentArticles.empty": "There is nothing here yet...",
|
||||||
|
/* Page titles */
|
||||||
|
"pageTitle.about": "About me",
|
||||||
|
"pageTitle.articles": "Articles",
|
||||||
|
"pageTitle.projects": "Projects",
|
||||||
|
"pageTitle.contacts": "Contacts",
|
||||||
|
/* Page descriptions */
|
||||||
|
"pageDescription.articles": "Technical articles, tutorials and various reference materials related to IT topics can be found here.",
|
||||||
|
"pageDescription.projects": "A few of my main projects that have been developed and made available to the public.",
|
||||||
|
"pageDescription.contacts": "Use the feedback form or write to me on social networks.",
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
"navigation.articles": "Articles",
|
"navigation.articles": "Articles",
|
||||||
"navigation.projects": "Projects",
|
"navigation.projects": "Projects",
|
||||||
@ -10,7 +23,7 @@ export default {
|
|||||||
"splash.subheadline": "Fullstack web-developer & freelancer",
|
"splash.subheadline": "Fullstack web-developer & freelancer",
|
||||||
"splash.description":
|
"splash.description":
|
||||||
"I develop websites and web applications of varying complexity, as well as publish various articles and tutorials on various IT topics.",
|
"I develop websites and web applications of varying complexity, as well as publish various articles and tutorials on various IT topics.",
|
||||||
"splash.about": "about",
|
"splash.about": "about me",
|
||||||
"splash.contact": "write me",
|
"splash.contact": "write me",
|
||||||
/* Recent articles */
|
/* Recent articles */
|
||||||
"recentArticles.heading": "recent publications",
|
"recentArticles.heading": "recent publications",
|
||||||
@ -18,4 +31,20 @@ export default {
|
|||||||
/* 404 page */
|
/* 404 page */
|
||||||
"404.heading": "404",
|
"404.heading": "404",
|
||||||
"404.description": "The page you are trying to find does not exist.",
|
"404.description": "The page you are trying to find does not exist.",
|
||||||
|
/* Buttons */
|
||||||
|
"btn.send": "Submit",
|
||||||
|
"btn.sending": "Submitting...",
|
||||||
|
"btn.sourceCode": "Source",
|
||||||
|
"btn.demo": "Demo",
|
||||||
|
"btn.article": "Article",
|
||||||
|
/* Inputs */
|
||||||
|
"input.nameField": "Your name",
|
||||||
|
"input.subjectField": "Message subject",
|
||||||
|
"input.messageField": "Your message...",
|
||||||
|
"input.search": "search by title or description...",
|
||||||
|
/* Forms */
|
||||||
|
"form.contactSuccess": "The message was submitted successfully!",
|
||||||
|
"form.contactError": "An error occurred while sending. Please try again later.",
|
||||||
|
/* Error messages */
|
||||||
|
"error.invalidEmail": "Fill in the field in the e-mail format.",
|
||||||
};
|
};
|
||||||
|
|||||||
4
src/i18n/languages.ts
Normal file
4
src/i18n/languages.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default {
|
||||||
|
ru: "Русский",
|
||||||
|
en: "English",
|
||||||
|
};
|
||||||
@ -1,5 +1,18 @@
|
|||||||
export default {
|
export default {
|
||||||
"site.title": "yarkov.tech",
|
"site.title": "yarkov.tech",
|
||||||
|
"site.description":
|
||||||
|
"Здесь я публикую статьи и туториалы на различные IT темы, а также рассказываю о своих проектах которые я сделал за последние несколько лет.",
|
||||||
|
"articles.empty": "Не найдено ни одной статьи...",
|
||||||
|
"recentArticles.empty": "Пока что здесь ничего нет...",
|
||||||
|
/* Page titles */
|
||||||
|
"pageTitle.about": "Обо мне",
|
||||||
|
"pageTitle.articles": "Статьи",
|
||||||
|
"pageTitle.projects": "Проекты",
|
||||||
|
"pageTitle.contacts": "Контакты",
|
||||||
|
/* Page descriptions */
|
||||||
|
"pageDescription.articles": "Технические статьи, туториалы и различные справочные материалы которые связаны с IT тематикой находятся здесь.",
|
||||||
|
"pageDescription.projects": "Несколько моих основных проектов которые были разработаны и выложены в открытый доступ.",
|
||||||
|
"pageDescription.contacts": "Воспользуйтесь формой обратной связи или напишите мне в социальные сети.",
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
"navigation.articles": "Статьи",
|
"navigation.articles": "Статьи",
|
||||||
"navigation.projects": "Проекты",
|
"navigation.projects": "Проекты",
|
||||||
@ -18,4 +31,20 @@ export default {
|
|||||||
/* 404 page */
|
/* 404 page */
|
||||||
"404.heading": "404",
|
"404.heading": "404",
|
||||||
"404.description": "Страницу которую вы пытаетесь найти не существует.",
|
"404.description": "Страницу которую вы пытаетесь найти не существует.",
|
||||||
|
/* Buttons */
|
||||||
|
"btn.send": "Отправить",
|
||||||
|
"btn.sending": "Отправка...",
|
||||||
|
"btn.sourceCode": "Исходник",
|
||||||
|
"btn.demo": "Демо",
|
||||||
|
"btn.article": "Статья",
|
||||||
|
/* Inputs */
|
||||||
|
"input.nameField": "Ваше имя",
|
||||||
|
"input.subjectField": "Тема сообщения",
|
||||||
|
"input.messageField": "Ваше сообщение...",
|
||||||
|
"input.search": "поиск по названию или описанию...",
|
||||||
|
/* Forms */
|
||||||
|
"form.contactSuccess": "Сообщение было успешно отправлено!",
|
||||||
|
"form.contactError": "При отправке произошла ошибка. Повторите запрос позже.",
|
||||||
|
/* Error messages */
|
||||||
|
"error.invalidEmail": "Заполните поле в формате e-mail.",
|
||||||
};
|
};
|
||||||
|
|||||||
28
src/i18n/utils.ts
Normal file
28
src/i18n/utils.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import type { AstroGlobal } from "astro";
|
||||||
|
import * as utils from "@utils/url";
|
||||||
|
import { FALLBACK_LANGUAGE } from "@root/config";
|
||||||
|
import languages from "./languages";
|
||||||
|
|
||||||
|
export function getLanguageKeys(): Array<string> {
|
||||||
|
return Object.keys(languages);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformExports<T>(modules: Record<string, { default: T }>) {
|
||||||
|
const translations: Record<string, T> = {};
|
||||||
|
for (const [path, module] of Object.entries(modules)) {
|
||||||
|
const [, lang] = path.split("/");
|
||||||
|
translations[lang] = module.default;
|
||||||
|
}
|
||||||
|
return translations;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translations = transformExports<UIDictionaryKeys>(import.meta.glob("./*/ui.ts", { eager: true }));
|
||||||
|
|
||||||
|
export function useTranslation(Astro: Readonly<AstroGlobal>): (key: UIDictionaryKeys) => string | undefined {
|
||||||
|
const lang = utils.getLanguageFromURL(Astro.url.pathname);
|
||||||
|
return function translate(key: UIDictionaryKeys): string | undefined {
|
||||||
|
const string: string | undefined = translations[lang]?.[key] || translations[FALLBACK_LANGUAGE][key];
|
||||||
|
if (string === undefined) console.error(`Cannot find any string for translation key "${key}" on "${Astro.url.pathname}" page.`);
|
||||||
|
return string;
|
||||||
|
};
|
||||||
|
}
|
||||||
3
src/icons/translate.svg
Normal file
3
src/icons/translate.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.2612 13.1862L9.03875 10.99L9.065 10.9637C10.5474 9.31621 11.6549 7.36685 12.3112 5.25H14.875V3.5H8.75V1.75H7V3.5H0.875V5.24125H10.6488C10.0625 6.93 9.135 8.53125 7.875 9.93125C7.06125 9.03 6.3875 8.04125 5.85375 7H4.10375C4.7425 8.42625 5.6175 9.77375 6.71125 10.99L2.2575 15.3825L3.5 16.625L7.875 12.25L10.5962 14.9713L11.2612 13.1862ZM16.1875 8.75H14.4375L10.5 19.25H12.25L13.23 16.625H17.3863L18.375 19.25H20.125L16.1875 8.75ZM13.895 14.875L15.3125 11.0863L16.73 14.875H13.895V14.875Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 620 B |
13
src/pages/[lang]/about.astro
Normal file
13
src/pages/[lang]/about.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
import { getLanguageKeys } from "@i18n/utils";
|
||||||
|
import { FALLBACK_LANGUAGE } from "@root/config";
|
||||||
|
import About from "@templates/About.astro";
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return getLanguageKeys()
|
||||||
|
.filter((lang) => lang !== FALLBACK_LANGUAGE)
|
||||||
|
.map((lang) => ({ params: { lang } }));
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<About />
|
||||||
13
src/pages/[lang]/articles.astro
Normal file
13
src/pages/[lang]/articles.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
import { getLanguageKeys } from "@i18n/utils";
|
||||||
|
import { FALLBACK_LANGUAGE } from "@root/config";
|
||||||
|
import Articles from "@templates/Articles.astro";
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return getLanguageKeys()
|
||||||
|
.filter((lang) => lang !== FALLBACK_LANGUAGE)
|
||||||
|
.map((lang) => ({ params: { lang } }));
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Articles />
|
||||||
13
src/pages/[lang]/contacts.astro
Normal file
13
src/pages/[lang]/contacts.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
import { getLanguageKeys } from "@i18n/utils";
|
||||||
|
import { FALLBACK_LANGUAGE } from "@root/config";
|
||||||
|
import Contacts from "@templates/Contacts.astro";
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return getLanguageKeys()
|
||||||
|
.filter((lang) => lang !== FALLBACK_LANGUAGE)
|
||||||
|
.map((lang) => ({ params: { lang } }));
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Contacts />
|
||||||
13
src/pages/[lang]/index.astro
Normal file
13
src/pages/[lang]/index.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
import { getLanguageKeys } from "@i18n/utils";
|
||||||
|
import { FALLBACK_LANGUAGE } from "@root/config";
|
||||||
|
import Home from "@templates/Home.astro";
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return getLanguageKeys()
|
||||||
|
.filter((lang) => lang !== FALLBACK_LANGUAGE)
|
||||||
|
.map((lang) => ({ params: { lang } }));
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Home />
|
||||||
13
src/pages/[lang]/projects.astro
Normal file
13
src/pages/[lang]/projects.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
import { getLanguageKeys } from "@i18n/utils";
|
||||||
|
import { FALLBACK_LANGUAGE } from "@root/config";
|
||||||
|
import Projects from "@templates/Projects.astro";
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return getLanguageKeys()
|
||||||
|
.filter((lang) => lang !== FALLBACK_LANGUAGE)
|
||||||
|
.map((lang) => ({ params: { lang } }));
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Projects />
|
||||||
@ -1,17 +1,21 @@
|
|||||||
---
|
---
|
||||||
import { SITE_METADATA } from '@root/config';
|
import { SITE_METADATA } from "@root/config";
|
||||||
import PageLayout from '@layouts/default.astro';
|
import PageLayout from "@layouts/default.astro";
|
||||||
import SkillsList from '@components/SkillsList';
|
import SkillsList from "@components/SkillsList";
|
||||||
import SkillsBlock from '@components/SkillsBlock';
|
import SkillsBlock from "@components/SkillsBlock";
|
||||||
|
|
||||||
// TODO: import content based on user ui language
|
// TODO: import content based on user ui language
|
||||||
import { default as Content } from '../../content/about/ru/index.mdx';
|
import ArticleContent from "@components/ArticleContent";
|
||||||
import ArticleContent from '@components/ArticleContent';
|
import { getLanguageFromURL } from "@root/utils";
|
||||||
|
import { useTranslation } from "@i18n/utils";
|
||||||
|
|
||||||
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
|
const t = useTranslation(Astro);
|
||||||
|
const { Content } = await import(`../../content/about/${lang}/index.mdx`);
|
||||||
const { skills } = SITE_METADATA;
|
const { skills } = SITE_METADATA;
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout title="Обо мне">
|
<PageLayout lang={lang} title={t("pageTitle.about")}>
|
||||||
<section>
|
<section>
|
||||||
<ArticleContent>
|
<ArticleContent>
|
||||||
<Content />
|
<Content />
|
||||||
|
|||||||
@ -2,20 +2,25 @@
|
|||||||
import PageLayout from "@layouts/default.astro";
|
import PageLayout from "@layouts/default.astro";
|
||||||
import ArticlesList from "@components/ArticlesList";
|
import ArticlesList from "@components/ArticlesList";
|
||||||
import { transformArticles, formatToArticleBlocks } from "@utils/articles";
|
import { transformArticles, formatToArticleBlocks } from "@utils/articles";
|
||||||
|
import { getLanguageFromURL } from "@root/utils";
|
||||||
|
import { useTranslation } from "@i18n/utils";
|
||||||
|
|
||||||
|
const t = useTranslation(Astro);
|
||||||
const articles = await Astro.glob<ArticleType>("../../content/articles/**/*.mdx");
|
const articles = await Astro.glob<ArticleType>("../../content/articles/**/*.mdx");
|
||||||
const tags = new Set(articles.map((article) => article.frontmatter.tags).flat());
|
const tags = new Set(articles.map((article) => article.frontmatter.tags).flat());
|
||||||
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout
|
<PageLayout lang={lang} title={t("pageTitle.articles")} description={t("pageDescription.articles")}>
|
||||||
title="Статьи"
|
|
||||||
description="Технические статьи, туториалы и различные справочные материалы которые связаны с IT тематикой находятся здесь."
|
|
||||||
>
|
|
||||||
<section>
|
<section>
|
||||||
<ArticlesList
|
<ArticlesList
|
||||||
client:load
|
client:load
|
||||||
|
lang={lang}
|
||||||
|
i18n={{
|
||||||
|
"input.search": t("input.search"),
|
||||||
|
"articles.empty": t("articles.empty"),
|
||||||
|
}}
|
||||||
data={{ articles: formatToArticleBlocks(transformArticles(articles)), tags: [...tags] }}
|
data={{ articles: formatToArticleBlocks(transformArticles(articles)), tags: [...tags] }}
|
||||||
emptyPlaceholder="Не найдено ни одной статьи..."
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@ -2,11 +2,28 @@
|
|||||||
import PageLayout from "@layouts/default.astro";
|
import PageLayout from "@layouts/default.astro";
|
||||||
import ContactForm from "@components/ContactForm";
|
import ContactForm from "@components/ContactForm";
|
||||||
import ContactsList from "@components/ContactsList";
|
import ContactsList from "@components/ContactsList";
|
||||||
|
import { getLanguageFromURL } from "@root/utils";
|
||||||
|
import { useTranslation } from "@i18n/utils";
|
||||||
|
|
||||||
|
const t = useTranslation(Astro);
|
||||||
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout title="Контакты" description="Воспользуйтесь формой обратной связи или напишите мне в социальные сети.">
|
<PageLayout lang={lang} title={t("pageTitle.contacts")} description={t("pageDescription.contacts")}>
|
||||||
<section>
|
<section>
|
||||||
<ContactForm client:idle />
|
<ContactForm
|
||||||
|
i18n={{
|
||||||
|
"input.nameField": t("input.nameField"),
|
||||||
|
"input.subjectField": t("input.subjectField"),
|
||||||
|
"input.messageField": t("input.messageField"),
|
||||||
|
"btn.send": t("btn.send"),
|
||||||
|
"btn.sending": t("btn.sending"),
|
||||||
|
"error.invalidEmail": t("error.invalidEmail"),
|
||||||
|
"form.contactSuccess": t("form.contactSuccess"),
|
||||||
|
"form.contactError": t("form.contactError"),
|
||||||
|
}}
|
||||||
|
client:load
|
||||||
|
/>
|
||||||
<ContactsList />
|
<ContactsList />
|
||||||
</section>
|
</section>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@ -3,23 +3,28 @@ 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";
|
||||||
|
import { getLanguageFromURL } from "@root/utils";
|
||||||
|
import { useTranslation } from "@i18n/utils";
|
||||||
|
import UIString from "@components/UIString";
|
||||||
|
|
||||||
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);
|
||||||
|
const t = useTranslation(Astro);
|
||||||
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout showPageHeading={false}>
|
<PageLayout lang={lang} description={t("site.description")} showPageHeading={false}>
|
||||||
<Splash>
|
<Splash>
|
||||||
<SplashHeading>
|
<SplashHeading>
|
||||||
<hgroup>
|
<hgroup>
|
||||||
<h1>Hello, I'am Sergey</h1>
|
<h1><UIString key={"splash.headline"} /></h1>
|
||||||
<h2>Fullstack <b>web-developer</b> & <b>freelancer</b></h2>
|
<h2>Fullstack <b>web-developer</b> & <b>freelancer</b></h2>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
<p>Я занимаюсь разработкой веб-сайтов и веб-приложений разной сложности, а также публикую различные статьи и туториалы на разные IT темы.</p>
|
<p><UIString key={"splash.description"} /></p>
|
||||||
</SplashHeading>
|
</SplashHeading>
|
||||||
<SplashLinks>
|
<SplashLinks>
|
||||||
<Link icon="information" href="/about">обо мне</Link>
|
<Link icon="information" href="/about/"><UIString key={"splash.about"} /></Link>
|
||||||
<Link icon="email_filled" href="/contacts">написать мне</Link>
|
<Link icon="email_filled" href="/contacts/"><UIString key={"splash.contact"} /></Link>
|
||||||
</SplashLinks>
|
</SplashLinks>
|
||||||
</Splash>
|
</Splash>
|
||||||
<RecentArticles articles={articles} length={4} />
|
<RecentArticles articles={articles} length={4} />
|
||||||
|
|||||||
@ -1,10 +1,15 @@
|
|||||||
---
|
---
|
||||||
import PageLayout from '@layouts/default.astro';
|
import PageLayout from "@layouts/default.astro";
|
||||||
|
import { useTranslation } from "@i18n/utils";
|
||||||
|
import { getLanguageFromURL } from "@root/utils";
|
||||||
|
|
||||||
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
|
const t = useTranslation(Astro);
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout title="404" showPageHeading={false}>
|
<PageLayout lang={lang} title="404" showPageHeading={false}>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 style="font-size:var(--font-size-5xl);">404</h1>
|
<h1 style="font-size:var(--font-size-5xl);">404</h1>
|
||||||
<p>Страницу которую вы пытаетесь найти не существует.</p>
|
<p>{t("404.description")}</p>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
---
|
---
|
||||||
import { SITE_METADATA } from '@root/config';
|
import { SITE_METADATA } from "@root/config";
|
||||||
import PageLayout from '@layouts/default.astro';
|
import PageLayout from "@layouts/default.astro";
|
||||||
import ProjectsList from '@components/ProjectsList';
|
import ProjectsList from "@components/ProjectsList";
|
||||||
import ProjectItem from '@components/ProjectItem';
|
import ProjectItem from "@components/ProjectItem";
|
||||||
|
import { getLanguageFromURL } from "@root/utils";
|
||||||
|
import { useTranslation } from "@i18n/utils";
|
||||||
|
|
||||||
|
const t = useTranslation(Astro);
|
||||||
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
const { projects } = SITE_METADATA;
|
const { projects } = SITE_METADATA;
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout title="Проекты" description="Несколько моих основных проектов которые были разработаны и выложены в открытый доступ.">
|
<PageLayout lang={lang} title={t("pageTitle.projects")} description={t("pageDescription.projects")}>
|
||||||
<section>
|
<section>
|
||||||
<ProjectsList>
|
<ProjectsList>
|
||||||
{projects.map((project) => <ProjectItem data={project} />)}
|
{projects.map((project) => <ProjectItem data={project} />)}
|
||||||
|
|||||||
24
src/types/main.d.ts
vendored
24
src/types/main.d.ts
vendored
@ -1,3 +1,5 @@
|
|||||||
|
import uiKeys from "@i18n/ru/ui";
|
||||||
|
import languages from "@i18n/languages";
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -10,28 +12,22 @@ declare global {
|
|||||||
onthemetoggled: CustomEvent<ThemeModeType>;
|
onthemetoggled: CustomEvent<ThemeModeType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LanguageKeys = keyof typeof languages;
|
||||||
|
type UIDictionaryKeys = keyof typeof uiKeys;
|
||||||
|
|
||||||
type SiteMetadataType = {
|
type SiteMetadataType = {
|
||||||
title: string;
|
title: string;
|
||||||
name: string;
|
name: string;
|
||||||
author: string;
|
author: string;
|
||||||
description: string;
|
|
||||||
themeColor: string;
|
themeColor: string;
|
||||||
contacts: {
|
contacts: {
|
||||||
email: string;
|
email: string;
|
||||||
githubUrl: string;
|
githubUrl: string;
|
||||||
telegramUrl: string;
|
telegramUrl: string;
|
||||||
};
|
};
|
||||||
nav: Array<{ name: string; url: string }>;
|
nav: Array<{ key: UIDictionaryKeys; url: string }>;
|
||||||
projects: Array<{
|
projects: Array<ProjectType>;
|
||||||
icon: string;
|
skills: Array<SkillsType>;
|
||||||
name: string;
|
|
||||||
year: string;
|
|
||||||
description: string;
|
|
||||||
sourceUrl?: string | undefined;
|
|
||||||
demoUrl?: string | undefined;
|
|
||||||
articleUrl?: string | undefined;
|
|
||||||
}>;
|
|
||||||
skills: Array<{ title: string; list: Array<{ icon: string; name: string }> }>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ArticleType = {
|
type ArticleType = {
|
||||||
@ -48,14 +44,14 @@ declare global {
|
|||||||
icon: string;
|
icon: string;
|
||||||
name: string;
|
name: string;
|
||||||
year: string;
|
year: string;
|
||||||
description: string;
|
description: Record<LanguageKeys, string>;
|
||||||
sourceUrl?: string | undefined;
|
sourceUrl?: string | undefined;
|
||||||
articleUrl?: string | undefined;
|
articleUrl?: string | undefined;
|
||||||
demoUrl?: string | undefined;
|
demoUrl?: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SkillsType = {
|
type SkillsType = {
|
||||||
title: string;
|
title: Record<LanguageKeys, string>;
|
||||||
list: Array<{ icon?: string | undefined; name: string }>;
|
list: Array<{ icon?: string | undefined; name: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
export const DateFormatter = new Intl.DateTimeFormat(undefined, {
|
export function createDateFormatter(lang: string) {
|
||||||
timeZone: "UTC",
|
return new Intl.DateTimeFormat(lang, {
|
||||||
month: "short",
|
timeZone: "UTC",
|
||||||
year: "numeric",
|
month: "short",
|
||||||
day: "numeric",
|
year: "numeric",
|
||||||
});
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./articles";
|
export * from "./articles";
|
||||||
export * from "./date";
|
export * from "./date";
|
||||||
export * from "./history";
|
export * from "./history";
|
||||||
|
export * from "./url";
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
|
import { FALLBACK_LANGUAGE } from "@root/config";
|
||||||
|
|
||||||
export const encode = (data: object) => {
|
export const encode = (data: object) => {
|
||||||
return Object.keys(data)
|
return Object.keys(data)
|
||||||
.map((key) => encodeURIComponent(key) + "=" + encodeURIComponent(data[key]))
|
.map((key) => encodeURIComponent(key) + "=" + encodeURIComponent(data[key]))
|
||||||
.join("&");
|
.join("&");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getLanguageFromURL = (pathname: string) => {
|
||||||
|
const langCodeMatch = pathname.match(/\/([a-z]{1}-?[a-z]{1})\//);
|
||||||
|
return langCodeMatch ? langCodeMatch[1] : FALLBACK_LANGUAGE;
|
||||||
|
};
|
||||||
|
|||||||
@ -17,7 +17,8 @@
|
|||||||
"@layouts/*": ["./src/layouts/*"],
|
"@layouts/*": ["./src/layouts/*"],
|
||||||
"@utils/*": ["./src/utils/*"],
|
"@utils/*": ["./src/utils/*"],
|
||||||
"@stores/*": ["./src/stores/*"],
|
"@stores/*": ["./src/stores/*"],
|
||||||
"@hooks/*": ["./src/hooks/*"]
|
"@hooks/*": ["./src/hooks/*"],
|
||||||
|
"@i18n/*": ["./src/i18n/*"]
|
||||||
},
|
},
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"typeRoots": ["./node_modules/@types", "./src/types"]
|
"typeRoots": ["./node_modules/@types", "./src/types"]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user