core-maugli 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -15
- package/package.json +6 -13
- package/public/img/examples/blog/previews/post-1-avtomatizaciya-marketinga-kak-ii-osvobozhdaet-predprinimatelei-ot-cifrovogo-rabstva.webp +0 -0
- package/public/img/examples/blog/previews/post-2-avtomatizaciya-kontenta-kak-neiroseti-ubivayut-perfekcionizm-v-biznese.webp +0 -0
- package/public/img/examples/blog/previews/post-3-laik-ne-valyuta-kak-avtomatizaciya-marketinga-spasaet-ot-lozhnyh-metrik.webp +0 -0
- package/public/img/examples/blog/previews/post-5-5-fatalnyh-oshibok-marketinga-kotorye-ubivayut-startapy-na-starte.webp +0 -0
- package/public/img/examples/blog/previews/post-6-5-strategii-kontent-marketinga-dlya-startapov-avtomatizaciya-i-revolyuciya.webp +0 -0
- package/public/img/examples/blog/previews/post-7-viralnyi-kontent-ne-udacha-a-strategiya-avtomatizaciya-marketinga.webp +0 -0
- package/public/img/examples/blog/previews/post-agent-experience-mcp-biznes-v-epohu-ii-agentov.webp +0 -0
- package/public/img/examples/blog/previews/post_11.webp +0 -0
- package/public/img/examples/blog/previews/post_12.webp +0 -0
- package/public/img/examples/blog/previews/post_1_jsonld_guide.webp +0 -0
- package/public/img/examples/blog/previews/test-post.webp +0 -0
- package/public/img/examples/blog/previews/tr-post-1.webp +0 -0
- package/public/img/examples/products/previews/product_1.webp +0 -0
- package/public/img/examples/products/previews/product_2.webp +0 -0
- package/public/img/examples/projects/previews/project_1.webp +0 -0
- package/public/img/examples/projects/previews/project_2.webp +0 -0
- package/scripts/generate-previews.js +175 -0
- package/scripts/update-components.js +166 -0
- package/scripts/upgrade-config.js +23 -3
- package/src/components/LanguageSwitcher.astro +2 -16
- package/astro.config.mjs +0 -92
- package/bin/init.js +0 -201
- package/public/img/default/autor_default.webp +0 -0
- package/public/img/default/blog_default.webp +0 -0
- package/public/img/default/default.webp +0 -0
- package/public/img/default/product_default.webp +0 -0
- package/public/img/default/project_default.webp +0 -0
- package/public/img/default/rubric_default.webp +0 -0
- package/public/img/default/test.webp +0 -0
- package/public/img/default/test2.webp +0 -0
- package/resize-all.cjs +0 -29
- package/src/i18n/de.json +0 -126
- package/src/i18n/en.json +0 -126
- package/src/i18n/es.json +0 -126
- package/src/i18n/fr.json +0 -126
- package/src/i18n/index.ts +0 -10
- package/src/i18n/ja.json +0 -126
- package/src/i18n/languages.ts +0 -23
- package/src/i18n/pt.json +0 -126
- package/src/i18n/ru.json +0 -123
- package/src/i18n/zh.json +0 -126
- package/src/icons/ArrowLeft.astro +0 -13
- package/src/icons/ArrowRight.astro +0 -13
- package/src/icons/flags/brazil.svg +0 -14
- package/src/icons/flags/china.svg +0 -15
- package/src/icons/flags/france.svg +0 -12
- package/src/icons/flags/germany.svg +0 -12
- package/src/icons/flags/japan.svg +0 -11
- package/src/icons/flags/russia.svg +0 -12
- package/src/icons/flags/spain.svg +0 -12
- package/src/icons/flags/united arab emirates.svg +0 -13
- package/src/icons/flags/united states.svg +0 -15
- package/src/icons/socials/BlueskyIcon.astro +0 -9
- package/src/icons/socials/EmailIcon.astro +0 -8
- package/src/icons/socials/LinkedinIcon.astro +0 -9
- package/src/icons/socials/MastodonIcon.astro +0 -9
- package/src/icons/socials/MediumIcon.astro +0 -9
- package/src/icons/socials/RedditIcon.astro +0 -11
- package/src/icons/socials/TelegramIcon.astro +0 -11
- package/src/icons/socials/TwitterIcon.astro +0 -9
- package/src/layouts/BaseLayout.astro +0 -59
- package/src/pages/404.astro +0 -24
- package/src/pages/[...id].astro +0 -50
- package/src/pages/about.astro +0 -0
- package/src/pages/authors/[...page].astro +0 -105
- package/src/pages/authors/[id].astro +0 -175
- package/src/pages/blog/[...page].astro +0 -59
- package/src/pages/blog/[id].astro +0 -175
- package/src/pages/index.astro +0 -90
- package/src/pages/products/[...page].astro +0 -50
- package/src/pages/products/[id].astro +0 -221
- package/src/pages/projects/[...page].astro +0 -74
- package/src/pages/projects/[id].astro +0 -165
- package/src/pages/projects/tags/[id]/[...page].astro +0 -58
- package/src/pages/rss.xml.js +0 -5
- package/src/pages/tags/[id]/[...page].astro +0 -110
- package/src/pages/tags/index.astro +0 -124
- package/src/scripts/infoCardFadeIn.js +0 -22
- package/src/styles/global.css +0 -273
- package/src/utils/common-utils.ts +0 -0
- package/src/utils/content-loader.ts +0 -14
- package/src/utils/data-utils.ts +0 -49
- package/src/utils/featuredManager.ts +0 -118
- package/src/utils/posts.ts +0 -43
- package/src/utils/reading-time.ts +0 -28
- package/src/utils/remark-slugify.js +0 -8
- package/src/utils/rss.ts +0 -23
- package/tsconfig.json +0 -8
- package/typograf-batch.js +0 -49
- package/vite.config.js +0 -11
@@ -1,124 +0,0 @@
|
|
1
|
-
---
|
2
|
-
import { getEntry } from 'astro:content';
|
3
|
-
import { getFilteredCollection } from '../../utils/content-loader';
|
4
|
-
import Breadcrumbs from '../../components/Breadcrumbs.astro';
|
5
|
-
import InfoCard from '../../components/InfoCard.astro';
|
6
|
-
import RubricCard from '../../components/RubricCard.astro';
|
7
|
-
import Subscribe from '../../components/Subscribe.astro';
|
8
|
-
import TagPills from '../../components/TagPills.astro';
|
9
|
-
import { maugliConfig } from '../../config/maugli.config';
|
10
|
-
import { LANGUAGES } from '../../i18n/languages';
|
11
|
-
import BaseLayout from '../../layouts/BaseLayout.astro';
|
12
|
-
import { slugify } from '../../utils/common-utils';
|
13
|
-
import { getAllTags, getPostsByTag, sortItemsByDateDesc } from '../../utils/data-utils';
|
14
|
-
|
15
|
-
let posts = (await getFilteredCollection('blog')).sort(sortItemsByDateDesc);
|
16
|
-
const tags = getAllTags(posts).sort((tagA, tagB) => {
|
17
|
-
const postCountTagA = getPostsByTag(posts, tagA.id).length;
|
18
|
-
const postCountTagB = getPostsByTag(posts, tagB.id).length;
|
19
|
-
return postCountTagB - postCountTagA;
|
20
|
-
});
|
21
|
-
|
22
|
-
// Получаем все mdx-файлы рубрик
|
23
|
-
let allRubricFiles = await getFilteredCollection('tags');
|
24
|
-
const rubricSlugs = allRubricFiles.map((r) => r.data.slug);
|
25
|
-
const rubricMeta = Object.fromEntries(allRubricFiles.map((r) => [r.data.slug, r]));
|
26
|
-
|
27
|
-
// Формируем список рубрик только по реально используемым тегам
|
28
|
-
const rubricCards = tags
|
29
|
-
.filter((tag) => rubricSlugs.includes(tag.id))
|
30
|
-
.map((tag) => {
|
31
|
-
const rubric = rubricMeta[tag.id];
|
32
|
-
const postCount = getPostsByTag(posts, tag.id).length;
|
33
|
-
const lastPost = posts
|
34
|
-
.filter((post) => (post.data.tags || []).some((t) => slugify(t) === tag.id))
|
35
|
-
.sort((a, b) => new Date(b.data.publishDate).getTime() - new Date(a.data.publishDate).getTime())[0];
|
36
|
-
const updatedAt = lastPost ? lastPost.data.publishDate?.toString() : undefined;
|
37
|
-
return {
|
38
|
-
slug: rubric.data.slug,
|
39
|
-
title: rubric.data.title,
|
40
|
-
description: rubric.data.description,
|
41
|
-
image: rubric.data.image,
|
42
|
-
postCount,
|
43
|
-
updatedAt,
|
44
|
-
quote: rubric.data.quote,
|
45
|
-
isFeatured: rubric.data.isFeatured
|
46
|
-
};
|
47
|
-
});
|
48
|
-
|
49
|
-
// Обычные теги — только те, для которых нет mdx-файла
|
50
|
-
const simpleTags = tags.filter((tag) => !rubricSlugs.includes(tag.id));
|
51
|
-
|
52
|
-
// Сортируем rubricCards по дате последнего поста (lastPublishDate), самые свежие — выше
|
53
|
-
const rubricCardsSorted = [...rubricCards].sort((a, b) => {
|
54
|
-
const dateA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
55
|
-
const dateB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
56
|
-
return dateB - dateA;
|
57
|
-
});
|
58
|
-
|
59
|
-
const dicts: Record<string, any> = Object.fromEntries(LANGUAGES.map((l) => [l.code, l.dict]));
|
60
|
-
const lang: string = typeof maugliConfig.defaultLang === 'string' && dicts[maugliConfig.defaultLang] ? maugliConfig.defaultLang : 'en';
|
61
|
-
const dict = dicts[lang] || dicts['en'];
|
62
|
-
const pageTitle = dict.pages?.tags?.title || dicts['en'].pages.tags.title;
|
63
|
-
const pageDesc = dict.pages?.tags?.description || dicts['en'].pages.tags.description;
|
64
|
-
const blogRubrics = dict.pages?.tags?.blogRubrics || dicts['en'].pages.tags.blogRubrics;
|
65
|
-
const moreTags = dict.pages?.tags?.moreTags || dicts['en'].pages.tags.moreTags || 'More tags';
|
66
|
-
|
67
|
-
const rubricsSection = await getEntry('pages', 'rubrics');
|
68
|
-
---
|
69
|
-
|
70
|
-
<BaseLayout title={pageTitle} description={pageDesc} showHeader={false} fullWidth={true}>
|
71
|
-
<div class="max-w-[1280px] mx-auto">
|
72
|
-
<Breadcrumbs />
|
73
|
-
|
74
|
-
<h1 class="mb-12 text-3xl font-serif font-[800] sm:mb-16 sm:text-5xl" style="color: var(--text-heading)">{blogRubrics}</h1>
|
75
|
-
|
76
|
-
{/* Карточки рубрик */}
|
77
|
-
{
|
78
|
-
rubricCardsSorted.length > 0 && (
|
79
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-16">
|
80
|
-
{rubricCardsSorted.map((rubric) => (
|
81
|
-
<RubricCard rubric={rubric} />
|
82
|
-
))}
|
83
|
-
</div>
|
84
|
-
)
|
85
|
-
}
|
86
|
-
|
87
|
-
{/* Обычные теги */}
|
88
|
-
{
|
89
|
-
simpleTags.length > 0 && (
|
90
|
-
<h2 class="mb-8 text-2xl sm:text-3xl font-serif font-[700]" style="color: var(--text-heading)">
|
91
|
-
{moreTags}
|
92
|
-
</h2>
|
93
|
-
)
|
94
|
-
}
|
95
|
-
{
|
96
|
-
simpleTags.length > 0 && (
|
97
|
-
<TagPills
|
98
|
-
tags={simpleTags.map((tag) => ({
|
99
|
-
id: tag.id,
|
100
|
-
name: tag.name,
|
101
|
-
count: getPostsByTag(posts, tag.id).length,
|
102
|
-
isRubric: rubricSlugs.includes(tag.id)
|
103
|
-
}))}
|
104
|
-
basePath="/tags"
|
105
|
-
class="mb-10"
|
106
|
-
/>
|
107
|
-
)
|
108
|
-
}
|
109
|
-
</div>
|
110
|
-
{maugliConfig.subscribe?.enabled && <Subscribe class="my-16 sm:my-24" />}
|
111
|
-
|
112
|
-
{/* InfoCard о рубриках внизу страницы */}
|
113
|
-
{
|
114
|
-
rubricsSection && (
|
115
|
-
<div class="max-w-[1280px] mx-auto">
|
116
|
-
<InfoCard title={rubricsSection.data.title} description={rubricsSection.body} image={rubricsSection.data.image} class="mt-16" />
|
117
|
-
</div>
|
118
|
-
)
|
119
|
-
}
|
120
|
-
</BaseLayout>
|
121
|
-
|
122
|
-
<!-- Пример использования TagsSection:
|
123
|
-
<TagsSection tags={tagsWithRubricFlag} totalCount={posts.length} />
|
124
|
-
-->
|
@@ -1,22 +0,0 @@
|
|
1
|
-
// Автоматически добавляет класс для анимации InfoCard при появлении в viewport
|
2
|
-
export function animateInfoCardsOnView() {
|
3
|
-
if (typeof window === 'undefined') return;
|
4
|
-
const cards = document.querySelectorAll('.info-card-fade-in');
|
5
|
-
if (!('IntersectionObserver' in window) || !cards.length) return;
|
6
|
-
|
7
|
-
const observer = new window.IntersectionObserver((entries, obs) => {
|
8
|
-
entries.forEach(entry => {
|
9
|
-
if (entry.isIntersecting) {
|
10
|
-
entry.target.classList.add('info-card-fade-in--active');
|
11
|
-
obs.unobserve(entry.target);
|
12
|
-
}
|
13
|
-
});
|
14
|
-
}, { threshold: 0.15 });
|
15
|
-
|
16
|
-
cards.forEach(card => observer.observe(card));
|
17
|
-
}
|
18
|
-
|
19
|
-
// Для автоинициализации на всех страницах, если нужно:
|
20
|
-
if (typeof window !== 'undefined') {
|
21
|
-
window.addEventListener('DOMContentLoaded', animateInfoCardsOnView);
|
22
|
-
}
|
package/src/styles/global.css
DELETED
@@ -1,273 +0,0 @@
|
|
1
|
-
/* Import fonts. Replace or add lines to use other Fontsource packages. */
|
2
|
-
@import '@fontsource-variable/inter/wght.css' layer(base);
|
3
|
-
@import '@fontsource-variable/geologica/wght.css' layer(base);
|
4
|
-
|
5
|
-
@import 'tailwindcss';
|
6
|
-
@plugin '@tailwindcss/typography';
|
7
|
-
|
8
|
-
@custom-variant dark (&:where(.dark, .dark *));
|
9
|
-
|
10
|
-
@layer base {
|
11
|
-
:root {
|
12
|
-
/* Brand */
|
13
|
-
--brand-color-rgb: 12, 191, 17;
|
14
|
-
--brand-color: rgb(var(--brand-color-rgb));
|
15
|
-
--brand-color-60: rgba(var(--brand-color-rgb), 0.6);
|
16
|
-
--brand-color-40: rgba(var(--brand-color-rgb), 0.4);
|
17
|
-
--brand-color-20: rgba(var(--brand-color-rgb), 0.1);
|
18
|
-
|
19
|
-
/* Light Theme */
|
20
|
-
--text-main: #111c2d;
|
21
|
-
--text-heading: #3b5174;
|
22
|
-
--text-muted: #6b7280;
|
23
|
-
--bg-main: #ffffff;
|
24
|
-
--bg-main-40: rgba(255, 255, 255, 0.4);
|
25
|
-
--bg-muted: rgba(237, 241, 247, 0.621);
|
26
|
-
--border-main: rgba(17, 28, 44, 0.13);
|
27
|
-
--font-sans: 'Inter Variable', sans-serif; /* Update if you change the sans-serif font */
|
28
|
-
--font-serif: 'Geologica Variable', sans-serif; /* Update if you change the serif font */
|
29
|
-
|
30
|
-
/* Card Styles */
|
31
|
-
--card-shadow:
|
32
|
-
0px 26.6953px 12.9008px -4px #8b8da80d, 0px 22.5312px 10.1812px -4px #8b8da80d, 0px 18.6953px 7.80859px -4px #8b8da60d,
|
33
|
-
0px 15.375px 5.75px -4px #8b8da80d, 0px 12.7578px 3.97266px -4px #8b8da60d, 0px 11.0312px 2.44375px -4px #8b8da60d,
|
34
|
-
0px 10.3828px 1.13047px -4px #8b8da60d;
|
35
|
-
--card-blur: blur(10px);
|
36
|
-
--card-bg: var(--bg-muted);
|
37
|
-
|
38
|
-
/* Rounded Styles */
|
39
|
-
--rounded-circle: 50%;
|
40
|
-
--rounded-custom: 8px 24px 8px 24px;
|
41
|
-
--rounded-normal: 4px;
|
42
|
-
}
|
43
|
-
|
44
|
-
html.dark {
|
45
|
-
/* Brand */
|
46
|
-
--brand-color-rgb: 13, 211, 18;
|
47
|
-
--brand-color: rgb(var(--brand-color-rgb));
|
48
|
-
--brand-color-60: rgba(var(--brand-color-rgb), 0.6);
|
49
|
-
--brand-color-40: rgba(var(--brand-color-rgb), 0.4);
|
50
|
-
--brand-color-20: rgba(var(--brand-color-rgb), 0.3);
|
51
|
-
|
52
|
-
/* Dark Theme */
|
53
|
-
--text-main: #ffffff;
|
54
|
-
--text-heading: rgba(202, 252, 254, 0.7);
|
55
|
-
--text-muted: #9ca3af;
|
56
|
-
--bg-main: #0b131e;
|
57
|
-
--bg-main-40: rgba(11, 19, 30, 0.4);
|
58
|
-
--bg-muted: #131d2cde;
|
59
|
-
--border-main: rgba(51, 66, 66, 0.6);
|
60
|
-
|
61
|
-
/* Card Styles */
|
62
|
-
--card-shadow:
|
63
|
-
0px 26.6953px 12.9008px -4px rgba(12, 12, 12, 0.0945547), 0px 22.5312px 10.1812px -4px rgba(12, 12, 12, 0.073125),
|
64
|
-
0px 18.6953px 7.80859px -4px rgba(12, 12, 12, 0.0614453), 0px 15.375px 5.75px -4px rgba(12, 12, 12, 0.05525),
|
65
|
-
0px 12.7578px 3.97266px -4px rgba(12, 12, 12, 0.0502734), 0px 11.0312px 2.44375px -4px rgba(12, 12, 12, 0.04225),
|
66
|
-
0px 10.3828px 1.13047px -4px rgba(12, 12, 12, 0.0269141);
|
67
|
-
--card-blur: blur(8px);
|
68
|
-
--card-bg: var(--bg-muted);
|
69
|
-
}
|
70
|
-
}
|
71
|
-
/* Cyan */
|
72
|
-
/*
|
73
|
-
:root {
|
74
|
-
--text-main: #162a2b;
|
75
|
-
--bg-main: #d6e0e2;
|
76
|
-
--bg-muted: #ccd8db;
|
77
|
-
--border-main: #162a2b;
|
78
|
-
}
|
79
|
-
html.dark {
|
80
|
-
--text-main: #d6e0e2;
|
81
|
-
--bg-main: #162a2b;
|
82
|
-
--bg-muted: #1c3537;
|
83
|
-
--border-main: #d6e0e2;
|
84
|
-
}
|
85
|
-
*/
|
86
|
-
|
87
|
-
/* Green */
|
88
|
-
/*
|
89
|
-
:root {
|
90
|
-
--text-main: #3a4238;
|
91
|
-
--bg-main: #f3efe6;
|
92
|
-
--bg-muted: #eee9dc;
|
93
|
-
--border-main: #3a4238;
|
94
|
-
}
|
95
|
-
html.dark {
|
96
|
-
--text-main: #f3efe6;
|
97
|
-
--bg-main: #5e6c5b;
|
98
|
-
--bg-muted: #596756;
|
99
|
-
--border-main: #f3efe6;
|
100
|
-
}
|
101
|
-
*/
|
102
|
-
|
103
|
-
@theme inline {
|
104
|
-
--font-sans: 'Inter Variable', sans-serif; /* Match --font-sans above if changed */
|
105
|
-
--font-serif: 'Geologica Variable', sans-serif; /* Match --font-serif above if changed */
|
106
|
-
--font-weight-bold: 700;
|
107
|
-
--font-weight-extra-bold: 800;
|
108
|
-
--text-color-main: var(--text-main);
|
109
|
-
--text-color-heading: var(--text-heading);
|
110
|
-
--background-color-main: var(--bg-main);
|
111
|
-
--background-color-muted: var(--bg-muted);
|
112
|
-
--border-color-main: var(--border-main);
|
113
|
-
--color-brand: var(--brand-color);
|
114
|
-
--color-brand-60: var(--brand-color-60);
|
115
|
-
--color-brand-20: var(--brand-color-20);
|
116
|
-
}
|
117
|
-
|
118
|
-
@layer utilities {
|
119
|
-
/* Кастомные скругления для плашек */
|
120
|
-
.rounded-custom {
|
121
|
-
border-radius: var(--rounded-custom);
|
122
|
-
}
|
123
|
-
|
124
|
-
/* Обычные скругления для тегов и форм */
|
125
|
-
.rounded-normal {
|
126
|
-
border-radius: var(--rounded-normal);
|
127
|
-
}
|
128
|
-
|
129
|
-
.rounded-circle {
|
130
|
-
border-radius: var(--rounded-circle);
|
131
|
-
}
|
132
|
-
|
133
|
-
.prose {
|
134
|
-
--tw-prose-body: var(--text-color-main);
|
135
|
-
--tw-prose-headings: var(--text-color-heading);
|
136
|
-
--tw-prose-lead: var(--text-color-main);
|
137
|
-
--tw-prose-links: var(--text-color-main);
|
138
|
-
--tw-prose-bold: var(--text-color-main);
|
139
|
-
--tw-prose-counters: var(--text-color-main);
|
140
|
-
--tw-prose-bullets: var(--text-color-main);
|
141
|
-
--tw-prose-hr: var(--border-color-main);
|
142
|
-
--tw-prose-quotes: var(--text-color-main);
|
143
|
-
--tw-prose-quote-borders: var(--border-color-main);
|
144
|
-
--tw-prose-captions: var(--text-color-main);
|
145
|
-
--tw-prose-kbd: var(--text-color-main);
|
146
|
-
--tw-prose-code: var(--text-color-main);
|
147
|
-
--tw-prose-th-borders: var(--border-color-main);
|
148
|
-
--tw-prose-td-borders: var(--border-color-main);
|
149
|
-
}
|
150
|
-
|
151
|
-
.prose a {
|
152
|
-
/* background: var(--brand-color-20); */ /* Фон под ссылкой */
|
153
|
-
/* border-radius: 8px 24px 8px 24px; */ /* кастомные скругления */
|
154
|
-
/* padding: 4px 12px; немного отступов для видимости фона */
|
155
|
-
text-decoration: none; /* убираем подчеркивание */
|
156
|
-
color: var(--color-brand);
|
157
|
-
/* transition: background-color 0.2s ease; */ /* плавный переход */
|
158
|
-
}
|
159
|
-
|
160
|
-
.prose a:hover {
|
161
|
-
/* background: var(--brand-color-60); */ /* более яркий фон при наведении */
|
162
|
-
text-decoration: underline; /* подчеркивание при наведении */
|
163
|
-
}
|
164
|
-
|
165
|
-
.prose :where(h1, h2, h3, h4, h5, h6) {
|
166
|
-
@apply font-serif font-[800];
|
167
|
-
line-height: 1.05 !important; /* 105% для заголовков */
|
168
|
-
}
|
169
|
-
|
170
|
-
.prose blockquote {
|
171
|
-
@apply font-serif font-[300] px-6 py-3 my-8;
|
172
|
-
background: var(--bg-muted); /* серый фон как у плашки */
|
173
|
-
border: 0px solid var(--border-main); /* прямые границы */
|
174
|
-
color: var(--text-main); /* тёмный текст */
|
175
|
-
font-size: 1.08em;
|
176
|
-
box-shadow: none;
|
177
|
-
margin-left: 0; /* чтобы не было сильного сдвига влево */
|
178
|
-
padding: 1em;
|
179
|
-
padding-left: 3em;
|
180
|
-
|
181
|
-
margin-right: 0;
|
182
|
-
border-radius: 8px 24px 8px 24px; /* кастомные скругления */
|
183
|
-
}
|
184
|
-
.prose blockquote p {
|
185
|
-
@apply m-0;
|
186
|
-
}
|
187
|
-
.prose blockquote p:first-child::before,
|
188
|
-
.prose blockquote p:last-child::after,
|
189
|
-
.prose blockquote::before,
|
190
|
-
.prose blockquote::after {
|
191
|
-
content: none !important;
|
192
|
-
}
|
193
|
-
|
194
|
-
.prose p,
|
195
|
-
.prose li,
|
196
|
-
.prose ul,
|
197
|
-
.prose ol {
|
198
|
-
line-height: 1.4 !important; /* 130% для body */
|
199
|
-
}
|
200
|
-
|
201
|
-
/* Sticky positioning support and fixes */
|
202
|
-
@supports (position: sticky) {
|
203
|
-
.sticky-container {
|
204
|
-
height: 100vh;
|
205
|
-
overflow-y: auto;
|
206
|
-
}
|
207
|
-
}
|
208
|
-
|
209
|
-
/* Ensure sticky positioning works in all browsers */
|
210
|
-
.sticky-toc {
|
211
|
-
position: -webkit-sticky !important;
|
212
|
-
position: sticky !important;
|
213
|
-
top: 2rem !important;
|
214
|
-
align-self: flex-start !important;
|
215
|
-
z-index: 10;
|
216
|
-
}
|
217
|
-
|
218
|
-
/* Grid layout for ToC */
|
219
|
-
.toc-grid {
|
220
|
-
display: grid;
|
221
|
-
grid-template-columns: minmax(320px, 360px) 1fr;
|
222
|
-
gap: 2.25rem;
|
223
|
-
align-items: start;
|
224
|
-
}
|
225
|
-
|
226
|
-
@media (max-width: 1023px) {
|
227
|
-
.toc-grid {
|
228
|
-
display: block;
|
229
|
-
}
|
230
|
-
}
|
231
|
-
|
232
|
-
.card-shadow {
|
233
|
-
box-shadow: var(--card-shadow) !important;
|
234
|
-
}
|
235
|
-
.card-blur {
|
236
|
-
backdrop-filter: var(--card-blur) !important;
|
237
|
-
-webkit-backdrop-filter: var(--card-blur) !important;
|
238
|
-
}
|
239
|
-
.card-bg {
|
240
|
-
background: var(--card-bg) !important;
|
241
|
-
}
|
242
|
-
|
243
|
-
/* Custom grid template for 16 columns */
|
244
|
-
.grid-cols-16 {
|
245
|
-
grid-template-columns: repeat(16, minmax(0, 1fr));
|
246
|
-
}
|
247
|
-
}
|
248
|
-
|
249
|
-
@layer base {
|
250
|
-
html {
|
251
|
-
background-color: var(--bg-main); /* Фон для html элемента */
|
252
|
-
/* Предотвращаем overscroll в Safari */
|
253
|
-
overscroll-behavior: none;
|
254
|
-
-webkit-overflow-scrolling: touch;
|
255
|
-
}
|
256
|
-
|
257
|
-
body {
|
258
|
-
min-height: 100vh;
|
259
|
-
background-color: var(--bg-main); /* Фон для body элемента */
|
260
|
-
/* Предотвращаем overscroll в Safari */
|
261
|
-
overscroll-behavior: none;
|
262
|
-
}
|
263
|
-
|
264
|
-
/* Предотвращаем переполнение контейнеров */
|
265
|
-
* {
|
266
|
-
box-sizing: border-box;
|
267
|
-
}
|
268
|
-
|
269
|
-
/* Custom grid template for 16 columns */
|
270
|
-
.grid-cols-16 {
|
271
|
-
grid-template-columns: repeat(16, minmax(0, 1fr));
|
272
|
-
}
|
273
|
-
}
|
Binary file
|
@@ -1,14 +0,0 @@
|
|
1
|
-
import { getCollection, type CollectionEntry, type AnyEntryMap } from 'astro:content';
|
2
|
-
import { maugliConfig } from '../config/maugli.config';
|
3
|
-
|
4
|
-
/**
|
5
|
-
* Load a collection and filter out demo/example content when showExamples is disabled.
|
6
|
-
* @param name Name of the collection
|
7
|
-
*/
|
8
|
-
export async function getFilteredCollection<C extends keyof AnyEntryMap>(name: C): Promise<CollectionEntry<C>[]> {
|
9
|
-
const items = await getCollection(name);
|
10
|
-
if (maugliConfig.showExamples) {
|
11
|
-
return items as CollectionEntry<C>[];
|
12
|
-
}
|
13
|
-
return items.filter((item: any) => !item.data.isExample) as CollectionEntry<C>[];
|
14
|
-
}
|
package/src/utils/data-utils.ts
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
import { type CollectionEntry } from 'astro:content';
|
2
|
-
import { maugliConfig } from '../config/maugli.config';
|
3
|
-
import { slugify } from './common-utils';
|
4
|
-
|
5
|
-
export function sortItemsByDateDesc(itemA: CollectionEntry<'blog' | 'projects'>, itemB: CollectionEntry<'blog' | 'projects'>) {
|
6
|
-
return new Date(itemB.data.publishDate).getTime() - new Date(itemA.data.publishDate).getTime();
|
7
|
-
}
|
8
|
-
|
9
|
-
export function sortItemsWithFeaturedFirst(itemA: CollectionEntry<'blog' | 'projects'>, itemB: CollectionEntry<'blog' | 'projects'>) {
|
10
|
-
// Сначала сортируем по featured (featured посты идут первыми)
|
11
|
-
if (itemA.data.isFeatured && !itemB.data.isFeatured) {
|
12
|
-
return -1; // A идет первым
|
13
|
-
}
|
14
|
-
if (!itemA.data.isFeatured && itemB.data.isFeatured) {
|
15
|
-
return 1; // B идет первым
|
16
|
-
}
|
17
|
-
|
18
|
-
// Если оба featured или оба не featured, сортируем по дате
|
19
|
-
return new Date(itemB.data.publishDate).getTime() - new Date(itemA.data.publishDate).getTime();
|
20
|
-
}
|
21
|
-
|
22
|
-
export function getAllTags(posts: CollectionEntry<'blog'>[]) {
|
23
|
-
const tags: string[] = [...new Set(posts.flatMap((post) => post.data.tags || []).filter(Boolean))];
|
24
|
-
return tags
|
25
|
-
.map((tag) => {
|
26
|
-
return {
|
27
|
-
name: tag,
|
28
|
-
id: slugify(tag)
|
29
|
-
};
|
30
|
-
})
|
31
|
-
.filter((obj, pos, arr) => {
|
32
|
-
return arr.map((mapObj) => mapObj.id).indexOf(obj.id) === pos;
|
33
|
-
});
|
34
|
-
}
|
35
|
-
|
36
|
-
export function getPostsByTag(posts: CollectionEntry<'blog'>[], tagId: string) {
|
37
|
-
const filteredPosts: CollectionEntry<'blog'>[] = posts.filter((post) => (post.data.tags || []).map((tag) => slugify(tag)).includes(tagId));
|
38
|
-
return filteredPosts;
|
39
|
-
}
|
40
|
-
|
41
|
-
// Получение автора поста с fallback на дефолтного автора из конфига
|
42
|
-
export function getPostAuthor(post: CollectionEntry<'blog'>): string {
|
43
|
-
return post.data.author || maugliConfig.defaultAuthorId || 'unknown-author';
|
44
|
-
}
|
45
|
-
|
46
|
-
// Получение постов по автору с учетом дефолтного автора
|
47
|
-
export function getPostsByAuthor(posts: CollectionEntry<'blog'>[], authorId: string) {
|
48
|
-
return posts.filter(post => getPostAuthor(post) === authorId);
|
49
|
-
}
|
@@ -1,118 +0,0 @@
|
|
1
|
-
import { getCollection } from 'astro:content';
|
2
|
-
import fs from 'fs/promises';
|
3
|
-
import path from 'path';
|
4
|
-
|
5
|
-
/**
|
6
|
-
* Универсальный менеджер featured-элементов для коллекций: blog, products, projects
|
7
|
-
*/
|
8
|
-
export class FeaturedManager {
|
9
|
-
private static readonly MAX_FEATURED = 3;
|
10
|
-
private static readonly CONTENT_DIRS = {
|
11
|
-
blog: './src/content/blog',
|
12
|
-
products: './src/content/products',
|
13
|
-
projects: './src/content/projects',
|
14
|
-
};
|
15
|
-
|
16
|
-
/**
|
17
|
-
* Добавляет элемент в featured и управляет лимитом
|
18
|
-
*/
|
19
|
-
static async addFeatured(collection: 'blog' | 'products' | 'projects', newId: string): Promise<void> {
|
20
|
-
try {
|
21
|
-
// Получаем все элементы коллекции
|
22
|
-
const allItems = await getCollection(collection);
|
23
|
-
// Находим текущие featured, сортируем по дате (старые первые)
|
24
|
-
const currentFeatured = allItems
|
25
|
-
.filter(item => item.data.isFeatured)
|
26
|
-
.sort((a, b) => a.data.publishDate.getTime() - b.data.publishDate.getTime());
|
27
|
-
// Если уже есть MAX_FEATURED, удаляем самый старый
|
28
|
-
if (currentFeatured.length >= this.MAX_FEATURED) {
|
29
|
-
const oldestFeatured = currentFeatured[0];
|
30
|
-
await this.removeFeatured(collection, oldestFeatured.id);
|
31
|
-
console.log(`🗑️ Убрали из featured самый старый: ${oldestFeatured.id}`);
|
32
|
-
}
|
33
|
-
// Добавляем новый элемент в featured
|
34
|
-
await this.setFeaturedStatus(collection, newId, true);
|
35
|
-
console.log(`⭐ Добавили в featured: ${newId}`);
|
36
|
-
// Выводим текущий список featured
|
37
|
-
await this.logCurrentFeatured(collection);
|
38
|
-
} catch (error) {
|
39
|
-
console.error('Ошибка при управлении featured:', error);
|
40
|
-
}
|
41
|
-
}
|
42
|
-
|
43
|
-
/**
|
44
|
-
* Убирает элемент из featured
|
45
|
-
*/
|
46
|
-
static async removeFeatured(collection: 'blog' | 'products' | 'projects', id: string): Promise<void> {
|
47
|
-
await this.setFeaturedStatus(collection, id, false);
|
48
|
-
}
|
49
|
-
|
50
|
-
/**
|
51
|
-
* Изменяет статус isFeatured в frontmatter файла
|
52
|
-
*/
|
53
|
-
private static async setFeaturedStatus(collection: 'blog' | 'products' | 'projects', id: string, isFeatured: boolean): Promise<void> {
|
54
|
-
const dir = this.CONTENT_DIRS[collection];
|
55
|
-
const filePath = path.join(dir, `${id}.md`);
|
56
|
-
try {
|
57
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
58
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
59
|
-
if (!frontmatterMatch) {
|
60
|
-
throw new Error(`Не найден frontmatter в файле: ${filePath}`);
|
61
|
-
}
|
62
|
-
let [, frontmatter, body] = frontmatterMatch;
|
63
|
-
if (frontmatter.includes('isFeatured:')) {
|
64
|
-
frontmatter = frontmatter.replace(
|
65
|
-
/isFeatured:\s*(true|false)/,
|
66
|
-
`isFeatured: ${isFeatured}`
|
67
|
-
);
|
68
|
-
} else {
|
69
|
-
if (frontmatter.includes('title:')) {
|
70
|
-
frontmatter = frontmatter.replace(
|
71
|
-
/(title:.*\n)/,
|
72
|
-
`$1isFeatured: ${isFeatured}\n`
|
73
|
-
);
|
74
|
-
} else {
|
75
|
-
frontmatter = `${frontmatter.trim()}\nisFeatured: ${isFeatured}`;
|
76
|
-
}
|
77
|
-
}
|
78
|
-
const updatedContent = `---\n${frontmatter}\n---\n${body}`;
|
79
|
-
await fs.writeFile(filePath, updatedContent, 'utf-8');
|
80
|
-
} catch (error) {
|
81
|
-
console.error(`Ошибка при обновлении файла ${filePath}:`, error);
|
82
|
-
}
|
83
|
-
}
|
84
|
-
|
85
|
-
/**
|
86
|
-
* Выводит текущий список featured элементов
|
87
|
-
*/
|
88
|
-
static async logCurrentFeatured(collection: 'blog' | 'products' | 'projects'): Promise<void> {
|
89
|
-
try {
|
90
|
-
const allItems = await getCollection(collection);
|
91
|
-
const featured = allItems
|
92
|
-
.filter(item => item.data.isFeatured)
|
93
|
-
.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime());
|
94
|
-
console.log(`\n📌 Текущие featured (${collection}):`);
|
95
|
-
featured.forEach((item, index) => {
|
96
|
-
console.log(`${index + 1}. ${item.id} (${item.data.publishDate.toLocaleDateString()})`);
|
97
|
-
});
|
98
|
-
console.log(`📊 Всего featured: ${featured.length}/${this.MAX_FEATURED}\n`);
|
99
|
-
} catch (error) {
|
100
|
-
console.error('Ошибка при получении featured:', error);
|
101
|
-
}
|
102
|
-
}
|
103
|
-
|
104
|
-
/**
|
105
|
-
* Получает все featured элементы для отображения
|
106
|
-
*/
|
107
|
-
static async getFeaturedItems(collection: 'blog' | 'products' | 'projects') {
|
108
|
-
try {
|
109
|
-
const allItems = await getCollection(collection);
|
110
|
-
return allItems
|
111
|
-
.filter(item => item.data.isFeatured)
|
112
|
-
.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime());
|
113
|
-
} catch (error) {
|
114
|
-
console.error('Ошибка при получении featured:', error);
|
115
|
-
return [];
|
116
|
-
}
|
117
|
-
}
|
118
|
-
}
|
package/src/utils/posts.ts
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
import { getFilteredCollection } from './content-loader';
|
2
|
-
|
3
|
-
/**
|
4
|
-
* Получает featured посты для отображения на главной странице
|
5
|
-
*/
|
6
|
-
export async function getFeaturedPosts() {
|
7
|
-
try {
|
8
|
-
const allPosts = await getFilteredCollection('blog');
|
9
|
-
return allPosts
|
10
|
-
.filter((post) => post.data.isFeatured)
|
11
|
-
.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime())
|
12
|
-
.slice(0, 3); // Максимум 3 featured поста
|
13
|
-
} catch (error) {
|
14
|
-
console.error('Ошибка при получении featured постов:', error);
|
15
|
-
return [];
|
16
|
-
}
|
17
|
-
}
|
18
|
-
|
19
|
-
/**
|
20
|
-
* Получает все посты (не featured) для отображения в блоге
|
21
|
-
*/
|
22
|
-
export async function getAllPosts() {
|
23
|
-
try {
|
24
|
-
const allPosts = await getFilteredCollection('blog');
|
25
|
-
return allPosts.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime());
|
26
|
-
} catch (error) {
|
27
|
-
console.error('Ошибка при получении постов:', error);
|
28
|
-
return [];
|
29
|
-
}
|
30
|
-
}
|
31
|
-
|
32
|
-
/**
|
33
|
-
* Получает только обычные посты (не featured)
|
34
|
-
*/
|
35
|
-
export async function getRegularPosts() {
|
36
|
-
try {
|
37
|
-
const allPosts = await getFilteredCollection('blog');
|
38
|
-
return allPosts.filter((post) => !post.data.isFeatured).sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime());
|
39
|
-
} catch (error) {
|
40
|
-
console.error('Ошибка при получении обычных постов:', error);
|
41
|
-
return [];
|
42
|
-
}
|
43
|
-
}
|
@@ -1,28 +0,0 @@
|
|
1
|
-
/**
|
2
|
-
* Вычисляет время чтения статьи на основе количества слов
|
3
|
-
* @param content - HTML или markdown контент
|
4
|
-
* @returns время чтения в формате "X мин"
|
5
|
-
*/
|
6
|
-
export function calculateReadingTime(content: string): string {
|
7
|
-
// Убираем HTML теги и markdown разметку
|
8
|
-
const plainText = content
|
9
|
-
.replace(/<[^>]*>/g, '') // убираем HTML теги
|
10
|
-
.replace(/#{1,6}\s+/g, '') // убираем markdown заголовки
|
11
|
-
.replace(/\*{1,2}([^*]+)\*{1,2}/g, '$1') // убираем markdown жирный/курсив
|
12
|
-
.replace(/`([^`]+)`/g, '$1') // убираем код в бэктиках
|
13
|
-
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // убираем markdown ссылки
|
14
|
-
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '') // убираем markdown изображения
|
15
|
-
.trim();
|
16
|
-
|
17
|
-
// Считаем слова (разделяем по пробелам и фильтруем пустые)
|
18
|
-
const words = plainText.split(/\s+/).filter(word => word.length > 0);
|
19
|
-
const wordCount = words.length;
|
20
|
-
|
21
|
-
// Средняя скорость чтения на русском языке: 200-250 слов в минуту
|
22
|
-
// Используем 220 слов в минуту как среднее значение
|
23
|
-
const wordsPerMinute = 220;
|
24
|
-
const minutes = Math.ceil(wordCount / wordsPerMinute);
|
25
|
-
|
26
|
-
// Минимум 1 минута
|
27
|
-
return `${Math.max(1, minutes)} `;
|
28
|
-
}
|