Merge branch 'dev' into hotfix/following
This commit is contained in:
commit
e7f17c3cc9
35
biome.json
35
biome.json
|
@ -1,21 +1,8 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
||||||
"files": {
|
"files": {
|
||||||
"include": [
|
"include": ["*.tsx", "*.ts", "*.js", "*.json"],
|
||||||
"*.tsx",
|
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.gen.ts", "*.d.ts"]
|
||||||
"*.ts",
|
|
||||||
"*.js",
|
|
||||||
"*.json"
|
|
||||||
],
|
|
||||||
"ignore": [
|
|
||||||
"./dist",
|
|
||||||
"./node_modules",
|
|
||||||
".husky",
|
|
||||||
"docs",
|
|
||||||
"gen",
|
|
||||||
"*.gen.ts",
|
|
||||||
"*.d.ts"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"defaultBranch": "dev",
|
"defaultBranch": "dev",
|
||||||
|
@ -23,19 +10,13 @@
|
||||||
},
|
},
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"ignore": [
|
"ignore": ["./api", "./gen"]
|
||||||
"./api",
|
|
||||||
"./gen"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
"indentWidth": 2,
|
"indentWidth": 2,
|
||||||
"lineWidth": 108,
|
"lineWidth": 108,
|
||||||
"ignore": [
|
"ignore": ["./src/graphql/schema", "./gen"]
|
||||||
"./src/graphql/schema",
|
|
||||||
"./gen"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"javascript": {
|
"javascript": {
|
||||||
"formatter": {
|
"formatter": {
|
||||||
|
@ -48,13 +29,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"ignore": [
|
"ignore": ["*.scss", "*.md", ".DS_Store", "*.svg", "*.d.ts"],
|
||||||
"*.scss",
|
|
||||||
"*.md",
|
|
||||||
".DS_Store",
|
|
||||||
"*.svg",
|
|
||||||
"*.d.ts"
|
|
||||||
],
|
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"all": true,
|
"all": true,
|
||||||
|
|
543
package-lock.json
generated
543
package-lock.json
generated
|
@ -34,9 +34,8 @@
|
||||||
"@solid-primitives/memo": "1.2.4",
|
"@solid-primitives/memo": "1.2.4",
|
||||||
"@solid-primitives/pagination": "0.2.10",
|
"@solid-primitives/pagination": "0.2.10",
|
||||||
"@solid-primitives/share": "2.0.4",
|
"@solid-primitives/share": "2.0.4",
|
||||||
"@solid-primitives/storage": "1.3.9",
|
"@solid-primitives/storage": "^3.5.0",
|
||||||
"@solid-primitives/upload": "0.0.110",
|
"@solid-primitives/upload": "0.0.115",
|
||||||
"@solidjs/meta": "0.29.1",
|
|
||||||
"@thisbeyond/solid-select": "0.14.0",
|
"@thisbeyond/solid-select": "0.14.0",
|
||||||
"@tiptap/core": "2.2.3",
|
"@tiptap/core": "2.2.3",
|
||||||
"@tiptap/extension-blockquote": "2.2.3",
|
"@tiptap/extension-blockquote": "2.2.3",
|
||||||
|
@ -95,9 +94,9 @@
|
||||||
"prosemirror-history": "1.3.2",
|
"prosemirror-history": "1.3.2",
|
||||||
"prosemirror-trailing-node": "2.0.7",
|
"prosemirror-trailing-node": "2.0.7",
|
||||||
"prosemirror-view": "1.32.7",
|
"prosemirror-view": "1.32.7",
|
||||||
"rollup": "4.11.0",
|
"rollup": "4.17.2",
|
||||||
"sass": "1.69.5",
|
"sass": "1.69.5",
|
||||||
"solid-js": "1.8.15",
|
"solid-js": "1.8.17",
|
||||||
"solid-popper": "0.3.0",
|
"solid-popper": "0.3.0",
|
||||||
"solid-tiptap": "0.7.0",
|
"solid-tiptap": "0.7.0",
|
||||||
"solid-transition-group": "0.2.3",
|
"solid-transition-group": "0.2.3",
|
||||||
|
@ -111,7 +110,7 @@
|
||||||
"typograf": "7.3.0",
|
"typograf": "7.3.0",
|
||||||
"uniqolor": "1.1.0",
|
"uniqolor": "1.1.0",
|
||||||
"vike": "0.4.148",
|
"vike": "0.4.148",
|
||||||
"vite": "5.2.10",
|
"vite": "5.2.11",
|
||||||
"vite-plugin-mkcert": "^1.17.3",
|
"vite-plugin-mkcert": "^1.17.3",
|
||||||
"vite-plugin-node-polyfills": "0.21.0",
|
"vite-plugin-node-polyfills": "0.21.0",
|
||||||
"vite-plugin-sass-dts": "^1.3.17",
|
"vite-plugin-sass-dts": "^1.3.17",
|
||||||
|
@ -1204,9 +1203,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/biome": {
|
"node_modules/@biomejs/biome": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.7.3.tgz",
|
||||||
"integrity": "sha512-6Skx9N47inLQzYi9RKgJ7PBnUnaHnMe/imqX43cOcJjZtfMnQLxEvfM2Eyo7gChkwrZlwc+VbA4huFRjw2fsYA==",
|
"integrity": "sha512-ogFQI+fpXftr+tiahA6bIXwZ7CSikygASdqMtH07J2cUzrpjyTMVc9Y97v23c7/tL1xCZhM+W9k4hYIBm7Q6cQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -1220,20 +1219,20 @@
|
||||||
"url": "https://opencollective.com/biome"
|
"url": "https://opencollective.com/biome"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@biomejs/cli-darwin-arm64": "1.7.2",
|
"@biomejs/cli-darwin-arm64": "1.7.3",
|
||||||
"@biomejs/cli-darwin-x64": "1.7.2",
|
"@biomejs/cli-darwin-x64": "1.7.3",
|
||||||
"@biomejs/cli-linux-arm64": "1.7.2",
|
"@biomejs/cli-linux-arm64": "1.7.3",
|
||||||
"@biomejs/cli-linux-arm64-musl": "1.7.2",
|
"@biomejs/cli-linux-arm64-musl": "1.7.3",
|
||||||
"@biomejs/cli-linux-x64": "1.7.2",
|
"@biomejs/cli-linux-x64": "1.7.3",
|
||||||
"@biomejs/cli-linux-x64-musl": "1.7.2",
|
"@biomejs/cli-linux-x64-musl": "1.7.3",
|
||||||
"@biomejs/cli-win32-arm64": "1.7.2",
|
"@biomejs/cli-win32-arm64": "1.7.3",
|
||||||
"@biomejs/cli-win32-x64": "1.7.2"
|
"@biomejs/cli-win32-x64": "1.7.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-arm64": {
|
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.7.3.tgz",
|
||||||
"integrity": "sha512-CrldIueHivWEWmeTkK8bTXajeX53F8i2Rrkkt8cPZyMtzkrwxf8Riq4a/jz3SQBHkxHFT4TqGbSTNMXe3X1ogA==",
|
"integrity": "sha512-eDvLQWmGRqrPIRY7AIrkPHkQ3visEItJKkPYSHCscSDdGvKzYjmBJwG1Gu8+QC5ed6R7eiU63LEC0APFBobmfQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -1247,9 +1246,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-x64": {
|
"node_modules/@biomejs/cli-darwin-x64": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.7.3.tgz",
|
||||||
"integrity": "sha512-UELnLJuJOsTL9meArvn8BtiXDURyPil2Ej9me2uVpEvee8UQdqd/bssP5we400OWShlL1AAML4fn6d2WX5332g==",
|
"integrity": "sha512-JXCaIseKRER7dIURsVlAJacnm8SG5I0RpxZ4ya3dudASYUc68WGl4+FEN03ABY3KMIq7hcK1tzsJiWlmXyosZg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -1263,9 +1262,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64": {
|
"node_modules/@biomejs/cli-linux-arm64": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.7.3.tgz",
|
||||||
"integrity": "sha512-Z1CSGQE6fHz55gkiFHv9E8wEAaSUd7dHSRaxSCBa7utonHqpIeMbvj3Evm1w0WfGLFDtRXLV1fTfEdM0FMTOhA==",
|
"integrity": "sha512-phNTBpo7joDFastnmZsFjYcDYobLTx4qR4oPvc9tJ486Bd1SfEVPHEvJdNJrMwUQK56T+TRClOQd/8X1nnjA9w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -1279,9 +1278,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.7.3.tgz",
|
||||||
"integrity": "sha512-kKYZiem7Sj7wI0dpVxJlK7C+TFQwzO/ctufIGXGJAyEmUe9vEKSzV8CXpv+JIRiTWyqaZJ4K+eHz4SPdPCv05w==",
|
"integrity": "sha512-c8AlO45PNFZ1BYcwaKzdt46kYbuP6xPGuGQ6h4j3XiEDpyseRRUy/h+6gxj07XovmyxKnSX9GSZ6nVbZvcVUAw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -1295,9 +1294,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64": {
|
"node_modules/@biomejs/cli-linux-x64": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.7.3.tgz",
|
||||||
"integrity": "sha512-vXXyox8/CQijBxAu0+r8FfSO7JlC4tob3PbaFda8gPJFRz2uFJw39HtxVUwbTV1EcU6wSPh4SiRu5sZfP1VHrQ==",
|
"integrity": "sha512-vnedYcd5p4keT3iD48oSKjOIRPYcjSNNbd8MO1bKo9ajg3GwQXZLAH+0Cvlr+eMsO67/HddWmscSQwTFrC/uPA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -1311,9 +1310,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.7.3.tgz",
|
||||||
"integrity": "sha512-x10LpGMepDrLS+h2TZ6/T7egpHjGKtiI4GuShNylmBQJWfTotbFf9eseHggrqJ4WZf9yrGoVYrtbxXftuB95sQ==",
|
"integrity": "sha512-UdEHKtYGWEX3eDmVWvQeT+z05T9/Sdt2+F/7zmMOFQ7boANeX8pcO6EkJPK3wxMudrApsNEKT26rzqK6sZRTRA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -1327,9 +1326,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-arm64": {
|
"node_modules/@biomejs/cli-win32-arm64": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.7.3.tgz",
|
||||||
"integrity": "sha512-kRXdlKzcU7INf6/ldu0nVmkOgt7bKqmyXRRCUqqaJfA32+9InTbkD8tGrHZEVYIWr+eTuKcg16qZVDsPSDFZ8g==",
|
"integrity": "sha512-unNCDqUKjujYkkSxs7gFIfdasttbDC4+z0kYmcqzRk6yWVoQBL4dNLcCbdnJS+qvVDNdI9rHp2NwpQ0WAdla4Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -1343,9 +1342,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-x64": {
|
"node_modules/@biomejs/cli-win32-x64": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.7.3.tgz",
|
||||||
"integrity": "sha512-qHTtpAs+CNglAAuaTy09htoqUhrQyd3nd0aGTuLNqD10h1llMVi8WFZfoa+e5MuDSfYtMK6nW2Tbf6WgzzR1Qw==",
|
"integrity": "sha512-ZmByhbrnmz/UUFYB622CECwhKIPjJLLPr5zr3edhu04LzbfcOrz16VYeNq5dpO1ADG70FORhAJkaIGdaVBG00w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -1392,9 +1391,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@csstools/css-parser-algorithms": {
|
"node_modules/@csstools/css-parser-algorithms": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.3.tgz",
|
||||||
"integrity": "sha512-ubEkAaTfVZa+WwGhs5jbo5Xfqpeaybr/RvWzvFxRs4jfq16wH8l8Ty/QEEpINxll4xhuGfdMbipRyz5QZh9+FA==",
|
"integrity": "sha512-xI/tL2zxzEbESvnSxwFgwvy5HS00oCXxL4MLs6HUiDcYfwowsoQaABKxUElp1ARITrINzBnsECOc1q0eg2GOrA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -1410,13 +1409,13 @@
|
||||||
"node": "^14 || ^16 || >=18"
|
"node": "^14 || ^16 || >=18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@csstools/css-tokenizer": "^2.2.4"
|
"@csstools/css-tokenizer": "^2.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@csstools/css-tokenizer": {
|
"node_modules/@csstools/css-tokenizer": {
|
||||||
"version": "2.2.4",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.3.1.tgz",
|
||||||
"integrity": "sha512-PuWRAewQLbDhGeTvFuq2oClaSCKPIBmHyIobCV39JHRYN0byDcUWJl5baPeNUcqrjtdMNqFooE0FGl31I3JOqw==",
|
"integrity": "sha512-iMNHTyxLbBlWIfGtabT157LH9DUx9X8+Y3oymFEuMj8HNc+rpE3dPFGFgHjpKfjeFDjLjYIAIhXPGvS2lKxL9g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -1433,9 +1432,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@csstools/media-query-list-parser": {
|
"node_modules/@csstools/media-query-list-parser": {
|
||||||
"version": "2.1.9",
|
"version": "2.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.11.tgz",
|
||||||
"integrity": "sha512-qqGuFfbn4rUmyOB0u8CVISIp5FfJ5GAR3mBrZ9/TKndHakdnm6pY0L/fbLcpPnrzwCyyTEZl1nUcXAYHEWneTA==",
|
"integrity": "sha512-uox5MVhvNHqitPP+SynrB1o8oPxPMt2JLgp5ghJOWf54WGQ5OKu47efne49r1SWqs3wRP8xSWjnO9MBKxhB1dA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -1451,8 +1450,8 @@
|
||||||
"node": "^14 || ^16 || >=18"
|
"node": "^14 || ^16 || >=18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@csstools/css-parser-algorithms": "^2.6.1",
|
"@csstools/css-parser-algorithms": "^2.6.3",
|
||||||
"@csstools/css-tokenizer": "^2.2.4"
|
"@csstools/css-tokenizer": "^2.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@csstools/selector-specificity": {
|
"node_modules/@csstools/selector-specificity": {
|
||||||
|
@ -1487,22 +1486,6 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
|
||||||
"version": "0.20.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
|
||||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"aix"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.17.19",
|
"version": "0.17.19",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
|
||||||
|
@ -3632,9 +3615,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz",
|
||||||
"integrity": "sha512-BV+u2QSfK3i1o6FucqJh5IK9cjAU6icjFFhvknzFgu472jzl0bBojfDAkJLBEsHFMo+YZg6rthBvBBt8z12IBQ==",
|
"integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
@ -3645,9 +3628,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz",
|
||||||
"integrity": "sha512-0ij3iw7sT5jbcdXofWO2NqDNjSVVsf6itcAkV2I6Xsq4+6wjW1A8rViVB67TfBEan7PV2kbLzT8rhOVWLI2YXw==",
|
"integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -3658,9 +3641,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz",
|
||||||
"integrity": "sha512-yPLs6RbbBMupArf6qv1UDk6dzZvlH66z6NLYEwqTU0VHtss1wkI4UYeeMS7TVj5QRVvaNAWYKP0TD/MOeZ76Zg==",
|
"integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -3671,9 +3654,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz",
|
||||||
"integrity": "sha512-OvqIgwaGAwnASzXaZEeoJY3RltOFg+WUbdkdfoluh2iqatd090UeOG3A/h0wNZmE93dDew9tAtXgm3/+U/B6bw==",
|
"integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -3684,9 +3667,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz",
|
||||||
"integrity": "sha512-X17s4hZK3QbRmdAuLd2EE+qwwxL8JxyVupEqAkxKPa/IgX49ZO+vf0ka69gIKsaYeo6c1CuwY3k8trfDtZ9dFg==",
|
"integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
@ -3710,9 +3693,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz",
|
||||||
"integrity": "sha512-673Lu9EJwxVB9NfYeA4AdNu0FOHz7g9t6N1DmT7bZPn1u6bTF+oZjj+fuxUcrfxWXE0r2jxl5QYMa9cUOj9NFg==",
|
"integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -3723,9 +3706,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz",
|
||||||
"integrity": "sha512-yFW2msTAQNpPJaMmh2NpRalr1KXI7ZUjlN6dY/FhWlOclMrZezm5GIhy3cP4Ts2rIAC+IPLAjNibjp1BsxCVGg==",
|
"integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -3749,9 +3732,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz",
|
||||||
"integrity": "sha512-kKT9XIuhbvYgiA3cPAGntvrBgzhWkGpBMzuk1V12Xuoqg7CI41chye4HU0vLJnGf9MiZzfNh4I7StPeOzOWJfA==",
|
"integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
@ -3775,9 +3758,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz",
|
||||||
"integrity": "sha512-6q4ESWlyTO+erp1PSCmASac+ixaDv11dBk1fqyIuvIUc/CmRAX2Zk+2qK1FGo5q7kyDcjHCFVwgGFCGIZGVwCA==",
|
"integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -3788,9 +3771,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz",
|
||||||
"integrity": "sha512-vIAQUmXeMLmaDN78HSE4Kh6xqof2e3TJUKr+LPqXWU4NYNON0MDN9h2+t4KHrPAQNmU3w1GxBQ/n01PaWFwa5w==",
|
"integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -3801,9 +3784,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz",
|
||||||
"integrity": "sha512-LVXo9dDTGPr0nezMdqa1hK4JeoMZ02nstUxGYY/sMIDtTYlli1ZxTXBYAz3vzuuvKO4X6NBETciIh7N9+abT1g==",
|
"integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -3814,9 +3797,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz",
|
||||||
"integrity": "sha512-xZVt6K70Gr3I7nUhug2dN6VRR1ibot3rXqXS3wo+8JP64t7djc3lBFyqO4GiVrhNaAIhUCJtwQ/20dr0h0thmQ==",
|
"integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
@ -3827,9 +3810,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz",
|
||||||
"integrity": "sha512-f3I7h9oTg79UitEco9/2bzwdciYkWr8pITs3meSDSlr1TdvQ7IxkQaaYN2YqZXX5uZhiYL+VuYDmHwNzhx+HOg==",
|
"integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -4072,15 +4055,24 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@solid-primitives/storage": {
|
"node_modules/@solid-primitives/storage": {
|
||||||
"version": "1.3.9",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@solid-primitives/storage/-/storage-1.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/@solid-primitives/storage/-/storage-3.5.0.tgz",
|
||||||
"integrity": "sha512-ysJSIycmToQD8Hpt4jpIlh7U8EuYdpQwkamppng3g93E5f6RZVPCzYmRZ+ckRN2cNLFpAuTEqZx7OBRh3PBWFQ==",
|
"integrity": "sha512-AqU3vrXz8XlxOgJIiP+oQxE/vFchGf4Qe7E5Xfa02DJdF9rD8CtiTmVZDBU08ViS7g0Nwc4IpStHvVO0jBMalQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solid-primitives/utils": "^6.0.0"
|
"@solid-primitives/utils": "^6.2.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@tauri-apps/plugin-store": "*",
|
||||||
"solid-js": "^1.6.12"
|
"solid-js": "^1.6.12"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@tauri-apps/plugin-store": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"solid-start": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@solid-primitives/transition-group": {
|
"node_modules/@solid-primitives/transition-group": {
|
||||||
|
@ -4093,26 +4085,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@solid-primitives/upload": {
|
"node_modules/@solid-primitives/upload": {
|
||||||
"version": "0.0.110",
|
"version": "0.0.115",
|
||||||
"resolved": "https://registry.npmjs.org/@solid-primitives/upload/-/upload-0.0.110.tgz",
|
"resolved": "https://registry.npmjs.org/@solid-primitives/upload/-/upload-0.0.115.tgz",
|
||||||
"integrity": "sha512-YQZGogXzc77c/3hxDoxGi78FkvQQfUbElbdSPn+E0GRl21XMuJbD/QKQKNXm7KyxX+cMTwLnQYCoqfRXcgHMIA==",
|
"integrity": "sha512-CWTXz28mmRGvZV90IzViNtBAKC6cnO2WSNb3UjvbkPRrtxlHrj/ewZwLRhuLSu6JzdY/c+rHi1j24v7H0SrdXg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solid-primitives/utils": "^5.5.1"
|
"@solid-primitives/utils": "^6.2.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"solid-js": "^1.6.12"
|
"solid-js": "^1.6.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@solid-primitives/upload/node_modules/@solid-primitives/utils": {
|
|
||||||
"version": "5.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-5.5.2.tgz",
|
|
||||||
"integrity": "sha512-L52ig3eHKU6CqbPCKJIb4lweBuINHBOERcE1duApyKozEN8+zCqEKwD1Qo9ljKeEzJTBGWClxNpwEiNTUWTGvg==",
|
|
||||||
"dev": true,
|
|
||||||
"peerDependencies": {
|
|
||||||
"solid-js": "^1.6.12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@solid-primitives/utils": {
|
"node_modules/@solid-primitives/utils": {
|
||||||
"version": "6.2.3",
|
"version": "6.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz",
|
||||||
|
@ -4122,15 +4105,6 @@
|
||||||
"solid-js": "^1.6.12"
|
"solid-js": "^1.6.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@solidjs/meta": {
|
|
||||||
"version": "0.29.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@solidjs/meta/-/meta-0.29.1.tgz",
|
|
||||||
"integrity": "sha512-qtrBYCnRRuzyvBg/u/SRO/2fM5r6DT1YKf+2W1RZhveMoeXHbZpWIrXjgpLFRHJLn6cqAGqrIzu42qS2o+1hKQ==",
|
|
||||||
"dev": true,
|
|
||||||
"peerDependencies": {
|
|
||||||
"solid-js": ">=1.8.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@thisbeyond/solid-select": {
|
"node_modules/@thisbeyond/solid-select": {
|
||||||
"version": "0.14.0",
|
"version": "0.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz",
|
||||||
|
@ -4630,9 +4604,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.12.8",
|
"version": "20.12.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz",
|
||||||
"integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==",
|
"integrity": "sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
|
@ -5454,9 +5428,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001615",
|
"version": "1.0.30001616",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz",
|
||||||
"integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==",
|
"integrity": "sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -6290,9 +6264,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.756",
|
"version": "1.4.757",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.756.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.757.tgz",
|
||||||
"integrity": "sha512-RJKZ9+vEBMeiPAvKNWyZjuYyUqMndcP1f335oHqn3BEQbs2NFtVrnK5+6Xg5wSM9TknNNpWghGDUCKGYF+xWXw==",
|
"integrity": "sha512-jftDaCknYSSt/+KKeXzH3LX5E2CvRLm75P3Hj+J/dv3CL0qUYcOt13d5FN1NiL5IJbbhzHrb3BomeG2tkSlZmw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/elliptic": {
|
"node_modules/elliptic": {
|
||||||
|
@ -10796,9 +10770,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-model": {
|
"node_modules/prosemirror-model": {
|
||||||
"version": "1.20.0",
|
"version": "1.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.21.0.tgz",
|
||||||
"integrity": "sha512-q7AY7vMjKYqDCeoedgUiAgrLabliXxndJuuFmcmc2+YU1SblvnOiG2WEACF2lwAZsMlfLpiAilA3L+TWlDqIsQ==",
|
"integrity": "sha512-zLpS1mVCZLA7VTp82P+BfMiYVPcX1/z0Mf3gsjKZtzMWubwn2pN7CceMV0DycjlgE5JeXPR7UF4hJPbBV98oWA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"orderedmap": "^2.0.0"
|
"orderedmap": "^2.0.0"
|
||||||
|
@ -10880,12 +10854,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-transform": {
|
"node_modules/prosemirror-transform": {
|
||||||
"version": "1.8.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.9.0.tgz",
|
||||||
"integrity": "sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==",
|
"integrity": "sha512-5UXkr1LIRx3jmpXXNKDhv8OyAOeLTGuXNwdVfg8x27uASna/wQkr9p6fD3eupGOi4PLJfbezxTyi/7fSJypXHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.0.0"
|
"prosemirror-model": "^1.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-view": {
|
"node_modules/prosemirror-view": {
|
||||||
|
@ -11193,9 +11167,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.11.0",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz",
|
||||||
"integrity": "sha512-2xIbaXDXjf3u2tajvA5xROpib7eegJ9Y/uPlSFhXLNpK9ampCczXAhLEb5yLzJyG3LAdI1NWtNjDXiLyniNdjQ==",
|
"integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.5"
|
"@types/estree": "1.0.5"
|
||||||
|
@ -11208,19 +11182,22 @@
|
||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.11.0",
|
"@rollup/rollup-android-arm-eabi": "4.17.2",
|
||||||
"@rollup/rollup-android-arm64": "4.11.0",
|
"@rollup/rollup-android-arm64": "4.17.2",
|
||||||
"@rollup/rollup-darwin-arm64": "4.11.0",
|
"@rollup/rollup-darwin-arm64": "4.17.2",
|
||||||
"@rollup/rollup-darwin-x64": "4.11.0",
|
"@rollup/rollup-darwin-x64": "4.17.2",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.11.0",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.17.2",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.11.0",
|
"@rollup/rollup-linux-arm-musleabihf": "4.17.2",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.11.0",
|
"@rollup/rollup-linux-arm64-gnu": "4.17.2",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.11.0",
|
"@rollup/rollup-linux-arm64-musl": "4.17.2",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.11.0",
|
"@rollup/rollup-linux-powerpc64le-gnu": "4.17.2",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.11.0",
|
"@rollup/rollup-linux-riscv64-gnu": "4.17.2",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.11.0",
|
"@rollup/rollup-linux-s390x-gnu": "4.17.2",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.11.0",
|
"@rollup/rollup-linux-x64-gnu": "4.17.2",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.11.0",
|
"@rollup/rollup-linux-x64-musl": "4.17.2",
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": "4.17.2",
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": "4.17.2",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.17.2",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -11550,13 +11527,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/solid-js": {
|
"node_modules/solid-js": {
|
||||||
"version": "1.8.15",
|
"version": "1.8.17",
|
||||||
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.15.tgz",
|
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.17.tgz",
|
||||||
"integrity": "sha512-d0QP/efr3UVcwGgWVPveQQ0IHOH6iU7yUhc2piy8arNG8wxKmvUy1kFxyF8owpmfCWGB87usDKMaVnsNYZm+Vw==",
|
"integrity": "sha512-E0FkUgv9sG/gEBWkHr/2XkBluHb1fkrHywUgA6o6XolPDCJ4g1HaLmQufcBBhiF36ee40q+HpG/vCZu7fLpI3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.1.0",
|
"csstype": "^3.1.0",
|
||||||
"seroval": "^1.0.3",
|
"seroval": "^1.0.4",
|
||||||
"seroval-plugins": "^1.0.3"
|
"seroval-plugins": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -12530,9 +12507,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.2.10",
|
"version": "5.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
|
||||||
"integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==",
|
"integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.20.1",
|
"esbuild": "^0.20.1",
|
||||||
|
@ -12660,6 +12637,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite/node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite/node_modules/@esbuild/android-arm": {
|
"node_modules/vite/node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||||
|
@ -13012,175 +13005,6 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-android-arm64": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-darwin-x64": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/esbuild": {
|
"node_modules/vite/node_modules/esbuild": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||||
|
@ -13233,41 +13057,6 @@
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite/node_modules/rollup": {
|
|
||||||
"version": "4.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz",
|
|
||||||
"integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/estree": "1.0.5"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"rollup": "dist/bin/rollup"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0",
|
|
||||||
"npm": ">=8.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@rollup/rollup-android-arm-eabi": "4.17.2",
|
|
||||||
"@rollup/rollup-android-arm64": "4.17.2",
|
|
||||||
"@rollup/rollup-darwin-arm64": "4.17.2",
|
|
||||||
"@rollup/rollup-darwin-x64": "4.17.2",
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.17.2",
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.17.2",
|
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.17.2",
|
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.17.2",
|
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.17.2",
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.17.2",
|
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.17.2",
|
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.17.2",
|
|
||||||
"@rollup/rollup-linux-x64-musl": "4.17.2",
|
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.17.2",
|
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.17.2",
|
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.17.2",
|
|
||||||
"fsevents": "~2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vitefu": {
|
"node_modules/vitefu": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
|
||||||
|
|
19
package.json
19
package.json
|
@ -11,12 +11,12 @@
|
||||||
"deploy": "graphql-codegen && npm run typecheck && vite build && vercel",
|
"deploy": "graphql-codegen && npm run typecheck && vite build && vercel",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"e2e": "npx playwright test --project=chromium",
|
"e2e": "npx playwright test --project=chromium",
|
||||||
"fix": "npm run lint:code:fix && stylelint **/*.{scss,css} --fix",
|
"fix": "npm run check:code:fix && stylelint **/*.{scss,css} --fix",
|
||||||
"format": "npx @biomejs/biome format src/. --write",
|
"format": "npx @biomejs/biome format src/. --write",
|
||||||
"hygen": "HYGEN_TMPLS=gen hygen",
|
"hygen": "HYGEN_TMPLS=gen hygen",
|
||||||
"postinstall": "npm run codegen && npx patch-package",
|
"postinstall": "npm run codegen && npx patch-package",
|
||||||
"check:code": "npx @biomejs/biome check src --log-kind=compact --verbose",
|
"check:code": "npx @biomejs/biome check src --log-kind=compact --verbose",
|
||||||
"check:code:fix": "npx @biomejs/biome lint src --log-kind=compact",
|
"check:code:fix": "npx @biomejs/biome check . --apply",
|
||||||
"lint": "npm run lint:code && stylelint **/*.{scss,css}",
|
"lint": "npm run lint:code && stylelint **/*.{scss,css}",
|
||||||
"lint:code": "npx @biomejs/biome lint src --log-kind=compact --verbose",
|
"lint:code": "npx @biomejs/biome lint src --log-kind=compact --verbose",
|
||||||
"lint:code:fix": "npx @biomejs/biome lint src --apply-unsafe --log-kind=compact --verbose",
|
"lint:code:fix": "npx @biomejs/biome lint src --apply-unsafe --log-kind=compact --verbose",
|
||||||
|
@ -52,9 +52,8 @@
|
||||||
"@solid-primitives/memo": "1.2.4",
|
"@solid-primitives/memo": "1.2.4",
|
||||||
"@solid-primitives/pagination": "0.2.10",
|
"@solid-primitives/pagination": "0.2.10",
|
||||||
"@solid-primitives/share": "2.0.4",
|
"@solid-primitives/share": "2.0.4",
|
||||||
"@solid-primitives/storage": "1.3.9",
|
"@solid-primitives/storage": "^3.5.0",
|
||||||
"@solid-primitives/upload": "0.0.110",
|
"@solid-primitives/upload": "0.0.115",
|
||||||
"@solidjs/meta": "0.29.1",
|
|
||||||
"@thisbeyond/solid-select": "0.14.0",
|
"@thisbeyond/solid-select": "0.14.0",
|
||||||
"@tiptap/core": "2.2.3",
|
"@tiptap/core": "2.2.3",
|
||||||
"@tiptap/extension-blockquote": "2.2.3",
|
"@tiptap/extension-blockquote": "2.2.3",
|
||||||
|
@ -113,9 +112,9 @@
|
||||||
"prosemirror-history": "1.3.2",
|
"prosemirror-history": "1.3.2",
|
||||||
"prosemirror-trailing-node": "2.0.7",
|
"prosemirror-trailing-node": "2.0.7",
|
||||||
"prosemirror-view": "1.32.7",
|
"prosemirror-view": "1.32.7",
|
||||||
"rollup": "4.11.0",
|
"rollup": "4.17.2",
|
||||||
"sass": "1.69.5",
|
"sass": "1.69.5",
|
||||||
"solid-js": "1.8.15",
|
"solid-js": "1.8.17",
|
||||||
"solid-popper": "0.3.0",
|
"solid-popper": "0.3.0",
|
||||||
"solid-tiptap": "0.7.0",
|
"solid-tiptap": "0.7.0",
|
||||||
"solid-transition-group": "0.2.3",
|
"solid-transition-group": "0.2.3",
|
||||||
|
@ -129,7 +128,7 @@
|
||||||
"typograf": "7.3.0",
|
"typograf": "7.3.0",
|
||||||
"uniqolor": "1.1.0",
|
"uniqolor": "1.1.0",
|
||||||
"vike": "0.4.148",
|
"vike": "0.4.148",
|
||||||
"vite": "5.2.10",
|
"vite": "5.2.11",
|
||||||
"vite-plugin-mkcert": "^1.17.3",
|
"vite-plugin-mkcert": "^1.17.3",
|
||||||
"vite-plugin-node-polyfills": "0.21.0",
|
"vite-plugin-node-polyfills": "0.21.0",
|
||||||
"vite-plugin-sass-dts": "^1.3.17",
|
"vite-plugin-sass-dts": "^1.3.17",
|
||||||
|
@ -141,7 +140,5 @@
|
||||||
"y-prosemirror": "1.2.2",
|
"y-prosemirror": "1.2.2",
|
||||||
"yjs": "13.6.12"
|
"yjs": "13.6.12"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": ["@biomejs/biome"]
|
||||||
"@biomejs/biome"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
4
public/icons/logout.svg
Normal file
4
public/icons/logout.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M16.1785 3.05371C15.1421 3.05371 14.3035 3.89486 14.3035 4.92871C14.3035 5.96256 15.1421 6.80371 16.1785 6.80371C17.215 6.80371 18.0535 5.96256 18.0535 4.92871C18.0535 3.89486 17.215 3.05371 16.1785 3.05371ZM14.6785 7.55371C14.4051 7.55371 14.1473 7.61621 13.9129 7.72038L10.9181 9.12923C10.7723 9.19694 10.6577 9.31413 10.5926 9.45736L9.12124 12.7308C9.07957 12.8089 9.05353 12.8975 9.05353 12.9912C9.05353 13.3011 9.30613 13.5537 9.61603 13.5537C9.8478 13.5537 10.0483 13.4131 10.1343 13.2126V13.21L11.702 10.5303L12.4858 10.249L11.7462 12.6761L11.7541 12.6787C11.7098 12.8376 11.6785 13.0042 11.6785 13.1787C11.6785 13.8923 12.0822 14.5068 12.6707 14.8245L12.6655 14.8298L15.5848 16.9626L16.5874 20.5225H16.59C16.6837 20.8298 16.965 21.0537 17.3035 21.0537C17.7176 21.0537 18.0535 20.7178 18.0535 20.3037C18.0535 20.2282 18.0379 20.1553 18.0171 20.085H18.0197L17.2671 16.3454C17.2436 16.223 17.1968 16.1058 17.1317 15.999L15.4806 13.2881L16.4806 9.99902L16.4572 9.99121C16.5145 9.81152 16.5535 9.62663 16.5535 9.42871C16.5535 8.39486 15.715 7.55371 14.6785 7.55371ZM17.1681 10.5355L16.603 12.0771L17.0353 12.4001C17.0718 12.4261 17.1108 12.4469 17.1525 12.4626L19.8869 13.5042C19.8973 13.5094 19.9103 13.512 19.9207 13.5173L19.9363 13.5225C19.9936 13.5407 20.0535 13.5537 20.116 13.5537C20.4259 13.5537 20.6785 13.3011 20.6785 12.9912C20.6785 12.7699 20.5483 12.5771 20.3608 12.486L17.9233 11.21L17.1681 10.5355ZM8.91551 13.9313C8.69676 13.9105 8.47801 14.0225 8.36863 14.2282L7.43895 15.9886L6.11343 15.2829C5.74884 15.0876 5.29572 15.2256 5.1004 15.5928L3.33738 18.9053C3.14468 19.2673 3.2853 19.723 3.64988 19.9183L4.48582 20.3636C4.47801 20.1058 4.5379 19.8454 4.66551 19.611C4.92593 19.1188 5.43374 18.8115 5.99103 18.8115C6.23322 18.8115 6.47801 18.874 6.69155 18.9886C6.80353 19.0485 6.9077 19.1188 6.99884 19.21L7.98843 17.348C7.99103 17.3454 7.99103 17.3454 7.99363 17.3428L8.2254 16.9027L8.43113 16.5173V16.5146L9.36343 14.7542C9.50926 14.4782 9.40249 14.1396 9.12905 13.9938C9.06134 13.9574 8.98843 13.9365 8.91551 13.9313ZM11.8608 15.3532L11.4988 17.0225L9.93113 19.8844L9.89988 19.9417C9.83999 20.0485 9.80353 20.1709 9.80353 20.3037C9.80353 20.7178 10.1395 21.0537 10.5535 21.0537C10.8244 21.0537 11.0613 20.9079 11.1916 20.6917L13.7332 16.7334L11.8608 15.3532ZM6.05613 19.5641C5.76447 19.5407 5.4728 19.6865 5.32697 19.96C5.13165 20.3271 5.27228 20.7803 5.63686 20.9756C6.00145 21.1683 6.45718 21.0303 6.65249 20.6657C6.8452 20.2985 6.70718 19.8454 6.33999 19.6501C6.24884 19.6006 6.15249 19.5745 6.05613 19.5641Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
4
public/icons/profile.svg
Normal file
4
public/icons/profile.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M12.4285 3.05347C11.392 3.05347 10.5535 3.89461 10.5535 4.92847C10.5535 5.96232 11.392 6.80347 12.4285 6.80347C13.4649 6.80347 14.3035 5.96232 14.3035 4.92847C14.3035 3.89461 13.4649 3.05347 12.4285 3.05347ZM12.4285 7.55347C10.3113 7.55347 9.05347 9.05347 9.05347 10.1785V14.6785C9.05347 15.0925 9.3894 15.4285 9.80347 15.4285H10.1785V21.7852C10.1785 22.2097 10.5222 22.5535 10.9467 22.5535C11.3582 22.5535 11.6941 22.2332 11.7149 21.8243L12.017 15.4285H12.8399L13.142 21.8243C13.1628 22.2332 13.4988 22.5535 13.9102 22.5535C14.3347 22.5535 14.6785 22.2097 14.6785 21.7852V15.4285H15.0535C15.4675 15.4285 15.8035 15.0925 15.8035 14.6785V10.1785C15.8035 9.05347 14.5457 7.55347 12.4285 7.55347Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 831 B |
|
@ -397,7 +397,7 @@
|
||||||
"Top authors": "Authors rating",
|
"Top authors": "Authors rating",
|
||||||
"Top commented": "Most commented",
|
"Top commented": "Most commented",
|
||||||
"Top discussed": "Top discussed",
|
"Top discussed": "Top discussed",
|
||||||
"Top month articles": "Top of the month",
|
"Top month": "Top of the month",
|
||||||
"Top rated": "Popular",
|
"Top rated": "Popular",
|
||||||
"Top recent": "Most recent",
|
"Top recent": "Most recent",
|
||||||
"Top topics": "Interesting topics",
|
"Top topics": "Interesting topics",
|
||||||
|
@ -448,6 +448,7 @@
|
||||||
"Write your colleagues name or email": "Write your colleague's name or email",
|
"Write your colleagues name or email": "Write your colleague's name or email",
|
||||||
"You can download multiple tracks at once in .mp3, .wav or .flac formats": "You can download multiple tracks at once in .mp3, .wav or .flac formats",
|
"You can download multiple tracks at once in .mp3, .wav or .flac formats": "You can download multiple tracks at once in .mp3, .wav or .flac formats",
|
||||||
"You can now login using your new password": "Теперь вы можете входить с помощью нового пароля",
|
"You can now login using your new password": "Теперь вы можете входить с помощью нового пароля",
|
||||||
|
"You can't edit this post": "You can't edit this post",
|
||||||
"You were successfully authorized": "You were successfully authorized",
|
"You were successfully authorized": "You were successfully authorized",
|
||||||
"You ll be able to participate in discussions, rate others' comments and learn about new responses": "You ll be able to participate in discussions, rate others' comments and learn about new responses",
|
"You ll be able to participate in discussions, rate others' comments and learn about new responses": "You ll be able to participate in discussions, rate others' comments and learn about new responses",
|
||||||
"You've confirmed email": "You've confirmed email",
|
"You've confirmed email": "You've confirmed email",
|
||||||
|
@ -530,5 +531,13 @@
|
||||||
"It's OK. Just enter your email to receive a link to change your password": "It's OK. Just enter your email to receive a link to change your password",
|
"It's OK. Just enter your email to receive a link to change your password": "It's OK. Just enter your email to receive a link to change your password",
|
||||||
"Restore password": "Restore password",
|
"Restore password": "Restore password",
|
||||||
"Subscribing...": "Subscribing...",
|
"Subscribing...": "Subscribing...",
|
||||||
"Unsubscribing...": "Unsubscribing..."
|
"Unsubscribing...": "Unsubscribing...",
|
||||||
|
"Login and security": "Login and security",
|
||||||
|
"Settings for account, email, password and login methods.": "Settings for account, email, password and login methods.",
|
||||||
|
"Current password": "Current password",
|
||||||
|
"Confirm your new password": "Confirm your new password",
|
||||||
|
"Connect": "Connect",
|
||||||
|
"Incorrect old password": "Incorrect old password",
|
||||||
|
"Repeat new password": "Repeat new password",
|
||||||
|
"Incorrect new password confirm": "Incorrect new password confirm"
|
||||||
}
|
}
|
||||||
|
|
|
@ -418,7 +418,7 @@
|
||||||
"Top authors": "Рейтинг авторов",
|
"Top authors": "Рейтинг авторов",
|
||||||
"Top commented": "Самое комментируемое",
|
"Top commented": "Самое комментируемое",
|
||||||
"Top discussed": "Обсуждаемое",
|
"Top discussed": "Обсуждаемое",
|
||||||
"Top month articles": "Лучшие материалы месяца",
|
"Top month": "Лучшее за месяц",
|
||||||
"Top rated": "Популярное",
|
"Top rated": "Популярное",
|
||||||
"Top recent": "Самое новое",
|
"Top recent": "Самое новое",
|
||||||
"Top topics": "Интересные темы",
|
"Top topics": "Интересные темы",
|
||||||
|
@ -471,6 +471,7 @@
|
||||||
"You can download multiple tracks at once in .mp3, .wav or .flac formats": "Можно загрузить сразу несколько треков в форматах .mp3, .wav или .flac",
|
"You can download multiple tracks at once in .mp3, .wav or .flac formats": "Можно загрузить сразу несколько треков в форматах .mp3, .wav или .flac",
|
||||||
"You can now login using your new password": "Теперь вы можете входить с помощью нового пароля",
|
"You can now login using your new password": "Теперь вы можете входить с помощью нового пароля",
|
||||||
"You was successfully authorized": "Вы были успешно авторизованы",
|
"You was successfully authorized": "Вы были успешно авторизованы",
|
||||||
|
"You can't edit this post": "Вы не можете редактировать этот материал",
|
||||||
"You ll be able to participate in discussions, rate others' comments and learn about new responses": "Вы сможете участвовать в обсуждениях, оценивать комментарии других и узнавать о новых ответах",
|
"You ll be able to participate in discussions, rate others' comments and learn about new responses": "Вы сможете участвовать в обсуждениях, оценивать комментарии других и узнавать о новых ответах",
|
||||||
"You've confirmed email": "Вы подтвердили почту",
|
"You've confirmed email": "Вы подтвердили почту",
|
||||||
"You've reached a non-existed page": "Вы попали на несуществующую страницу",
|
"You've reached a non-existed page": "Вы попали на несуществующую страницу",
|
||||||
|
@ -557,5 +558,13 @@
|
||||||
"It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля",
|
"It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля",
|
||||||
"Restore password": "Восстановить пароль",
|
"Restore password": "Восстановить пароль",
|
||||||
"Subscribing...": "Подписываем...",
|
"Subscribing...": "Подписываем...",
|
||||||
"Unsubscribing...": "Отписываем..."
|
"Unsubscribing...": "Отписываем...",
|
||||||
|
"Login and security": "Вход и безопасность",
|
||||||
|
"Settings for account, email, password and login methods.": "Настройки аккаунта, почты, пароля и способов входа.",
|
||||||
|
"Current password": "Текущий пароль",
|
||||||
|
"Confirm your new password": "Подтвердите новый пароль",
|
||||||
|
"Connect": "Привязать",
|
||||||
|
"Incorrect old password": "Старый пароль не верен",
|
||||||
|
"Repeat new password": "Повторите новый пароль",
|
||||||
|
"Incorrect new password confirm": "Неверное подтверждение нового пароля"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { PageProps, RootSearchParams } from '../pages/types'
|
import type { PageProps, RootSearchParams } from '../pages/types'
|
||||||
|
|
||||||
import { Meta, MetaProvider } from '@solidjs/meta'
|
|
||||||
import { Component, createEffect, createMemo } from 'solid-js'
|
import { Component, createEffect, createMemo } from 'solid-js'
|
||||||
import { Dynamic } from 'solid-js/web'
|
import { Dynamic } from 'solid-js/web'
|
||||||
|
import { Meta, MetaProvider } from '../context/meta'
|
||||||
|
|
||||||
import { ConfirmProvider } from '../context/confirm'
|
import { ConfirmProvider } from '../context/confirm'
|
||||||
import { ConnectProvider } from '../context/connect'
|
import { ConnectProvider } from '../context/connect'
|
||||||
|
@ -12,6 +12,7 @@ import { InboxProvider } from '../context/inbox'
|
||||||
import { LocalizeProvider } from '../context/localize'
|
import { LocalizeProvider } from '../context/localize'
|
||||||
import { MediaQueryProvider } from '../context/mediaQuery'
|
import { MediaQueryProvider } from '../context/mediaQuery'
|
||||||
import { NotificationsProvider } from '../context/notifications'
|
import { NotificationsProvider } from '../context/notifications'
|
||||||
|
import { SeenProvider } from '../context/seen'
|
||||||
import { SessionProvider } from '../context/session'
|
import { SessionProvider } from '../context/session'
|
||||||
import { SnackbarProvider } from '../context/snackbar'
|
import { SnackbarProvider } from '../context/snackbar'
|
||||||
import { TopicsProvider } from '../context/topics'
|
import { TopicsProvider } from '../context/topics'
|
||||||
|
@ -40,16 +41,12 @@ import { InboxPage } from '../pages/inbox.page'
|
||||||
import { HomePage } from '../pages/index.page'
|
import { HomePage } from '../pages/index.page'
|
||||||
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
|
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
|
||||||
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
|
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
|
||||||
//TODO: ProfileSubscriptionsPage - garbage code?
|
|
||||||
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
|
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
|
||||||
import { SearchPage } from '../pages/search.page'
|
import { SearchPage } from '../pages/search.page'
|
||||||
import { TopicPage } from '../pages/topic.page'
|
import { TopicPage } from '../pages/topic.page'
|
||||||
import { ROUTES, useRouter } from '../stores/router'
|
import { ROUTES, useRouter } from '../stores/router'
|
||||||
import { MODALS, showModal } from '../stores/ui'
|
import { MODALS, showModal } from '../stores/ui'
|
||||||
|
|
||||||
// TODO: lazy load
|
|
||||||
// const SomePage = lazy(() => import('./Pages/SomePage'))
|
|
||||||
|
|
||||||
const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
|
const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
|
||||||
author: AuthorPage,
|
author: AuthorPage,
|
||||||
authorComments: AuthorPage,
|
authorComments: AuthorPage,
|
||||||
|
@ -119,6 +116,7 @@ export const App = (props: Props) => {
|
||||||
<MediaQueryProvider>
|
<MediaQueryProvider>
|
||||||
<SnackbarProvider>
|
<SnackbarProvider>
|
||||||
<TopicsProvider>
|
<TopicsProvider>
|
||||||
|
<SeenProvider>
|
||||||
<ConfirmProvider>
|
<ConfirmProvider>
|
||||||
<SessionProvider onStateChangeCallback={console.log}>
|
<SessionProvider onStateChangeCallback={console.log}>
|
||||||
<FollowingProvider>
|
<FollowingProvider>
|
||||||
|
@ -134,6 +132,7 @@ export const App = (props: Props) => {
|
||||||
</FollowingProvider>
|
</FollowingProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</ConfirmProvider>
|
</ConfirmProvider>
|
||||||
|
</SeenProvider>
|
||||||
</TopicsProvider>
|
</TopicsProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
</MediaQueryProvider>
|
</MediaQueryProvider>
|
||||||
|
|
|
@ -127,7 +127,7 @@ export const Comment = (props: Props) => {
|
||||||
<li
|
<li
|
||||||
id={`comment_${props.comment.id}`}
|
id={`comment_${props.comment.id}`}
|
||||||
class={clsx(styles.comment, props.class, {
|
class={clsx(styles.comment, props.class, {
|
||||||
[styles.isNew]: props.comment?.created_at > props.lastSeen,
|
[styles.isNew]: props.lastSeen > (props.comment.updated_at || props.comment.created_at),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Show when={!!body()}>
|
<Show when={!!body()}>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
||||||
|
|
||||||
import { Comment } from './Comment'
|
import { Comment } from './Comment'
|
||||||
|
|
||||||
|
import { useSeen } from '../../context/seen'
|
||||||
import styles from './Article.module.scss'
|
import styles from './Article.module.scss'
|
||||||
|
|
||||||
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
|
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
|
||||||
|
@ -29,7 +30,7 @@ export const CommentsTree = (props: Props) => {
|
||||||
const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
|
const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
|
||||||
const [clearEditor, setClearEditor] = createSignal(false)
|
const [clearEditor, setClearEditor] = createSignal(false)
|
||||||
const [clickedReplyId, setClickedReplyId] = createSignal<number>()
|
const [clickedReplyId, setClickedReplyId] = createSignal<number>()
|
||||||
const { reactionEntities, createReaction } = useReactions()
|
const { reactionEntities, createReaction, loadReactionsBy } = useReactions()
|
||||||
|
|
||||||
const comments = createMemo(() =>
|
const comments = createMemo(() =>
|
||||||
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT'),
|
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT'),
|
||||||
|
@ -48,27 +49,28 @@ export const CommentsTree = (props: Props) => {
|
||||||
}
|
}
|
||||||
return newSortedComments
|
return newSortedComments
|
||||||
})
|
})
|
||||||
|
const { seen } = useSeen()
|
||||||
const dateFromLocalStorage = Number.parseInt(localStorage.getItem(`${props.shoutSlug}`))
|
const shoutLastSeen = createMemo(() => seen()[props.shoutSlug] ?? 0)
|
||||||
const currentDate = new Date()
|
const currentDate = new Date()
|
||||||
const setCookie = () => localStorage.setItem(`${props.shoutSlug}`, `${currentDate}`)
|
const setCookie = () => localStorage.setItem(`${props.shoutSlug}`, `${currentDate}`)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!dateFromLocalStorage) {
|
if (!shoutLastSeen()) {
|
||||||
setCookie()
|
setCookie()
|
||||||
} else if (currentDate.getTime() > dateFromLocalStorage) {
|
} else if (currentDate.getTime() > shoutLastSeen()) {
|
||||||
const newComments = comments().filter((c) => {
|
const newComments = comments().filter((c) => {
|
||||||
if (c.reply_to || c.created_by.slug === author()?.slug) {
|
if (c.reply_to || c.created_by.slug === author()?.slug) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const created = c.created_at
|
return (c.updated_at || c.created_at) > shoutLastSeen()
|
||||||
return created > dateFromLocalStorage
|
|
||||||
})
|
})
|
||||||
setNewReactions(newComments)
|
setNewReactions(newComments)
|
||||||
setCookie()
|
setCookie()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const [posting, setPosting] = createSignal(false)
|
||||||
const handleSubmitComment = async (value: string) => {
|
const handleSubmitComment = async (value: string) => {
|
||||||
|
setPosting(true)
|
||||||
try {
|
try {
|
||||||
await createReaction({
|
await createReaction({
|
||||||
kind: ReactionKind.Comment,
|
kind: ReactionKind.Comment,
|
||||||
|
@ -76,10 +78,12 @@ export const CommentsTree = (props: Props) => {
|
||||||
shout: props.shoutId,
|
shout: props.shoutId,
|
||||||
})
|
})
|
||||||
setClearEditor(true)
|
setClearEditor(true)
|
||||||
|
await loadReactionsBy({ by: { shout: props.shoutSlug } })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[handleCreate reaction]:', error)
|
console.error('[handleCreate reaction]:', error)
|
||||||
}
|
}
|
||||||
setClearEditor(false)
|
setClearEditor(false)
|
||||||
|
setPosting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -130,7 +134,7 @@ export const CommentsTree = (props: Props) => {
|
||||||
comment={reaction}
|
comment={reaction}
|
||||||
clickedReply={(id) => setClickedReplyId(id)}
|
clickedReply={(id) => setClickedReplyId(id)}
|
||||||
clickedReplyId={clickedReplyId()}
|
clickedReplyId={clickedReplyId()}
|
||||||
lastSeen={dateFromLocalStorage}
|
lastSeen={shoutLastSeen()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
@ -157,6 +161,7 @@ export const CommentsTree = (props: Props) => {
|
||||||
placeholder={t('Write a comment...')}
|
placeholder={t('Write a comment...')}
|
||||||
onSubmit={(value) => handleSubmitComment(value)}
|
onSubmit={(value) => handleSubmitComment(value)}
|
||||||
setClear={clearEditor()}
|
setClear={clearEditor()}
|
||||||
|
isPosting={posting()}
|
||||||
/>
|
/>
|
||||||
</ShowIfAuthenticated>
|
</ShowIfAuthenticated>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -2,11 +2,11 @@ import type { Author, Shout, Topic } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { createPopper } from '@popperjs/core'
|
import { createPopper } from '@popperjs/core'
|
||||||
import { Link, Meta } from '@solidjs/meta'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { install } from 'ga-gtag'
|
import { install } from 'ga-gtag'
|
||||||
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||||
import { isServer } from 'solid-js/web'
|
import { isServer } from 'solid-js/web'
|
||||||
|
import { Link, Meta } from '../../context/meta'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useReactions } from '../../context/reactions'
|
import { useReactions } from '../../context/reactions'
|
||||||
|
@ -38,6 +38,7 @@ import { CommentsTree } from './CommentsTree'
|
||||||
import { SharePopup, getShareUrl } from './SharePopup'
|
import { SharePopup, getShareUrl } from './SharePopup'
|
||||||
import { ShoutRatingControl } from './ShoutRatingControl'
|
import { ShoutRatingControl } from './ShoutRatingControl'
|
||||||
|
|
||||||
|
import { useSeen } from '../../context/seen'
|
||||||
import stylesHeader from '../Nav/Header/Header.module.scss'
|
import stylesHeader from '../Nav/Header/Header.module.scss'
|
||||||
import styles from './Article.module.scss'
|
import styles from './Article.module.scss'
|
||||||
|
|
||||||
|
@ -75,7 +76,8 @@ export const FullArticle = (props: Props) => {
|
||||||
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
||||||
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
||||||
const { t, formatDate, lang } = useLocalize()
|
const { t, formatDate, lang } = useLocalize()
|
||||||
const { author, session, isAuthenticated, requireAuthentication } = useSession()
|
const { author, session, requireAuthentication } = useSession()
|
||||||
|
const { addSeen } = useSeen()
|
||||||
|
|
||||||
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000)))
|
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000)))
|
||||||
|
|
||||||
|
@ -302,6 +304,7 @@ export const FullArticle = (props: Props) => {
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
install('G-LQ4B87H8C2')
|
install('G-LQ4B87H8C2')
|
||||||
await loadReactionsBy({ by: { shout: props.article.slug } })
|
await loadReactionsBy({ by: { shout: props.article.slug } })
|
||||||
|
addSeen(props.article.slug)
|
||||||
setIsReactionsLoaded(true)
|
setIsReactionsLoaded(true)
|
||||||
document.title = props.article.title
|
document.title = props.article.title
|
||||||
window?.addEventListener('resize', updateIframeSizes)
|
window?.addEventListener('resize', updateIframeSizes)
|
||||||
|
@ -535,7 +538,7 @@ export const FullArticle = (props: Props) => {
|
||||||
{(triggerRef: (el) => void) => (
|
{(triggerRef: (el) => void) => (
|
||||||
<div class={styles.shoutStatsItem} ref={triggerRef}>
|
<div class={styles.shoutStatsItem} ref={triggerRef}>
|
||||||
<a
|
<a
|
||||||
href={getPagePath(router, 'edit', { shoutId: props.article.id.toString() })}
|
href={getPagePath(router, 'edit', { shoutId: props.article?.id.toString() })}
|
||||||
class={styles.shoutStatsItemInner}
|
class={styles.shoutStatsItemInner}
|
||||||
>
|
>
|
||||||
<Icon name="pencil-outline" class={styles.icon} />
|
<Icon name="pencil-outline" class={styles.icon} />
|
||||||
|
@ -561,7 +564,7 @@ export const FullArticle = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={isAuthenticated() && !canEdit()}>
|
<Show when={author()?.id && !canEdit()}>
|
||||||
<div class={styles.help}>
|
<div class={styles.help}>
|
||||||
<button class="button">{t('Cooperate')}</button>
|
<button class="button">{t('Cooperate')}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const SharePopup = (props: SharePopupProps) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup {...props} variant="bordered" onVisibilityChange={(value) => setIsVisible(value)}>
|
<Popup {...props} onVisibilityChange={(value) => setIsVisible(value)}>
|
||||||
<ShareLinks
|
<ShareLinks
|
||||||
variant="inPopup"
|
variant="inPopup"
|
||||||
title={props.title}
|
title={props.title}
|
||||||
|
|
|
@ -12,7 +12,7 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthGuard = (props: Props) => {
|
export const AuthGuard = (props: Props) => {
|
||||||
const { isAuthenticated, isSessionLoaded } = useSession()
|
const { author, isSessionLoaded } = useSession()
|
||||||
const { changeSearchParams } = useRouter<RootSearchParams & AuthModalSearchParams>()
|
const { changeSearchParams } = useRouter<RootSearchParams & AuthModalSearchParams>()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
@ -20,7 +20,7 @@ export const AuthGuard = (props: Props) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isSessionLoaded()) {
|
if (isSessionLoaded()) {
|
||||||
if (isAuthenticated()) {
|
if (author()?.id) {
|
||||||
hideModal()
|
hideModal()
|
||||||
} else {
|
} else {
|
||||||
changeSearchParams(
|
changeSearchParams(
|
||||||
|
@ -37,5 +37,5 @@ export const AuthGuard = (props: Props) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return <Show when={(isSessionLoaded() && isAuthenticated()) || props.disabled}>{props.children}</Show>
|
return <Show when={(isSessionLoaded() && author()?.id) || props.disabled}>{props.children}</Show>
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ export const AuthorBadge = (props: Props) => {
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
openPage(router, 'inbox')
|
openPage(router, 'inbox')
|
||||||
changeSearchParams({
|
changeSearchParams({
|
||||||
initChat: props.author.id.toString(),
|
initChat: props.author?.id.toString(),
|
||||||
})
|
})
|
||||||
}, 'discussions')
|
}, 'discussions')
|
||||||
}
|
}
|
||||||
|
@ -118,6 +118,9 @@ export const AuthorBadge = (props: Props) => {
|
||||||
<Show when={props.author?.stat.shouts > 0}>
|
<Show when={props.author?.stat.shouts > 0}>
|
||||||
<div>{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}</div>
|
<div>{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={props.author?.stat.comments > 0}>
|
||||||
|
<div>{t('CommentsWithCount', { count: props.author.stat?.comments ?? 0 })}</div>
|
||||||
|
</Show>
|
||||||
<Show when={props.author?.stat.followers > 0}>
|
<Show when={props.author?.stat.followers > 0}>
|
||||||
<div>{t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}</div>
|
<div>{t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -65,7 +65,7 @@ export const AuthorCard = (props: Props) => {
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
openPage(router, 'inbox')
|
openPage(router, 'inbox')
|
||||||
changeSearchParams({
|
changeSearchParams({
|
||||||
initChat: props.author.id.toString(),
|
initChat: props.author?.id.toString(),
|
||||||
})
|
})
|
||||||
}, 'discussions')
|
}, 'discussions')
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { showModal } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
import { AuthModalSearchParams } from '../Nav/AuthModal/types'
|
import type { AuthModalSearchParams } from '../Nav/AuthModal/types'
|
||||||
|
|
||||||
import styles from './Hero.module.scss'
|
import styles from './Hero.module.scss'
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ export const Draft = (props: Props) => {
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
<a
|
<a
|
||||||
class={styles.actionItem}
|
class={styles.actionItem}
|
||||||
href={getPagePath(router, 'edit', { shoutId: props.shout.id.toString() })}
|
href={getPagePath(router, 'edit', { shoutId: props.shout?.id.toString() })}
|
||||||
>
|
>
|
||||||
{t('Edit')}
|
{t('Edit')}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { composeMediaItems } from '../../../utils/composeMediaItems'
|
||||||
import { AudioPlayer } from '../../Article/AudioPlayer'
|
import { AudioPlayer } from '../../Article/AudioPlayer'
|
||||||
import { DropArea } from '../../_shared/DropArea'
|
import { DropArea } from '../../_shared/DropArea'
|
||||||
|
|
||||||
|
// import { Buffer } from 'node:buffer'
|
||||||
import styles from './AudioUploader.module.scss'
|
import styles from './AudioUploader.module.scss'
|
||||||
|
|
||||||
window.Buffer = Buffer
|
window.Buffer = Buffer
|
||||||
|
|
|
@ -151,7 +151,7 @@ export const Editor = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
showSnackbar({ body: t('Uploading image') })
|
showSnackbar({ body: t('Uploading image') })
|
||||||
const result = await handleImageUpload(uplFile)
|
const result = await handleImageUpload(uplFile, session()?.access_token)
|
||||||
|
|
||||||
editor()
|
editor()
|
||||||
.chain()
|
.chain()
|
||||||
|
|
|
@ -28,11 +28,12 @@ const embedData = (data) => {
|
||||||
|
|
||||||
const result: { src: string; width?: string; height?: string } = { src: '' }
|
const result: { src: string; width?: string; height?: string } = { src: '' }
|
||||||
|
|
||||||
// biome-ignore lint/style/useForOf: <explanation>
|
|
||||||
for (let i = 0; i < attributes.length; i++) {
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
const attribute = attributes[i]
|
const attribute = attributes.item(i)
|
||||||
|
if (attribute) {
|
||||||
result[attribute.name] = attribute.value
|
result[attribute.name] = attribute.value
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,16 @@ type Props = {
|
||||||
|
|
||||||
export const Panel = (props: Props) => {
|
export const Panel = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { isEditorPanelVisible, wordCounter, editorRef, form, toggleEditorPanel, saveShout, publishShout } =
|
const {
|
||||||
useEditorContext()
|
isEditorPanelVisible,
|
||||||
|
wordCounter,
|
||||||
|
editorRef,
|
||||||
|
form,
|
||||||
|
toggleEditorPanel,
|
||||||
|
saveShout,
|
||||||
|
saveDraft,
|
||||||
|
publishShout,
|
||||||
|
} = useEditorContext()
|
||||||
|
|
||||||
const containerRef: { current: HTMLElement } = { current: null }
|
const containerRef: { current: HTMLElement } = { current: null }
|
||||||
const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false)
|
const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false)
|
||||||
|
@ -43,7 +51,12 @@ export const Panel = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSaveClick = () => {
|
const handleSaveClick = () => {
|
||||||
|
const hasTopics = form.selectedTopics?.length > 0
|
||||||
|
if (hasTopics) {
|
||||||
saveShout(form)
|
saveShout(form)
|
||||||
|
} else {
|
||||||
|
saveDraft(form)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = useEditorHTML(() => editorRef.current())
|
const html = useEditorHTML(() => editorRef.current())
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { UploadModalContent } from './UploadModalContent'
|
||||||
import { Figcaption } from './extensions/Figcaption'
|
import { Figcaption } from './extensions/Figcaption'
|
||||||
import { Figure } from './extensions/Figure'
|
import { Figure } from './extensions/Figure'
|
||||||
|
|
||||||
|
import { Loading } from '../_shared/Loading'
|
||||||
import styles from './SimplifiedEditor.module.scss'
|
import styles from './SimplifiedEditor.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -47,17 +48,20 @@ type Props = {
|
||||||
onChange?: (text: string) => void
|
onChange?: (text: string) => void
|
||||||
variant?: 'minimal' | 'bordered'
|
variant?: 'minimal' | 'bordered'
|
||||||
maxLength?: number
|
maxLength?: number
|
||||||
|
noLimits?: boolean
|
||||||
maxHeight?: number
|
maxHeight?: number
|
||||||
submitButtonText?: string
|
submitButtonText?: string
|
||||||
quoteEnabled?: boolean
|
quoteEnabled?: boolean
|
||||||
imageEnabled?: boolean
|
imageEnabled?: boolean
|
||||||
setClear?: boolean
|
setClear?: boolean
|
||||||
|
resetToInitial?: boolean
|
||||||
smallHeight?: boolean
|
smallHeight?: boolean
|
||||||
submitByCtrlEnter?: boolean
|
submitByCtrlEnter?: boolean
|
||||||
onlyBubbleControls?: boolean
|
onlyBubbleControls?: boolean
|
||||||
controlsAlwaysVisible?: boolean
|
controlsAlwaysVisible?: boolean
|
||||||
autoFocus?: boolean
|
autoFocus?: boolean
|
||||||
isCancelButtonVisible?: boolean
|
isCancelButtonVisible?: boolean
|
||||||
|
isPosting?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_MAX_LENGTH = 400
|
const DEFAULT_MAX_LENGTH = 400
|
||||||
|
@ -122,7 +126,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
openOnClick: false,
|
openOnClick: false,
|
||||||
}),
|
}),
|
||||||
CharacterCount.configure({
|
CharacterCount.configure({
|
||||||
limit: maxLength,
|
limit: props.noLimits ? null : maxLength,
|
||||||
}),
|
}),
|
||||||
Blockquote.configure({
|
Blockquote.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
|
@ -214,6 +218,10 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
if (props.setClear) {
|
if (props.setClear) {
|
||||||
editor().commands.clearContent(true)
|
editor().commands.clearContent(true)
|
||||||
}
|
}
|
||||||
|
if (props.resetToInitial) {
|
||||||
|
editor().commands.clearContent(true)
|
||||||
|
editor().commands.setContent(props.initialContent)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = (event) => {
|
||||||
|
@ -365,12 +373,14 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
<Show when={isCancelButtonVisible()}>
|
<Show when={isCancelButtonVisible()}>
|
||||||
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
|
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={!props.isPosting} fallback={<Loading />}>
|
||||||
<Button
|
<Button
|
||||||
value={props.submitButtonText ?? t('Send')}
|
value={props.submitButtonText ?? t('Send')}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={isEmpty()}
|
disabled={isEmpty()}
|
||||||
onClick={() => props.onSubmit(html())}
|
onClick={() => props.onSubmit(html())}
|
||||||
/>
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { Icon } from '../../_shared/Icon'
|
||||||
import { Loading } from '../../_shared/Loading'
|
import { Loading } from '../../_shared/Loading'
|
||||||
import { InlineForm } from '../InlineForm'
|
import { InlineForm } from '../InlineForm'
|
||||||
|
|
||||||
|
import { useSession } from '../../../context/session'
|
||||||
import styles from './UploadModalContent.module.scss'
|
import styles from './UploadModalContent.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -24,12 +25,12 @@ export const UploadModalContent = (props: Props) => {
|
||||||
const [uploadError, setUploadError] = createSignal<string | undefined>()
|
const [uploadError, setUploadError] = createSignal<string | undefined>()
|
||||||
const [dragActive, setDragActive] = createSignal(false)
|
const [dragActive, setDragActive] = createSignal(false)
|
||||||
const [dragError, setDragError] = createSignal<string | undefined>()
|
const [dragError, setDragError] = createSignal<string | undefined>()
|
||||||
|
const { session } = useSession()
|
||||||
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
|
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
|
||||||
const runUpload = async (file: UploadFile) => {
|
const runUpload = async (file: UploadFile) => {
|
||||||
try {
|
try {
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
const result = await handleImageUpload(file)
|
const result = await handleImageUpload(file, session()?.access_token)
|
||||||
props.onClose(result)
|
props.onClose(result)
|
||||||
setIsUploading(false)
|
setIsUploading(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -603,6 +603,7 @@
|
||||||
.shoutCardDetailsItem {
|
.shoutCardDetailsItem {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
font-size: 1.4rem;
|
||||||
margin-right: 1.2rem;
|
margin-right: 1.2rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
|
|
@ -89,8 +89,8 @@ const getTitleAndSubtitle = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMainTopicTitle = (article: Shout, lng: string) => {
|
const getMainTopicTitle = (article: Shout, lng: string) => {
|
||||||
const mainTopicSlug = article.main_topic || ''
|
const mainTopicSlug = article?.main_topic || ''
|
||||||
const mainTopic = article.topics?.find((tpc: Topic) => tpc.slug === mainTopicSlug)
|
const mainTopic = article?.topics?.find((tpc: Topic) => tpc.slug === mainTopicSlug)
|
||||||
const mainTopicTitle =
|
const mainTopicTitle =
|
||||||
mainTopicSlug && lng === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || ''
|
mainTopicSlug && lng === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || ''
|
||||||
|
|
||||||
|
@ -111,8 +111,8 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
||||||
const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
|
const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
|
||||||
const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true)
|
const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true)
|
||||||
const description = getDescription(props.article.body)
|
const description = getDescription(props.article?.body)
|
||||||
const aspectRatio = () => LAYOUT_ASPECT[props.article.layout]
|
const aspectRatio = () => LAYOUT_ASPECT[props.article?.layout]
|
||||||
const [mainTopicTitle, mainTopicSlug] = getMainTopicTitle(props.article, lang())
|
const [mainTopicTitle, mainTopicSlug] = getMainTopicTitle(props.article, lang())
|
||||||
const { title, subtitle } = getTitleAndSubtitle(props.article)
|
const { title, subtitle } = getTitleAndSubtitle(props.article)
|
||||||
|
|
||||||
|
@ -328,7 +328,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
<Popover content={t('Edit')} disabled={isActionPopupActive()}>
|
<Popover content={t('Edit')} disabled={isActionPopupActive()}>
|
||||||
{(triggerRef: (el) => void) => (
|
{(triggerRef: (el) => void) => (
|
||||||
<div class={styles.shoutCardDetailsItem} ref={triggerRef}>
|
<div class={styles.shoutCardDetailsItem} ref={triggerRef}>
|
||||||
<a href={getPagePath(router, 'edit', { shoutId: props.article.id.toString() })}>
|
<a href={getPagePath(router, 'edit', { shoutId: props.article?.id.toString() })}>
|
||||||
<Icon name="pencil-outline" class={clsx(styles.icon, styles.feedControlIcon)} />
|
<Icon name="pencil-outline" class={clsx(styles.icon, styles.feedControlIcon)} />
|
||||||
<Icon
|
<Icon
|
||||||
name="pencil-outline-hover"
|
name="pencil-outline-hover"
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
.feedArticlePopup {
|
.feedArticlePopup {
|
||||||
box-shadow: none !important;
|
|
||||||
border: 1px solid rgb(0 0 0 / 15%);
|
|
||||||
border-radius: 1.6rem;
|
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -4,21 +4,20 @@ import { For, Show, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useFollowing } from '../../../context/following'
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { useSeen } from '../../../context/seen'
|
||||||
import { Author } from '../../../graphql/schema/core.gen'
|
import { Author } from '../../../graphql/schema/core.gen'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { useArticlesStore } from '../../../stores/zine/articles'
|
import { useArticlesStore } from '../../../stores/zine/articles'
|
||||||
import { useSeenStore } from '../../../stores/zine/seen'
|
|
||||||
import { Userpic } from '../../Author/Userpic'
|
import { Userpic } from '../../Author/Userpic'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
|
||||||
import styles from './Sidebar.module.scss'
|
import styles from './Sidebar.module.scss'
|
||||||
|
|
||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { seen } = useSeenStore()
|
const { seen } = useSeen()
|
||||||
const { subscriptions } = useFollowing()
|
const { subscriptions } = useFollowing()
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const { articlesByTopic } = useArticlesStore()
|
const { articlesByTopic, articlesByAuthor } = useArticlesStore()
|
||||||
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
|
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
|
||||||
|
|
||||||
const checkTopicIsSeen = (topicSlug: string) => {
|
const checkTopicIsSeen = (topicSlug: string) => {
|
||||||
|
@ -26,8 +25,9 @@ export const Sidebar = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkAuthorIsSeen = (authorSlug: string) => {
|
const checkAuthorIsSeen = (authorSlug: string) => {
|
||||||
return Boolean(seen()[authorSlug])
|
return articlesByAuthor()[authorSlug]?.every((article) => Boolean(seen()[article.slug]))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.sidebar}>
|
<div class={styles.sidebar}>
|
||||||
<ul class={styles.feedFilters}>
|
<ul class={styles.feedFilters}>
|
||||||
|
|
|
@ -31,7 +31,7 @@ export const LoginForm = () => {
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
// FIXME: use signal or remove
|
||||||
const [_isLinkSent, setIsLinkSent] = createSignal(false)
|
const [_isLinkSent, setIsLinkSent] = createSignal(false)
|
||||||
const authFormRef: { current: HTMLFormElement } = { current: null }
|
const authFormRef: { current: HTMLFormElement } = { current: null }
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
|
@ -97,7 +97,12 @@ export const LoginForm = () => {
|
||||||
const { errors } = await signIn({ email: email(), password: password() })
|
const { errors } = await signIn({ email: email(), password: password() })
|
||||||
console.error('[signIn errors]', errors)
|
console.error('[signIn errors]', errors)
|
||||||
if (errors?.length > 0) {
|
if (errors?.length > 0) {
|
||||||
if (errors.some((error) => error.message.includes('bad user credentials'))) {
|
if (
|
||||||
|
errors.some(
|
||||||
|
(error) =>
|
||||||
|
error.message.includes('bad user credentials') || error.message.includes('user not found'),
|
||||||
|
)
|
||||||
|
) {
|
||||||
setValidationErrors((prev) => ({
|
setValidationErrors((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
password: t('Something went wrong, check email and password'),
|
password: t('Something went wrong, check email and password'),
|
||||||
|
|
|
@ -16,6 +16,9 @@ type Props = {
|
||||||
onBlur?: (value: string) => void
|
onBlur?: (value: string) => void
|
||||||
variant?: 'login' | 'registration'
|
variant?: 'login' | 'registration'
|
||||||
disableAutocomplete?: boolean
|
disableAutocomplete?: boolean
|
||||||
|
noValidate?: boolean
|
||||||
|
onFocus?: () => void
|
||||||
|
value?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const minLength = 8
|
const minLength = 8
|
||||||
|
@ -27,7 +30,7 @@ export const PasswordField = (props: Props) => {
|
||||||
const [showPassword, setShowPassword] = createSignal(false)
|
const [showPassword, setShowPassword] = createSignal(false)
|
||||||
const [error, setError] = createSignal<string>()
|
const [error, setError] = createSignal<string>()
|
||||||
|
|
||||||
const validatePassword = (passwordToCheck) => {
|
const validatePassword = (passwordToCheck: string) => {
|
||||||
if (passwordToCheck.length < minLength) {
|
if (passwordToCheck.length < minLength) {
|
||||||
return t('Password should be at least 8 characters')
|
return t('Password should be at least 8 characters')
|
||||||
}
|
}
|
||||||
|
@ -50,6 +53,7 @@ export const PasswordField = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onInput(value)
|
props.onInput(value)
|
||||||
|
if (!props.noValidate) {
|
||||||
const errorValue = validatePassword(value)
|
const errorValue = validatePassword(value)
|
||||||
if (errorValue) {
|
if (errorValue) {
|
||||||
setError(errorValue)
|
setError(errorValue)
|
||||||
|
@ -57,6 +61,7 @@ export const PasswordField = (props: Props) => {
|
||||||
setError()
|
setError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
|
@ -78,6 +83,8 @@ export const PasswordField = (props: Props) => {
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
|
onFocus={props.onFocus}
|
||||||
|
value={props.value ? props.value : ''}
|
||||||
autocomplete={props.disableAutocomplete ? 'one-time-code' : 'current-password'}
|
autocomplete={props.disableAutocomplete ? 'one-time-code' : 'current-password'}
|
||||||
type={showPassword() ? 'text' : 'password'}
|
type={showPassword() ? 'text' : 'password'}
|
||||||
placeholder={props.placeholder || t('Password')}
|
placeholder={props.placeholder || t('Password')}
|
||||||
|
|
|
@ -32,6 +32,7 @@ export const RegisterForm = () => {
|
||||||
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { signUp, isRegistered, resendVerifyEmail } = useSession()
|
const { signUp, isRegistered, resendVerifyEmail } = useSession()
|
||||||
|
// FIXME: use submit error data or remove signal
|
||||||
const [_submitError, setSubmitError] = createSignal('')
|
const [_submitError, setSubmitError] = createSignal('')
|
||||||
const [fullName, setFullName] = createSignal('')
|
const [fullName, setFullName] = createSignal('')
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
|
|
|
@ -61,7 +61,12 @@ export const SendResetLinkForm = () => {
|
||||||
redirect_uri: window.location.origin,
|
redirect_uri: window.location.origin,
|
||||||
})
|
})
|
||||||
console.debug('[SendResetLinkForm] authorizer response:', data)
|
console.debug('[SendResetLinkForm] authorizer response:', data)
|
||||||
if (errors?.some((error) => error.message.includes('bad user credentials'))) {
|
if (
|
||||||
|
errors?.some(
|
||||||
|
(error) =>
|
||||||
|
error.message.includes('bad user credentials') || error.message.includes('user not found'),
|
||||||
|
)
|
||||||
|
) {
|
||||||
setIsUserNotFound(true)
|
setIsUserNotFound(true)
|
||||||
}
|
}
|
||||||
if (data.message) setMessage(data.message)
|
if (data.message) setMessage(data.message)
|
||||||
|
|
|
@ -48,7 +48,7 @@ export const Header = (props: Props) => {
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const { requireAuthentication } = useSession()
|
const { requireAuthentication } = useSession()
|
||||||
const { searchParams } = useRouter<HeaderSearchParams>()
|
const { searchParams } = useRouter<HeaderSearchParams>()
|
||||||
const { topics } = useTopics()
|
const { sortedTopics: topics } = useTopics()
|
||||||
const [randomTopics, setRandomTopics] = createSignal([])
|
const [randomTopics, setRandomTopics] = createSignal([])
|
||||||
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
|
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
|
||||||
const [getIsScrolled, setIsScrolled] = createSignal(false)
|
const [getIsScrolled, setIsScrolled] = createSignal(false)
|
||||||
|
@ -59,7 +59,7 @@ export const Header = (props: Props) => {
|
||||||
const [isTopicsVisible, setIsTopicsVisible] = createSignal(false)
|
const [isTopicsVisible, setIsTopicsVisible] = createSignal(false)
|
||||||
const [isZineVisible, setIsZineVisible] = createSignal(false)
|
const [isZineVisible, setIsZineVisible] = createSignal(false)
|
||||||
const [isFeedVisible, setIsFeedVisible] = createSignal(false)
|
const [isFeedVisible, setIsFeedVisible] = createSignal(false)
|
||||||
const { isAuthenticated } = useSession()
|
const { session } = useSession()
|
||||||
|
|
||||||
const toggleFixed = () => setFixed(!fixed())
|
const toggleFixed = () => setFixed(!fixed())
|
||||||
|
|
||||||
|
@ -69,7 +69,9 @@ export const Header = (props: Props) => {
|
||||||
let windowScrollTop = 0
|
let windowScrollTop = 0
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
if (topics()?.length) {
|
||||||
setRandomTopics(getRandomTopicsFromArray(topics()))
|
setRandomTopics(getRandomTopicsFromArray(topics()))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
@ -333,7 +335,7 @@ export const Header = (props: Props) => {
|
||||||
<Show when={props.title}>
|
<Show when={props.title}>
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.articleControls, 'col-auto', {
|
class={clsx(styles.articleControls, 'col-auto', {
|
||||||
[styles.articleControlsAuthorized]: isAuthenticated(),
|
[styles.articleControlsAuthorized]: session()?.user?.id,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<SharePopup
|
<SharePopup
|
||||||
|
|
|
@ -32,14 +32,14 @@ const MD_WIDTH_BREAKPOINT = 992
|
||||||
export const HeaderAuth = (props: Props) => {
|
export const HeaderAuth = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const { session, author, isAuthenticated, isSessionLoaded } = useSession()
|
const { session, author, isSessionLoaded } = useSession()
|
||||||
const { unreadNotificationsCount, showNotificationsPanel } = useNotifications()
|
const { unreadNotificationsCount, showNotificationsPanel } = useNotifications()
|
||||||
const { form, toggleEditorPanel, saveShout, publishShout } = useEditorContext()
|
const { form, toggleEditorPanel, publishShout } = useEditorContext()
|
||||||
|
|
||||||
const handleBellIconClick = (event: Event) => {
|
const handleBellIconClick = (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
if (!isAuthenticated()) {
|
if (!session()?.access_token) {
|
||||||
showModal('auth')
|
showModal('auth')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -48,21 +48,17 @@ export const HeaderAuth = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings')
|
const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings')
|
||||||
const isNotificationsVisible = createMemo(() => isAuthenticated() && !isEditorPage())
|
const isNotificationsVisible = createMemo(() => session()?.access_token && !isEditorPage())
|
||||||
const isSaveButtonVisible = createMemo(() => isAuthenticated() && isEditorPage())
|
const isSaveButtonVisible = createMemo(() => session()?.access_token && isEditorPage())
|
||||||
const isCreatePostButtonVisible = createMemo(() => !isEditorPage())
|
const isCreatePostButtonVisible = createMemo(() => !isEditorPage())
|
||||||
const isAuthenticatedControlsVisible = createMemo(
|
const isAuthenticatedControlsVisible = createMemo(
|
||||||
() => isAuthenticated() && session()?.user?.email_verified,
|
() => session()?.access_token && session()?.user?.email_verified,
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleBurgerButtonClick = () => {
|
const handleBurgerButtonClick = () => {
|
||||||
toggleEditorPanel()
|
toggleEditorPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
const _handleSaveButtonClick = () => {
|
|
||||||
saveShout(form)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [width, setWidth] = createSignal(0)
|
const [width, setWidth] = createSignal(0)
|
||||||
const [editorMode, setEditorMode] = createSignal(t('Editing'))
|
const [editorMode, setEditorMode] = createSignal(t('Editing'))
|
||||||
|
|
||||||
|
@ -106,14 +102,8 @@ export const HeaderAuth = (props: Props) => {
|
||||||
<Show when={isSessionLoaded()} keyed={true}>
|
<Show when={isSessionLoaded()} keyed={true}>
|
||||||
<div class={clsx('col-auto col-lg-7', styles.usernav)}>
|
<div class={clsx('col-auto col-lg-7', styles.usernav)}>
|
||||||
<div class={styles.userControl}>
|
<div class={styles.userControl}>
|
||||||
<Show when={isCreatePostButtonVisible() && isAuthenticated()}>
|
<Show when={isCreatePostButtonVisible() && session()?.access_token}>
|
||||||
<div
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
class={clsx(
|
|
||||||
styles.userControlItem,
|
|
||||||
styles.userControlItemVerbose,
|
|
||||||
styles.userControlItemCreate,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<a href={getPagePath(router, 'create')}>
|
<a href={getPagePath(router, 'create')}>
|
||||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||||
<Icon name="pencil-outline" class={styles.icon} />
|
<Icon name="pencil-outline" class={styles.icon} />
|
||||||
|
@ -156,7 +146,6 @@ export const HeaderAuth = (props: Props) => {
|
||||||
{editorMode()}
|
{editorMode()}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
variant="bordered"
|
|
||||||
popupCssClass={styles.editorPopup}
|
popupCssClass={styles.editorPopup}
|
||||||
>
|
>
|
||||||
<ul class={clsx('nodash', styles.editorModesList)}>
|
<ul class={clsx('nodash', styles.editorModesList)}>
|
||||||
|
@ -220,18 +209,12 @@ export const HeaderAuth = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={isCreatePostButtonVisible() && !isAuthenticated()}>
|
<Show when={isCreatePostButtonVisible() && !session()?.access_token}>
|
||||||
<div
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
class={clsx(
|
|
||||||
styles.userControlItem,
|
|
||||||
styles.userControlItemVerbose,
|
|
||||||
styles.userControlItemCreate,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<a href={getPagePath(router, 'create')}>
|
<a href={getPagePath(router, 'create')}>
|
||||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||||
<Icon name="pencil-outline" class={styles.icon} />
|
<Icon name="pencil" class={styles.icon} />
|
||||||
<Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} />
|
<Icon name="pencil" class={clsx(styles.icon, styles.iconHover)} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -239,12 +222,12 @@ export const HeaderAuth = (props: Props) => {
|
||||||
<Show
|
<Show
|
||||||
when={isAuthenticatedControlsVisible()}
|
when={isAuthenticatedControlsVisible()}
|
||||||
fallback={
|
fallback={
|
||||||
<Show when={!isAuthenticated()}>
|
<Show when={!session()?.access_token}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
||||||
<a href="?m=auth&mode=login">
|
<a href="?m=auth&mode=login">
|
||||||
<span class={styles.textLabel}>{t('Enter')}</span>
|
<span class={styles.textLabel}>{t('Enter')}</span>
|
||||||
<Icon name="key" class={styles.icon} />
|
<Icon name="key" class={styles.icon} />
|
||||||
<Icon name="key" class={clsx(styles.icon, styles.iconHover)} />
|
{/*<Icon name="user-default" class={clsx(styles.icon, styles.iconHover)} />*/}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -268,7 +251,7 @@ export const HeaderAuth = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={isAuthenticated()}>
|
<Show when={session()?.access_token}>
|
||||||
<ProfilePopup
|
<ProfilePopup
|
||||||
onVisibilityChange={(isVisible) => {
|
onVisibilityChange={(isVisible) => {
|
||||||
props.setIsProfilePopupVisible(isVisible)
|
props.setIsProfilePopupVisible(isVisible)
|
||||||
|
|
|
@ -5,8 +5,10 @@ import { getPagePath } from '@nanostores/router'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { router } from '../../stores/router'
|
import { router } from '../../stores/router'
|
||||||
|
import { Icon } from '../_shared/Icon'
|
||||||
import { Popup } from '../_shared/Popup'
|
import { Popup } from '../_shared/Popup'
|
||||||
|
|
||||||
|
import { clsx } from 'clsx'
|
||||||
import styles from '../_shared/Popup/Popup.module.scss'
|
import styles from '../_shared/Popup/Popup.module.scss'
|
||||||
|
|
||||||
type ProfilePopupProps = Omit<PopupProps, 'children'>
|
type ProfilePopupProps = Omit<PopupProps, 'children'>
|
||||||
|
@ -16,30 +18,53 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup {...props} horizontalAnchor="right" variant="bordered">
|
<Popup {...props} horizontalAnchor="right" popupCssClass={styles.profilePopup}>
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<li>
|
||||||
<a href={getPagePath(router, 'author', { slug: author().slug })}>{t('Profile')}</a>
|
<a class={styles.action} href={getPagePath(router, 'author', { slug: author().slug })}>
|
||||||
|
<Icon name="profile" class={styles.icon} />
|
||||||
|
{t('Profile')}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={getPagePath(router, 'drafts')}>{t('Drafts')}</a>
|
<a class={styles.action} href={getPagePath(router, 'drafts')}>
|
||||||
|
<Icon name="pencil-outline" class={styles.icon} />
|
||||||
|
{t('Drafts')}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={`${getPagePath(router, 'author', { slug: author().slug })}?m=following`}>
|
<a
|
||||||
|
class={styles.action}
|
||||||
|
href={`${getPagePath(router, 'author', { slug: author().slug })}?m=following`}
|
||||||
|
>
|
||||||
|
<Icon name="feed-all" class={styles.icon} />
|
||||||
{t('Subscriptions')}
|
{t('Subscriptions')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={`${getPagePath(router, 'authorComments', { slug: author().slug })}`}>{t('Comments')}</a>
|
<a
|
||||||
|
class={styles.action}
|
||||||
|
href={`${getPagePath(router, 'authorComments', { slug: author().slug })}`}
|
||||||
|
>
|
||||||
|
<Icon name="comment" class={styles.icon} />
|
||||||
|
{t('Comments')}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#">{t('Bookmarks')}</a>
|
<a class={styles.action} href="#">
|
||||||
|
<Icon name="bookmark" class={styles.icon} />
|
||||||
|
{t('Bookmarks')}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={getPagePath(router, 'profileSettings')}>{t('Settings')}</a>
|
<a class={styles.action} href={getPagePath(router, 'profileSettings')}>
|
||||||
|
<Icon name="settings" class={styles.icon} />
|
||||||
|
{t('Settings')}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class={styles.topBorderItem}>
|
<li class={styles.topBorderItem}>
|
||||||
<span class="link" onClick={() => signOut()}>
|
<span class={clsx(styles.action, 'link')} onClick={() => signOut()}>
|
||||||
|
<Icon name="logout" class={styles.icon} />
|
||||||
{t('Logout')}
|
{t('Logout')}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
left: 0;
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
background-color: #d00820;
|
background-color: #d00820;
|
||||||
|
|
|
@ -46,7 +46,7 @@ const isEarlier = (date: Date) => {
|
||||||
|
|
||||||
export const NotificationsPanel = (props: Props) => {
|
export const NotificationsPanel = (props: Props) => {
|
||||||
const [isLoading, setIsLoading] = createSignal(false)
|
const [isLoading, setIsLoading] = createSignal(false)
|
||||||
const { isAuthenticated } = useSession()
|
const { author } = useSession()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const {
|
const {
|
||||||
after,
|
after,
|
||||||
|
@ -150,16 +150,13 @@ export const NotificationsPanel = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(author, async (a) => {
|
||||||
() => isAuthenticated(),
|
if (a?.id) {
|
||||||
async () => {
|
|
||||||
if (isAuthenticated()) {
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
await loadNextPage()
|
await loadNextPage()
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -20,6 +20,8 @@ import { useLocalize } from '../../context/localize'
|
||||||
import { useProfileForm } from '../../context/profile'
|
import { useProfileForm } from '../../context/profile'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { useSnackbar } from '../../context/snackbar'
|
import { useSnackbar } from '../../context/snackbar'
|
||||||
|
import { ProfileInput } from '../../graphql/schema/core.gen'
|
||||||
|
import styles from '../../pages/profile/Settings.module.scss'
|
||||||
import { hideModal, showModal } from '../../stores/ui'
|
import { hideModal, showModal } from '../../stores/ui'
|
||||||
import { clone } from '../../utils/clone'
|
import { clone } from '../../utils/clone'
|
||||||
import { getImageUrl } from '../../utils/getImageUrl'
|
import { getImageUrl } from '../../utils/getImageUrl'
|
||||||
|
@ -35,14 +37,12 @@ import { Loading } from '../_shared/Loading'
|
||||||
import { Popover } from '../_shared/Popover'
|
import { Popover } from '../_shared/Popover'
|
||||||
import { SocialNetworkInput } from '../_shared/SocialNetworkInput'
|
import { SocialNetworkInput } from '../_shared/SocialNetworkInput'
|
||||||
|
|
||||||
import styles from '../../pages/profile/Settings.module.scss'
|
|
||||||
|
|
||||||
const SimplifiedEditor = lazy(() => import('../../components/Editor/SimplifiedEditor'))
|
const SimplifiedEditor = lazy(() => import('../../components/Editor/SimplifiedEditor'))
|
||||||
const GrowingTextarea = lazy(() => import('../../components/_shared/GrowingTextarea/GrowingTextarea'))
|
const GrowingTextarea = lazy(() => import('../../components/_shared/GrowingTextarea/GrowingTextarea'))
|
||||||
|
|
||||||
export const ProfileSettings = () => {
|
export const ProfileSettings = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const [prevForm, setPrevForm] = createStore({})
|
const [prevForm, setPrevForm] = createStore<ProfileInput>({})
|
||||||
const [isFormInitialized, setIsFormInitialized] = createSignal(false)
|
const [isFormInitialized, setIsFormInitialized] = createSignal(false)
|
||||||
const [isSaving, setIsSaving] = createSignal(false)
|
const [isSaving, setIsSaving] = createSignal(false)
|
||||||
const [social, setSocial] = createSignal([])
|
const [social, setSocial] = createSignal([])
|
||||||
|
@ -57,8 +57,9 @@ export const ProfileSettings = () => {
|
||||||
const [nameError, setNameError] = createSignal<string>()
|
const [nameError, setNameError] = createSignal<string>()
|
||||||
const { form, submit, updateFormField, setForm } = useProfileForm()
|
const { form, submit, updateFormField, setForm } = useProfileForm()
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
const { loadAuthor } = useSession()
|
const { loadAuthor, session } = useSession()
|
||||||
const { showConfirm } = useConfirm()
|
const { showConfirm } = useConfirm()
|
||||||
|
const [clearAbout, setClearAbout] = createSignal(false)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (Object.keys(form).length > 0 && !isFormInitialized()) {
|
if (Object.keys(form).length > 0 && !isFormInitialized()) {
|
||||||
|
@ -121,7 +122,9 @@ export const ProfileSettings = () => {
|
||||||
declineButtonVariant: 'secondary',
|
declineButtonVariant: 'secondary',
|
||||||
})
|
})
|
||||||
if (isConfirmed) {
|
if (isConfirmed) {
|
||||||
|
setClearAbout(true)
|
||||||
setForm(clone(prevForm))
|
setForm(clone(prevForm))
|
||||||
|
setClearAbout(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +143,7 @@ export const ProfileSettings = () => {
|
||||||
setUploadError(false)
|
setUploadError(false)
|
||||||
setIsUserpicUpdating(true)
|
setIsUserpicUpdating(true)
|
||||||
|
|
||||||
const result = await handleImageUpload(uploadFile)
|
const result = await handleImageUpload(uploadFile, session()?.access_token)
|
||||||
updateFormField('pic', result.url)
|
updateFormField('pic', result.url)
|
||||||
|
|
||||||
setUserpicFile(null)
|
setUserpicFile(null)
|
||||||
|
@ -171,11 +174,13 @@ export const ProfileSettings = () => {
|
||||||
on(
|
on(
|
||||||
() => deepEqual(form, prevForm),
|
() => deepEqual(form, prevForm),
|
||||||
() => {
|
() => {
|
||||||
|
if (Object.keys(prevForm).length > 0) {
|
||||||
setIsFloatingPanelVisible(!deepEqual(form, prevForm))
|
setIsFloatingPanelVisible(!deepEqual(form, prevForm))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ defer: true },
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleDeleteSocialLink = (link) => {
|
const handleDeleteSocialLink = (link) => {
|
||||||
updateFormField('links', link, true)
|
updateFormField('links', link, true)
|
||||||
}
|
}
|
||||||
|
@ -317,6 +322,8 @@ export const ProfileSettings = () => {
|
||||||
|
|
||||||
<h4>{t('About')}</h4>
|
<h4>{t('About')}</h4>
|
||||||
<SimplifiedEditor
|
<SimplifiedEditor
|
||||||
|
resetToInitial={clearAbout()}
|
||||||
|
noLimits={true}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
onlyBubbleControls={true}
|
onlyBubbleControls={true}
|
||||||
smallHeight={true}
|
smallHeight={true}
|
||||||
|
|
|
@ -49,11 +49,9 @@ export const TopicCard = (props: TopicProps) => {
|
||||||
|
|
||||||
const handleFollowClick = () => {
|
const handleFollowClick = () => {
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
if (isSubscribed()) {
|
isSubscribed()
|
||||||
unfollow(FollowingEntity.Topic, props.topic.slug)
|
? unfollow(FollowingEntity.Topic, props.topic.slug)
|
||||||
} else {
|
: follow(FollowingEntity.Topic, props.topic.slug)
|
||||||
follow(FollowingEntity.Topic, props.topic.slug)
|
|
||||||
}
|
|
||||||
}, 'subscribe')
|
}, 'subscribe')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ export const TopicBadge = (props: Props) => {
|
||||||
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
|
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.TopicBadge, { [styles.TopicBadgeSubscriptionsMode]: props.subscriptionsMode })}>
|
<div class={clsx(styles.TopicBadge, props.subscriptionsMode)}>
|
||||||
<div class={styles.content}>
|
<div class={styles.content}>
|
||||||
<div class={styles.basicInfo}>
|
<div class={styles.basicInfo}>
|
||||||
<Show when={props.subscriptionsMode}>
|
<Show when={props.subscriptionsMode}>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { Author } from '../../../graphql/schema/core.gen'
|
import type { Author } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { Meta } from '@solidjs/meta'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createMemo, createSignal } from 'solid-js'
|
import { For, Show, createMemo, createSignal } from 'solid-js'
|
||||||
|
import { Meta } from '../../../context/meta'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import type { Topic } from '../../../graphql/schema/core.gen'
|
import type { Topic } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { Meta } from '@solidjs/meta'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { Meta } from '../../../context/meta'
|
||||||
|
import { useTopics } from '../../../context/topics'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import { setTopicsSort, useTopicsStore } from '../../../stores/zine/topics'
|
|
||||||
import { capitalize } from '../../../utils/capitalize'
|
import { capitalize } from '../../../utils/capitalize'
|
||||||
import { dummyFilter } from '../../../utils/dummyFilter'
|
import { dummyFilter } from '../../../utils/dummyFilter'
|
||||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
|
@ -33,11 +33,7 @@ export const AllTopics = (props: Props) => {
|
||||||
const [limit, setLimit] = createSignal(PAGE_SIZE)
|
const [limit, setLimit] = createSignal(PAGE_SIZE)
|
||||||
const ALPHABET =
|
const ALPHABET =
|
||||||
lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ#'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ#']
|
lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ#'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ#']
|
||||||
|
const { sortedTopics, setTopicsSort } = useTopics()
|
||||||
const { sortedTopics } = useTopicsStore({
|
|
||||||
topics: props.topics,
|
|
||||||
sortBy: searchParams().by || 'shouts',
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!searchParams().by) {
|
if (!searchParams().by) {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/core.gen'
|
import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { Meta, Title } from '@solidjs/meta'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { useFollowing } from '../../../context/following'
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { Meta, Title } from '../../../context/meta'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { apiClient } from '../../../graphql/client/core'
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
|
@ -42,7 +42,6 @@ export const AuthorView = (props: Props) => {
|
||||||
const { followers: myFollowers } = useFollowing()
|
const { followers: myFollowers } = useFollowing()
|
||||||
const { session } = useSession()
|
const { session } = useSession()
|
||||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||||
// const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
|
||||||
const { page: getPage, searchParams } = useRouter()
|
const { page: getPage, searchParams } = useRouter()
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
||||||
|
@ -244,7 +243,7 @@ export const AuthorView = (props: Props) => {
|
||||||
class={styles.longBio}
|
class={styles.longBio}
|
||||||
classList={{ [styles.longBioExpanded]: isBioExpanded() }}
|
classList={{ [styles.longBioExpanded]: isBioExpanded() }}
|
||||||
>
|
>
|
||||||
<div ref={(el) => (bioContainerRef.current = el)} innerHTML={author().about} />
|
<div ref={(el) => (bioContainerRef.current = el)} innerHTML={author()?.about || ''} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={showExpandBioControl()}>
|
<Show when={showExpandBioControl()}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { openPage } from '@nanostores/router'
|
import { openPage } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createSignal } from 'solid-js'
|
import { For, Show, createEffect, createSignal, on } from 'solid-js'
|
||||||
|
|
||||||
import { useEditorContext } from '../../../context/editor'
|
import { useEditorContext } from '../../../context/editor'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
@ -9,22 +9,28 @@ import { Shout } from '../../../graphql/schema/core.gen'
|
||||||
import { router } from '../../../stores/router'
|
import { router } from '../../../stores/router'
|
||||||
import { Draft } from '../../Draft'
|
import { Draft } from '../../Draft'
|
||||||
|
|
||||||
|
import { Loading } from '../../_shared/Loading'
|
||||||
import styles from './DraftsView.module.scss'
|
import styles from './DraftsView.module.scss'
|
||||||
|
|
||||||
export const DraftsView = () => {
|
export const DraftsView = () => {
|
||||||
const { isAuthenticated, isSessionLoaded } = useSession()
|
const { author, loadSession } = useSession()
|
||||||
const [drafts, setDrafts] = createSignal<Shout[]>([])
|
const [drafts, setDrafts] = createSignal<Shout[]>([])
|
||||||
|
|
||||||
const loadDrafts = async () => {
|
createEffect(
|
||||||
if (apiClient.private) {
|
on(
|
||||||
const loadedDrafts = await apiClient.getDrafts()
|
() => author(),
|
||||||
setDrafts(loadedDrafts.reverse() || [])
|
async (a) => {
|
||||||
|
if (a) {
|
||||||
|
const { shouts: loadedDrafts, error } = await apiClient.getDrafts()
|
||||||
|
if (error) {
|
||||||
|
console.warn(error)
|
||||||
|
await loadSession()
|
||||||
}
|
}
|
||||||
|
setDrafts(loadedDrafts || [])
|
||||||
}
|
}
|
||||||
|
},
|
||||||
createEffect(() => {
|
),
|
||||||
if (isSessionLoaded()) loadDrafts()
|
)
|
||||||
})
|
|
||||||
|
|
||||||
const { publishShoutById, deleteShout } = useEditorContext()
|
const { publishShoutById, deleteShout } = useEditorContext()
|
||||||
|
|
||||||
|
@ -44,11 +50,10 @@ export const DraftsView = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.DraftsView)}>
|
<div class={clsx(styles.DraftsView)}>
|
||||||
<Show when={isSessionLoaded()}>
|
<Show when={author()?.id} fallback={<Loading />}>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
|
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
|
||||||
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
|
|
||||||
<For each={drafts()}>
|
<For each={drafts()}>
|
||||||
{(draft) => (
|
{(draft) => (
|
||||||
<Draft
|
<Draft
|
||||||
|
@ -59,7 +64,6 @@ export const DraftsView = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { clsx } from 'clsx'
|
||||||
import deepEqual from 'fast-deep-equal'
|
import deepEqual from 'fast-deep-equal'
|
||||||
import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js'
|
import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
import { throttle } from 'throttle-debounce'
|
||||||
|
|
||||||
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
@ -41,7 +42,9 @@ export const EMPTY_TOPIC: Topic = {
|
||||||
slug: '',
|
slug: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const THROTTLING_INTERVAL = 2000
|
||||||
const AUTO_SAVE_INTERVAL = 5000
|
const AUTO_SAVE_INTERVAL = 5000
|
||||||
|
const AUTO_SAVE_DELAY = 5000
|
||||||
const handleScrollTopButtonClick = (e) => {
|
const handleScrollTopButtonClick = (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
|
@ -65,12 +68,14 @@ export const EditView = (props: Props) => {
|
||||||
} = useEditorContext()
|
} = useEditorContext()
|
||||||
const shoutTopics = props.shout.topics || []
|
const shoutTopics = props.shout.topics || []
|
||||||
|
|
||||||
// TODO: проверить сохранение черновика в local storage (не работает)
|
|
||||||
const draft = getDraftFromLocalStorage(props.shout.id)
|
const draft = getDraftFromLocalStorage(props.shout.id)
|
||||||
|
|
||||||
if (draft) {
|
if (draft) {
|
||||||
setForm(Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id })
|
const draftForm = Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id }
|
||||||
|
setForm(draftForm)
|
||||||
|
console.debug('draft from localstorage: ', draftForm)
|
||||||
} else {
|
} else {
|
||||||
setForm({
|
const draftForm = {
|
||||||
slug: props.shout.slug,
|
slug: props.shout.slug,
|
||||||
shoutId: props.shout.id,
|
shoutId: props.shout.id,
|
||||||
title: props.shout.title,
|
title: props.shout.title,
|
||||||
|
@ -83,7 +88,9 @@ export const EditView = (props: Props) => {
|
||||||
coverImageUrl: props.shout.cover,
|
coverImageUrl: props.shout.cover,
|
||||||
media: props.shout.media,
|
media: props.shout.media,
|
||||||
layout: props.shout.layout,
|
layout: props.shout.layout,
|
||||||
})
|
}
|
||||||
|
setForm(draftForm)
|
||||||
|
console.debug('draft from props data: ', draftForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtitleInput: { current: HTMLTextAreaElement } = { current: null }
|
const subtitleInput: { current: HTMLTextAreaElement } = { current: null }
|
||||||
|
@ -106,9 +113,6 @@ export const EditView = (props: Props) => {
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
window.removeEventListener('scroll', handleScroll)
|
window.removeEventListener('scroll', handleScroll)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||||
const handleBeforeUnload = (event) => {
|
const handleBeforeUnload = (event) => {
|
||||||
if (!deepEqual(prevForm, form)) {
|
if (!deepEqual(prevForm, form)) {
|
||||||
|
@ -180,42 +184,39 @@ export const EditView = (props: Props) => {
|
||||||
|
|
||||||
let autoSaveTimeOutId: number | string | NodeJS.Timeout
|
let autoSaveTimeOutId: number | string | NodeJS.Timeout
|
||||||
|
|
||||||
//TODO: add throttle
|
const autoSave = async () => {
|
||||||
const autoSaveRecursive = () => {
|
|
||||||
autoSaveTimeOutId = setTimeout(async () => {
|
|
||||||
const hasChanges = !deepEqual(form, prevForm)
|
const hasChanges = !deepEqual(form, prevForm)
|
||||||
if (hasChanges) {
|
const hasTopic = Boolean(form.mainTopic)
|
||||||
|
if (hasChanges || hasTopic) {
|
||||||
|
console.debug('saving draft', form)
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
if (props.shout?.published_at) {
|
|
||||||
saveDraftToLocalStorage(form)
|
saveDraftToLocalStorage(form)
|
||||||
} else {
|
|
||||||
await saveDraft(form)
|
await saveDraft(form)
|
||||||
}
|
|
||||||
setPrevForm(clone(form))
|
setPrevForm(clone(form))
|
||||||
setTimeout(() => {
|
setTimeout(() => setSaving(false), AUTO_SAVE_DELAY)
|
||||||
setSaving(false)
|
|
||||||
}, 2000)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throttle the autoSave function
|
||||||
|
const throttledAutoSave = throttle(THROTTLING_INTERVAL, autoSave)
|
||||||
|
|
||||||
|
const autoSaveRecursive = () => {
|
||||||
|
autoSaveTimeOutId = setTimeout(() => {
|
||||||
|
throttledAutoSave()
|
||||||
autoSaveRecursive()
|
autoSaveRecursive()
|
||||||
}, AUTO_SAVE_INTERVAL)
|
}, AUTO_SAVE_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopAutoSave = () => {
|
|
||||||
clearTimeout(autoSaveTimeOutId)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
autoSaveRecursive()
|
autoSaveRecursive()
|
||||||
})
|
onCleanup(() => clearTimeout(autoSaveTimeOutId))
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
stopAutoSave()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const showSubtitleInput = () => {
|
const showSubtitleInput = () => {
|
||||||
setIsSubtitleVisible(true)
|
setIsSubtitleVisible(true)
|
||||||
subtitleInput.current.focus()
|
subtitleInput.current.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
const showLeadInput = () => {
|
const showLeadInput = () => {
|
||||||
setIsLeadVisible(true)
|
setIsLeadVisible(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,7 +192,7 @@ export const Expo = (props: Props) => {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
|
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
|
||||||
<ArticleCardSwiper title={t('Top month articles')} slides={reactedTopMonthArticles()} />
|
<ArticleCardSwiper title={t('Top month')} slides={reactedTopMonthArticles()} />
|
||||||
</Show>
|
</Show>
|
||||||
<For each={expoShouts().slice(LOAD_MORE_PAGE_SIZE, LOAD_MORE_PAGE_SIZE * 2)}>
|
<For each={expoShouts().slice(LOAD_MORE_PAGE_SIZE, LOAD_MORE_PAGE_SIZE * 2)}>
|
||||||
{(shout) => (
|
{(shout) => (
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { Meta } from '@solidjs/meta'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { Meta } from '../../../context/meta'
|
||||||
import { useReactions } from '../../../context/reactions'
|
import { useReactions } from '../../../context/reactions'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
import { useTopics } from '../../../context/topics'
|
||||||
import { apiClient } from '../../../graphql/client/core'
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../../graphql/schema/core.gen'
|
import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../../graphql/schema/core.gen'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { showModal } from '../../../stores/ui'
|
import { showModal } from '../../../stores/ui'
|
||||||
import { resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles'
|
import { resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles'
|
||||||
import { useTopAuthorsStore } from '../../../stores/zine/topAuthors'
|
import { useTopAuthorsStore } from '../../../stores/zine/topAuthors'
|
||||||
import { useTopicsStore } from '../../../stores/zine/topics'
|
|
||||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
import { byCreated } from '../../../utils/sortby'
|
import { byCreated } from '../../../utils/sortby'
|
||||||
import { CommentDate } from '../../Article/CommentDate'
|
import { CommentDate } from '../../Article/CommentDate'
|
||||||
|
@ -49,7 +49,7 @@ type VisibilityItem = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeedSearchParams = {
|
type FeedSearchParams = {
|
||||||
by: 'publish_date' | 'likes' | 'comments'
|
by: 'publish_date' | 'likes' | 'last_comment'
|
||||||
period: FeedPeriod
|
period: FeedPeriod
|
||||||
visibility: VisibilityMode
|
visibility: VisibilityMode
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ export const FeedView = (props: Props) => {
|
||||||
const { session } = useSession()
|
const { session } = useSession()
|
||||||
const { loadReactionsBy } = useReactions()
|
const { loadReactionsBy } = useReactions()
|
||||||
const { sortedArticles } = useArticlesStore()
|
const { sortedArticles } = useArticlesStore()
|
||||||
const { topTopics } = useTopicsStore()
|
const { topTopics } = useTopics()
|
||||||
const { topAuthors } = useTopAuthorsStore()
|
const { topAuthors } = useTopAuthorsStore()
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
const [topComments, setTopComments] = createSignal<Reaction[]>([])
|
const [topComments, setTopComments] = createSignal<Reaction[]>([])
|
||||||
|
@ -258,10 +258,10 @@ export const FeedView = (props: Props) => {
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class={clsx({
|
class={clsx({
|
||||||
'view-switcher__item--selected': searchParams().by === 'comments',
|
'view-switcher__item--selected': searchParams().by === 'last_comment',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span class="link" onClick={() => changeSearchParams({ by: 'comments' })}>
|
<span class="link" onClick={() => changeSearchParams({ by: 'last_comment' })}>
|
||||||
{t('Most commented')}
|
{t('Most commented')}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { getPagePath } from '@nanostores/router'
|
||||||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js'
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
import { useTopics } from '../../context/topics'
|
||||||
import { Shout, Topic } from '../../graphql/schema/core.gen'
|
import { Shout, Topic } from '../../graphql/schema/core.gen'
|
||||||
import { router } from '../../stores/router'
|
import { router } from '../../stores/router'
|
||||||
import {
|
import {
|
||||||
|
@ -11,7 +12,6 @@ import {
|
||||||
useArticlesStore,
|
useArticlesStore,
|
||||||
} from '../../stores/zine/articles'
|
} from '../../stores/zine/articles'
|
||||||
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
|
||||||
import { capitalize } from '../../utils/capitalize'
|
import { capitalize } from '../../utils/capitalize'
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||||
import { splitToPages } from '../../utils/splitToPages'
|
import { splitToPages } from '../../utils/splitToPages'
|
||||||
|
@ -46,7 +46,7 @@ export const HomeView = (props: Props) => {
|
||||||
shouts: props.shouts,
|
shouts: props.shouts,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { topTopics } = useTopicsStore()
|
const { topTopics } = useTopics()
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
const { topAuthors } = useTopAuthorsStore()
|
const { topAuthors } = useTopAuthorsStore()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
@ -110,7 +110,7 @@ export const HomeView = (props: Props) => {
|
||||||
nodate={true}
|
nodate={true}
|
||||||
/>
|
/>
|
||||||
<Show when={topMonthArticles()}>
|
<Show when={topMonthArticles()}>
|
||||||
<ArticleCardSwiper title={t('Top month articles')} slides={topMonthArticles()} />
|
<ArticleCardSwiper title={t('Top month')} slides={topMonthArticles()} />
|
||||||
</Show>
|
</Show>
|
||||||
<Row2 articles={sortedArticles().slice(10, 12)} nodate={true} />
|
<Row2 articles={sortedArticles().slice(10, 12)} nodate={true} />
|
||||||
<RowShort articles={sortedArticles().slice(12, 16)} />
|
<RowShort articles={sortedArticles().slice(12, 16)} />
|
||||||
|
|
|
@ -6,11 +6,11 @@ import { createStore } from 'solid-js/store'
|
||||||
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
import { useTopics } from '../../../context/topics'
|
||||||
import { Topic } from '../../../graphql/schema/core.gen'
|
import { Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { UploadedFile } from '../../../pages/types'
|
import { UploadedFile } from '../../../pages/types'
|
||||||
import { router } from '../../../stores/router'
|
import { router } from '../../../stores/router'
|
||||||
import { hideModal, showModal } from '../../../stores/ui'
|
import { hideModal, showModal } from '../../../stores/ui'
|
||||||
import { loadAllTopics, useTopicsStore } from '../../../stores/zine/topics'
|
|
||||||
import { TopicSelect, UploadModalContent } from '../../Editor'
|
import { TopicSelect, UploadModalContent } from '../../Editor'
|
||||||
import { Modal } from '../../Nav/Modal'
|
import { Modal } from '../../Nav/Modal'
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
|
@ -53,13 +53,13 @@ const emptyConfig = {
|
||||||
export const PublishSettings = (props: Props) => {
|
export const PublishSettings = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { author } = useSession()
|
const { author } = useSession()
|
||||||
const { sortedTopics } = useTopicsStore()
|
const { sortedTopics } = useTopics()
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
const [topics, setTopics] = createSignal<Topic[]>(sortedTopics())
|
const [topics, setTopics] = createSignal<Topic[]>(sortedTopics())
|
||||||
|
|
||||||
const composeDescription = () => {
|
const composeDescription = () => {
|
||||||
if (!props.form.description) {
|
if (!props.form.description) {
|
||||||
const cleanFootnotes = props.form.body.replaceAll(/<footnote data-value=".*?">.*?<\/footnote>/g, '')
|
const cleanFootnotes = props.form.body.replaceAll(/<footnote data-value=".*?">(.*?)<\/footnote>/g, '')
|
||||||
const leadText = cleanFootnotes.replaceAll(/<\/?[^>]+(>|$)/gi, ' ')
|
const leadText = cleanFootnotes.replaceAll(/<\/?[^>]+(>|$)/gi, ' ')
|
||||||
return shorten(leadText, DESCRIPTION_MAX_LENGTH).trim()
|
return shorten(leadText, DESCRIPTION_MAX_LENGTH).trim()
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,6 @@ export const PublishSettings = (props: Props) => {
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setSettingsForm(initialData())
|
setSettingsForm(initialData())
|
||||||
loadAllTopics()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => setTopics(sortedTopics()))
|
createEffect(() => setTopics(sortedTopics()))
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { LoadShoutsOptions, Shout, Topic } from '../../graphql/schema/core.gen'
|
import { LoadShoutsOptions, Shout, Topic } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { Meta } from '@solidjs/meta'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||||
|
import { Meta } from '../../context/meta'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
import { useTopics } from '../../context/topics'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
|
import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
|
||||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
|
||||||
import { capitalize } from '../../utils/capitalize'
|
import { capitalize } from '../../utils/capitalize'
|
||||||
import { getImageUrl } from '../../utils/getImageUrl'
|
import { getImageUrl } from '../../utils/getImageUrl'
|
||||||
import { getDescription } from '../../utils/meta'
|
import { getDescription } from '../../utils/meta'
|
||||||
|
@ -43,7 +43,7 @@ export const TopicView = (props: Props) => {
|
||||||
const { searchParams, changeSearchParams } = useRouter<TopicsPageSearchParams>()
|
const { searchParams, changeSearchParams } = useRouter<TopicsPageSearchParams>()
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||||
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
const { topicEntities } = useTopics()
|
||||||
const { authorsByTopic } = useAuthorsStore()
|
const { authorsByTopic } = useAuthorsStore()
|
||||||
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
|
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
|
||||||
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
||||||
|
@ -216,7 +216,7 @@ export const TopicView = (props: Props) => {
|
||||||
wrapper={'author'}
|
wrapper={'author'}
|
||||||
/>
|
/>
|
||||||
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
|
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
|
||||||
<ArticleCardSwiper title={t('Top month articles')} slides={reactedTopMonthArticles()} />
|
<ArticleCardSwiper title={t('Top month')} slides={reactedTopMonthArticles()} />
|
||||||
</Show>
|
</Show>
|
||||||
<Beside
|
<Beside
|
||||||
beside={sortedArticles()[12]}
|
beside={sortedArticles()[12]}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { clsx } from 'clsx'
|
||||||
import { JSX, Show, createSignal } from 'solid-js'
|
import { JSX, Show, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { useSession } from '../../../context/session'
|
||||||
import { UploadedFile } from '../../../pages/types'
|
import { UploadedFile } from '../../../pages/types'
|
||||||
import { handleFileUpload } from '../../../utils/handleFileUpload'
|
import { handleFileUpload } from '../../../utils/handleFileUpload'
|
||||||
import { handleImageUpload } from '../../../utils/handleImageUpload'
|
import { handleImageUpload } from '../../../utils/handleImageUpload'
|
||||||
|
@ -27,6 +28,7 @@ export const DropArea = (props: Props) => {
|
||||||
const [dragActive, setDragActive] = createSignal(false)
|
const [dragActive, setDragActive] = createSignal(false)
|
||||||
const [dropAreaError, setDropAreaError] = createSignal<string>()
|
const [dropAreaError, setDropAreaError] = createSignal<string>()
|
||||||
const [loading, setLoading] = createSignal(false)
|
const [loading, setLoading] = createSignal(false)
|
||||||
|
const { session } = useSession()
|
||||||
|
|
||||||
const runUpload = async (files) => {
|
const runUpload = async (files) => {
|
||||||
try {
|
try {
|
||||||
|
@ -35,7 +37,7 @@ export const DropArea = (props: Props) => {
|
||||||
const results: UploadedFile[] = []
|
const results: UploadedFile[] = []
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const handler = props.fileType === 'image' ? handleImageUpload : handleFileUpload
|
const handler = props.fileType === 'image' ? handleImageUpload : handleFileUpload
|
||||||
const result = await handler(file)
|
const result = await handler(file, session()?.access_token)
|
||||||
results.push(result)
|
results.push(result)
|
||||||
}
|
}
|
||||||
props.onUpload(results)
|
props.onUpload(results)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { For, Show, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { Popup } from '../Popup'
|
import { Popup } from '../Popup'
|
||||||
|
|
||||||
|
import popupStyles from '../Popup/Popup.module.scss'
|
||||||
import styles from './DropDown.module.scss'
|
import styles from './DropDown.module.scss'
|
||||||
|
|
||||||
export type Option = {
|
export type Option = {
|
||||||
|
@ -56,16 +57,22 @@ export const DropDown = <TOption extends Option = Option>(props: Props<TOption>)
|
||||||
onVisibilityChange={(isVisible) => setIsPopupVisible(isVisible)}
|
onVisibilityChange={(isVisible) => setIsPopupVisible(isVisible)}
|
||||||
{...props.popupProps}
|
{...props.popupProps}
|
||||||
>
|
>
|
||||||
|
<ul class="nodash">
|
||||||
<For each={props.options}>
|
<For each={props.options}>
|
||||||
{(option) => (
|
{(option) => (
|
||||||
<div
|
<li>
|
||||||
class={clsx('link', { [styles.active]: props.currentOption.value === option.value })}
|
<button
|
||||||
|
class={clsx(popupStyles.action, {
|
||||||
|
[styles.active]: props.currentOption.value === option.value,
|
||||||
|
})}
|
||||||
onClick={() => props.onChange(option)}
|
onClick={() => props.onChange(option)}
|
||||||
>
|
>
|
||||||
{option.title}
|
{option.title}
|
||||||
</div>
|
</button>
|
||||||
|
</li>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
</ul>
|
||||||
</Popup>
|
</Popup>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
|
|
||||||
import { Link } from '@solidjs/meta'
|
|
||||||
import { splitProps } from 'solid-js'
|
import { splitProps } from 'solid-js'
|
||||||
|
import { Link } from '../../../context/meta'
|
||||||
|
|
||||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
|
|
||||||
import { Title } from '@solidjs/meta'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createEffect, createSignal } from 'solid-js'
|
import { Show, createEffect, createSignal } from 'solid-js'
|
||||||
|
import { Title } from '../../context/meta'
|
||||||
|
|
||||||
import { Footer } from '../Discours/Footer'
|
import { Footer } from '../Discours/Footer'
|
||||||
import { Header } from '../Nav/Header'
|
import { Header } from '../Nav/Header'
|
||||||
|
|
|
@ -8,10 +8,14 @@
|
||||||
|
|
||||||
.popup {
|
.popup {
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
|
border: 1px solid rgb(0 0 0 / 15%);
|
||||||
|
border-radius: 1.6rem;
|
||||||
|
box-shadow: 0 8px 16px 0 rgb(0 0 0 / 5%);
|
||||||
color: var(--default-color);
|
color: var(--default-color);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
min-width: 144px;
|
min-width: 144px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
overflow: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
top: calc(100% + 11px);
|
top: calc(100% + 11px);
|
||||||
|
@ -21,6 +25,7 @@
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
margin: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
|
@ -29,33 +34,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bordered {
|
|
||||||
@include font-size(1.6rem);
|
|
||||||
|
|
||||||
border: 2px solid #000;
|
|
||||||
padding: 2.4rem;
|
|
||||||
|
|
||||||
ul li {
|
|
||||||
margin-bottom: 1.6rem;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.tiny {
|
&.tiny {
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
box-shadow: 0 4px 60px rgb(0 0 0 / 10%);
|
.action {
|
||||||
padding: 1rem;
|
padding: 0.5rem 1rem;
|
||||||
|
|
||||||
ul li {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li:first-child .action {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:last-child .action {
|
||||||
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +66,6 @@
|
||||||
|
|
||||||
.topBorderItem {
|
.topBorderItem {
|
||||||
border-top: 2px solid;
|
border-top: 2px solid;
|
||||||
padding-top: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:link,
|
a:link,
|
||||||
|
@ -83,26 +73,64 @@
|
||||||
border: none;
|
border: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&::before {
|
||||||
.icon img {
|
content: '';
|
||||||
filter: invert(0);
|
height: 100%;
|
||||||
}
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 3.6rem;
|
margin-right: 1rem;
|
||||||
|
width: 2.4rem;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
filter: invert(1);
|
max-height: 2.4rem;
|
||||||
max-height: 2rem;
|
max-width: 2.4rem;
|
||||||
max-width: 2rem;
|
|
||||||
transition: filter 0.3s;
|
transition: filter 0.3s;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--black-500);
|
||||||
|
color: var(--black-50) !important;
|
||||||
|
|
||||||
|
.icon img {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li:first-child .action {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:last-child .action {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilePopup {
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
min-width: 22rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: animation
|
// TODO: animation
|
||||||
|
|
|
@ -14,7 +14,7 @@ export type PopupProps = {
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
onVisibilityChange?: (isVisible: boolean) => void
|
onVisibilityChange?: (isVisible: boolean) => void
|
||||||
horizontalAnchor?: HorizontalAnchor
|
horizontalAnchor?: HorizontalAnchor
|
||||||
variant?: 'bordered' | 'tiny'
|
variant?: 'tiny'
|
||||||
closePopup?: boolean
|
closePopup?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,6 @@ export const Popup = (props: PopupProps) => {
|
||||||
class={clsx(styles.popup, props.popupCssClass, {
|
class={clsx(styles.popup, props.popupCssClass, {
|
||||||
[styles.horizontalAnchorCenter]: horizontalAnchor === 'center',
|
[styles.horizontalAnchorCenter]: horizontalAnchor === 'center',
|
||||||
[styles.horizontalAnchorRight]: horizontalAnchor === 'right',
|
[styles.horizontalAnchorRight]: horizontalAnchor === 'right',
|
||||||
[styles.bordered]: props.variant === 'bordered',
|
|
||||||
[styles.tiny]: props.variant === 'tiny',
|
[styles.tiny]: props.variant === 'tiny',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
@ -7,9 +7,6 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #000;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
.icon img {
|
.icon img {
|
||||||
filter: invert(0);
|
filter: invert(0);
|
||||||
}
|
}
|
||||||
|
@ -17,7 +14,7 @@
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 3.6rem;
|
width: 2rem;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useSnackbar } from '../../../context/snackbar'
|
||||||
import { Icon } from '../Icon'
|
import { Icon } from '../Icon'
|
||||||
import { Popover } from '../Popover'
|
import { Popover } from '../Popover'
|
||||||
|
|
||||||
|
import popupStyles from '../Popup/Popup.module.scss'
|
||||||
import styles from './ShareLinks.module.scss'
|
import styles from './ShareLinks.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -53,26 +54,42 @@ export const ShareLinks = (props: Props) => {
|
||||||
<div class={clsx(styles.ShareLinks, props.class, { [styles.inModal]: props.variant === 'inModal' })}>
|
<div class={clsx(styles.ShareLinks, props.class, { [styles.inModal]: props.variant === 'inModal' })}>
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<li>
|
||||||
<button role="button" class={styles.shareControl} onClick={() => handleShare(FACEBOOK)}>
|
<button
|
||||||
<Icon name="facebook-white" class={styles.icon} />
|
role="button"
|
||||||
|
class={clsx(styles.shareControl, popupStyles.action)}
|
||||||
|
onClick={() => handleShare(FACEBOOK)}
|
||||||
|
>
|
||||||
|
<Icon name="facebook-white" class={clsx(styles.icon, popupStyles.icon)} />
|
||||||
Facebook
|
Facebook
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button role="button" class={styles.shareControl} onClick={() => handleShare(TWITTER)}>
|
<button
|
||||||
<Icon name="twitter-white" class={styles.icon} />
|
role="button"
|
||||||
|
class={clsx(styles.shareControl, popupStyles.action)}
|
||||||
|
onClick={() => handleShare(TWITTER)}
|
||||||
|
>
|
||||||
|
<Icon name="twitter-white" class={clsx(styles.icon, popupStyles.icon)} />
|
||||||
Twitter
|
Twitter
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button role="button" class={styles.shareControl} onClick={() => handleShare(TELEGRAM)}>
|
<button
|
||||||
<Icon name="telegram-white" class={styles.icon} />
|
role="button"
|
||||||
|
class={clsx(styles.shareControl, popupStyles.action)}
|
||||||
|
onClick={() => handleShare(TELEGRAM)}
|
||||||
|
>
|
||||||
|
<Icon name="telegram-white" class={clsx(styles.icon, popupStyles.icon)} />
|
||||||
Telegram
|
Telegram
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button role="button" class={styles.shareControl} onClick={() => handleShare(VK)}>
|
<button
|
||||||
<Icon name="vk-white" class={styles.icon} />
|
role="button"
|
||||||
|
class={clsx(styles.shareControl, popupStyles.action)}
|
||||||
|
onClick={() => handleShare(VK)}
|
||||||
|
>
|
||||||
|
<Icon name="vk-white" class={clsx(styles.icon, popupStyles.icon)} />
|
||||||
VK
|
VK
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -80,8 +97,12 @@ export const ShareLinks = (props: Props) => {
|
||||||
<Show
|
<Show
|
||||||
when={props.variant === 'inModal'}
|
when={props.variant === 'inModal'}
|
||||||
fallback={
|
fallback={
|
||||||
<button role="button" class={styles.shareControl} onClick={copyLink}>
|
<button
|
||||||
<Icon name="link-white" class={styles.icon} />
|
role="button"
|
||||||
|
class={clsx(styles.shareControl, popupStyles.action)}
|
||||||
|
onClick={copyLink}
|
||||||
|
>
|
||||||
|
<Icon name="link-white" class={clsx(styles.icon, popupStyles.icon)} />
|
||||||
{t('Copy link')}
|
{t('Copy link')}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@ -93,7 +114,7 @@ export const ShareLinks = (props: Props) => {
|
||||||
<Popover content={t('Copy link')}>
|
<Popover content={t('Copy link')}>
|
||||||
{(triggerRef: (el) => void) => (
|
{(triggerRef: (el) => void) => (
|
||||||
<div class={styles.copyButton} onClick={copyLink} ref={triggerRef}>
|
<div class={styles.copyButton} onClick={copyLink} ref={triggerRef}>
|
||||||
<Icon name="copy" class={styles.icon} />
|
<Icon name="copy" class={clsx(styles.icon, popupStyles.icon)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { ShowOnlyOnClient } from '../ShowOnlyOnClient'
|
||||||
|
|
||||||
import { SwiperRef } from './swiper'
|
import { SwiperRef } from './swiper'
|
||||||
|
|
||||||
|
import { Row1 } from '../../Feed/Row1'
|
||||||
|
import { Row2 } from '../../Feed/Row2'
|
||||||
import styles from './Swiper.module.scss'
|
import styles from './Swiper.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -21,19 +23,29 @@ export const ArticleCardSwiper = (props: Props) => {
|
||||||
const mainSwipeRef: { current: SwiperRef } = { current: null }
|
const mainSwipeRef: { current: SwiperRef } = { current: null }
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
if (props.slides.length > 1) {
|
||||||
const { register } = await import('swiper/element/bundle')
|
const { register } = await import('swiper/element/bundle')
|
||||||
register()
|
register()
|
||||||
SwiperCore.use([Pagination, Navigation, Manipulation])
|
SwiperCore.use([Pagination, Navigation, Manipulation])
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowOnlyOnClient>
|
<ShowOnlyOnClient>
|
||||||
<div class={clsx(styles.Swiper, styles.articleMode, styles.ArticleCardSwiper)}>
|
<div
|
||||||
|
class={clsx({
|
||||||
|
[styles.Swiper]: props.slides.length > 1,
|
||||||
|
[styles.articleMode]: true,
|
||||||
|
[styles.ArticleCardSwiper]: props.slides.length > 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Show when={props.title}>
|
<Show when={props.title}>
|
||||||
<h2 class={styles.sliderTitle}>{props.title}</h2>
|
<h2 class={styles.sliderTitle}>{props.title}</h2>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={styles.container}>
|
<div class={styles.container}>
|
||||||
<Show when={props.slides.length > 0}>
|
<Show when={props.slides.length > 0}>
|
||||||
|
<Show when={props.slides.length !== 1} fallback={<Row1 article={props.slides[0]} />}>
|
||||||
|
<Show when={props.slides.length !== 2} fallback={<Row2 articles={props.slides} />}>
|
||||||
<div class={styles.holder}>
|
<div class={styles.holder}>
|
||||||
<swiper-container
|
<swiper-container
|
||||||
ref={(el) => (mainSwipeRef.current = el)}
|
ref={(el) => (mainSwipeRef.current = el)}
|
||||||
|
@ -86,6 +98,8 @@ export const ArticleCardSwiper = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ShowOnlyOnClient>
|
</ShowOnlyOnClient>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { Popover } from '../Popover'
|
||||||
|
|
||||||
import { SwiperRef } from './swiper'
|
import { SwiperRef } from './swiper'
|
||||||
|
|
||||||
|
import { useSession } from '../../../context/session'
|
||||||
import styles from './Swiper.module.scss'
|
import styles from './Swiper.module.scss'
|
||||||
|
|
||||||
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
|
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
|
||||||
|
@ -36,7 +37,7 @@ export const EditorSwiper = (props: Props) => {
|
||||||
const [loading, setLoading] = createSignal(false)
|
const [loading, setLoading] = createSignal(false)
|
||||||
const [slideIndex, setSlideIndex] = createSignal(0)
|
const [slideIndex, setSlideIndex] = createSignal(0)
|
||||||
const [slideBody, setSlideBody] = createSignal<string>()
|
const [slideBody, setSlideBody] = createSignal<string>()
|
||||||
|
const { session } = useSession()
|
||||||
const mainSwipeRef: { current: SwiperRef } = { current: null }
|
const mainSwipeRef: { current: SwiperRef } = { current: null }
|
||||||
const thumbSwipeRef: { current: SwiperRef } = { current: null }
|
const thumbSwipeRef: { current: SwiperRef } = { current: null }
|
||||||
|
|
||||||
|
@ -100,7 +101,7 @@ export const EditorSwiper = (props: Props) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const results: UploadedFile[] = []
|
const results: UploadedFile[] = []
|
||||||
for (const file of selectedFiles) {
|
for (const file of selectedFiles) {
|
||||||
const result = await handleImageUpload(file)
|
const result = await handleImageUpload(file, session()?.access_token)
|
||||||
results.push(result)
|
results.push(result)
|
||||||
}
|
}
|
||||||
props.onImagesAdd(composeMediaItems(results))
|
props.onImagesAdd(composeMediaItems(results))
|
||||||
|
|
275
src/context/meta.tsx
Normal file
275
src/context/meta.tsx
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
JSX,
|
||||||
|
ParentComponent,
|
||||||
|
createContext,
|
||||||
|
createRenderEffect,
|
||||||
|
createUniqueId,
|
||||||
|
onCleanup,
|
||||||
|
sharedConfig,
|
||||||
|
useContext,
|
||||||
|
} from 'solid-js'
|
||||||
|
import { escape as escapeMeta, isServer, spread, ssr, useAssets } from 'solid-js/web'
|
||||||
|
|
||||||
|
export const MetaContext = createContext<MetaContextType>()
|
||||||
|
|
||||||
|
interface TagDescription {
|
||||||
|
tag: string
|
||||||
|
props: Record<string, unknown>
|
||||||
|
setting?: { close?: boolean; escape?: boolean }
|
||||||
|
id: string
|
||||||
|
name?: string
|
||||||
|
ref?: Element
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetaContextType {
|
||||||
|
addTag: (tag: TagDescription) => number
|
||||||
|
removeTag: (tag: TagDescription, index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const cascadingTags = ['title', 'meta']
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/semantics.html#the-title-element
|
||||||
|
const titleTagProperties: string[] = []
|
||||||
|
|
||||||
|
const metaTagProperties: string[] =
|
||||||
|
// https://html.spec.whatwg.org/multipage/semantics.html#the-meta-element
|
||||||
|
['name', 'http-equiv', 'content', 'charset', 'media']
|
||||||
|
// additional properties
|
||||||
|
.concat(['property'])
|
||||||
|
|
||||||
|
const getTagKey = (tag: TagDescription, properties: string[]) => {
|
||||||
|
// pick allowed properties and sort them
|
||||||
|
const tagProps = Object.fromEntries(
|
||||||
|
Object.entries(tag.props)
|
||||||
|
.filter(([k]) => properties.includes(k))
|
||||||
|
.sort(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// treat `property` as `name` for meta tags
|
||||||
|
if (Object.hasOwn(tagProps, 'name') || Object.hasOwn(tagProps, 'property')) {
|
||||||
|
tagProps.name = tagProps.name || tagProps.property
|
||||||
|
tagProps.property = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// concat tag name and properties as unique key for this tag
|
||||||
|
return tag.tag + JSON.stringify(tagProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
function initClientProvider() {
|
||||||
|
if (!sharedConfig.context) {
|
||||||
|
const ssrTags = document.head.querySelectorAll('[data-sm]')
|
||||||
|
// `forEach` on `NodeList` is not supported in Googlebot, so use a workaround
|
||||||
|
Array.prototype.forEach.call(ssrTags, (ssrTag: Node) => ssrTag.parentNode?.removeChild(ssrTag))
|
||||||
|
}
|
||||||
|
|
||||||
|
const cascadedTagInstances = new Map()
|
||||||
|
// TODO: use one element for all tags of the same type, just swap out
|
||||||
|
// where the props get applied
|
||||||
|
function getElement(tag: TagDescription) {
|
||||||
|
if (tag.ref) {
|
||||||
|
return tag.ref
|
||||||
|
}
|
||||||
|
let el = document.querySelector(`[data-sm="${tag.id}"]`)
|
||||||
|
if (el) {
|
||||||
|
if (el.tagName.toLowerCase() !== tag.tag) {
|
||||||
|
if (el.parentNode) {
|
||||||
|
// remove the old tag
|
||||||
|
el.parentNode.removeChild(el)
|
||||||
|
}
|
||||||
|
// add the new tag
|
||||||
|
el = document.createElement(tag.tag)
|
||||||
|
}
|
||||||
|
// use the old tag
|
||||||
|
el.removeAttribute('data-sm')
|
||||||
|
} else {
|
||||||
|
// create a new tag
|
||||||
|
el = document.createElement(tag.tag)
|
||||||
|
}
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addTag(tag: TagDescription) {
|
||||||
|
if (cascadingTags.indexOf(tag.tag) !== -1) {
|
||||||
|
const properties = tag.tag === 'title' ? titleTagProperties : metaTagProperties
|
||||||
|
const tagKey = getTagKey(tag, properties)
|
||||||
|
|
||||||
|
// only cascading tags need to be kept as singletons
|
||||||
|
if (!cascadedTagInstances.has(tagKey)) {
|
||||||
|
cascadedTagInstances.set(tagKey, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
let instances = cascadedTagInstances.get(tagKey)
|
||||||
|
const index = instances.length
|
||||||
|
|
||||||
|
instances = [...instances, tag]
|
||||||
|
|
||||||
|
// track indices synchronously
|
||||||
|
cascadedTagInstances.set(tagKey, instances)
|
||||||
|
|
||||||
|
const element = getElement(tag)
|
||||||
|
tag.ref = element
|
||||||
|
|
||||||
|
spread(element, tag.props)
|
||||||
|
|
||||||
|
let lastVisited = null
|
||||||
|
for (let i = index - 1; i >= 0; i--) {
|
||||||
|
if (instances[i] != null) {
|
||||||
|
lastVisited = instances[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.parentNode !== document.head) {
|
||||||
|
document.head.appendChild(element)
|
||||||
|
}
|
||||||
|
if (lastVisited?.ref?.parentNode) {
|
||||||
|
document.head?.removeChild(lastVisited.ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = getElement(tag)
|
||||||
|
tag.ref = element
|
||||||
|
|
||||||
|
spread(element, tag.props)
|
||||||
|
|
||||||
|
if (element.parentNode !== document.head) {
|
||||||
|
document.head.appendChild(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
},
|
||||||
|
removeTag(tag: TagDescription, index: number) {
|
||||||
|
const properties = tag.tag === 'title' ? titleTagProperties : metaTagProperties
|
||||||
|
const tagKey = getTagKey(tag, properties)
|
||||||
|
|
||||||
|
if (tag.ref) {
|
||||||
|
const t = cascadedTagInstances.get(tagKey)
|
||||||
|
if (t) {
|
||||||
|
if (tag.ref.parentNode) {
|
||||||
|
tag.ref.parentNode.removeChild(tag.ref)
|
||||||
|
for (let i = index - 1; i >= 0; i--) {
|
||||||
|
if (t[i] != null) {
|
||||||
|
document.head.appendChild(t[i].ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t[index] = null
|
||||||
|
cascadedTagInstances.set(tagKey, t)
|
||||||
|
} else if (tag.ref.parentNode) {
|
||||||
|
tag.ref.parentNode.removeChild(tag.ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initServerProvider() {
|
||||||
|
const tags: TagDescription[] = []
|
||||||
|
useAssets(() => {
|
||||||
|
const rendered = renderTags(tags)
|
||||||
|
return ssr(rendered as string) as unknown as Element
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
addTag(tagDesc: TagDescription) {
|
||||||
|
// tweak only cascading tags
|
||||||
|
if (cascadingTags.indexOf(tagDesc.tag) !== -1) {
|
||||||
|
const properties = tagDesc.tag === 'title' ? titleTagProperties : metaTagProperties
|
||||||
|
const tagDescKey = getTagKey(tagDesc, properties)
|
||||||
|
const index = tags.findIndex(
|
||||||
|
(prev) => prev.tag === tagDesc.tag && getTagKey(prev, properties) === tagDescKey,
|
||||||
|
)
|
||||||
|
if (index !== -1) {
|
||||||
|
tags.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags.push(tagDesc)
|
||||||
|
return tags.length
|
||||||
|
},
|
||||||
|
// biome-ignore lint/suspicious/noEmptyBlockStatements: initial value
|
||||||
|
removeTag(_tag: TagDescription, _index: number) {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MetaProvider: ParentComponent = (props) => {
|
||||||
|
const actions = isServer ? initServerProvider() : initClientProvider()
|
||||||
|
return <MetaContext.Provider value={actions as MetaContextType}>{props.children}</MetaContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetaTag = (
|
||||||
|
tag: string,
|
||||||
|
props: { [k: string]: string },
|
||||||
|
setting?: { escape?: boolean; close?: boolean },
|
||||||
|
) => {
|
||||||
|
useHead({
|
||||||
|
tag,
|
||||||
|
props,
|
||||||
|
setting,
|
||||||
|
id: createUniqueId(),
|
||||||
|
get name() {
|
||||||
|
return props.name || props.property
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHead(tagDesc: TagDescription) {
|
||||||
|
const c = useContext(MetaContext)
|
||||||
|
if (!c) throw new Error('<MetaProvider /> should be in the tree')
|
||||||
|
|
||||||
|
createRenderEffect(() => {
|
||||||
|
const index = c?.addTag(tagDesc)
|
||||||
|
onCleanup(() => c?.removeTag(tagDesc, index))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTags(tags: TagDescription[]) {
|
||||||
|
return tags
|
||||||
|
.map((tag) => {
|
||||||
|
const keys = Object.keys(tag.props)
|
||||||
|
const props = keys
|
||||||
|
.map((k) =>
|
||||||
|
k === 'children'
|
||||||
|
? ''
|
||||||
|
: ` ${k}="${
|
||||||
|
// @ts-expect-error
|
||||||
|
escapeMeta(tag.props[k], true)
|
||||||
|
}"`,
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
const children = tag.props.children
|
||||||
|
if (tag.setting?.close) {
|
||||||
|
return `<${tag.tag} data-sm="${tag.id}"${props}>${
|
||||||
|
// @ts-expect-error
|
||||||
|
tag.setting?.escape ? escapeMeta(children) : children || ''
|
||||||
|
}</${tag.tag}>`
|
||||||
|
}
|
||||||
|
return `<${tag.tag} data-sm="${tag.id}"${props}/>`
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Title: Component<JSX.HTMLAttributes<HTMLTitleElement>> = (props) =>
|
||||||
|
MetaTag('title', props as { [k: string]: string }, { escape: true, close: true })
|
||||||
|
|
||||||
|
export const Style: Component<JSX.StyleHTMLAttributes<HTMLStyleElement>> = (props) =>
|
||||||
|
MetaTag('style', props as { [k: string]: string }, { close: true })
|
||||||
|
|
||||||
|
export const Meta: Component<JSX.MetaHTMLAttributes<HTMLMetaElement>> = (props) =>
|
||||||
|
MetaTag('meta', props as { [k: string]: string })
|
||||||
|
|
||||||
|
export const Link: Component<JSX.LinkHTMLAttributes<HTMLLinkElement>> = (props) =>
|
||||||
|
MetaTag('link', props as { [k: string]: string })
|
||||||
|
|
||||||
|
export const Base: Component<JSX.BaseHTMLAttributes<HTMLBaseElement>> = (props) =>
|
||||||
|
MetaTag('base', props as { [k: string]: string })
|
||||||
|
|
||||||
|
export const Stylesheet: Component<Omit<JSX.LinkHTMLAttributes<HTMLLinkElement>, 'rel'>> = (props) => (
|
||||||
|
<Link rel="stylesheet" {...props} />
|
||||||
|
)
|
|
@ -40,11 +40,11 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
||||||
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
|
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
|
||||||
const [notificationEntities, setNotificationEntities] = createStore<Record<string, NotificationGroup>>({})
|
const [notificationEntities, setNotificationEntities] = createStore<Record<string, NotificationGroup>>({})
|
||||||
const { isAuthenticated } = useSession()
|
const { author } = useSession()
|
||||||
const { addHandler } = useConnect()
|
const { addHandler } = useConnect()
|
||||||
|
|
||||||
const loadNotificationsGrouped = async (options: { after: number; limit?: number; offset?: number }) => {
|
const loadNotificationsGrouped = async (options: { after: number; limit?: number; offset?: number }) => {
|
||||||
if (isAuthenticated() && notifierClient?.private) {
|
if (author()?.id && notifierClient?.private) {
|
||||||
const notificationsResult = await notifierClient.getNotifications(options)
|
const notificationsResult = await notifierClient.getNotifications(options)
|
||||||
const groups = notificationsResult?.notifications || []
|
const groups = notificationsResult?.notifications || []
|
||||||
const total = notificationsResult?.total || 0
|
const total = notificationsResult?.total || 0
|
||||||
|
@ -74,7 +74,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
addHandler((data: SSEMessage) => {
|
addHandler((data: SSEMessage) => {
|
||||||
if (data.entity === 'reaction' && isAuthenticated()) {
|
if (data.entity === 'reaction' && author()?.id) {
|
||||||
console.info('[context.notifications] event', data)
|
console.info('[context.notifications] event', data)
|
||||||
loadNotificationsGrouped({ after: after(), limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
|
loadNotificationsGrouped({ after: after(), limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
|
||||||
}
|
}
|
||||||
|
@ -91,14 +91,14 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const markSeenAll = async () => {
|
const markSeenAll = async () => {
|
||||||
if (isAuthenticated() && notifierClient.private) {
|
if (author()?.id && notifierClient.private) {
|
||||||
await notifierClient.markSeenAfter({ after: after() })
|
await notifierClient.markSeenAfter({ after: after() })
|
||||||
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
|
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const markSeen = async (notification_id: number) => {
|
const markSeen = async (notification_id: number) => {
|
||||||
if (isAuthenticated() && notifierClient.private) {
|
if (author()?.id && notifierClient.private) {
|
||||||
await notifierClient.markSeen(notification_id)
|
await notifierClient.markSeen(notification_id)
|
||||||
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
|
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
|
||||||
}
|
}
|
||||||
|
|
29
src/context/seen.tsx
Normal file
29
src/context/seen.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { makePersisted } from '@solid-primitives/storage'
|
||||||
|
import { Accessor, JSX, createContext, createSignal, useContext } from 'solid-js'
|
||||||
|
|
||||||
|
type SeenContextType = {
|
||||||
|
seen: Accessor<{ [slug: string]: number }>
|
||||||
|
addSeen: (slug: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SeenContext = createContext<SeenContextType>()
|
||||||
|
export function useSeen() {
|
||||||
|
return useContext(SeenContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SeenProvider = (props: { children: JSX.Element }) => {
|
||||||
|
const [seen, setSeen] = makePersisted(createSignal<{ [slug: string]: number }>({}), {
|
||||||
|
name: 'discoursio-seen',
|
||||||
|
})
|
||||||
|
|
||||||
|
const addSeen = async (slug: string) => {
|
||||||
|
setSeen((prev) => {
|
||||||
|
const newSeen = { ...prev, [slug]: Date.now() }
|
||||||
|
return newSeen
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: SeenContextType = { seen, addSeen }
|
||||||
|
|
||||||
|
return <SeenContext.Provider value={value}>{props.children}</SeenContext.Provider>
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import {
|
||||||
LoginInput,
|
LoginInput,
|
||||||
ResendVerifyEmailInput,
|
ResendVerifyEmailInput,
|
||||||
SignupInput,
|
SignupInput,
|
||||||
|
UpdateProfileInput,
|
||||||
VerifyEmailInput,
|
VerifyEmailInput,
|
||||||
} from '@authorizerdev/authorizer-js'
|
} from '@authorizerdev/authorizer-js'
|
||||||
import {
|
import {
|
||||||
|
@ -48,7 +49,6 @@ export type SessionContextType = {
|
||||||
author: Resource<Author | null>
|
author: Resource<Author | null>
|
||||||
authError: Accessor<string>
|
authError: Accessor<string>
|
||||||
isSessionLoaded: Accessor<boolean>
|
isSessionLoaded: Accessor<boolean>
|
||||||
isAuthenticated: Accessor<boolean>
|
|
||||||
loadSession: () => AuthToken | Promise<AuthToken>
|
loadSession: () => AuthToken | Promise<AuthToken>
|
||||||
setSession: (token: AuthToken | null) => void // setSession
|
setSession: (token: AuthToken | null) => void // setSession
|
||||||
loadAuthor: (info?: unknown) => Author | Promise<Author>
|
loadAuthor: (info?: unknown) => Author | Promise<Author>
|
||||||
|
@ -59,6 +59,7 @@ export type SessionContextType = {
|
||||||
) => void
|
) => void
|
||||||
signUp: (params: SignupInput) => Promise<{ data: AuthToken; errors: Error[] }>
|
signUp: (params: SignupInput) => Promise<{ data: AuthToken; errors: Error[] }>
|
||||||
signIn: (params: LoginInput) => Promise<{ data: AuthToken; errors: Error[] }>
|
signIn: (params: LoginInput) => Promise<{ data: AuthToken; errors: Error[] }>
|
||||||
|
updateProfile: (params: UpdateProfileInput) => Promise<{ data: AuthToken; errors: Error[] }>
|
||||||
signOut: () => Promise<void>
|
signOut: () => Promise<void>
|
||||||
oauth: (provider: string) => Promise<void>
|
oauth: (provider: string) => Promise<void>
|
||||||
forgotPassword: (
|
forgotPassword: (
|
||||||
|
@ -224,9 +225,12 @@ export const SessionProvider = (props: {
|
||||||
const appdata = session()?.user.app_data
|
const appdata = session()?.user.app_data
|
||||||
if (appdata) {
|
if (appdata) {
|
||||||
const { profile } = appdata
|
const { profile } = appdata
|
||||||
|
if (profile?.id) {
|
||||||
setAuthor(profile)
|
setAuthor(profile)
|
||||||
addAuthors([profile])
|
addAuthors([profile])
|
||||||
if (!profile) loadAuthor()
|
} else {
|
||||||
|
setTimeout(loadAuthor, 15)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
@ -271,12 +275,9 @@ export const SessionProvider = (props: {
|
||||||
|
|
||||||
// callback state updater
|
// callback state updater
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on([() => props.onStateChangeCallback, session], ([_, ses]) => {
|
||||||
() => props.onStateChangeCallback,
|
ses?.user?.id && props.onStateChangeCallback(ses)
|
||||||
() => {
|
}),
|
||||||
props.onStateChangeCallback(session())
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const [authCallback, setAuthCallback] = createSignal<() => void>(noop)
|
const [authCallback, setAuthCallback] = createSignal<() => void>(noop)
|
||||||
|
@ -309,6 +310,8 @@ export const SessionProvider = (props: {
|
||||||
}
|
}
|
||||||
const signUp = async (params: SignupInput) => await authenticate(authorizer().signup, params)
|
const signUp = async (params: SignupInput) => await authenticate(authorizer().signup, params)
|
||||||
const signIn = async (params: LoginInput) => await authenticate(authorizer().login, params)
|
const signIn = async (params: LoginInput) => await authenticate(authorizer().login, params)
|
||||||
|
const updateProfile = async (params: UpdateProfileInput) =>
|
||||||
|
await authenticate(authorizer().updateProfile, params)
|
||||||
|
|
||||||
const signOut = async () => {
|
const signOut = async () => {
|
||||||
const authResult: ApiResponse<GenericResponse> = await authorizer().logout()
|
const authResult: ApiResponse<GenericResponse> = await authorizer().logout()
|
||||||
|
@ -378,9 +381,6 @@ export const SessionProvider = (props: {
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAuthenticated = createMemo(() => Boolean(author()))
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
loadSession,
|
loadSession,
|
||||||
requireAuthentication,
|
requireAuthentication,
|
||||||
|
@ -388,6 +388,7 @@ export const SessionProvider = (props: {
|
||||||
signIn,
|
signIn,
|
||||||
signOut,
|
signOut,
|
||||||
confirmEmail,
|
confirmEmail,
|
||||||
|
updateProfile,
|
||||||
setIsSessionLoaded,
|
setIsSessionLoaded,
|
||||||
setSession,
|
setSession,
|
||||||
setAuthor,
|
setAuthor,
|
||||||
|
@ -405,7 +406,6 @@ export const SessionProvider = (props: {
|
||||||
isSessionLoaded,
|
isSessionLoaded,
|
||||||
author,
|
author,
|
||||||
...actions,
|
...actions,
|
||||||
isAuthenticated,
|
|
||||||
resendVerifyEmail,
|
resendVerifyEmail,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,19 @@
|
||||||
|
import { createLazyMemo } from '@solid-primitives/memo'
|
||||||
import { openDB } from 'idb'
|
import { openDB } from 'idb'
|
||||||
import { Accessor, JSX, createContext, createSignal, onMount, useContext } from 'solid-js'
|
import { Accessor, JSX, createContext, createMemo, createSignal, onMount, useContext } from 'solid-js'
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { Topic } from '../graphql/schema/core.gen'
|
import { Topic } from '../graphql/schema/core.gen'
|
||||||
|
import { useRouter } from '../stores/router'
|
||||||
|
import { byTopicStatDesc } from '../utils/sortby'
|
||||||
|
|
||||||
type TopicsContextType = {
|
type TopicsContextType = {
|
||||||
topics: Accessor<Topic[]>
|
topicEntities: Accessor<{ [topicSlug: string]: Topic }>
|
||||||
|
sortedTopics: Accessor<Topic[]>
|
||||||
|
randomTopics: Accessor<Topic[]>
|
||||||
|
topTopics: Accessor<Topic[]>
|
||||||
|
setTopicsSort: (sortBy: string) => void
|
||||||
|
addTopics: (topics: Topic[]) => void
|
||||||
|
loadTopics: () => Promise<Topic[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
const TopicsContext = createContext<TopicsContextType>()
|
const TopicsContext = createContext<TopicsContextType>()
|
||||||
|
@ -25,11 +34,13 @@ const setupIndexedDB = async () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTopicsFromIndexedDB = (db) => {
|
const getTopicsFromIndexedDB = async (db) => {
|
||||||
const tx = db.transaction(STORE_NAME, 'readonly')
|
const tx = db.transaction(STORE_NAME, 'readonly')
|
||||||
const store = tx.objectStore(STORE_NAME)
|
const store = tx.objectStore(STORE_NAME)
|
||||||
return store.getAll()
|
const topics = await store.getAll()
|
||||||
|
return { topics, timestamp: tx.done }
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveTopicsToIndexedDB = async (db, topics) => {
|
const saveTopicsToIndexedDB = async (db, topics) => {
|
||||||
const tx = db.transaction(STORE_NAME, 'readwrite')
|
const tx = db.transaction(STORE_NAME, 'readwrite')
|
||||||
const store = tx.objectStore(STORE_NAME)
|
const store = tx.objectStore(STORE_NAME)
|
||||||
|
@ -40,20 +51,93 @@ const saveTopicsToIndexedDB = async (db, topics) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopicsProvider = (props: { children: JSX.Element }) => {
|
export const TopicsProvider = (props: { children: JSX.Element }) => {
|
||||||
|
const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({})
|
||||||
|
const [sortAllBy, setSortAllBy] = createSignal<'shouts' | 'followers' | 'authors' | 'title'>('shouts')
|
||||||
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
|
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
|
||||||
|
|
||||||
|
const sortedTopics = createLazyMemo<Topic[]>(() => {
|
||||||
|
const topics = Object.values(topicEntities())
|
||||||
|
const { changeSearchParams } = useRouter()
|
||||||
|
switch (sortAllBy()) {
|
||||||
|
case 'followers': {
|
||||||
|
topics.sort(byTopicStatDesc('followers'))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'shouts': {
|
||||||
|
topics.sort(byTopicStatDesc('shouts'))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'authors': {
|
||||||
|
topics.sort(byTopicStatDesc('authors'))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'title': {
|
||||||
|
topics.sort((a, b) => a.title.localeCompare(b.title))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
topics.sort(byTopicStatDesc('shouts'))
|
||||||
|
changeSearchParams({ by: 'shouts' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return topics
|
||||||
|
})
|
||||||
|
|
||||||
|
const topTopics = createMemo(() => {
|
||||||
|
const topics = Object.values(topicEntities())
|
||||||
|
topics.sort(byTopicStatDesc('shouts'))
|
||||||
|
return topics
|
||||||
|
})
|
||||||
|
|
||||||
|
const addTopics = (...args: Topic[][]) => {
|
||||||
|
const allTopics = args.flatMap((topics) => (topics || []).filter(Boolean))
|
||||||
|
|
||||||
|
const newTopicEntities = allTopics.reduce(
|
||||||
|
(acc, topic) => {
|
||||||
|
acc[topic.slug] = topic
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, Topic>,
|
||||||
|
)
|
||||||
|
|
||||||
|
setTopicEntities((prevTopicEntities) => {
|
||||||
|
return {
|
||||||
|
...prevTopicEntities,
|
||||||
|
...newTopicEntities,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const [db, setDb] = createSignal()
|
||||||
|
const loadTopics = async () => {
|
||||||
|
const ttt = await apiClient.getAllTopics()
|
||||||
|
await saveTopicsToIndexedDB(db(), ttt)
|
||||||
|
return ttt
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const db = await setupIndexedDB()
|
const db = await setupIndexedDB()
|
||||||
let topics = getTopicsFromIndexedDB(db)
|
setDb(db)
|
||||||
|
let { topics, timestamp } = await getTopicsFromIndexedDB(db)
|
||||||
|
|
||||||
if (topics.length === 0) {
|
if (topics.length < 100 || Date.now() - timestamp > 3600000) {
|
||||||
topics = await apiClient.getAllTopics()
|
const newTopics = await loadTopics()
|
||||||
await saveTopicsToIndexedDB(db, topics)
|
await saveTopicsToIndexedDB(db, newTopics)
|
||||||
|
topics = newTopics
|
||||||
}
|
}
|
||||||
|
addTopics(topics)
|
||||||
setRandomTopics(topics)
|
setRandomTopics(topics)
|
||||||
})
|
})
|
||||||
|
|
||||||
const value: TopicsContextType = { topics: randomTopics }
|
const value: TopicsContextType = {
|
||||||
|
setTopicsSort: setSortAllBy,
|
||||||
|
topicEntities,
|
||||||
|
sortedTopics,
|
||||||
|
randomTopics,
|
||||||
|
topTopics,
|
||||||
|
addTopics,
|
||||||
|
loadTopics,
|
||||||
|
}
|
||||||
|
|
||||||
return <TopicsContext.Provider value={value}>{props.children}</TopicsContext.Provider>
|
return <TopicsContext.Provider value={value}>{props.children}</TopicsContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import type {
|
import type {
|
||||||
Author,
|
Author,
|
||||||
AuthorFollowsResult,
|
|
||||||
CommonResult,
|
CommonResult,
|
||||||
FollowingEntity,
|
FollowingEntity,
|
||||||
LoadShoutsOptions,
|
LoadShoutsOptions,
|
||||||
|
@ -134,7 +133,7 @@ export const apiClient = {
|
||||||
slug?: string
|
slug?: string
|
||||||
author_id?: number
|
author_id?: number
|
||||||
user?: string
|
user?: string
|
||||||
}): Promise<AuthorFollowsResult> => {
|
}): Promise<CommonResult> => {
|
||||||
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
|
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
|
||||||
return response.data.get_author_follows
|
return response.data.get_author_follows
|
||||||
},
|
},
|
||||||
|
@ -175,7 +174,7 @@ export const apiClient = {
|
||||||
console.debug('[graphql.client.core] deleteShout:', response)
|
console.debug('[graphql.client.core] deleteShout:', response)
|
||||||
},
|
},
|
||||||
|
|
||||||
getDrafts: async (): Promise<Shout[]> => {
|
getDrafts: async (): Promise<CommonResult> => {
|
||||||
const response = await apiClient.private.query(draftsLoad, {}).toPromise()
|
const response = await apiClient.private.query(draftsLoad, {}).toPromise()
|
||||||
console.debug('[graphql.client.core] getDrafts:', response)
|
console.debug('[graphql.client.core] getDrafts:', response)
|
||||||
return response.data.get_shouts_drafts
|
return response.data.get_shouts_drafts
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { gql } from '@urql/core'
|
||||||
export default gql`
|
export default gql`
|
||||||
query LoadDraftsQuery {
|
query LoadDraftsQuery {
|
||||||
get_shouts_drafts {
|
get_shouts_drafts {
|
||||||
|
error
|
||||||
|
shouts {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
subtitle
|
subtitle
|
||||||
|
@ -40,4 +42,5 @@ export default gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { gql } from '@urql/core'
|
||||||
export default gql`
|
export default gql`
|
||||||
query GetAuthorFollows($slug: String, $user: String, $author_id: Int) {
|
query GetAuthorFollows($slug: String, $user: String, $author_id: Int) {
|
||||||
get_author_follows(slug: $slug, user: $user, author_id: $author_id) {
|
get_author_follows(slug: $slug, user: $user, author_id: $author_id) {
|
||||||
|
error
|
||||||
authors {
|
authors {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { gql } from '@urql/core'
|
|
||||||
|
|
||||||
export default gql`
|
|
||||||
query MySubscriptionsQuery {
|
|
||||||
get_my_followed {
|
|
||||||
topics {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
body
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
authors {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
pic
|
|
||||||
created_at
|
|
||||||
}
|
|
||||||
communities {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
pic
|
|
||||||
created_at
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '../../context/meta'
|
||||||
|
|
||||||
import { StaticPage } from '../../components/Views/StaticPage'
|
import { StaticPage } from '../../components/Views/StaticPage'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '../../context/meta'
|
||||||
|
|
||||||
import { StaticPage } from '../../components/Views/StaticPage'
|
import { StaticPage } from '../../components/Views/StaticPage'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '../../context/meta'
|
||||||
|
|
||||||
import { StaticPage } from '../../components/Views/StaticPage'
|
import { StaticPage } from '../../components/Views/StaticPage'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '../../context/meta'
|
||||||
|
|
||||||
import { Donate } from '../../components/Discours/Donate'
|
import { Donate } from '../../components/Discours/Donate'
|
||||||
import { StaticPage } from '../../components/Views/StaticPage'
|
import { StaticPage } from '../../components/Views/StaticPage'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '../../context/meta'
|
||||||
|
|
||||||
import { Feedback } from '../../components/Discours/Feedback'
|
import { Feedback } from '../../components/Discours/Feedback'
|
||||||
import { Modal } from '../../components/Nav/Modal'
|
import { Modal } from '../../components/Nav/Modal'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '../../context/meta'
|
||||||
|
|
||||||
import { StaticPage } from '../../components/Views/StaticPage'
|
import { StaticPage } from '../../components/Views/StaticPage'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '../../context/meta'
|
||||||
|
|
||||||
import { StaticPage } from '../../components/Views/StaticPage'
|
import { StaticPage } from '../../components/Views/StaticPage'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '../../context/meta'
|
||||||
|
|
||||||
import { StaticPage } from '../../components/Views/StaticPage'
|
import { StaticPage } from '../../components/Views/StaticPage'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '../../context/meta'
|
||||||
|
|
||||||
import { StaticPage } from '../../components/Views/StaticPage'
|
import { StaticPage } from '../../components/Views/StaticPage'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
|
@ -1,29 +1,15 @@
|
||||||
import type { PageProps } from './types'
|
|
||||||
|
|
||||||
import { createSignal, onMount } from 'solid-js'
|
|
||||||
|
|
||||||
import { AllTopics } from '../components/Views/AllTopics'
|
import { AllTopics } from '../components/Views/AllTopics'
|
||||||
import { PageLayout } from '../components/_shared/PageLayout'
|
import { PageLayout } from '../components/_shared/PageLayout'
|
||||||
import { useLocalize } from '../context/localize'
|
import { useLocalize } from '../context/localize'
|
||||||
import { loadAllTopics } from '../stores/zine/topics'
|
import { useTopics } from '../context/topics'
|
||||||
|
|
||||||
export const AllTopicsPage = (props: PageProps) => {
|
export const AllTopicsPage = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
const { sortedTopics } = useTopics()
|
||||||
const [isLoaded, setIsLoaded] = createSignal<boolean>(Boolean(props.allTopics))
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (isLoaded()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadAllTopics()
|
|
||||||
setIsLoaded(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title={t('Themes and plots')}>
|
<PageLayout title={t('Themes and plots')}>
|
||||||
<AllTopics isLoaded={isLoaded()} topics={props.allTopics} />
|
<AllTopics isLoaded={!!sortedTopics()?.length} topics={sortedTopics()} />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { redirectPage } from '@nanostores/router'
|
import { redirectPage } from '@nanostores/router'
|
||||||
import { Meta } from '@solidjs/meta'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { Meta } from '../context/meta'
|
||||||
|
|
||||||
import { AuthGuard } from '../components/AuthGuard'
|
import { AuthGuard } from '../components/AuthGuard'
|
||||||
import { Button } from '../components/_shared/Button'
|
import { Button } from '../components/_shared/Button'
|
||||||
|
@ -18,7 +18,7 @@ import styles from '../styles/Create.module.scss'
|
||||||
const handleCreate = async (layout: LayoutType) => {
|
const handleCreate = async (layout: LayoutType) => {
|
||||||
const shout = await apiClient.createArticle({ article: { layout: layout } })
|
const shout = await apiClient.createArticle({ article: { layout: layout } })
|
||||||
redirectPage(router, 'edit', {
|
redirectPage(router, 'edit', {
|
||||||
shoutId: shout.id.toString(),
|
shoutId: shout?.id.toString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { Show, Suspense, createMemo, createSignal, lazy, onMount } from 'solid-js'
|
import { Show, Suspense, createEffect, createMemo, createSignal, lazy, on, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { AuthGuard } from '../components/AuthGuard'
|
import { AuthGuard } from '../components/AuthGuard'
|
||||||
import { Loading } from '../components/_shared/Loading'
|
import { Loading } from '../components/_shared/Loading'
|
||||||
import { PageLayout } from '../components/_shared/PageLayout'
|
import { PageLayout } from '../components/_shared/PageLayout'
|
||||||
import { useLocalize } from '../context/localize'
|
import { useLocalize } from '../context/localize'
|
||||||
|
import { useSession } from '../context/session'
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { Shout } from '../graphql/schema/core.gen'
|
import { Shout } from '../graphql/schema/core.gen'
|
||||||
import { router } from '../stores/router'
|
import { router } from '../stores/router'
|
||||||
|
@ -14,67 +15,71 @@ import { LayoutType } from './types'
|
||||||
|
|
||||||
const EditView = lazy(() => import('../components/Views/EditView/EditView'))
|
const EditView = lazy(() => import('../components/Views/EditView/EditView'))
|
||||||
|
|
||||||
export const EditPage = () => {
|
const getContentTypeTitle = (layout: LayoutType) => {
|
||||||
const snackbar = useSnackbar()
|
switch (layout) {
|
||||||
const { t } = useLocalize()
|
case 'audio':
|
||||||
|
return 'Publish Album'
|
||||||
|
case 'image':
|
||||||
|
return 'Create gallery'
|
||||||
|
case 'video':
|
||||||
|
return 'Create video'
|
||||||
|
case 'literature':
|
||||||
|
return 'New literary work'
|
||||||
|
default:
|
||||||
|
return 'Write an article'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [shout, setShout] = createSignal<Shout>(null)
|
export const EditPage = () => {
|
||||||
const loadMyShout = async (shout_id: number) => {
|
const { t } = useLocalize()
|
||||||
if (shout_id) {
|
const { session } = useSession()
|
||||||
const { shout: loadedShout, error } = await apiClient.getMyShout(shout_id)
|
const snackbar = useSnackbar()
|
||||||
console.log(loadedShout)
|
|
||||||
if (error) {
|
const fail = async (error: string) => {
|
||||||
await snackbar?.showSnackbar({ type: 'error', body: t('This content is not published yet') })
|
console.error(error)
|
||||||
|
const errorMessage = error === 'forbidden' ? "You can't edit this post" : error
|
||||||
|
await snackbar?.showSnackbar({ type: 'error', body: t(errorMessage) })
|
||||||
redirectPage(router, 'drafts')
|
redirectPage(router, 'drafts')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [shoutId, setShoutId] = createSignal<number>(0)
|
||||||
|
const [shout, setShout] = createSignal<Shout>()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const shoutId = window.location.pathname.split('/').pop()
|
||||||
|
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
|
||||||
|
console.debug(`editing shout ${shoutIdFromUrl}`)
|
||||||
|
if (shoutIdFromUrl) setShoutId(shoutIdFromUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on([session, shout, shoutId], async ([ses, sh, shid]) => {
|
||||||
|
if (ses?.user && !sh && shid) {
|
||||||
|
const { shout: loadedShout, error } = await apiClient.getMyShout(shid)
|
||||||
|
if (error) {
|
||||||
|
fail(error)
|
||||||
} else {
|
} else {
|
||||||
setShout(loadedShout)
|
setShout(loadedShout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
onMount(async () => {
|
|
||||||
const shout_id = window.location.pathname.split('/').pop()
|
|
||||||
if (shout_id) {
|
|
||||||
try {
|
|
||||||
await loadMyShout(Number.parseInt(shout_id, 10))
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
if (!shout()) {
|
if (!shout()) {
|
||||||
return t('Create post')
|
return t('Create post')
|
||||||
}
|
}
|
||||||
|
return t(getContentTypeTitle(shout()?.layout as LayoutType))
|
||||||
switch (shout().layout as LayoutType) {
|
|
||||||
case 'audio': {
|
|
||||||
return t('Publish Album')
|
|
||||||
}
|
|
||||||
case 'image': {
|
|
||||||
return t('Create gallery')
|
|
||||||
}
|
|
||||||
case 'video': {
|
|
||||||
return t('Create video')
|
|
||||||
}
|
|
||||||
case 'literature': {
|
|
||||||
return t('New literary work')
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return t('Write an article')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title={title()}>
|
<PageLayout title={title()}>
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
<Show when={shout()}>
|
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<EditView shout={shout()} />
|
<Show when={shout()} fallback={<Loading />}>
|
||||||
</Suspense>
|
<EditView shout={shout() as Shout} />
|
||||||
</Show>
|
</Show>
|
||||||
|
</Suspense>
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,13 +2,12 @@ import type { PageProps } from './types'
|
||||||
|
|
||||||
import { Show, createSignal, onCleanup, onMount } from 'solid-js'
|
import { Show, createSignal, onCleanup, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { HomeView, PRERENDERED_ARTICLES_COUNT, RANDOM_TOPICS_COUNT } from '../components/Views/Home'
|
import { HomeView, PRERENDERED_ARTICLES_COUNT } from '../components/Views/Home'
|
||||||
import { Loading } from '../components/_shared/Loading'
|
import { Loading } from '../components/_shared/Loading'
|
||||||
import { PageLayout } from '../components/_shared/PageLayout'
|
import { PageLayout } from '../components/_shared/PageLayout'
|
||||||
import { useLocalize } from '../context/localize'
|
import { useLocalize } from '../context/localize'
|
||||||
import { ReactionsProvider } from '../context/reactions'
|
import { ReactionsProvider } from '../context/reactions'
|
||||||
import { loadShouts, resetSortedArticles } from '../stores/zine/articles'
|
import { loadShouts, resetSortedArticles } from '../stores/zine/articles'
|
||||||
import { loadRandomTopics } from '../stores/zine/topics'
|
|
||||||
|
|
||||||
export const HomePage = (props: PageProps) => {
|
export const HomePage = (props: PageProps) => {
|
||||||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.homeShouts))
|
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.homeShouts))
|
||||||
|
@ -19,10 +18,7 @@ export const HomePage = (props: PageProps) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await loadShouts({ filters: { featured: true }, limit: PRERENDERED_ARTICLES_COUNT })
|
||||||
loadShouts({ filters: { featured: true }, limit: PRERENDERED_ARTICLES_COUNT }),
|
|
||||||
loadRandomTopics({ amount: RANDOM_TOPICS_COUNT }),
|
|
||||||
])
|
|
||||||
|
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
})
|
})
|
||||||
|
|
|
@ -100,17 +100,6 @@ h5 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.passwordToggleControl {
|
|
||||||
position: absolute;
|
|
||||||
right: 1em;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.passwordInput {
|
|
||||||
padding-right: 3em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchContainer {
|
.searchContainer {
|
||||||
margin-top: 2.4rem;
|
margin-top: 2.4rem;
|
||||||
}
|
}
|
||||||
|
@ -331,3 +320,12 @@ div[data-lastpass-infield="true"] {
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.emailValidationError {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
|
@ -6,14 +6,143 @@ import { Icon } from '../../components/_shared/Icon'
|
||||||
import { PageLayout } from '../../components/_shared/PageLayout'
|
import { PageLayout } from '../../components/_shared/PageLayout'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
||||||
|
import { UpdateProfileInput } from '@authorizerdev/authorizer-js'
|
||||||
|
import { Show, createEffect, createSignal, on } from 'solid-js'
|
||||||
|
import { PasswordField } from '../../components/Nav/AuthModal/PasswordField'
|
||||||
|
import { Button } from '../../components/_shared/Button'
|
||||||
|
import { Loading } from '../../components/_shared/Loading'
|
||||||
|
import { useConfirm } from '../../context/confirm'
|
||||||
|
import { useSession } from '../../context/session'
|
||||||
|
import { useSnackbar } from '../../context/snackbar'
|
||||||
|
import { DEFAULT_HEADER_OFFSET } from '../../stores/router'
|
||||||
|
import { validateEmail } from '../../utils/validateEmail'
|
||||||
import styles from './Settings.module.scss'
|
import styles from './Settings.module.scss'
|
||||||
|
|
||||||
|
type FormField = 'oldPassword' | 'newPassword' | 'newPasswordConfirm' | 'email'
|
||||||
export const ProfileSecurityPage = () => {
|
export const ProfileSecurityPage = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
const { updateProfile, session, isSessionLoaded } = useSession()
|
||||||
|
const { showSnackbar } = useSnackbar()
|
||||||
|
const { showConfirm } = useConfirm()
|
||||||
|
|
||||||
|
const [newPasswordError, setNewPasswordError] = createSignal<string | undefined>()
|
||||||
|
const [oldPasswordError, setOldPasswordError] = createSignal<string | undefined>()
|
||||||
|
const [emailError, setEmailError] = createSignal<string | undefined>()
|
||||||
|
const [isSubmitting, setIsSubmitting] = createSignal<boolean>()
|
||||||
|
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
oldPassword: undefined,
|
||||||
|
newPassword: undefined,
|
||||||
|
newPasswordConfirm: undefined,
|
||||||
|
email: undefined,
|
||||||
|
}
|
||||||
|
const [formData, setFormData] = createSignal(initialState)
|
||||||
|
const oldPasswordRef: { current: HTMLDivElement } = { current: null }
|
||||||
|
const newPasswordRepeatRef: { current: HTMLDivElement } = { current: null }
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => session()?.user?.email,
|
||||||
|
() => {
|
||||||
|
setFormData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
['email']: session()?.user?.email,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const handleInputChange = (name: FormField, value: string) => {
|
||||||
|
if (
|
||||||
|
name === 'email' ||
|
||||||
|
(name === 'newPasswordConfirm' && value && value?.length > 0 && !emailError() && !newPasswordError())
|
||||||
|
) {
|
||||||
|
setIsFloatingPanelVisible(true)
|
||||||
|
} else {
|
||||||
|
setIsFloatingPanelVisible(false)
|
||||||
|
}
|
||||||
|
setFormData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
[name]: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = async () => {
|
||||||
|
const isConfirmed = await showConfirm({
|
||||||
|
confirmBody: t('Do you really want to reset all changes?'),
|
||||||
|
confirmButtonVariant: 'primary',
|
||||||
|
declineButtonVariant: 'secondary',
|
||||||
|
})
|
||||||
|
if (isConfirmed) {
|
||||||
|
setEmailError()
|
||||||
|
setFormData({
|
||||||
|
...initialState,
|
||||||
|
['email']: session()?.user?.email,
|
||||||
|
})
|
||||||
|
setIsFloatingPanelVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleChangeEmail = (_value: string) => {
|
||||||
|
if (!validateEmail(formData()['email'])) {
|
||||||
|
setEmailError(t('Invalid email'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleCheckNewPassword = (value: string) => {
|
||||||
|
handleInputChange('newPasswordConfirm', value)
|
||||||
|
if (value !== formData()['newPassword']) {
|
||||||
|
const rect = newPasswordRepeatRef.current.getBoundingClientRect()
|
||||||
|
const topPosition = window.scrollY + rect.top - DEFAULT_HEADER_OFFSET * 2
|
||||||
|
window.scrollTo({
|
||||||
|
top: topPosition,
|
||||||
|
left: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
showSnackbar({ type: 'error', body: t('Incorrect new password confirm') })
|
||||||
|
setNewPasswordError(t('Passwords are not equal'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
const options: UpdateProfileInput = {
|
||||||
|
old_password: formData()['oldPassword'],
|
||||||
|
new_password: formData()['newPassword'] || formData()['oldPassword'],
|
||||||
|
confirm_new_password: formData()['newPassword'] || formData()['oldPassword'],
|
||||||
|
email: formData()['email'],
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { errors } = await updateProfile(options)
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error(errors)
|
||||||
|
if (errors.some((obj) => obj.message === 'incorrect old password')) {
|
||||||
|
setOldPasswordError(t('Incorrect old password'))
|
||||||
|
showSnackbar({ type: 'error', body: t('Incorrect old password') })
|
||||||
|
const rect = oldPasswordRef.current.getBoundingClientRect()
|
||||||
|
const topPosition = window.scrollY + rect.top - DEFAULT_HEADER_OFFSET * 2
|
||||||
|
window.scrollTo({
|
||||||
|
top: topPosition,
|
||||||
|
left: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
setIsFloatingPanelVisible(false)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showSnackbar({ type: 'success', body: t('Profile successfully saved') })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title={t('Profile')}>
|
<PageLayout title={t('Profile')}>
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
|
<Show when={isSessionLoaded()} fallback={<Loading />}>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
|
@ -25,63 +154,86 @@ export const ProfileSecurityPage = () => {
|
||||||
<div class="col-md-19">
|
<div class="col-md-19">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-20 col-lg-18 col-xl-16">
|
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||||
<h1>Вход и безопасность</h1>
|
<h1>{t('Login and security')}</h1>
|
||||||
<p class="description">Настройки аккаунта, почты, пароля и способов входа.</p>
|
<p class="description">
|
||||||
|
{t('Settings for account, email, password and login methods.')}
|
||||||
|
</p>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<h4>Почта</h4>
|
<h4>{t('Email')}</h4>
|
||||||
<div class="pretty-form__item">
|
|
||||||
<input type="text" name="email" id="email" placeholder="Почта" />
|
|
||||||
<label for="email">Почта</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4>Изменить пароль</h4>
|
|
||||||
<h5>Текущий пароль</h5>
|
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="password-current"
|
name="email"
|
||||||
id="password-current"
|
id="email"
|
||||||
class={clsx(styles.passwordInput, 'nolabel')}
|
disabled={isSubmitting()}
|
||||||
|
value={formData()['email'] || ''}
|
||||||
|
placeholder={t('Email')}
|
||||||
|
onFocus={() => setEmailError()}
|
||||||
|
onInput={(event) => handleChangeEmail(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<button type="button" class={styles.passwordToggleControl}>
|
<label for="email">{t('Email')}</label>
|
||||||
<Icon name="password-hide" />
|
<Show when={emailError()}>
|
||||||
</button>
|
<div
|
||||||
|
class={clsx(styles.emailValidationError, {
|
||||||
|
'form-message--error': emailError(),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{emailError()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h5>Новый пароль</h5>
|
<h4>{t('Change password')}</h4>
|
||||||
<div class="pretty-form__item">
|
<h5>{t('Current password')}</h5>
|
||||||
<input
|
|
||||||
type="password"
|
<div ref={(el) => (oldPasswordRef.current = el)}>
|
||||||
name="password-new"
|
<PasswordField
|
||||||
id="password-new"
|
onFocus={() => setOldPasswordError()}
|
||||||
class={clsx(styles.passwordInput, 'nolabel')}
|
setError={oldPasswordError()}
|
||||||
|
onInput={(value) => handleInputChange('oldPassword', value)}
|
||||||
|
value={formData()['oldPassword'] ?? null}
|
||||||
|
disabled={isSubmitting()}
|
||||||
/>
|
/>
|
||||||
<button type="button" class={styles.passwordToggleControl}>
|
|
||||||
<Icon name="password-open" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h5>Подтвердите новый пароль</h5>
|
<h5>{t('New password')}</h5>
|
||||||
<div class="pretty-form__item">
|
<PasswordField
|
||||||
<input
|
onInput={(value) => {
|
||||||
type="password"
|
handleInputChange('newPassword', value)
|
||||||
name="password-new-confirm"
|
handleInputChange('newPasswordConfirm', '')
|
||||||
id="password-new-confirm"
|
}}
|
||||||
class={clsx(styles.passwordInput, 'nolabel')}
|
value={formData()['newPassword'] ?? ''}
|
||||||
|
disabled={isSubmitting()}
|
||||||
|
disableAutocomplete={true}
|
||||||
/>
|
/>
|
||||||
<button type="button" class={styles.passwordToggleControl}>
|
|
||||||
<Icon name="password-open" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4>Социальные сети</h4>
|
<h5>{t('Confirm your new password')}</h5>
|
||||||
|
<div ref={(el) => (newPasswordRepeatRef.current = el)}>
|
||||||
|
<PasswordField
|
||||||
|
noValidate={true}
|
||||||
|
value={
|
||||||
|
formData()['newPasswordConfirm']?.length > 0
|
||||||
|
? formData()['newPasswordConfirm']
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onFocus={() => setNewPasswordError()}
|
||||||
|
setError={newPasswordError()}
|
||||||
|
onInput={(value) => handleCheckNewPassword(value)}
|
||||||
|
disabled={isSubmitting()}
|
||||||
|
disableAutocomplete={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h4>{t('Social networks')}</h4>
|
||||||
<h5>Google</h5>
|
<h5>Google</h5>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<p>
|
<p>
|
||||||
<button class={clsx('button', 'button--light', styles.socialButton)} type="button">
|
<button
|
||||||
|
class={clsx('button', 'button--light', styles.socialButton)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
<Icon name="google" class={styles.icon} />
|
<Icon name="google" class={styles.icon} />
|
||||||
Привязать
|
{t('Connect')}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -89,9 +241,12 @@ export const ProfileSecurityPage = () => {
|
||||||
<h5>VK</h5>
|
<h5>VK</h5>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<p>
|
<p>
|
||||||
<button class={clsx(styles.socialButton, 'button', 'button--light')} type="button">
|
<button
|
||||||
|
class={clsx(styles.socialButton, 'button', 'button--light')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
<Icon name="vk" class={styles.icon} />
|
<Icon name="vk" class={styles.icon} />
|
||||||
Привязать
|
{t('Connect')}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -99,9 +254,12 @@ export const ProfileSecurityPage = () => {
|
||||||
<h5>Facebook</h5>
|
<h5>Facebook</h5>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<p>
|
<p>
|
||||||
<button class={clsx(styles.socialButton, 'button', 'button--light')} type="button">
|
<button
|
||||||
|
class={clsx(styles.socialButton, 'button', 'button--light')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
<Icon name="facebook" class={styles.icon} />
|
<Icon name="facebook" class={styles.icon} />
|
||||||
Привязать
|
{t('Connect')}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,23 +276,51 @@ export const ProfileSecurityPage = () => {
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Icon name="apple" class={styles.icon} />
|
<Icon name="apple" class={styles.icon} />
|
||||||
Привязать
|
{t('Connect')}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
<button class="button button--submit" type="submit">
|
|
||||||
Сохранить настройки
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={isFloatingPanelVisible() && !emailError() && !newPasswordError()}>
|
||||||
|
<div class={styles.formActions}>
|
||||||
|
<div class="wide-container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-19 offset-md-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||||
|
<div class={styles.content}>
|
||||||
|
<Button
|
||||||
|
class={styles.cancel}
|
||||||
|
variant="light"
|
||||||
|
value={
|
||||||
|
<>
|
||||||
|
<span class={styles.cancelLabel}>{t('Cancel changes')}</span>
|
||||||
|
<span class={styles.cancelLabelMobile}>{t('Cancel')}</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onClick={handleCancel}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
variant="primary"
|
||||||
|
disabled={isSubmitting()}
|
||||||
|
value={isSubmitting() ? t('Saving...') : t('Save settings')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { PageLayout } from '../components/_shared/PageLayout'
|
||||||
import { ReactionsProvider } from '../context/reactions'
|
import { ReactionsProvider } from '../context/reactions'
|
||||||
import { useRouter } from '../stores/router'
|
import { useRouter } from '../stores/router'
|
||||||
import { loadShouts, resetSortedArticles } from '../stores/zine/articles'
|
import { loadShouts, resetSortedArticles } from '../stores/zine/articles'
|
||||||
import { loadTopic } from '../stores/zine/topics'
|
|
||||||
|
|
||||||
export const TopicPage = (props: PageProps) => {
|
export const TopicPage = (props: PageProps) => {
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
|
@ -25,7 +24,6 @@ export const TopicPage = (props: PageProps) => {
|
||||||
limit: PRERENDERED_ARTICLES_COUNT,
|
limit: PRERENDERED_ARTICLES_COUNT,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
}),
|
}),
|
||||||
loadTopic({ slug: slug() }),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { PageContextBuiltInClientWithClientRouting } from 'vike/types'
|
import type { PageContextBuiltInClientWithClientRouting } from 'vike/types'
|
||||||
import type { PageContext } from './types'
|
import type { PageContext } from './types'
|
||||||
|
|
||||||
// import * as Sentry from '@sentry/browser'
|
import { init as SentryInit, replayIntegration } from '@sentry/browser'
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
import HttpApi from 'i18next-http-backend'
|
import HttpApi from 'i18next-http-backend'
|
||||||
import ICU from 'i18next-icu'
|
import ICU from 'i18next-icu'
|
||||||
|
@ -9,7 +9,7 @@ import { hydrate } from 'solid-js/web'
|
||||||
|
|
||||||
import { App } from '../components/App'
|
import { App } from '../components/App'
|
||||||
import { initRouter } from '../stores/router'
|
import { initRouter } from '../stores/router'
|
||||||
// import { SENTRY_DSN } from '../utils/config'
|
import { GLITCHTIP_DSN } from '../utils/config'
|
||||||
import { resolveHydrationPromise } from '../utils/hydrationPromise'
|
import { resolveHydrationPromise } from '../utils/hydrationPromise'
|
||||||
|
|
||||||
let layoutReady = false
|
let layoutReady = false
|
||||||
|
@ -20,13 +20,16 @@ export const render = async (pageContext: PageContextBuiltInClientWithClientRout
|
||||||
const { pathname, search } = window.location
|
const { pathname, search } = window.location
|
||||||
const searchParams = Object.fromEntries(new URLSearchParams(search))
|
const searchParams = Object.fromEntries(new URLSearchParams(search))
|
||||||
initRouter(pathname, searchParams)
|
initRouter(pathname, searchParams)
|
||||||
/*
|
|
||||||
if (SENTRY_DSN) {
|
SentryInit({
|
||||||
Sentry.init({
|
dsn: GLITCHTIP_DSN,
|
||||||
dsn: SENTRY_DSN,
|
tracesSampleRate: 0.01,
|
||||||
|
integrations: [replayIntegration()],
|
||||||
|
// Session Replay
|
||||||
|
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
|
||||||
|
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
|
||||||
})
|
})
|
||||||
}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line import/no-named-as-default-member
|
// eslint-disable-next-line import/no-named-as-default-member
|
||||||
await i18next
|
await i18next
|
||||||
.use(HttpApi)
|
.use(HttpApi)
|
||||||
|
|
|
@ -69,6 +69,7 @@ const checkOpenOnClient = (link: HTMLAnchorElement, event) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: use scrollToHash or remove
|
||||||
const _scrollToHash = (hash: string) => {
|
const _scrollToHash = (hash: string) => {
|
||||||
let selector = hash
|
let selector = hash
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
|
export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
|
||||||
type SortedAuthorsSetter = (prev: Author[]) => Author[]
|
type SortedAuthorsSetter = (prev: Author[]) => Author[]
|
||||||
|
// FIXME: use signal or remove
|
||||||
const [_sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('name')
|
const [_sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('name')
|
||||||
|
|
||||||
export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy)
|
export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy)
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { createStorageSignal } from '@solid-primitives/storage'
|
|
||||||
|
|
||||||
// TODO: use indexedDB here
|
|
||||||
export const [seen, setSeen] = createStorageSignal<{ [slug: string]: Date }>('seen', {})
|
|
||||||
export const addSeen = (slug) => setSeen({ ...seen(), [slug]: Date.now() })
|
|
||||||
|
|
||||||
export const useSeenStore = (initialData: { [slug: string]: Date } = {}) => {
|
|
||||||
setSeen({ ...seen(), ...initialData })
|
|
||||||
|
|
||||||
return {
|
|
||||||
seen,
|
|
||||||
setSeen,
|
|
||||||
addSeen,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
import type { Topic } from '../../graphql/schema/core.gen'
|
|
||||||
|
|
||||||
import { createLazyMemo } from '@solid-primitives/memo'
|
|
||||||
import { createMemo, createSignal } from 'solid-js'
|
|
||||||
|
|
||||||
import { apiClient } from '../../graphql/client/core'
|
|
||||||
import { byTopicStatDesc } from '../../utils/sortby'
|
|
||||||
import { useRouter } from '../router'
|
|
||||||
|
|
||||||
export type TopicsSortBy = 'followers' | 'title' | 'authors' | 'shouts'
|
|
||||||
|
|
||||||
const [sortAllBy, setSortAllBy] = createSignal<TopicsSortBy>('shouts')
|
|
||||||
|
|
||||||
export const setTopicsSort = (sortBy: TopicsSortBy) => setSortAllBy(sortBy)
|
|
||||||
|
|
||||||
const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({})
|
|
||||||
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
|
|
||||||
|
|
||||||
const sortedTopics = createLazyMemo<Topic[]>(() => {
|
|
||||||
const topics = Object.values(topicEntities())
|
|
||||||
const { changeSearchParams } = useRouter()
|
|
||||||
switch (sortAllBy()) {
|
|
||||||
case 'followers': {
|
|
||||||
// console.debug('[store.topics] sorted by followers')
|
|
||||||
topics.sort(byTopicStatDesc('followers'))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'shouts': {
|
|
||||||
// log.debug(`sorted by shouts`)
|
|
||||||
topics.sort(byTopicStatDesc('shouts'))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'authors': {
|
|
||||||
// log.debug(`sorted by authors`)
|
|
||||||
topics.sort(byTopicStatDesc('authors'))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'title': {
|
|
||||||
// console.debug('[store.topics] sorted by title')
|
|
||||||
topics.sort((a, b) => a.title.localeCompare(b.title))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
topics.sort(byTopicStatDesc('shouts'))
|
|
||||||
changeSearchParams({ by: 'shouts' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return topics
|
|
||||||
})
|
|
||||||
|
|
||||||
const topTopics = createMemo(() => {
|
|
||||||
const topics = Object.values(topicEntities())
|
|
||||||
topics.sort(byTopicStatDesc('shouts'))
|
|
||||||
return topics
|
|
||||||
})
|
|
||||||
|
|
||||||
const addTopics = (...args: Topic[][]) => {
|
|
||||||
const allTopics = args.flatMap((topics) => (topics || []).filter(Boolean))
|
|
||||||
|
|
||||||
const newTopicEntities = allTopics.reduce(
|
|
||||||
(acc, topic) => {
|
|
||||||
acc[topic.slug] = topic
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, Topic>,
|
|
||||||
)
|
|
||||||
|
|
||||||
setTopicEntities((prevTopicEntities) => {
|
|
||||||
return {
|
|
||||||
...prevTopicEntities,
|
|
||||||
...newTopicEntities,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadAllTopics = async (): Promise<void> => {
|
|
||||||
const topics = await apiClient.getAllTopics()
|
|
||||||
addTopics(topics)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadRandomTopics = async ({ amount }: { amount: number }): Promise<void> => {
|
|
||||||
const topics = await apiClient.getRandomTopics({ amount })
|
|
||||||
setRandomTopics(topics)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadTopic = async ({ slug }: { slug: string }): Promise<void> => {
|
|
||||||
const topic = await apiClient.getTopic({ slug })
|
|
||||||
addTopics([topic])
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitialState = {
|
|
||||||
topics?: Topic[]
|
|
||||||
randomTopics?: Topic[]
|
|
||||||
sortBy?: TopicsSortBy
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useTopicsStore = (initialState: InitialState = {}) => {
|
|
||||||
if (initialState.sortBy) {
|
|
||||||
setSortAllBy(initialState.sortBy)
|
|
||||||
}
|
|
||||||
|
|
||||||
addTopics(initialState.topics, initialState.randomTopics)
|
|
||||||
|
|
||||||
if (initialState.randomTopics) {
|
|
||||||
setRandomTopics(initialState.randomTopics)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { topicEntities, sortedTopics, randomTopics, topTopics }
|
|
||||||
}
|
|
|
@ -5,6 +5,7 @@ export const cdnUrl = 'https://cdn.discours.io'
|
||||||
export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || defaultThumborUrl
|
export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || defaultThumborUrl
|
||||||
|
|
||||||
export const SENTRY_DSN = import.meta.env.PUBLIC_SENTRY_DSN || ''
|
export const SENTRY_DSN = import.meta.env.PUBLIC_SENTRY_DSN || ''
|
||||||
|
export const GLITCHTIP_DSN = import.meta.env.PUBLIC_GLITCHTIP_DSN || ''
|
||||||
|
|
||||||
const defaultSearchUrl = 'https://search.discours.io'
|
const defaultSearchUrl = 'https://search.discours.io'
|
||||||
export const searchUrl = import.meta.env.PUBLIC_SEARCH_URL || defaultSearchUrl
|
export const searchUrl = import.meta.env.PUBLIC_SEARCH_URL || defaultSearchUrl
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { RANDOM_TOPICS_COUNT } from '../components/Views/Home'
|
||||||
import { Topic } from '../graphql/schema/core.gen'
|
import { Topic } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
export const getRandomTopicsFromArray = (topics: Topic[], count: number = RANDOM_TOPICS_COUNT): Topic[] => {
|
export const getRandomTopicsFromArray = (topics: Topic[], count: number = RANDOM_TOPICS_COUNT): Topic[] => {
|
||||||
|
if (!Array.isArray(topics)) return []
|
||||||
const shuffledTopics = [...topics].sort(() => 0.5 - Math.random())
|
const shuffledTopics = [...topics].sort(() => 0.5 - Math.random())
|
||||||
return shuffledTopics.slice(0, count)
|
return shuffledTopics.slice(0, count)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,15 @@ import { UploadedFile } from '../pages/types'
|
||||||
const apiBaseUrl = 'https://core.discours.io'
|
const apiBaseUrl = 'https://core.discours.io'
|
||||||
const apiUrl = `${apiBaseUrl}/upload`
|
const apiUrl = `${apiBaseUrl}/upload`
|
||||||
|
|
||||||
export const handleFileUpload = async (uploadFile: UploadFile): Promise<UploadedFile> => {
|
export const handleFileUpload = async (uploadFile: UploadFile, token: string): Promise<UploadedFile> => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', uploadFile.file, uploadFile.name)
|
formData.append('file', uploadFile.file, uploadFile.name)
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
Authorization: token,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,14 @@ import { UploadedFile } from '../pages/types'
|
||||||
|
|
||||||
import { thumborUrl } from './config'
|
import { thumborUrl } from './config'
|
||||||
|
|
||||||
export const handleImageUpload = async (uploadFile: UploadFile): Promise<UploadedFile> => {
|
export const handleImageUpload = async (uploadFile: UploadFile, token: string): Promise<UploadedFile> => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('media', uploadFile.file, uploadFile.name)
|
formData.append('media', uploadFile.file, uploadFile.name)
|
||||||
|
const headers = token ? { Authorization: token } : {}
|
||||||
const response = await fetch(`${thumborUrl}/image`, {
|
const response = await fetch(`${thumborUrl}/image`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
|
headers,
|
||||||
})
|
})
|
||||||
|
|
||||||
const location = response.headers.get('Location')
|
const location = response.headers.get('Location')
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Shout } from '../graphql/schema/core.gen'
|
import { Shout } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
const MAX_DESCRIPTION_LENGTH = 150
|
const MAX_DESCRIPTION_LENGTH = 150
|
||||||
|
|
||||||
export const getDescription = (body: string): string => {
|
export const getDescription = (body: string): string => {
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return ''
|
return ''
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import ssrPlugin from 'vike/plugin'
|
import ssrPlugin from 'vike/plugin'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import mkcert from 'vite-plugin-mkcert'
|
import mkcert from 'vite-plugin-mkcert'
|
||||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
import sassDts from 'vite-plugin-sass-dts'
|
import sassDts from 'vite-plugin-sass-dts'
|
||||||
import solidPlugin from 'vite-plugin-solid'
|
import solidPlugin from 'vite-plugin-solid'
|
||||||
|
|
||||||
|
@ -69,6 +69,7 @@ export default defineConfig(({ mode, command }) => {
|
||||||
https: {},
|
https: {},
|
||||||
port: 3000,
|
port: 3000,
|
||||||
},
|
},
|
||||||
|
sourcemap: isDev,
|
||||||
css: {
|
css: {
|
||||||
devSourcemap: isDev,
|
devSourcemap: isDev,
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user