diff --git a/backend/pb_migrations/1776514159_created_comments.js b/backend/pb_migrations/1776514159_created_comments.js
new file mode 100644
index 0000000..00f2597
--- /dev/null
+++ b/backend/pb_migrations/1776514159_created_comments.js
@@ -0,0 +1,126 @@
+///
+migrate((app) => {
+ const collection = new Collection({
+ "createRule": null,
+ "deleteRule": null,
+ "fields": [
+ {
+ "autogeneratePattern": "[a-z0-9]{15}",
+ "hidden": false,
+ "id": "text3208210256",
+ "max": 15,
+ "min": 15,
+ "name": "id",
+ "pattern": "^[a-z0-9]+$",
+ "presentable": false,
+ "primaryKey": true,
+ "required": true,
+ "system": true,
+ "type": "text"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text4274335913",
+ "max": 0,
+ "min": 0,
+ "name": "content",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text1372126313",
+ "max": 0,
+ "min": 0,
+ "name": "post_slug",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "cascadeDelete": false,
+ "collectionId": "_pb_users_auth_",
+ "hidden": false,
+ "id": "relation2375276105",
+ "maxSelect": 1,
+ "minSelect": 0,
+ "name": "user",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "relation"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text1032740943",
+ "max": 0,
+ "min": 0,
+ "name": "parent",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text2063623452",
+ "max": 0,
+ "min": 0,
+ "name": "status",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "hidden": false,
+ "id": "autodate2990389176",
+ "name": "created",
+ "onCreate": true,
+ "onUpdate": false,
+ "presentable": false,
+ "system": false,
+ "type": "autodate"
+ },
+ {
+ "hidden": false,
+ "id": "autodate3332085495",
+ "name": "updated",
+ "onCreate": true,
+ "onUpdate": true,
+ "presentable": false,
+ "system": false,
+ "type": "autodate"
+ }
+ ],
+ "id": "pbc_533777971",
+ "indexes": [],
+ "listRule": null,
+ "name": "comments",
+ "system": false,
+ "type": "base",
+ "updateRule": null,
+ "viewRule": null
+ });
+
+ return app.save(collection);
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_533777971");
+
+ return app.delete(collection);
+})
diff --git a/backend/pb_migrations/1776514585_updated_comments.js b/backend/pb_migrations/1776514585_updated_comments.js
new file mode 100644
index 0000000..d7e21f7
--- /dev/null
+++ b/backend/pb_migrations/1776514585_updated_comments.js
@@ -0,0 +1,28 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_533777971")
+
+ // update collection data
+ unmarshal({
+ "createRule": "@request.auth.id != \"\"",
+ "deleteRule": "user.id = @request.auth.id ",
+ "listRule": " status = \"published\" || @request.auth.id != \"\" ",
+ "updateRule": " user.id = @request.auth.id ",
+ "viewRule": " status = \"published\" || @request.auth.id != \"\" "
+ }, collection)
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_533777971")
+
+ // update collection data
+ unmarshal({
+ "createRule": null,
+ "deleteRule": null,
+ "listRule": null,
+ "updateRule": null,
+ "viewRule": null
+ }, collection)
+
+ return app.save(collection)
+})
diff --git a/bun.lockb b/bun.lockb
new file mode 100644
index 0000000..7a2b718
Binary files /dev/null and b/bun.lockb differ
diff --git a/frontend/astro.config.mjs b/frontend/astro.config.mjs
index f25366d..32b6d47 100644
--- a/frontend/astro.config.mjs
+++ b/frontend/astro.config.mjs
@@ -5,6 +5,7 @@ import node from '@astrojs/node';
import mdx from '@astrojs/mdx';
import icon from "astro-icon";
import sitemap from '@astrojs/sitemap';
+import solidJs from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
@@ -14,7 +15,7 @@ export default defineConfig({
const blockedPaths = ['/auth/', '/blog/search', '/404'];
return !blockedPaths.some(path => page.includes(path));
},
- })],
+ }), solidJs()],
vite: {
plugins: [tailwindcss()],
build: {
diff --git a/frontend/package.json b/frontend/package.json
index 9fa0dc3..1300204 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,14 +13,16 @@
},
"dependencies": {
"@astrojs/mdx": "^5.0.3",
- "nodemailer": "^6.9.14",
"@astrojs/node": "^10.0.4",
"@astrojs/sitemap": "^3.7.2",
+ "@astrojs/solid-js": "^6.0.1",
"@tailwindcss/vite": "^4.2.2",
"astro": "^6.0.8",
"astro-icon": "^1.1.5",
"marked": "^18.0.0",
+ "nodemailer": "^6.9.14",
"pocketbase": "^0.21.0",
+ "solid-js": "^1.9.12",
"tailwindcss": "^4.2.2"
},
"devDependencies": {
diff --git a/frontend/src/components/blog/BlogCard.astro b/frontend/src/components/blog/BlogCard.astro
index 8f050d7..0509663 100644
--- a/frontend/src/components/blog/BlogCard.astro
+++ b/frontend/src/components/blog/BlogCard.astro
@@ -6,6 +6,7 @@ export interface Props {
categoryColor?: string;
date: string;
readTime: string;
+ readmeTime?: string;
image?: string;
slug?: string;
}
@@ -17,6 +18,7 @@ const {
categoryColor = 'bg-gold',
date,
readTime,
+ readmeTime,
image,
slug = '#'
} = Astro.props;
@@ -52,7 +54,7 @@ const imageUrl = image || '/images/blog/default.avif';
- {readTime}
+ {readmeTime || readTime}
diff --git a/frontend/src/components/blog/PostReactionButtons.astro b/frontend/src/components/blog/PostReactionButtons.astro
index 9446a03..8180083 100644
--- a/frontend/src/components/blog/PostReactionButtons.astro
+++ b/frontend/src/components/blog/PostReactionButtons.astro
@@ -224,18 +224,25 @@ const { initialLikes = 0, initialDislikes = 0, postId } = Astro.props;
const dislikeBtn = container.querySelector('.dislike-btn');
async function loadUserVote() {
- if (!pb.authStore.isValid) return;
-
try {
+ console.log('[Vote] Загрузка голосов для post:', postId);
const response = await fetch(`/api/votes?post_id=${postId}`, {
credentials: 'include',
});
+ console.log('[Vote] Ответ:', response.status);
if (response.ok) {
const result = await response.json();
- if (result.userVote === 'like') {
- likeBtn?.classList.add('active');
- } else if (result.userVote === 'dislike') {
- dislikeBtn?.classList.add('active');
+ console.log('[Vote] Данные:', result);
+ // Обновляем счетчики
+ if (likesCount) likesCount.textContent = result.likes.toString();
+ if (dislikesCount) dislikesCount.textContent = result.dislikes.toString();
+ // Показываем голос пользователя
+ if (pb.authStore.isValid) {
+ if (result.userVote === 'like') {
+ likeBtn?.classList.add('active');
+ } else if (result.userVote === 'dislike') {
+ dislikeBtn?.classList.add('active');
+ }
}
}
} catch (e) {
@@ -243,7 +250,15 @@ const { initialLikes = 0, initialDislikes = 0, postId } = Astro.props;
}
}
+ // Вызываем при загрузке и при изменении авторизации
loadUserVote();
+
+ // Также при изменении авторизации
+ window.addEventListener('storage', (e) => {
+ if (e.key === 'pb_auth') {
+ loadUserVote();
+ }
+ });
likeBtn?.addEventListener('click', () => {
if (postId) handleVote(postId, 'like');
diff --git a/frontend/src/components/blog/RelatedPosts.astro b/frontend/src/components/blog/RelatedPosts.astro
index 93b30f6..5e8da5b 100644
--- a/frontend/src/components/blog/RelatedPosts.astro
+++ b/frontend/src/components/blog/RelatedPosts.astro
@@ -11,6 +11,7 @@ interface Post {
categoryColor: string;
date: string;
readTime: string;
+ readmeTime?: string;
image: string;
}
@@ -48,6 +49,7 @@ const filteredPosts = currentSlug
categoryColor={post.categoryColor}
date={formatDate(post.date)}
readTime={post.readTime}
+ readmeTime={post.readmeTime}
image={getPostImageUrl(post)}
slug={`/blog/${post.slug}`}
/>
diff --git a/frontend/src/components/blog/comments/CommentForm.tsx b/frontend/src/components/blog/comments/CommentForm.tsx
new file mode 100644
index 0000000..ad092d6
--- /dev/null
+++ b/frontend/src/components/blog/comments/CommentForm.tsx
@@ -0,0 +1,197 @@
+import { createSignal, Show } from "solid-js";
+
+interface CommentFormData {
+ content: string;
+}
+
+interface ValidationErrors {
+ content?: string;
+}
+
+interface CommentFormProps {
+ onSubmit: (data: CommentFormData) => void;
+ isReply?: boolean;
+ onCancel?: () => void;
+ user?: {
+ name: string;
+ email: string;
+ avatar?: string;
+ };
+}
+
+const MAX_MESSAGE_LENGTH = 2000;
+const MIN_MESSAGE_LENGTH = 10;
+
+const DANGEROUS_PATTERNS = [
+ /