Насторил работу блога через Backend
This commit is contained in:
parent
014439d565
commit
edd730b438
33 changed files with 1019 additions and 200 deletions
37
backend/pb_migrations/1776199882_created_posts.js
Normal file
37
backend/pb_migrations/1776199882_created_posts.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = new Collection({
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"id": "pbc_1125843985",
|
||||
"indexes": [],
|
||||
"listRule": "",
|
||||
"name": "posts",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"viewRule": ""
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
29
backend/pb_migrations/1776200325_updated_posts.js
Normal file
29
backend/pb_migrations/1776200325_updated_posts.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(1, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text724990059",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "title",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("text724990059")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
29
backend/pb_migrations/1776200337_updated_posts.js
Normal file
29
backend/pb_migrations/1776200337_updated_posts.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(2, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1843675174",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "description",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("text1843675174")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
29
backend/pb_migrations/1776200351_updated_posts.js
Normal file
29
backend/pb_migrations/1776200351_updated_posts.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(3, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text3182418120",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "author",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("text3182418120")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
29
backend/pb_migrations/1776200365_updated_posts.js
Normal file
29
backend/pb_migrations/1776200365_updated_posts.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(4, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text105650625",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "category",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("text105650625")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
29
backend/pb_migrations/1776200395_updated_posts.js
Normal file
29
backend/pb_migrations/1776200395_updated_posts.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(5, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1898540465",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "categoryColor",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("text1898540465")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
26
backend/pb_migrations/1776200412_updated_posts.js
Normal file
26
backend/pb_migrations/1776200412_updated_posts.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(6, new Field({
|
||||
"hidden": false,
|
||||
"id": "date2862495610",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "date",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("date2862495610")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
29
backend/pb_migrations/1776200439_updated_posts.js
Normal file
29
backend/pb_migrations/1776200439_updated_posts.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(7, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1864383797",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "readmeTime",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("text1864383797")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
29
backend/pb_migrations/1776200454_updated_posts.js
Normal file
29
backend/pb_migrations/1776200454_updated_posts.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(8, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text2548032275",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "imageUrl",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("text2548032275")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
29
backend/pb_migrations/1776200465_updated_posts.js
Normal file
29
backend/pb_migrations/1776200465_updated_posts.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(9, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text2560465762",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "slug",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("text2560465762")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
24
backend/pb_migrations/1776200481_updated_posts.js
Normal file
24
backend/pb_migrations/1776200481_updated_posts.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(10, new Field({
|
||||
"hidden": false,
|
||||
"id": "bool1182570132",
|
||||
"name": "draft",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("bool1182570132")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
26
backend/pb_migrations/1776200499_updated_posts.js
Normal file
26
backend/pb_migrations/1776200499_updated_posts.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(11, new Field({
|
||||
"convertURLs": false,
|
||||
"hidden": false,
|
||||
"id": "editor4274335913",
|
||||
"maxSize": 0,
|
||||
"name": "content",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "editor"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("editor4274335913")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
22
backend/pb_migrations/1776200529_updated_posts.js
Normal file
22
backend/pb_migrations/1776200529_updated_posts.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"listRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"listRule": "",
|
||||
"viewRule": ""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
22
backend/pb_migrations/1776200951_updated_posts.js
Normal file
22
backend/pb_migrations/1776200951_updated_posts.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"listRule": " draft = false || @request.auth.id != \"\" ",
|
||||
"viewRule": " draft = false || @request.auth.id != \"\" "
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1125843985")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"listRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
39
bun.lock
39
bun.lock
|
|
@ -4,6 +4,12 @@
|
|||
"workspaces": {
|
||||
"": {
|
||||
"name": "avtourist086",
|
||||
"dependencies": {
|
||||
"pocketbase": "^0.26.8",
|
||||
},
|
||||
"devDependencies": {
|
||||
"gray-matter": "^4.0.3",
|
||||
},
|
||||
},
|
||||
"frontend": {
|
||||
"name": "frontend",
|
||||
|
|
@ -15,6 +21,7 @@
|
|||
"@tailwindcss/vite": "^4.2.2",
|
||||
"astro": "^6.0.8",
|
||||
"astro-icon": "^1.1.5",
|
||||
"marked": "^18.0.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -360,7 +367,7 @@
|
|||
|
||||
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
|
|
@ -494,6 +501,8 @@
|
|||
|
||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
|
||||
|
||||
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
|
||||
|
|
@ -516,6 +525,8 @@
|
|||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
||||
|
||||
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
|
@ -554,6 +565,8 @@
|
|||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
|
||||
|
||||
"h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
|
||||
|
||||
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
|
||||
|
|
@ -606,6 +619,8 @@
|
|||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
|
|
@ -618,12 +633,14 @@
|
|||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
"js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
|
||||
|
||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
|
||||
|
|
@ -666,6 +683,8 @@
|
|||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"marked": ["marked@18.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-2e7Qiv/HJSXj8rDEpgTvGKsP8yYtI9xXHKDnrftrmnrJPaFNM7VRb2YCzWaX4BP1iCJ/XPduzDJZMFoqTCcIMA=="],
|
||||
|
||||
"mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
|
||||
|
||||
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||
|
|
@ -848,6 +867,8 @@
|
|||
|
||||
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.8", "", {}, "sha512-aQ/ewvS7ncvAE8wxoW10iAZu6ElgbeFpBhKPnCfvRovNzm2gW8u/sQNPGN6vNgVEagz44kK//C61oKjfa+7Low=="],
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
|
||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
|
|
@ -922,6 +943,8 @@
|
|||
|
||||
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
||||
|
||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||
|
||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
|
@ -946,6 +969,8 @@
|
|||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="],
|
||||
|
|
@ -956,6 +981,8 @@
|
|||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
|
||||
|
||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
|
@ -1108,6 +1135,8 @@
|
|||
|
||||
"@astrojs/language-server/@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="],
|
||||
|
||||
"@astrojs/markdown-remark/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"@iconify/tools/svgo": ["svgo@3.3.3", "", { "dependencies": { "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0", "sax": "^1.5.0" }, "bin": "./bin/svgo" }, "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
|
@ -1130,6 +1159,8 @@
|
|||
|
||||
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"astro/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
|
@ -1150,6 +1181,8 @@
|
|||
|
||||
"yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"@astrojs/markdown-remark/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"@iconify/tools/svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
|
||||
"@iconify/tools/svgo/css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
|
||||
|
|
@ -1158,6 +1191,8 @@
|
|||
|
||||
"@types/yauzl/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
||||
|
||||
"astro/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
|
||||
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"@tailwindcss/vite": "^4.2.2",
|
||||
"astro": "^6.0.8",
|
||||
"astro-icon": "^1.1.5",
|
||||
"marked": "^18.0.0",
|
||||
"tailwindcss": "^4.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,5 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
|
||||
// Сортируем и форматируем для поиска
|
||||
const searchData = posts
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
||||
.map(post => ({
|
||||
title: post.data.title,
|
||||
description: post.data.description,
|
||||
slug: post.id,
|
||||
category: post.data.category,
|
||||
categoryColor: post.data.categoryColor,
|
||||
date: post.data.date.toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
}));
|
||||
|
||||
const title = 'Поиск по статьям';
|
||||
const postsJson = JSON.stringify(searchData);
|
||||
---
|
||||
|
||||
<div id="search-modal" class="modal-overlay" aria-hidden="true">
|
||||
|
|
@ -61,16 +40,33 @@ const postsJson = JSON.stringify(searchData);
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script define:vars={{ postsJson }}>
|
||||
<script>
|
||||
(function() {
|
||||
const modal = document.getElementById('search-modal');
|
||||
const closeBtn = document.getElementById('search-modal-close-btn');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const resultsContainer = document.getElementById('search-results');
|
||||
const posts = JSON.parse(postsJson);
|
||||
let postsCache = null;
|
||||
|
||||
if (!modal || !searchInput || !resultsContainer) return;
|
||||
|
||||
async function fetchPosts() {
|
||||
if (postsCache) return postsCache;
|
||||
try {
|
||||
const res = await fetch('/api/posts?per_page=100');
|
||||
const data = await res.json();
|
||||
postsCache = data.posts || [];
|
||||
return postsCache;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
modal.classList.add('active');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
|
|
@ -86,7 +82,7 @@ const postsJson = JSON.stringify(searchData);
|
|||
resultsContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
function handleSearch(query) {
|
||||
async function handleSearch(query) {
|
||||
const trimmed = query.trim().toLowerCase();
|
||||
|
||||
if (trimmed.length < 2) {
|
||||
|
|
@ -94,6 +90,8 @@ const postsJson = JSON.stringify(searchData);
|
|||
return;
|
||||
}
|
||||
|
||||
const posts = await fetchPosts();
|
||||
|
||||
const filtered = posts.filter(post =>
|
||||
post.title.toLowerCase().includes(trimmed) ||
|
||||
post.description.toLowerCase().includes(trimmed) ||
|
||||
|
|
@ -112,10 +110,9 @@ const postsJson = JSON.stringify(searchData);
|
|||
return;
|
||||
}
|
||||
|
||||
// вывод динамического списка статей
|
||||
resultsContainer.innerHTML = filtered.map(post => `
|
||||
<a href="/blog/${post.id}" class="search-result-item">
|
||||
<span class="result-date">${post.date}</span>
|
||||
<a href="/blog/${post.slug}" class="search-result-item">
|
||||
<span class="result-date">${formatDate(post.date)}</span>
|
||||
<h4 class="result-title">${post.title}</h4>
|
||||
<p class="result-description">${post.description.substring(0, 120)}...</p>
|
||||
<span class="result-arrow">
|
||||
|
|
|
|||
|
|
@ -1,29 +1,28 @@
|
|||
---
|
||||
import BlogCard from '@components/blog/BlogCard.astro';
|
||||
|
||||
interface CollectionEntry {
|
||||
interface Post {
|
||||
id: string;
|
||||
data: {
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
categoryColor: string;
|
||||
date: Date;
|
||||
readTime: string;
|
||||
imageUrl: string;
|
||||
};
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
categoryColor: string;
|
||||
date: string;
|
||||
readTime: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
posts: CollectionEntry[];
|
||||
posts: Post[];
|
||||
currentSlug?: string;
|
||||
}
|
||||
|
||||
const { posts, currentSlug } = Astro.props;
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
|
|
@ -32,7 +31,7 @@ const formatDate = (date: Date) => {
|
|||
|
||||
// Фильтруем текущую статью
|
||||
const filteredPosts = currentSlug
|
||||
? posts.filter(post => post.id !== currentSlug).slice(0, 3)
|
||||
? posts.filter(post => post.slug !== currentSlug).slice(0, 3)
|
||||
: posts.slice(0, 3);
|
||||
---
|
||||
|
||||
|
|
@ -42,14 +41,14 @@ const filteredPosts = currentSlug
|
|||
<div class="related-grid">
|
||||
{filteredPosts.map((post) => (
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
category={post.data.category}
|
||||
categoryColor={post.data.categoryColor}
|
||||
date={formatDate(post.data.date)}
|
||||
readTime={post.data.readTime}
|
||||
imageUrl={post.data.imageUrl}
|
||||
slug={`/blog/${post.id}`}
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
category={post.category}
|
||||
categoryColor={post.categoryColor}
|
||||
date={formatDate(post.date)}
|
||||
readTime={post.readTime}
|
||||
imageUrl={post.imageUrl}
|
||||
slug={`/blog/${post.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
74
frontend/src/lib/pb.ts
Normal file
74
frontend/src/lib/pb.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const PB_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||
|
||||
export const pb = new PocketBase(PB_URL);
|
||||
|
||||
export interface Post {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
author: string;
|
||||
category: string;
|
||||
categoryColor: string;
|
||||
date: string;
|
||||
readTime: string;
|
||||
imageUrl: string;
|
||||
content?: string;
|
||||
draft: boolean;
|
||||
}
|
||||
|
||||
export async function getPosts(options?: {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
category?: string;
|
||||
search?: string;
|
||||
}): Promise<{ posts: Post[]; total: number; page: number; totalPages: number }> {
|
||||
const page = options?.page || 1;
|
||||
const perPage = options?.perPage || 10;
|
||||
|
||||
const filter: string[] = ['draft = false'];
|
||||
|
||||
if (options?.category && options.category !== 'Все') {
|
||||
filter.push(`category = "${options.category}"`);
|
||||
}
|
||||
|
||||
if (options?.search) {
|
||||
filter.push(`(title ~ "${options.search}" || description ~ "${options.search}")`);
|
||||
}
|
||||
|
||||
const result = await pb.collection('posts').getList(page, perPage, {
|
||||
filter: filter.join(' && '),
|
||||
sort: '-date',
|
||||
});
|
||||
|
||||
return {
|
||||
posts: (result.items || []) as unknown as Post[],
|
||||
total: result.totalItems || 0,
|
||||
page: (result.pageInfo?.page || 1),
|
||||
totalPages: result.totalPages || 1,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPostBySlug(slug: string): Promise<Post | null> {
|
||||
const result = await pb.collection('posts').getList(1, 1, {
|
||||
filter: `slug="${slug}" && draft = false`,
|
||||
});
|
||||
|
||||
if (!result.items || result.totalItems === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.items[0] as unknown as Post;
|
||||
}
|
||||
|
||||
export async function getAllCategories(): Promise<string[]> {
|
||||
const result = await pb.collection('posts').getFullList({
|
||||
filter: 'draft = false',
|
||||
fields: 'category',
|
||||
});
|
||||
|
||||
const categories = (result || []).map((post: any) => post.category).filter(Boolean);
|
||||
return ['Все', ...new Set(categories)];
|
||||
}
|
||||
45
frontend/src/pages/api/auth/sign-in.ts
Normal file
45
frontend/src/pages/api/auth/sign-in.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const POST: APIRoute = async ({ request, cookies }) => {
|
||||
try {
|
||||
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
|
||||
const data = await request.json();
|
||||
|
||||
const { email, password } = data;
|
||||
|
||||
if (!email || !password) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Email и пароль обязательны'
|
||||
}), { status: 400 });
|
||||
}
|
||||
|
||||
const authRecord = await pb.collection('users').authWithPassword(email, password);
|
||||
|
||||
cookies.set('pb_auth', JSON.stringify(pb.authStore.exportToCookie()), {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
user: {
|
||||
id: authRecord.id,
|
||||
name: authRecord.name,
|
||||
email: authRecord.email,
|
||||
}
|
||||
}), { status: 200 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Sign in error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || 'Неверный email или пароль'
|
||||
}), { status: 401 });
|
||||
}
|
||||
};
|
||||
10
frontend/src/pages/api/auth/sign-out.ts
Normal file
10
frontend/src/pages/api/auth/sign-out.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
|
||||
export const POST: APIRoute = async ({ cookies }) => {
|
||||
cookies.delete('pb_auth', { path: '/' });
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Вы успешно вышли из аккаунта'
|
||||
}), { status: 200 });
|
||||
};
|
||||
45
frontend/src/pages/api/auth/sign-up.ts
Normal file
45
frontend/src/pages/api/auth/sign-up.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const POST: APIRoute = async ({ request, redirect }) => {
|
||||
try {
|
||||
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
|
||||
const data = await request.json();
|
||||
|
||||
const { name, email, phone, password } = data;
|
||||
|
||||
if (!email || !password) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Email и пароль обязательны'
|
||||
}), { status: 400 });
|
||||
}
|
||||
|
||||
const record = await pb.collection('users').create({
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
});
|
||||
|
||||
await pb.collection('users').authWithPassword(email, password);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
record: {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
email: record.email,
|
||||
}
|
||||
}), { status: 201 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Sign up error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || 'Ошибка при регистрации'
|
||||
}), { status: 400 });
|
||||
}
|
||||
};
|
||||
40
frontend/src/pages/api/consultation.ts
Normal file
40
frontend/src/pages/api/consultation.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
|
||||
const data = await request.json();
|
||||
|
||||
const { name, phone, service } = data;
|
||||
|
||||
if (!name || !phone) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Имя и телефон обязательны'
|
||||
}), { status: 400 });
|
||||
}
|
||||
|
||||
const record = await pb.collection('consultations').create({
|
||||
name,
|
||||
phone,
|
||||
service: service || '',
|
||||
status: 'new',
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Заявка отправлена! Мы свяжемся с вами в течение 15 минут.',
|
||||
id: record.id
|
||||
}), { status: 201 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Consultation error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || 'Ошибка при отправке заявки'
|
||||
}), { status: 400 });
|
||||
}
|
||||
};
|
||||
48
frontend/src/pages/api/posts/[slug].ts
Normal file
48
frontend/src/pages/api/posts/[slug].ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const GET: APIRoute = async ({ params }) => {
|
||||
try {
|
||||
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
|
||||
const { slug } = params;
|
||||
|
||||
if (!slug) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'slug обязателен'
|
||||
}), { status: 400 });
|
||||
}
|
||||
|
||||
const result = await pb.collection('posts').getList(1, 1, {
|
||||
filter: `slug="${slug}" && draft = false`,
|
||||
});
|
||||
|
||||
if (result.totalItems === 0) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Пост не найден'
|
||||
}), { status: 404 });
|
||||
}
|
||||
|
||||
const post = result.items[0];
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
id: post.id,
|
||||
slug: post.slug,
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
author: post.author,
|
||||
category: post.category,
|
||||
categoryColor: post.categoryColor,
|
||||
date: post.date,
|
||||
readTime: post.readTime,
|
||||
imageUrl: post.imageUrl,
|
||||
content: post.content,
|
||||
}), { status: 200 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('API post error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
error: error.message || 'Ошибка при получении поста'
|
||||
}), { status: 500 });
|
||||
}
|
||||
};
|
||||
54
frontend/src/pages/api/posts/index.ts
Normal file
54
frontend/src/pages/api/posts/index.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const GET: APIRoute = async ({ url }) => {
|
||||
try {
|
||||
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
|
||||
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const perPage = parseInt(url.searchParams.get('per_page') || '10');
|
||||
const category = url.searchParams.get('category');
|
||||
const search = url.searchParams.get('search');
|
||||
|
||||
const filter: string[] = ['draft = false'];
|
||||
|
||||
if (category) {
|
||||
filter.push(`category = "${category}"`);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
filter.push(`(title ~ "${search}" || description ~ "${search}")`);
|
||||
}
|
||||
|
||||
const result = await pb.collection('posts').getList(page, perPage, {
|
||||
filter: filter.join(' && '),
|
||||
sort: '-date',
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
posts: result.items.map(post => ({
|
||||
id: post.id,
|
||||
slug: post.slug,
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
author: post.author,
|
||||
category: post.category,
|
||||
categoryColor: post.categoryColor,
|
||||
date: post.date,
|
||||
readTime: post.readTime,
|
||||
imageUrl: post.imageUrl,
|
||||
})),
|
||||
total: result.totalItems,
|
||||
page: result.pageInfo.page,
|
||||
perPage: result.pageInfo.perPage,
|
||||
totalPages: result.totalPages,
|
||||
}), { status: 200 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('API posts error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
error: error.message || 'Ошибка при получении постов'
|
||||
}), { status: 500 });
|
||||
}
|
||||
};
|
||||
|
|
@ -4,16 +4,10 @@ import { SITE_URL } from '@constants';
|
|||
import PostCommentForm from '@components/blog/PostCommentForm.astro';
|
||||
import RelatedPosts from '@components/blog/RelatedPosts.astro';
|
||||
import ArticleTableOfContents from '@components/blog/ArticleTableOfContents.astro';
|
||||
import { getCollection, getEntry, render } from 'astro:content';
|
||||
import { getPostBySlug, getPosts } from '@lib/pb';
|
||||
import { marked } from 'marked';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog') as { id: string; data: Record<string, any> }[];
|
||||
return posts.map((post: { id: string }) => ({
|
||||
params: { slug: post.id },
|
||||
}));
|
||||
}
|
||||
export const prerender = false;
|
||||
|
||||
const slug = Astro.params.slug;
|
||||
|
||||
|
|
@ -21,87 +15,77 @@ if (!slug) {
|
|||
return Astro.redirect('/blog');
|
||||
}
|
||||
|
||||
const post = await getEntry('blog', slug);
|
||||
const post = await getPostBySlug(slug);
|
||||
|
||||
if (!post) {
|
||||
return Astro.redirect('/blog');
|
||||
}
|
||||
|
||||
const { Content } = await render(post);
|
||||
// Конвертируем markdown в HTML
|
||||
const contentHtml = marked(post.content || '');
|
||||
|
||||
// Извлекаем заголовки из MDX тела для оглавления
|
||||
const body = post.body || '';
|
||||
// Извлекаем заголовки для оглавления
|
||||
const headingRegex = /^(#{2,3})\s+(.+)$/gm;
|
||||
const tocItems: { id: string; text: string; level: number }[] = [];
|
||||
const body = post.content || '';
|
||||
let match;
|
||||
let headingIndex = 0;
|
||||
while ((match = headingRegex.exec(body)) !== null) {
|
||||
const level = match[1].length;
|
||||
const text = match[2].trim();
|
||||
// Генерируем ID из текста заголовка
|
||||
const id = text.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-');
|
||||
tocItems.push({ level, id: `heading-${headingIndex++}`, text });
|
||||
}
|
||||
|
||||
console.log('=== TOC ITEMS ===', tocItems);
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Логика авторизации (пока статичная переменная)
|
||||
const isAuthorized = false;
|
||||
// Для related posts берем те же категории
|
||||
const { posts: relatedPosts } = await getPosts({ perPage: 4, category: post.category });
|
||||
const filteredRelated = relatedPosts.filter(p => p.slug !== slug).slice(0, 3);
|
||||
|
||||
// URL текущей страницы
|
||||
const currentUrl = `${SITE_URL}/blog/${post.id}`;
|
||||
|
||||
// Получаем все посты для блока "Читайте также"
|
||||
const allPosts = await getCollection('blog');
|
||||
const currentUrl = `${SITE_URL}/blog/${slug}`;
|
||||
---
|
||||
|
||||
<ArticleLayout
|
||||
title={`${post.data.title} — автоюрист в Сургуте`}
|
||||
description={post.data.description}
|
||||
canonicalLink={`${SITE_URL}/blog/${post.id}`}
|
||||
title={`${post.title} — автоюрист в Сургуте`}
|
||||
description={post.description}
|
||||
canonicalLink={`${SITE_URL}/blog/${slug}`}
|
||||
breadcrumbs={[
|
||||
{ label: 'Главная', href: '/' },
|
||||
{ label: 'Блог', href: '/blog' },
|
||||
{ label: post.data.title }
|
||||
{ label: post.title }
|
||||
]}
|
||||
heroImage={post.data.imageUrl}
|
||||
heroAlt={post.data.title}
|
||||
category={post.data.category}
|
||||
postTitle={post.data.title}
|
||||
date={formatDate(post.data.date)}
|
||||
author={post.data.author}
|
||||
readTime={post.data.readTime}
|
||||
heroImage={post.imageUrl}
|
||||
heroAlt={post.title}
|
||||
category={post.category}
|
||||
postTitle={post.title}
|
||||
date={formatDate(post.date)}
|
||||
author={post.author}
|
||||
readTime={post.readTime}
|
||||
postId={post.id}
|
||||
postUrl={currentUrl}
|
||||
initialLikes={12}
|
||||
initialDislikes={2}
|
||||
>
|
||||
<!-- Содержимое статьи -->
|
||||
<div class="post-content">
|
||||
<Content />
|
||||
</div>
|
||||
<div class="post-content" set:html={contentHtml} />
|
||||
|
||||
<!-- Форма комментариев -->
|
||||
<PostCommentForm
|
||||
postId={post.id}
|
||||
isAuthorized={isAuthorized}
|
||||
isAuthorized={false}
|
||||
/>
|
||||
|
||||
<!-- Похожие статьи -->
|
||||
<RelatedPosts
|
||||
posts={allPosts}
|
||||
currentSlug={post.id}
|
||||
posts={filteredRelated}
|
||||
currentSlug={slug}
|
||||
/>
|
||||
|
||||
<!-- Оглавление в сайдбаре -->
|
||||
|
|
@ -168,4 +152,4 @@ const allPosts = await getCollection('blog');
|
|||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -7,32 +7,21 @@ import BlogCard from '@components/blog/BlogCard.astro';
|
|||
import Pagination from '@components/base/Pagination.astro';
|
||||
import CTA from '@components/base/CTA.astro';
|
||||
import SearchModal from '@components/base/SearchModal.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
|
||||
// Сортируем посты по дате (новые сверху)
|
||||
const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
import { getPosts, getAllCategories } from '@lib/pb';
|
||||
|
||||
const POSTS_PER_PAGE = 6;
|
||||
const currentPage = 1;
|
||||
const totalPages = Math.ceil(sortedPosts.length / POSTS_PER_PAGE);
|
||||
|
||||
const startIndex = 0;
|
||||
const endIndex = POSTS_PER_PAGE;
|
||||
const paginatedPosts = sortedPosts.slice(startIndex, endIndex);
|
||||
const { posts, total, totalPages } = await getPosts({ page: currentPage, perPage: POSTS_PER_PAGE });
|
||||
const categories = await getAllCategories();
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Категории
|
||||
const categories = ['Все', ...new Set(posts.map((post: any) => post.data.category))];
|
||||
---
|
||||
|
||||
<Layout
|
||||
|
|
@ -66,17 +55,17 @@ const categories = ['Все', ...new Set(posts.map((post: any) => post.data.cate
|
|||
<section class="blog-grid-section">
|
||||
<div class="site-container">
|
||||
<div class="blog-grid" id="blog-grid">
|
||||
{paginatedPosts.map((post: any) => (
|
||||
<article class="blog-card-wrapper" data-category={post.data.category}>
|
||||
{posts.map((post: any) => (
|
||||
<article class="blog-card-wrapper" data-category={post.category}>
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
category={post.data.category}
|
||||
categoryColor={post.data.categoryColor}
|
||||
date={formatDate(post.data.date)}
|
||||
readTime={post.data.readTime}
|
||||
imageUrl={post.data.imageUrl}
|
||||
slug={`/blog/${post.id}`}
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
category={post.category}
|
||||
categoryColor={post.categoryColor}
|
||||
date={formatDate(post.date)}
|
||||
readTime={post.readTime}
|
||||
imageUrl={post.imageUrl}
|
||||
slug={`/blog/${post.slug}`}
|
||||
/>
|
||||
</article>
|
||||
))}
|
||||
|
|
@ -206,4 +195,4 @@ const categories = ['Все', ...new Set(posts.map((post: any) => post.data.cate
|
|||
setupAnimations();
|
||||
setupFilter();
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -7,49 +7,28 @@ import BlogCard from '@components/blog/BlogCard.astro';
|
|||
import Pagination from '@components/base/Pagination.astro';
|
||||
import CTA from '@components/base/CTA.astro';
|
||||
import SearchModal from '@components/base/SearchModal.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getPosts, getAllCategories } from '@lib/pb';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
const POSTS_PER_PAGE = 6;
|
||||
const totalPages = Math.ceil(posts.length / POSTS_PER_PAGE);
|
||||
|
||||
return Array.from({ length: totalPages }, (_, i) => ({
|
||||
params: { page: String(i + 2) }, // Начинаем со 2-й страницы (1-я это /blog/)
|
||||
}));
|
||||
}
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
|
||||
// Сортируем посты по дате (новые сверху)
|
||||
const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
export const prerender = false;
|
||||
|
||||
const POSTS_PER_PAGE = 6;
|
||||
const currentPage = Number(Astro.params.page) || 1;
|
||||
const totalPages = Math.ceil(sortedPosts.length / POSTS_PER_PAGE);
|
||||
|
||||
const startIndex = (currentPage - 1) * POSTS_PER_PAGE;
|
||||
const endIndex = startIndex + POSTS_PER_PAGE;
|
||||
const paginatedPosts = sortedPosts.slice(startIndex, endIndex);
|
||||
const { posts, total, totalPages } = await getPosts({ page: currentPage, perPage: POSTS_PER_PAGE });
|
||||
const categories = await getAllCategories();
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Категории
|
||||
const categories = ['Все', ...new Set(posts.map(post => post.data.category))];
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Блог — страница ${currentPage} — автоюрист в Сургуте"
|
||||
description="Полезные статьи и советы по автоспорам, ДТП, ОСАГО, лишении прав и защите прав водителей. Страница ${currentPage}."
|
||||
title={`Блог — страница ${currentPage} — автоюрист в Сургуте`}
|
||||
description={`Полезные статьи и советы по автоспорам, ДТП, ОСАГО, лишении прав и защите прав водителей. Страница ${currentPage}.`}
|
||||
canonicalLink={`${SITE_URL}/blog/page/${currentPage}`}
|
||||
breadcrumbs={[
|
||||
{ label: 'Главная', href: '/' },
|
||||
|
|
@ -78,17 +57,17 @@ const categories = ['Все', ...new Set(posts.map(post => post.data.category))]
|
|||
<section class="blog-grid-section">
|
||||
<div class="site-container">
|
||||
<div class="blog-grid" id="blog-grid">
|
||||
{paginatedPosts.map((post: any) => (
|
||||
<article class="blog-card-wrapper" data-category={post.data.category}>
|
||||
{posts.map((post: any) => (
|
||||
<article class="blog-card-wrapper" data-category={post.category}>
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
category={post.data.category}
|
||||
categoryColor={post.data.categoryColor}
|
||||
date={formatDate(post.data.date)}
|
||||
readTime={post.data.readTime}
|
||||
imageUrl={post.data.imageUrl}
|
||||
slug={`/blog/${post.id}`}
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
category={post.category}
|
||||
categoryColor={post.categoryColor}
|
||||
date={formatDate(post.date)}
|
||||
readTime={post.readTime}
|
||||
imageUrl={post.imageUrl}
|
||||
slug={`/blog/${post.slug}`}
|
||||
/>
|
||||
</article>
|
||||
))}
|
||||
|
|
@ -179,4 +158,4 @@ const categories = ['Все', ...new Set(posts.map(post => post.data.category))]
|
|||
|
||||
setupAnimations();
|
||||
document.addEventListener('astro:after-swap', setupAnimations);
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -3,40 +3,23 @@ import Layout from '@layouts/Layout.astro';
|
|||
import { SITE_URL } from '@constants';
|
||||
import BlogCard from '@components/blog/BlogCard.astro';
|
||||
import SearchModal from '@components/base/SearchModal.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getPosts } from '@lib/pb';
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
|
||||
// Получаем параметр поиска из URL
|
||||
const url = new URL(Astro.request.url);
|
||||
const searchQuery = url.searchParams.get('q') || '';
|
||||
|
||||
const { posts: searchResults } = searchQuery
|
||||
? await getPosts({ perPage: 20, search: searchQuery })
|
||||
: { posts: [], total: 0, page: 1, totalPages: 1 };
|
||||
|
||||
const breadcrumbsItems = [
|
||||
{ label: 'Главная', href: '/' },
|
||||
{ label: 'Блог', href: '/blog' },
|
||||
{ label: searchQuery ? `Поиск: "${searchQuery}"` : 'Поиск' }
|
||||
];
|
||||
|
||||
// Функция поиска по статьям
|
||||
function searchArticles(query: string, allPosts: typeof posts) {
|
||||
if (!query.trim()) return [];
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
return allPosts.filter(post => {
|
||||
const titleMatch = post.data.title.toLowerCase().includes(lowerQuery);
|
||||
const descriptionMatch = post.data.description.toLowerCase().includes(lowerQuery);
|
||||
const categoryMatch = post.data.category.toLowerCase().includes(lowerQuery);
|
||||
|
||||
return titleMatch || descriptionMatch || categoryMatch;
|
||||
});
|
||||
}
|
||||
|
||||
const searchResults = searchArticles(searchQuery, posts);
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
|
|
@ -89,14 +72,14 @@ const formatDate = (date: Date) => {
|
|||
<div class="results-grid">
|
||||
{searchResults.map((post: any) => (
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
category={post.data.category}
|
||||
categoryColor={post.data.categoryColor}
|
||||
date={formatDate(post.data.date)}
|
||||
readTime={post.data.readTime}
|
||||
imageUrl={post.data.imageUrl}
|
||||
slug={`/blog/${post.id}`}
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
category={post.category}
|
||||
categoryColor={post.categoryColor}
|
||||
date={formatDate(post.date)}
|
||||
readTime={post.readTime}
|
||||
imageUrl={post.imageUrl}
|
||||
slug={`/blog/${post.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -149,7 +132,6 @@ const formatDate = (date: Date) => {
|
|||
}));
|
||||
});
|
||||
|
||||
// Автоматически открываем поиск при загрузке страницы без параметров
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const query = urlParams.get('q');
|
||||
|
|
@ -338,4 +320,4 @@ const formatDate = (date: Date) => {
|
|||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -12,7 +12,8 @@
|
|||
"@scripts/*": ["src/scripts/*"],
|
||||
"@layouts/*": ["src/layouts/*"],
|
||||
"@assets/*": ["src/assets/*"],
|
||||
"@pages/*": ["src/pages/*"]
|
||||
"@pages/*": ["src/pages/*"],
|
||||
"@lib/*": ["src/lib/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,5 +12,11 @@
|
|||
},
|
||||
"workspaces": [
|
||||
"frontend"
|
||||
]
|
||||
],
|
||||
"dependencies": {
|
||||
"pocketbase": "^0.26.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gray-matter": "^4.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
59
scripts/create-posts-collection.ts
Normal file
59
scripts/create-posts-collection.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const PB_URL = process.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
|
||||
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD;
|
||||
|
||||
async function createCollection() {
|
||||
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
||||
console.error('❌ Укажите PB_ADMIN_EMAIL и PB_ADMIN_PASSWORD в .env');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pb = new PocketBase(PB_URL);
|
||||
|
||||
try {
|
||||
await pb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
|
||||
console.log('✅ Подключено к PocketBase');
|
||||
} catch (e) {
|
||||
console.error('❌ Ошибка авторизации');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const collection = await pb.collections.create({
|
||||
name: 'posts',
|
||||
type: 'base',
|
||||
system: false,
|
||||
schema: [
|
||||
{ name: 'title', type: 'text', required: true },
|
||||
{ name: 'description', type: 'text', required: true },
|
||||
{ name: 'author', type: 'text', required: false },
|
||||
{ name: 'category', type: 'text', required: false },
|
||||
{ name: 'categoryColor', type: 'text', required: false },
|
||||
{ name: 'date', type: 'date', required: true },
|
||||
{ name: 'readTime', type: 'text', required: false },
|
||||
{ name: 'imageUrl', type: 'text', required: false },
|
||||
{ name: 'slug', type: 'text', required: true },
|
||||
{ name: 'draft', type: 'bool', required: false },
|
||||
{ name: 'content', type: 'editor', required: false },
|
||||
],
|
||||
listRule: '',
|
||||
viewRule: '',
|
||||
createRule: '@request.auth.id != ""',
|
||||
updateRule: '@request.auth.id != ""',
|
||||
deleteRule: '@request.auth.id != ""',
|
||||
});
|
||||
|
||||
console.log('✅ Коллекция "posts" создана');
|
||||
console.log(' ID:', collection.id);
|
||||
} catch (e: any) {
|
||||
if (e.data?.message?.includes('already exists')) {
|
||||
console.log('ℹ️ Коллекция "posts" уже существует');
|
||||
} else {
|
||||
console.error('❌ Ошибка:', e.data || e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createCollection();
|
||||
82
scripts/migrate-posts.ts
Normal file
82
scripts/migrate-posts.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
|
||||
const PB_URL = process.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||
|
||||
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
|
||||
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD;
|
||||
|
||||
interface PostFrontmatter {
|
||||
title: string;
|
||||
description: string;
|
||||
author: string;
|
||||
category: string;
|
||||
categoryColor: string;
|
||||
date: string;
|
||||
readTime: string;
|
||||
imageUrl: string;
|
||||
draft: boolean;
|
||||
}
|
||||
|
||||
async function migrate() {
|
||||
console.log('🔄 Миграция постов в PocketBase...\n');
|
||||
|
||||
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
||||
console.error('❌ Укажите PB_ADMIN_EMAIL и PB_ADMIN_PASSWORD в .env');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pb = new PocketBase(PB_URL);
|
||||
|
||||
try {
|
||||
await pb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
|
||||
console.log('✅ Подключено к PocketBase');
|
||||
} catch (e) {
|
||||
console.error('❌ Ошибка авторизации в PocketBase');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const blogDir = path.join(process.cwd(), 'frontend/src/content/blog');
|
||||
const files = fs.readdirSync(blogDir).filter(f => f.endsWith('.mdx'));
|
||||
|
||||
console.log(`📂 Найдено ${files.length} постов в ${blogDir}\n`);
|
||||
|
||||
let migrated = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(blogDir, file);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||
const { data, content } = matter(fileContent);
|
||||
|
||||
const frontmatter = data as PostFrontmatter;
|
||||
const slug = file.replace('.mdx', '');
|
||||
|
||||
try {
|
||||
const record = await pb.collection('posts').create({
|
||||
title: frontmatter.title,
|
||||
description: frontmatter.description,
|
||||
author: frontmatter.author,
|
||||
category: frontmatter.category,
|
||||
categoryColor: frontmatter.categoryColor,
|
||||
date: frontmatter.date,
|
||||
readTime: frontmatter.readTime,
|
||||
imageUrl: frontmatter.imageUrl,
|
||||
slug: slug,
|
||||
draft: frontmatter.draft ?? false,
|
||||
content: content,
|
||||
});
|
||||
|
||||
console.log(`✅ Мигрирован: ${slug}`);
|
||||
migrated++;
|
||||
} catch (e: any) {
|
||||
console.error(`❌ Ошибка миграции ${slug}:`, e.response?.data || e.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 Готово! Мигрировано: ${migrated}, пропущено: ${skipped}`);
|
||||
}
|
||||
|
||||
migrate().catch(console.error);
|
||||
Loading…
Add table
Add a link
Reference in a new issue