mirror of
https://github.com/sergeyyarkov/yarkov.tech.git
synced 2026-10-11 18:39:14 +03:00
render article items
This commit is contained in:
parent
43950ae134
commit
25c37af405
@ -311,6 +311,13 @@ export default defineConfig({
|
|||||||
label: "Настройки веб-сайта",
|
label: "Настройки веб-сайта",
|
||||||
file: "siteconfig.json",
|
file: "siteconfig.json",
|
||||||
fields: [
|
fields: [
|
||||||
|
{
|
||||||
|
label: "Название сайта",
|
||||||
|
name: "name",
|
||||||
|
widget: "string",
|
||||||
|
hint: "Отображается в шапке.",
|
||||||
|
default: "yarkov.tech",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Глобальный заголовок",
|
label: "Глобальный заголовок",
|
||||||
name: "globalTitle",
|
name: "globalTitle",
|
||||||
|
|||||||
@ -9,5 +9,6 @@
|
|||||||
"telegram": "https://t.me/sergeyyarkov",
|
"telegram": "https://t.me/sergeyyarkov",
|
||||||
"github": "https://github.com/sergeyyarkov"
|
"github": "https://github.com/sergeyyarkov"
|
||||||
},
|
},
|
||||||
"ymCounter": 90034656
|
"ymCounter": 90034656,
|
||||||
|
"name": "yarkov.tech"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,83 +0,0 @@
|
|||||||
---
|
|
||||||
import type { CollectionEntry } from "astro:content";
|
|
||||||
import { URL_BLOG_PREFIX } from "@root/pages/[...blog].astro";
|
|
||||||
import { getLanguageFromURL } from "@i18n/utils";
|
|
||||||
import { createDateFormatter, createRelativeArticleUrl } from "@root/utils";
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
entry: CollectionEntry<"blog">;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
|
||||||
const { entry } = Astro.props as Props;
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
data: { title, pubDate },
|
|
||||||
} = entry;
|
|
||||||
const articleLang = id.split("/")[0];
|
|
||||||
---
|
|
||||||
|
|
||||||
<article
|
|
||||||
itemprop="blogPosts"
|
|
||||||
itemscope
|
|
||||||
itemtype="http://schema.org/BlogPosting"
|
|
||||||
class="article-item"
|
|
||||||
>
|
|
||||||
<a href={`${createRelativeArticleUrl(entry, URL_BLOG_PREFIX)}`}>
|
|
||||||
<div class="flex">
|
|
||||||
<h3 itemprop="headline">{title}</h3>
|
|
||||||
{articleLang !== pageLang && <sup>{articleLang.toLocaleUpperCase()}</sup>}
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<time datetime={pubDate.toISOString()}>
|
|
||||||
{createDateFormatter(pageLang).format(pubDate)}
|
|
||||||
</time>
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
article {
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
text-decoration: none;
|
|
||||||
& a {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
column-gap: 10px;
|
|
||||||
color: var(--font-color);
|
|
||||||
padding: 8px 0;
|
|
||||||
& sup {
|
|
||||||
margin-left: 5px;
|
|
||||||
font-size: 0.7em;
|
|
||||||
}
|
|
||||||
& h3 {
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
font-weight: normal;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
& p {
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
font-family: var(--font-family-monospace);
|
|
||||||
color: var(--font-color-gray);
|
|
||||||
text-transform: uppercase;
|
|
||||||
text-align: right;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 560px) {
|
|
||||||
article {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
& a {
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
align-items: flex-start;
|
|
||||||
& p {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
43
src/components/ArticleItem/ArticleItem.scss
Normal file
43
src/components/ArticleItem/ArticleItem.scss
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.article-item {
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
text-decoration: none;
|
||||||
|
& a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 10px;
|
||||||
|
color: var(--font-color);
|
||||||
|
padding: 8px 0;
|
||||||
|
& sup {
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
& h3 {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
& p {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family-monospace);
|
||||||
|
color: var(--font-color-gray);
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 560px) {
|
||||||
|
.article-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
& a {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
align-items: flex-start;
|
||||||
|
& p {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/components/ArticleItem/ArticleItem.tsx
Normal file
42
src/components/ArticleItem/ArticleItem.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import type { Component } from "solid-js";
|
||||||
|
import { createDateFormatter, createRelativeArticleUrl } from "@root/utils";
|
||||||
|
import { URL_BLOG_PREFIX } from "@root/constants";
|
||||||
|
import "./ArticleItem.scss";
|
||||||
|
|
||||||
|
export type ArticleItemProps = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
pubDate: Date;
|
||||||
|
pageLang: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArticleItem: Component<ArticleItemProps> = (props) => {
|
||||||
|
const articleLang = props.id.split("/")[0];
|
||||||
|
const href = createRelativeArticleUrl(
|
||||||
|
{ id: props.id, title: props.title, pubDate: props.pubDate },
|
||||||
|
URL_BLOG_PREFIX
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
itemprop="blogPosts"
|
||||||
|
itemscope
|
||||||
|
itemtype="http://schema.org/BlogPosting"
|
||||||
|
class="article-item"
|
||||||
|
>
|
||||||
|
<a href={href}>
|
||||||
|
<div class="flex">
|
||||||
|
<h3 itemprop="headline">{props.title}</h3>
|
||||||
|
{articleLang !== props.pageLang && <sup>{articleLang.toLocaleUpperCase()}</sup>}
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<time datetime={props.pubDate.toISOString()}>
|
||||||
|
{createDateFormatter(props.pageLang).format(props.pubDate)}
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArticleItem;
|
||||||
1
src/components/ArticleItem/index.ts
Normal file
1
src/components/ArticleItem/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./ArticleItem";
|
||||||
18
src/components/ArticlesList/ArticlesList.scss
Normal file
18
src/components/ArticlesList/ArticlesList.scss
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.articles {
|
||||||
|
&-list {
|
||||||
|
&__wrapper {
|
||||||
|
margin-top: 40px;
|
||||||
|
& h2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__empty {
|
||||||
|
color: var(--font-color-gray);
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/components/ArticlesList/ArticlesList.tsx
Normal file
101
src/components/ArticlesList/ArticlesList.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import type { Component } from "solid-js";
|
||||||
|
import { createSignal, createEffect, For } from "solid-js";
|
||||||
|
import { search, selectedTags } from "@stores/searchStore";
|
||||||
|
import ArticleItem from "@components/ArticleItem";
|
||||||
|
import "./ArticlesList.scss";
|
||||||
|
|
||||||
|
type UiStringsType = { "articles.empty": string };
|
||||||
|
|
||||||
|
type ArticleType = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
pubDate: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ArticlesType = Record<string, ArticleType[]>;
|
||||||
|
|
||||||
|
type ArticlesListProps = {
|
||||||
|
data: { articles: ArticlesType; pageLang: string };
|
||||||
|
i18n: UiStringsType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArticlesList: Component<ArticlesListProps> = (props) => {
|
||||||
|
const [articles, setArticles] = createSignal<ArticlesType>(props.data.articles);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort articles by year in descending order
|
||||||
|
*/
|
||||||
|
const sortYears = (articles: ArticlesType) => {
|
||||||
|
return Object.keys(articles)
|
||||||
|
.map(Number)
|
||||||
|
.sort((a, b) => b - a);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEmpty = () => Object.keys(articles()).length === 0;
|
||||||
|
const getLength = () => Object.values(articles()).flat().length;
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
// const filtered = Object.values(articles()).flat(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
let filtered = props.data.articles;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find articles by title
|
||||||
|
*/
|
||||||
|
if (search()) {
|
||||||
|
// filtered = findArticlesBySearch(search().trim(), filtered);
|
||||||
|
// setArticles(filtered);
|
||||||
|
console.log(search());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find articles by selected tags
|
||||||
|
*/
|
||||||
|
if (selectedTags().length !== 0) {
|
||||||
|
// filtered = findArticlesByTags(selectedTags(), filtered);
|
||||||
|
// setArticles(filtered);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set filtered variable as default value
|
||||||
|
*/
|
||||||
|
if (!search() && selectedTags().length === 0) {
|
||||||
|
filtered = props.data.articles;
|
||||||
|
setArticles(filtered);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div itemscope itemtype="http://schema.org/Blog" class="articles-list">
|
||||||
|
{!isEmpty() ? (
|
||||||
|
<For each={sortYears(articles())}>
|
||||||
|
{(year) => (
|
||||||
|
<div class="articles-list__wrapper">
|
||||||
|
<h2>{year}</h2>
|
||||||
|
<For each={articles()[year]}>
|
||||||
|
{(a) => (
|
||||||
|
<ArticleItem
|
||||||
|
id={a.id}
|
||||||
|
title={a.title}
|
||||||
|
pubDate={a.pubDate}
|
||||||
|
pageLang={props.data.pageLang}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
) : (
|
||||||
|
<div class="articles-list__empty">
|
||||||
|
<p>{props.i18n["articles.empty"]}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArticlesList;
|
||||||
1
src/components/ArticlesList/index.ts
Normal file
1
src/components/ArticlesList/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./ArticlesList";
|
||||||
@ -71,7 +71,7 @@ const t = useTranslation(Astro);
|
|||||||
border: 1px solid var(--color-error);
|
border: 1px solid var(--color-error);
|
||||||
}
|
}
|
||||||
& .footer {
|
& .footer {
|
||||||
text-align: center;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 560px) {
|
@media screen and (max-width: 560px) {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import UIString from "./UIString.astro";
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div>
|
<div>
|
||||||
<i>
|
<i>
|
||||||
<UIString key={"footer.developed"} />: <TranslatedLink to="/contacts">
|
<UIString key={"footer.developed"} />: <TranslatedLink to="/contacts/">
|
||||||
{SITE_METADATA.author}
|
{SITE_METADATA.author}
|
||||||
</TranslatedLink>
|
</TranslatedLink>
|
||||||
© {new Date().getFullYear()}
|
© {new Date().getFullYear()}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { Icon } from "astro-icon";
|
import { Icon } from "astro-icon";
|
||||||
import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES } from "@root/constants";
|
import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES, URL_BLOG_PREFIX } from "@root/constants";
|
||||||
import { URL_BLOG_PREFIX } from "@root/pages/[...blog].astro";
|
|
||||||
import { getLanguageFromURL } from "@i18n/utils";
|
import { getLanguageFromURL } from "@i18n/utils";
|
||||||
|
|
||||||
const lang = getLanguageFromURL(Astro.url.pathname);
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import type { CollectionEntry } from "astro:content";
|
|||||||
import siteconfig from "siteconfig.json";
|
import siteconfig from "siteconfig.json";
|
||||||
import { getLanguageFromURL } from "@i18n/utils";
|
import { getLanguageFromURL } from "@i18n/utils";
|
||||||
import * as utils from "@utils/index";
|
import * as utils from "@utils/index";
|
||||||
import ArticleItem from "@components/ArticleItem.astro";
|
import ArticleItem from "@components/ArticleItem";
|
||||||
import EmptyList from "@components/EmptyList.astro";
|
import EmptyList from "@components/EmptyList.astro";
|
||||||
import UIString from "@components/UIString.astro";
|
import UIString from "@components/UIString.astro";
|
||||||
import TranslatedLink from "@components/TranslatedLink.astro";
|
import TranslatedLink from "@components/TranslatedLink.astro";
|
||||||
@ -24,12 +24,25 @@ const isEmpty = articles.length === 0;
|
|||||||
<h2>
|
<h2>
|
||||||
<UIString key={"recentArticles.heading"} />
|
<UIString key={"recentArticles.heading"} />
|
||||||
</h2>
|
</h2>
|
||||||
<TranslatedLink to="/articles">
|
<TranslatedLink to="/articles/">
|
||||||
<UIString key={"recentArticles.all"} />
|
<UIString key={"recentArticles.all"} />
|
||||||
</TranslatedLink>
|
</TranslatedLink>
|
||||||
</div>
|
</div>
|
||||||
<div itemscope itemtype="http://schema.org/Blog">
|
<div itemscope itemtype="http://schema.org/Blog">
|
||||||
{!isEmpty ? filtered.map((article) => <ArticleItem entry={article} />) : <EmptyList />}
|
{
|
||||||
|
!isEmpty ? (
|
||||||
|
filtered.map((article) => (
|
||||||
|
<ArticleItem
|
||||||
|
id={article.id}
|
||||||
|
title={article.data.title}
|
||||||
|
pubDate={article.data.pubDate}
|
||||||
|
pageLang={pageLang}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<EmptyList />
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -40,7 +53,7 @@ const isEmpty = articles.length === 0;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: 15px;
|
column-gap: 15px;
|
||||||
row-gap: 5px;
|
row-gap: 5px;
|
||||||
flex-wrap: wrap;
|
article-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
|||||||
27
src/components/SearchInput/SearchInput.scss
Normal file
27
src/components/SearchInput/SearchInput.scss
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
.search-input {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
fill: var(--font-color-grayer);
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& input {
|
||||||
|
padding-left: 40px;
|
||||||
|
color: var(--font-color-gray);
|
||||||
|
background-color: var(--background-color-hover);
|
||||||
|
}
|
||||||
|
& span {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--font-color-gray);
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/components/SearchInput/SearchInput.tsx
Normal file
76
src/components/SearchInput/SearchInput.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import type { Component, JSX } from "solid-js";
|
||||||
|
import { createEffect } from "solid-js";
|
||||||
|
import { search, setSearch, count } from "@stores/searchStore";
|
||||||
|
import * as utils from "@utils/history";
|
||||||
|
import "./SearchInput.scss";
|
||||||
|
|
||||||
|
type UiStringsType = { "input.search": string };
|
||||||
|
type SearchInputProps = { i18n: UiStringsType };
|
||||||
|
|
||||||
|
const SearchInput: Component<SearchInputProps> = (props) => {
|
||||||
|
/**
|
||||||
|
* Reset state and delete `search` param from url
|
||||||
|
*/
|
||||||
|
const resetSearch = () => {
|
||||||
|
const url = utils.deleteParamFromUrl("search");
|
||||||
|
setSearch("");
|
||||||
|
history.replaceState(null, "", url);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update state and set url param
|
||||||
|
*/
|
||||||
|
const onSearch: JSX.EventHandler<HTMLInputElement, InputEvent> = (e) => {
|
||||||
|
const value = e.currentTarget.value;
|
||||||
|
|
||||||
|
if (value.trim().length === 0) {
|
||||||
|
resetSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = utils.setParamToUrl("search", value.trim());
|
||||||
|
history.replaceState(null, "", `${location.pathname}?${url}`);
|
||||||
|
setSearch(value);
|
||||||
|
|
||||||
|
// TODO: search articles
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the signal state by reading the `search` parameter from the URL.
|
||||||
|
*/
|
||||||
|
createEffect(() => {
|
||||||
|
const param = new URLSearchParams(location.search).get("search");
|
||||||
|
|
||||||
|
if (!param) {
|
||||||
|
resetSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearch(param);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="search-input">
|
||||||
|
<svg
|
||||||
|
width="25"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 25 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
data-filter="none"
|
||||||
|
>
|
||||||
|
<path d="M10.6026 3.60001C11.8798 3.59993 13.1316 3.94283 14.2172 4.5901C15.3028 5.23736 16.179 6.16331 16.7472 7.26368C17.3154 8.36405 17.5531 9.59517 17.4334 10.8184C17.3137 12.0417 16.8415 13.2085 16.0699 14.1876L21.2427 19.164C21.4085 19.3248 21.5053 19.5399 21.5135 19.7662C21.5218 19.9926 21.4411 20.2137 21.2874 20.3853C21.1337 20.5569 20.9184 20.6665 20.6845 20.6921C20.4505 20.7178 20.2151 20.6576 20.0253 20.5236L19.9205 20.436L14.7476 15.4596C13.8802 16.0922 12.8687 16.5172 11.7968 16.6994C10.7248 16.8815 9.62324 16.8157 8.58298 16.5074C7.54272 16.199 6.59366 15.6569 5.81422 14.9259C5.03478 14.1949 4.44733 13.296 4.10042 12.3034C3.7535 11.3108 3.65708 10.2531 3.81911 9.21757C3.98114 8.18204 4.39698 7.19847 5.03226 6.34812C5.66755 5.49778 6.50404 4.80506 7.47263 4.32721C8.44122 3.84936 9.51411 3.60009 10.6026 3.60001V3.60001ZM10.6026 5.40001C9.27934 5.40001 8.01025 5.90572 7.07454 6.80589C6.13884 7.70607 5.61316 8.92697 5.61316 10.2C5.61316 11.473 6.13884 12.6939 7.07454 13.5941C8.01025 14.4943 9.27934 15 10.6026 15C11.9259 15 13.195 14.4943 14.1307 13.5941C15.0664 12.6939 15.5921 11.473 15.5921 10.2C15.5921 8.92697 15.0664 7.70607 14.1307 6.80589C13.195 5.90572 11.9259 5.40001 10.6026 5.40001Z" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
onInput={onSearch}
|
||||||
|
value={search()}
|
||||||
|
type="search"
|
||||||
|
placeholder={props.i18n["input.search"] || "Search article by title..."}
|
||||||
|
autocapitalize="off"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<span>{count()}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchInput;
|
||||||
1
src/components/SearchInput/index.ts
Normal file
1
src/components/SearchInput/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./SearchInput";
|
||||||
100
src/components/TagsList/TagsList.tsx
Normal file
100
src/components/TagsList/TagsList.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import type { Component, JSX } from "solid-js";
|
||||||
|
import { For, createEffect } from "solid-js";
|
||||||
|
import { selectedTags, setSelectedTags } from "@stores/searchStore";
|
||||||
|
import * as utils from "@utils/history";
|
||||||
|
|
||||||
|
type TagsListProps = { data: string[] };
|
||||||
|
|
||||||
|
const TagsList: Component<TagsListProps> = (props) => {
|
||||||
|
const isTagSelected = (tag: string) => selectedTags().includes(tag.toLocaleLowerCase());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset signal state and `tags` property in history state
|
||||||
|
*/
|
||||||
|
const resetSelected = () => {
|
||||||
|
const url = utils.deleteParamFromUrl("tags");
|
||||||
|
|
||||||
|
setSelectedTags([]);
|
||||||
|
history.replaceState(null, "", url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect: JSX.EventHandler<HTMLLIElement, PointerEvent> = (e) => {
|
||||||
|
const tag = e.target.getAttribute("data-tag");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete tag from selected tags if already selected
|
||||||
|
* and update param in url
|
||||||
|
*/
|
||||||
|
if (isTagSelected(tag)) {
|
||||||
|
const updated = selectedTags().filter((selectedTag) => selectedTag !== tag);
|
||||||
|
const url = utils.setParamToUrl("tags", updated.join(","));
|
||||||
|
|
||||||
|
if (updated.length === 0) {
|
||||||
|
resetSelected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedTags(updated);
|
||||||
|
history.replaceState(null, "", `${location.pathname}?${url}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add selected tag to state and update url
|
||||||
|
*/
|
||||||
|
const updated = [...selectedTags(), tag];
|
||||||
|
const url = utils.setParamToUrl("tags", updated.join(","));
|
||||||
|
|
||||||
|
setSelectedTags(updated);
|
||||||
|
history.replaceState(null, "", `${location.pathname}?${url}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
let param: string | string[] = new URLSearchParams(location.search).get("tags");
|
||||||
|
|
||||||
|
if (!param) {
|
||||||
|
resetSelected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* to array */
|
||||||
|
param = param.split(",");
|
||||||
|
|
||||||
|
if (param.length === 0) {
|
||||||
|
resetSelected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags: string[] = [];
|
||||||
|
|
||||||
|
for (const tag of props.data) {
|
||||||
|
const isTagExist = param.includes(tag.toLocaleLowerCase());
|
||||||
|
|
||||||
|
if (isTagExist) {
|
||||||
|
tags.push(tag.toLocaleLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedTags(tags);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul class="list-links">
|
||||||
|
<For
|
||||||
|
each={props.data}
|
||||||
|
children={(tag) => (
|
||||||
|
<li
|
||||||
|
onClick={onSelect}
|
||||||
|
class="btn-link"
|
||||||
|
data-tag={tag.toLocaleLowerCase()}
|
||||||
|
data-active={isTagSelected(tag)}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagsList;
|
||||||
1
src/components/TagsList/index.ts
Normal file
1
src/components/TagsList/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./TagsList";
|
||||||
@ -3,9 +3,10 @@ import siteconfig from "../siteconfig.json";
|
|||||||
export const DEFAULT_LANGUAGE: LanguageKeys = "ru";
|
export const DEFAULT_LANGUAGE: LanguageKeys = "ru";
|
||||||
export const IS_SITE_UNDER_CONSTRUCTION = siteconfig.isSiteUnderConstruction;
|
export const IS_SITE_UNDER_CONSTRUCTION = siteconfig.isSiteUnderConstruction;
|
||||||
export const SUPPORTED_LANGUAGES: SupportedLanguages = ["ru", "en"];
|
export const SUPPORTED_LANGUAGES: SupportedLanguages = ["ru", "en"];
|
||||||
|
export const URL_BLOG_PREFIX = "blog";
|
||||||
export const SITE_METADATA: SiteMetadataType = {
|
export const SITE_METADATA: SiteMetadataType = {
|
||||||
title: siteconfig.globalTitle,
|
title: siteconfig.globalTitle,
|
||||||
name: "yarkov.tech",
|
name: siteconfig.name,
|
||||||
author: "Sergey Yarkov",
|
author: "Sergey Yarkov",
|
||||||
themeColor: "#1e2327",
|
themeColor: "#1e2327",
|
||||||
contacts: siteconfig.contacts,
|
contacts: siteconfig.contacts,
|
||||||
|
|||||||
10
src/content/blog/ru/тестовая-статьия.md
Normal file
10
src/content/blog/ru/тестовая-статьия.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
author: Сергей Ярков
|
||||||
|
title: Тестовая статьия
|
||||||
|
description: фывфы
|
||||||
|
tags:
|
||||||
|
- Digital Electronics
|
||||||
|
pubDate: 2023-02-19T10:46:31.109Z
|
||||||
|
updatedDate: 2023-02-19T10:46:31.127Z
|
||||||
|
---
|
||||||
|
123
|
||||||
@ -6,7 +6,7 @@ const blog = defineCollection({
|
|||||||
draft: z.boolean().nullable().optional(),
|
draft: z.boolean().nullable().optional(),
|
||||||
coverImage: z.string().nullable().optional(),
|
coverImage: z.string().nullable().optional(),
|
||||||
author: z.string(),
|
author: z.string(),
|
||||||
tags: z.string().array().optional(),
|
tags: z.string().array(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
pubDate: z
|
pubDate: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export default {
|
|||||||
"input.nameField": "Your name",
|
"input.nameField": "Your name",
|
||||||
"input.subjectField": "Message subject",
|
"input.subjectField": "Message subject",
|
||||||
"input.messageField": "Your message...",
|
"input.messageField": "Your message...",
|
||||||
"input.search": "search by title or description...",
|
"input.search": "search publications by title...",
|
||||||
/* Forms */
|
/* Forms */
|
||||||
"form.contactSuccess": "The message was submitted successfully!",
|
"form.contactSuccess": "The message was submitted successfully!",
|
||||||
"form.contactError": "An error occurred while sending. Please try again later.",
|
"form.contactError": "An error occurred while sending. Please try again later.",
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export default {
|
|||||||
"input.nameField": "Ваше имя",
|
"input.nameField": "Ваше имя",
|
||||||
"input.subjectField": "Тема сообщения",
|
"input.subjectField": "Тема сообщения",
|
||||||
"input.messageField": "Ваше сообщение...",
|
"input.messageField": "Ваше сообщение...",
|
||||||
"input.search": "поиск по названию или описанию...",
|
"input.search": "поиск публикаций по названию...",
|
||||||
/* Forms */
|
/* Forms */
|
||||||
"form.contactSuccess": "Сообщение было успешно отправлено!",
|
"form.contactSuccess": "Сообщение было успешно отправлено!",
|
||||||
"form.contactError": "При отправке произошла ошибка. Повторите запрос позже.",
|
"form.contactError": "При отправке произошла ошибка. Повторите запрос позже.",
|
||||||
|
|||||||
@ -3,13 +3,12 @@ import { CollectionEntry, getCollection } from "astro:content";
|
|||||||
import ArticleLayout from "@root/layouts/article.astro";
|
import ArticleLayout from "@root/layouts/article.astro";
|
||||||
import { createRelativeArticleUrl, removeLanguageCodeFromPath } from "@root/utils";
|
import { createRelativeArticleUrl, removeLanguageCodeFromPath } from "@root/utils";
|
||||||
import { getLanguageFromURL, useTranslation } from "@i18n/utils";
|
import { getLanguageFromURL, useTranslation } from "@i18n/utils";
|
||||||
|
import { URL_BLOG_PREFIX } from "@root/constants";
|
||||||
import languages from "@i18n/languages";
|
import languages from "@i18n/languages";
|
||||||
|
|
||||||
const t = useTranslation(Astro);
|
const t = useTranslation(Astro);
|
||||||
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
||||||
|
|
||||||
export const URL_BLOG_PREFIX = "blog";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate articles pages from `blog` collection.
|
* Generate articles pages from `blog` collection.
|
||||||
* The final url will be `http://website.com/blog/2023-01-01/my-article` with default language
|
* The final url will be `http://website.com/blog/2023-01-01/my-article` with default language
|
||||||
@ -18,7 +17,11 @@ export const URL_BLOG_PREFIX = "blog";
|
|||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const collection = await getCollection("blog", (e) => !e.data.draft);
|
const collection = await getCollection("blog", (e) => !e.data.draft);
|
||||||
return collection.map((article) => {
|
return collection.map((article) => {
|
||||||
const url = createRelativeArticleUrl(article);
|
const {
|
||||||
|
id,
|
||||||
|
data: { title, pubDate },
|
||||||
|
} = article;
|
||||||
|
const url = createRelativeArticleUrl({ id, title, pubDate });
|
||||||
return {
|
return {
|
||||||
params: { blog: url.slice(1) },
|
params: { blog: url.slice(1) },
|
||||||
props: {
|
props: {
|
||||||
@ -44,9 +47,9 @@ const { article, availableOnLang } = Astro.props;
|
|||||||
{t("article.availableOn")}:{" "}
|
{t("article.availableOn")}:{" "}
|
||||||
{availableOnLang
|
{availableOnLang
|
||||||
.filter((a) => !a.id.startsWith(`${pageLang}/`))
|
.filter((a) => !a.id.startsWith(`${pageLang}/`))
|
||||||
.map((a) => (
|
.map(({ id, data: { title, pubDate } }) => (
|
||||||
<a href={`${createRelativeArticleUrl(a, URL_BLOG_PREFIX)}`}>{`${
|
<a href={`${createRelativeArticleUrl({ id, title, pubDate }, URL_BLOG_PREFIX)}`}>{`${
|
||||||
languages[a.id.split("/")[0] as LanguageKeys]
|
languages[id.split("/")[0] as LanguageKeys]
|
||||||
}`}</a>
|
}`}</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
src/stores/index.ts
Normal file
1
src/stores/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./searchStore";
|
||||||
5
src/stores/searchStore.ts
Normal file
5
src/stores/searchStore.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
|
export const [search, setSearch] = createSignal<string>("");
|
||||||
|
export const [count, setCount] = createSignal<number>(0);
|
||||||
|
export const [selectedTags, setSelectedTags] = createSignal<string[]>([]);
|
||||||
@ -1,10 +1,18 @@
|
|||||||
---
|
---
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import { useTranslation, getLanguageFromURL } from "@i18n/utils";
|
||||||
|
import * as utils from "@utils/index";
|
||||||
import PageLayout from "@layouts/default.astro";
|
import PageLayout from "@layouts/default.astro";
|
||||||
import PageHeading from "@components/PageHeading.astro";
|
import PageHeading from "@components/PageHeading.astro";
|
||||||
import { useTranslation, getLanguageFromURL } from "@i18n/utils";
|
import ArticlesList from "@root/components/ArticlesList";
|
||||||
|
import SearchInput from "@components/SearchInput";
|
||||||
|
import TagsList from "@components/TagsList";
|
||||||
|
|
||||||
const t = useTranslation(Astro);
|
const t = useTranslation(Astro);
|
||||||
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
||||||
|
const articles = await getCollection("blog", (e) => !e.data.draft);
|
||||||
|
const tags = utils.getUniqueTags(articles);
|
||||||
|
const filtered = utils.removeDuplicates(articles, pageLang);
|
||||||
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -16,15 +24,12 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
|||||||
>
|
>
|
||||||
<PageHeading heading={t("pageTitle.articles")} description={t("pageDescription.articles")} />
|
<PageHeading heading={t("pageTitle.articles")} description={t("pageDescription.articles")} />
|
||||||
<section>
|
<section>
|
||||||
...
|
<SearchInput i18n={{ "input.search": t("input.search") }} client:load />
|
||||||
<!-- <ArticlesList
|
<TagsList data={tags} client:load />
|
||||||
|
<ArticlesList
|
||||||
client:load
|
client:load
|
||||||
lang={lang}
|
i18n={{ "articles.empty": t("articles.empty") }}
|
||||||
i18n={{
|
data={{ articles: utils.formatToArticleBlocks(filtered), pageLang }}
|
||||||
"input.search": t("input.search"),
|
/>
|
||||||
"articles.empty": t("articles.empty"),
|
|
||||||
}}
|
|
||||||
data={{ articles: utils.formatToArticleBlocks(articles), tags: [...tags] }}
|
|
||||||
/> -->
|
|
||||||
</section>
|
</section>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import PageHeading from "@components/PageHeading.astro";
|
|||||||
import ContactForm from "@components/ContactForm.astro";
|
import ContactForm from "@components/ContactForm.astro";
|
||||||
import ContactsList from "@components/ContactsList.astro";
|
import ContactsList from "@components/ContactsList.astro";
|
||||||
import { getLanguageFromURL, useTranslation } from "@i18n/utils";
|
import { getLanguageFromURL, useTranslation } from "@i18n/utils";
|
||||||
|
|
||||||
const t = useTranslation(Astro);
|
const t = useTranslation(Astro);
|
||||||
const lang = getLanguageFromURL(Astro.url.pathname);
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
---
|
---
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import RecentArticles from "@components/RecentArticles.astro";
|
|||||||
import UIString from "@components/UIString.astro";
|
import UIString from "@components/UIString.astro";
|
||||||
|
|
||||||
const articles = (await getCollection("blog", (e) => !e.data.draft)).sort(
|
const articles = (await getCollection("blog", (e) => !e.data.draft)).sort(
|
||||||
(a, b) => a.data.pubDate.valueOf() - b.data.pubDate.valueOf()
|
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
|
||||||
);
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
---
|
---
|
||||||
import PageLayout from "@layouts/default.astro";
|
import PageLayout from "@layouts/default.astro";
|
||||||
import { getLanguageFromURL } from "@i18n/utils";
|
import { getLanguageFromURL } from "@i18n/utils";
|
||||||
|
import UIString from "@root/components/UIString.astro";
|
||||||
|
|
||||||
const lang = getLanguageFromURL(Astro.url.pathname);
|
const lang = getLanguageFromURL(Astro.url.pathname);
|
||||||
// const t = useTranslation(Astro);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout lang={lang}>
|
<PageLayout lang={lang}>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 style="font-size:var(--font-size-5xl);">404</h1>
|
<h1 style="font-size:var(--font-size-5xl);"><UIString key={"404.heading"} /></h1>
|
||||||
<p>{"Страницу которую вы пытаетесь найти не существует."}</p>
|
<p><UIString key={"404.description"} /></p>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@ -7,10 +7,6 @@ import { removeLanguageCodeFromPath } from "./url";
|
|||||||
/**
|
/**
|
||||||
* If there is a translation of an article in the current language of the page,
|
* If there is a translation of an article in the current language of the page,
|
||||||
* the remaining articles with other translations should be removed.
|
* the remaining articles with other translations should be removed.
|
||||||
*
|
|
||||||
* @param entries Array of articles
|
|
||||||
* @param pageLang Current language on page
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export function removeDuplicates(
|
export function removeDuplicates(
|
||||||
entries: CollectionEntry<"blog">[],
|
entries: CollectionEntry<"blog">[],
|
||||||
@ -34,14 +30,30 @@ export function removeDuplicates(
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will create a relative url based on article entry.
|
||||||
|
* `/en/blog/2023-01-01/my-first-article`
|
||||||
|
*/
|
||||||
export function createRelativeArticleUrl(
|
export function createRelativeArticleUrl(
|
||||||
article: CollectionEntry<"blog">,
|
params: { id: string; title: string; pubDate: Date },
|
||||||
prefix: string = "blog"
|
prefix: string = "blog"
|
||||||
): string {
|
): string {
|
||||||
const articleLang = article.id.split("/")[0];
|
const articleLang = params.id.split("/")[0];
|
||||||
const date = article.data.pubDate.toISOString().split("T")[0];
|
const date = params.pubDate.toISOString().split("T")[0];
|
||||||
return `${articleLang !== DEFAULT_LANGUAGE ? "/" + articleLang : ""}/${prefix}/${date}/${slugify(
|
const lang = articleLang !== DEFAULT_LANGUAGE ? "/" + articleLang : "";
|
||||||
article.data.title,
|
return `${lang}/${prefix}/${date}/${slugify(params.title, { lowercase: true })}`;
|
||||||
{ lowercase: true }
|
}
|
||||||
)}`;
|
|
||||||
|
export function formatToArticleBlocks(articles: CollectionEntry<"blog">[]) {
|
||||||
|
const blocks: Record<string, any[]> = {};
|
||||||
|
articles.forEach((a) => {
|
||||||
|
const year = a.data.pubDate.getFullYear();
|
||||||
|
if (!blocks[year]) blocks[year] = [];
|
||||||
|
blocks[year].push({ id: a.id, title: a.data.title, pubDate: a.data.pubDate });
|
||||||
|
});
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUniqueTags(articles: CollectionEntry<"blog">[]): string[] {
|
||||||
|
return Array.from(new Set(articles.map((a) => a.data.tags).flat()));
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user