From cd8380720486e39382bba887b73fc207f06c7210 Mon Sep 17 00:00:00 2001 From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com> Date: Fri, 14 Jul 2023 16:06:21 +0300 Subject: [PATCH] Feature/audio upload (#120) * Audio upload * Audio player Article View --- package-lock.json | 363 +++++++++++++++++- public/icons/datepicker.svg | 5 + public/icons/expand-circle.svg | 9 + public/icons/list.svg | 10 + public/icons/pencil-stroke.svg | 5 + public/locales/en/translation.json | 17 + public/locales/ru/translation.json | 21 +- src/components/Article/Article.module.scss | 3 +- .../AudioHeader/AudioHeader.module.scss | 79 ++++ .../Article/AudioHeader/AudioHeader.tsx | 53 +++ src/components/Article/AudioHeader/index.ts | 1 + .../AudioPlayer/AudioPlayer.module.scss | 109 ++++-- .../Article/AudioPlayer/AudioPlayer.tsx | 127 +++--- .../Article/AudioPlayer/PlayerHeader.tsx | 6 +- .../Article/AudioPlayer/PlayerPlaylist.tsx | 188 ++++++--- src/components/Article/AudioPlayer/index.ts | 1 + src/components/Article/FullArticle.tsx | 89 +++-- .../AudioUploader/AudioUploader.module.scss | 17 + .../Editor/AudioUploader/AudioUploader.tsx | 41 ++ src/components/Editor/AudioUploader/index.ts | 1 + src/components/Views/Edit.module.scss | 55 ++- src/components/Views/Edit.tsx | 159 ++++++-- .../_shared/DropArea/DropArea.module.scss | 18 + src/components/_shared/DropArea/DropArea.tsx | 22 +- .../GrowingTextarea/GrowingTextarea.tsx | 3 +- .../_shared/SolidSwiper/SolidSwiper.tsx | 23 +- src/pages/create.page.tsx | 4 +- src/pages/types.ts | 14 +- src/styles/app.scss | 1 + src/utils/apiClient.ts | 1 - src/utils/composeMediaItems.ts | 10 + src/utils/validateFile.ts | 4 + 32 files changed, 1222 insertions(+), 237 deletions(-) create mode 100644 public/icons/datepicker.svg create mode 100644 public/icons/expand-circle.svg create mode 100644 public/icons/list.svg create mode 100644 public/icons/pencil-stroke.svg create mode 100644 src/components/Article/AudioHeader/AudioHeader.module.scss create mode 100644 src/components/Article/AudioHeader/AudioHeader.tsx create mode 100644 src/components/Article/AudioHeader/index.ts create mode 100644 src/components/Article/AudioPlayer/index.ts create mode 100644 src/components/Editor/AudioUploader/AudioUploader.module.scss create mode 100644 src/components/Editor/AudioUploader/AudioUploader.tsx create mode 100644 src/components/Editor/AudioUploader/index.ts create mode 100644 src/utils/composeMediaItems.ts diff --git a/package-lock.json b/package-lock.json index 8140903c..6bb369e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/public/icons/datepicker.svg b/public/icons/datepicker.svg new file mode 100644 index 00000000..f931e1b8 --- /dev/null +++ b/public/icons/datepicker.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icons/expand-circle.svg b/public/icons/expand-circle.svg new file mode 100644 index 00000000..2201323a --- /dev/null +++ b/public/icons/expand-circle.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/list.svg b/public/icons/list.svg new file mode 100644 index 00000000..780635e3 --- /dev/null +++ b/public/icons/list.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/pencil-stroke.svg b/public/icons/pencil-stroke.svg new file mode 100644 index 00000000..8f0db39c --- /dev/null +++ b/public/icons/pencil-stroke.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 99e2c25f..54e95c52 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -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", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index d7326f93..243f955f 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -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": "нумер. список", diff --git a/src/components/Article/Article.module.scss b/src/components/Article/Article.module.scss index 7a0b56b4..6b99af4e 100644 --- a/src/components/Article/Article.module.scss +++ b/src/components/Article/Article.module.scss @@ -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; diff --git a/src/components/Article/AudioHeader/AudioHeader.module.scss b/src/components/Article/AudioHeader/AudioHeader.module.scss new file mode 100644 index 00000000..51da7246 --- /dev/null +++ b/src/components/Article/AudioHeader/AudioHeader.module.scss @@ -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; + } + } +} diff --git a/src/components/Article/AudioHeader/AudioHeader.tsx b/src/components/Article/AudioHeader/AudioHeader.tsx new file mode 100644 index 00000000..548c0ef4 --- /dev/null +++ b/src/components/Article/AudioHeader/AudioHeader.tsx @@ -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 ( +
+
+ {props.title} + + + +
+
+ + + +

{props.title}

+
+ +
{props.artistData.artist}
+
+ +
{props.artistData.date}
+
+ +
{props.artistData.genre}
+
+
+
+
+ ) +} diff --git a/src/components/Article/AudioHeader/index.ts b/src/components/Article/AudioHeader/index.ts new file mode 100644 index 00000000..c382f62b --- /dev/null +++ b/src/components/Article/AudioHeader/index.ts @@ -0,0 +1 @@ +export { AudioHeader } from './AudioHeader' diff --git a/src/components/Article/AudioPlayer/AudioPlayer.module.scss b/src/components/Article/AudioPlayer/AudioPlayer.module.scss index 29ba0a9b..84b5671c 100644 --- a/src/components/Article/AudioPlayer/AudioPlayer.module.scss +++ b/src/components/Article/AudioPlayer/AudioPlayer.module.scss @@ -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; + } + } } diff --git a/src/components/Article/AudioPlayer/AudioPlayer.tsx b/src/components/Article/AudioPlayer/AudioPlayer.tsx index f0b36bd6..b3444e25 100644 --- a/src/components/Article/AudioPlayer/AudioPlayer.tsx +++ b/src/components/Article/AudioPlayer/AudioPlayer.tsx @@ -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() const [gainNode, setGainNode] = createSignal() - - const [tracks, setTracks] = createSignal(prepareMedia(props.media)) - + const [tracks, setTracks] = createSignal(prepareMedia(props.media)) const [duration, setDuration] = createSignal(0) const [currentTimeContent, setCurrentTimeContent] = createSignal('00:00') const [currentDurationContent, setCurrentDurationContent] = createSignal('00:00') - const [mousedown, setMousedown] = createSignal(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 (
@@ -177,24 +195,24 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
(progressRef.current = el)} onClick={(e) => scrub(e, progressRef, duration(), audioRef)} onMouseMove={(e) => mousedown() && scrub(e, progressRef, duration(), audioRef)} onMouseDown={() => setMousedown(true)} onMouseUp={() => setMousedown(false)} > -
+
(progressFilledRef.current = el)} />
{currentTimeContent()} {currentDurationContent()}
-
diff --git a/src/components/Article/AudioPlayer/PlayerHeader.tsx b/src/components/Article/AudioPlayer/PlayerHeader.tsx index b5c09d23..04aa50e7 100644 --- a/src/components/Article/AudioPlayer/PlayerHeader.tsx +++ b/src/components/Article/AudioPlayer/PlayerHeader.tsx @@ -35,29 +35,29 @@ export const PlayerHeader = (props) => {
{getCurrentTrack().title}
-
{m.title}
-
- - {(triggerRef: (el) => void) => ( -
- - -
- } - /> -
- )} - + + {(m: Audio, index) => ( +
  • +
    + +
    + +
    + {m.title.replace(/\.(wav|flac|mp3|aac)$/i, '') || t('Song title')} +
    +
    {m.artist || t('Artist')}
    + + } + > + updateData('title', e.target.value)} + /> + updateData('artist', e.target.value)} + /> +
    +
    +
    + + + {(triggerRef: (el) => void) => ( + + )} + + + + {(triggerRef: (el) => void) => ( +
    + toggleDropDown(index())}> + + + } + > + + +
    + } + /> + +
    + )} + +
  • + + + +
    + +
    +
    + +
    + +
    +
    +
    + } + > +
    + updateData('body', value)} + initialValue={m.body || ''} + /> + updateData('lyrics', value)} + initialValue={m.lyrics || ''} + /> +
    + + )} diff --git a/src/components/Article/AudioPlayer/index.ts b/src/components/Article/AudioPlayer/index.ts new file mode 100644 index 00000000..d45cffdf --- /dev/null +++ b/src/components/Article/AudioPlayer/index.ts @@ -0,0 +1 @@ +export { AudioPlayer } from './AudioPlayer' diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 158d915f..5f24ea34 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -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) => {
    {/*TODO: Check styles.shoutTopic*/} -
    - -
    - - {mainTopic().title} - + + +
    + + + + +

    {props.article.title}

    + +

    {capitalize(props.article.subtitle, false)}

    +
    + +
    + + {(a: Author, index) => ( + <> + 0}>, + {a.name} + + )} + +
    + +
    +
    -
    - -

    {props.article.title}

    - -

    {capitalize(props.article.subtitle, false)}

    -
    - -
    - - {(a: Author, index) => ( - <> - 0}>, - {a.name} - - )} - -
    - -
    + + - -
    + +
    diff --git a/src/components/Editor/AudioUploader/AudioUploader.module.scss b/src/components/Editor/AudioUploader/AudioUploader.module.scss new file mode 100644 index 00000000..1a15ec91 --- /dev/null +++ b/src/components/Editor/AudioUploader/AudioUploader.module.scss @@ -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; +} diff --git a/src/components/Editor/AudioUploader/AudioUploader.tsx b/src/components/Editor/AudioUploader/AudioUploader.tsx new file mode 100644 index 00000000..c1f9114c --- /dev/null +++ b/src/components/Editor/AudioUploader/AudioUploader.tsx @@ -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 ( +
    + 0}> + + + props.onAudioAdd(composeMediaItems(value))} + /> +
    + ) +} diff --git a/src/components/Editor/AudioUploader/index.ts b/src/components/Editor/AudioUploader/index.ts new file mode 100644 index 00000000..ee94d29c --- /dev/null +++ b/src/components/Editor/AudioUploader/index.ts @@ -0,0 +1 @@ +export { AudioUploader } from './AudioUploader' diff --git a/src/components/Views/Edit.module.scss b/src/components/Views/Edit.module.scss index 62bc4333..e1be9830 100644 --- a/src/components/Views/Edit.module.scss +++ b/src/components/Views/Edit.module.scss @@ -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; } } diff --git a/src/components/Views/Edit.tsx b/src/components/Views/Edit.tsx index 6a5bcb78..2eec8189 100644 --- a/src/components/Views/Edit.tsx +++ b/src/components/Views/Edit.tsx @@ -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 = 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 ( <>
    - {t('Write an article')} + {pageTitle()}