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