+ {!isEmpty() ? (
+
+ {(year) => (
+
+
{year}
+
+ {(a) => (
+
+ )}
+
+
+ )}
+
+ ) : (
+
+
{props.i18n["articles.empty"]}
+
+ )}
+
+ );
+};
+
+export default ArticlesList;
diff --git a/src/components/ArticlesList/index.ts b/src/components/ArticlesList/index.ts
new file mode 100644
index 0000000..17b28e3
--- /dev/null
+++ b/src/components/ArticlesList/index.ts
@@ -0,0 +1 @@
+export { default } from "./ArticlesList";
diff --git a/src/components/ContactForm.astro b/src/components/ContactForm.astro
index 670ecfa..4071a86 100644
--- a/src/components/ContactForm.astro
+++ b/src/components/ContactForm.astro
@@ -71,7 +71,7 @@ const t = useTranslation(Astro);
border: 1px solid var(--color-error);
}
& .footer {
- text-align: center;
+ text-align: right;
}
}
@media screen and (max-width: 560px) {
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
index 1bb8b2a..89a2394 100644
--- a/src/components/Footer.astro
+++ b/src/components/Footer.astro
@@ -9,7 +9,7 @@ import UIString from "./UIString.astro";
- :
+ :
{SITE_METADATA.author}
© {new Date().getFullYear()}
diff --git a/src/components/LanguageSelect.astro b/src/components/LanguageSelect.astro
index 4c4d483..02bd794 100644
--- a/src/components/LanguageSelect.astro
+++ b/src/components/LanguageSelect.astro
@@ -1,7 +1,6 @@
---
import { Icon } from "astro-icon";
-import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES } from "@root/constants";
-import { URL_BLOG_PREFIX } from "@root/pages/[...blog].astro";
+import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES, URL_BLOG_PREFIX } from "@root/constants";
import { getLanguageFromURL } from "@i18n/utils";
const lang = getLanguageFromURL(Astro.url.pathname);
diff --git a/src/components/RecentArticles.astro b/src/components/RecentArticles.astro
index fb81519..ec9b88f 100644
--- a/src/components/RecentArticles.astro
+++ b/src/components/RecentArticles.astro
@@ -3,7 +3,7 @@ import type { CollectionEntry } from "astro:content";
import siteconfig from "siteconfig.json";
import { getLanguageFromURL } from "@i18n/utils";
import * as utils from "@utils/index";
-import ArticleItem from "@components/ArticleItem.astro";
+import ArticleItem from "@components/ArticleItem";
import EmptyList from "@components/EmptyList.astro";
import UIString from "@components/UIString.astro";
import TranslatedLink from "@components/TranslatedLink.astro";
@@ -24,12 +24,25 @@ const isEmpty = articles.length === 0;
-
+
- {!isEmpty ? filtered.map((article) =>
) :
}
+ {
+ !isEmpty ? (
+ filtered.map((article) => (
+
+ ))
+ ) : (
+
+ )
+ }
@@ -40,7 +53,7 @@ const isEmpty = articles.length === 0;
align-items: center;
column-gap: 15px;
row-gap: 5px;
- flex-wrap: wrap;
+ article-wrap: wrap;
justify-content: space-between;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
diff --git a/src/components/SearchInput/SearchInput.scss b/src/components/SearchInput/SearchInput.scss
new file mode 100644
index 0000000..22fac36
--- /dev/null
+++ b/src/components/SearchInput/SearchInput.scss
@@ -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;
+ }
+}
diff --git a/src/components/SearchInput/SearchInput.tsx b/src/components/SearchInput/SearchInput.tsx
new file mode 100644
index 0000000..247928c
--- /dev/null
+++ b/src/components/SearchInput/SearchInput.tsx
@@ -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
= (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 = (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 (
+
+ );
+};
+
+export default SearchInput;
diff --git a/src/components/SearchInput/index.ts b/src/components/SearchInput/index.ts
new file mode 100644
index 0000000..1a2fa40
--- /dev/null
+++ b/src/components/SearchInput/index.ts
@@ -0,0 +1 @@
+export { default } from "./SearchInput";
diff --git a/src/components/TagsList/TagsList.tsx b/src/components/TagsList/TagsList.tsx
new file mode 100644
index 0000000..f3b7525
--- /dev/null
+++ b/src/components/TagsList/TagsList.tsx
@@ -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 = (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 = (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 (
+
+ (
+ -
+ {tag}
+
+ )}
+ />
+
+ );
+};
+
+export default TagsList;
diff --git a/src/components/TagsList/index.ts b/src/components/TagsList/index.ts
new file mode 100644
index 0000000..c010bfe
--- /dev/null
+++ b/src/components/TagsList/index.ts
@@ -0,0 +1 @@
+export { default } from "./TagsList";
diff --git a/src/constants.ts b/src/constants.ts
index 8e4ffbb..b6f5a78 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -3,9 +3,10 @@ import siteconfig from "../siteconfig.json";
export const DEFAULT_LANGUAGE: LanguageKeys = "ru";
export const IS_SITE_UNDER_CONSTRUCTION = siteconfig.isSiteUnderConstruction;
export const SUPPORTED_LANGUAGES: SupportedLanguages = ["ru", "en"];
+export const URL_BLOG_PREFIX = "blog";
export const SITE_METADATA: SiteMetadataType = {
title: siteconfig.globalTitle,
- name: "yarkov.tech",
+ name: siteconfig.name,
author: "Sergey Yarkov",
themeColor: "#1e2327",
contacts: siteconfig.contacts,
diff --git a/src/content/blog/ru/тестовая-статьия.md b/src/content/blog/ru/тестовая-статьия.md
new file mode 100644
index 0000000..ab2dda3
--- /dev/null
+++ b/src/content/blog/ru/тестовая-статьия.md
@@ -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
\ No newline at end of file
diff --git a/src/content/config.ts b/src/content/config.ts
index 6d13e53..7b40cd3 100644
--- a/src/content/config.ts
+++ b/src/content/config.ts
@@ -6,7 +6,7 @@ const blog = defineCollection({
draft: z.boolean().nullable().optional(),
coverImage: z.string().nullable().optional(),
author: z.string(),
- tags: z.string().array().optional(),
+ tags: z.string().array(),
description: z.string(),
pubDate: z
.string()
diff --git a/src/i18n/en/ui.ts b/src/i18n/en/ui.ts
index 37bd267..0a7a174 100644
--- a/src/i18n/en/ui.ts
+++ b/src/i18n/en/ui.ts
@@ -47,7 +47,7 @@ export default {
"input.nameField": "Your name",
"input.subjectField": "Message subject",
"input.messageField": "Your message...",
- "input.search": "search by title or description...",
+ "input.search": "search publications by title...",
/* Forms */
"form.contactSuccess": "The message was submitted successfully!",
"form.contactError": "An error occurred while sending. Please try again later.",
diff --git a/src/i18n/ru/ui.ts b/src/i18n/ru/ui.ts
index 08464fa..77f4fa2 100644
--- a/src/i18n/ru/ui.ts
+++ b/src/i18n/ru/ui.ts
@@ -48,7 +48,7 @@ export default {
"input.nameField": "Ваше имя",
"input.subjectField": "Тема сообщения",
"input.messageField": "Ваше сообщение...",
- "input.search": "поиск по названию или описанию...",
+ "input.search": "поиск публикаций по названию...",
/* Forms */
"form.contactSuccess": "Сообщение было успешно отправлено!",
"form.contactError": "При отправке произошла ошибка. Повторите запрос позже.",
diff --git a/src/pages/[...blog].astro b/src/pages/[...blog].astro
index cd3afe8..9fa0c1b 100644
--- a/src/pages/[...blog].astro
+++ b/src/pages/[...blog].astro
@@ -3,13 +3,12 @@ import { CollectionEntry, getCollection } from "astro:content";
import ArticleLayout from "@root/layouts/article.astro";
import { createRelativeArticleUrl, removeLanguageCodeFromPath } from "@root/utils";
import { getLanguageFromURL, useTranslation } from "@i18n/utils";
+import { URL_BLOG_PREFIX } from "@root/constants";
import languages from "@i18n/languages";
const t = useTranslation(Astro);
const pageLang = getLanguageFromURL(Astro.url.pathname);
-export const URL_BLOG_PREFIX = "blog";
-
/**
* Generate articles pages from `blog` collection.
* 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() {
const collection = await getCollection("blog", (e) => !e.data.draft);
return collection.map((article) => {
- const url = createRelativeArticleUrl(article);
+ const {
+ id,
+ data: { title, pubDate },
+ } = article;
+ const url = createRelativeArticleUrl({ id, title, pubDate });
return {
params: { blog: url.slice(1) },
props: {
@@ -44,9 +47,9 @@ const { article, availableOnLang } = Astro.props;
{t("article.availableOn")}:{" "}
{availableOnLang
.filter((a) => !a.id.startsWith(`${pageLang}/`))
- .map((a) => (
- {`${
- languages[a.id.split("/")[0] as LanguageKeys]
+ .map(({ id, data: { title, pubDate } }) => (
+ {`${
+ languages[id.split("/")[0] as LanguageKeys]
}`}
))}
diff --git a/src/stores/index.ts b/src/stores/index.ts
new file mode 100644
index 0000000..4aa0760
--- /dev/null
+++ b/src/stores/index.ts
@@ -0,0 +1 @@
+export * from "./searchStore";
diff --git a/src/stores/searchStore.ts b/src/stores/searchStore.ts
new file mode 100644
index 0000000..382faa5
--- /dev/null
+++ b/src/stores/searchStore.ts
@@ -0,0 +1,5 @@
+import { createSignal } from "solid-js";
+
+export const [search, setSearch] = createSignal