parent
e6b1550056
commit
cd83807204
363
package-lock.json
generated
363
package-lock.json
generated
|
@ -13,11 +13,14 @@
|
|||
"@aws-sdk/client-s3": "3.303.0",
|
||||
"@aws-sdk/lib-storage": "3.303.0",
|
||||
"@hocuspocus/provider": "2.0.6",
|
||||
"@rnwonder/solid-date-picker": "0.7.7",
|
||||
"@solid-primitives/media": "2.2.3",
|
||||
"@thisbeyond/solid-dnd": "0.7.4",
|
||||
"form-data": "4.0.0",
|
||||
"formidable": "2.1.1",
|
||||
"i18next": "22.4.15",
|
||||
"mailgun.js": "8.2.1",
|
||||
"music-metadata-browser": "2.5.10",
|
||||
"node-fetch": "3.3.1",
|
||||
"solid-popper": "0.3.0",
|
||||
"typograf": "7.1.0"
|
||||
|
@ -5612,6 +5615,17 @@
|
|||
"integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@rnwonder/solid-date-picker": {
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@rnwonder/solid-date-picker/-/solid-date-picker-0.7.7.tgz",
|
||||
"integrity": "sha512-GZDd0zNJNfQoUF18oxdsv6Vo2/7G/zFH2xMrJUg4gBe9tqhT0MLBzAUr+ZRBBmViFVYfanSc2iCLN81xXsMOIg==",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.6.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.25.24",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||
|
@ -5827,6 +5841,14 @@
|
|||
"solid-js": ">=1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@thisbeyond/solid-dnd": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.4.tgz",
|
||||
"integrity": "sha512-jgV9EtR3gAtVsILG8p1OAGrhHIgnK4W04YxpyLgJRCDKEFYQWuDrMdUe8F5Kc6pcVXlC4IMXr4cB8fS2Ut3/Ow==",
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@thisbeyond/solid-select": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz",
|
||||
|
@ -6269,6 +6291,11 @@
|
|||
"@tiptap/core": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tokenizer/token": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
|
@ -6975,6 +7002,17 @@
|
|||
"integrity": "sha512-IqnKIDWfXBJkvy/k6tzskWTc2NK3LcqHlb+KHGCrjOCH4jfQckRX0NAiIcC/vIqQkzLYw2r2CTSwAxcrtcD6lA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/abstract-leveldown": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz",
|
||||
|
@ -8305,6 +8343,14 @@
|
|||
"upper-case": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
||||
|
@ -8509,7 +8555,6 @@
|
|||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
|
@ -9973,6 +10018,14 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
|
@ -10231,6 +10284,22 @@
|
|||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-type": {
|
||||
"version": "16.5.4",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
|
||||
"integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
|
||||
"dependencies": {
|
||||
"readable-web-to-node-stream": "^3.0.0",
|
||||
"strtok3": "^6.2.4",
|
||||
"token-types": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
|
@ -15960,6 +16029,14 @@
|
|||
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/meow": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
|
||||
|
@ -16223,8 +16300,82 @@
|
|||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/music-metadata": {
|
||||
"version": "7.13.4",
|
||||
"resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-7.13.4.tgz",
|
||||
"integrity": "sha512-eRRoEMhhYdth2Ws24FmkvIqrtkIBE9sqjHbrRNpkg2Iux3zc37PQKRv2/r/mTtELb7XlB1uWC2UcKKX7BzNMGA==",
|
||||
"dependencies": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.3.4",
|
||||
"file-type": "^16.5.4",
|
||||
"media-typer": "^1.1.0",
|
||||
"strtok3": "^6.3.0",
|
||||
"token-types": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/music-metadata-browser": {
|
||||
"version": "2.5.10",
|
||||
"resolved": "https://registry.npmjs.org/music-metadata-browser/-/music-metadata-browser-2.5.10.tgz",
|
||||
"integrity": "sha512-03UnAmsSJoZZ5kK2BnEnd2zpH8LXRWQ6xlc7akKudhc2d9FT+yAiqapnmOzjW3g4cxxvIsSK5MVBO2Gi+Ymjfw==",
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3",
|
||||
"debug": "^4.3.4",
|
||||
"music-metadata": "^7.13.3",
|
||||
"readable-stream": "^4.3.0",
|
||||
"readable-web-to-node-stream": "^3.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/music-metadata-browser/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/music-metadata-browser/node_modules/readable-stream": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz",
|
||||
"integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "0.0.8",
|
||||
|
@ -16845,6 +16996,18 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/peek-readable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
|
||||
"integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
|
@ -17189,6 +17352,14 @@
|
|||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
|
@ -17679,6 +17850,21 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-web-to-node-stream": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
|
||||
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
|
||||
"dependencies": {
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
|
@ -18812,6 +18998,22 @@
|
|||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
|
||||
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
|
||||
},
|
||||
"node_modules/strtok3": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
|
||||
"integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
|
||||
"dependencies": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"peek-readable": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/style-search": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
|
||||
|
@ -19456,6 +19658,22 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/token-types": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
|
||||
"integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
|
||||
"dependencies": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"ieee754": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz",
|
||||
|
@ -24799,6 +25017,12 @@
|
|||
"integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==",
|
||||
"dev": true
|
||||
},
|
||||
"@rnwonder/solid-date-picker": {
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@rnwonder/solid-date-picker/-/solid-date-picker-0.7.7.tgz",
|
||||
"integrity": "sha512-GZDd0zNJNfQoUF18oxdsv6Vo2/7G/zFH2xMrJUg4gBe9tqhT0MLBzAUr+ZRBBmViFVYfanSc2iCLN81xXsMOIg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@sinclair/typebox": {
|
||||
"version": "0.25.24",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||
|
@ -24980,6 +25204,12 @@
|
|||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@thisbeyond/solid-dnd": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.4.tgz",
|
||||
"integrity": "sha512-jgV9EtR3gAtVsILG8p1OAGrhHIgnK4W04YxpyLgJRCDKEFYQWuDrMdUe8F5Kc6pcVXlC4IMXr4cB8fS2Ut3/Ow==",
|
||||
"requires": {}
|
||||
},
|
||||
"@thisbeyond/solid-select": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz",
|
||||
|
@ -25223,6 +25453,11 @@
|
|||
"prosemirror-view": "^1.28.2"
|
||||
}
|
||||
},
|
||||
"@tokenizer/token": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
|
@ -25806,6 +26041,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"requires": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"abstract-leveldown": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz",
|
||||
|
@ -26788,6 +27031,11 @@
|
|||
"upper-case": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
|
||||
},
|
||||
"convert-source-map": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
||||
|
@ -26947,7 +27195,6 @@
|
|||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
|
@ -28012,6 +28259,11 @@
|
|||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"dev": true
|
||||
},
|
||||
"event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
||||
},
|
||||
"events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
|
@ -28213,6 +28465,16 @@
|
|||
"flat-cache": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"file-type": {
|
||||
"version": "16.5.4",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
|
||||
"integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
|
||||
"requires": {
|
||||
"readable-web-to-node-stream": "^3.0.0",
|
||||
"strtok3": "^6.2.4",
|
||||
"token-types": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
|
@ -32470,6 +32732,11 @@
|
|||
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
|
||||
"dev": true
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="
|
||||
},
|
||||
"meow": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
|
||||
|
@ -32655,8 +32922,56 @@
|
|||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"music-metadata": {
|
||||
"version": "7.13.4",
|
||||
"resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-7.13.4.tgz",
|
||||
"integrity": "sha512-eRRoEMhhYdth2Ws24FmkvIqrtkIBE9sqjHbrRNpkg2Iux3zc37PQKRv2/r/mTtELb7XlB1uWC2UcKKX7BzNMGA==",
|
||||
"requires": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.3.4",
|
||||
"file-type": "^16.5.4",
|
||||
"media-typer": "^1.1.0",
|
||||
"strtok3": "^6.3.0",
|
||||
"token-types": "^4.2.1"
|
||||
}
|
||||
},
|
||||
"music-metadata-browser": {
|
||||
"version": "2.5.10",
|
||||
"resolved": "https://registry.npmjs.org/music-metadata-browser/-/music-metadata-browser-2.5.10.tgz",
|
||||
"integrity": "sha512-03UnAmsSJoZZ5kK2BnEnd2zpH8LXRWQ6xlc7akKudhc2d9FT+yAiqapnmOzjW3g4cxxvIsSK5MVBO2Gi+Ymjfw==",
|
||||
"requires": {
|
||||
"buffer": "^6.0.3",
|
||||
"debug": "^4.3.4",
|
||||
"music-metadata": "^7.13.3",
|
||||
"readable-stream": "^4.3.0",
|
||||
"readable-web-to-node-stream": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz",
|
||||
"integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==",
|
||||
"requires": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mute-stream": {
|
||||
"version": "0.0.8",
|
||||
|
@ -33102,6 +33417,11 @@
|
|||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true
|
||||
},
|
||||
"peek-readable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
|
||||
"integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
|
@ -33326,6 +33646,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
|
||||
},
|
||||
"promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
|
@ -33732,6 +34057,14 @@
|
|||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"readable-web-to-node-stream": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
|
||||
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
|
||||
"requires": {
|
||||
"readable-stream": "^3.6.0"
|
||||
}
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
|
@ -34574,6 +34907,15 @@
|
|||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
|
||||
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
|
||||
},
|
||||
"strtok3": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
|
||||
"integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
|
||||
"requires": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"peek-readable": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"style-search": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
|
||||
|
@ -35068,6 +35410,15 @@
|
|||
"is-number": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"token-types": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
|
||||
"integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
|
||||
"requires": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"totalist": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz",
|
||||
|
|
5
public/icons/datepicker.svg
Normal file
5
public/icons/datepicker.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.3">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 5H13V6.6H17V5H19V6.6H21H23V8.6V20.6V22.6H21H9H7V20.6V8.6V6.6H9H11V5ZM17 8.6V9.8H19V8.6H21V11.4H9V8.6H11V9.8H13V8.6H17ZM9 13.4V20.6H21V13.4H9Z" fill="black"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 337 B |
9
public/icons/expand-circle.svg
Normal file
9
public/icons/expand-circle.svg
Normal file
|
@ -0,0 +1,9 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="insert_link">
|
||||
<circle id="Ellipse 13" opacity="0.5" cx="16" cy="16" r="16" fill="black"/>
|
||||
<g id="Group">
|
||||
<path id="Vector" d="M9 22.9999H13.3243V21.7027H11.2054L15.4 17.5297L14.4704 16.6001L10.2974 20.7947V18.6758H9.00019L9 22.9999Z" fill="white"/>
|
||||
<path id="Vector_2" d="M17.5744 15.3568L21.7041 11.2054V13.3243H23.0013V9H18.677V10.2972H20.7959L16.6445 14.4269L17.5744 15.3568Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 515 B |
10
public/icons/list.svg
Normal file
10
public/icons/list.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="List plus">
|
||||
<g id="Group">
|
||||
<path id="Vector" d="M4.5 2.99988V20.9999H19.5V8.39988L13.875 2.99988H4.5ZM17.625 19.1999H6.375V4.79988H12V10.1999H17.625V19.1999ZM13.8751 8.39988V5.54563L16.8483 8.39988H13.8751Z" fill="#141414"/>
|
||||
<path id="Vector_2" d="M8.25 8.39978H10.125V10.1998H8.25V8.39978Z" fill="#141414"/>
|
||||
<path id="Vector_3" d="M8.25 11.9999H15.75V13.7999H8.25V11.9999Z" fill="#141414"/>
|
||||
<path id="Vector_4" d="M8.25 15.6H15.75V17.4H8.25V15.6Z" fill="#141414"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 586 B |
5
public/icons/pencil-stroke.svg
Normal file
5
public/icons/pencil-stroke.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="bookmark">
|
||||
<path id="Vector" d="M19.0195 4.90048C17.8189 3.69984 15.7378 3.69984 14.4571 4.90048L6.13272 13.2249C6.05265 13.3049 5.97259 13.465 5.97259 13.545L4.0516 18.9079C3.81152 19.5481 4.45181 20.1886 5.09209 19.9484L10.3749 17.9473C10.455 17.9473 10.615 17.7872 10.695 17.7872L19.0194 9.38272C20.3001 8.10213 20.3001 6.10098 19.0194 4.90035L19.0195 4.90048ZM6.13272 17.7873L7.01321 15.2259C7.97378 16.1865 7.6535 15.8662 8.61406 16.8267L6.13272 17.7873ZM17.8989 8.26226L10.0548 16.1064L7.81367 13.8653L15.6578 6.02113C16.2981 5.38085 17.3387 5.38085 17.8989 6.02113C18.5393 6.58137 18.5393 7.62197 17.8989 8.26226V8.26226Z" fill="black"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 760 B |
|
@ -3,13 +3,16 @@
|
|||
"About myself": "About myself",
|
||||
"About the project": "About the project",
|
||||
"Add another image": "Add another image",
|
||||
"Add audio": "Add audio",
|
||||
"Add comment": "Comment",
|
||||
"Add cover": "Add cover",
|
||||
"Add image": "Add image",
|
||||
"Add images": "Add images",
|
||||
"Add link": "Add link",
|
||||
"Add signature": "Add signature",
|
||||
"Add url": "Add url",
|
||||
"Address on Discourse": "Address on Discourse",
|
||||
"Album name": "Название aльбома",
|
||||
"Alignment center": "Alignment center",
|
||||
"Alignment left": "Alignment left",
|
||||
"Alignment right": "Alignment right",
|
||||
|
@ -18,6 +21,7 @@
|
|||
"All posts": "All posts",
|
||||
"All topics": "All topics",
|
||||
"Almost done! Check your email.": "Almost done! Just checking your email.",
|
||||
"Artist": "Artist",
|
||||
"Artworks": "Artworks",
|
||||
"Audio": "Audio",
|
||||
"Author": "Author",
|
||||
|
@ -62,9 +66,12 @@
|
|||
"Create account from follow": "Create an account to subscribe",
|
||||
"Create account from subscribe": "Create an account to subscribe to new publications",
|
||||
"Create account from vote": "Create an account to vote",
|
||||
"Create gallery": "Create gallery",
|
||||
"Create post": "Create post",
|
||||
"Create video": "Create video",
|
||||
"Date of Birth": "Date of Birth",
|
||||
"Delete": "Delete",
|
||||
"Description": "Description...",
|
||||
"Discours": "Discours",
|
||||
"Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects",
|
||||
"Discours is created with our common effort": "Discours exists because of our common effort",
|
||||
|
@ -101,6 +108,7 @@
|
|||
"Forgot password?": "Forgot your password?",
|
||||
"Forward": "Forward",
|
||||
"Full name": "First and last name",
|
||||
"Gallery name": "Gallery name",
|
||||
"Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine",
|
||||
"Go to main page": "Go to main page",
|
||||
"Group Chat": "Group Chat",
|
||||
|
@ -162,6 +170,7 @@
|
|||
"My feed": "My feed",
|
||||
"My subscriptions": "Subscriptions",
|
||||
"Name": "Name",
|
||||
"New literary work": "New literary work",
|
||||
"New only": "New only",
|
||||
"New password": "New password",
|
||||
"New stories every day and even more!": "New stories and more are waiting for you every day!",
|
||||
|
@ -199,6 +208,7 @@
|
|||
"Profile": "Profile",
|
||||
"Profile settings": "Profile settings",
|
||||
"Publications": "Publications",
|
||||
"Publish Album": "Publish Album",
|
||||
"Publish Settings": "Publish Settings",
|
||||
"Punchline": "Punchline",
|
||||
"Quit": "Quit",
|
||||
|
@ -226,9 +236,12 @@
|
|||
"Share": "Share",
|
||||
"Short opening": "Short opening",
|
||||
"Show": "Show",
|
||||
"Show lyrics": "Текст песни",
|
||||
"Social networks": "Social networks",
|
||||
"Something went wrong, check email and password": "Something went wrong. Check your email and password",
|
||||
"Something went wrong, please try again": "Something went wrong, please try again",
|
||||
"Song lyrics": "Song lyrics...",
|
||||
"Song title": "Song title",
|
||||
"Sorry, this address is already taken, please choose another one.": "Sorry, this address is already taken, please choose another one",
|
||||
"Special projects": "Special projects",
|
||||
"Specify the source and the name of the author": "Specify the source and the name of the author",
|
||||
|
@ -268,6 +281,7 @@
|
|||
"Unfollow the topic": "Unfollow the topic",
|
||||
"Unnamed draft": "Unnamed draft",
|
||||
"Upload": "Upload",
|
||||
"Upload error": "Upload error",
|
||||
"Upload video": "Upload video",
|
||||
"Username": "Username",
|
||||
"Userpic": "Userpic",
|
||||
|
@ -290,6 +304,7 @@
|
|||
"Write message": "Write a message",
|
||||
"Write to us": "Write to us",
|
||||
"You are subscribed": "You are subscribed",
|
||||
"You can download multiple tracks at once in .mp3, .wav or .flac formats": "You can download multiple tracks at once in .mp3, .wav or .flac formats",
|
||||
"You were successfully authorized": "You were successfully authorized",
|
||||
"You've confirmed email": "You've confirmed email",
|
||||
"You've reached a non-existed page": "You've reached a non-existed page",
|
||||
|
@ -322,8 +337,10 @@
|
|||
"images": "images",
|
||||
"invalid password": "invalid password",
|
||||
"italic": "italic",
|
||||
"jpg, .png, max. 10 mb.": "jpg, .png, макс. 10 мб.",
|
||||
"literature": "literature",
|
||||
"marker list": "marker list",
|
||||
"min. 1400×1400 pix": "мин. 1400×1400 пикс.",
|
||||
"music": "music",
|
||||
"my feed": "my ribbon",
|
||||
"number list": "number list",
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
"About myself": "О себе",
|
||||
"About the project": "О проекте",
|
||||
"Accomplices": "Соучастники",
|
||||
"Accomplices": "Соучастники",
|
||||
"Add another image": "Добавить другое изображение",
|
||||
"Add audio": "Добавить аудио",
|
||||
"Add comment": "Комментировать",
|
||||
"Add cover": "Добавить обложку",
|
||||
"Add image": "Добавить изображение",
|
||||
"Add images": "Добавить изображения",
|
||||
"Add link": "Добавить ссылку",
|
||||
|
@ -14,6 +15,7 @@
|
|||
"Add to bookmarks": "Добавить в закладки",
|
||||
"Add url": "Добавить ссылку",
|
||||
"Address on Discourse": "Адрес на Дискурсе",
|
||||
"Album name": "Название альбома",
|
||||
"Alignment center": "По центру",
|
||||
"Alignment left": "По левому краю",
|
||||
"Alignment right": "По правому краю",
|
||||
|
@ -22,6 +24,8 @@
|
|||
"All posts": "Все публикации",
|
||||
"All topics": "Все темы",
|
||||
"Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.",
|
||||
"Artist": "Исполнитель",
|
||||
"Artist...": "Исполнитель...",
|
||||
"Artworks": "Артворки",
|
||||
"Audio": "Аудио",
|
||||
"Author": "Автор",
|
||||
|
@ -66,9 +70,12 @@
|
|||
"Create account from follow": "Создайте аккаунт, чтобы подписаться",
|
||||
"Create account from subscribe": "Создайте аккаунт для подписки на новые публикации",
|
||||
"Create account from vote": "Создайте аккаунт, чтобы голосовать",
|
||||
"Create gallery": "Создать галерею",
|
||||
"Create post": "Создать публикацию",
|
||||
"Create video": "Создать видео",
|
||||
"Date of Birth": "Дата рождения",
|
||||
"Delete": "Удалить",
|
||||
"Description": "Описание...",
|
||||
"Discours": "Дискурс",
|
||||
"Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Дискурс — это интеллектуальная среда, веб-пространство и инструменты, которые позволяют авторам сотрудничать с читателями и объединяться для совместного создания публикаций и медиапроектов",
|
||||
"Discours is created with our common effort": "Дискурс существует благодаря нашему общему вкладу",
|
||||
|
@ -106,6 +113,8 @@
|
|||
"Forgot password?": "Забыли пароль?",
|
||||
"Forward": "Переслать",
|
||||
"Full name": "Имя и фамилия",
|
||||
"Gallery name": "Название галереи",
|
||||
"Genre...": "Жанр...",
|
||||
"Get notifications": "Получать уведомления",
|
||||
"Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале",
|
||||
"Go to main page": "Перейти на главную",
|
||||
|
@ -171,6 +180,7 @@
|
|||
"My feed": "Новое",
|
||||
"My subscriptions": "Подписки",
|
||||
"Name": "Имя",
|
||||
"New literary work": "Новое произведение",
|
||||
"New only": "Только новые",
|
||||
"New password": "Новый пароль",
|
||||
"New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!",
|
||||
|
@ -212,6 +222,7 @@
|
|||
"Publication settings": "Настройки публикации",
|
||||
"Publications": "Публикации",
|
||||
"Publish": "Опубликовать",
|
||||
"Publish Album": "Опубликовать альбом",
|
||||
"Publish Settings": "Настройки публикации",
|
||||
"Punchline": "Панчлайн",
|
||||
"Quit": "Выйти",
|
||||
|
@ -219,6 +230,7 @@
|
|||
"Quotes": "Цитаты",
|
||||
"Reason uknown": "Причина неизвестна",
|
||||
"Recent": "Свежее",
|
||||
"Release date...": "Дата выхода...",
|
||||
"Reply": "Ответить",
|
||||
"Report": "Пожаловаться",
|
||||
"Required": "Поле обязательно для заполнения",
|
||||
|
@ -240,9 +252,12 @@
|
|||
"Share": "Поделиться",
|
||||
"Short opening": "Небольшое вступление, чтобы заинтересовать читателя",
|
||||
"Show": "Показать",
|
||||
"Show lyrics": "Текст песни",
|
||||
"Social networks": "Социальные сети",
|
||||
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль",
|
||||
"Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз",
|
||||
"Song lyrics": "Текст песни...",
|
||||
"Song title": "Название песни",
|
||||
"Sorry, this address is already taken, please choose another one.": "Увы, этот адрес уже занят, выберите другой",
|
||||
"Special projects": "Спецпроекты",
|
||||
"Specify the source and the name of the author": "Укажите источник и имя автора",
|
||||
|
@ -283,6 +298,7 @@
|
|||
"Unfollow the topic": "Отписаться от темы",
|
||||
"Unnamed draft": "Unnamed draft",
|
||||
"Upload": "Загрузить",
|
||||
"Upload error": "Ошибка загрузки",
|
||||
"Upload video": "Загрузить видео",
|
||||
"Username": "Имя пользователя",
|
||||
"Userpic": "Аватар",
|
||||
|
@ -306,6 +322,7 @@
|
|||
"Write message": "Написать сообщение",
|
||||
"Write to us": "Напишите нам",
|
||||
"You are subscribed": "Вы подписаны",
|
||||
"You can download multiple tracks at once in .mp3, .wav or .flac formats": "Можно загрузить сразу несколько треков в форматах .mp3, .wav или .flac",
|
||||
"You was successfully authorized": "Вы были успешно авторизованы",
|
||||
"You've confirmed email": "Вы подтвердили почту",
|
||||
"You've reached a non-existed page": "Вы попали на несуществующую страницу",
|
||||
|
@ -341,8 +358,10 @@
|
|||
"images": "изображения",
|
||||
"invalid password": "некорректный пароль",
|
||||
"italic": "курсив",
|
||||
"jpg, .png, max. 10 mb.": "jpg, .png, макс. 10 мб.",
|
||||
"literature": "литература",
|
||||
"marker list": "маркир. список",
|
||||
"min. 1400×1400 pix": "мин. 1400×1400 пикс.",
|
||||
"music": "музыка",
|
||||
"my feed": "моя лента",
|
||||
"number list": "нумер. список",
|
||||
|
|
|
@ -291,8 +291,9 @@ img {
|
|||
}
|
||||
|
||||
.shoutStatsItem {
|
||||
align-items: center;
|
||||
@include font-size(1.5rem);
|
||||
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
margin: 0 6% 1em 0;
|
||||
|
|
79
src/components/Article/AudioHeader/AudioHeader.module.scss
Normal file
79
src/components/Article/AudioHeader/AudioHeader.module.scss
Normal file
|
@ -0,0 +1,79 @@
|
|||
.AudioHeader {
|
||||
overflow: hidden;
|
||||
margin-bottom: 32px;
|
||||
|
||||
.albumInfo {
|
||||
margin-right: 224px;
|
||||
|
||||
.topic {
|
||||
.link {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
color: var(--blue-link);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > h1 {
|
||||
margin: 16px 0 0;
|
||||
}
|
||||
|
||||
.artistData {
|
||||
margin: 18px 0 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.item {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
font-weight: 500;
|
||||
padding: 2px 12px;
|
||||
border-left: 2px solid #e9e9ee;
|
||||
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cover {
|
||||
display: block;
|
||||
position: relative;
|
||||
float: right;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
background: var(--placeholder-color-semi) url('icons/create-music.svg') no-repeat 50% 50%;
|
||||
|
||||
.image {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.expand {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.expandedImage {
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.albumInfo {
|
||||
margin-right: 0;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
}
|
53
src/components/Article/AudioHeader/AudioHeader.tsx
Normal file
53
src/components/Article/AudioHeader/AudioHeader.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './AudioHeader.module.scss'
|
||||
import { imageProxy } from '../../../utils/imageProxy'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Topic } from '../../../graphql/types.gen'
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import { router } from '../../../stores/router'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
cover?: string
|
||||
artistData?: MediaItem
|
||||
topic: Topic
|
||||
}
|
||||
|
||||
export const AudioHeader = (props: Props) => {
|
||||
const [expandedImage, setExpandedImage] = createSignal(false)
|
||||
return (
|
||||
<div class={clsx(styles.AudioHeader, { [styles.expandedImage]: expandedImage() })}>
|
||||
<div class={styles.cover}>
|
||||
<img class={styles.image} src={imageProxy(props.cover)} alt={props.title} />
|
||||
<Show when={props.cover}>
|
||||
<button type="button" class={styles.expand} onClick={() => setExpandedImage(!expandedImage())}>
|
||||
<Icon name="expand-circle" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<div class={styles.albumInfo}>
|
||||
<Show when={props.topic}>
|
||||
<div class={styles.topic}>
|
||||
<a href={getPagePath(router, 'topic', { slug: props.topic.slug })} class={styles.link}>
|
||||
{props.topic.title}
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
<h1>{props.title}</h1>
|
||||
<div class={styles.artistData}>
|
||||
<Show when={props.artistData.artist}>
|
||||
<div class={styles.item}>{props.artistData.artist}</div>
|
||||
</Show>
|
||||
<Show when={props.artistData.date}>
|
||||
<div class={styles.item}>{props.artistData.date}</div>
|
||||
</Show>
|
||||
<Show when={props.artistData.genre}>
|
||||
<div class={styles.item}>{props.artistData.genre}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/Article/AudioHeader/index.ts
Normal file
1
src/components/Article/AudioHeader/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { AudioHeader } from './AudioHeader'
|
|
@ -46,20 +46,20 @@
|
|||
margin-top: 20px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.playButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.playButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #141414;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #141414;
|
||||
|
||||
& img {
|
||||
width: 14px;
|
||||
height: auto;
|
||||
& img {
|
||||
width: 14px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,22 +113,25 @@
|
|||
|
||||
.progressFilled {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 4px solid #141414;
|
||||
border-bottom: 4px solid var(--default-color);
|
||||
transition: width 0.3s linear;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
bottom: -10px;
|
||||
right: -8px;
|
||||
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid #141414;
|
||||
background-color: #fff;
|
||||
border: 4px solid var(--default-color);
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,7 +230,7 @@
|
|||
flex-direction: column;
|
||||
|
||||
list-style-type: none;
|
||||
margin: 32px 0 58px;
|
||||
margin: 32px 0 16px;
|
||||
padding: 0;
|
||||
|
||||
& > li {
|
||||
|
@ -243,6 +246,11 @@
|
|||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.playlistItemPlayButton {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
@ -250,18 +258,42 @@
|
|||
height: auto;
|
||||
}
|
||||
|
||||
.playlistItemTitle {
|
||||
max-width: 254px;
|
||||
.playlistItemText {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 16px;
|
||||
gap: 16px;
|
||||
color: var(--default-color);
|
||||
|
||||
margin-left: 17px;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.01em;
|
||||
color: #000000;
|
||||
.artist,
|
||||
.title {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
overflow: hidden;
|
||||
max-width: calc(50% - 16px);
|
||||
text-overflow: ellipsis;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
font-weight: 400;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.playlistItemControls {
|
||||
|
@ -275,7 +307,7 @@
|
|||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.01em;
|
||||
color: #000000;
|
||||
color: var(--default-color);
|
||||
}
|
||||
|
||||
.timelinePlaceholder {
|
||||
|
@ -293,6 +325,29 @@
|
|||
height: 67px;
|
||||
}
|
||||
|
||||
.shareMedia {
|
||||
.actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.descriptionBlock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 8px 0 24px 0;
|
||||
|
||||
.description,
|
||||
.lyrics {
|
||||
@include font-size(1.4rem);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-weight: 500;
|
||||
|
||||
& > textarea::placeholder {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,54 +1,63 @@
|
|||
import { createEffect, createSignal, onMount, Show } from 'solid-js'
|
||||
|
||||
import { createEffect, createSignal, on, onMount, Show } from 'solid-js'
|
||||
import { PlayerHeader } from './PlayerHeader'
|
||||
import { PlayerPlaylist } from './PlayerPlaylist'
|
||||
|
||||
import styles from './AudioPlayer.module.scss'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
|
||||
export type MediaItem = {
|
||||
id?: number
|
||||
body: string
|
||||
pic: string
|
||||
title: string
|
||||
url: string
|
||||
isCurrent: boolean
|
||||
isPlaying: boolean
|
||||
export type Audio = {
|
||||
pic?: string
|
||||
index?: number
|
||||
isCurrent?: boolean
|
||||
isPlaying?: boolean
|
||||
} & MediaItem
|
||||
|
||||
type Props = {
|
||||
media: Audio[]
|
||||
articleSlug?: string
|
||||
body?: string
|
||||
editorMode?: boolean
|
||||
onAudioChange?: (index: number, field: string, value: string) => void
|
||||
}
|
||||
|
||||
const prepareMedia = (media: MediaItem[]) =>
|
||||
const prepareMedia = (media: Audio[]) =>
|
||||
media.map((item, index) => ({
|
||||
...item,
|
||||
id: index,
|
||||
index: index,
|
||||
isCurrent: false,
|
||||
isPlaying: false
|
||||
}))
|
||||
|
||||
const progressUpdate = (audioRef, progressFilledRef, duration) => {
|
||||
progressFilledRef.style.width = `${(audioRef.currentTime / duration) * 100 || 0}%`
|
||||
progressFilledRef.current.style.width = `${(audioRef.current.currentTime / duration) * 100 || 0}%`
|
||||
}
|
||||
|
||||
const scrub = (event, progressRef, duration, audioRef) => {
|
||||
audioRef.currentTime = (event.offsetX / progressRef.offsetWidth) * duration
|
||||
audioRef.current.currentTime = (event.offsetX / progressRef.current.offsetWidth) * duration
|
||||
}
|
||||
|
||||
const getFormattedTime = (point) => new Date(point * 1000).toISOString().slice(14, -5)
|
||||
|
||||
export default (props: { media: MediaItem[]; articleSlug: string; body: string }) => {
|
||||
let audioRef: HTMLAudioElement
|
||||
let progressRef: HTMLDivElement
|
||||
let progressFilledRef: HTMLDivElement
|
||||
export const AudioPlayer = (props: Props) => {
|
||||
const audioRef: { current: HTMLAudioElement } = { current: null }
|
||||
const progressRef: { current: HTMLDivElement } = { current: null }
|
||||
const progressFilledRef: { current: HTMLDivElement } = { current: null }
|
||||
|
||||
const [audioContext, setAudioContext] = createSignal<AudioContext>()
|
||||
const [gainNode, setGainNode] = createSignal<GainNode>()
|
||||
|
||||
const [tracks, setTracks] = createSignal<MediaItem[] | null>(prepareMedia(props.media))
|
||||
|
||||
const [tracks, setTracks] = createSignal<Audio[] | null>(prepareMedia(props.media))
|
||||
const [duration, setDuration] = createSignal<number>(0)
|
||||
const [currentTimeContent, setCurrentTimeContent] = createSignal<string>('00:00')
|
||||
const [currentDurationContent, setCurrentDurationContent] = createSignal<string>('00:00')
|
||||
|
||||
const [mousedown, setMousedown] = createSignal<boolean>(false)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.media,
|
||||
() => {
|
||||
setTracks(prepareMedia(props.media))
|
||||
}
|
||||
)
|
||||
)
|
||||
const getCurrentTrack = () =>
|
||||
tracks().find((track) => track.isCurrent) ||
|
||||
(() => {
|
||||
|
@ -63,10 +72,10 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
|
|||
})()
|
||||
|
||||
createEffect(() => {
|
||||
if (audioRef.src !== getCurrentTrack().url) {
|
||||
audioRef.src = getCurrentTrack().url
|
||||
if (audioRef.current.src !== getCurrentTrack().url) {
|
||||
audioRef.current.src = getCurrentTrack().url
|
||||
|
||||
audioRef.load()
|
||||
audioRef.current.load()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -76,33 +85,33 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
|
|||
}
|
||||
})
|
||||
|
||||
const playMedia = async (m: MediaItem) => {
|
||||
const playMedia = async (m: Audio) => {
|
||||
setTracks(
|
||||
tracks().map((track) => ({
|
||||
...track,
|
||||
isCurrent: track.id === m.id,
|
||||
isPlaying: track.id === m.id ? !track.isPlaying : false
|
||||
isCurrent: track.index === m.index,
|
||||
isPlaying: track.index === m.index ? !track.isPlaying : false
|
||||
}))
|
||||
)
|
||||
|
||||
progressUpdate(audioRef, progressFilledRef, duration())
|
||||
|
||||
if (audioContext().state === 'suspended') audioContext().resume()
|
||||
if (audioContext().state === 'suspended') await audioContext().resume()
|
||||
|
||||
if (getCurrentTrack().isPlaying) {
|
||||
await audioRef.play()
|
||||
await audioRef.current.play()
|
||||
} else {
|
||||
audioRef.pause()
|
||||
audioRef.current.pause()
|
||||
}
|
||||
}
|
||||
|
||||
const setTimes = () => {
|
||||
setCurrentTimeContent(getFormattedTime(audioRef.currentTime))
|
||||
setCurrentTimeContent(getFormattedTime(audioRef.current.currentTime))
|
||||
}
|
||||
|
||||
const handleAudioEnd = () => {
|
||||
progressFilledRef.style.width = '0%'
|
||||
audioRef.currentTime = 0
|
||||
progressFilledRef.current.style.width = '0%'
|
||||
audioRef.current.currentTime = 0
|
||||
}
|
||||
|
||||
const handleAudioTimeUpdate = () => {
|
||||
|
@ -117,42 +126,42 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
|
|||
|
||||
setTimes()
|
||||
|
||||
const track = audioContext().createMediaElementSource(audioRef)
|
||||
const track = audioContext().createMediaElementSource(audioRef.current)
|
||||
track.connect(gainNode()).connect(audioContext().destination)
|
||||
})
|
||||
|
||||
const playPrevTrack = () => {
|
||||
const { id } = getCurrentTrack()
|
||||
const currIndex = tracks().findIndex((track) => track.id === id)
|
||||
const { index } = getCurrentTrack()
|
||||
const currIndex = tracks().findIndex((track) => track.index === index)
|
||||
|
||||
const getUpdatedStatus = (trackId) =>
|
||||
currIndex === 0
|
||||
? trackId === tracks()[tracks().length - 1].id
|
||||
: trackId === tracks()[currIndex - 1].id
|
||||
? trackId === tracks()[tracks().length - 1].index
|
||||
: trackId === tracks()[currIndex - 1].index
|
||||
|
||||
setTracks(
|
||||
tracks().map((track) => ({
|
||||
...track,
|
||||
isCurrent: getUpdatedStatus(track.id),
|
||||
isPlaying: getUpdatedStatus(track.id)
|
||||
isCurrent: getUpdatedStatus(track.index),
|
||||
isPlaying: getUpdatedStatus(track.index)
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
const playNextTrack = () => {
|
||||
const { id } = getCurrentTrack()
|
||||
const currIndex = tracks().findIndex((track) => track.id === id)
|
||||
const { index } = getCurrentTrack()
|
||||
const currIndex = tracks().findIndex((track) => track.index === index)
|
||||
|
||||
const getUpdatedStatus = (trackId) =>
|
||||
currIndex === tracks().length - 1
|
||||
? trackId === tracks()[0].id
|
||||
: trackId === tracks()[currIndex + 1].id
|
||||
? trackId === tracks()[0].index
|
||||
: trackId === tracks()[currIndex + 1].index
|
||||
|
||||
setTracks(
|
||||
tracks().map((track) => ({
|
||||
...track,
|
||||
isCurrent: getUpdatedStatus(track.id),
|
||||
isPlaying: getUpdatedStatus(track.id)
|
||||
isCurrent: getUpdatedStatus(track.index),
|
||||
isPlaying: getUpdatedStatus(track.index)
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
@ -161,6 +170,15 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
|
|||
setDuration(target.duration)
|
||||
}
|
||||
|
||||
const handleAudioDescriptionChange = (index: number, field: string, value) => {
|
||||
props.onAudioChange(index, field, value)
|
||||
setTracks(
|
||||
tracks().map((track, idx) => {
|
||||
return idx === index ? { ...track, [field]: value } : track
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Show when={getCurrentTrack()}>
|
||||
|
@ -177,24 +195,24 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
|
|||
<div class={styles.timeline}>
|
||||
<div
|
||||
class={styles.progress}
|
||||
ref={progressRef}
|
||||
ref={(el) => (progressRef.current = el)}
|
||||
onClick={(e) => scrub(e, progressRef, duration(), audioRef)}
|
||||
onMouseMove={(e) => mousedown() && scrub(e, progressRef, duration(), audioRef)}
|
||||
onMouseDown={() => setMousedown(true)}
|
||||
onMouseUp={() => setMousedown(false)}
|
||||
>
|
||||
<div class={styles.progressFilled} ref={progressFilledRef}></div>
|
||||
<div class={styles.progressFilled} ref={(el) => (progressFilledRef.current = el)} />
|
||||
</div>
|
||||
<div class={styles.progressTiming}>
|
||||
<span>{currentTimeContent()}</span>
|
||||
<span>{currentDurationContent()}</span>
|
||||
</div>
|
||||
<audio
|
||||
ref={audioRef}
|
||||
ref={(el) => (audioRef.current = el)}
|
||||
onTimeUpdate={handleAudioTimeUpdate}
|
||||
onCanPlay={() => {
|
||||
if (getCurrentTrack().isPlaying) {
|
||||
audioRef.play()
|
||||
audioRef.current.play()
|
||||
}
|
||||
}}
|
||||
onLoadedMetadata={handleOnAudioMetadataLoad}
|
||||
|
@ -203,14 +221,15 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
|
|||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={tracks()}>
|
||||
<PlayerPlaylist
|
||||
editorMode={props.editorMode}
|
||||
playMedia={playMedia}
|
||||
tracks={tracks()}
|
||||
getCurrentTrack={getCurrentTrack}
|
||||
currentTrack={getCurrentTrack()}
|
||||
articleSlug={props.articleSlug}
|
||||
body={props.body}
|
||||
onAudioChange={handleAudioDescriptionChange}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -35,29 +35,29 @@ export const PlayerHeader = (props) => {
|
|||
<div class={styles.playerTitle}>{getCurrentTrack().title}</div>
|
||||
<div class={styles.playerControls}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPlayMedia}
|
||||
class={clsx(
|
||||
styles.playButton,
|
||||
getCurrentTrack().isPlaying ? styles.playButtonInvertPause : styles.playButtonInvertPlay
|
||||
)}
|
||||
role="button"
|
||||
aria-label="Play"
|
||||
data-playing="false"
|
||||
>
|
||||
<Icon name={getCurrentTrack().isPlaying ? 'pause' : 'play'} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={playPrevTrack}
|
||||
class={clsx(styles.controlsButton)}
|
||||
role="button"
|
||||
aria-label="Previous"
|
||||
>
|
||||
<Icon name="player-arrow" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={playNextTrack}
|
||||
class={clsx(styles.controlsButton, styles.controlsButtonNext)}
|
||||
role="button"
|
||||
aria-label="Next"
|
||||
>
|
||||
<Icon name="player-arrow" />
|
||||
|
|
|
@ -1,61 +1,159 @@
|
|||
import { For } from 'solid-js'
|
||||
|
||||
import { createEffect, createSignal, For, Show } from 'solid-js'
|
||||
import { SharePopup, getShareUrl } from '../SharePopup'
|
||||
import { getDescription } from '../../../utils/meta'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
||||
import type { MediaItem } from './AudioPlayer'
|
||||
|
||||
import type { Audio } from './AudioPlayer'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import styles from './AudioPlayer.module.scss'
|
||||
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
|
||||
import MD from '../MD'
|
||||
|
||||
export const PlayerPlaylist = (props) => {
|
||||
type Props = {
|
||||
tracks: Audio[]
|
||||
currentTrack: Audio
|
||||
playMedia: (audio: Audio) => void
|
||||
articleSlug?: string
|
||||
body?: string
|
||||
editorMode?: boolean
|
||||
onAudioChange?: (index: number, field: string, value: string) => void
|
||||
}
|
||||
|
||||
export const PlayerPlaylist = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const [activeEditIndex, setActiveEditIndex] = createSignal(-1)
|
||||
|
||||
const { tracks, getCurrentTrack, playMedia, articleSlug, body } = props
|
||||
const toggleDropDown = (index) => {
|
||||
setActiveEditIndex(activeEditIndex() === index ? -1 : index)
|
||||
}
|
||||
const updateData = (key, value) => {
|
||||
props.onAudioChange(activeEditIndex(), key, value)
|
||||
}
|
||||
|
||||
return (
|
||||
<ul class={styles.playlist}>
|
||||
<For each={tracks}>
|
||||
{(m: MediaItem) => (
|
||||
<li class={styles.playlistItem}>
|
||||
<button
|
||||
class={styles.playlistItemPlayButton}
|
||||
onClick={() => playMedia(m)}
|
||||
role="button"
|
||||
aria-label="Play"
|
||||
>
|
||||
<Icon
|
||||
name={
|
||||
getCurrentTrack() && getCurrentTrack().id === m.id && getCurrentTrack().isPlaying
|
||||
? 'pause'
|
||||
: 'play'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
<div class={styles.playlistItemTitle}>{m.title}</div>
|
||||
<div class={styles.shareMedia}>
|
||||
<Popover content={t('Share')}>
|
||||
{(triggerRef: (el) => void) => (
|
||||
<div ref={triggerRef}>
|
||||
<SharePopup
|
||||
title={m.title}
|
||||
description={getDescription(body)}
|
||||
imageUrl={m.pic}
|
||||
shareUrl={getShareUrl({ pathname: `/${articleSlug}` })}
|
||||
trigger={
|
||||
<div>
|
||||
<Icon name="share-media" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
<For each={props.tracks}>
|
||||
{(m: Audio, index) => (
|
||||
<li>
|
||||
<div class={styles.playlistItem}>
|
||||
<button
|
||||
class={styles.playlistItemPlayButton}
|
||||
onClick={() => props.playMedia(m)}
|
||||
type="button"
|
||||
aria-label="Play"
|
||||
>
|
||||
<Icon
|
||||
name={
|
||||
props.currentTrack &&
|
||||
props.currentTrack.index === m.index &&
|
||||
props.currentTrack.isPlaying
|
||||
? 'pause'
|
||||
: 'play'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
<div class={styles.playlistItemText}>
|
||||
<Show
|
||||
when={activeEditIndex() === index() && props.editorMode}
|
||||
fallback={
|
||||
<>
|
||||
<div class={styles.title}>
|
||||
{m.title.replace(/\.(wav|flac|mp3|aac)$/i, '') || t('Song title')}
|
||||
</div>
|
||||
<div class={styles.artist}>{m.artist || t('Artist')}</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={m.title}
|
||||
class={styles.title}
|
||||
placeholder={t('Song title')}
|
||||
onChange={(e) => updateData('title', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={m.artist}
|
||||
class={styles.artist}
|
||||
placeholder={t('Artist')}
|
||||
onChange={(e) => updateData('artist', e.target.value)}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<div class={styles.actions}>
|
||||
<Show when={(m.lyrics || m.body) && !props.editorMode}>
|
||||
<Popover content={t('Show lyrics')}>
|
||||
{(triggerRef: (el) => void) => (
|
||||
<button ref={triggerRef} type="button" onClick={() => toggleDropDown(index())}>
|
||||
<Icon name="list" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
</Show>
|
||||
<Popover content={props.editorMode ? t('Edit') : t('Share')}>
|
||||
{(triggerRef: (el) => void) => (
|
||||
<div ref={triggerRef}>
|
||||
<Show
|
||||
when={!props.editorMode}
|
||||
fallback={
|
||||
<button type="button" onClick={() => toggleDropDown(index())}>
|
||||
<Icon name="pencil-stroke" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<SharePopup
|
||||
title={m.title}
|
||||
description={getDescription(props.body)}
|
||||
imageUrl={m.pic}
|
||||
shareUrl={getShareUrl({ pathname: `/${props.articleSlug}` })}
|
||||
trigger={
|
||||
<div>
|
||||
<Icon name="share-media" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={activeEditIndex() === index()}>
|
||||
<Show
|
||||
when={props.editorMode}
|
||||
fallback={
|
||||
<div class={styles.descriptionBlock}>
|
||||
<Show when={m.body}>
|
||||
<div class={styles.description}>
|
||||
<MD body={m.body} />
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={m.lyrics}>
|
||||
<div class={styles.lyrics}>
|
||||
<MD body={m.lyrics} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class={styles.descriptionBlock}>
|
||||
<GrowingTextarea
|
||||
allowEnterKey={true}
|
||||
class={styles.description}
|
||||
placeholder={t('Description')}
|
||||
value={(value) => updateData('body', value)}
|
||||
initialValue={m.body || ''}
|
||||
/>
|
||||
<GrowingTextarea
|
||||
allowEnterKey={true}
|
||||
class={styles.lyrics}
|
||||
placeholder={t('Song lyrics')}
|
||||
value={(value) => updateData('lyrics', value)}
|
||||
initialValue={m.lyrics || ''}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
|
|
1
src/components/Article/AudioPlayer/index.ts
Normal file
1
src/components/Article/AudioPlayer/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { AudioPlayer } from './AudioPlayer'
|
|
@ -1,7 +1,7 @@
|
|||
import { capitalize, formatDate } from '../../utils'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { AuthorCard } from '../Author/AuthorCard'
|
||||
import AudioPlayer from './AudioPlayer/AudioPlayer'
|
||||
import { AudioPlayer } from './AudioPlayer'
|
||||
import type { Author, Shout } from '../../graphql/types.gen'
|
||||
import MD from './MD'
|
||||
import { SharePopup } from './SharePopup'
|
||||
|
@ -21,20 +21,15 @@ import styles from './Article.module.scss'
|
|||
import { imageProxy } from '../../utils/imageProxy'
|
||||
import { Popover } from '../_shared/Popover'
|
||||
import article from '../Editor/extensions/Article'
|
||||
import { SolidSwiper } from '../_shared/SolidSwiper'
|
||||
import { createEffect, For, createMemo, Match, onMount, Show, Switch, createSignal } from 'solid-js'
|
||||
import { createEffect, For, createMemo, onMount, Show, createSignal, Switch, Match } from 'solid-js'
|
||||
import { MediaItem } from '../../pages/types'
|
||||
import { AudioHeader } from './AudioHeader'
|
||||
|
||||
interface ArticleProps {
|
||||
article: Shout
|
||||
scrollToComments?: boolean
|
||||
}
|
||||
|
||||
interface MediaItem {
|
||||
url?: string
|
||||
title?: string
|
||||
body?: string
|
||||
}
|
||||
|
||||
export const FullArticle = (props: ArticleProps) => {
|
||||
const { t } = useLocalize()
|
||||
const {
|
||||
|
@ -117,40 +112,52 @@ export const FullArticle = (props: ArticleProps) => {
|
|||
<div class="row">
|
||||
<article class="col-md-16 col-lg-14 col-xl-12 offset-md-5">
|
||||
{/*TODO: Check styles.shoutTopic*/}
|
||||
<div class={styles.shoutHeader}>
|
||||
<Show when={mainTopic()}>
|
||||
<div class={styles.shoutTopic}>
|
||||
<a
|
||||
href={getPagePath(router, 'topic', { slug: props.article.mainTopic })}
|
||||
class={styles.mainTopicLink}
|
||||
>
|
||||
{mainTopic().title}
|
||||
</a>
|
||||
<Switch>
|
||||
<Match when={props.article.layout !== 'audio'}>
|
||||
<div class={styles.shoutHeader}>
|
||||
<Show when={mainTopic()}>
|
||||
<div class={styles.shoutTopic}>
|
||||
<a
|
||||
href={getPagePath(router, 'topic', { slug: props.article.mainTopic })}
|
||||
class={styles.mainTopicLink}
|
||||
>
|
||||
{mainTopic().title}
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<h1>{props.article.title}</h1>
|
||||
<Show when={props.article.subtitle}>
|
||||
<h4>{capitalize(props.article.subtitle, false)}</h4>
|
||||
</Show>
|
||||
|
||||
<div class={styles.shoutAuthor}>
|
||||
<For each={props.article.authors}>
|
||||
{(a: Author, index) => (
|
||||
<>
|
||||
<Show when={index() > 0}>, </Show>
|
||||
<a href={getPagePath(router, 'author', { slug: a.slug })}>{a.name}</a>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={props.article.cover && props.article.layout !== 'video'}>
|
||||
<div
|
||||
class={styles.shoutCover}
|
||||
style={{ 'background-image': `url('${imageProxy(props.article.cover)}')` }}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<h1>{props.article.title}</h1>
|
||||
<Show when={props.article.subtitle}>
|
||||
<h4>{capitalize(props.article.subtitle, false)}</h4>
|
||||
</Show>
|
||||
|
||||
<div class={styles.shoutAuthor}>
|
||||
<For each={props.article.authors}>
|
||||
{(a: Author, index) => (
|
||||
<>
|
||||
<Show when={index() > 0}>, </Show>
|
||||
<a href={getPagePath(router, 'author', { slug: a.slug })}>{a.name}</a>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={props.article.cover && props.article.layout !== 'video'}>
|
||||
<div
|
||||
class={styles.shoutCover}
|
||||
style={{ 'background-image': `url('${imageProxy(props.article.cover)}')` }}
|
||||
</Match>
|
||||
<Match when={props.article.layout === 'audio'}>
|
||||
<AudioHeader
|
||||
title={props.article.title}
|
||||
cover={props.article.cover}
|
||||
artistData={media()[0]}
|
||||
topic={mainTopic()}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<Show when={media() && props.article.layout === 'video'}>
|
||||
<div class="media-items">
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
.AudioUploader {
|
||||
display: block;
|
||||
margin-top: 2rem;
|
||||
|
||||
.draggable {
|
||||
margin: 8px 0;
|
||||
padding: 8px 0;
|
||||
&:hover {
|
||||
background: var(--placeholder-color-semi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sortable {
|
||||
background: red;
|
||||
padding: 8px 0;
|
||||
}
|
41
src/components/Editor/AudioUploader/AudioUploader.tsx
Normal file
41
src/components/Editor/AudioUploader/AudioUploader.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './AudioUploader.module.scss'
|
||||
import { DropArea } from '../../_shared/DropArea'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { createEffect, createSignal, on, Show } from 'solid-js'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
import { composeMediaItems } from '../../../utils/composeMediaItems'
|
||||
import { AudioPlayer } from '../../Article/AudioPlayer'
|
||||
import { Buffer } from 'buffer'
|
||||
|
||||
window.Buffer = Buffer
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
audio: MediaItem[]
|
||||
onAudioChange: (index: number, value: MediaItem) => void
|
||||
onAudioAdd: (value: MediaItem[]) => void
|
||||
}
|
||||
|
||||
export const AudioUploader = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
|
||||
const handleAudioDescriptionChange = (index: number, field: string, value) => {
|
||||
props.onAudioChange(index, { ...props.audio[index], [field]: value })
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.AudioUploader, props.class)}>
|
||||
<Show when={props.audio.length > 0}>
|
||||
<AudioPlayer editorMode={true} media={props.audio} onAudioChange={handleAudioDescriptionChange} />
|
||||
</Show>
|
||||
<DropArea
|
||||
isMultiply={true}
|
||||
placeholder={t('Add audio')}
|
||||
description={t('You can download multiple tracks at once in .mp3, .wav or .flac formats')}
|
||||
fileType={'audio'}
|
||||
onUpload={(value) => props.onAudioAdd(composeMediaItems(value))}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/Editor/AudioUploader/index.ts
Normal file
1
src/components/Editor/AudioUploader/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { AudioUploader } from './AudioUploader'
|
|
@ -90,6 +90,34 @@
|
|||
.titleInput {
|
||||
font-weight: 700;
|
||||
}
|
||||
.additional {
|
||||
margin-top: auto;
|
||||
|
||||
.additionalInput {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
font-weight: 600;
|
||||
padding: 0;
|
||||
margin: 14px 0 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.datepicker {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin: 14px 0 0;
|
||||
|
||||
.additionalInput {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grow input
|
||||
|
@ -215,13 +243,36 @@
|
|||
|
||||
.inputContainer {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
.validationError {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 100%;
|
||||
top: calc(100% + 4px);
|
||||
font-size: small;
|
||||
color: #f00;
|
||||
color: var(--danger-color);
|
||||
}
|
||||
}
|
||||
|
||||
.audioHeader {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
|
||||
.inputContainer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 228px;
|
||||
height: 228px;
|
||||
flex-basis: 228px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { Accessor, createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { clsx } from 'clsx'
|
||||
import { Title } from '@solidjs/meta'
|
||||
|
@ -16,9 +16,12 @@ import { hideModal, showModal } from '../../stores/ui'
|
|||
import { imageProxy } from '../../utils/imageProxy'
|
||||
import { GrowingTextarea } from '../_shared/GrowingTextarea'
|
||||
import { VideoUploader } from '../Editor/VideoUploader'
|
||||
import { AudioUploader } from '../Editor/AudioUploader'
|
||||
import { VideoPlayer } from '../_shared/VideoPlayer'
|
||||
import { slugify } from '../../utils/slugify'
|
||||
import { SolidSwiper } from '../_shared/SolidSwiper'
|
||||
import { DropArea } from '../_shared/DropArea'
|
||||
import { LayoutType, MediaItem } from '../../pages/types'
|
||||
|
||||
type Props = {
|
||||
shout: Shout
|
||||
|
@ -66,7 +69,7 @@ export const EditView = (props: Props) => {
|
|||
layout: props.shout.layout
|
||||
})
|
||||
|
||||
const mediaItems = createMemo(() => {
|
||||
const mediaItems: Accessor<MediaItem[]> = createMemo(() => {
|
||||
return JSON.parse(form.media || '[]')
|
||||
})
|
||||
|
||||
|
@ -125,9 +128,9 @@ export const EditView = (props: Props) => {
|
|||
setForm('selectedTopics', newSelectedTopics)
|
||||
}
|
||||
|
||||
const handleAddImages = (data) => {
|
||||
const newImages = [...mediaItems(), ...data]
|
||||
setForm('media', JSON.stringify(newImages))
|
||||
const handleAddMedia = (data) => {
|
||||
const newMedia = [...mediaItems(), ...data]
|
||||
setForm('media', JSON.stringify(newMedia))
|
||||
}
|
||||
const handleSortedImages = (data) => {
|
||||
setForm('media', JSON.stringify(data))
|
||||
|
@ -139,15 +142,54 @@ export const EditView = (props: Props) => {
|
|||
setForm('media', JSON.stringify(copy))
|
||||
}
|
||||
|
||||
const handleImageChange = (index, value) => {
|
||||
const handleMediaChange = (index, value) => {
|
||||
const updated = mediaItems().map((item, idx) => (idx === index ? value : item))
|
||||
setForm('media', JSON.stringify(updated))
|
||||
}
|
||||
|
||||
const handleBaseFieldsChange = (key, value) => {
|
||||
const updated = mediaItems().map((media) => ({ ...media, [key]: value }))
|
||||
setForm('media', JSON.stringify(updated))
|
||||
}
|
||||
|
||||
const articleTitle = () => {
|
||||
switch (props.shout.layout as LayoutType) {
|
||||
case 'audio': {
|
||||
return t('Album name')
|
||||
}
|
||||
case 'image': {
|
||||
return t('Gallery name')
|
||||
}
|
||||
default: {
|
||||
return t('Header')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pageTitle = () => {
|
||||
switch (props.shout.layout as LayoutType) {
|
||||
case 'audio': {
|
||||
return t('Publish Album')
|
||||
}
|
||||
case 'image': {
|
||||
return t('Create gallery')
|
||||
}
|
||||
case 'video': {
|
||||
return t('Create video')
|
||||
}
|
||||
case 'literature': {
|
||||
return t('New literary work')
|
||||
}
|
||||
default: {
|
||||
return t('Write an article')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class={styles.container}>
|
||||
<Title>{t('Write an article')}</Title>
|
||||
<Title>{pageTitle()}</Title>
|
||||
<form>
|
||||
<div class="wide-container">
|
||||
<button
|
||||
|
@ -167,33 +209,88 @@ export const EditView = (props: Props) => {
|
|||
[styles.visible]: page().route === 'edit'
|
||||
})}
|
||||
>
|
||||
<div class={styles.inputContainer}>
|
||||
<GrowingTextarea
|
||||
value={(value) => handleTitleInputChange(value)}
|
||||
class={styles.titleInput}
|
||||
placeholder={t('Header')}
|
||||
initialValue={form.title}
|
||||
maxLength={100}
|
||||
/>
|
||||
<Show when={formErrors.title}>
|
||||
<div class={styles.validationError}>{formErrors.title}</div>
|
||||
<div class={clsx({ [styles.audioHeader]: props.shout.layout === 'audio' })}>
|
||||
<div class={styles.inputContainer}>
|
||||
<GrowingTextarea
|
||||
allowEnterKey={true}
|
||||
value={(value) => handleTitleInputChange(value)}
|
||||
class={styles.titleInput}
|
||||
placeholder={articleTitle()}
|
||||
initialValue={form.title}
|
||||
maxLength={100}
|
||||
/>
|
||||
|
||||
<Show when={formErrors.title}>
|
||||
<div class={styles.validationError}>{formErrors.title}</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.shout.layout === 'audio'}>
|
||||
<div class={styles.additional}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('Artist...')}
|
||||
class={styles.additionalInput}
|
||||
value={mediaItems()[0]?.artist || t('Artist')}
|
||||
onChange={(event) => handleBaseFieldsChange('artist', event.target.value)}
|
||||
/>
|
||||
<input
|
||||
class={styles.additionalInput}
|
||||
placeholder={t('Release date...')}
|
||||
onChange={(event) => handleBaseFieldsChange('date', event.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('Genre...')}
|
||||
class={styles.additionalInput}
|
||||
onChange={(event) => handleBaseFieldsChange('genre', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.shout.layout !== 'audio'}>
|
||||
<GrowingTextarea
|
||||
allowEnterKey={false}
|
||||
value={(value) => setForm('subtitle', value)}
|
||||
class={styles.subtitleInput}
|
||||
placeholder={t('Subheader')}
|
||||
initialValue={form.subtitle}
|
||||
maxLength={100}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<Show
|
||||
when={form.coverImageUrl}
|
||||
fallback={
|
||||
<DropArea
|
||||
isSquare={true}
|
||||
placeholder={t('Add cover')}
|
||||
description={
|
||||
<>
|
||||
{t('min. 1400×1400 pix')}
|
||||
<br />
|
||||
{t('jpg, .png, max. 10 mb.')}
|
||||
</>
|
||||
}
|
||||
isMultiply={false}
|
||||
fileType={'image'}
|
||||
onUpload={(val) => setForm('coverImageUrl', val[0].url)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div
|
||||
class={styles.cover}
|
||||
style={{ 'background-image': `url(${imageProxy(form.coverImageUrl)})` }}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<GrowingTextarea
|
||||
value={(value) => setForm('subtitle', value)}
|
||||
class={styles.subtitleInput}
|
||||
placeholder={t('Subheader')}
|
||||
initialValue={form.subtitle}
|
||||
maxLength={100}
|
||||
/>
|
||||
|
||||
<Show when={props.shout.layout === 'image'}>
|
||||
<SolidSwiper
|
||||
editorMode={true}
|
||||
images={mediaItems()}
|
||||
onImageChange={handleImageChange}
|
||||
onImageChange={handleMediaChange}
|
||||
onImageDelete={(index) => handleImageDelete(index)}
|
||||
onImagesAdd={(value) => handleAddImages(value)}
|
||||
onImagesAdd={(value) => handleAddMedia(value)}
|
||||
onImagesSorted={(value) => handleSortedImages(value)}
|
||||
/>
|
||||
</Show>
|
||||
|
@ -204,7 +301,7 @@ export const EditView = (props: Props) => {
|
|||
fallback={
|
||||
<VideoUploader
|
||||
data={(data) => {
|
||||
handleAddImages(data)
|
||||
handleAddMedia(data)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
@ -224,6 +321,14 @@ export const EditView = (props: Props) => {
|
|||
</Show>
|
||||
</Show>
|
||||
|
||||
<Show when={props.shout.layout === 'audio'}>
|
||||
<AudioUploader
|
||||
audio={mediaItems()}
|
||||
onAudioAdd={(value) => handleAddMedia(value)}
|
||||
onAudioChange={handleMediaChange}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Editor
|
||||
shoutId={props.shout.id}
|
||||
initialContent={props.shout.body}
|
||||
|
|
|
@ -58,6 +58,24 @@
|
|||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
&.square {
|
||||
.field {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
width: 228px;
|
||||
height: 228px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 8px;
|
||||
opacity: 0.3;
|
||||
color: var(--default-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
|
|
|
@ -6,14 +6,16 @@ import { useLocalize } from '../../../context/localize'
|
|||
import { validateFiles } from '../../../utils/validateFile'
|
||||
import type { FileTypeToUpload } from '../../../pages/types'
|
||||
import { handleFileUpload } from '../../../utils/handleFileUpload'
|
||||
import { UploadedFile } from '../../../pages/types'
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
placeholder: string
|
||||
description?: string | JSX.Element
|
||||
fileType: FileTypeToUpload
|
||||
isMultiply: boolean
|
||||
onUpload: (value: string[]) => void
|
||||
fileType: FileTypeToUpload
|
||||
onUpload: (value: UploadedFile[]) => void
|
||||
description?: string | JSX.Element
|
||||
isSquare?: boolean
|
||||
}
|
||||
|
||||
export const DropArea = (props: Props) => {
|
||||
|
@ -26,15 +28,16 @@ export const DropArea = (props: Props) => {
|
|||
try {
|
||||
setLoading(true)
|
||||
|
||||
const results: string[] = []
|
||||
const results: UploadedFile[] = []
|
||||
for (const file of files) {
|
||||
const result = await handleFileUpload(file)
|
||||
results.push(result.url)
|
||||
results.push(result)
|
||||
}
|
||||
props.onUpload(results)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
setDropAreaError('Error')
|
||||
setLoading(false)
|
||||
setDropAreaError(t('Upload error'))
|
||||
console.error('[runUpload]', error)
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +84,7 @@ export const DropArea = (props: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.DropArea, props.class)}>
|
||||
<div class={clsx(styles.DropArea, props.class, props.isSquare && styles['square'])}>
|
||||
<div
|
||||
class={clsx(styles.field, { [styles.active]: dragActive() })}
|
||||
onDragEnter={handleDrag}
|
||||
|
@ -91,11 +94,14 @@ export const DropArea = (props: Props) => {
|
|||
onClick={handleDropFieldClick}
|
||||
>
|
||||
<div class={styles.text}>{loading() ? 'Loading...' : props.placeholder}</div>
|
||||
<Show when={!loading() && props.isSquare && props.description}>
|
||||
<div class={styles.description}>{props.description}</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={dropAreaError()}>
|
||||
<div class={styles.error}>{dropAreaError()}</div>
|
||||
</Show>
|
||||
<Show when={!dropAreaError() && props.description}>
|
||||
<Show when={!dropAreaError() && props.description && !props.isSquare}>
|
||||
<div class={styles.description}>{props.description}</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@ type Props = {
|
|||
initialValue?: string
|
||||
value: (string) => void
|
||||
maxLength?: number
|
||||
allowEnterKey: boolean
|
||||
}
|
||||
|
||||
export const GrowingTextarea = (props: Props) => {
|
||||
|
@ -36,7 +37,7 @@ export const GrowingTextarea = (props: Props) => {
|
|||
autocomplete="off"
|
||||
class={clsx(styles.textInput, props.class)}
|
||||
value={props.initialValue}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyDown={props.allowEnterKey ? handleKeyDown : null}
|
||||
onInput={(event) => handleChangeValue(event)}
|
||||
onChange={(event) => props.value(event.target.value)}
|
||||
placeholder={props.placeholder}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createEffect, createSignal, For, Match, Show, Switch, on } from 'solid-js'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
import { MediaItem, UploadedFile } from '../../../pages/types'
|
||||
import { Icon } from '../Icon'
|
||||
import { Popover } from '../Popover'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
@ -17,6 +17,7 @@ import { Loading } from '../Loading'
|
|||
import { imageProxy } from '../../../utils/imageProxy'
|
||||
import { clsx } from 'clsx'
|
||||
import styles from './Swiper.module.scss'
|
||||
import { composeMediaItems } from '../../../utils/composeMediaItems'
|
||||
|
||||
type Props = {
|
||||
images: MediaItem[]
|
||||
|
@ -27,17 +28,6 @@ type Props = {
|
|||
onImageChange?: (index: number, value: MediaItem) => void
|
||||
}
|
||||
|
||||
const composeMediaItem = (value) => {
|
||||
return value.map((url) => {
|
||||
return {
|
||||
url: url,
|
||||
source: '',
|
||||
title: '',
|
||||
body: ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
register()
|
||||
|
||||
SwiperCore.use([Pagination, Navigation, Manipulation])
|
||||
|
@ -47,7 +37,6 @@ export const SolidSwiper = (props: Props) => {
|
|||
const [loading, setLoading] = createSignal(false)
|
||||
const [slideIndex, setSlideIndex] = createSignal(0)
|
||||
|
||||
const dropAreaRef: { current: HTMLElement } = { current: null }
|
||||
const mainSwipeRef: { current: SwiperRef } = { current: null }
|
||||
const thumbSwipeRef: { current: SwiperRef } = { current: null }
|
||||
|
||||
|
@ -78,8 +67,8 @@ export const SolidSwiper = (props: Props) => {
|
|||
)
|
||||
)
|
||||
|
||||
const handleDropAreaUpload = (value: string[]) => {
|
||||
props.onImagesAdd(composeMediaItem(value))
|
||||
const handleDropAreaUpload = (value: UploadedFile[]) => {
|
||||
props.onImagesAdd(composeMediaItems(value))
|
||||
swipeToUploaded()
|
||||
}
|
||||
|
||||
|
@ -108,7 +97,7 @@ export const SolidSwiper = (props: Props) => {
|
|||
const result = await handleFileUpload(file)
|
||||
results.push(result.url)
|
||||
}
|
||||
props.onImagesAdd(composeMediaItem(results))
|
||||
props.onImagesAdd(composeMediaItems(results))
|
||||
setLoading(false)
|
||||
swipeToUploaded()
|
||||
} catch (error) {
|
||||
|
@ -146,7 +135,6 @@ export const SolidSwiper = (props: Props) => {
|
|||
<div class={styles.container}>
|
||||
<Show when={props.editorMode && props.images.length === 0}>
|
||||
<DropArea
|
||||
ref={(el) => (dropAreaRef.current = el)}
|
||||
fileType="image"
|
||||
isMultiply={true}
|
||||
placeholder={t('Add images')}
|
||||
|
@ -212,6 +200,7 @@ export const SolidSwiper = (props: Props) => {
|
|||
}
|
||||
/>
|
||||
<GrowingTextarea
|
||||
allowEnterKey={true}
|
||||
class={styles.descriptionText}
|
||||
placeholder={t('Enter image description')}
|
||||
initialValue={slide.body}
|
||||
|
|
|
@ -42,10 +42,10 @@ export const CreatePage = () => {
|
|||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
<div class={styles.link} onClick={() => handleCreate('audio')}>
|
||||
<Icon name="create-music" class={styles.icon} />
|
||||
<div>{t('music')}</div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class={styles.link} onClick={() => handleCreate('video')}>
|
||||
|
|
|
@ -34,11 +34,23 @@ export type UploadFile = {
|
|||
|
||||
export type LayoutType = 'article' | 'audio' | 'video' | 'image' | 'literature'
|
||||
|
||||
export type FileTypeToUpload = 'image' | 'video' | 'doc'
|
||||
export type FileTypeToUpload = 'image' | 'video' | 'doc' | 'audio'
|
||||
|
||||
export type AudioDescription = {
|
||||
date?: string
|
||||
genre?: string
|
||||
artist?: string
|
||||
lyrics?: string
|
||||
}
|
||||
|
||||
export type MediaItem = {
|
||||
url: string
|
||||
title: string
|
||||
body: string
|
||||
source?: string
|
||||
} & AudioDescription
|
||||
|
||||
export type UploadedFile = {
|
||||
url: string
|
||||
originalFilename: string
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
--icon-filter: invert(0);
|
||||
--icon-filter-hover: invert(1);
|
||||
--editor-bubble-menu-background: #fff;
|
||||
--blue-link: #2638d9;
|
||||
}
|
||||
|
||||
[data-editor-dark-mode='true'] {
|
||||
|
|
|
@ -246,7 +246,6 @@ export const apiClient = {
|
|||
},
|
||||
createArticle: async ({ article }: { article: ShoutInput }): Promise<Shout> => {
|
||||
const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise()
|
||||
console.log('!!! [createArticle]:', response.data)
|
||||
return response.data.createShout.shout
|
||||
},
|
||||
updateArticle: async ({
|
||||
|
|
10
src/utils/composeMediaItems.ts
Normal file
10
src/utils/composeMediaItems.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export const composeMediaItems = (value) => {
|
||||
return value.map((fileData) => {
|
||||
return {
|
||||
url: fileData.url,
|
||||
source: '',
|
||||
title: fileData.originalFilename,
|
||||
body: ''
|
||||
}
|
||||
})
|
||||
}
|
|
@ -23,6 +23,10 @@ export const validateFiles = (fileType: FileTypeToUpload, files: UploadFile[]):
|
|||
isValid = docExtension ? docExtensions.has(docExtension) : false
|
||||
break
|
||||
}
|
||||
case 'audio': {
|
||||
isValid = file.file.type.startsWith('audio/')
|
||||
break
|
||||
}
|
||||
default: {
|
||||
isValid = false
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user