Новы глобальные изменения компонентоы
This commit is contained in:
parent
5d7bb04bf1
commit
a269d3459e
43 changed files with 1667 additions and 517 deletions
70
backend/pb_migrations/1776343047_created_post_votes.js
Normal file
70
backend/pb_migrations/1776343047_created_post_votes.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
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": "relation1267270444",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "post_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"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_941672112",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "post_votes",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
28
backend/pb_migrations/1776343064_updated_post_votes.js
Normal file
28
backend/pb_migrations/1776343064_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(2, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2809058197",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation2809058197")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
backend/pb_migrations/1776343096_updated_post_votes.js
Normal file
28
backend/pb_migrations/1776343096_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(3, new Field({
|
||||
"hidden": false,
|
||||
"id": "select1002219032",
|
||||
"maxSelect": 1,
|
||||
"name": "vote_type",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"like"
|
||||
]
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("select1002219032")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
41
backend/pb_migrations/1776343107_updated_post_votes.js
Normal file
41
backend/pb_migrations/1776343107_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(3, new Field({
|
||||
"hidden": false,
|
||||
"id": "select1002219032",
|
||||
"maxSelect": 1,
|
||||
"name": "vote_type",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"like",
|
||||
"dislike"
|
||||
]
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(3, new Field({
|
||||
"hidden": false,
|
||||
"id": "select1002219032",
|
||||
"maxSelect": 1,
|
||||
"name": "vote_type",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"like"
|
||||
]
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
22
backend/pb_migrations/1776343360_updated_post_votes.js
Normal file
22
backend/pb_migrations/1776343360_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"listRule": "",
|
||||
"viewRule": ""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"listRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
24
backend/pb_migrations/1776344533_updated_post_votes.js
Normal file
24
backend/pb_migrations/1776344533_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": null,
|
||||
"listRule": "",
|
||||
"viewRule": ""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
22
backend/pb_migrations/1776344561_updated_post_votes.js
Normal file
22
backend/pb_migrations/1776344561_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"deleteRule": null,
|
||||
"updateRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
40
backend/pb_migrations/1776352196_updated_post_votes.js
Normal file
40
backend/pb_migrations/1776352196_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(1, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1125843985",
|
||||
"hidden": false,
|
||||
"id": "relation1267270444",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "post",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(1, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1125843985",
|
||||
"hidden": false,
|
||||
"id": "relation1267270444",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "post_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
40
backend/pb_migrations/1776352206_updated_post_votes.js
Normal file
40
backend/pb_migrations/1776352206_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(2, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2809058197",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(2, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2809058197",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
backend/pb_migrations/1776353507_updated_post_votes.js
Normal file
28
backend/pb_migrations/1776353507_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation2809058197")
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(2, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2809058197",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
backend/pb_migrations/1776353523_updated_post_votes.js
Normal file
28
backend/pb_migrations/1776353523_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(3, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2375276105",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation2375276105")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
22
backend/pb_migrations/1776353710_updated_post_votes.js
Normal file
22
backend/pb_migrations/1776353710_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"listRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
20
backend/pb_migrations/1776353752_updated_post_votes.js
Normal file
20
backend/pb_migrations/1776353752_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": "@request.auth.id != \"\""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
24
backend/pb_migrations/1776354539_updated_post_votes.js
Normal file
24
backend/pb_migrations/1776354539_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": null,
|
||||
"listRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
97
backend/pb_migrations/1776355090_deleted_post_votes.js
Normal file
97
backend/pb_migrations/1776355090_deleted_post_votes.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112");
|
||||
|
||||
return app.delete(collection);
|
||||
}, (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"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2375276105",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1125843985",
|
||||
"hidden": false,
|
||||
"id": "relation1267270444",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "post",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "select1002219032",
|
||||
"maxSelect": 1,
|
||||
"name": "vote_type",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"like",
|
||||
"dislike"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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_941672112",
|
||||
"indexes": [],
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"name": "post_votes",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\""
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
})
|
||||
57
backend/pb_migrations/1776355139_created_post_votes.js
Normal file
57
backend/pb_migrations/1776355139_created_post_votes.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
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"
|
||||
},
|
||||
{
|
||||
"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_941672112",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "post_votes",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
28
backend/pb_migrations/1776355194_updated_post_votes.js
Normal file
28
backend/pb_migrations/1776355194_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(1, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2375276105",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation2375276105")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
backend/pb_migrations/1776355205_updated_post_votes.js
Normal file
28
backend/pb_migrations/1776355205_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(2, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1125843985",
|
||||
"hidden": false,
|
||||
"id": "relation1519021197",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "post",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation1519021197")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
83
backend/pb_migrations/1776355216_deleted_post_votes.js
Normal file
83
backend/pb_migrations/1776355216_deleted_post_votes.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112");
|
||||
|
||||
return app.delete(collection);
|
||||
}, (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": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2375276105",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1125843985",
|
||||
"hidden": false,
|
||||
"id": "relation1519021197",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "post",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"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_941672112",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "post_votes",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
})
|
||||
83
backend/pb_migrations/1776355245_created_post_votes.js
Normal file
83
backend/pb_migrations/1776355245_created_post_votes.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
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": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2375276105",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1125843985",
|
||||
"hidden": false,
|
||||
"id": "relation1519021197",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "post",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"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_941672112",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "post_votes",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
29
backend/pb_migrations/1776355285_updated_post_votes.js
Normal file
29
backend/pb_migrations/1776355285_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(3, new Field({
|
||||
"hidden": false,
|
||||
"id": "select1002219032",
|
||||
"maxSelect": 1,
|
||||
"name": "vote_type",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"like",
|
||||
"dislike"
|
||||
]
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("select1002219032")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
24
backend/pb_migrations/1776355306_updated_post_votes.js
Normal file
24
backend/pb_migrations/1776355306_updated_post_votes.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": "",
|
||||
"listRule": "",
|
||||
"viewRule": ""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_941672112")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": null,
|
||||
"listRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
54
frontend/scripts/test-votes.ts
Normal file
54
frontend/scripts/test-votes.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Тест голосования
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const PB_URL = process.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||
|
||||
// Симулируем токен пользователя (нужно получить реальный)
|
||||
const testToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NzY5NTI4NjUsImlkIjoidGR0Z2JuNGFrb3BsZGhvIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.A1oacYog9de5GjZj5aNkHeRDWyjQKXvTSkEBFr4hi9Q';
|
||||
|
||||
async function testVoting() {
|
||||
const pb = new PocketBase(PB_URL);
|
||||
|
||||
// Симулируем авторизацию
|
||||
pb.authStore.save(testToken, { id: 'tdtgbn4akopldho', email: 'redibedi2019@gmail.com' });
|
||||
|
||||
console.log('Auth valid:', pb.authStore.isValid);
|
||||
console.log('User ID:', pb.authStore.model?.id);
|
||||
|
||||
// Тест: получить голоса поста
|
||||
try {
|
||||
const postId = 'test-post-id';
|
||||
const votes = await pb.collection('post_votes').getList(1, 1000, {
|
||||
filter: `post_id="${postId}"`,
|
||||
});
|
||||
|
||||
console.log('\n--- Тест получения голосов ---');
|
||||
console.log('Всего голосов:', votes.totalItems);
|
||||
|
||||
const likes = votes.items.filter(v => v.vote_type === 'like').length;
|
||||
const dislikes = votes.items.filter(v => v.vote_type === 'dislike').length;
|
||||
console.log('Likes:', likes);
|
||||
console.log('Dislikes:', dislikes);
|
||||
} catch (e) {
|
||||
console.error('Ошибка:', e);
|
||||
}
|
||||
|
||||
// Тест: создать голос
|
||||
try {
|
||||
console.log('\n--- Тест создания голоса ---');
|
||||
const newVote = await pb.collection('post_votes').create({
|
||||
post_id: 'test-post-123',
|
||||
user_id: 'tdtgbn4akopldho',
|
||||
vote_type: 'like',
|
||||
});
|
||||
console.log('Создан голос:', newVote.id);
|
||||
|
||||
// Удаляем тестовый голос
|
||||
await pb.collection('post_votes').delete(newVote.id);
|
||||
console.log('Удален тестовый голос');
|
||||
} catch (e) {
|
||||
console.error('Ошибка создания:', e);
|
||||
}
|
||||
}
|
||||
|
||||
testVoting();
|
||||
|
|
@ -39,7 +39,7 @@ const {
|
|||
modalTarget,
|
||||
bgImage = "",
|
||||
minHeight = "100vh",
|
||||
headerOffset = "80px",
|
||||
headerOffset = "10px",
|
||||
layout = "default",
|
||||
sideImage = "",
|
||||
sideImageAlt = "",
|
||||
|
|
@ -50,7 +50,6 @@ const {
|
|||
const showImage = layout === 'with-image' && sideImage;
|
||||
---
|
||||
|
||||
<!-- КРИТИЧНО: Инлайн-скрипт скрывает элементы ДО рендера, но только если JS работает -->
|
||||
<script is:inline>
|
||||
(function() {
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
|
|
@ -67,7 +66,7 @@ const showImage = layout === 'with-image' && sideImage;
|
|||
class="hero-section"
|
||||
style={`min-height: ${minHeight}; padding-top: ${headerOffset}; ${bgImage ? `background-image: url("${bgImage}")` : ''}`}
|
||||
>
|
||||
<!-- КРИТИЧЕСКИ: img для LCP - позволяет Lighthouse засечь Largest Contentful Paint -->
|
||||
|
||||
{bgImage && (
|
||||
<img
|
||||
src={bgImage}
|
||||
|
|
@ -185,7 +184,7 @@ const showImage = layout === 'with-image' && sideImage;
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* LCP helper image - скрыт визуально, но видим для Lighthouse */
|
||||
|
||||
.hero-bg-lcp {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -361,7 +360,7 @@ const showImage = layout === 'with-image' && sideImage;
|
|||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 3.5rem;
|
||||
margin-bottom: 2.5rem;
|
||||
max-width: 580px;
|
||||
}
|
||||
|
||||
|
|
@ -461,7 +460,7 @@ const showImage = layout === 'with-image' && sideImage;
|
|||
.hero-grid {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 1.5rem 1.5rem 2rem;
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
|
|
@ -493,7 +492,7 @@ const showImage = layout === 'with-image' && sideImage;
|
|||
|
||||
@media (max-width: 767px) {
|
||||
.hero-section {
|
||||
padding-top: 70px;
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
|
|
@ -516,7 +515,7 @@ const showImage = layout === 'with-image' && sideImage;
|
|||
|
||||
@media (max-width: 480px) {
|
||||
.hero-section {
|
||||
padding-top: 70px;
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
|
|
|
|||
180
frontend/src/components/base/Toast.astro
Normal file
180
frontend/src/components/base/Toast.astro
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
---
|
||||
---
|
||||
|
||||
<div class="toast-container"></div>
|
||||
|
||||
<script>
|
||||
class ToastManager {
|
||||
static instance: ToastManager;
|
||||
container: HTMLElement | null = null;
|
||||
|
||||
static getInstance() {
|
||||
if (!ToastManager.instance) {
|
||||
ToastManager.instance = new ToastManager();
|
||||
}
|
||||
return ToastManager.instance;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.container) {
|
||||
this.container = document.querySelector('.toast-container');
|
||||
if (!this.container) {
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = 'toast-container';
|
||||
document.body.insertBefore(this.container, document.body.firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
show(message: string, type: string = 'info', duration: number = 3000) {
|
||||
this.init();
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast-item toast-${type}`;
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">
|
||||
${type === 'success' ? '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>' : ''}
|
||||
${type === 'error' ? '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>' : ''}
|
||||
${type === 'warning' ? '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512"><path fill="currentColor" d="M240 176h32v176h-32zm0 208h32v32h-32z"/><path fill="currentColor" d="M274.014 16h-36.028L16 445.174V496h480v-50.826ZM464 464H48v-11.041L256 50.826l208 402.133Z"/></svg>' : ''}
|
||||
${type === 'info' ? '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>' : ''}
|
||||
</div>
|
||||
<span class="toast-message">${message}</span>
|
||||
<button class="toast-close" aria-label="Закрыть">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
const closeBtn = toast.querySelector('.toast-close');
|
||||
closeBtn?.addEventListener('click', () => this.remove(toast));
|
||||
|
||||
this.container?.appendChild(toast);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add('show');
|
||||
});
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => this.remove(toast), duration);
|
||||
}
|
||||
}
|
||||
|
||||
remove(toast: Element) {
|
||||
toast.classList.remove('show');
|
||||
toast.classList.add('hide');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).toast = ToastManager.getInstance();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
:global(.toast-item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.2);
|
||||
color: #fff;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
transform: translateX(120%);
|
||||
opacity: 0;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
:global(.toast-item.show) {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:global(.toast-item.hide) {
|
||||
transform: translateX(120%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:global(.toast-success) {
|
||||
border-left-color: #22c55e;
|
||||
}
|
||||
|
||||
:global(.toast-error) {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
:global(.toast-warning) {
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
|
||||
:global(.toast-info) {
|
||||
border-left-color: #3b82f6;
|
||||
}
|
||||
|
||||
:global(.toast-item .toast-icon) {
|
||||
flex-shrink: 0;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
:global(.toast-item .toast-icon svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.toast-success .toast-icon) { color: #22c55e; }
|
||||
:global(.toast-error .toast-icon) { color: #ef4444; }
|
||||
:global(.toast-warning .toast-icon) { color: #f59e0b; }
|
||||
:global(.toast-info .toast-icon) { color: #3b82f6; }
|
||||
|
||||
:global(.toast-item .toast-message) {
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
:global(.toast-item .toast-close) {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.toast-item .toast-close:hover) {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:global(.toast-item .toast-close svg) {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.toast-container {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -9,7 +9,12 @@ const { initialLikes = 0, initialDislikes = 0, postId } = Astro.props;
|
|||
---
|
||||
|
||||
<div class="post-reactions" data-post-id={postId}>
|
||||
<button class="reaction-btn like-btn" data-action="like" aria-label="Нравится">
|
||||
<button
|
||||
class="reaction-btn like-btn"
|
||||
data-action="like"
|
||||
aria-label="Нравится"
|
||||
type="button"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="reaction-icon">
|
||||
<path d="M7 10v12"></path>
|
||||
<path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"></path>
|
||||
|
|
@ -17,7 +22,12 @@ const { initialLikes = 0, initialDislikes = 0, postId } = Astro.props;
|
|||
<span class="reaction-count" data-count="likes">{initialLikes}</span>
|
||||
</button>
|
||||
|
||||
<button class="reaction-btn dislike-btn" data-action="dislike" aria-label="Не нравится">
|
||||
<button
|
||||
class="reaction-btn dislike-btn"
|
||||
data-action="dislike"
|
||||
aria-label="Не нравится"
|
||||
type="button"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="reaction-icon">
|
||||
<path d="M17 14V2"></path>
|
||||
<path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"></path>
|
||||
|
|
@ -87,53 +97,160 @@ const { initialLikes = 0, initialDislikes = 0, postId } = Astro.props;
|
|||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.toast-link) {
|
||||
color: #60a5fa;
|
||||
text-decoration: underline;
|
||||
font-size: 0.875rem;
|
||||
margin-left: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.toast-link:hover) {
|
||||
color: #93c5fd;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.post-reactions').forEach((container) => {
|
||||
const likeBtn = container.querySelector('.like-btn');
|
||||
const dislikeBtn = container.querySelector('.dislike-btn');
|
||||
import { pb } from '../../lib/pb';
|
||||
|
||||
console.log('PostReactionButtons loaded');
|
||||
|
||||
function showToast(message: string, type: string = 'info', duration: number = 3000, link?: { text: string; href: string }) {
|
||||
console.log('showToast called:', message, type);
|
||||
const container = document.querySelector('.toast-container');
|
||||
console.log('toast-container found:', container);
|
||||
if (!container) {
|
||||
console.error('Toast container not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast-item toast-${type}`;
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">
|
||||
${type === 'success' ? '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>' : ''}
|
||||
${type === 'error' ? '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>' : ''}
|
||||
${type === 'warning' ? '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512"><path fill="currentColor" d="M240 176h32v176h-32zm0 208h32v32h-32z"/><path fill="currentColor" d="M274.014 16h-36.028L16 445.174V496h480v-50.826ZM464 464H48v-11.041L256 50.826l208 402.133Z"/></svg>' : ''}
|
||||
${type === 'info' ? '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>' : ''}
|
||||
</div>
|
||||
<span class="toast-message">${message}</span>
|
||||
${link ? `<a href="${link.href}" class="toast-link">${link.text}</a>` : ''}
|
||||
<button class="toast-close" aria-label="Закрыть">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
const closeBtn = toast.querySelector('.toast-close');
|
||||
closeBtn?.addEventListener('click', () => {
|
||||
toast.classList.remove('show');
|
||||
toast.classList.add('hide');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
});
|
||||
|
||||
container.appendChild(toast);
|
||||
requestAnimationFrame(() => toast.classList.add('show'));
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
toast.classList.add('hide');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVote(postId: string, voteType: 'like' | 'dislike') {
|
||||
const container = document.querySelector(`[data-post-id="${postId}"]`);
|
||||
if (!container) return;
|
||||
|
||||
const likeBtn = container.querySelector('.like-btn') as HTMLButtonElement;
|
||||
const dislikeBtn = container.querySelector('.dislike-btn') as HTMLButtonElement;
|
||||
const likesCount = container.querySelector('[data-count="likes"]');
|
||||
const dislikesCount = container.querySelector('[data-count="dislikes"]');
|
||||
|
||||
let likes = parseInt(likesCount?.textContent || '0');
|
||||
let dislikes = parseInt(dislikesCount?.textContent || '0');
|
||||
let userAction: 'like' | 'dislike' | null = null;
|
||||
if (!pb.authStore.isValid) {
|
||||
console.log('pb.authStore.isValid:', pb.authStore.isValid);
|
||||
console.log('pb.authStore.model:', pb.authStore.model);
|
||||
console.log('pb.authStore.token:', pb.authStore.token ? 'exists' : 'none');
|
||||
showToast('Войдите, чтобы голосовать', 'warning', 5000, { text: 'Войти', href: '/auth/sign-in' });
|
||||
return;
|
||||
}
|
||||
|
||||
likeBtn?.addEventListener('click', () => {
|
||||
if (userAction === 'like') {
|
||||
likes--;
|
||||
userAction = null;
|
||||
likeBtn.classList.remove('active');
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('post_id', postId);
|
||||
formData.append('vote_type', voteType);
|
||||
|
||||
const response = await fetch('/api/votes', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ post_id: postId, vote_type: voteType }),
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
console.error('Vote error response:', error);
|
||||
throw new Error(error.message || error.details || 'Ошибка голосования');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (likesCount) likesCount.textContent = result.likes.toString();
|
||||
if (dislikesCount) dislikesCount.textContent = result.dislikes.toString();
|
||||
|
||||
if (result.userVote === 'like') {
|
||||
likeBtn?.classList.add('active');
|
||||
dislikeBtn?.classList.remove('active');
|
||||
} else if (result.userVote === 'dislike') {
|
||||
dislikeBtn?.classList.add('active');
|
||||
likeBtn?.classList.remove('active');
|
||||
} else {
|
||||
if (userAction === 'dislike') {
|
||||
dislikes--;
|
||||
likeBtn?.classList.remove('active');
|
||||
dislikeBtn?.classList.remove('active');
|
||||
}
|
||||
likes++;
|
||||
userAction = 'like';
|
||||
likeBtn.classList.add('active');
|
||||
|
||||
showToast('Ваш голос учтён', 'success', 2000);
|
||||
} catch (error) {
|
||||
console.error('Ошибка голосования:', error);
|
||||
showToast('Не удалось отправить голос. Попробуйте позже.', 'error', 3000);
|
||||
}
|
||||
if (likesCount) likesCount.textContent = likes.toString();
|
||||
if (dislikesCount) dislikesCount.textContent = dislikes.toString();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.post-reactions').forEach((container) => {
|
||||
const postId = (container as HTMLElement).dataset.postId;
|
||||
const likeBtn = container.querySelector('.like-btn');
|
||||
const dislikeBtn = container.querySelector('.dislike-btn');
|
||||
|
||||
async function loadUserVote() {
|
||||
if (!pb.authStore.isValid) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/votes?post_id=${postId}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
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');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Ошибка загрузки голоса:', e);
|
||||
}
|
||||
}
|
||||
|
||||
loadUserVote();
|
||||
|
||||
likeBtn?.addEventListener('click', () => {
|
||||
if (postId) handleVote(postId, 'like');
|
||||
});
|
||||
|
||||
dislikeBtn?.addEventListener('click', () => {
|
||||
if (userAction === 'dislike') {
|
||||
dislikes--;
|
||||
userAction = null;
|
||||
dislikeBtn.classList.remove('active');
|
||||
} else {
|
||||
if (userAction === 'like') {
|
||||
likes--;
|
||||
likeBtn?.classList.remove('active');
|
||||
}
|
||||
dislikes++;
|
||||
userAction = 'dislike';
|
||||
dislikeBtn.classList.add('active');
|
||||
}
|
||||
if (likesCount) likesCount.textContent = likes.toString();
|
||||
if (dislikesCount) dislikesCount.textContent = dislikes.toString();
|
||||
if (postId) handleVote(postId, 'dislike');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
---
|
||||
import telegramIcon from '../../icons/telegram.svg?raw';
|
||||
import vkIcon from '../../icons/vk.svg?raw';
|
||||
import whatsappIcon from '../../icons/whatsapp.svg?raw';
|
||||
import okIcon from '../../icons/ok.svg?raw';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
url: string;
|
||||
|
|
@ -22,9 +27,7 @@ const encodedUrl = encodeURIComponent(url);
|
|||
class="social-icon telegram"
|
||||
aria-label="Поделиться в Telegram"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
|
||||
</svg>
|
||||
<span class="icon-wrapper" set:html={telegramIcon} />
|
||||
</a>
|
||||
|
||||
<!-- VK -->
|
||||
|
|
@ -35,9 +38,7 @@ const encodedUrl = encodeURIComponent(url);
|
|||
class="social-icon vk"
|
||||
aria-label="Поделиться в VK"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M15.684 0H8.316C1.592 0 0 1.592 0 8.316v7.368C0 22.408 1.592 24 8.316 24h7.368C22.408 24 24 22.408 24 15.684V8.316C24 1.592 22.391 0 15.684 0zm3.692 17.123h-1.744c-.66 0-.864-.525-2.05-1.727-1.033-1-1.49-1.135-1.744-1.135-.356 0-.458.102-.458.593v1.575c0 .424-.135.678-1.253.678-1.846 0-3.896-1.118-5.335-3.202C4.624 10.857 4.03 8.57 4.03 8.096c0-.254.102-.491.593-.491h1.744c.44 0 .61.203.78.677.847 2.49 2.27 4.675 2.862 4.675.22 0 .322-.102.322-.66V9.721c-.068-1.186-.695-1.287-.695-1.71 0-.204.17-.407.44-.407h2.744c.373 0 .508.203.508.643v3.473c0 .372.17.508.271.508.22 0 .407-.136.813-.542 1.27-1.422 2.18-3.61 2.18-3.61.119-.254.322-.491.763-.491h1.744c.525 0 .644.27.525.643-.22 1.017-2.354 4.031-2.354 4.031-.186.305-.254.44 0 .78.186.254.796.779 1.203 1.253.745.847 1.32 1.558 1.473 2.05.17.49-.085.744-.576.744z"/>
|
||||
</svg>
|
||||
<span class="icon-wrapper" set:html={vkIcon} />
|
||||
</a>
|
||||
|
||||
<!-- WhatsApp -->
|
||||
|
|
@ -48,9 +49,7 @@ const encodedUrl = encodeURIComponent(url);
|
|||
class="social-icon whatsapp"
|
||||
aria-label="Поделиться в WhatsApp"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413z"/>
|
||||
</svg>
|
||||
<span class="icon-wrapper" set:html={whatsappIcon} />
|
||||
</a>
|
||||
|
||||
<!-- Одноклассники -->
|
||||
|
|
@ -61,9 +60,7 @@ const encodedUrl = encodeURIComponent(url);
|
|||
class="social-icon odnoklassniki"
|
||||
aria-label="Поделиться в Одноклассниках"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M14.505 17.043c.098.07.222.102.373.102.15 0 .274-.032.372-.102a.597.597 0 0 0 .214-.348.89.89 0 0 0 .064-.408 1.09 1.09 0 0 0-.136-.443c-.09-.153-.212-.277-.365-.373l-.01-.006c-.154-.097-.275-.222-.363-.373a.997.997 0 0 1-.136-.438.86.86 0 0 1 .063-.397.577.577 0 0 1 .204-.338.636.636 0 0 1 .372-.102c.15 0 .274.034.372.102a.577.577 0 0 1 .204.338.86.86 0 0 1 .063.397.997.997 0 0 1-.136.438c-.088.151-.209.276-.363.373l-.01.006c-.153.096-.275.22-.365.373a1.09 1.09 0 0 0-.136.443.89.89 0 0 0 .064.408.597.597 0 0 0 .214.348zM12 13.5c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5 1.5.672 1.5 1.5-.672 1.5-1.5 1.5zm6.793-9.216A11.952 11.952 0 0 0 12 .5C5.373.5.008 5.865.008 12.5c0 2.768.929 5.372 2.534 7.458.065.085.136.165.208.244.016.018.034.034.05.051l.01.011c.135.147.277.288.425.421.043.039.087.077.131.114.129.111.263.216.4.315.076.056.154.109.233.161.116.077.235.149.357.217.095.054.191.105.29.151.115.054.234.102.355.145.097.034.195.065.295.091.12.032.241.058.365.079.097.016.195.03.293.039.131.012.264.017.398.017.135 0 .268-.005.4-.017.098-.009.196-.023.293-.039.124-.021.245-.047.365-.079.1-.026.198-.057.295-.091.121-.043.24-.091.355-.145.099-.046.195-.097.29-.151.122-.068.241-.14.357-.217.079-.052.157-.105.233-.161.137-.099.271-.204.4-.315.044-.037.088-.075.131-.114.148-.133.29-.274.425-.421l.01-.011c.016-.017.034-.033.05-.051.072-.079.143-.159.208-.244A11.953 11.953 0 0 0 23.992 12.5C23.992 5.865 18.627.5 12 .5z"/>
|
||||
</svg>
|
||||
<span class="icon-wrapper" set:html={okIcon} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -97,7 +94,7 @@ const encodedUrl = encodeURIComponent(url);
|
|||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.social-icon svg {
|
||||
.social-icon :global(svg) {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,448 +0,0 @@
|
|||
---
|
||||
import Button from '@components/base/Button.astro';
|
||||
|
||||
const badgeText = "ЗАЩИТА ВОДИТЕЛЕЙ В СУРГУТЕ";
|
||||
const titleWhite = "Защитите свои права";
|
||||
const titleGold = "и водительское удостоверение";
|
||||
const description = "Профессиональная юридическая помощь при ДТП, спорах с ГИБДД и страховыми компаниями. Работаем на результат в судах ХМАО-Югры.";
|
||||
const btnPrimary = "Бесплатная консультация";
|
||||
const btnSecondary = "Мои услуги";
|
||||
|
||||
const bgImageUrl = "/images/home/bg_hero.avif";
|
||||
const lawyerImageUrl = "/images/home/avtourist-surgut.avif";
|
||||
---
|
||||
|
||||
<section class="hero-section">
|
||||
<img src={bgImageUrl} alt="" class="hero-bg-image" decoding="async" />
|
||||
<div class="hero-overlay"></div>
|
||||
|
||||
<div class="site-container hero-grid">
|
||||
<div class="hero-content">
|
||||
<div class="badge animate-load" data-delay="100">
|
||||
<span class="status-dot"></span>
|
||||
{badgeText}
|
||||
</div>
|
||||
|
||||
<h1 class="hero-title">
|
||||
<span class="text-white animate-load" data-delay="300">{titleWhite}</span>
|
||||
<br />
|
||||
<span class="text-gold animate-load" data-delay="500">{titleGold}</span>
|
||||
</h1>
|
||||
|
||||
<p class="hero-description animate-load" data-delay="700">{description}</p>
|
||||
|
||||
<div class="hero-actions animate-load" data-delay="900">
|
||||
<Button variant="gold" size="lg" id="consultation-btn" data-modal-target="consultation-modal">
|
||||
{btnPrimary}
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" href="/services" class="btn-services">
|
||||
{btnSecondary}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-image-wrapper animate-load" data-delay="400">
|
||||
<div class="image-composition">
|
||||
<img src={lawyerImageUrl} alt="Юрист" class="main-image" width="380" height="500" fetchpriority="high" decoding="async" />
|
||||
<div class="experience-badge animate-load" data-delay="1100">
|
||||
<span class="exp-number">20+</span>
|
||||
<span class="exp-text">ЛЕТ ОПЫТА В СУДАХ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// Анимация при загрузке страницы
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const animatedElements = document.querySelectorAll('.animate-load');
|
||||
|
||||
animatedElements.forEach((el) => {
|
||||
const delay = parseInt((el as HTMLElement).dataset.delay || '0');
|
||||
|
||||
setTimeout(() => {
|
||||
el.classList.add('is-visible');
|
||||
}, delay);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.hero-section {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-image: url("/images/home/bg_hero.avif");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
margin: 0 !important;
|
||||
padding-top: 80px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-bg-image {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: linear-gradient(90deg, #0a2540 0%, rgba(10, 37, 64, 0.9) 50%, rgba(10, 37, 64, 0.7) 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
flex: 1.2;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
/* --- АНИМАЦИИ ПРИ ЗАГРУЗКЕ --- */
|
||||
.animate-load {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.animate-load.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Специальная анимация для изображения - сбоку */
|
||||
.hero-image-wrapper.animate-load {
|
||||
opacity: 0;
|
||||
transform: translateX(50px) scale(0.95);
|
||||
transition: opacity 1s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 1s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.hero-image-wrapper.animate-load.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
}
|
||||
|
||||
/* Анимация для бейджа опыта - снизу с масштабом */
|
||||
.experience-badge.animate-load {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.9);
|
||||
transition: opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.experience-badge.animate-load.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
/* Задержки для последовательного появления */
|
||||
.badge[data-delay="100"] { transition-delay: 0.1s; }
|
||||
.hero-image-wrapper[data-delay="400"] { transition-delay: 0.4s; }
|
||||
.text-white[data-delay="300"] { transition-delay: 0.3s; }
|
||||
.text-gold[data-delay="500"] { transition-delay: 0.5s; }
|
||||
.hero-description[data-delay="700"] { transition-delay: 0.7s; }
|
||||
.hero-actions[data-delay="900"] { transition-delay: 0.9s; }
|
||||
.experience-badge[data-delay="1100"] { transition-delay: 1.1s; }
|
||||
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 0.6rem;
|
||||
background-color: rgba(234, 194, 110, 0.15);
|
||||
border: 1px solid rgba(234, 194, 110, 0.3);
|
||||
color: #eac26e; padding: 0.5rem 1rem; border-radius: 6px;
|
||||
font-size: 0.8rem; font-weight: 700; letter-spacing: 1.5px; margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* Мерцающая точка "На связи" */
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #22c55e;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 6px rgba(34, 197, 94, 0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(2rem, 5vw, 3.8rem);
|
||||
line-height: 1.15;
|
||||
margin: 0 0 2rem 0;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.hero-title .text-white,
|
||||
.hero-title .text-gold {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.text-white { color: #ffffff; }
|
||||
.text-gold { color: #eac26e; }
|
||||
|
||||
.hero-description {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 2rem;
|
||||
max-width: 580px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.hero-actions > * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Кастомные стили для кнопки "Мои услуги" */
|
||||
.btn-services {
|
||||
background: transparent !important;
|
||||
border: 2px solid #ffffff !important;
|
||||
color: #ffffff !important;
|
||||
padding: 0.875rem 1.5rem !important;
|
||||
font-size: 1.125rem !important;
|
||||
line-height: 1.75rem !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
height: auto !important;
|
||||
min-height: 3.25rem !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-services::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.btn-services::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.2));
|
||||
transition: width 0.4s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.btn-services:hover {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
border-color: #eac26e !important;
|
||||
color: #eac26e !important;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(234, 194, 110, 0.3);
|
||||
}
|
||||
|
||||
.btn-services:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-services:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-services:active {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 15px rgba(234, 194, 110, 0.2);
|
||||
}
|
||||
|
||||
.hero-image-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-composition {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.image-composition::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 20px;
|
||||
right: -20px;
|
||||
bottom: 10px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
transform: rotate(3deg);
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
animation: fadeInRotate 0.8s ease 0.6s forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInRotate {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: rotate(0deg) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: rotate(3deg) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.main-image {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
border: 8px solid #ffffff;
|
||||
filter: grayscale(100%);
|
||||
box-shadow: 0 30px 60px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.experience-badge {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: -30px;
|
||||
background: #ffffff;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 15px 35px rgba(0,0,0,0.2);
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.exp-number {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 900;
|
||||
color: #1e3050;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.exp-text {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
color: #535e6c;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* АДАПТИВНОСТЬ */
|
||||
@media (max-width: 992px) {
|
||||
.hero-section {
|
||||
padding-top: 70px;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 1.5rem 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-image-wrapper {
|
||||
margin-top: 3rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.experience-badge {
|
||||
left: 0;
|
||||
bottom: -20px;
|
||||
}
|
||||
|
||||
/* На мобильных меняем направление анимации изображения */
|
||||
.hero-image-wrapper.animate-load {
|
||||
transform: translateY(40px) scale(0.95);
|
||||
}
|
||||
|
||||
.hero-image-wrapper.animate-load.is-visible {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.hero-section {
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero-actions > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-composition::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Уважаем prefers-reduced-motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-load,
|
||||
.hero-image-wrapper.animate-load,
|
||||
.experience-badge.animate-load {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.image-composition::before {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -121,7 +121,7 @@ const {
|
|||
description: "Полное ведение дела в суде: от подготовки искового до исполнения решения.",
|
||||
price: "от 30 000 ₽",
|
||||
icon: "🏛️",
|
||||
href: "/services/court-defense",
|
||||
href: "/services/court-representation",
|
||||
features: ["Подготовка иска/жалобы", "Представительство на заседаниях", "Сбор доказательств", "Исполнение решения"]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
1
frontend/src/icons/ok.svg
Normal file
1
frontend/src/icons/ok.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Logos free icons by Streamline - https://creativecommons.org/licenses/by/4.0/ --><path fill="currentColor" fill-rule="evenodd" d="M5 1a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4zm3.364 6.636a3.636 3.636 0 1 1 7.272 0a3.636 3.636 0 0 1-7.272 0M12 6.182a1.455 1.455 0 1 0 0 2.909a1.455 1.455 0 0 0 0-2.91ZM8.72 12.24a1.2 1.2 0 1 0-1.44 1.92c.533.4 1.684 1.015 3.17 1.294l-2.498 2.497a1.2 1.2 0 1 0 1.697 1.697L12 17.298l2.351 2.35a1.2 1.2 0 1 0 1.698-1.697l-2.498-2.497c1.485-.279 2.636-.894 3.169-1.294a1.2 1.2 0 1 0-1.44-1.92c-.36.27-1.66.96-3.28.96s-2.92-.69-3.28-.96" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 716 B |
1
frontend/src/icons/telegram.svg
Normal file
1
frontend/src/icons/telegram.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
|
||||
|
After Width: | Height: | Size: 722 B |
1
frontend/src/icons/vk.svg
Normal file
1
frontend/src/icons/vk.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.684 0H8.316C1.592 0 0 1.592 0 8.316v7.368C0 22.408 1.592 24 8.316 24h7.368C22.408 24 24 22.408 24 15.684V8.316C24 1.592 22.391 0 15.684 0zm3.692 17.123h-1.744c-.66 0-.864-.525-2.05-1.727-1.033-1-1.49-1.135-1.744-1.135-.356 0-.458.102-.458.593v1.575c0 .424-.135.678-1.253.678-1.846 0-3.896-1.118-5.335-3.202C4.624 10.857 4.03 8.57 4.03 8.096c0-.254.102-.491.593-.491h1.744c.44 0 .61.203.78.677.847 2.49 2.27 4.675 2.862 4.675.22 0 .322-.102.322-.66V9.721c-.068-1.186-.695-1.287-.695-1.71 0-.204.17-.407.44-.407h2.744c.373 0 .508.203.508.643v3.473c0 .372.17.508.271.508.22 0 .407-.136.813-.542 1.27-1.422 2.18-3.61 2.18-3.61.119-.254.322-.491.763-.491h1.744c.525 0 .644.27.525.643-.22 1.017-2.354 4.031-2.354 4.031-.186.305-.254.44 0 .78.186.254.796.779 1.203 1.253.745.847 1.32 1.558 1.473 2.05.17.49-.085.744-.576.744z"/>
|
||||
|
After Width: | Height: | Size: 914 B |
1
frontend/src/icons/warning.svg
Normal file
1
frontend/src/icons/warning.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><!-- Icon from CoreUI Free by creativeLabs Łukasz Holeczek - https://creativecommons.org/licenses/by/4.0/ --><path fill="currentColor" d="M240 176h32v176h-32zm0 208h32v32h-32z"/><path fill="currentColor" d="M274.014 16h-36.028L16 445.174V496h480v-50.826ZM464 464H48v-11.041L256 50.826l208 402.133Z"/></svg>
|
||||
|
After Width: | Height: | Size: 392 B |
1
frontend/src/icons/whatsapp.svg
Normal file
1
frontend/src/icons/whatsapp.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413z"/>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -6,6 +6,7 @@ import Header from "@components/layout/header/Header.astro";
|
|||
import Footer from "@components/layout/footer/Footer.astro";
|
||||
import Breadcrumbs from "@components/base/Breadcrumbs.astro";
|
||||
import ConsultationModal from "@components/base/ConsultationModal.astro";
|
||||
import Toast from "@components/base/Toast.astro";
|
||||
import PostSocialShare from "@components/blog/PostSocialShare.astro";
|
||||
import PostReactionButtons from "@components/blog/PostReactionButtons.astro";
|
||||
|
||||
|
|
@ -59,6 +60,7 @@ const {
|
|||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
</head>
|
||||
<body>
|
||||
<Toast />
|
||||
<Header />
|
||||
<main class="main-content">
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import Header from "@components/layout/header/Header.astro";
|
|||
import Footer from "@components/layout/footer/Footer.astro";
|
||||
import Breadcrumbs from "@components/base/Breadcrumbs.astro";
|
||||
import ConsultationModal from "@components/base/ConsultationModal.astro";
|
||||
import Toast from "@components/base/Toast.astro";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
|
|
@ -32,6 +33,7 @@ const { title, description, canonicalLink, breadcrumbs } = Astro.props;
|
|||
<meta name="yandex-verification" content="be3edfd138348e43" />
|
||||
</head>
|
||||
<body>
|
||||
<Toast />
|
||||
<Header />
|
||||
<main class="main-content">
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,101 @@ const PB_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
|||
|
||||
export const pb = new PocketBase(PB_URL);
|
||||
|
||||
pb.collection('_superusers').authRefresh().catch(() => {});
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const userStr = localStorage.getItem('user');
|
||||
|
||||
// Инициализируем куку из localStorage если её нет
|
||||
if (token && !document.cookie.includes('pb_auth')) {
|
||||
document.cookie = `pb_auth=${token}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
|
||||
}
|
||||
|
||||
if (token && userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
pb.authStore.save(token, user);
|
||||
} catch (e) {
|
||||
console.error('Failed to restore auth:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PostVotes {
|
||||
id: string;
|
||||
post_id: string;
|
||||
user_id: string;
|
||||
vote_type: 'like' | 'dislike';
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface VoteStats {
|
||||
likes: number;
|
||||
dislikes: number;
|
||||
userVote: 'like' | 'dislike' | null;
|
||||
}
|
||||
|
||||
export async function getPostVotes(postId: string): Promise<VoteStats> {
|
||||
const votes = await pb.collection('post_votes').getList(1, 1000, {
|
||||
filter: `post_id="${postId}"`,
|
||||
});
|
||||
|
||||
const likes = votes.items.filter((v) => v.vote_type === 'like').length;
|
||||
const dislikes = votes.items.filter((v) => v.vote_type === 'dislike').length;
|
||||
|
||||
let userVote: 'like' | 'dislike' | null = null;
|
||||
if (pb.authStore.isValid) {
|
||||
const userId = pb.authStore.model?.id;
|
||||
const userVoteRecord = votes.items.find((v) => v.user_id === userId);
|
||||
if (userVoteRecord) {
|
||||
userVote = userVoteRecord.vote_type as 'like' | 'dislike';
|
||||
}
|
||||
}
|
||||
|
||||
return { likes, dislikes, userVote };
|
||||
}
|
||||
|
||||
export async function getPostVotesStats(postId: string): Promise<{ likes: number; dislikes: number }> {
|
||||
// Получаем данные поста напрямую (likes/dislikes хранятся в самом посте)
|
||||
const post = await pb.collection('posts').getOne(postId);
|
||||
|
||||
return {
|
||||
likes: post.likes || 0,
|
||||
dislikes: post.dislikes || 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function vote(postId: string, voteType: 'like' | 'dislike'): Promise<VoteStats> {
|
||||
const userId = pb.authStore.model?.id;
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('Требуется авторизация');
|
||||
}
|
||||
|
||||
const existingVotes = await pb.collection('post_votes').getList(1, 1, {
|
||||
filter: `post_id="${postId}" && user_id="${userId}"`,
|
||||
});
|
||||
|
||||
if (existingVotes.items.length > 0) {
|
||||
const existingVote = existingVotes.items[0] as unknown as PostVotes;
|
||||
|
||||
if (existingVote.vote_type === voteType) {
|
||||
await pb.collection('post_votes').delete(existingVote.id);
|
||||
} else {
|
||||
await pb.collection('post_votes').update(existingVote.id, {
|
||||
vote_type: voteType,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await pb.collection('post_votes').create({
|
||||
post_id: postId,
|
||||
user_id: userId,
|
||||
vote_type: voteType,
|
||||
});
|
||||
}
|
||||
|
||||
return getPostVotes(postId);
|
||||
}
|
||||
|
||||
export async function getPosts(options?: {
|
||||
page?: number;
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|||
}), { status: 401 });
|
||||
}
|
||||
|
||||
cookies.set('pb_auth', JSON.stringify(pb.authStore.exportToCookie()), {
|
||||
cookies.set('pb_auth', authData.token, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
|
|
|||
277
frontend/src/pages/api/votes.ts
Normal file
277
frontend/src/pages/api/votes.ts
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
|
||||
const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||
|
||||
// Валидация ID PocketBase (15 символов, буквы и цифры)
|
||||
const POCKETBASE_ID_REGEX = /^[a-z0-9]{15}$/;
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
export const POST: APIRoute = async ({ request, cookies }) => {
|
||||
try {
|
||||
// Получаем токен из куки
|
||||
const token = cookies.get('pb_auth')?.value;
|
||||
|
||||
if (!token) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Требуется авторизация' }),
|
||||
{ status: 401, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
}
|
||||
|
||||
// Проверяем токен и получаем пользователя через auth-refresh
|
||||
const authResponse = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/users/auth-refresh`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!authResponse.ok) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Недействительная сессия' }),
|
||||
{ status: 401, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
}
|
||||
|
||||
const authData = await authResponse.json();
|
||||
|
||||
// ВАЖНО: Явно берем id, а не email
|
||||
const userId = authData.record?.id;
|
||||
const userEmail = authData.record?.email;
|
||||
|
||||
console.log('[Vote API] Auth data:', { userId, userEmail, record: authData.record });
|
||||
|
||||
// Защита: проверяем что это действительно ID, а не email
|
||||
if (!userId || EMAIL_REGEX.test(userId)) {
|
||||
console.error('[Vote API] Invalid userId (looks like email):', userId);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Ошибка идентификации пользователя' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
}
|
||||
|
||||
if (!POCKETBASE_ID_REGEX.test(userId)) {
|
||||
console.error('[Vote API] Invalid userId format:', userId);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Некорректный ID пользователя' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { post_id, vote_type } = body;
|
||||
|
||||
if (!post_id || !POCKETBASE_ID_REGEX.test(post_id)) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Некорректный post_id' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
}
|
||||
|
||||
if (!vote_type || !['like', 'dislike'].includes(vote_type)) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Некорректный vote_type' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
}
|
||||
|
||||
// Проверяем существующий голос
|
||||
const existingVoteRes = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/post_votes/records?` +
|
||||
new URLSearchParams({
|
||||
filter: `post="${post_id}" && user="${userId}"`,
|
||||
}),
|
||||
{
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
}
|
||||
);
|
||||
|
||||
let userVote: 'like' | 'dislike' | null = null;
|
||||
let method = 'POST';
|
||||
let url = `${POCKETBASE_URL}/api/collections/post_votes/records`;
|
||||
let voteId = null;
|
||||
|
||||
if (existingVoteRes.ok) {
|
||||
const existingData = await existingVoteRes.json();
|
||||
if (existingData.items?.length > 0) {
|
||||
const existing = existingData.items[0];
|
||||
voteId = existing.id;
|
||||
|
||||
if (existing.vote_type === vote_type) {
|
||||
// Удаляем голос (toggle off)
|
||||
method = 'DELETE';
|
||||
url = `${POCKETBASE_URL}/api/collections/post_votes/records/${voteId}`;
|
||||
} else {
|
||||
// Обновляем
|
||||
method = 'PATCH';
|
||||
url = `${POCKETBASE_URL}/api/collections/post_votes/records/${voteId}`;
|
||||
userVote = vote_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Выполняем операцию с голосом
|
||||
const voteBody = method === 'POST' ? JSON.stringify({
|
||||
post: post_id,
|
||||
user: userId, // Теперь точно ID, а не email
|
||||
vote_type,
|
||||
}) : method === 'PATCH' ? JSON.stringify({ vote_type }) : null;
|
||||
|
||||
const voteRes = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: voteBody,
|
||||
});
|
||||
|
||||
if (!voteRes.ok && method !== 'DELETE') {
|
||||
const errorText = await voteRes.text();
|
||||
console.error('[Vote API] Failed to save vote:', errorText);
|
||||
throw new Error('Failed to save vote');
|
||||
}
|
||||
|
||||
if (method === 'POST') userVote = vote_type;
|
||||
if (method === 'DELETE') userVote = null;
|
||||
|
||||
// Получаем актуальные счетчики
|
||||
const votesRes = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/post_votes/records?` +
|
||||
new URLSearchParams({
|
||||
filter: `post="${post_id}"`,
|
||||
fields: 'vote_type',
|
||||
}),
|
||||
{
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
}
|
||||
);
|
||||
|
||||
let likes = 0;
|
||||
let dislikes = 0;
|
||||
|
||||
if (votesRes.ok) {
|
||||
const votesData = await votesRes.json();
|
||||
likes = votesData.items.filter((v: any) => v.vote_type === 'like').length;
|
||||
dislikes = votesData.items.filter((v: any) => v.vote_type === 'dislike').length;
|
||||
}
|
||||
|
||||
// Обновляем счетчики в посте
|
||||
await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/posts/records/${post_id}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ likes, dislikes }),
|
||||
}
|
||||
);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ likes, dislikes, userVote }),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Vote API] Error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Внутренняя ошибка сервера' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const GET: APIRoute = async ({ url, cookies }) => {
|
||||
try {
|
||||
const postId = url.searchParams.get('post_id');
|
||||
const token = cookies.get('pb_auth')?.value;
|
||||
|
||||
if (!postId || !POCKETBASE_ID_REGEX.test(postId)) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Некорректный post_id' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
}
|
||||
|
||||
// Получаем пост со счетчиками
|
||||
const postRes = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/posts/records/${postId}`,
|
||||
{
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||
}
|
||||
);
|
||||
|
||||
if (!postRes.ok) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Пост не найден' }),
|
||||
{ status: 404, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
}
|
||||
|
||||
const post = await postRes.json();
|
||||
|
||||
// Определяем голос текущего пользователя
|
||||
let userVote: 'like' | 'dislike' | null = null;
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
// Проверяем токен
|
||||
const authRes = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/users/auth-refresh`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
}
|
||||
);
|
||||
|
||||
if (authRes.ok) {
|
||||
const authData = await authRes.json();
|
||||
const userId = authData.record?.id;
|
||||
|
||||
if (userId && POCKETBASE_ID_REGEX.test(userId)) {
|
||||
const userVoteRes = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/post_votes/records?` +
|
||||
new URLSearchParams({
|
||||
filter: `post="${postId}" && user="${userId}"`,
|
||||
}),
|
||||
{
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
}
|
||||
);
|
||||
|
||||
if (userVoteRes.ok) {
|
||||
const userVoteData = await userVoteRes.json();
|
||||
if (userVoteData.items?.length > 0) {
|
||||
userVote = userVoteData.items[0].vote_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Vote API] Error fetching user vote:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
likes: post.likes || 0,
|
||||
dislikes: post.dislikes || 0,
|
||||
userVote,
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Vote API GET] Error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Внутренняя ошибка сервера' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' }}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -462,10 +462,13 @@ import { SITE_URL } from '@constants';
|
|||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.token) {
|
||||
// Сохраняем токен
|
||||
// Сохраняем токен в localStorage
|
||||
localStorage.setItem('auth_token', data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
|
||||
// Сохраняем токен в куку для API
|
||||
document.cookie = `pb_auth=${data.token}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
|
||||
|
||||
// Перенаправляем в личный кабинет
|
||||
window.location.href = '/cabinet';
|
||||
} else if (data.error?.includes('подтверждён')) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ 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 { getPostBySlug, getPosts, getPostImageUrl } from '@lib/pb';
|
||||
import { getPostBySlug, getPosts, getPostImageUrl, getPostVotesStats } from '@lib/pb';
|
||||
import { marked } from 'marked';
|
||||
|
||||
export const prerender = false;
|
||||
|
|
@ -21,6 +21,8 @@ if (!post) {
|
|||
return Astro.redirect('/blog');
|
||||
}
|
||||
|
||||
const { likes = 0, dislikes = 0 } = await getPostVotesStats(post.id).catch(() => ({ likes: 0, dislikes: 0 }));
|
||||
|
||||
// Конвертируем markdown в HTML
|
||||
const contentHtml = marked(post.content || '');
|
||||
|
||||
|
|
@ -71,8 +73,8 @@ const heroImage = getPostImageUrl(post);
|
|||
readTime={post.readTime}
|
||||
postId={post.id}
|
||||
postUrl={currentUrl}
|
||||
initialLikes={12}
|
||||
initialDislikes={2}
|
||||
initialLikes={likes}
|
||||
initialDislikes={dislikes}
|
||||
>
|
||||
<!-- Содержимое статьи -->
|
||||
<div class="post-content" set:html={contentHtml} />
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { SITE_URL } from '@constants';
|
|||
sideImageAlt="Юрист"
|
||||
bgImage="/images/home/bg_hero.avif"
|
||||
minHeight="100vh"
|
||||
headerOffset="80px"
|
||||
experienceBadge={{
|
||||
number: "20+",
|
||||
text: "ЛЕТ ОПЫТА В СУДАХ"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"],
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@styles/*": ["src/styles/*"],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue