Feature/audio upload (#120)

* Audio upload 
* Audio player Article View
This commit is contained in:
Ilya Y 2023-07-14 16:06:21 +03:00 committed by GitHub
parent e6b1550056
commit cd83807204
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1222 additions and 237 deletions

363
package-lock.json generated
View File

@ -13,11 +13,14 @@
"@aws-sdk/client-s3": "3.303.0", "@aws-sdk/client-s3": "3.303.0",
"@aws-sdk/lib-storage": "3.303.0", "@aws-sdk/lib-storage": "3.303.0",
"@hocuspocus/provider": "2.0.6", "@hocuspocus/provider": "2.0.6",
"@rnwonder/solid-date-picker": "0.7.7",
"@solid-primitives/media": "2.2.3", "@solid-primitives/media": "2.2.3",
"@thisbeyond/solid-dnd": "0.7.4",
"form-data": "4.0.0", "form-data": "4.0.0",
"formidable": "2.1.1", "formidable": "2.1.1",
"i18next": "22.4.15", "i18next": "22.4.15",
"mailgun.js": "8.2.1", "mailgun.js": "8.2.1",
"music-metadata-browser": "2.5.10",
"node-fetch": "3.3.1", "node-fetch": "3.3.1",
"solid-popper": "0.3.0", "solid-popper": "0.3.0",
"typograf": "7.1.0" "typograf": "7.1.0"
@ -5612,6 +5615,17 @@
"integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==", "integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==",
"dev": true "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": { "node_modules/@sinclair/typebox": {
"version": "0.25.24", "version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@ -5827,6 +5841,14 @@
"solid-js": ">=1.4.0" "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": { "node_modules/@thisbeyond/solid-select": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz", "resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz",
@ -6269,6 +6291,11 @@
"@tiptap/core": "^2.0.0" "@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": { "node_modules/@tootallnate/once": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@ -6975,6 +7002,17 @@
"integrity": "sha512-IqnKIDWfXBJkvy/k6tzskWTc2NK3LcqHlb+KHGCrjOCH4jfQckRX0NAiIcC/vIqQkzLYw2r2CTSwAxcrtcD6lA==", "integrity": "sha512-IqnKIDWfXBJkvy/k6tzskWTc2NK3LcqHlb+KHGCrjOCH4jfQckRX0NAiIcC/vIqQkzLYw2r2CTSwAxcrtcD6lA==",
"dev": true "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": { "node_modules/abstract-leveldown": {
"version": "6.2.3", "version": "6.2.3",
"resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz",
@ -8305,6 +8343,14 @@
"upper-case": "^2.0.2" "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": { "node_modules/convert-source-map": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
@ -8509,7 +8555,6 @@
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
}, },
@ -9973,6 +10018,14 @@
"node": ">=0.10.0" "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": { "node_modules/events": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@ -10231,6 +10284,22 @@
"node": "^10.12.0 || >=12.0.0" "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": { "node_modules/filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@ -15960,6 +16029,14 @@
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
"dev": true "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": { "node_modules/meow": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
@ -16223,8 +16300,82 @@
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"dev": true },
"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": { "node_modules/mute-stream": {
"version": "0.0.8", "version": "0.0.8",
@ -16845,6 +16996,18 @@
"node": ">=8" "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": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -17189,6 +17352,14 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "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": { "node_modules/promise": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
@ -17679,6 +17850,21 @@
"node": ">= 6" "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": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "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", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" "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": { "node_modules/style-search": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
@ -19456,6 +19658,22 @@
"node": ">=8.0" "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": { "node_modules/totalist": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz",
@ -24799,6 +25017,12 @@
"integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==", "integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==",
"dev": true "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": { "@sinclair/typebox": {
"version": "0.25.24", "version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@ -24980,6 +25204,12 @@
"dev": true, "dev": true,
"requires": {} "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": { "@thisbeyond/solid-select": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz", "resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz",
@ -25223,6 +25453,11 @@
"prosemirror-view": "^1.28.2" "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": { "@tootallnate/once": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "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": { "abstract-leveldown": {
"version": "6.2.3", "version": "6.2.3",
"resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz",
@ -26788,6 +27031,11 @@
"upper-case": "^2.0.2" "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": { "convert-source-map": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
@ -26947,7 +27195,6 @@
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": { "requires": {
"ms": "2.1.2" "ms": "2.1.2"
} }
@ -28012,6 +28259,11 @@
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true "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": { "events": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@ -28213,6 +28465,16 @@
"flat-cache": "^3.0.4" "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": { "filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@ -32470,6 +32732,11 @@
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
"dev": true "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": { "meow": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
@ -32655,8 +32922,56 @@
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"dev": true },
"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": { "mute-stream": {
"version": "0.0.8", "version": "0.0.8",
@ -33102,6 +33417,11 @@
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true "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": { "picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "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": { "promise": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
@ -33732,6 +34057,14 @@
"util-deprecate": "^1.0.1" "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": { "readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "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", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" "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": { "style-search": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
@ -35068,6 +35410,15 @@
"is-number": "^7.0.0" "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": { "totalist": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz",

View 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

View 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
View 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

View 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

View File

@ -3,13 +3,16 @@
"About myself": "About myself", "About myself": "About myself",
"About the project": "About the project", "About the project": "About the project",
"Add another image": "Add another image", "Add another image": "Add another image",
"Add audio": "Add audio",
"Add comment": "Comment", "Add comment": "Comment",
"Add cover": "Add cover",
"Add image": "Add image", "Add image": "Add image",
"Add images": "Add images", "Add images": "Add images",
"Add link": "Add link", "Add link": "Add link",
"Add signature": "Add signature", "Add signature": "Add signature",
"Add url": "Add url", "Add url": "Add url",
"Address on Discourse": "Address on Discourse", "Address on Discourse": "Address on Discourse",
"Album name": "Название aльбома",
"Alignment center": "Alignment center", "Alignment center": "Alignment center",
"Alignment left": "Alignment left", "Alignment left": "Alignment left",
"Alignment right": "Alignment right", "Alignment right": "Alignment right",
@ -18,6 +21,7 @@
"All posts": "All posts", "All posts": "All posts",
"All topics": "All topics", "All topics": "All topics",
"Almost done! Check your email.": "Almost done! Just checking your email.", "Almost done! Check your email.": "Almost done! Just checking your email.",
"Artist": "Artist",
"Artworks": "Artworks", "Artworks": "Artworks",
"Audio": "Audio", "Audio": "Audio",
"Author": "Author", "Author": "Author",
@ -62,9 +66,12 @@
"Create account from follow": "Create an account to subscribe", "Create account from follow": "Create an account to subscribe",
"Create account from subscribe": "Create an account to subscribe to new publications", "Create account from subscribe": "Create an account to subscribe to new publications",
"Create account from vote": "Create an account to vote", "Create account from vote": "Create an account to vote",
"Create gallery": "Create gallery",
"Create post": "Create post", "Create post": "Create post",
"Create video": "Create video",
"Date of Birth": "Date of Birth", "Date of Birth": "Date of Birth",
"Delete": "Delete", "Delete": "Delete",
"Description": "Description...",
"Discours": "Discours", "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 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", "Discours is created with our common effort": "Discours exists because of our common effort",
@ -101,6 +108,7 @@
"Forgot password?": "Forgot your password?", "Forgot password?": "Forgot your password?",
"Forward": "Forward", "Forward": "Forward",
"Full name": "First and last name", "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", "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", "Go to main page": "Go to main page",
"Group Chat": "Group Chat", "Group Chat": "Group Chat",
@ -162,6 +170,7 @@
"My feed": "My feed", "My feed": "My feed",
"My subscriptions": "Subscriptions", "My subscriptions": "Subscriptions",
"Name": "Name", "Name": "Name",
"New literary work": "New literary work",
"New only": "New only", "New only": "New only",
"New password": "New password", "New password": "New password",
"New stories every day and even more!": "New stories and more are waiting for you every day!", "New stories every day and even more!": "New stories and more are waiting for you every day!",
@ -199,6 +208,7 @@
"Profile": "Profile", "Profile": "Profile",
"Profile settings": "Profile settings", "Profile settings": "Profile settings",
"Publications": "Publications", "Publications": "Publications",
"Publish Album": "Publish Album",
"Publish Settings": "Publish Settings", "Publish Settings": "Publish Settings",
"Punchline": "Punchline", "Punchline": "Punchline",
"Quit": "Quit", "Quit": "Quit",
@ -226,9 +236,12 @@
"Share": "Share", "Share": "Share",
"Short opening": "Short opening", "Short opening": "Short opening",
"Show": "Show", "Show": "Show",
"Show lyrics": "Текст песни",
"Social networks": "Social networks", "Social networks": "Social networks",
"Something went wrong, check email and password": "Something went wrong. Check your email and password", "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", "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", "Sorry, this address is already taken, please choose another one.": "Sorry, this address is already taken, please choose another one",
"Special projects": "Special projects", "Special projects": "Special projects",
"Specify the source and the name of the author": "Specify the source and the name of the author", "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", "Unfollow the topic": "Unfollow the topic",
"Unnamed draft": "Unnamed draft", "Unnamed draft": "Unnamed draft",
"Upload": "Upload", "Upload": "Upload",
"Upload error": "Upload error",
"Upload video": "Upload video", "Upload video": "Upload video",
"Username": "Username", "Username": "Username",
"Userpic": "Userpic", "Userpic": "Userpic",
@ -290,6 +304,7 @@
"Write message": "Write a message", "Write message": "Write a message",
"Write to us": "Write to us", "Write to us": "Write to us",
"You are subscribed": "You are subscribed", "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 were successfully authorized": "You were successfully authorized",
"You've confirmed email": "You've confirmed email", "You've confirmed email": "You've confirmed email",
"You've reached a non-existed page": "You've reached a non-existed page", "You've reached a non-existed page": "You've reached a non-existed page",
@ -322,8 +337,10 @@
"images": "images", "images": "images",
"invalid password": "invalid password", "invalid password": "invalid password",
"italic": "italic", "italic": "italic",
"jpg, .png, max. 10 mb.": "jpg, .png, макс. 10 мб.",
"literature": "literature", "literature": "literature",
"marker list": "marker list", "marker list": "marker list",
"min. 1400×1400 pix": "мин. 1400×1400 пикс.",
"music": "music", "music": "music",
"my feed": "my ribbon", "my feed": "my ribbon",
"number list": "number list", "number list": "number list",

View File

@ -4,9 +4,10 @@
"About myself": "О себе", "About myself": "О себе",
"About the project": "О проекте", "About the project": "О проекте",
"Accomplices": "Соучастники", "Accomplices": "Соучастники",
"Accomplices": "Соучастники",
"Add another image": "Добавить другое изображение", "Add another image": "Добавить другое изображение",
"Add audio": "Добавить аудио",
"Add comment": "Комментировать", "Add comment": "Комментировать",
"Add cover": "Добавить обложку",
"Add image": "Добавить изображение", "Add image": "Добавить изображение",
"Add images": "Добавить изображения", "Add images": "Добавить изображения",
"Add link": "Добавить ссылку", "Add link": "Добавить ссылку",
@ -14,6 +15,7 @@
"Add to bookmarks": "Добавить в закладки", "Add to bookmarks": "Добавить в закладки",
"Add url": "Добавить ссылку", "Add url": "Добавить ссылку",
"Address on Discourse": "Адрес на Дискурсе", "Address on Discourse": "Адрес на Дискурсе",
"Album name": "Название альбома",
"Alignment center": "По центру", "Alignment center": "По центру",
"Alignment left": "По левому краю", "Alignment left": "По левому краю",
"Alignment right": "По правому краю", "Alignment right": "По правому краю",
@ -22,6 +24,8 @@
"All posts": "Все публикации", "All posts": "Все публикации",
"All topics": "Все темы", "All topics": "Все темы",
"Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.", "Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.",
"Artist": "Исполнитель",
"Artist...": "Исполнитель...",
"Artworks": "Артворки", "Artworks": "Артворки",
"Audio": "Аудио", "Audio": "Аудио",
"Author": "Автор", "Author": "Автор",
@ -66,9 +70,12 @@
"Create account from follow": "Создайте аккаунт, чтобы подписаться", "Create account from follow": "Создайте аккаунт, чтобы подписаться",
"Create account from subscribe": "Создайте аккаунт для подписки на новые публикации", "Create account from subscribe": "Создайте аккаунт для подписки на новые публикации",
"Create account from vote": "Создайте аккаунт, чтобы голосовать", "Create account from vote": "Создайте аккаунт, чтобы голосовать",
"Create gallery": "Создать галерею",
"Create post": "Создать публикацию", "Create post": "Создать публикацию",
"Create video": "Создать видео",
"Date of Birth": "Дата рождения", "Date of Birth": "Дата рождения",
"Delete": "Удалить", "Delete": "Удалить",
"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 is created with our common effort": "Дискурс существует благодаря нашему общему вкладу",
@ -106,6 +113,8 @@
"Forgot password?": "Забыли пароль?", "Forgot password?": "Забыли пароль?",
"Forward": "Переслать", "Forward": "Переслать",
"Full name": "Имя и фамилия", "Full name": "Имя и фамилия",
"Gallery name": "Название галереи",
"Genre...": "Жанр...",
"Get notifications": "Получать уведомления", "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": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале", "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": "Перейти на главную",
@ -171,6 +180,7 @@
"My feed": "Новое", "My feed": "Новое",
"My subscriptions": "Подписки", "My subscriptions": "Подписки",
"Name": "Имя", "Name": "Имя",
"New literary work": "Новое произведение",
"New only": "Только новые", "New only": "Только новые",
"New password": "Новый пароль", "New password": "Новый пароль",
"New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!", "New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!",
@ -212,6 +222,7 @@
"Publication settings": "Настройки публикации", "Publication settings": "Настройки публикации",
"Publications": "Публикации", "Publications": "Публикации",
"Publish": "Опубликовать", "Publish": "Опубликовать",
"Publish Album": "Опубликовать альбом",
"Publish Settings": "Настройки публикации", "Publish Settings": "Настройки публикации",
"Punchline": "Панчлайн", "Punchline": "Панчлайн",
"Quit": "Выйти", "Quit": "Выйти",
@ -219,6 +230,7 @@
"Quotes": "Цитаты", "Quotes": "Цитаты",
"Reason uknown": "Причина неизвестна", "Reason uknown": "Причина неизвестна",
"Recent": "Свежее", "Recent": "Свежее",
"Release date...": "Дата выхода...",
"Reply": "Ответить", "Reply": "Ответить",
"Report": "Пожаловаться", "Report": "Пожаловаться",
"Required": "Поле обязательно для заполнения", "Required": "Поле обязательно для заполнения",
@ -240,9 +252,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 email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль",
"Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз", "Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз",
"Song lyrics": "Текст песни...",
"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": "Укажите источник и имя автора",
@ -283,6 +298,7 @@
"Unfollow the topic": "Отписаться от темы", "Unfollow the topic": "Отписаться от темы",
"Unnamed draft": "Unnamed draft", "Unnamed draft": "Unnamed draft",
"Upload": "Загрузить", "Upload": "Загрузить",
"Upload error": "Ошибка загрузки",
"Upload video": "Загрузить видео", "Upload video": "Загрузить видео",
"Username": "Имя пользователя", "Username": "Имя пользователя",
"Userpic": "Аватар", "Userpic": "Аватар",
@ -306,6 +322,7 @@
"Write message": "Написать сообщение", "Write 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": "Можно загрузить сразу несколько треков в форматах .mp3, .wav или .flac",
"You was successfully authorized": "Вы были успешно авторизованы", "You was successfully authorized": "Вы были успешно авторизованы",
"You've confirmed email": "Вы подтвердили почту", "You've confirmed email": "Вы подтвердили почту",
"You've reached a non-existed page": "Вы попали на несуществующую страницу", "You've reached a non-existed page": "Вы попали на несуществующую страницу",
@ -341,8 +358,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 feed": "моя лента",
"number list": "нумер. список", "number list": "нумер. список",

View File

@ -291,8 +291,9 @@ img {
} }
.shoutStatsItem { .shoutStatsItem {
align-items: center;
@include font-size(1.5rem); @include font-size(1.5rem);
align-items: center;
font-weight: 500; font-weight: 500;
display: flex; display: flex;
margin: 0 6% 1em 0; margin: 0 6% 1em 0;

View 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;
}
}
}

View 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>
)
}

View File

@ -0,0 +1 @@
export { AudioHeader } from './AudioHeader'

View File

@ -46,7 +46,6 @@
margin-top: 20px; margin-top: 20px;
margin-left: 0; margin-left: 0;
} }
}
.playButton { .playButton {
display: flex; display: flex;
@ -55,13 +54,14 @@
width: 40px; width: 40px;
height: 40px; height: 40px;
background-color: #141414; background: #141414;
& img { & img {
width: 14px; width: 14px;
height: auto; height: auto;
} }
} }
}
.playButtonInvertPause img { .playButtonInvertPause img {
filter: invert(100%); filter: invert(100%);
@ -113,22 +113,25 @@
.progressFilled { .progressFilled {
position: absolute; position: absolute;
top: -1px;
left: 0;
z-index: 2; z-index: 2;
box-sizing: border-box; box-sizing: border-box;
border-bottom: 4px solid #141414; border-bottom: 4px solid var(--default-color);
transition: width 0.3s linear;
&::after { &::after {
content: ''; content: '';
display: block; display: block;
position: absolute; position: absolute;
bottom: -8px; bottom: -10px;
right: -8px; right: -8px;
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
border: 4px solid #141414; border: 4px solid var(--default-color);
background-color: #fff; background-color: var(--background-color);
} }
} }
@ -227,7 +230,7 @@
flex-direction: column; flex-direction: column;
list-style-type: none; list-style-type: none;
margin: 32px 0 58px; margin: 32px 0 16px;
padding: 0; padding: 0;
& > li { & > li {
@ -243,6 +246,11 @@
padding: 16px 0; padding: 16px 0;
} }
.description {
display: flex;
flex-direction: column;
}
.playlistItemPlayButton { .playlistItemPlayButton {
border: none; border: none;
cursor: pointer; cursor: pointer;
@ -250,18 +258,42 @@
height: auto; height: auto;
} }
.playlistItemTitle { .playlistItemText {
max-width: 254px; @include font-size(1.6rem);
display: flex;
flex-direction: row;
flex: 1;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; margin: 0 16px;
gap: 16px;
color: var(--default-color);
margin-left: 17px; .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; font-weight: 400;
font-size: 16px; color: var(--secondary-color);
line-height: 22px; }
letter-spacing: -0.01em; }
color: #000000;
.title {
font-weight: 500;
}
} }
.playlistItemControls { .playlistItemControls {
@ -275,7 +307,7 @@
font-size: 16px; font-size: 16px;
line-height: 22px; line-height: 22px;
letter-spacing: -0.01em; letter-spacing: -0.01em;
color: #000000; color: var(--default-color);
} }
.timelinePlaceholder { .timelinePlaceholder {
@ -293,6 +325,29 @@
height: 67px; height: 67px;
} }
.shareMedia { .actions {
margin-left: auto; 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;
}
}
} }

View File

@ -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 { PlayerHeader } from './PlayerHeader'
import { PlayerPlaylist } from './PlayerPlaylist' import { PlayerPlaylist } from './PlayerPlaylist'
import styles from './AudioPlayer.module.scss' import styles from './AudioPlayer.module.scss'
import { MediaItem } from '../../../pages/types'
export type MediaItem = { export type Audio = {
id?: number pic?: string
body: string index?: number
pic: string isCurrent?: boolean
title: string isPlaying?: boolean
url: string } & MediaItem
isCurrent: boolean
isPlaying: boolean 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) => ({ media.map((item, index) => ({
...item, ...item,
id: index, index: index,
isCurrent: false, isCurrent: false,
isPlaying: false isPlaying: false
})) }))
const progressUpdate = (audioRef, progressFilledRef, duration) => { 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) => { 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) const getFormattedTime = (point) => new Date(point * 1000).toISOString().slice(14, -5)
export default (props: { media: MediaItem[]; articleSlug: string; body: string }) => { export const AudioPlayer = (props: Props) => {
let audioRef: HTMLAudioElement const audioRef: { current: HTMLAudioElement } = { current: null }
let progressRef: HTMLDivElement const progressRef: { current: HTMLDivElement } = { current: null }
let progressFilledRef: HTMLDivElement const progressFilledRef: { current: HTMLDivElement } = { current: null }
const [audioContext, setAudioContext] = createSignal<AudioContext>() const [audioContext, setAudioContext] = createSignal<AudioContext>()
const [gainNode, setGainNode] = createSignal<GainNode>() const [gainNode, setGainNode] = createSignal<GainNode>()
const [tracks, setTracks] = createSignal<Audio[] | null>(prepareMedia(props.media))
const [tracks, setTracks] = createSignal<MediaItem[] | null>(prepareMedia(props.media))
const [duration, setDuration] = createSignal<number>(0) const [duration, setDuration] = createSignal<number>(0)
const [currentTimeContent, setCurrentTimeContent] = createSignal<string>('00:00') const [currentTimeContent, setCurrentTimeContent] = createSignal<string>('00:00')
const [currentDurationContent, setCurrentDurationContent] = createSignal<string>('00:00') const [currentDurationContent, setCurrentDurationContent] = createSignal<string>('00:00')
const [mousedown, setMousedown] = createSignal<boolean>(false) const [mousedown, setMousedown] = createSignal<boolean>(false)
createEffect(
on(
() => props.media,
() => {
setTracks(prepareMedia(props.media))
}
)
)
const getCurrentTrack = () => const getCurrentTrack = () =>
tracks().find((track) => track.isCurrent) || tracks().find((track) => track.isCurrent) ||
(() => { (() => {
@ -63,10 +72,10 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
})() })()
createEffect(() => { createEffect(() => {
if (audioRef.src !== getCurrentTrack().url) { if (audioRef.current.src !== getCurrentTrack().url) {
audioRef.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( setTracks(
tracks().map((track) => ({ tracks().map((track) => ({
...track, ...track,
isCurrent: track.id === m.id, isCurrent: track.index === m.index,
isPlaying: track.id === m.id ? !track.isPlaying : false isPlaying: track.index === m.index ? !track.isPlaying : false
})) }))
) )
progressUpdate(audioRef, progressFilledRef, duration()) progressUpdate(audioRef, progressFilledRef, duration())
if (audioContext().state === 'suspended') audioContext().resume() if (audioContext().state === 'suspended') await audioContext().resume()
if (getCurrentTrack().isPlaying) { if (getCurrentTrack().isPlaying) {
await audioRef.play() await audioRef.current.play()
} else { } else {
audioRef.pause() audioRef.current.pause()
} }
} }
const setTimes = () => { const setTimes = () => {
setCurrentTimeContent(getFormattedTime(audioRef.currentTime)) setCurrentTimeContent(getFormattedTime(audioRef.current.currentTime))
} }
const handleAudioEnd = () => { const handleAudioEnd = () => {
progressFilledRef.style.width = '0%' progressFilledRef.current.style.width = '0%'
audioRef.currentTime = 0 audioRef.current.currentTime = 0
} }
const handleAudioTimeUpdate = () => { const handleAudioTimeUpdate = () => {
@ -117,42 +126,42 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
setTimes() setTimes()
const track = audioContext().createMediaElementSource(audioRef) const track = audioContext().createMediaElementSource(audioRef.current)
track.connect(gainNode()).connect(audioContext().destination) track.connect(gainNode()).connect(audioContext().destination)
}) })
const playPrevTrack = () => { const playPrevTrack = () => {
const { id } = getCurrentTrack() const { index } = getCurrentTrack()
const currIndex = tracks().findIndex((track) => track.id === id) const currIndex = tracks().findIndex((track) => track.index === index)
const getUpdatedStatus = (trackId) => const getUpdatedStatus = (trackId) =>
currIndex === 0 currIndex === 0
? trackId === tracks()[tracks().length - 1].id ? trackId === tracks()[tracks().length - 1].index
: trackId === tracks()[currIndex - 1].id : trackId === tracks()[currIndex - 1].index
setTracks( setTracks(
tracks().map((track) => ({ tracks().map((track) => ({
...track, ...track,
isCurrent: getUpdatedStatus(track.id), isCurrent: getUpdatedStatus(track.index),
isPlaying: getUpdatedStatus(track.id) isPlaying: getUpdatedStatus(track.index)
})) }))
) )
} }
const playNextTrack = () => { const playNextTrack = () => {
const { id } = getCurrentTrack() const { index } = getCurrentTrack()
const currIndex = tracks().findIndex((track) => track.id === id) const currIndex = tracks().findIndex((track) => track.index === index)
const getUpdatedStatus = (trackId) => const getUpdatedStatus = (trackId) =>
currIndex === tracks().length - 1 currIndex === tracks().length - 1
? trackId === tracks()[0].id ? trackId === tracks()[0].index
: trackId === tracks()[currIndex + 1].id : trackId === tracks()[currIndex + 1].index
setTracks( setTracks(
tracks().map((track) => ({ tracks().map((track) => ({
...track, ...track,
isCurrent: getUpdatedStatus(track.id), isCurrent: getUpdatedStatus(track.index),
isPlaying: getUpdatedStatus(track.id) isPlaying: getUpdatedStatus(track.index)
})) }))
) )
} }
@ -161,6 +170,15 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
setDuration(target.duration) 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 ( return (
<div> <div>
<Show when={getCurrentTrack()}> <Show when={getCurrentTrack()}>
@ -177,24 +195,24 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
<div class={styles.timeline}> <div class={styles.timeline}>
<div <div
class={styles.progress} class={styles.progress}
ref={progressRef} ref={(el) => (progressRef.current = el)}
onClick={(e) => scrub(e, progressRef, duration(), audioRef)} onClick={(e) => scrub(e, progressRef, duration(), audioRef)}
onMouseMove={(e) => mousedown() && scrub(e, progressRef, duration(), audioRef)} onMouseMove={(e) => mousedown() && scrub(e, progressRef, duration(), audioRef)}
onMouseDown={() => setMousedown(true)} onMouseDown={() => setMousedown(true)}
onMouseUp={() => setMousedown(false)} onMouseUp={() => setMousedown(false)}
> >
<div class={styles.progressFilled} ref={progressFilledRef}></div> <div class={styles.progressFilled} ref={(el) => (progressFilledRef.current = el)} />
</div> </div>
<div class={styles.progressTiming}> <div class={styles.progressTiming}>
<span>{currentTimeContent()}</span> <span>{currentTimeContent()}</span>
<span>{currentDurationContent()}</span> <span>{currentDurationContent()}</span>
</div> </div>
<audio <audio
ref={audioRef} ref={(el) => (audioRef.current = el)}
onTimeUpdate={handleAudioTimeUpdate} onTimeUpdate={handleAudioTimeUpdate}
onCanPlay={() => { onCanPlay={() => {
if (getCurrentTrack().isPlaying) { if (getCurrentTrack().isPlaying) {
audioRef.play() audioRef.current.play()
} }
}} }}
onLoadedMetadata={handleOnAudioMetadataLoad} onLoadedMetadata={handleOnAudioMetadataLoad}
@ -203,14 +221,15 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
/> />
</div> </div>
</Show> </Show>
<Show when={tracks()}> <Show when={tracks()}>
<PlayerPlaylist <PlayerPlaylist
editorMode={props.editorMode}
playMedia={playMedia} playMedia={playMedia}
tracks={tracks()} tracks={tracks()}
getCurrentTrack={getCurrentTrack} currentTrack={getCurrentTrack()}
articleSlug={props.articleSlug} articleSlug={props.articleSlug}
body={props.body} body={props.body}
onAudioChange={handleAudioDescriptionChange}
/> />
</Show> </Show>
</div> </div>

View File

@ -35,29 +35,29 @@ export const PlayerHeader = (props) => {
<div class={styles.playerTitle}>{getCurrentTrack().title}</div> <div class={styles.playerTitle}>{getCurrentTrack().title}</div>
<div class={styles.playerControls}> <div class={styles.playerControls}>
<button <button
type="button"
onClick={onPlayMedia} onClick={onPlayMedia}
class={clsx( class={clsx(
styles.playButton, styles.playButton,
getCurrentTrack().isPlaying ? styles.playButtonInvertPause : styles.playButtonInvertPlay getCurrentTrack().isPlaying ? styles.playButtonInvertPause : styles.playButtonInvertPlay
)} )}
role="button"
aria-label="Play" aria-label="Play"
data-playing="false" data-playing="false"
> >
<Icon name={getCurrentTrack().isPlaying ? 'pause' : 'play'} /> <Icon name={getCurrentTrack().isPlaying ? 'pause' : 'play'} />
</button> </button>
<button <button
type="button"
onClick={playPrevTrack} onClick={playPrevTrack}
class={clsx(styles.controlsButton)} class={clsx(styles.controlsButton)}
role="button"
aria-label="Previous" aria-label="Previous"
> >
<Icon name="player-arrow" /> <Icon name="player-arrow" />
</button> </button>
<button <button
type="button"
onClick={playNextTrack} onClick={playNextTrack}
class={clsx(styles.controlsButton, styles.controlsButtonNext)} class={clsx(styles.controlsButton, styles.controlsButtonNext)}
role="button"
aria-label="Next" aria-label="Next"
> >
<Icon name="player-arrow" /> <Icon name="player-arrow" />

View File

@ -1,61 +1,159 @@
import { For } from 'solid-js' import { createEffect, createSignal, For, Show } from 'solid-js'
import { SharePopup, getShareUrl } from '../SharePopup' import { SharePopup, getShareUrl } from '../SharePopup'
import { getDescription } from '../../../utils/meta' import { getDescription } from '../../../utils/meta'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import type { Audio } from './AudioPlayer'
import type { MediaItem } from './AudioPlayer'
import { Popover } from '../../_shared/Popover' import { Popover } from '../../_shared/Popover'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import styles from './AudioPlayer.module.scss' 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 { 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 ( return (
<ul class={styles.playlist}> <ul class={styles.playlist}>
<For each={tracks}> <For each={props.tracks}>
{(m: MediaItem) => ( {(m: Audio, index) => (
<li class={styles.playlistItem}> <li>
<div class={styles.playlistItem}>
<button <button
class={styles.playlistItemPlayButton} class={styles.playlistItemPlayButton}
onClick={() => playMedia(m)} onClick={() => props.playMedia(m)}
role="button" type="button"
aria-label="Play" aria-label="Play"
> >
<Icon <Icon
name={ name={
getCurrentTrack() && getCurrentTrack().id === m.id && getCurrentTrack().isPlaying props.currentTrack &&
props.currentTrack.index === m.index &&
props.currentTrack.isPlaying
? 'pause' ? 'pause'
: 'play' : 'play'
} }
/> />
</button> </button>
<div class={styles.playlistItemTitle}>{m.title}</div> <div class={styles.playlistItemText}>
<div class={styles.shareMedia}> <Show
<Popover content={t('Share')}> 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) => ( {(triggerRef: (el) => void) => (
<div ref={triggerRef}> <div ref={triggerRef}>
<Show
when={!props.editorMode}
fallback={
<button type="button" onClick={() => toggleDropDown(index())}>
<Icon name="pencil-stroke" />
</button>
}
>
<SharePopup <SharePopup
title={m.title} title={m.title}
description={getDescription(body)} description={getDescription(props.body)}
imageUrl={m.pic} imageUrl={m.pic}
shareUrl={getShareUrl({ pathname: `/${articleSlug}` })} shareUrl={getShareUrl({ pathname: `/${props.articleSlug}` })}
trigger={ trigger={
<div> <div>
<Icon name="share-media" /> <Icon name="share-media" />
</div> </div>
} }
/> />
</Show>
</div> </div>
)} )}
</Popover> </Popover>
</div> </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> </li>
)} )}
</For> </For>

View File

@ -0,0 +1 @@
export { AudioPlayer } from './AudioPlayer'

View File

@ -1,7 +1,7 @@
import { capitalize, formatDate } from '../../utils' import { capitalize, formatDate } from '../../utils'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { AuthorCard } from '../Author/AuthorCard' import { AuthorCard } from '../Author/AuthorCard'
import AudioPlayer from './AudioPlayer/AudioPlayer' import { AudioPlayer } from './AudioPlayer'
import type { Author, Shout } from '../../graphql/types.gen' import type { Author, Shout } from '../../graphql/types.gen'
import MD from './MD' import MD from './MD'
import { SharePopup } from './SharePopup' import { SharePopup } from './SharePopup'
@ -21,20 +21,15 @@ import styles from './Article.module.scss'
import { imageProxy } from '../../utils/imageProxy' import { imageProxy } from '../../utils/imageProxy'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import article from '../Editor/extensions/Article' import article from '../Editor/extensions/Article'
import { SolidSwiper } from '../_shared/SolidSwiper' import { createEffect, For, createMemo, onMount, Show, createSignal, Switch, Match } from 'solid-js'
import { createEffect, For, createMemo, Match, onMount, Show, Switch, createSignal } from 'solid-js' import { MediaItem } from '../../pages/types'
import { AudioHeader } from './AudioHeader'
interface ArticleProps { interface ArticleProps {
article: Shout article: Shout
scrollToComments?: boolean scrollToComments?: boolean
} }
interface MediaItem {
url?: string
title?: string
body?: string
}
export const FullArticle = (props: ArticleProps) => { export const FullArticle = (props: ArticleProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { const {
@ -117,6 +112,8 @@ export const FullArticle = (props: ArticleProps) => {
<div class="row"> <div class="row">
<article class="col-md-16 col-lg-14 col-xl-12 offset-md-5"> <article class="col-md-16 col-lg-14 col-xl-12 offset-md-5">
{/*TODO: Check styles.shoutTopic*/} {/*TODO: Check styles.shoutTopic*/}
<Switch>
<Match when={props.article.layout !== 'audio'}>
<div class={styles.shoutHeader}> <div class={styles.shoutHeader}>
<Show when={mainTopic()}> <Show when={mainTopic()}>
<div class={styles.shoutTopic}> <div class={styles.shoutTopic}>
@ -151,6 +148,16 @@ export const FullArticle = (props: ArticleProps) => {
/> />
</Show> </Show>
</div> </div>
</Match>
<Match when={props.article.layout === 'audio'}>
<AudioHeader
title={props.article.title}
cover={props.article.cover}
artistData={media()[0]}
topic={mainTopic()}
/>
</Match>
</Switch>
<Show when={media() && props.article.layout === 'video'}> <Show when={media() && props.article.layout === 'video'}>
<div class="media-items"> <div class="media-items">

View File

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

View 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>
)
}

View File

@ -0,0 +1 @@
export { AudioUploader } from './AudioUploader'

View File

@ -90,6 +90,34 @@
.titleInput { .titleInput {
font-weight: 700; 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 // Grow input
@ -215,13 +243,36 @@
.inputContainer { .inputContainer {
position: relative; position: relative;
flex: 1;
display: flex;
flex-flow: column;
.validationError { .validationError {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
top: 100%; top: calc(100% + 4px);
font-size: small; 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;
} }
} }

View File

@ -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 { useLocalize } from '../../context/localize'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Title } from '@solidjs/meta' import { Title } from '@solidjs/meta'
@ -16,9 +16,12 @@ import { hideModal, showModal } from '../../stores/ui'
import { imageProxy } from '../../utils/imageProxy' import { imageProxy } from '../../utils/imageProxy'
import { GrowingTextarea } from '../_shared/GrowingTextarea' import { GrowingTextarea } from '../_shared/GrowingTextarea'
import { VideoUploader } from '../Editor/VideoUploader' import { VideoUploader } from '../Editor/VideoUploader'
import { AudioUploader } from '../Editor/AudioUploader'
import { VideoPlayer } from '../_shared/VideoPlayer' import { VideoPlayer } from '../_shared/VideoPlayer'
import { slugify } from '../../utils/slugify' import { slugify } from '../../utils/slugify'
import { SolidSwiper } from '../_shared/SolidSwiper' import { SolidSwiper } from '../_shared/SolidSwiper'
import { DropArea } from '../_shared/DropArea'
import { LayoutType, MediaItem } from '../../pages/types'
type Props = { type Props = {
shout: Shout shout: Shout
@ -66,7 +69,7 @@ export const EditView = (props: Props) => {
layout: props.shout.layout layout: props.shout.layout
}) })
const mediaItems = createMemo(() => { const mediaItems: Accessor<MediaItem[]> = createMemo(() => {
return JSON.parse(form.media || '[]') return JSON.parse(form.media || '[]')
}) })
@ -125,9 +128,9 @@ export const EditView = (props: Props) => {
setForm('selectedTopics', newSelectedTopics) setForm('selectedTopics', newSelectedTopics)
} }
const handleAddImages = (data) => { const handleAddMedia = (data) => {
const newImages = [...mediaItems(), ...data] const newMedia = [...mediaItems(), ...data]
setForm('media', JSON.stringify(newImages)) setForm('media', JSON.stringify(newMedia))
} }
const handleSortedImages = (data) => { const handleSortedImages = (data) => {
setForm('media', JSON.stringify(data)) setForm('media', JSON.stringify(data))
@ -139,15 +142,54 @@ export const EditView = (props: Props) => {
setForm('media', JSON.stringify(copy)) setForm('media', JSON.stringify(copy))
} }
const handleImageChange = (index, value) => { const handleMediaChange = (index, value) => {
const updated = mediaItems().map((item, idx) => (idx === index ? value : item)) const updated = mediaItems().map((item, idx) => (idx === index ? value : item))
setForm('media', JSON.stringify(updated)) 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 ( return (
<> <>
<div class={styles.container}> <div class={styles.container}>
<Title>{t('Write an article')}</Title> <Title>{pageTitle()}</Title>
<form> <form>
<div class="wide-container"> <div class="wide-container">
<button <button
@ -167,33 +209,88 @@ export const EditView = (props: Props) => {
[styles.visible]: page().route === 'edit' [styles.visible]: page().route === 'edit'
})} })}
> >
<div class={clsx({ [styles.audioHeader]: props.shout.layout === 'audio' })}>
<div class={styles.inputContainer}> <div class={styles.inputContainer}>
<GrowingTextarea <GrowingTextarea
allowEnterKey={true}
value={(value) => handleTitleInputChange(value)} value={(value) => handleTitleInputChange(value)}
class={styles.titleInput} class={styles.titleInput}
placeholder={t('Header')} placeholder={articleTitle()}
initialValue={form.title} initialValue={form.title}
maxLength={100} maxLength={100}
/> />
<Show when={formErrors.title}> <Show when={formErrors.title}>
<div class={styles.validationError}>{formErrors.title}</div> <div class={styles.validationError}>{formErrors.title}</div>
</Show> </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> </div>
</Show>
<Show when={props.shout.layout !== 'audio'}>
<GrowingTextarea <GrowingTextarea
allowEnterKey={false}
value={(value) => setForm('subtitle', value)} value={(value) => setForm('subtitle', value)}
class={styles.subtitleInput} class={styles.subtitleInput}
placeholder={t('Subheader')} placeholder={t('Subheader')}
initialValue={form.subtitle} initialValue={form.subtitle}
maxLength={100} 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>
<Show when={props.shout.layout === 'image'}> <Show when={props.shout.layout === 'image'}>
<SolidSwiper <SolidSwiper
editorMode={true} editorMode={true}
images={mediaItems()} images={mediaItems()}
onImageChange={handleImageChange} onImageChange={handleMediaChange}
onImageDelete={(index) => handleImageDelete(index)} onImageDelete={(index) => handleImageDelete(index)}
onImagesAdd={(value) => handleAddImages(value)} onImagesAdd={(value) => handleAddMedia(value)}
onImagesSorted={(value) => handleSortedImages(value)} onImagesSorted={(value) => handleSortedImages(value)}
/> />
</Show> </Show>
@ -204,7 +301,7 @@ export const EditView = (props: Props) => {
fallback={ fallback={
<VideoUploader <VideoUploader
data={(data) => { data={(data) => {
handleAddImages(data) handleAddMedia(data)
}} }}
/> />
} }
@ -224,6 +321,14 @@ export const EditView = (props: Props) => {
</Show> </Show>
</Show> </Show>
<Show when={props.shout.layout === 'audio'}>
<AudioUploader
audio={mediaItems()}
onAudioAdd={(value) => handleAddMedia(value)}
onAudioChange={handleMediaChange}
/>
</Show>
<Editor <Editor
shoutId={props.shout.id} shoutId={props.shout.id}
initialContent={props.shout.body} initialContent={props.shout.body}

View File

@ -58,6 +58,24 @@
text-align: center; text-align: center;
padding: 1rem; 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 { @keyframes slide {

View File

@ -6,14 +6,16 @@ import { useLocalize } from '../../../context/localize'
import { validateFiles } from '../../../utils/validateFile' import { validateFiles } from '../../../utils/validateFile'
import type { FileTypeToUpload } from '../../../pages/types' import type { FileTypeToUpload } from '../../../pages/types'
import { handleFileUpload } from '../../../utils/handleFileUpload' import { handleFileUpload } from '../../../utils/handleFileUpload'
import { UploadedFile } from '../../../pages/types'
type Props = { type Props = {
class?: string class?: string
placeholder: string placeholder: string
description?: string | JSX.Element
fileType: FileTypeToUpload
isMultiply: boolean isMultiply: boolean
onUpload: (value: string[]) => void fileType: FileTypeToUpload
onUpload: (value: UploadedFile[]) => void
description?: string | JSX.Element
isSquare?: boolean
} }
export const DropArea = (props: Props) => { export const DropArea = (props: Props) => {
@ -26,15 +28,16 @@ export const DropArea = (props: Props) => {
try { try {
setLoading(true) setLoading(true)
const results: string[] = [] const results: UploadedFile[] = []
for (const file of files) { for (const file of files) {
const result = await handleFileUpload(file) const result = await handleFileUpload(file)
results.push(result.url) results.push(result)
} }
props.onUpload(results) props.onUpload(results)
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
setDropAreaError('Error') setLoading(false)
setDropAreaError(t('Upload error'))
console.error('[runUpload]', error) console.error('[runUpload]', error)
} }
} }
@ -81,7 +84,7 @@ export const DropArea = (props: Props) => {
} }
return ( return (
<div class={clsx(styles.DropArea, props.class)}> <div class={clsx(styles.DropArea, props.class, props.isSquare && styles['square'])}>
<div <div
class={clsx(styles.field, { [styles.active]: dragActive() })} class={clsx(styles.field, { [styles.active]: dragActive() })}
onDragEnter={handleDrag} onDragEnter={handleDrag}
@ -91,11 +94,14 @@ export const DropArea = (props: Props) => {
onClick={handleDropFieldClick} onClick={handleDropFieldClick}
> >
<div class={styles.text}>{loading() ? 'Loading...' : props.placeholder}</div> <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> </div>
<Show when={dropAreaError()}> <Show when={dropAreaError()}>
<div class={styles.error}>{dropAreaError()}</div> <div class={styles.error}>{dropAreaError()}</div>
</Show> </Show>
<Show when={!dropAreaError() && props.description}> <Show when={!dropAreaError() && props.description && !props.isSquare}>
<div class={styles.description}>{props.description}</div> <div class={styles.description}>{props.description}</div>
</Show> </Show>
</div> </div>

View File

@ -8,6 +8,7 @@ type Props = {
initialValue?: string initialValue?: string
value: (string) => void value: (string) => void
maxLength?: number maxLength?: number
allowEnterKey: boolean
} }
export const GrowingTextarea = (props: Props) => { export const GrowingTextarea = (props: Props) => {
@ -36,7 +37,7 @@ export const GrowingTextarea = (props: Props) => {
autocomplete="off" autocomplete="off"
class={clsx(styles.textInput, props.class)} class={clsx(styles.textInput, props.class)}
value={props.initialValue} value={props.initialValue}
onKeyDown={handleKeyDown} onKeyDown={props.allowEnterKey ? handleKeyDown : null}
onInput={(event) => handleChangeValue(event)} onInput={(event) => handleChangeValue(event)}
onChange={(event) => props.value(event.target.value)} onChange={(event) => props.value(event.target.value)}
placeholder={props.placeholder} placeholder={props.placeholder}

View File

@ -1,5 +1,5 @@
import { createEffect, createSignal, For, Match, Show, Switch, on } from 'solid-js' 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 { Icon } from '../Icon'
import { Popover } from '../Popover' import { Popover } from '../Popover'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -17,6 +17,7 @@ import { Loading } from '../Loading'
import { imageProxy } from '../../../utils/imageProxy' import { imageProxy } from '../../../utils/imageProxy'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './Swiper.module.scss' import styles from './Swiper.module.scss'
import { composeMediaItems } from '../../../utils/composeMediaItems'
type Props = { type Props = {
images: MediaItem[] images: MediaItem[]
@ -27,17 +28,6 @@ type Props = {
onImageChange?: (index: number, value: MediaItem) => void onImageChange?: (index: number, value: MediaItem) => void
} }
const composeMediaItem = (value) => {
return value.map((url) => {
return {
url: url,
source: '',
title: '',
body: ''
}
})
}
register() register()
SwiperCore.use([Pagination, Navigation, Manipulation]) SwiperCore.use([Pagination, Navigation, Manipulation])
@ -47,7 +37,6 @@ export const SolidSwiper = (props: Props) => {
const [loading, setLoading] = createSignal(false) const [loading, setLoading] = createSignal(false)
const [slideIndex, setSlideIndex] = createSignal(0) const [slideIndex, setSlideIndex] = createSignal(0)
const dropAreaRef: { current: HTMLElement } = { current: null }
const mainSwipeRef: { current: SwiperRef } = { current: null } const mainSwipeRef: { current: SwiperRef } = { current: null }
const thumbSwipeRef: { current: SwiperRef } = { current: null } const thumbSwipeRef: { current: SwiperRef } = { current: null }
@ -78,8 +67,8 @@ export const SolidSwiper = (props: Props) => {
) )
) )
const handleDropAreaUpload = (value: string[]) => { const handleDropAreaUpload = (value: UploadedFile[]) => {
props.onImagesAdd(composeMediaItem(value)) props.onImagesAdd(composeMediaItems(value))
swipeToUploaded() swipeToUploaded()
} }
@ -108,7 +97,7 @@ export const SolidSwiper = (props: Props) => {
const result = await handleFileUpload(file) const result = await handleFileUpload(file)
results.push(result.url) results.push(result.url)
} }
props.onImagesAdd(composeMediaItem(results)) props.onImagesAdd(composeMediaItems(results))
setLoading(false) setLoading(false)
swipeToUploaded() swipeToUploaded()
} catch (error) { } catch (error) {
@ -146,7 +135,6 @@ export const SolidSwiper = (props: Props) => {
<div class={styles.container}> <div class={styles.container}>
<Show when={props.editorMode && props.images.length === 0}> <Show when={props.editorMode && props.images.length === 0}>
<DropArea <DropArea
ref={(el) => (dropAreaRef.current = el)}
fileType="image" fileType="image"
isMultiply={true} isMultiply={true}
placeholder={t('Add images')} placeholder={t('Add images')}
@ -212,6 +200,7 @@ export const SolidSwiper = (props: Props) => {
} }
/> />
<GrowingTextarea <GrowingTextarea
allowEnterKey={true}
class={styles.descriptionText} class={styles.descriptionText}
placeholder={t('Enter image description')} placeholder={t('Enter image description')}
initialValue={slide.body} initialValue={slide.body}

View File

@ -42,10 +42,10 @@ export const CreatePage = () => {
</div> </div>
</li> </li>
<li> <li>
<a href="#"> <div class={styles.link} onClick={() => handleCreate('audio')}>
<Icon name="create-music" class={styles.icon} /> <Icon name="create-music" class={styles.icon} />
<div>{t('music')}</div> <div>{t('music')}</div>
</a> </div>
</li> </li>
<li> <li>
<div class={styles.link} onClick={() => handleCreate('video')}> <div class={styles.link} onClick={() => handleCreate('video')}>

View File

@ -34,11 +34,23 @@ export type UploadFile = {
export type LayoutType = 'article' | 'audio' | 'video' | 'image' | 'literature' 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 = { export type MediaItem = {
url: string url: string
title: string title: string
body: string body: string
source?: string source?: string
} & AudioDescription
export type UploadedFile = {
url: string
originalFilename: string
} }

View File

@ -27,6 +27,7 @@
--icon-filter: invert(0); --icon-filter: invert(0);
--icon-filter-hover: invert(1); --icon-filter-hover: invert(1);
--editor-bubble-menu-background: #fff; --editor-bubble-menu-background: #fff;
--blue-link: #2638d9;
} }
[data-editor-dark-mode='true'] { [data-editor-dark-mode='true'] {

View File

@ -246,7 +246,6 @@ export const apiClient = {
}, },
createArticle: async ({ article }: { article: ShoutInput }): Promise<Shout> => { createArticle: async ({ article }: { article: ShoutInput }): Promise<Shout> => {
const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise() const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise()
console.log('!!! [createArticle]:', response.data)
return response.data.createShout.shout return response.data.createShout.shout
}, },
updateArticle: async ({ updateArticle: async ({

View File

@ -0,0 +1,10 @@
export const composeMediaItems = (value) => {
return value.map((fileData) => {
return {
url: fileData.url,
source: '',
title: fileData.originalFilename,
body: ''
}
})
}

View File

@ -23,6 +23,10 @@ export const validateFiles = (fileType: FileTypeToUpload, files: UploadFile[]):
isValid = docExtension ? docExtensions.has(docExtension) : false isValid = docExtension ? docExtensions.has(docExtension) : false
break break
} }
case 'audio': {
isValid = file.file.type.startsWith('audio/')
break
}
default: { default: {
isValid = false isValid = false
} }