diff --git a/backend/pb_migrations/1777232002_created_post_views.js b/backend/pb_migrations/1777232002_created_post_views.js
new file mode 100644
index 0000000..37ad7c4
--- /dev/null
+++ b/backend/pb_migrations/1777232002_created_post_views.js
@@ -0,0 +1,112 @@
+///
+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"
+ },
+ {
+ "cascadeDelete": false,
+ "collectionId": "pbc_1125843985",
+ "hidden": false,
+ "id": "relation1519021197",
+ "maxSelect": 1,
+ "minSelect": 0,
+ "name": "post",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "relation"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text791980464",
+ "max": 0,
+ "min": 0,
+ "name": "visitor_hash",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text2783163181",
+ "max": 0,
+ "min": 0,
+ "name": "ip",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text3293145029",
+ "max": 0,
+ "min": 0,
+ "name": "user_agent",
+ "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_2019238136",
+ "indexes": [],
+ "listRule": null,
+ "name": "post_views",
+ "system": false,
+ "type": "base",
+ "updateRule": null,
+ "viewRule": null
+ });
+
+ return app.save(collection);
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_2019238136");
+
+ return app.delete(collection);
+})
diff --git a/backend/pb_migrations/1777232058_updated_post_views.js b/backend/pb_migrations/1777232058_updated_post_views.js
new file mode 100644
index 0000000..51b79ab
--- /dev/null
+++ b/backend/pb_migrations/1777232058_updated_post_views.js
@@ -0,0 +1,22 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_2019238136")
+
+ // update collection data
+ unmarshal({
+ "deleteRule": "@request.auth.id != ''",
+ "updateRule": "@request.auth.id != ''"
+ }, collection)
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_2019238136")
+
+ // update collection data
+ unmarshal({
+ "deleteRule": null,
+ "updateRule": null
+ }, collection)
+
+ return app.save(collection)
+})
diff --git a/backend/pb_migrations/1777232226_updated_post_views.js b/backend/pb_migrations/1777232226_updated_post_views.js
new file mode 100644
index 0000000..4fd4490
--- /dev/null
+++ b/backend/pb_migrations/1777232226_updated_post_views.js
@@ -0,0 +1,20 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_2019238136")
+
+ // update collection data
+ unmarshal({
+ "updateRule": "@request.auth.id != \"\""
+ }, collection)
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_2019238136")
+
+ // update collection data
+ unmarshal({
+ "updateRule": "@request.auth.id != ''"
+ }, collection)
+
+ return app.save(collection)
+})
diff --git a/backend/pb_migrations/1777232798_updated_post_views.js b/backend/pb_migrations/1777232798_updated_post_views.js
new file mode 100644
index 0000000..2b07e62
--- /dev/null
+++ b/backend/pb_migrations/1777232798_updated_post_views.js
@@ -0,0 +1,24 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_2019238136")
+
+ // update collection data
+ unmarshal({
+ "createRule": "@request.method = \"POST\"",
+ "listRule": "@request.method = \"GET\"",
+ "viewRule": "@request.method = \"GET\""
+ }, collection)
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_2019238136")
+
+ // update collection data
+ unmarshal({
+ "createRule": null,
+ "listRule": null,
+ "viewRule": null
+ }, collection)
+
+ return app.save(collection)
+})
diff --git a/frontend/public/images/sc_pocketbase.jpg b/frontend/public/images/sc_pocketbase.jpg
deleted file mode 100644
index 22c2453..0000000
Binary files a/frontend/public/images/sc_pocketbase.jpg and /dev/null differ
diff --git a/frontend/src/layouts/ArticleLayout.astro b/frontend/src/layouts/ArticleLayout.astro
index 493cab6..82f3a9a 100644
--- a/frontend/src/layouts/ArticleLayout.astro
+++ b/frontend/src/layouts/ArticleLayout.astro
@@ -314,20 +314,15 @@ const {
const viewsEl = document.querySelector('.meta-views') as HTMLElement & { dataset: { postId: string } };
if (viewsEl?.dataset?.postId) {
const postId = viewsEl.dataset.postId;
- const hasViewed = sessionStorage.getItem(`viewed_${postId}`);
- if (!hasViewed) {
- fetch(`/api/increment-views?postId=${postId}`, { method: 'POST' })
- .then(res => res.json())
- .then(data => {
- if (data.views !== undefined) {
- viewsEl.textContent = formatViews(data.views);
- }
- })
- .catch(() => {});
-
- sessionStorage.setItem(`viewed_${postId}`, 'true');
- }
+ fetch(`/api/increment-views?postId=${postId}`, { method: 'POST' })
+ .then(res => res.json())
+ .then(data => {
+ if (data.views !== undefined) {
+ viewsEl.textContent = formatViews(data.views);
+ }
+ })
+ .catch(() => {});
}
function formatViews(n: number): string {
diff --git a/frontend/src/lib/pbServer.ts b/frontend/src/lib/pbServer.ts
new file mode 100644
index 0000000..07dbef2
--- /dev/null
+++ b/frontend/src/lib/pbServer.ts
@@ -0,0 +1,16 @@
+import PocketBase from 'pocketbase';
+
+const PB_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
+const PB_ADMIN_EMAIL = import.meta.env.PB_ADMIN_EMAIL || 'admin@example.com';
+const PB_ADMIN_PASSWORD = import.meta.env.PB_ADMIN_PASSWORD || 'admin_password';
+
+const pbServer = new PocketBase(PB_URL);
+
+export { pbServer };
+
+export async function initPbServer() {
+ if (!pbServer.authStore.isValid) {
+ await pbServer.collection('_superusers').authWithPassword(PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD);
+ }
+ return pbServer;
+}
\ No newline at end of file
diff --git a/frontend/src/pages/api/increment-views.ts b/frontend/src/pages/api/increment-views.ts
index cf4f247..b273108 100644
--- a/frontend/src/pages/api/increment-views.ts
+++ b/frontend/src/pages/api/increment-views.ts
@@ -1,7 +1,6 @@
import type { APIRoute } from 'astro';
import crypto from 'crypto';
-
-const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
+import { initPbServer } from '@lib/pbServer';
const POCKETBASE_ID_REGEX = /^[a-z0-9]{15}$/;
@@ -19,6 +18,8 @@ function generateVisitorHash(ip: string, userAgent: string): string {
export const POST: APIRoute = async ({ request, url }) => {
try {
+ const pb = await initPbServer();
+
const postId = url.searchParams.get('postId');
if (!postId || !POCKETBASE_ID_REGEX.test(postId)) {
@@ -28,19 +29,16 @@ export const POST: APIRoute = async ({ request, url }) => {
);
}
- const postRes = await fetch(
- `${POCKETBASE_URL}/api/collections/posts/records/${postId}`,
- );
-
- if (!postRes.ok) {
+ let post;
+ try {
+ post = await pb.collection('posts').getOne(postId);
+ } catch {
return new Response(
JSON.stringify({ error: 'Пост не найден' }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
);
}
- const post = await postRes.json();
-
const ip = getClientIp(request);
const userAgent = request.headers.get('user-agent') || 'unknown';
const visitorHash = generateVisitorHash(ip, userAgent);
@@ -49,33 +47,29 @@ export const POST: APIRoute = async ({ request, url }) => {
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const yesterdayStr = yesterday.toISOString();
- const existingViewRes = await fetch(
- `${POCKETBASE_URL}/api/collections/post_views/records?` +
- new URLSearchParams({
+ let existingViews;
+ try {
+ existingViews = await pb.collection('post_views').getList(1, 1, {
filter: `post="${postId}" && visitor_hash="${visitorHash}" && created >= "${yesterdayStr}"`,
- })
- );
+ });
+ } catch {
+ existingViews = { totalItems: 0 };
+ }
let isNewView = false;
- if (existingViewRes.ok) {
- const existingData = await existingViewRes.json();
- if (existingData.items?.length === 0) {
- isNewView = true;
+ if (existingViews.totalItems === 0) {
+ isNewView = true;
- await fetch(
- `${POCKETBASE_URL}/api/collections/post_views/records`,
- {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- post: postId,
- visitor_hash: visitorHash,
- ip: ip,
- user_agent: userAgent,
- }),
- }
- );
+ try {
+ await pb.collection('post_views').create({
+ post: postId,
+ visitor_hash: visitorHash,
+ ip: ip,
+ user_agent: userAgent,
+ });
+ } catch {
+ // ignore
}
}
@@ -84,14 +78,11 @@ export const POST: APIRoute = async ({ request, url }) => {
if (isNewView) {
totalViews += 1;
- await fetch(
- `${POCKETBASE_URL}/api/collections/posts/records/${postId}`,
- {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ views: totalViews }),
- }
- );
+ try {
+ await pb.collection('posts').update(postId, { views: totalViews });
+ } catch {
+ // ignore
+ }
}
return new Response(
@@ -100,10 +91,10 @@ export const POST: APIRoute = async ({ request, url }) => {
);
} catch (error) {
- console.error('[Increment Views API] Error:', error);
+ console.error('[Increment Views] Error:', error);
return new Response(
JSON.stringify({ error: 'Внутренняя ошибка сервера' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
-};
+};
\ No newline at end of file