Merge branch 'dev' of github.com:Discours/discoursio-webapp into feature/rating

This commit is contained in:
Untone 2024-05-18 15:01:43 +03:00
commit 909c9a2983
69 changed files with 1288 additions and 951 deletions

View File

@ -13,7 +13,7 @@ jobs:
run: npm ci
- name: Check types
run: npm run check:types
run: npm run typecheck
- name: Lint with Biome
run: npm run check:code

View File

@ -1,21 +1,8 @@
{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"files": {
"include": [
"*.tsx",
"*.ts",
"*.js",
"*.json"
],
"ignore": [
"./dist",
"./node_modules",
".husky",
"docs",
"gen",
"*.gen.ts",
"*.d.ts"
]
"include": ["*.tsx", "*.ts", "*.js", "*.json"],
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.gen.ts", "*.d.ts"]
},
"vcs": {
"defaultBranch": "dev",
@ -23,19 +10,13 @@
},
"organizeImports": {
"enabled": true,
"ignore": [
"./api",
"./gen"
]
"ignore": ["./api", "./gen"]
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 108,
"ignore": [
"./src/graphql/schema",
"./gen"
]
"ignore": ["./src/graphql/schema", "./gen"]
},
"javascript": {
"formatter": {
@ -48,13 +29,7 @@
}
},
"linter": {
"ignore": [
"*.scss",
"*.md",
".DS_Store",
"*.svg",
"*.d.ts"
],
"ignore": ["*.scss", "*.md", ".DS_Store", "*.svg", "*.d.ts"],
"enabled": true,
"rules": {
"all": true,

543
package-lock.json generated
View File

@ -34,9 +34,8 @@
"@solid-primitives/memo": "1.2.4",
"@solid-primitives/pagination": "0.2.10",
"@solid-primitives/share": "2.0.4",
"@solid-primitives/storage": "1.3.9",
"@solid-primitives/upload": "0.0.110",
"@solidjs/meta": "0.29.1",
"@solid-primitives/storage": "^3.5.0",
"@solid-primitives/upload": "0.0.115",
"@thisbeyond/solid-select": "0.14.0",
"@tiptap/core": "2.2.3",
"@tiptap/extension-blockquote": "2.2.3",
@ -95,9 +94,9 @@
"prosemirror-history": "1.3.2",
"prosemirror-trailing-node": "2.0.7",
"prosemirror-view": "1.32.7",
"rollup": "4.11.0",
"rollup": "4.17.2",
"sass": "1.69.5",
"solid-js": "1.8.15",
"solid-js": "1.8.17",
"solid-popper": "0.3.0",
"solid-tiptap": "0.7.0",
"solid-transition-group": "0.2.3",
@ -111,7 +110,7 @@
"typograf": "7.3.0",
"uniqolor": "1.1.0",
"vike": "0.4.148",
"vite": "5.2.10",
"vite": "5.2.11",
"vite-plugin-mkcert": "^1.17.3",
"vite-plugin-node-polyfills": "0.21.0",
"vite-plugin-sass-dts": "^1.3.17",
@ -1204,9 +1203,9 @@
}
},
"node_modules/@biomejs/biome": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.7.2.tgz",
"integrity": "sha512-6Skx9N47inLQzYi9RKgJ7PBnUnaHnMe/imqX43cOcJjZtfMnQLxEvfM2Eyo7gChkwrZlwc+VbA4huFRjw2fsYA==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.7.3.tgz",
"integrity": "sha512-ogFQI+fpXftr+tiahA6bIXwZ7CSikygASdqMtH07J2cUzrpjyTMVc9Y97v23c7/tL1xCZhM+W9k4hYIBm7Q6cQ==",
"dev": true,
"hasInstallScript": true,
"bin": {
@ -1220,20 +1219,20 @@
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "1.7.2",
"@biomejs/cli-darwin-x64": "1.7.2",
"@biomejs/cli-linux-arm64": "1.7.2",
"@biomejs/cli-linux-arm64-musl": "1.7.2",
"@biomejs/cli-linux-x64": "1.7.2",
"@biomejs/cli-linux-x64-musl": "1.7.2",
"@biomejs/cli-win32-arm64": "1.7.2",
"@biomejs/cli-win32-x64": "1.7.2"
"@biomejs/cli-darwin-arm64": "1.7.3",
"@biomejs/cli-darwin-x64": "1.7.3",
"@biomejs/cli-linux-arm64": "1.7.3",
"@biomejs/cli-linux-arm64-musl": "1.7.3",
"@biomejs/cli-linux-x64": "1.7.3",
"@biomejs/cli-linux-x64-musl": "1.7.3",
"@biomejs/cli-win32-arm64": "1.7.3",
"@biomejs/cli-win32-x64": "1.7.3"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.7.2.tgz",
"integrity": "sha512-CrldIueHivWEWmeTkK8bTXajeX53F8i2Rrkkt8cPZyMtzkrwxf8Riq4a/jz3SQBHkxHFT4TqGbSTNMXe3X1ogA==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.7.3.tgz",
"integrity": "sha512-eDvLQWmGRqrPIRY7AIrkPHkQ3visEItJKkPYSHCscSDdGvKzYjmBJwG1Gu8+QC5ed6R7eiU63LEC0APFBobmfQ==",
"cpu": [
"arm64"
],
@ -1247,9 +1246,9 @@
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.7.2.tgz",
"integrity": "sha512-UELnLJuJOsTL9meArvn8BtiXDURyPil2Ej9me2uVpEvee8UQdqd/bssP5we400OWShlL1AAML4fn6d2WX5332g==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.7.3.tgz",
"integrity": "sha512-JXCaIseKRER7dIURsVlAJacnm8SG5I0RpxZ4ya3dudASYUc68WGl4+FEN03ABY3KMIq7hcK1tzsJiWlmXyosZg==",
"cpu": [
"x64"
],
@ -1263,9 +1262,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.7.2.tgz",
"integrity": "sha512-Z1CSGQE6fHz55gkiFHv9E8wEAaSUd7dHSRaxSCBa7utonHqpIeMbvj3Evm1w0WfGLFDtRXLV1fTfEdM0FMTOhA==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.7.3.tgz",
"integrity": "sha512-phNTBpo7joDFastnmZsFjYcDYobLTx4qR4oPvc9tJ486Bd1SfEVPHEvJdNJrMwUQK56T+TRClOQd/8X1nnjA9w==",
"cpu": [
"arm64"
],
@ -1279,9 +1278,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.7.2.tgz",
"integrity": "sha512-kKYZiem7Sj7wI0dpVxJlK7C+TFQwzO/ctufIGXGJAyEmUe9vEKSzV8CXpv+JIRiTWyqaZJ4K+eHz4SPdPCv05w==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.7.3.tgz",
"integrity": "sha512-c8AlO45PNFZ1BYcwaKzdt46kYbuP6xPGuGQ6h4j3XiEDpyseRRUy/h+6gxj07XovmyxKnSX9GSZ6nVbZvcVUAw==",
"cpu": [
"arm64"
],
@ -1295,9 +1294,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.7.2.tgz",
"integrity": "sha512-vXXyox8/CQijBxAu0+r8FfSO7JlC4tob3PbaFda8gPJFRz2uFJw39HtxVUwbTV1EcU6wSPh4SiRu5sZfP1VHrQ==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.7.3.tgz",
"integrity": "sha512-vnedYcd5p4keT3iD48oSKjOIRPYcjSNNbd8MO1bKo9ajg3GwQXZLAH+0Cvlr+eMsO67/HddWmscSQwTFrC/uPA==",
"cpu": [
"x64"
],
@ -1311,9 +1310,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.7.2.tgz",
"integrity": "sha512-x10LpGMepDrLS+h2TZ6/T7egpHjGKtiI4GuShNylmBQJWfTotbFf9eseHggrqJ4WZf9yrGoVYrtbxXftuB95sQ==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.7.3.tgz",
"integrity": "sha512-UdEHKtYGWEX3eDmVWvQeT+z05T9/Sdt2+F/7zmMOFQ7boANeX8pcO6EkJPK3wxMudrApsNEKT26rzqK6sZRTRA==",
"cpu": [
"x64"
],
@ -1327,9 +1326,9 @@
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.7.2.tgz",
"integrity": "sha512-kRXdlKzcU7INf6/ldu0nVmkOgt7bKqmyXRRCUqqaJfA32+9InTbkD8tGrHZEVYIWr+eTuKcg16qZVDsPSDFZ8g==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.7.3.tgz",
"integrity": "sha512-unNCDqUKjujYkkSxs7gFIfdasttbDC4+z0kYmcqzRk6yWVoQBL4dNLcCbdnJS+qvVDNdI9rHp2NwpQ0WAdla4Q==",
"cpu": [
"arm64"
],
@ -1343,9 +1342,9 @@
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.7.2.tgz",
"integrity": "sha512-qHTtpAs+CNglAAuaTy09htoqUhrQyd3nd0aGTuLNqD10h1llMVi8WFZfoa+e5MuDSfYtMK6nW2Tbf6WgzzR1Qw==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.7.3.tgz",
"integrity": "sha512-ZmByhbrnmz/UUFYB622CECwhKIPjJLLPr5zr3edhu04LzbfcOrz16VYeNq5dpO1ADG70FORhAJkaIGdaVBG00w==",
"cpu": [
"x64"
],
@ -1392,9 +1391,9 @@
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz",
"integrity": "sha512-ubEkAaTfVZa+WwGhs5jbo5Xfqpeaybr/RvWzvFxRs4jfq16wH8l8Ty/QEEpINxll4xhuGfdMbipRyz5QZh9+FA==",
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.3.tgz",
"integrity": "sha512-xI/tL2zxzEbESvnSxwFgwvy5HS00oCXxL4MLs6HUiDcYfwowsoQaABKxUElp1ARITrINzBnsECOc1q0eg2GOrA==",
"dev": true,
"funding": [
{
@ -1410,13 +1409,13 @@
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^2.2.4"
"@csstools/css-tokenizer": "^2.3.1"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.4.tgz",
"integrity": "sha512-PuWRAewQLbDhGeTvFuq2oClaSCKPIBmHyIobCV39JHRYN0byDcUWJl5baPeNUcqrjtdMNqFooE0FGl31I3JOqw==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.3.1.tgz",
"integrity": "sha512-iMNHTyxLbBlWIfGtabT157LH9DUx9X8+Y3oymFEuMj8HNc+rpE3dPFGFgHjpKfjeFDjLjYIAIhXPGvS2lKxL9g==",
"dev": true,
"funding": [
{
@ -1433,9 +1432,9 @@
}
},
"node_modules/@csstools/media-query-list-parser": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.9.tgz",
"integrity": "sha512-qqGuFfbn4rUmyOB0u8CVISIp5FfJ5GAR3mBrZ9/TKndHakdnm6pY0L/fbLcpPnrzwCyyTEZl1nUcXAYHEWneTA==",
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.11.tgz",
"integrity": "sha512-uox5MVhvNHqitPP+SynrB1o8oPxPMt2JLgp5ghJOWf54WGQ5OKu47efne49r1SWqs3wRP8xSWjnO9MBKxhB1dA==",
"dev": true,
"funding": [
{
@ -1451,8 +1450,8 @@
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^2.6.1",
"@csstools/css-tokenizer": "^2.2.4"
"@csstools/css-parser-algorithms": "^2.6.3",
"@csstools/css-tokenizer": "^2.3.1"
}
},
"node_modules/@csstools/selector-specificity": {
@ -1487,22 +1486,6 @@
"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": {
"version": "0.17.19",
"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": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.11.0.tgz",
"integrity": "sha512-BV+u2QSfK3i1o6FucqJh5IK9cjAU6icjFFhvknzFgu472jzl0bBojfDAkJLBEsHFMo+YZg6rthBvBBt8z12IBQ==",
"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"
],
@ -3645,9 +3628,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.11.0.tgz",
"integrity": "sha512-0ij3iw7sT5jbcdXofWO2NqDNjSVVsf6itcAkV2I6Xsq4+6wjW1A8rViVB67TfBEan7PV2kbLzT8rhOVWLI2YXw==",
"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"
],
@ -3658,9 +3641,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.11.0.tgz",
"integrity": "sha512-yPLs6RbbBMupArf6qv1UDk6dzZvlH66z6NLYEwqTU0VHtss1wkI4UYeeMS7TVj5QRVvaNAWYKP0TD/MOeZ76Zg==",
"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"
],
@ -3671,9 +3654,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.11.0.tgz",
"integrity": "sha512-OvqIgwaGAwnASzXaZEeoJY3RltOFg+WUbdkdfoluh2iqatd090UeOG3A/h0wNZmE93dDew9tAtXgm3/+U/B6bw==",
"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"
],
@ -3684,9 +3667,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.11.0.tgz",
"integrity": "sha512-X17s4hZK3QbRmdAuLd2EE+qwwxL8JxyVupEqAkxKPa/IgX49ZO+vf0ka69gIKsaYeo6c1CuwY3k8trfDtZ9dFg==",
"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"
],
@ -3710,9 +3693,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.11.0.tgz",
"integrity": "sha512-673Lu9EJwxVB9NfYeA4AdNu0FOHz7g9t6N1DmT7bZPn1u6bTF+oZjj+fuxUcrfxWXE0r2jxl5QYMa9cUOj9NFg==",
"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"
],
@ -3723,9 +3706,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.11.0.tgz",
"integrity": "sha512-yFW2msTAQNpPJaMmh2NpRalr1KXI7ZUjlN6dY/FhWlOclMrZezm5GIhy3cP4Ts2rIAC+IPLAjNibjp1BsxCVGg==",
"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"
],
@ -3749,9 +3732,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.11.0.tgz",
"integrity": "sha512-kKT9XIuhbvYgiA3cPAGntvrBgzhWkGpBMzuk1V12Xuoqg7CI41chye4HU0vLJnGf9MiZzfNh4I7StPeOzOWJfA==",
"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"
],
@ -3775,9 +3758,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.11.0.tgz",
"integrity": "sha512-6q4ESWlyTO+erp1PSCmASac+ixaDv11dBk1fqyIuvIUc/CmRAX2Zk+2qK1FGo5q7kyDcjHCFVwgGFCGIZGVwCA==",
"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"
],
@ -3788,9 +3771,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.11.0.tgz",
"integrity": "sha512-vIAQUmXeMLmaDN78HSE4Kh6xqof2e3TJUKr+LPqXWU4NYNON0MDN9h2+t4KHrPAQNmU3w1GxBQ/n01PaWFwa5w==",
"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"
],
@ -3801,9 +3784,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.11.0.tgz",
"integrity": "sha512-LVXo9dDTGPr0nezMdqa1hK4JeoMZ02nstUxGYY/sMIDtTYlli1ZxTXBYAz3vzuuvKO4X6NBETciIh7N9+abT1g==",
"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"
],
@ -3814,9 +3797,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.11.0.tgz",
"integrity": "sha512-xZVt6K70Gr3I7nUhug2dN6VRR1ibot3rXqXS3wo+8JP64t7djc3lBFyqO4GiVrhNaAIhUCJtwQ/20dr0h0thmQ==",
"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"
],
@ -3827,9 +3810,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.11.0.tgz",
"integrity": "sha512-f3I7h9oTg79UitEco9/2bzwdciYkWr8pITs3meSDSlr1TdvQ7IxkQaaYN2YqZXX5uZhiYL+VuYDmHwNzhx+HOg==",
"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"
],
@ -4072,15 +4055,24 @@
}
},
"node_modules/@solid-primitives/storage": {
"version": "1.3.9",
"resolved": "https://registry.npmjs.org/@solid-primitives/storage/-/storage-1.3.9.tgz",
"integrity": "sha512-ysJSIycmToQD8Hpt4jpIlh7U8EuYdpQwkamppng3g93E5f6RZVPCzYmRZ+ckRN2cNLFpAuTEqZx7OBRh3PBWFQ==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@solid-primitives/storage/-/storage-3.5.0.tgz",
"integrity": "sha512-AqU3vrXz8XlxOgJIiP+oQxE/vFchGf4Qe7E5Xfa02DJdF9rD8CtiTmVZDBU08ViS7g0Nwc4IpStHvVO0jBMalQ==",
"dev": true,
"dependencies": {
"@solid-primitives/utils": "^6.0.0"
"@solid-primitives/utils": "^6.2.3"
},
"peerDependencies": {
"@tauri-apps/plugin-store": "*",
"solid-js": "^1.6.12"
},
"peerDependenciesMeta": {
"@tauri-apps/plugin-store": {
"optional": true
},
"solid-start": {
"optional": true
}
}
},
"node_modules/@solid-primitives/transition-group": {
@ -4093,26 +4085,17 @@
}
},
"node_modules/@solid-primitives/upload": {
"version": "0.0.110",
"resolved": "https://registry.npmjs.org/@solid-primitives/upload/-/upload-0.0.110.tgz",
"integrity": "sha512-YQZGogXzc77c/3hxDoxGi78FkvQQfUbElbdSPn+E0GRl21XMuJbD/QKQKNXm7KyxX+cMTwLnQYCoqfRXcgHMIA==",
"version": "0.0.115",
"resolved": "https://registry.npmjs.org/@solid-primitives/upload/-/upload-0.0.115.tgz",
"integrity": "sha512-CWTXz28mmRGvZV90IzViNtBAKC6cnO2WSNb3UjvbkPRrtxlHrj/ewZwLRhuLSu6JzdY/c+rHi1j24v7H0SrdXg==",
"dev": true,
"dependencies": {
"@solid-primitives/utils": "^5.5.1"
"@solid-primitives/utils": "^6.2.2"
},
"peerDependencies": {
"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": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz",
@ -4122,15 +4105,6 @@
"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": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz",
@ -4630,9 +4604,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.12.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz",
"integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==",
"version": "20.12.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz",
"integrity": "sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@ -5454,9 +5428,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001615",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz",
"integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==",
"version": "1.0.30001616",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz",
"integrity": "sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==",
"dev": true,
"funding": [
{
@ -6290,9 +6264,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.756",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.756.tgz",
"integrity": "sha512-RJKZ9+vEBMeiPAvKNWyZjuYyUqMndcP1f335oHqn3BEQbs2NFtVrnK5+6Xg5wSM9TknNNpWghGDUCKGYF+xWXw==",
"version": "1.4.757",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.757.tgz",
"integrity": "sha512-jftDaCknYSSt/+KKeXzH3LX5E2CvRLm75P3Hj+J/dv3CL0qUYcOt13d5FN1NiL5IJbbhzHrb3BomeG2tkSlZmw==",
"dev": true
},
"node_modules/elliptic": {
@ -10796,9 +10770,9 @@
}
},
"node_modules/prosemirror-model": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.20.0.tgz",
"integrity": "sha512-q7AY7vMjKYqDCeoedgUiAgrLabliXxndJuuFmcmc2+YU1SblvnOiG2WEACF2lwAZsMlfLpiAilA3L+TWlDqIsQ==",
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.21.0.tgz",
"integrity": "sha512-zLpS1mVCZLA7VTp82P+BfMiYVPcX1/z0Mf3gsjKZtzMWubwn2pN7CceMV0DycjlgE5JeXPR7UF4hJPbBV98oWA==",
"dev": true,
"dependencies": {
"orderedmap": "^2.0.0"
@ -10880,12 +10854,12 @@
}
},
"node_modules/prosemirror-transform": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz",
"integrity": "sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.9.0.tgz",
"integrity": "sha512-5UXkr1LIRx3jmpXXNKDhv8OyAOeLTGuXNwdVfg8x27uASna/wQkr9p6fD3eupGOi4PLJfbezxTyi/7fSJypXHg==",
"dev": true,
"dependencies": {
"prosemirror-model": "^1.0.0"
"prosemirror-model": "^1.21.0"
}
},
"node_modules/prosemirror-view": {
@ -11193,9 +11167,9 @@
}
},
"node_modules/rollup": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.11.0.tgz",
"integrity": "sha512-2xIbaXDXjf3u2tajvA5xROpib7eegJ9Y/uPlSFhXLNpK9ampCczXAhLEb5yLzJyG3LAdI1NWtNjDXiLyniNdjQ==",
"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"
@ -11208,19 +11182,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.11.0",
"@rollup/rollup-android-arm64": "4.11.0",
"@rollup/rollup-darwin-arm64": "4.11.0",
"@rollup/rollup-darwin-x64": "4.11.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.11.0",
"@rollup/rollup-linux-arm64-gnu": "4.11.0",
"@rollup/rollup-linux-arm64-musl": "4.11.0",
"@rollup/rollup-linux-riscv64-gnu": "4.11.0",
"@rollup/rollup-linux-x64-gnu": "4.11.0",
"@rollup/rollup-linux-x64-musl": "4.11.0",
"@rollup/rollup-win32-arm64-msvc": "4.11.0",
"@rollup/rollup-win32-ia32-msvc": "4.11.0",
"@rollup/rollup-win32-x64-msvc": "4.11.0",
"@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"
}
},
@ -11550,13 +11527,13 @@
}
},
"node_modules/solid-js": {
"version": "1.8.15",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.15.tgz",
"integrity": "sha512-d0QP/efr3UVcwGgWVPveQQ0IHOH6iU7yUhc2piy8arNG8wxKmvUy1kFxyF8owpmfCWGB87usDKMaVnsNYZm+Vw==",
"version": "1.8.17",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.17.tgz",
"integrity": "sha512-E0FkUgv9sG/gEBWkHr/2XkBluHb1fkrHywUgA6o6XolPDCJ4g1HaLmQufcBBhiF36ee40q+HpG/vCZu7fLpI3Q==",
"dev": true,
"dependencies": {
"csstype": "^3.1.0",
"seroval": "^1.0.3",
"seroval": "^1.0.4",
"seroval-plugins": "^1.0.3"
}
},
@ -12530,9 +12507,9 @@
}
},
"node_modules/vite": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz",
"integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==",
"version": "5.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
"integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
"dev": true,
"dependencies": {
"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": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
@ -13012,175 +13005,6 @@
"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": {
"version": "0.20.2",
"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_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": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",

View File

@ -11,12 +11,12 @@
"deploy": "graphql-codegen && npm run typecheck && vite build && vercel",
"dev": "vite",
"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",
"hygen": "HYGEN_TMPLS=gen hygen",
"postinstall": "npm run codegen && npx patch-package",
"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:code": "npx @biomejs/biome lint src --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/pagination": "0.2.10",
"@solid-primitives/share": "2.0.4",
"@solid-primitives/storage": "1.3.9",
"@solid-primitives/upload": "0.0.110",
"@solidjs/meta": "0.29.1",
"@solid-primitives/storage": "^3.5.0",
"@solid-primitives/upload": "0.0.115",
"@thisbeyond/solid-select": "0.14.0",
"@tiptap/core": "2.2.3",
"@tiptap/extension-blockquote": "2.2.3",
@ -113,9 +112,9 @@
"prosemirror-history": "1.3.2",
"prosemirror-trailing-node": "2.0.7",
"prosemirror-view": "1.32.7",
"rollup": "4.11.0",
"rollup": "4.17.2",
"sass": "1.69.5",
"solid-js": "1.8.15",
"solid-js": "1.8.17",
"solid-popper": "0.3.0",
"solid-tiptap": "0.7.0",
"solid-transition-group": "0.2.3",
@ -129,7 +128,7 @@
"typograf": "7.3.0",
"uniqolor": "1.1.0",
"vike": "0.4.148",
"vite": "5.2.10",
"vite": "5.2.11",
"vite-plugin-mkcert": "^1.17.3",
"vite-plugin-node-polyfills": "0.21.0",
"vite-plugin-sass-dts": "^1.3.17",
@ -141,7 +140,5 @@
"y-prosemirror": "1.2.2",
"yjs": "13.6.12"
},
"trustedDependencies": [
"@biomejs/biome"
]
"trustedDependencies": ["@biomejs/biome"]
}

4
public/icons/logout.svg Normal file
View 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
View 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

View File

@ -398,7 +398,7 @@
"Top authors": "Authors rating",
"Top commented": "Most commented",
"Top discussed": "Top discussed",
"Top month articles": "Top of the month",
"Top month": "Top of the month",
"Top rated": "Popular",
"Top recent": "Most recent",
"Top topics": "Interesting topics",
@ -532,5 +532,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..."
"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"
}

View File

@ -419,7 +419,7 @@
"Top authors": "Рейтинг авторов",
"Top commented": "Самое комментируемое",
"Top discussed": "Обсуждаемое",
"Top month articles": "Лучшие материалы месяца",
"Top month": "Лучшее за месяц",
"Top rated": "Популярное",
"Top recent": "Самое новое",
"Top topics": "Интересные темы",
@ -559,5 +559,13 @@
"It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля",
"Restore password": "Восстановить пароль",
"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": "Неверное подтверждение нового пароля"
}

View File

@ -1,8 +1,8 @@
import type { PageProps, RootSearchParams } from '../pages/types'
import { Meta, MetaProvider } from '@solidjs/meta'
import { Component, createEffect, createMemo } from 'solid-js'
import { Dynamic } from 'solid-js/web'
import { Meta, MetaProvider } from '../context/meta'
import { ConfirmProvider } from '../context/confirm'
import { ConnectProvider } from '../context/connect'
@ -12,6 +12,7 @@ import { InboxProvider } from '../context/inbox'
import { LocalizeProvider } from '../context/localize'
import { MediaQueryProvider } from '../context/mediaQuery'
import { NotificationsProvider } from '../context/notifications'
import { SeenProvider } from '../context/seen'
import { SessionProvider } from '../context/session'
import { SnackbarProvider } from '../context/snackbar'
import { TopicsProvider } from '../context/topics'
@ -115,6 +116,7 @@ export const App = (props: Props) => {
<MediaQueryProvider>
<SnackbarProvider>
<TopicsProvider>
<SeenProvider>
<ConfirmProvider>
<SessionProvider onStateChangeCallback={console.log}>
<FollowingProvider>
@ -130,6 +132,7 @@ export const App = (props: Props) => {
</FollowingProvider>
</SessionProvider>
</ConfirmProvider>
</SeenProvider>
</TopicsProvider>
</SnackbarProvider>
</MediaQueryProvider>

View File

@ -127,7 +127,7 @@ export const Comment = (props: Props) => {
<li
id={`comment_${props.comment.id}`}
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()}>

View File

@ -11,6 +11,7 @@ import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
import { Comment } from './Comment'
import { useSeen } from '../../context/seen'
import styles from './Article.module.scss'
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
@ -48,21 +49,20 @@ export const CommentsTree = (props: Props) => {
}
return newSortedComments
})
const dateFromLocalStorage = Number.parseInt(localStorage.getItem(`${props.shoutSlug}`))
const { seen } = useSeen()
const shoutLastSeen = createMemo(() => seen()[props.shoutSlug] ?? 0)
const currentDate = new Date()
const setCookie = () => localStorage.setItem(`${props.shoutSlug}`, `${currentDate}`)
onMount(() => {
if (!dateFromLocalStorage) {
if (!shoutLastSeen()) {
setCookie()
} else if (currentDate.getTime() > dateFromLocalStorage) {
} else if (currentDate.getTime() > shoutLastSeen()) {
const newComments = comments().filter((c) => {
if (c.reply_to || c.created_by.slug === author()?.slug) {
return
}
const created = c.created_at
return created > dateFromLocalStorage
return (c.updated_at || c.created_at) > shoutLastSeen()
})
setNewReactions(newComments)
setCookie()
@ -134,7 +134,7 @@ export const CommentsTree = (props: Props) => {
comment={reaction}
clickedReply={(id) => setClickedReplyId(id)}
clickedReplyId={clickedReplyId()}
lastSeen={dateFromLocalStorage}
lastSeen={shoutLastSeen()}
/>
)}
</For>

View File

@ -2,11 +2,11 @@ import type { Author, Reaction, Shout, Topic } from '../../graphql/schema/core.g
import { getPagePath } from '@nanostores/router'
import { createPopper } from '@popperjs/core'
import { Link, Meta } from '@solidjs/meta'
import { clsx } from 'clsx'
import { install } from 'ga-gtag'
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
import { isServer } from 'solid-js/web'
import { Link, Meta } from '../../context/meta'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
@ -38,6 +38,7 @@ import { CommentsTree } from './CommentsTree'
import { RatingControl as ShoutRatingControl } from './RatingControl'
import { SharePopup, getShareUrl } from './SharePopup'
import { useSeen } from '../../context/seen'
import stylesHeader from '../Nav/Header/Header.module.scss'
import styles from './Article.module.scss'
@ -76,6 +77,7 @@ export const FullArticle = (props: Props) => {
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const { t, formatDate, lang } = useLocalize()
const { author, session, requireAuthentication } = useSession()
const { addSeen } = useSeen()
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000)))
@ -301,9 +303,13 @@ export const FullArticle = (props: Props) => {
const [ratings, setRatings] = createSignal<Reaction[]>([])
onMount(async () => {
document.title = props.article?.title
install('G-LQ4B87H8C2')
await loadReactionsBy({ by: { shout: props.article.slug } })
addSeen(props.article.slug)
setIsReactionsLoaded(true)
document.title = props.article.title
window?.addEventListener('resize', updateIframeSizes)
onCleanup(() => window.removeEventListener('resize', updateIframeSizes))
createEffect(() => {
@ -550,7 +556,7 @@ export const FullArticle = (props: Props) => {
{(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef}>
<a
href={getPagePath(router, 'edit', { shoutId: props.article.id.toString() })}
href={getPagePath(router, 'edit', { shoutId: props.article?.id.toString() })}
class={styles.shoutStatsItemInner}
>
<Icon name="pencil-outline" class={styles.icon} />

View File

@ -28,7 +28,7 @@ export const SharePopup = (props: SharePopupProps) => {
})
return (
<Popup {...props} variant="bordered" onVisibilityChange={(value) => setIsVisible(value)}>
<Popup {...props} onVisibilityChange={(value) => setIsVisible(value)}>
<ShareLinks
variant="inPopup"
title={props.title}

View File

@ -53,7 +53,7 @@ export const AuthorBadge = (props: Props) => {
requireAuthentication(() => {
openPage(router, 'inbox')
changeSearchParams({
initChat: props.author.id.toString(),
initChat: props.author?.id.toString(),
})
}, 'discussions')
}

View File

@ -65,7 +65,7 @@ export const AuthorCard = (props: Props) => {
requireAuthentication(() => {
openPage(router, 'inbox')
changeSearchParams({
initChat: props.author.id.toString(),
initChat: props.author?.id.toString(),
})
}, 'discussions')
}

View File

@ -60,7 +60,7 @@ export const Draft = (props: Props) => {
<div class={styles.actions}>
<a
class={styles.actionItem}
href={getPagePath(router, 'edit', { shoutId: props.shout.id.toString() })}
href={getPagePath(router, 'edit', { shoutId: props.shout?.id.toString() })}
>
{t('Edit')}
</a>

View File

@ -48,11 +48,13 @@ type Props = {
onChange?: (text: string) => void
variant?: 'minimal' | 'bordered'
maxLength?: number
noLimits?: boolean
maxHeight?: number
submitButtonText?: string
quoteEnabled?: boolean
imageEnabled?: boolean
setClear?: boolean
resetToInitial?: boolean
smallHeight?: boolean
submitByCtrlEnter?: boolean
onlyBubbleControls?: boolean
@ -124,7 +126,7 @@ const SimplifiedEditor = (props: Props) => {
openOnClick: false,
}),
CharacterCount.configure({
limit: maxLength,
limit: props.noLimits ? null : maxLength,
}),
Blockquote.configure({
HTMLAttributes: {
@ -216,6 +218,10 @@ const SimplifiedEditor = (props: Props) => {
if (props.setClear) {
editor().commands.clearContent(true)
}
if (props.resetToInitial) {
editor().commands.clearContent(true)
editor().commands.setContent(props.initialContent)
}
})
const handleKeyDown = (event) => {

View File

@ -603,6 +603,7 @@
.shoutCardDetailsItem {
align-items: center;
display: flex;
font-size: 1.4rem;
margin-right: 1.2rem;
white-space: nowrap;

View File

@ -89,8 +89,8 @@ const getTitleAndSubtitle = (
}
const getMainTopicTitle = (article: Shout, lng: string) => {
const mainTopicSlug = article.main_topic || ''
const mainTopic = article.topics?.find((tpc: Topic) => tpc.slug === mainTopicSlug)
const mainTopicSlug = article?.main_topic || ''
const mainTopic = article?.topics?.find((tpc: Topic) => tpc.slug === mainTopicSlug)
const mainTopicTitle =
mainTopicSlug && lng === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || ''
@ -111,8 +111,8 @@ export const ArticleCard = (props: ArticleCardProps) => {
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true)
const description = getDescription(props.article.body)
const aspectRatio = () => LAYOUT_ASPECT[props.article.layout]
const description = getDescription(props.article?.body)
const aspectRatio = () => LAYOUT_ASPECT[props.article?.layout]
const [mainTopicTitle, mainTopicSlug] = getMainTopicTitle(props.article, lang())
const { title, subtitle } = getTitleAndSubtitle(props.article)
@ -328,7 +328,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
<Popover content={t('Edit')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => (
<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-hover"

View File

@ -1,7 +1,4 @@
.feedArticlePopup {
box-shadow: none !important;
border: 1px solid rgb(0 0 0 / 15%);
border-radius: 1.6rem;
padding: 0 !important;
text-align: left;
overflow: hidden;

View File

@ -4,21 +4,20 @@ import { For, Show, createSignal } from 'solid-js'
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize'
import { useSeen } from '../../../context/seen'
import { Author } from '../../../graphql/schema/core.gen'
import { router, useRouter } from '../../../stores/router'
import { useArticlesStore } from '../../../stores/zine/articles'
import { useSeenStore } from '../../../stores/zine/seen'
import { Userpic } from '../../Author/Userpic'
import { Icon } from '../../_shared/Icon'
import styles from './Sidebar.module.scss'
export const Sidebar = () => {
const { t } = useLocalize()
const { seen } = useSeenStore()
const { seen } = useSeen()
const { subscriptions } = useFollowing()
const { page } = useRouter()
const { articlesByTopic } = useArticlesStore()
const { articlesByTopic, articlesByAuthor } = useArticlesStore()
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
const checkTopicIsSeen = (topicSlug: string) => {
@ -26,8 +25,9 @@ export const Sidebar = () => {
}
const checkAuthorIsSeen = (authorSlug: string) => {
return Boolean(seen()[authorSlug])
return articlesByAuthor()[authorSlug]?.every((article) => Boolean(seen()[article.slug]))
}
return (
<div class={styles.sidebar}>
<ul class={styles.feedFilters}>

View File

@ -16,6 +16,9 @@ type Props = {
onBlur?: (value: string) => void
variant?: 'login' | 'registration'
disableAutocomplete?: boolean
noValidate?: boolean
onFocus?: () => void
value?: string
}
const minLength = 8
@ -27,7 +30,7 @@ export const PasswordField = (props: Props) => {
const [showPassword, setShowPassword] = createSignal(false)
const [error, setError] = createSignal<string>()
const validatePassword = (passwordToCheck) => {
const validatePassword = (passwordToCheck: string) => {
if (passwordToCheck.length < minLength) {
return t('Password should be at least 8 characters')
}
@ -50,6 +53,7 @@ export const PasswordField = (props: Props) => {
}
props.onInput(value)
if (!props.noValidate) {
const errorValue = validatePassword(value)
if (errorValue) {
setError(errorValue)
@ -57,6 +61,7 @@ export const PasswordField = (props: Props) => {
setError()
}
}
}
createEffect(
on(
@ -78,6 +83,8 @@ export const PasswordField = (props: Props) => {
id="password"
name="password"
disabled={props.disabled}
onFocus={props.onFocus}
value={props.value ? props.value : ''}
autocomplete={props.disableAutocomplete ? 'one-time-code' : 'current-password'}
type={showPassword() ? 'text' : 'password'}
placeholder={props.placeholder || t('Password')}

View File

@ -48,7 +48,7 @@ export const Header = (props: Props) => {
const { page } = useRouter()
const { requireAuthentication } = useSession()
const { searchParams } = useRouter<HeaderSearchParams>()
const { topics } = useTopics()
const { sortedTopics: topics } = useTopics()
const [randomTopics, setRandomTopics] = createSignal([])
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
const [getIsScrolled, setIsScrolled] = createSignal(false)
@ -334,7 +334,7 @@ export const Header = (props: Props) => {
<Show when={props.title}>
<div
class={clsx(styles.articleControls, 'col-auto', {
// FIXME: use or remove [styles.articleControlsAuthorized]: session()?.user?.id,
[styles.articleControlsAuthorized]: session()?.user?.id,
})}
>
<SharePopup

View File

@ -34,7 +34,7 @@ export const HeaderAuth = (props: Props) => {
const { page } = useRouter()
const { session, author, isSessionLoaded } = useSession()
const { unreadNotificationsCount, showNotificationsPanel } = useNotifications()
const { form, toggleEditorPanel, saveShout, publishShout } = useEditorContext()
const { form, toggleEditorPanel, publishShout } = useEditorContext()
const handleBellIconClick = (event: Event) => {
event.preventDefault()
@ -59,10 +59,6 @@ export const HeaderAuth = (props: Props) => {
toggleEditorPanel()
}
const _handleSaveButtonClick = () => {
saveShout(form)
}
const [width, setWidth] = createSignal(0)
const [editorMode, setEditorMode] = createSignal(t('Editing'))
@ -150,7 +146,6 @@ export const HeaderAuth = (props: Props) => {
{editorMode()}
</span>
}
variant="bordered"
popupCssClass={styles.editorPopup}
>
<ul class={clsx('nodash', styles.editorModesList)}>
@ -239,7 +234,12 @@ export const HeaderAuth = (props: Props) => {
}
>
<Show when={!isSaveButtonVisible()}>
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
<div
class={clsx(
styles.userControlItem,
// styles.userControlItemInbox
)}
>
<a href={getPagePath(router, 'inbox')}>
<div classList={{ entered: page().path === '/inbox' }}>
<Icon name="inbox-white" class={styles.icon} />

View File

@ -5,8 +5,10 @@ import { getPagePath } from '@nanostores/router'
import { useLocalize } from '../../context/localize'
import { useSession } from '../../context/session'
import { router } from '../../stores/router'
import { Icon } from '../_shared/Icon'
import { Popup } from '../_shared/Popup'
import { clsx } from 'clsx'
import styles from '../_shared/Popup/Popup.module.scss'
type ProfilePopupProps = Omit<PopupProps, 'children'>
@ -16,30 +18,53 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
const { t } = useLocalize()
return (
<Popup {...props} horizontalAnchor="right" variant="bordered">
<Popup {...props} horizontalAnchor="right" popupCssClass={styles.profilePopup}>
<ul class="nodash">
<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>
<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>
<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')}
</a>
</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>
<a href="#">{t('Bookmarks')}</a>
<a class={styles.action} href="#">
<Icon name="bookmark" class={styles.icon} />
{t('Bookmarks')}
</a>
</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 class={styles.topBorderItem}>
<span class="link" onClick={() => signOut()}>
<span class={clsx(styles.action, 'link')} onClick={() => signOut()}>
<Icon name="logout" class={styles.icon} />
{t('Logout')}
</span>
</li>

View File

@ -3,7 +3,10 @@
color: #fff;
font-size: 2rem;
font-weight: 500;
left: 0;
transition: background-color 0.3s;
position: absolute;
width: 100%;
&.error {
background-color: #d00820;

View File

@ -20,6 +20,8 @@ import { useLocalize } from '../../context/localize'
import { useProfileForm } from '../../context/profile'
import { useSession } from '../../context/session'
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 { clone } from '../../utils/clone'
import { getImageUrl } from '../../utils/getImageUrl'
@ -35,14 +37,12 @@ import { Loading } from '../_shared/Loading'
import { Popover } from '../_shared/Popover'
import { SocialNetworkInput } from '../_shared/SocialNetworkInput'
import styles from '../../pages/profile/Settings.module.scss'
const SimplifiedEditor = lazy(() => import('../../components/Editor/SimplifiedEditor'))
const GrowingTextarea = lazy(() => import('../../components/_shared/GrowingTextarea/GrowingTextarea'))
export const ProfileSettings = () => {
const { t } = useLocalize()
const [prevForm, setPrevForm] = createStore({})
const [prevForm, setPrevForm] = createStore<ProfileInput>({})
const [isFormInitialized, setIsFormInitialized] = createSignal(false)
const [isSaving, setIsSaving] = createSignal(false)
const [social, setSocial] = createSignal([])
@ -59,6 +59,7 @@ export const ProfileSettings = () => {
const { showSnackbar } = useSnackbar()
const { loadAuthor, session } = useSession()
const { showConfirm } = useConfirm()
const [clearAbout, setClearAbout] = createSignal(false)
createEffect(() => {
if (Object.keys(form).length > 0 && !isFormInitialized()) {
@ -121,7 +122,9 @@ export const ProfileSettings = () => {
declineButtonVariant: 'secondary',
})
if (isConfirmed) {
setClearAbout(true)
setForm(clone(prevForm))
setClearAbout(false)
}
}
@ -171,11 +174,13 @@ export const ProfileSettings = () => {
on(
() => deepEqual(form, prevForm),
() => {
if (Object.keys(prevForm).length > 0) {
setIsFloatingPanelVisible(!deepEqual(form, prevForm))
}
},
{ defer: true },
),
)
const handleDeleteSocialLink = (link) => {
updateFormField('links', link, true)
}
@ -317,6 +322,8 @@ export const ProfileSettings = () => {
<h4>{t('About')}</h4>
<SimplifiedEditor
resetToInitial={clearAbout()}
noLimits={true}
variant="bordered"
onlyBubbleControls={true}
smallHeight={true}

View File

@ -1,8 +1,8 @@
import type { Author } from '../../../graphql/schema/core.gen'
import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx'
import { For, Show, createMemo, createSignal } from 'solid-js'
import { Meta } from '../../../context/meta'
import { useLocalize } from '../../../context/localize'
import { useRouter } from '../../../stores/router'

View File

@ -1,11 +1,11 @@
import type { Topic } from '../../../graphql/schema/core.gen'
import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Meta } from '../../../context/meta'
import { useTopics } from '../../../context/topics'
import { useRouter } from '../../../stores/router'
import { setTopicsSort, useTopicsStore } from '../../../stores/zine/topics'
import { capitalize } from '../../../utils/capitalize'
import { dummyFilter } from '../../../utils/dummyFilter'
import { getImageUrl } from '../../../utils/getImageUrl'
@ -33,11 +33,7 @@ export const AllTopics = (props: Props) => {
const [limit, setLimit] = createSignal(PAGE_SIZE)
const ALPHABET =
lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ#'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ#']
const { sortedTopics } = useTopicsStore({
topics: props.topics,
sortBy: searchParams().by || 'shouts',
})
const { sortedTopics, setTopicsSort } = useTopics()
createEffect(() => {
if (!searchParams().by) {

View File

@ -1,12 +1,12 @@
import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/core.gen'
import { getPagePath } from '@nanostores/router'
import { Meta, Title } from '@solidjs/meta'
import { clsx } from 'clsx'
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize'
import { Meta, Title } from '../../../context/meta'
import { useSession } from '../../../context/session'
import { apiClient } from '../../../graphql/client/core'
import { router, useRouter } from '../../../stores/router'
@ -86,7 +86,7 @@ export const AuthorView = (props: Props) => {
setFollowing([...(authors || []), ...(topics || [])])
setFollowers(followersResult || [])
console.debug('[components.Author] following data loaded', subscriptionsResult)
console.info('[components.Author] data loaded')
} catch (error) {
console.error('[components.Author] fetch error', error)
}
@ -243,7 +243,7 @@ export const AuthorView = (props: Props) => {
class={styles.longBio}
classList={{ [styles.longBioExpanded]: isBioExpanded() }}
>
<div ref={(el) => (bioContainerRef.current = el)} innerHTML={author().about} />
<div ref={(el) => (bioContainerRef.current = el)} innerHTML={author()?.about || ''} />
</div>
<Show when={showExpandBioControl()}>

View File

@ -192,7 +192,7 @@ export const Expo = (props: Props) => {
)}
</For>
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
<ArticleCardSwiper title={t('Top month articles')} slides={reactedTopMonthArticles()} />
<ArticleCardSwiper title={t('Top month')} slides={reactedTopMonthArticles()} />
</Show>
<For each={expoShouts().slice(LOAD_MORE_PAGE_SIZE, LOAD_MORE_PAGE_SIZE * 2)}>
{(shout) => (

View File

@ -1,18 +1,18 @@
import { getPagePath } from '@nanostores/router'
import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Meta } from '../../../context/meta'
import { useReactions } from '../../../context/reactions'
import { useSession } from '../../../context/session'
import { useTopics } from '../../../context/topics'
import { apiClient } from '../../../graphql/client/core'
import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../../graphql/schema/core.gen'
import { router, useRouter } from '../../../stores/router'
import { showModal } from '../../../stores/ui'
import { resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles'
import { useTopAuthorsStore } from '../../../stores/zine/topAuthors'
import { useTopicsStore } from '../../../stores/zine/topics'
import { getImageUrl } from '../../../utils/getImageUrl'
import { byCreated } from '../../../utils/sortby'
import { CommentDate } from '../../Article/CommentDate'
@ -103,7 +103,7 @@ export const FeedView = (props: Props) => {
const { session } = useSession()
const { loadReactionsBy } = useReactions()
const { sortedArticles } = useArticlesStore()
const { topTopics } = useTopicsStore()
const { topTopics } = useTopics()
const { topAuthors } = useTopAuthorsStore()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [topComments, setTopComments] = createSignal<Reaction[]>([])

View File

@ -2,6 +2,7 @@ import { getPagePath } from '@nanostores/router'
import { For, Show, createMemo, createSignal, onMount } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useTopics } from '../../context/topics'
import { Shout, Topic } from '../../graphql/schema/core.gen'
import { router } from '../../stores/router'
import {
@ -11,7 +12,6 @@ import {
useArticlesStore,
} from '../../stores/zine/articles'
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
import { useTopicsStore } from '../../stores/zine/topics'
import { capitalize } from '../../utils/capitalize'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages'
@ -46,7 +46,7 @@ export const HomeView = (props: Props) => {
shouts: props.shouts,
})
const { topTopics } = useTopicsStore()
const { topTopics } = useTopics()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const { topAuthors } = useTopAuthorsStore()
const { t } = useLocalize()
@ -110,7 +110,7 @@ export const HomeView = (props: Props) => {
nodate={true}
/>
<Show when={topMonthArticles()}>
<ArticleCardSwiper title={t('Top month articles')} slides={topMonthArticles()} />
<ArticleCardSwiper title={t('Top month')} slides={topMonthArticles()} />
</Show>
<Row2 articles={sortedArticles().slice(10, 12)} nodate={true} />
<RowShort articles={sortedArticles().slice(12, 16)} />

View File

@ -6,11 +6,11 @@ import { createStore } from 'solid-js/store'
import { ShoutForm, useEditorContext } from '../../../context/editor'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { useTopics } from '../../../context/topics'
import { Topic } from '../../../graphql/schema/core.gen'
import { UploadedFile } from '../../../pages/types'
import { router } from '../../../stores/router'
import { hideModal, showModal } from '../../../stores/ui'
import { loadAllTopics, useTopicsStore } from '../../../stores/zine/topics'
import { TopicSelect, UploadModalContent } from '../../Editor'
import { Modal } from '../../Nav/Modal'
import { Button } from '../../_shared/Button'
@ -53,13 +53,13 @@ const emptyConfig = {
export const PublishSettings = (props: Props) => {
const { t } = useLocalize()
const { author } = useSession()
const { sortedTopics } = useTopicsStore()
const { sortedTopics } = useTopics()
const { showSnackbar } = useSnackbar()
const [topics, setTopics] = createSignal<Topic[]>(sortedTopics())
const composeDescription = () => {
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, ' ')
return shorten(leadText, DESCRIPTION_MAX_LENGTH).trim()
}
@ -82,7 +82,6 @@ export const PublishSettings = (props: Props) => {
onMount(() => {
setSettingsForm(initialData())
loadAllTopics()
})
createEffect(() => setTopics(sortedTopics()))

View File

@ -1,14 +1,14 @@
import { LoadShoutsOptions, Shout, Topic } from '../../graphql/schema/core.gen'
import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import { Meta } from '../../context/meta'
import { useLocalize } from '../../context/localize'
import { useTopics } from '../../context/topics'
import { useRouter } from '../../stores/router'
import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors'
import { useTopicsStore } from '../../stores/zine/topics'
import { capitalize } from '../../utils/capitalize'
import { getImageUrl } from '../../utils/getImageUrl'
import { getDescription } from '../../utils/meta'
@ -43,7 +43,7 @@ export const TopicView = (props: Props) => {
const { searchParams, changeSearchParams } = useRouter<TopicsPageSearchParams>()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
const { topicEntities } = useTopics()
const { authorsByTopic } = useAuthorsStore()
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
@ -216,7 +216,7 @@ export const TopicView = (props: Props) => {
wrapper={'author'}
/>
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
<ArticleCardSwiper title={t('Top month articles')} slides={reactedTopMonthArticles()} />
<ArticleCardSwiper title={t('Top month')} slides={reactedTopMonthArticles()} />
</Show>
<Beside
beside={sortedArticles()[12]}

View File

@ -5,6 +5,7 @@ import { For, Show, createSignal } from 'solid-js'
import { Popup } from '../Popup'
import popupStyles from '../Popup/Popup.module.scss'
import styles from './DropDown.module.scss'
export type Option = {
@ -56,16 +57,22 @@ export const DropDown = <TOption extends Option = Option>(props: Props<TOption>)
onVisibilityChange={(isVisible) => setIsPopupVisible(isVisible)}
{...props.popupProps}
>
<ul class="nodash">
<For each={props.options}>
{(option) => (
<div
class={clsx('link', { [styles.active]: props.currentOption.value === option.value })}
<li>
<button
class={clsx(popupStyles.action, {
[styles.active]: props.currentOption.value === option.value,
})}
onClick={() => props.onChange(option)}
>
{option.title}
</div>
</button>
</li>
)}
</For>
</ul>
</Popup>
</Show>
)

View File

@ -1,7 +1,7 @@
import type { JSX } from 'solid-js'
import { Link } from '@solidjs/meta'
import { splitProps } from 'solid-js'
import { Link } from '../../../context/meta'
import { getImageUrl } from '../../../utils/getImageUrl'

View File

@ -1,8 +1,8 @@
import type { JSX } from 'solid-js'
import { Title } from '@solidjs/meta'
import { clsx } from 'clsx'
import { Show, createEffect, createSignal } from 'solid-js'
import { Title } from '../../context/meta'
import { Footer } from '../Discours/Footer'
import { Header } from '../Nav/Header'

View File

@ -8,10 +8,14 @@
.popup {
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);
cursor: default;
min-width: 144px;
opacity: 1;
overflow: hidden;
position: absolute;
text-align: left;
top: calc(100% + 11px);
@ -21,6 +25,7 @@
margin-bottom: 0;
li {
margin: 0;
position: relative;
&: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 {
@include font-size(1.4rem);
box-shadow: 0 4px 60px rgb(0 0 0 / 10%);
padding: 1rem;
ul li {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
.action {
padding: 0.5rem 1rem;
}
li:first-child .action {
padding-top: 1rem;
}
li:last-child .action {
padding-bottom: 1rem;
}
}
@ -75,7 +66,6 @@
.topBorderItem {
border-top: 2px solid;
padding-top: 1em;
}
a:link,
@ -83,26 +73,64 @@
border: none;
white-space: nowrap;
&:hover {
.icon img {
filter: invert(0);
}
&::before {
content: '';
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
}
.icon {
display: inline-block;
width: 3.6rem;
margin-right: 1rem;
width: 2.4rem;
img {
display: inline-block;
filter: invert(1);
max-height: 2rem;
max-width: 2rem;
max-height: 2.4rem;
max-width: 2.4rem;
transition: filter 0.3s;
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

View File

@ -14,7 +14,7 @@ export type PopupProps = {
children: JSX.Element
onVisibilityChange?: (isVisible: boolean) => void
horizontalAnchor?: HorizontalAnchor
variant?: 'bordered' | 'tiny'
variant?: 'tiny'
closePopup?: boolean
}
@ -54,7 +54,6 @@ export const Popup = (props: PopupProps) => {
class={clsx(styles.popup, props.popupCssClass, {
[styles.horizontalAnchorCenter]: horizontalAnchor === 'center',
[styles.horizontalAnchorRight]: horizontalAnchor === 'right',
[styles.bordered]: props.variant === 'bordered',
[styles.tiny]: props.variant === 'tiny',
})}
>

View File

@ -7,9 +7,6 @@
white-space: nowrap;
&:hover {
background: #000;
color: #fff;
.icon img {
filter: invert(0);
}
@ -17,7 +14,7 @@
.icon {
display: inline-block;
width: 3.6rem;
width: 2rem;
img {
display: inline-block;

View File

@ -7,6 +7,7 @@ import { useSnackbar } from '../../../context/snackbar'
import { Icon } from '../Icon'
import { Popover } from '../Popover'
import popupStyles from '../Popup/Popup.module.scss'
import styles from './ShareLinks.module.scss'
type Props = {
@ -53,26 +54,42 @@ export const ShareLinks = (props: Props) => {
<div class={clsx(styles.ShareLinks, props.class, { [styles.inModal]: props.variant === 'inModal' })}>
<ul class="nodash">
<li>
<button role="button" class={styles.shareControl} onClick={() => handleShare(FACEBOOK)}>
<Icon name="facebook-white" class={styles.icon} />
<button
role="button"
class={clsx(styles.shareControl, popupStyles.action)}
onClick={() => handleShare(FACEBOOK)}
>
<Icon name="facebook-white" class={clsx(styles.icon, popupStyles.icon)} />
Facebook
</button>
</li>
<li>
<button role="button" class={styles.shareControl} onClick={() => handleShare(TWITTER)}>
<Icon name="twitter-white" class={styles.icon} />
<button
role="button"
class={clsx(styles.shareControl, popupStyles.action)}
onClick={() => handleShare(TWITTER)}
>
<Icon name="twitter-white" class={clsx(styles.icon, popupStyles.icon)} />
Twitter
</button>
</li>
<li>
<button role="button" class={styles.shareControl} onClick={() => handleShare(TELEGRAM)}>
<Icon name="telegram-white" class={styles.icon} />
<button
role="button"
class={clsx(styles.shareControl, popupStyles.action)}
onClick={() => handleShare(TELEGRAM)}
>
<Icon name="telegram-white" class={clsx(styles.icon, popupStyles.icon)} />
Telegram
</button>
</li>
<li>
<button role="button" class={styles.shareControl} onClick={() => handleShare(VK)}>
<Icon name="vk-white" class={styles.icon} />
<button
role="button"
class={clsx(styles.shareControl, popupStyles.action)}
onClick={() => handleShare(VK)}
>
<Icon name="vk-white" class={clsx(styles.icon, popupStyles.icon)} />
VK
</button>
</li>
@ -80,8 +97,12 @@ export const ShareLinks = (props: Props) => {
<Show
when={props.variant === 'inModal'}
fallback={
<button role="button" class={styles.shareControl} onClick={copyLink}>
<Icon name="link-white" class={styles.icon} />
<button
role="button"
class={clsx(styles.shareControl, popupStyles.action)}
onClick={copyLink}
>
<Icon name="link-white" class={clsx(styles.icon, popupStyles.icon)} />
{t('Copy link')}
</button>
}
@ -93,7 +114,7 @@ export const ShareLinks = (props: Props) => {
<Popover content={t('Copy link')}>
{(triggerRef: (el) => void) => (
<div class={styles.copyButton} onClick={copyLink} ref={triggerRef}>
<Icon name="copy" class={styles.icon} />
<Icon name="copy" class={clsx(styles.icon, popupStyles.icon)} />
</div>
)}
</Popover>

View File

@ -10,6 +10,8 @@ import { ShowOnlyOnClient } from '../ShowOnlyOnClient'
import { SwiperRef } from './swiper'
import { Row1 } from '../../Feed/Row1'
import { Row2 } from '../../Feed/Row2'
import styles from './Swiper.module.scss'
type Props = {
@ -21,19 +23,29 @@ export const ArticleCardSwiper = (props: Props) => {
const mainSwipeRef: { current: SwiperRef } = { current: null }
onMount(async () => {
if (props.slides.length > 1) {
const { register } = await import('swiper/element/bundle')
register()
SwiperCore.use([Pagination, Navigation, Manipulation])
}
})
return (
<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}>
<h2 class={styles.sliderTitle}>{props.title}</h2>
</Show>
<div class={styles.container}>
<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}>
<swiper-container
ref={(el) => (mainSwipeRef.current = el)}
@ -86,6 +98,8 @@ export const ArticleCardSwiper = (props: Props) => {
</div>
</div>
</Show>
</Show>
</Show>
</div>
</div>
</ShowOnlyOnClient>

View File

@ -57,7 +57,7 @@
margin: 0;
position: relative;
& > swiper-container {
&>swiper-container {
display: flex;
flex-direction: row;
gap: 10px;

275
src/context/meta.tsx Normal file
View 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} />
)

29
src/context/seen.tsx Normal file
View 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>
}

View File

@ -13,6 +13,7 @@ import {
LoginInput,
ResendVerifyEmailInput,
SignupInput,
UpdateProfileInput,
VerifyEmailInput,
} from '@authorizerdev/authorizer-js'
import {
@ -58,6 +59,7 @@ export type SessionContextType = {
) => void
signUp: (params: SignupInput) => Promise<{ data: AuthToken; errors: Error[] }>
signIn: (params: LoginInput) => Promise<{ data: AuthToken; errors: Error[] }>
updateProfile: (params: UpdateProfileInput) => Promise<{ data: AuthToken; errors: Error[] }>
signOut: () => Promise<void>
oauth: (provider: string) => Promise<void>
forgotPassword: (
@ -223,9 +225,12 @@ export const SessionProvider = (props: {
const appdata = session()?.user.app_data
if (appdata) {
const { profile } = appdata
if (profile?.id) {
setAuthor(profile)
addAuthors([profile])
if (!profile) loadAuthor()
} else {
setTimeout(loadAuthor, 15)
}
}
} catch (e) {
console.error(e)
@ -305,6 +310,8 @@ export const SessionProvider = (props: {
}
const signUp = async (params: SignupInput) => await authenticate(authorizer().signup, 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 authResult: ApiResponse<GenericResponse> = await authorizer().logout()
@ -381,6 +388,7 @@ export const SessionProvider = (props: {
signIn,
signOut,
confirmEmail,
updateProfile,
setIsSessionLoaded,
setSession,
setAuthor,

View File

@ -1,10 +1,19 @@
import { createLazyMemo } from '@solid-primitives/memo'
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 { Topic } from '../graphql/schema/core.gen'
import { useRouter } from '../stores/router'
import { byTopicStatDesc } from '../utils/sortby'
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>()
@ -25,11 +34,13 @@ const setupIndexedDB = async () => {
})
}
const getTopicsFromIndexedDB = (db) => {
const getTopicsFromIndexedDB = async (db) => {
const tx = db.transaction(STORE_NAME, 'readonly')
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 tx = db.transaction(STORE_NAME, 'readwrite')
const store = tx.objectStore(STORE_NAME)
@ -40,20 +51,93 @@ const saveTopicsToIndexedDB = async (db, topics) => {
}
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 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 () => {
const db = await setupIndexedDB()
let topics = getTopicsFromIndexedDB(db)
setDb(db)
let { topics, timestamp } = await getTopicsFromIndexedDB(db)
if (topics.length === 0) {
topics = await apiClient.getAllTopics()
await saveTopicsToIndexedDB(db, topics)
if (topics.length < 100 || Date.now() - timestamp > 3600000) {
const newTopics = await loadTopics()
await saveTopicsToIndexedDB(db, newTopics)
topics = newTopics
}
addTopics(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>
}

View File

@ -1,6 +1,5 @@
import type {
Author,
AuthorFollowsResult,
CommonResult,
FollowingEntity,
LoadShoutsOptions,
@ -134,7 +133,7 @@ export const apiClient = {
slug?: string
author_id?: number
user?: string
}): Promise<AuthorFollowsResult> => {
}): Promise<CommonResult> => {
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
return response.data.get_author_follows
},

View File

@ -3,6 +3,7 @@ import { gql } from '@urql/core'
export default gql`
query GetAuthorFollows($slug: String, $user: String, $author_id: Int) {
get_author_follows(slug: $slug, user: $user, author_id: $author_id) {
error
authors {
id
slug

View File

@ -1,4 +1,4 @@
import { Meta } from '@solidjs/meta'
import { Meta } from '../../context/meta'
import { StaticPage } from '../../components/Views/StaticPage'
import { useLocalize } from '../../context/localize'

View File

@ -1,4 +1,4 @@
import { Meta } from '@solidjs/meta'
import { Meta } from '../../context/meta'
import { StaticPage } from '../../components/Views/StaticPage'
import { useLocalize } from '../../context/localize'

View File

@ -1,4 +1,4 @@
import { Meta } from '@solidjs/meta'
import { Meta } from '../../context/meta'
import { StaticPage } from '../../components/Views/StaticPage'
import { useLocalize } from '../../context/localize'

View File

@ -1,4 +1,4 @@
import { Meta } from '@solidjs/meta'
import { Meta } from '../../context/meta'
import { Donate } from '../../components/Discours/Donate'
import { StaticPage } from '../../components/Views/StaticPage'

View File

@ -1,4 +1,4 @@
import { Meta } from '@solidjs/meta'
import { Meta } from '../../context/meta'
import { Feedback } from '../../components/Discours/Feedback'
import { Modal } from '../../components/Nav/Modal'

View File

@ -1,4 +1,4 @@
import { Meta } from '@solidjs/meta'
import { Meta } from '../../context/meta'
import { StaticPage } from '../../components/Views/StaticPage'
import { useLocalize } from '../../context/localize'

View File

@ -1,4 +1,4 @@
import { Meta } from '@solidjs/meta'
import { Meta } from '../../context/meta'
import { StaticPage } from '../../components/Views/StaticPage'
import { useLocalize } from '../../context/localize'

View File

@ -1,4 +1,4 @@
import { Meta } from '@solidjs/meta'
import { Meta } from '../../context/meta'
import { StaticPage } from '../../components/Views/StaticPage'
import { useLocalize } from '../../context/localize'

View File

@ -1,4 +1,4 @@
import { Meta } from '@solidjs/meta'
import { Meta } from '../../context/meta'
import { StaticPage } from '../../components/Views/StaticPage'
import { useLocalize } from '../../context/localize'

View File

@ -1,29 +1,15 @@
import type { PageProps } from './types'
import { createSignal, onMount } from 'solid-js'
import { AllTopics } from '../components/Views/AllTopics'
import { PageLayout } from '../components/_shared/PageLayout'
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 [isLoaded, setIsLoaded] = createSignal<boolean>(Boolean(props.allTopics))
onMount(async () => {
if (isLoaded()) {
return
}
await loadAllTopics()
setIsLoaded(true)
})
const { sortedTopics } = useTopics()
return (
<PageLayout title={t('Themes and plots')}>
<AllTopics isLoaded={isLoaded()} topics={props.allTopics} />
<AllTopics isLoaded={!!sortedTopics()?.length} topics={sortedTopics()} />
</PageLayout>
)
}

View File

@ -1,6 +1,6 @@
import { redirectPage } from '@nanostores/router'
import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx'
import { Meta } from '../context/meta'
import { AuthGuard } from '../components/AuthGuard'
import { Button } from '../components/_shared/Button'
@ -18,7 +18,7 @@ import styles from '../styles/Create.module.scss'
const handleCreate = async (layout: LayoutType) => {
const shout = await apiClient.createArticle({ article: { layout: layout } })
redirectPage(router, 'edit', {
shoutId: shout.id.toString(),
shoutId: shout?.id.toString(),
})
}

View File

@ -2,13 +2,12 @@ import type { PageProps } from './types'
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 { PageLayout } from '../components/_shared/PageLayout'
import { useLocalize } from '../context/localize'
import { ReactionsProvider } from '../context/reactions'
import { loadShouts, resetSortedArticles } from '../stores/zine/articles'
import { loadRandomTopics } from '../stores/zine/topics'
export const HomePage = (props: PageProps) => {
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.homeShouts))
@ -19,10 +18,7 @@ export const HomePage = (props: PageProps) => {
return
}
await Promise.all([
loadShouts({ filters: { featured: true }, limit: PRERENDERED_ARTICLES_COUNT }),
loadRandomTopics({ amount: RANDOM_TOPICS_COUNT }),
])
await loadShouts({ filters: { featured: true }, limit: PRERENDERED_ARTICLES_COUNT })
setIsLoaded(true)
})

View File

@ -100,17 +100,6 @@ h5 {
}
}
.passwordToggleControl {
position: absolute;
right: 1em;
transform: translateY(-50%);
top: 50%;
}
.passwordInput {
padding-right: 3em !important;
}
.searchContainer {
margin-top: 2.4rem;
}
@ -331,3 +320,12 @@ div[data-lastpass-infield="true"] {
opacity: 0 !important;
}
.emailValidationError {
position: absolute;
top: 100%;
font-size: 12px;
line-height: 16px;
margin-top: 0.3em;
color: var(--danger-color);
}

View File

@ -6,14 +6,143 @@ import { Icon } from '../../components/_shared/Icon'
import { PageLayout } from '../../components/_shared/PageLayout'
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'
type FormField = 'oldPassword' | 'newPassword' | 'newPasswordConfirm' | 'email'
export const ProfileSecurityPage = () => {
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 (
<PageLayout title={t('Profile')}>
<AuthGuard>
<Show when={isSessionLoaded()} fallback={<Loading />}>
<div class="wide-container">
<div class="row">
<div class="col-md-5">
@ -25,63 +154,86 @@ export const ProfileSecurityPage = () => {
<div class="col-md-19">
<div class="row">
<div class="col-md-20 col-lg-18 col-xl-16">
<h1>Вход и&nbsp;безопасность</h1>
<p class="description">Настройки аккаунта, почты, пароля и&nbsp;способов входа.</p>
<h1>{t('Login and security')}</h1>
<p class="description">
{t('Settings for account, email, password and login methods.')}
</p>
<form>
<h4>Почта</h4>
<div class="pretty-form__item">
<input type="text" name="email" id="email" placeholder="Почта" />
<label for="email">Почта</label>
</div>
<h4>Изменить пароль</h4>
<h5>Текущий пароль</h5>
<h4>{t('Email')}</h4>
<div class="pretty-form__item">
<input
type="text"
name="password-current"
id="password-current"
class={clsx(styles.passwordInput, 'nolabel')}
name="email"
id="email"
disabled={isSubmitting()}
value={formData()['email'] || ''}
placeholder={t('Email')}
onFocus={() => setEmailError()}
onInput={(event) => handleChangeEmail(event.target.value)}
/>
<button type="button" class={styles.passwordToggleControl}>
<Icon name="password-hide" />
</button>
<label for="email">{t('Email')}</label>
<Show when={emailError()}>
<div
class={clsx(styles.emailValidationError, {
'form-message--error': emailError(),
})}
>
{emailError()}
</div>
</Show>
</div>
<h5>Новый пароль</h5>
<div class="pretty-form__item">
<input
type="password"
name="password-new"
id="password-new"
class={clsx(styles.passwordInput, 'nolabel')}
<h4>{t('Change password')}</h4>
<h5>{t('Current password')}</h5>
<div ref={(el) => (oldPasswordRef.current = el)}>
<PasswordField
onFocus={() => setOldPasswordError()}
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>
<h5>Подтвердите новый пароль</h5>
<div class="pretty-form__item">
<input
type="password"
name="password-new-confirm"
id="password-new-confirm"
class={clsx(styles.passwordInput, 'nolabel')}
<h5>{t('New password')}</h5>
<PasswordField
onInput={(value) => {
handleInputChange('newPassword', value)
handleInputChange('newPasswordConfirm', '')
}}
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>
<div class="pretty-form__item">
<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} />
Привязать
{t('Connect')}
</button>
</p>
</div>
@ -89,9 +241,12 @@ export const ProfileSecurityPage = () => {
<h5>VK</h5>
<div class="pretty-form__item">
<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} />
Привязать
{t('Connect')}
</button>
</p>
</div>
@ -99,9 +254,12 @@ export const ProfileSecurityPage = () => {
<h5>Facebook</h5>
<div class="pretty-form__item">
<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} />
Привязать
{t('Connect')}
</button>
</p>
</div>
@ -118,23 +276,51 @@ export const ProfileSecurityPage = () => {
type="button"
>
<Icon name="apple" class={styles.icon} />
Привязать
{t('Connect')}
</button>
</p>
</div>
<br />
<p>
<button class="button button--submit" type="submit">
Сохранить настройки
</button>
</p>
</form>
</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>
</PageLayout>
)

View File

@ -8,7 +8,6 @@ import { PageLayout } from '../components/_shared/PageLayout'
import { ReactionsProvider } from '../context/reactions'
import { useRouter } from '../stores/router'
import { loadShouts, resetSortedArticles } from '../stores/zine/articles'
import { loadTopic } from '../stores/zine/topics'
export const TopicPage = (props: PageProps) => {
const { page } = useRouter()
@ -25,7 +24,6 @@ export const TopicPage = (props: PageProps) => {
limit: PRERENDERED_ARTICLES_COUNT,
offset: 0,
}),
loadTopic({ slug: slug() }),
])
onMount(async () => {

View File

@ -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,
}
}

View File

@ -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 }
}

View File

@ -1,6 +1,7 @@
import { Shout } from '../graphql/schema/core.gen'
const MAX_DESCRIPTION_LENGTH = 150
export const getDescription = (body: string): string => {
if (!body) {
return ''

View File

@ -1,7 +1,7 @@
import ssrPlugin from 'vike/plugin'
import { defineConfig, splitVendorChunkPlugin } from 'vite'
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 solidPlugin from 'vite-plugin-solid'