Added audio and video player
This commit is contained in:
parent
3e979c4005
commit
895fee1bae
|
@ -16,6 +16,7 @@
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"cuid": "^2.1.8",
|
"cuid": "^2.1.8",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"intl-messageformat": "^10.3.5",
|
"intl-messageformat": "^10.3.5",
|
||||||
"isomorphic-dompurify": "^1.5.0",
|
"isomorphic-dompurify": "^1.5.0",
|
||||||
"jotai": "^2.1.0",
|
"jotai": "^2.1.0",
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
"@graphql-codegen/typescript": "3.0.4",
|
"@graphql-codegen/typescript": "3.0.4",
|
||||||
"@graphql-codegen/typescript-graphql-request": "^4.5.9",
|
"@graphql-codegen/typescript-graphql-request": "^4.5.9",
|
||||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||||
|
"@types/html-to-text": "^9.0.0",
|
||||||
"@types/marked": "^4.3.0",
|
"@types/marked": "^4.3.0",
|
||||||
"@types/node": "20.1.3",
|
"@types/node": "20.1.3",
|
||||||
"@types/nodemailer": "^6.4.7",
|
"@types/nodemailer": "^6.4.7",
|
||||||
|
@ -3642,6 +3644,18 @@
|
||||||
"integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==",
|
"integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@selderee/plugin-htmlparser2": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"selderee": "^0.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz",
|
||||||
|
@ -3683,6 +3697,12 @@
|
||||||
"@types/trusted-types": "*"
|
"@types/trusted-types": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/html-to-text": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-FnF3p2FJZ1kJT/0C/lmBzw7HSlH3RhtACVYyrwUsJoCmFNuiLpusWT2FWWB7P9A48CaYpvD6Q2fprn7sZeffpw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/js-yaml": {
|
"node_modules/@types/js-yaml": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
|
||||||
|
@ -5156,6 +5176,14 @@
|
||||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/deepmerge": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/defaults": {
|
"node_modules/defaults": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
|
||||||
|
@ -5243,6 +5271,30 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/domexception": {
|
"node_modules/domexception": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
|
||||||
|
@ -5262,6 +5314,20 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/domino": {
|
"node_modules/domino": {
|
||||||
"version": "2.1.6",
|
"version": "2.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
|
||||||
|
@ -5272,6 +5338,19 @@
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
|
||||||
"integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
|
"integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dot-case": {
|
"node_modules/dot-case": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
||||||
|
@ -6865,6 +6944,39 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-to-text": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@selderee/plugin-htmlparser2": "^0.11.0",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"htmlparser2": "^8.0.2",
|
||||||
|
"selderee": "^0.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-proxy-agent": {
|
"node_modules/http-proxy-agent": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
||||||
|
@ -7792,6 +7904,14 @@
|
||||||
"language-subtag-registry": "~0.3.2"
|
"language-subtag-registry": "~0.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leac": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
|
@ -8668,6 +8788,18 @@
|
||||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parseley": {
|
||||||
|
"version": "0.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||||
|
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
|
||||||
|
"dependencies": {
|
||||||
|
"leac": "^0.6.0",
|
||||||
|
"peberminta": "^0.9.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pascal-case": {
|
"node_modules/pascal-case": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
|
||||||
|
@ -8845,6 +8977,14 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/peberminta": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
|
@ -9565,6 +9705,17 @@
|
||||||
"integrity": "sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==",
|
"integrity": "sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/selderee": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
|
||||||
|
"dependencies": {
|
||||||
|
"parseley": "^0.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.3.8",
|
"version": "7.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||||
|
@ -13631,6 +13782,15 @@
|
||||||
"integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==",
|
"integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@selderee/plugin-htmlparser2": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
|
||||||
|
"requires": {
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"selderee": "^0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@swc/helpers": {
|
"@swc/helpers": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz",
|
||||||
|
@ -13667,6 +13827,12 @@
|
||||||
"@types/trusted-types": "*"
|
"@types/trusted-types": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/html-to-text": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-FnF3p2FJZ1kJT/0C/lmBzw7HSlH3RhtACVYyrwUsJoCmFNuiLpusWT2FWWB7P9A48CaYpvD6Q2fprn7sZeffpw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/js-yaml": {
|
"@types/js-yaml": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
|
||||||
|
@ -14773,6 +14939,11 @@
|
||||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"deepmerge": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
|
||||||
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
|
||||||
|
@ -14839,6 +15010,21 @@
|
||||||
"esutils": "^2.0.2"
|
"esutils": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
|
||||||
|
},
|
||||||
"domexception": {
|
"domexception": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
|
||||||
|
@ -14854,6 +15040,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"domino": {
|
"domino": {
|
||||||
"version": "2.1.6",
|
"version": "2.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
|
||||||
|
@ -14864,6 +15058,16 @@
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
|
||||||
"integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
|
"integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
|
||||||
},
|
},
|
||||||
|
"domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"requires": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"dot-case": {
|
"dot-case": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
||||||
|
@ -16083,6 +16287,29 @@
|
||||||
"whatwg-encoding": "^2.0.0"
|
"whatwg-encoding": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"html-to-text": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
|
||||||
|
"requires": {
|
||||||
|
"@selderee/plugin-htmlparser2": "^0.11.0",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"htmlparser2": "^8.0.2",
|
||||||
|
"selderee": "^0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"http-proxy-agent": {
|
"http-proxy-agent": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
||||||
|
@ -16764,6 +16991,11 @@
|
||||||
"language-subtag-registry": "~0.3.2"
|
"language-subtag-registry": "~0.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"leac": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="
|
||||||
|
},
|
||||||
"levn": {
|
"levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
|
@ -17374,6 +17606,15 @@
|
||||||
"entities": "^4.4.0"
|
"entities": "^4.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"parseley": {
|
||||||
|
"version": "0.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||||
|
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
|
||||||
|
"requires": {
|
||||||
|
"leac": "^0.6.0",
|
||||||
|
"peberminta": "^0.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"pascal-case": {
|
"pascal-case": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
|
||||||
|
@ -17511,6 +17752,11 @@
|
||||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"peberminta": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="
|
||||||
|
},
|
||||||
"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",
|
||||||
|
@ -17966,6 +18212,14 @@
|
||||||
"integrity": "sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==",
|
"integrity": "sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"selderee": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
|
||||||
|
"requires": {
|
||||||
|
"parseley": "^0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "7.3.8",
|
"version": "7.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"cuid": "^2.1.8",
|
"cuid": "^2.1.8",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"intl-messageformat": "^10.3.5",
|
"intl-messageformat": "^10.3.5",
|
||||||
"isomorphic-dompurify": "^1.5.0",
|
"isomorphic-dompurify": "^1.5.0",
|
||||||
"jotai": "^2.1.0",
|
"jotai": "^2.1.0",
|
||||||
|
@ -61,6 +62,7 @@
|
||||||
"@graphql-codegen/typescript": "3.0.4",
|
"@graphql-codegen/typescript": "3.0.4",
|
||||||
"@graphql-codegen/typescript-graphql-request": "^4.5.9",
|
"@graphql-codegen/typescript-graphql-request": "^4.5.9",
|
||||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||||
|
"@types/html-to-text": "^9.0.0",
|
||||||
"@types/marked": "^4.3.0",
|
"@types/marked": "^4.3.0",
|
||||||
"@types/node": "20.1.3",
|
"@types/node": "20.1.3",
|
||||||
"@types/nodemailer": "^6.4.7",
|
"@types/nodemailer": "^6.4.7",
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { Chip } from "components/Chip";
|
||||||
|
import { Markdawn } from "components/Markdown/Markdawn";
|
||||||
|
import { RecorderChip } from "components/RecorderChip";
|
||||||
|
import { ToolTip } from "components/ToolTip";
|
||||||
|
import { atoms } from "contexts/atoms";
|
||||||
|
import { RecorderChipFragment } from "graphql/generated";
|
||||||
|
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||||
|
import { useAtomGetter } from "helpers/atoms";
|
||||||
|
import { prettyLanguage } from "helpers/formatters";
|
||||||
|
import { ContentStatus, useFormat } from "hooks/useFormat";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭─────────────╮
|
||||||
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
languageCode?: string;
|
||||||
|
sourceLanguageCode?: string;
|
||||||
|
status?: ContentStatus | null;
|
||||||
|
transcribers?: { attributes?: RecorderChipFragment | null }[];
|
||||||
|
translators?: { attributes?: RecorderChipFragment | null }[];
|
||||||
|
proofreaders?: { attributes?: RecorderChipFragment | null }[];
|
||||||
|
dubbers?: { attributes?: RecorderChipFragment | null }[];
|
||||||
|
subbers?: { attributes?: RecorderChipFragment | null }[];
|
||||||
|
authors?: { attributes?: RecorderChipFragment | null }[];
|
||||||
|
notes?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
|
export const Credits = ({
|
||||||
|
languageCode,
|
||||||
|
sourceLanguageCode,
|
||||||
|
status,
|
||||||
|
transcribers = [],
|
||||||
|
translators = [],
|
||||||
|
dubbers = [],
|
||||||
|
proofreaders = [],
|
||||||
|
subbers = [],
|
||||||
|
authors = [],
|
||||||
|
notes,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
|
const { format, formatStatusDescription, formatStatusLabel } = useFormat();
|
||||||
|
const languages = useAtomGetter(atoms.localData.languages);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid place-items-center gap-5">
|
||||||
|
{isDefined(languageCode) && isDefined(sourceLanguageCode) && (
|
||||||
|
<>
|
||||||
|
{languageCode === sourceLanguageCode ? (
|
||||||
|
<h2 className="text-xl">{format("transcript_notice")}</h2>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2 className="text-xl">{format("translation_notice")}</h2>
|
||||||
|
<div className="flex flex-wrap place-content-center place-items-center gap-2">
|
||||||
|
<p className="font-headers font-bold">{format("source_language")}:</p>
|
||||||
|
<Chip text={prettyLanguage(sourceLanguageCode, languages)} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status && (
|
||||||
|
<div className="flex flex-wrap place-content-center place-items-center gap-2">
|
||||||
|
<p className="font-headers font-bold">{format("status")}:</p>
|
||||||
|
<ToolTip content={formatStatusDescription(status)} maxWidth={"20rem"}>
|
||||||
|
<Chip text={formatStatusLabel(status)} />
|
||||||
|
</ToolTip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{transcribers.length > 0 && (
|
||||||
|
<RecorderChips
|
||||||
|
title={format("transcriber", { count: transcribers.length })}
|
||||||
|
recorders={transcribers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{translators.length > 0 && (
|
||||||
|
<RecorderChips
|
||||||
|
title={format("translator", { count: translators.length })}
|
||||||
|
recorders={translators}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{proofreaders.length > 0 && (
|
||||||
|
<RecorderChips
|
||||||
|
title={format("proofreader", { count: proofreaders.length })}
|
||||||
|
recorders={proofreaders}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{dubbers.length > 0 && (
|
||||||
|
<RecorderChips title={format("dubber", { count: dubbers.length })} recorders={dubbers} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subbers.length > 0 && (
|
||||||
|
<RecorderChips title={format("subber", { count: subbers.length })} recorders={subbers} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{authors.length > 0 && (
|
||||||
|
<RecorderChips title={format("author", { count: authors.length })} recorders={authors} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isDefinedAndNotEmpty(notes) && (
|
||||||
|
<div>
|
||||||
|
<p className="font-headers font-bold">{format("notes")}:</p>
|
||||||
|
<div className="grid place-content-center place-items-center gap-2">
|
||||||
|
<Markdawn text={notes} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RecorderChipsProps {
|
||||||
|
title: string;
|
||||||
|
recorders: { attributes?: RecorderChipFragment | null }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecorderChips = ({ title, recorders }: RecorderChipsProps) => (
|
||||||
|
<div className="flex flex-wrap place-content-center place-items-center gap-1">
|
||||||
|
<p className="pr-1 font-headers font-bold">{title}:</p>
|
||||||
|
{filterHasAttributes(recorders, ["attributes"]).map((recorder) => (
|
||||||
|
<RecorderChip key={recorder.attributes.anonymous_code} recorder={recorder.attributes} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -58,11 +58,11 @@ export const Button = ({
|
||||||
onMouseUp={onMouseUp}
|
onMouseUp={onMouseUp}
|
||||||
onFocus={(event) => event.target.blur()}
|
onFocus={(event) => event.target.blur()}
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
`group grid w-full grid-flow-col place-content-center
|
`group grid w-full grid-flow-col
|
||||||
place-items-center gap-2 rounded-full border border-dark
|
place-content-center place-items-center gap-2 rounded-full border
|
||||||
leading-none text-dark transition-all disabled:cursor-not-allowed
|
border-dark leading-none text-dark transition-all
|
||||||
disabled:opacity-50 disabled:grayscale`,
|
disabled:cursor-not-allowed disabled:opacity-50 disabled:grayscale`,
|
||||||
cIf(size === "small", "px-3 py-1 text-xs", "px-4 py-3"),
|
cIf(size === "small", "h-6 px-3 py-1 text-xs", "h-10 px-4 py-3"),
|
||||||
cIf(active, "!border-black bg-black !text-light shadow-lg shadow-black"),
|
cIf(active, "!border-black bg-black !text-light shadow-lg shadow-black"),
|
||||||
cIf(
|
cIf(
|
||||||
!disabled && !active,
|
!disabled && !active,
|
||||||
|
@ -91,7 +91,9 @@ export const Button = ({
|
||||||
weight={size === "normal" ? 500 : 800}
|
weight={size === "normal" ? 500 : 800}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isDefinedAndNotEmpty(text) && <p className="-translate-y-[0.05em] text-center">{text}</p>}
|
{isDefinedAndNotEmpty(text) && (
|
||||||
|
<p className="line-clamp-1 -translate-y-[0.05em] text-center leading-5">{text}</p>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Placement } from "tippy.js";
|
import type { Placement } from "tippy.js";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { ToolTip } from "components/ToolTip";
|
import { ToolTip } from "components/ToolTip";
|
||||||
import { cJoin } from "helpers/className";
|
import { cIf, cJoin } from "helpers/className";
|
||||||
import { ConditionalWrapper, Wrapper } from "helpers/component";
|
import { ConditionalWrapper, Wrapper } from "helpers/component";
|
||||||
import { isDefined } from "helpers/asserts";
|
import { isDefined } from "helpers/asserts";
|
||||||
|
|
||||||
|
@ -10,9 +10,13 @@ import { isDefined } from "helpers/asserts";
|
||||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
type ButtonProps = Parameters<typeof Button>[0];
|
||||||
|
|
||||||
export interface ButtonGroupProps {
|
export interface ButtonGroupProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
buttonsProps: (Parameters<typeof Button>[0] & {
|
vertical?: boolean;
|
||||||
|
size?: ButtonProps["size"];
|
||||||
|
buttonsProps: (Omit<ButtonProps, "size"> & {
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
tooltip?: React.ReactNode | null | undefined;
|
tooltip?: React.ReactNode | null | undefined;
|
||||||
tooltipPlacement?: Placement;
|
tooltipPlacement?: Placement;
|
||||||
|
@ -21,35 +25,73 @@ export interface ButtonGroupProps {
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const ButtonGroup = ({ buttonsProps, className }: ButtonGroupProps): JSX.Element => (
|
export const ButtonGroup = ({
|
||||||
|
buttonsProps,
|
||||||
|
className,
|
||||||
|
vertical,
|
||||||
|
size,
|
||||||
|
}: ButtonGroupProps): JSX.Element => (
|
||||||
<FilteredButtonGroup
|
<FilteredButtonGroup
|
||||||
buttonsProps={buttonsProps.filter((button) => button.visible !== false)}
|
buttonsProps={buttonsProps.filter((button) => button.visible !== false)}
|
||||||
className={className}
|
className={className}
|
||||||
|
vertical={vertical}
|
||||||
|
size={size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const FilteredButtonGroup = ({ buttonsProps, className }: ButtonGroupProps) => (
|
const FilteredButtonGroup = ({
|
||||||
<div className={cJoin("grid grid-flow-col", className)}>
|
buttonsProps,
|
||||||
{buttonsProps.map((buttonProps, index) => (
|
className,
|
||||||
<ConditionalWrapper
|
vertical = false,
|
||||||
key={index}
|
size = "normal",
|
||||||
isWrapping={isDefined(buttonProps.tooltip)}
|
}: ButtonGroupProps) => {
|
||||||
wrapper={ToolTipWrapper}
|
const firstClassName = cIf(
|
||||||
wrapperProps={{ text: buttonProps.tooltip ?? "", placement: buttonProps.tooltipPlacement }}>
|
vertical,
|
||||||
<Button
|
cJoin("rounded-b-none border-b-0", cIf(size === "normal", "rounded-t-3xl", "rounded-t-xl")),
|
||||||
{...buttonProps}
|
"rounded-r-none border-r-0"
|
||||||
className={
|
);
|
||||||
index === 0
|
|
||||||
? "rounded-r-none border-r-0"
|
const lastClassName = cIf(
|
||||||
: index === buttonsProps.length - 1
|
vertical,
|
||||||
? "rounded-l-none"
|
cJoin("rounded-t-none border-t-0", cIf(size === "normal", "rounded-b-3xl", "rounded-b-xl")),
|
||||||
: "rounded-none border-r-0"
|
"rounded-l-none border-l-0"
|
||||||
}
|
);
|
||||||
/>
|
|
||||||
</ConditionalWrapper>
|
const middleClassName = cIf(vertical, "rounded-none border-y-0", "rounded-none border-x-0");
|
||||||
))}
|
|
||||||
</div>
|
return (
|
||||||
);
|
<div className={cJoin("grid", cIf(!vertical, "grid-flow-col"), className)}>
|
||||||
|
{buttonsProps.map((buttonProps, index) => (
|
||||||
|
<ConditionalWrapper
|
||||||
|
key={index}
|
||||||
|
isWrapping={isDefined(buttonProps.tooltip)}
|
||||||
|
wrapper={ToolTipWrapper}
|
||||||
|
wrapperProps={{
|
||||||
|
text: buttonProps.tooltip ?? "",
|
||||||
|
placement: buttonProps.tooltipPlacement,
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
{...buttonProps}
|
||||||
|
size={size}
|
||||||
|
className={cJoin(
|
||||||
|
"relative",
|
||||||
|
cIf(
|
||||||
|
vertical && buttonProps.active && index < buttonsProps.length - 1,
|
||||||
|
"shadow-black/60"
|
||||||
|
),
|
||||||
|
cIf(buttonProps.active, "z-10", "z-0"),
|
||||||
|
index === 0
|
||||||
|
? firstClassName
|
||||||
|
: index === buttonsProps.length - 1
|
||||||
|
? lastClassName
|
||||||
|
: middleClassName
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</ConditionalWrapper>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭──────────────────────╮
|
* ╭──────────────────────╮
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "components/Inputs/Button";
|
|
||||||
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
||||||
import { OrderableList } from "components/Inputs/OrderableList";
|
import { OrderableList } from "components/Inputs/OrderableList";
|
||||||
import { Select } from "components/Inputs/Select";
|
import { Select } from "components/Inputs/Select";
|
||||||
|
@ -203,23 +202,28 @@ export const SettingsPopup = (): JSX.Element => {
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl">{format("font")}</h3>
|
<h3 className="text-xl">{format("font")}</h3>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Button
|
<ButtonGroup
|
||||||
active={!isDyslexic}
|
vertical
|
||||||
onClick={() => {
|
buttonsProps={[
|
||||||
setDyslexic(false);
|
{
|
||||||
sendAnalytics("Settings", "Change font (Zen Maru Gothic)");
|
active: !isDyslexic,
|
||||||
}}
|
onClick: () => {
|
||||||
className="font-zenMaruGothic"
|
setDyslexic(false);
|
||||||
text="Zen Maru Gothic"
|
sendAnalytics("Settings", "Change font (Zen Maru Gothic)");
|
||||||
/>
|
},
|
||||||
<Button
|
className: "font-zenMaruGothic",
|
||||||
active={isDyslexic}
|
text: "Zen Maru Gothic",
|
||||||
onClick={() => {
|
},
|
||||||
setDyslexic(true);
|
{
|
||||||
sendAnalytics("Settings", "Change font (OpenDyslexic)");
|
active: isDyslexic,
|
||||||
}}
|
onClick: () => {
|
||||||
className="font-openDyslexic"
|
setDyslexic(true);
|
||||||
text="OpenDyslexic"
|
sendAnalytics("Settings", "Change font (OpenDyslexic)");
|
||||||
|
},
|
||||||
|
className: "font-openDyslexic",
|
||||||
|
text: "OpenDyslexic",
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,293 @@
|
||||||
|
import { useCallback, useEffect, useId, useState } from "react";
|
||||||
|
import Slider from "rc-slider";
|
||||||
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
|
import { Button } from "components/Inputs/Button";
|
||||||
|
import { prettyDuration } from "helpers/formatters";
|
||||||
|
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
||||||
|
import { cIf, cJoin } from "helpers/className";
|
||||||
|
import { useFullscreen } from "hooks/useFullscreen";
|
||||||
|
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts";
|
||||||
|
import { useAtomGetter } from "helpers/atoms";
|
||||||
|
import { atoms } from "contexts/atoms";
|
||||||
|
import { ToolTip } from "components/ToolTip";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭─────────────╮
|
||||||
|
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
|
const STEP_MULTIPLIER = 100;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭─────────────╮
|
||||||
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface AudioPlayerProps {
|
||||||
|
src?: string;
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AudioPlayer = ({ src, className, title }: AudioPlayerProps): JSX.Element => {
|
||||||
|
const [ref, setRef] = useState<HTMLAudioElement | null>(null);
|
||||||
|
const [isFocused, setFocus] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cJoin("w-full", className)}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={() => setFocus(true)}
|
||||||
|
onBlur={() => setFocus(false)}>
|
||||||
|
<audio ref={setRef} src={src} />
|
||||||
|
{ref && (
|
||||||
|
<PlayerControls
|
||||||
|
className={className}
|
||||||
|
mediaRef={ref}
|
||||||
|
type="audio"
|
||||||
|
src={src}
|
||||||
|
title={title}
|
||||||
|
isFocused={isFocused}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
|
interface VideoPlayerProps {
|
||||||
|
src?: string;
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
rounded?: boolean;
|
||||||
|
subSrc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoPlayer = ({
|
||||||
|
src,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
subSrc,
|
||||||
|
rounded = true,
|
||||||
|
}: VideoPlayerProps): JSX.Element => {
|
||||||
|
const [ref, setRef] = useState<HTMLVideoElement | null>(null);
|
||||||
|
const videoId = useId();
|
||||||
|
const { isFullscreen, toggleFullscreen } = useFullscreen(videoId);
|
||||||
|
const [isPlaying, setPlaying] = useState(false);
|
||||||
|
const [isFocused, setFocus] = useState(false);
|
||||||
|
|
||||||
|
const togglePlayback = useCallback(
|
||||||
|
async () => (isPlaying ? ref?.pause() : await ref?.play()),
|
||||||
|
[isPlaying, ref]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cJoin("grid w-full", className)}
|
||||||
|
id={videoId}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={() => setFocus(true)}
|
||||||
|
onBlur={() => setFocus(false)}>
|
||||||
|
<video
|
||||||
|
ref={setRef}
|
||||||
|
className={cJoin("h-full w-full", cIf(!isFullscreen && rounded, "rounded-t-4xl"))}
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
onClick={togglePlayback}
|
||||||
|
onDoubleClick={toggleFullscreen}>
|
||||||
|
<source type="video/mp4" src={src} />
|
||||||
|
{subSrc && <track label="English" kind="subtitles" srcLang="en" src={subSrc} default />}
|
||||||
|
</video>
|
||||||
|
{ref && (
|
||||||
|
<PlayerControls
|
||||||
|
title={title}
|
||||||
|
mediaRef={ref}
|
||||||
|
src={src}
|
||||||
|
type="video"
|
||||||
|
className={cIf(isFullscreen || !rounded, "rounded-none", "rounded-b-4xl rounded-t-none")}
|
||||||
|
fullscreen={{ isFullscreen, toggleFullscreen }}
|
||||||
|
onPlaybackChanged={setPlaying}
|
||||||
|
isFocused={isFocused}
|
||||||
|
hasCC={isDefined(subSrc)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
|
interface PlayerControls {
|
||||||
|
mediaRef: HTMLMediaElement;
|
||||||
|
src?: string;
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
isFocused?: boolean;
|
||||||
|
type: "audio" | "video";
|
||||||
|
fullscreen?: {
|
||||||
|
isFullscreen: boolean;
|
||||||
|
toggleFullscreen: () => void;
|
||||||
|
};
|
||||||
|
onPlaybackChanged?: (isPlaying: boolean) => void;
|
||||||
|
hasCC?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlayerControls = ({
|
||||||
|
mediaRef,
|
||||||
|
className,
|
||||||
|
src,
|
||||||
|
title,
|
||||||
|
fullscreen,
|
||||||
|
isFocused = false,
|
||||||
|
hasCC = false,
|
||||||
|
type,
|
||||||
|
onPlaybackChanged,
|
||||||
|
}: PlayerControls) => {
|
||||||
|
const [isPlaying, setPlaying] = useState(false);
|
||||||
|
const [duration, setDuration] = useState(mediaRef.duration);
|
||||||
|
const [currentTime, setCurrentTime] = useState(mediaRef.currentTime);
|
||||||
|
const [isMuted, setMuted] = useState(mediaRef.volume === 0);
|
||||||
|
const [hasEnded, setEnded] = useState(false);
|
||||||
|
const [ccVisible, setCCVisible] = useState(hasCC);
|
||||||
|
const isContentPanelAtLeastXl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastXl);
|
||||||
|
const isContentPanelAtLeastMd = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastMd);
|
||||||
|
|
||||||
|
const togglePlayback = useCallback(
|
||||||
|
async () => (isPlaying ? mediaRef.pause() : await mediaRef.play()),
|
||||||
|
[isPlaying, mediaRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
"left",
|
||||||
|
() => {
|
||||||
|
mediaRef.currentTime -= 5;
|
||||||
|
},
|
||||||
|
{ enabled: isFocused }
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
"right",
|
||||||
|
() => {
|
||||||
|
mediaRef.currentTime += 5;
|
||||||
|
},
|
||||||
|
{ enabled: isFocused }
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = mediaRef;
|
||||||
|
audio.addEventListener("loadedmetadata", () => {
|
||||||
|
setDuration(audio.duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener("play", () => {
|
||||||
|
setPlaying(true);
|
||||||
|
onPlaybackChanged?.(true);
|
||||||
|
setEnded(false);
|
||||||
|
});
|
||||||
|
audio.addEventListener("pause", () => {
|
||||||
|
setPlaying(false);
|
||||||
|
onPlaybackChanged?.(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener("ended", () => setEnded(true));
|
||||||
|
|
||||||
|
audio.addEventListener("timeupdate", () => {
|
||||||
|
setCurrentTime(audio.currentTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => audio.pause();
|
||||||
|
}, [mediaRef, onPlaybackChanged]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const textTrack = mediaRef.textTracks[0];
|
||||||
|
if (isUndefined(textTrack)) return;
|
||||||
|
textTrack.mode = ccVisible ? "showing" : "hidden";
|
||||||
|
}, [ccVisible, mediaRef.textTracks]);
|
||||||
|
|
||||||
|
const buttonGroup = (
|
||||||
|
<ButtonGroup
|
||||||
|
vertical={!isContentPanelAtLeastXl && type === "video"}
|
||||||
|
buttonsProps={[
|
||||||
|
{
|
||||||
|
icon: isMuted ? "volume_off" : "volume_up",
|
||||||
|
active: isMuted,
|
||||||
|
onClick: () => {
|
||||||
|
setMuted((oldMutedValue) => {
|
||||||
|
const newMutedValue = !oldMutedValue;
|
||||||
|
mediaRef.volume = newMutedValue ? 0 : 1;
|
||||||
|
return newMutedValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "closed_caption",
|
||||||
|
active: ccVisible,
|
||||||
|
onClick: () => setCCVisible((value) => !value),
|
||||||
|
visible: hasCC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: fullscreen?.isFullscreen ? "fullscreen_exit" : "fullscreen",
|
||||||
|
active: fullscreen?.isFullscreen,
|
||||||
|
onClick: fullscreen?.toggleFullscreen,
|
||||||
|
visible: isDefined(fullscreen),
|
||||||
|
},
|
||||||
|
{ icon: "download", href: src, alwaysNewTab: true },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cJoin(
|
||||||
|
`relative flex w-full place-items-center rounded-full
|
||||||
|
bg-highlight p-3 shadow-md shadow-shade/50`,
|
||||||
|
cIf(isContentPanelAtLeastMd, "gap-5", "gap-3"),
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
<Button
|
||||||
|
icon={hasEnded ? "replay" : isPlaying ? "pause" : "play_arrow"}
|
||||||
|
active={isPlaying}
|
||||||
|
onClick={togglePlayback}
|
||||||
|
/>
|
||||||
|
<div className="grid w-full place-items-start">
|
||||||
|
{isDefinedAndNotEmpty(title) && (
|
||||||
|
<p className="line-clamp-1 text-left text-xs text-dark">{title}</p>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cJoin(
|
||||||
|
"flex w-full place-content-between place-items-center",
|
||||||
|
cIf(isContentPanelAtLeastMd, "gap-5", "gap-3")
|
||||||
|
)}>
|
||||||
|
<p className={cJoin("font-mono", cIf(isContentPanelAtLeastMd, "h-5", "h-3 text-xs"))}>
|
||||||
|
{prettyDuration(currentTime)}
|
||||||
|
</p>
|
||||||
|
<Slider
|
||||||
|
className={cIf(
|
||||||
|
!isContentPanelAtLeastXl && type === "video",
|
||||||
|
"!absolute left-0 right-0 top-[-5px]"
|
||||||
|
)}
|
||||||
|
value={currentTime * STEP_MULTIPLIER}
|
||||||
|
onChange={(value) => {
|
||||||
|
const newTime = (value as number) / STEP_MULTIPLIER;
|
||||||
|
mediaRef.currentTime = newTime;
|
||||||
|
setCurrentTime(newTime);
|
||||||
|
}}
|
||||||
|
onAfterChange={async () => await mediaRef.play()}
|
||||||
|
max={duration * STEP_MULTIPLIER}
|
||||||
|
/>
|
||||||
|
{!isContentPanelAtLeastXl && type === "video" && <p>/</p>}
|
||||||
|
<p className={cJoin("font-mono", cIf(isContentPanelAtLeastMd, "h-5", "h-3 text-xs"))}>
|
||||||
|
{prettyDuration(duration)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isContentPanelAtLeastXl ? (
|
||||||
|
buttonGroup
|
||||||
|
) : (
|
||||||
|
<ToolTip content={buttonGroup}>
|
||||||
|
<Button icon="more_vert" />
|
||||||
|
</ToolTip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,22 +1,19 @@
|
||||||
import { Fragment, useCallback } from "react";
|
import { Fragment, useCallback } from "react";
|
||||||
import { AppLayout, AppLayoutRequired } from "./AppLayout";
|
import { AppLayout, AppLayoutRequired } from "./AppLayout";
|
||||||
import { Chip } from "./Chip";
|
|
||||||
import { getTocFromMarkdawn, Markdawn, TableOfContents } from "./Markdown/Markdawn";
|
import { getTocFromMarkdawn, Markdawn, TableOfContents } from "./Markdown/Markdawn";
|
||||||
import { ReturnButton } from "./PanelComponents/ReturnButton";
|
import { ReturnButton } from "./PanelComponents/ReturnButton";
|
||||||
import { ContentPanel } from "./Containers/ContentPanel";
|
import { ContentPanel } from "./Containers/ContentPanel";
|
||||||
import { SubPanel } from "./Containers/SubPanel";
|
import { SubPanel } from "./Containers/SubPanel";
|
||||||
import { RecorderChip } from "./RecorderChip";
|
|
||||||
import { ThumbnailHeader } from "./ThumbnailHeader";
|
import { ThumbnailHeader } from "./ThumbnailHeader";
|
||||||
import { ToolTip } from "./ToolTip";
|
|
||||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||||
import { PostWithTranslations } from "types/types";
|
import { PostWithTranslations } from "types/types";
|
||||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
import { isDefined } from "helpers/asserts";
|
||||||
import { prettySlug } from "helpers/formatters";
|
import { prettySlug } from "helpers/formatters";
|
||||||
import { useFormat } from "hooks/useFormat";
|
|
||||||
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
import { ElementsSeparator } from "helpers/component";
|
import { ElementsSeparator } from "helpers/component";
|
||||||
import { HorizontalLine } from "components/HorizontalLine";
|
import { HorizontalLine } from "components/HorizontalLine";
|
||||||
|
import { Credits } from "components/Credits";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
|
@ -51,7 +48,6 @@ export const PostPage = ({
|
||||||
displayTitle = true,
|
displayTitle = true,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const { format, formatStatusDescription } = useFormat();
|
|
||||||
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
||||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||||
|
|
||||||
|
@ -77,34 +73,7 @@ export const PostPage = ({
|
||||||
<ReturnButton href={returnHref} title={returnTitle} />
|
<ReturnButton href={returnHref} title={returnTitle} />
|
||||||
),
|
),
|
||||||
|
|
||||||
displayCredits && (
|
displayCredits && <Credits status={selectedTranslation?.status} authors={post.authors?.data} />,
|
||||||
<>
|
|
||||||
{selectedTranslation && (
|
|
||||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
|
||||||
<p className="font-headers font-bold">{format("status")}:</p>
|
|
||||||
|
|
||||||
<ToolTip
|
|
||||||
content={formatStatusDescription(selectedTranslation.status)}
|
|
||||||
maxWidth={"20rem"}>
|
|
||||||
<Chip text={selectedTranslation.status} />
|
|
||||||
</ToolTip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{post.authors && post.authors.data.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<p className="font-headers font-bold">{"Authors"}:</p>
|
|
||||||
<div className="grid place-content-center place-items-center gap-2">
|
|
||||||
{filterHasAttributes(post.authors.data, ["id", "attributes"]).map((author) => (
|
|
||||||
<Fragment key={author.id}>
|
|
||||||
<RecorderChip recorder={author.attributes} />
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
|
|
||||||
displayToc && isDefined(toc) && (
|
displayToc && isDefined(toc) && (
|
||||||
<TableOfContents toc={toc} onContentClicked={() => setSubPanelOpened(false)} />
|
<TableOfContents toc={toc} onContentClicked={() => setSubPanelOpened(false)} />
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { PostWithTranslations } from "types/types";
|
||||||
import { getOpenGraph } from "helpers/openGraph";
|
import { getOpenGraph } from "helpers/openGraph";
|
||||||
import { prettyDate, prettySlug } from "helpers/formatters";
|
import { prettyDate, prettySlug } from "helpers/formatters";
|
||||||
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
||||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
import { filterHasAttributes } from "helpers/asserts";
|
||||||
import { getDescription } from "helpers/description";
|
import { getDescription } from "helpers/description";
|
||||||
import { AppLayoutRequired } from "components/AppLayout";
|
import { AppLayoutRequired } from "components/AppLayout";
|
||||||
import { getFormat } from "helpers/i18n";
|
import { getFormat } from "helpers/i18n";
|
||||||
|
@ -22,40 +22,36 @@ export const getPostStaticProps =
|
||||||
slug: slug,
|
slug: slug,
|
||||||
language_code: context.locale ?? "en",
|
language_code: context.locale ?? "en",
|
||||||
});
|
});
|
||||||
if (
|
|
||||||
post.posts?.data &&
|
|
||||||
post.posts.data.length > 0 &&
|
|
||||||
post.posts.data[0]?.attributes?.translations &&
|
|
||||||
isDefined(context.locale) &&
|
|
||||||
isDefined(context.locales)
|
|
||||||
) {
|
|
||||||
const selectedTranslation = staticSmartLanguage({
|
|
||||||
items: post.posts.data[0].attributes.translations,
|
|
||||||
languageExtractor: (item) => item.language?.data?.attributes?.code,
|
|
||||||
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
|
|
||||||
});
|
|
||||||
|
|
||||||
const title = selectedTranslation?.title ?? prettySlug(slug);
|
if (!post.posts?.data[0]?.attributes?.translations || !context.locale || !context.locales) {
|
||||||
|
return { notFound: true };
|
||||||
const description = getDescription(selectedTranslation?.excerpt, {
|
|
||||||
[format("release_date")]: [prettyDate(post.posts.data[0].attributes.date, context.locale)],
|
|
||||||
[format("category", { count: Infinity })]: filterHasAttributes(
|
|
||||||
post.posts.data[0].attributes.categories?.data,
|
|
||||||
["attributes"]
|
|
||||||
).map((category) => category.attributes.short),
|
|
||||||
});
|
|
||||||
|
|
||||||
const thumbnail =
|
|
||||||
selectedTranslation?.thumbnail?.data?.attributes ??
|
|
||||||
post.posts.data[0].attributes.thumbnail?.data?.attributes;
|
|
||||||
|
|
||||||
const props: PostStaticProps = {
|
|
||||||
post: post.posts.data[0].attributes as PostWithTranslations,
|
|
||||||
openGraph: getOpenGraph(format, title, description, thumbnail),
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
props: props,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return { notFound: true };
|
|
||||||
|
const selectedTranslation = staticSmartLanguage({
|
||||||
|
items: post.posts.data[0].attributes.translations,
|
||||||
|
languageExtractor: (item) => item.language?.data?.attributes?.code,
|
||||||
|
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = selectedTranslation?.title ?? prettySlug(slug);
|
||||||
|
|
||||||
|
const description = getDescription(selectedTranslation?.excerpt ?? selectedTranslation?.body, {
|
||||||
|
[format("release_date")]: [prettyDate(post.posts.data[0].attributes.date, context.locale)],
|
||||||
|
[format("category", { count: Infinity })]: filterHasAttributes(
|
||||||
|
post.posts.data[0].attributes.categories?.data,
|
||||||
|
["attributes"]
|
||||||
|
).map((category) => category.attributes.short),
|
||||||
|
});
|
||||||
|
|
||||||
|
const thumbnail =
|
||||||
|
selectedTranslation?.thumbnail?.data?.attributes ??
|
||||||
|
post.posts.data[0].attributes.thumbnail?.data?.attributes;
|
||||||
|
|
||||||
|
const props: PostStaticProps = {
|
||||||
|
post: post.posts.data[0].attributes as PostWithTranslations,
|
||||||
|
openGraph: getOpenGraph(format, title, description, thumbnail),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -185,6 +185,43 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
}
|
}
|
||||||
notes
|
notes
|
||||||
}
|
}
|
||||||
|
video_set {
|
||||||
|
status
|
||||||
|
source_language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
has_subfile
|
||||||
|
subbers(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
...recorderChip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notes
|
||||||
|
}
|
||||||
|
audio_set {
|
||||||
|
status
|
||||||
|
source_language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dubbers(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
...recorderChip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnail {
|
thumbnail {
|
||||||
|
|
|
@ -133,6 +133,11 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tracks(pagination: { limit: -1 }) {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
... on ComponentMetadataGroup {
|
... on ComponentMetadataGroup {
|
||||||
subtype {
|
subtype {
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { convert } from "html-to-text";
|
||||||
|
import { sanitize } from "isomorphic-dompurify";
|
||||||
|
import { marked } from "marked";
|
||||||
import { filterDefined, isDefined, isDefinedAndNotEmpty } from "./asserts";
|
import { filterDefined, isDefined, isDefinedAndNotEmpty } from "./asserts";
|
||||||
|
|
||||||
export const getDescription = (
|
export const getDescription = (
|
||||||
|
@ -6,13 +9,6 @@ export const getDescription = (
|
||||||
): string => {
|
): string => {
|
||||||
let result = "";
|
let result = "";
|
||||||
|
|
||||||
if (isDefinedAndNotEmpty(description)) {
|
|
||||||
result += prettyMarkdown(description);
|
|
||||||
if (isDefined(chipsGroups)) {
|
|
||||||
result += "\n\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in chipsGroups) {
|
for (const key in chipsGroups) {
|
||||||
if (Object.hasOwn(chipsGroups, key)) {
|
if (Object.hasOwn(chipsGroups, key)) {
|
||||||
const chipsGroup = filterDefined(chipsGroups[key]);
|
const chipsGroup = filterDefined(chipsGroups[key]);
|
||||||
|
@ -22,11 +18,52 @@ export const getDescription = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDefinedAndNotEmpty(description)) {
|
||||||
|
if (result !== "") {
|
||||||
|
result += "\n";
|
||||||
|
}
|
||||||
|
result += prettyMarkdown(description);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const prettyMarkdown = (markdown: string): string =>
|
const block = (text: string) => `${text}\n\n`;
|
||||||
markdown.replace(/[*]/gu, "").replace(/[_]/gu, "");
|
const escapeBlock = (text: string) => `${escape(text)}\n\n`;
|
||||||
|
const line = (text: string) => `${text}\n`;
|
||||||
|
const inline = (text: string) => text;
|
||||||
|
const newline = () => "\n";
|
||||||
|
const empty = () => "";
|
||||||
|
|
||||||
|
const TxtRenderer: marked.Renderer = {
|
||||||
|
// Block elements
|
||||||
|
code: escapeBlock,
|
||||||
|
blockquote: block,
|
||||||
|
html: empty,
|
||||||
|
heading: block,
|
||||||
|
hr: newline,
|
||||||
|
list: (text) => block(text.trim()),
|
||||||
|
listitem: line,
|
||||||
|
checkbox: empty,
|
||||||
|
paragraph: block,
|
||||||
|
table: (header, body) => line(header + body),
|
||||||
|
tablerow: (text) => line(text.trim()),
|
||||||
|
tablecell: (text) => `${text} `,
|
||||||
|
// Inline elements
|
||||||
|
strong: inline,
|
||||||
|
em: inline,
|
||||||
|
codespan: inline,
|
||||||
|
br: newline,
|
||||||
|
del: inline,
|
||||||
|
link: (_0, _1, text) => text,
|
||||||
|
image: (_0, _1, text) => text,
|
||||||
|
text: inline,
|
||||||
|
// etc.
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const prettyMarkdown = (markdown: string): string =>
|
||||||
|
convert(sanitize(marked(markdown, { renderer: TxtRenderer }))).trim();
|
||||||
|
|
||||||
const prettyChip = (items: (string | undefined)[]): string =>
|
const prettyChip = (items: (string | undefined)[]): string =>
|
||||||
items
|
items
|
||||||
|
|
|
@ -16,3 +16,6 @@ export const isUntangibleGroupItem = (
|
||||||
|
|
||||||
export const getScanArchiveURL = (slug: string): string =>
|
export const getScanArchiveURL = (slug: string): string =>
|
||||||
`${process.env.NEXT_PUBLIC_URL_ASSETS}/library/scans/${slug}.zip`;
|
`${process.env.NEXT_PUBLIC_URL_ASSETS}/library/scans/${slug}.zip`;
|
||||||
|
|
||||||
|
export const getTrackURL = (itemSlug: string, trackSlug: string): string =>
|
||||||
|
`${process.env.NEXT_PUBLIC_URL_ASSETS}/library/tracks/${itemSlug}/${trackSlug}.mp3`;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getFormat } from "./i18n";
|
||||||
import { UploadImageFragment } from "graphql/generated";
|
import { UploadImageFragment } from "graphql/generated";
|
||||||
import { useFormat } from "hooks/useFormat";
|
import { useFormat } from "hooks/useFormat";
|
||||||
|
|
||||||
const DEFAULT_OG_THUMBNAIL = {
|
const DEFAULT_OG_THUMBNAIL: OgImage = {
|
||||||
image: `${process.env.NEXT_PUBLIC_URL_SELF}/default_og.jpg`,
|
image: `${process.env.NEXT_PUBLIC_URL_SELF}/default_og.jpg`,
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
|
@ -24,14 +24,26 @@ export const getOpenGraph = (
|
||||||
format: ReturnType<typeof getFormat>["format"] | ReturnType<typeof useFormat>["format"],
|
format: ReturnType<typeof getFormat>["format"] | ReturnType<typeof useFormat>["format"],
|
||||||
title?: string | null | undefined,
|
title?: string | null | undefined,
|
||||||
description?: string | null | undefined,
|
description?: string | null | undefined,
|
||||||
thumbnail?: UploadImageFragment | null | undefined
|
thumbnail?: UploadImageFragment | string | null | undefined
|
||||||
): OpenGraph => ({
|
): OpenGraph => ({
|
||||||
title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) ? `${TITLE_SEPARATOR}${title}` : ""}`,
|
title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) ? `${TITLE_SEPARATOR}${title}` : ""}`,
|
||||||
description: isDefinedAndNotEmpty(description) ? description : format("default_description"),
|
description: isDefinedAndNotEmpty(description)
|
||||||
|
? description.length > 350
|
||||||
|
? `${description.slice(0, 349)}…`
|
||||||
|
: description
|
||||||
|
: format("default_description"),
|
||||||
thumbnail: thumbnail ? getOgImage(thumbnail) : DEFAULT_OG_THUMBNAIL,
|
thumbnail: thumbnail ? getOgImage(thumbnail) : DEFAULT_OG_THUMBNAIL,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getOgImage = (image: UploadImageFragment): OgImage => {
|
const getOgImage = (image: UploadImageFragment | string): OgImage => {
|
||||||
|
if (typeof image === "string") {
|
||||||
|
return {
|
||||||
|
image,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
alt: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
const imgSize = getImgSizesByQuality(image.width ?? 0, image.height ?? 0, ImageQuality.Og);
|
const imgSize = getImgSizesByQuality(image.width ?? 0, image.height ?? 0, ImageQuality.Og);
|
||||||
return {
|
return {
|
||||||
image: getAssetURL(image.url, ImageQuality.Og),
|
image: getAssetURL(image.url, ImageQuality.Og),
|
||||||
|
|
|
@ -14,12 +14,15 @@ import { GetVideoQuery } from "graphql/generated";
|
||||||
import { getReadySdk } from "graphql/sdk";
|
import { getReadySdk } from "graphql/sdk";
|
||||||
import { prettyDate, prettyShortenNumber } from "helpers/formatters";
|
import { prettyDate, prettyShortenNumber } from "helpers/formatters";
|
||||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
||||||
import { getVideoFile } from "helpers/videos";
|
import { getVideoFile, getVideoThumbnailURL } from "helpers/videos";
|
||||||
import { getOpenGraph } from "helpers/openGraph";
|
import { getOpenGraph } from "helpers/openGraph";
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||||
import { useFormat } from "hooks/useFormat";
|
import { useFormat } from "hooks/useFormat";
|
||||||
import { getFormat } from "helpers/i18n";
|
import { getFormat } from "helpers/i18n";
|
||||||
|
import { VideoPlayer } from "components/Player";
|
||||||
|
import { getDescription } from "helpers/description";
|
||||||
|
import { Markdown } from "components/Markdown/Markdown";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭────────╮
|
* ╭────────╮
|
||||||
|
@ -65,7 +68,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
|
||||||
<div className="grid place-items-center gap-12">
|
<div className="grid place-items-center gap-12">
|
||||||
<div id="video" className="w-full overflow-hidden rounded-xl shadow-xl shadow-shade/80">
|
<div id="video" className="w-full overflow-hidden rounded-xl shadow-xl shadow-shade/80">
|
||||||
{video.gone ? (
|
{video.gone ? (
|
||||||
<video className="w-full" src={getVideoFile(video.uid)} controls />
|
<VideoPlayer className="w-full" src={getVideoFile(video.uid)} rounded={false} />
|
||||||
) : (
|
) : (
|
||||||
<iframe
|
<iframe
|
||||||
src={`https://www.youtube-nocookie.com/embed/${video.uid}`}
|
src={`https://www.youtube-nocookie.com/embed/${video.uid}`}
|
||||||
|
@ -132,7 +135,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
|
||||||
<InsetBox id="description" className="grid place-items-center">
|
<InsetBox id="description" className="grid place-items-center">
|
||||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
|
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
|
||||||
<h2 className="text-2xl">{format("description")}</h2>
|
<h2 className="text-2xl">{format("description")}</h2>
|
||||||
<p className="whitespace-pre-line">{video.description}</p>
|
<Markdown className="whitespace-pre-line" text={video.description} />
|
||||||
</div>
|
</div>
|
||||||
</InsetBox>
|
</InsetBox>
|
||||||
</div>
|
</div>
|
||||||
|
@ -158,7 +161,14 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
||||||
|
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
video: videos.videos.data[0].attributes,
|
video: videos.videos.data[0].attributes,
|
||||||
openGraph: getOpenGraph(format, videos.videos.data[0].attributes.title),
|
openGraph: getOpenGraph(
|
||||||
|
format,
|
||||||
|
videos.videos.data[0].attributes.title,
|
||||||
|
getDescription(videos.videos.data[0].attributes.description, {
|
||||||
|
[format("channel")]: [videos.videos.data[0].attributes.channel?.data?.attributes?.title],
|
||||||
|
}),
|
||||||
|
getVideoThumbnailURL(videos.videos.data[0].attributes.uid)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
|
|
|
@ -1,26 +1,18 @@
|
||||||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||||
import { Fragment, useCallback, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import Collapsible from "react-collapsible";
|
import Collapsible from "react-collapsible";
|
||||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||||
import { Chip } from "components/Chip";
|
|
||||||
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
|
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
|
||||||
import { getTocFromMarkdawn, Markdawn, TableOfContents } from "components/Markdown/Markdawn";
|
import { getTocFromMarkdawn, Markdawn, TableOfContents } from "components/Markdown/Markdawn";
|
||||||
import { TranslatedReturnButton } from "components/PanelComponents/ReturnButton";
|
import { TranslatedReturnButton } from "components/PanelComponents/ReturnButton";
|
||||||
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
|
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
|
||||||
import { SubPanel } from "components/Containers/SubPanel";
|
import { SubPanel } from "components/Containers/SubPanel";
|
||||||
import { PreviewCard, TranslatedPreviewCard } from "components/PreviewCard";
|
import { PreviewCard, TranslatedPreviewCard } from "components/PreviewCard";
|
||||||
import { RecorderChip } from "components/RecorderChip";
|
|
||||||
import { ThumbnailHeader } from "components/ThumbnailHeader";
|
import { ThumbnailHeader } from "components/ThumbnailHeader";
|
||||||
import { ToolTip } from "components/ToolTip";
|
|
||||||
import { getReadySdk } from "graphql/sdk";
|
import { getReadySdk } from "graphql/sdk";
|
||||||
import {
|
import { prettyInlineTitle, prettyItemSubType, prettySlug } from "helpers/formatters";
|
||||||
prettyInlineTitle,
|
|
||||||
prettyLanguage,
|
|
||||||
prettyItemSubType,
|
|
||||||
prettySlug,
|
|
||||||
} from "helpers/formatters";
|
|
||||||
import { isUntangibleGroupItem } from "helpers/libraryItem";
|
import { isUntangibleGroupItem } from "helpers/libraryItem";
|
||||||
import { filterHasAttributes, isDefinedAndNotEmpty } from "helpers/asserts";
|
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||||
import { ContentWithTranslations } from "types/types";
|
import { ContentWithTranslations } from "types/types";
|
||||||
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
||||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||||
|
@ -36,12 +28,18 @@ import { getFormat } from "helpers/i18n";
|
||||||
import { ElementsSeparator } from "helpers/component";
|
import { ElementsSeparator } from "helpers/component";
|
||||||
import { RelatedContentPreviewFragment } from "graphql/generated";
|
import { RelatedContentPreviewFragment } from "graphql/generated";
|
||||||
import { Button } from "components/Inputs/Button";
|
import { Button } from "components/Inputs/Button";
|
||||||
|
import { ButtonGroup, ButtonGroupProps } from "components/Inputs/ButtonGroup";
|
||||||
|
import { AudioPlayer, VideoPlayer } from "components/Player";
|
||||||
|
import { HorizontalLine } from "components/HorizontalLine";
|
||||||
|
import { Credits } from "components/Credits";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭────────╮
|
* ╭────────╮
|
||||||
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
type SetType = "audio_set" | "text_set" | "video_set";
|
||||||
|
|
||||||
interface Props extends AppLayoutRequired {
|
interface Props extends AppLayoutRequired {
|
||||||
content: ContentWithTranslations;
|
content: ContentWithTranslations;
|
||||||
}
|
}
|
||||||
|
@ -49,9 +47,7 @@ interface Props extends AppLayoutRequired {
|
||||||
const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
||||||
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
||||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||||
|
const { format } = useFormat();
|
||||||
const { format, formatStatusDescription } = useFormat();
|
|
||||||
const languages = useAtomGetter(atoms.localData.languages);
|
|
||||||
|
|
||||||
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
|
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
|
||||||
items: content.translations,
|
items: content.translations,
|
||||||
|
@ -63,6 +59,20 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
||||||
});
|
});
|
||||||
|
|
||||||
useScrollTopOnChange(Ids.ContentPanel, [selectedTranslation]);
|
useScrollTopOnChange(Ids.ContentPanel, [selectedTranslation]);
|
||||||
|
const [selectedSetType, setSelectedSetType] = useState<SetType>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDefined(selectedSetType) && selectedTranslation?.[selectedSetType]) return;
|
||||||
|
if (selectedTranslation?.text_set) {
|
||||||
|
setSelectedSetType("text_set");
|
||||||
|
} else if (selectedTranslation?.audio_set) {
|
||||||
|
setSelectedSetType("audio_set");
|
||||||
|
} else if (selectedTranslation?.video_set) {
|
||||||
|
setSelectedSetType("video_set");
|
||||||
|
} else {
|
||||||
|
setSelectedSetType(undefined);
|
||||||
|
}
|
||||||
|
}, [selectedSetType, selectedTranslation]);
|
||||||
|
|
||||||
const returnButtonProps = {
|
const returnButtonProps = {
|
||||||
href: content.folder?.data?.attributes
|
href: content.folder?.data?.attributes
|
||||||
|
@ -91,104 +101,73 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setTypeSelectorProps: ButtonGroupProps["buttonsProps"] = [
|
||||||
|
{
|
||||||
|
text: "Text",
|
||||||
|
icon: "subject",
|
||||||
|
visible: isDefined(selectedTranslation?.text_set),
|
||||||
|
onClick: () => setSelectedSetType("text_set"),
|
||||||
|
active: selectedSetType === "text_set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Audio",
|
||||||
|
icon: "headphones",
|
||||||
|
visible: isDefined(selectedTranslation?.audio_set),
|
||||||
|
onClick: () => setSelectedSetType("audio_set"),
|
||||||
|
active: selectedSetType === "audio_set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Video",
|
||||||
|
icon: "movie",
|
||||||
|
visible: isDefined(selectedTranslation?.video_set),
|
||||||
|
onClick: () => setSelectedSetType("video_set"),
|
||||||
|
active: selectedSetType === "video_set",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const subPanel = (
|
const subPanel = (
|
||||||
<SubPanel>
|
<SubPanel>
|
||||||
<ElementsSeparator>
|
<ElementsSeparator>
|
||||||
{[
|
{[
|
||||||
!is1ColumnLayout && <TranslatedReturnButton {...returnButtonProps} />,
|
!is1ColumnLayout && <TranslatedReturnButton {...returnButtonProps} />,
|
||||||
|
|
||||||
selectedTranslation?.text_set?.source_language?.data?.attributes?.code !== undefined && (
|
selectedSetType === "text_set" ? (
|
||||||
<div className="grid gap-5">
|
<Credits
|
||||||
<h2 className="text-xl">
|
key="credits"
|
||||||
{selectedTranslation.text_set.source_language.data.attributes.code ===
|
languageCode={selectedTranslation?.language?.data?.attributes?.code}
|
||||||
selectedTranslation.language?.data?.attributes?.code
|
sourceLanguageCode={
|
||||||
? format("transcript_notice")
|
selectedTranslation?.text_set?.source_language?.data?.attributes?.code
|
||||||
: format("translation_notice")}
|
}
|
||||||
</h2>
|
status={selectedTranslation?.text_set?.status}
|
||||||
|
transcribers={selectedTranslation?.text_set?.transcribers?.data}
|
||||||
{selectedTranslation.text_set.source_language.data.attributes.code !==
|
translators={selectedTranslation?.text_set?.translators?.data}
|
||||||
selectedTranslation.language?.data?.attributes?.code && (
|
proofreaders={selectedTranslation?.text_set?.proofreaders?.data}
|
||||||
<div className="grid place-items-center gap-2">
|
notes={selectedTranslation?.text_set?.notes}
|
||||||
<p className="font-headers font-bold">{format("source_language")}:</p>
|
/>
|
||||||
<Chip
|
) : selectedSetType === "audio_set" ? (
|
||||||
text={prettyLanguage(
|
<Credits
|
||||||
selectedTranslation.text_set.source_language.data.attributes.code,
|
key="credits"
|
||||||
languages
|
languageCode={selectedTranslation?.language?.data?.attributes?.code}
|
||||||
)}
|
sourceLanguageCode={
|
||||||
/>
|
selectedTranslation?.audio_set?.source_language?.data?.attributes?.code
|
||||||
</div>
|
}
|
||||||
)}
|
status={selectedTranslation?.audio_set?.status}
|
||||||
|
dubbers={selectedTranslation?.audio_set?.dubbers?.data}
|
||||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
notes={selectedTranslation?.audio_set?.notes}
|
||||||
<p className="font-headers font-bold">{format("status")}:</p>
|
/>
|
||||||
|
) : (
|
||||||
<ToolTip
|
selectedSetType === "video_set" && (
|
||||||
content={formatStatusDescription(selectedTranslation.text_set.status)}
|
<Credits
|
||||||
maxWidth={"20rem"}>
|
key="credits"
|
||||||
<Chip text={selectedTranslation.text_set.status} />
|
languageCode={selectedTranslation?.language?.data?.attributes?.code}
|
||||||
</ToolTip>
|
sourceLanguageCode={
|
||||||
</div>
|
selectedTranslation?.video_set?.source_language?.data?.attributes?.code
|
||||||
|
}
|
||||||
{selectedTranslation.text_set.transcribers &&
|
status={selectedTranslation?.video_set?.status}
|
||||||
selectedTranslation.text_set.transcribers.data.length > 0 && (
|
subbers={selectedTranslation?.video_set?.subbers?.data}
|
||||||
<div>
|
notes={selectedTranslation?.video_set?.notes}
|
||||||
<p className="font-headers font-bold">{format("transcribers")}:</p>
|
/>
|
||||||
<div className="grid place-content-center place-items-center gap-2">
|
)
|
||||||
{filterHasAttributes(selectedTranslation.text_set.transcribers.data, [
|
|
||||||
"attributes",
|
|
||||||
"id",
|
|
||||||
]).map((recorder) => (
|
|
||||||
<Fragment key={recorder.id}>
|
|
||||||
<RecorderChip recorder={recorder.attributes} />
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedTranslation.text_set.translators &&
|
|
||||||
selectedTranslation.text_set.translators.data.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<p className="font-headers font-bold">{format("translators")}:</p>
|
|
||||||
<div className="grid place-content-center place-items-center gap-2">
|
|
||||||
{filterHasAttributes(selectedTranslation.text_set.translators.data, [
|
|
||||||
"attributes",
|
|
||||||
"id",
|
|
||||||
]).map((recorder) => (
|
|
||||||
<Fragment key={recorder.id}>
|
|
||||||
<RecorderChip recorder={recorder.attributes} />
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedTranslation.text_set.proofreaders &&
|
|
||||||
selectedTranslation.text_set.proofreaders.data.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<p className="font-headers font-bold">{format("proofreaders")}:</p>
|
|
||||||
<div className="grid place-content-center place-items-center gap-2">
|
|
||||||
{filterHasAttributes(selectedTranslation.text_set.proofreaders.data, [
|
|
||||||
"attributes",
|
|
||||||
"id",
|
|
||||||
]).map((recorder) => (
|
|
||||||
<Fragment key={recorder.id}>
|
|
||||||
<RecorderChip recorder={recorder.attributes} />
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
|
|
||||||
<div>
|
|
||||||
<p className="font-headers font-bold">{format("notes")}:</p>
|
|
||||||
<div className="grid place-content-center place-items-center gap-2">
|
|
||||||
<Markdawn text={selectedTranslation.text_set.notes} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
|
|
||||||
toc && <TableOfContents toc={toc} onContentClicked={() => setSubPanelOpened(false)} />,
|
toc && <TableOfContents toc={toc} onContentClicked={() => setSubPanelOpened(false)} />,
|
||||||
|
@ -254,24 +233,35 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid place-items-center">
|
<div className="grid place-items-center">
|
||||||
<ElementsSeparator className="max-w-2xl">
|
<ElementsSeparator
|
||||||
|
separator={
|
||||||
|
selectedSetType === "text_set" ? (
|
||||||
|
<HorizontalLine className="max-w-2xl" />
|
||||||
|
) : (
|
||||||
|
<div className="py-8" />
|
||||||
|
)
|
||||||
|
}>
|
||||||
{[
|
{[
|
||||||
<ThumbnailHeader
|
<div key="thumbnailHeader" className="grid place-items-center gap-6">
|
||||||
key="thumbnailHeader"
|
<ThumbnailHeader
|
||||||
className="max-w-2xl"
|
className="max-w-2xl"
|
||||||
thumbnail={content.thumbnail?.data?.attributes}
|
thumbnail={content.thumbnail?.data?.attributes}
|
||||||
pre_title={selectedTranslation?.pre_title}
|
pre_title={selectedTranslation?.pre_title}
|
||||||
title={selectedTranslation?.title}
|
title={selectedTranslation?.title}
|
||||||
subtitle={selectedTranslation?.subtitle}
|
subtitle={selectedTranslation?.subtitle}
|
||||||
description={selectedTranslation?.description}
|
description={selectedTranslation?.description}
|
||||||
type={content.type}
|
type={content.type}
|
||||||
categories={content.categories}
|
categories={content.categories}
|
||||||
languageSwitcher={
|
languageSwitcher={
|
||||||
languageSwitcherProps.locales.size > 1 ? (
|
languageSwitcherProps.locales.size > 1 ? (
|
||||||
<LanguageSwitcher {...languageSwitcherProps} />
|
<LanguageSwitcher {...languageSwitcherProps} />
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
/>,
|
/>
|
||||||
|
{setTypeSelectorProps.filter((button) => button.visible).length > 1 && (
|
||||||
|
<ButtonGroup buttonsProps={setTypeSelectorProps} />
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
|
||||||
content.previous_contents?.data && content.previous_contents.data.length > 0 && (
|
content.previous_contents?.data && content.previous_contents.data.length > 0 && (
|
||||||
<RelatedContentsSection
|
<RelatedContentsSection
|
||||||
|
@ -283,8 +273,35 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|
||||||
selectedTranslation?.text_set?.text && (
|
selectedSetType === "text_set" && selectedTranslation?.text_set?.text ? (
|
||||||
<Markdawn className="max-w-2xl" text={selectedTranslation.text_set.text} />
|
<Markdawn className="max-w-2xl" text={selectedTranslation.text_set.text} />
|
||||||
|
) : selectedSetType === "audio_set" && selectedTranslation?.audio_set ? (
|
||||||
|
<AudioPlayer
|
||||||
|
title={prettyInlineTitle(
|
||||||
|
selectedTranslation.pre_title,
|
||||||
|
selectedTranslation.title,
|
||||||
|
selectedTranslation.subtitle
|
||||||
|
)}
|
||||||
|
src={`${process.env.NEXT_PUBLIC_URL_ASSETS}/contents/audios/\
|
||||||
|
${content.slug}_${selectedTranslation.language?.data?.attributes?.code}.mp3`}
|
||||||
|
className="max-w-2xl"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
selectedSetType === "video_set" &&
|
||||||
|
selectedTranslation?.video_set && (
|
||||||
|
<VideoPlayer
|
||||||
|
title={prettyInlineTitle(
|
||||||
|
selectedTranslation.pre_title,
|
||||||
|
selectedTranslation.title,
|
||||||
|
selectedTranslation.subtitle
|
||||||
|
)}
|
||||||
|
src={`${process.env.NEXT_PUBLIC_URL_ASSETS}/contents/videos/\
|
||||||
|
${content.slug}_${selectedTranslation.language?.data?.attributes?.code}.mp4`}
|
||||||
|
subSrc={`${process.env.NEXT_PUBLIC_URL_ASSETS}/contents/videos/\
|
||||||
|
${content.slug}_${selectedTranslation.language?.data?.attributes?.code}.vtt`}
|
||||||
|
className="max-w-[90vh]"
|
||||||
|
/>
|
||||||
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
content.next_contents?.data && content.next_contents.data.length > 0 && (
|
content.next_contents?.data && content.next_contents.data.length > 0 && (
|
||||||
|
@ -331,13 +348,16 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
||||||
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
|
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
|
||||||
});
|
});
|
||||||
if (selectedTranslation) {
|
if (selectedTranslation) {
|
||||||
|
const rawDescription = isDefinedAndNotEmpty(selectedTranslation.description)
|
||||||
|
? selectedTranslation.description
|
||||||
|
: selectedTranslation.text_set?.text;
|
||||||
return {
|
return {
|
||||||
title: prettyInlineTitle(
|
title: prettyInlineTitle(
|
||||||
selectedTranslation.pre_title,
|
selectedTranslation.pre_title,
|
||||||
selectedTranslation.title,
|
selectedTranslation.title,
|
||||||
selectedTranslation.subtitle
|
selectedTranslation.subtitle
|
||||||
),
|
),
|
||||||
description: getDescription(selectedTranslation.description, {
|
description: getDescription(rawDescription, {
|
||||||
[format("type", { count: Infinity })]: [
|
[format("type", { count: Infinity })]: [
|
||||||
content.contents.data[0].attributes.type?.data?.attributes?.titles?.[0]?.title,
|
content.contents.data[0].attributes.type?.data?.attributes?.titles?.[0]?.title,
|
||||||
],
|
],
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||||
import { getOpenGraph } from "helpers/openGraph";
|
import { getOpenGraph } from "helpers/openGraph";
|
||||||
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
|
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
|
||||||
import { Button } from "components/Inputs/Button";
|
import { Button } from "components/Inputs/Button";
|
||||||
import { cJoin } from "helpers/className";
|
import { cIf, cJoin } from "helpers/className";
|
||||||
import { HorizontalLine } from "components/HorizontalLine";
|
import { HorizontalLine } from "components/HorizontalLine";
|
||||||
import { Switch } from "components/Inputs/Switch";
|
import { Switch } from "components/Inputs/Switch";
|
||||||
import { TextInput } from "components/Inputs/TextInput";
|
import { TextInput } from "components/Inputs/TextInput";
|
||||||
|
@ -17,6 +17,7 @@ import { PreviewCard } from "components/PreviewCard";
|
||||||
import { ChroniclePreview } from "components/Chronicles/ChroniclePreview";
|
import { ChroniclePreview } from "components/Chronicles/ChroniclePreview";
|
||||||
import { PreviewFolder } from "components/Contents/PreviewFolder";
|
import { PreviewFolder } from "components/Contents/PreviewFolder";
|
||||||
import { getFormat } from "helpers/i18n";
|
import { getFormat } from "helpers/i18n";
|
||||||
|
import { AudioPlayer, VideoPlayer } from "components/Player";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭────────╮
|
* ╭────────╮
|
||||||
|
@ -32,6 +33,7 @@ const DesignSystem = (props: Props): JSX.Element => {
|
||||||
const [textInputState, setTextInputState] = useState("");
|
const [textInputState, setTextInputState] = useState("");
|
||||||
const [textAreaState, setTextAreaState] = useState("");
|
const [textAreaState, setTextAreaState] = useState("");
|
||||||
const [buttonGroupState, setButtonGroupState] = useState(0);
|
const [buttonGroupState, setButtonGroupState] = useState(0);
|
||||||
|
const [verticalButtonGroupState, setVerticalButtonGroupState] = useState(0);
|
||||||
|
|
||||||
const contentPanel = (
|
const contentPanel = (
|
||||||
<ContentPanel
|
<ContentPanel
|
||||||
|
@ -190,7 +192,7 @@ const DesignSystem = (props: Props): JSX.Element => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HorizontalLine />
|
<HorizontalLine />
|
||||||
<h3 className="text-xl">Small sized</h3>
|
<h3 className="-mt-6 text-xl">Small sized</h3>
|
||||||
|
|
||||||
<div className="grid grid-cols-[repeat(4,auto)] place-content-center gap-4">
|
<div className="grid grid-cols-[repeat(4,auto)] place-content-center gap-4">
|
||||||
<p className="self-center justify-self-start">Normal</p>
|
<p className="self-center justify-self-start">Normal</p>
|
||||||
|
@ -215,12 +217,22 @@ const DesignSystem = (props: Props): JSX.Element => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HorizontalLine />
|
<HorizontalLine />
|
||||||
<h3 className="text-xl">Groups</h3>
|
<h3 className="-mt-6 text-xl">Groups</h3>
|
||||||
<div className="grid place-items-center gap-4">
|
<div className="grid grid-cols-2 place-items-center gap-4">
|
||||||
|
<p>Normal sized</p>
|
||||||
|
<p>Small sized</p>
|
||||||
<ButtonGroup buttonsProps={[{ icon: "call_end" }, { icon: "zoom_in_map" }]} />
|
<ButtonGroup buttonsProps={[{ icon: "call_end" }, { icon: "zoom_in_map" }]} />
|
||||||
|
<ButtonGroup
|
||||||
|
buttonsProps={[{ icon: "call_end" }, { icon: "zoom_in_map" }]}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
buttonsProps={[{ icon: "car_crash" }, { icon: "timelapse" }, { icon: "leak_add" }]}
|
buttonsProps={[{ icon: "car_crash" }, { icon: "timelapse" }, { icon: "leak_add" }]}
|
||||||
/>
|
/>
|
||||||
|
<ButtonGroup
|
||||||
|
buttonsProps={[{ icon: "car_crash" }, { icon: "timelapse" }, { icon: "leak_add" }]}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
buttonsProps={[
|
buttonsProps={[
|
||||||
{ icon: "car_crash" },
|
{ icon: "car_crash" },
|
||||||
|
@ -229,6 +241,40 @@ const DesignSystem = (props: Props): JSX.Element => {
|
||||||
{ icon: "cable" },
|
{ icon: "cable" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<ButtonGroup
|
||||||
|
buttonsProps={[
|
||||||
|
{ icon: "car_crash" },
|
||||||
|
{ icon: "timelapse", text: "Label", active: true },
|
||||||
|
{ text: "Another Label" },
|
||||||
|
{ icon: "cable" },
|
||||||
|
]}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<ButtonGroup
|
||||||
|
buttonsProps={[
|
||||||
|
{
|
||||||
|
text: "Try me!",
|
||||||
|
active: buttonGroupState === 0,
|
||||||
|
onClick: () => setButtonGroupState(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ad_units",
|
||||||
|
text: "Label",
|
||||||
|
active: buttonGroupState === 1,
|
||||||
|
onClick: () => setButtonGroupState(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Yet another label",
|
||||||
|
active: buttonGroupState === 2,
|
||||||
|
onClick: () => setButtonGroupState(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "security",
|
||||||
|
active: buttonGroupState === 3,
|
||||||
|
onClick: () => setButtonGroupState(3),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
buttonsProps={[
|
buttonsProps={[
|
||||||
{
|
{
|
||||||
|
@ -253,6 +299,102 @@ const DesignSystem = (props: Props): JSX.Element => {
|
||||||
onClick: () => setButtonGroupState(3),
|
onClick: () => setButtonGroupState(3),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HorizontalLine />
|
||||||
|
|
||||||
|
<h3 className="-mt-6 text-xl">Vertical groups</h3>
|
||||||
|
<div className="grid grid-cols-2 place-items-center gap-4">
|
||||||
|
<p>Normal sized</p>
|
||||||
|
<p>Small sized</p>
|
||||||
|
<ButtonGroup buttonsProps={[{ icon: "call_end" }, { icon: "zoom_in_map" }]} vertical />
|
||||||
|
<ButtonGroup
|
||||||
|
buttonsProps={[{ icon: "call_end" }, { icon: "zoom_in_map" }]}
|
||||||
|
vertical
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<ButtonGroup
|
||||||
|
buttonsProps={[{ icon: "car_crash" }, { icon: "timelapse" }, { icon: "leak_add" }]}
|
||||||
|
vertical
|
||||||
|
/>
|
||||||
|
<ButtonGroup
|
||||||
|
buttonsProps={[{ icon: "car_crash" }, { icon: "timelapse" }, { icon: "leak_add" }]}
|
||||||
|
vertical
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<ButtonGroup
|
||||||
|
buttonsProps={[
|
||||||
|
{ icon: "car_crash" },
|
||||||
|
{ icon: "timelapse", text: "Label", active: true },
|
||||||
|
{ text: "Another Label" },
|
||||||
|
{ icon: "cable" },
|
||||||
|
]}
|
||||||
|
vertical
|
||||||
|
/>
|
||||||
|
<ButtonGroup
|
||||||
|
buttonsProps={[
|
||||||
|
{ icon: "car_crash" },
|
||||||
|
{ icon: "timelapse", text: "Label", active: true },
|
||||||
|
{ text: "Another Label" },
|
||||||
|
{ icon: "cable" },
|
||||||
|
]}
|
||||||
|
vertical
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<ButtonGroup
|
||||||
|
buttonsProps={[
|
||||||
|
{
|
||||||
|
text: "Try me!",
|
||||||
|
active: verticalButtonGroupState === 0,
|
||||||
|
onClick: () => setVerticalButtonGroupState(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ad_units",
|
||||||
|
text: "Label",
|
||||||
|
active: verticalButtonGroupState === 1,
|
||||||
|
onClick: () => setVerticalButtonGroupState(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Yet another label",
|
||||||
|
active: verticalButtonGroupState === 2,
|
||||||
|
onClick: () => setVerticalButtonGroupState(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "security",
|
||||||
|
active: verticalButtonGroupState === 3,
|
||||||
|
onClick: () => setVerticalButtonGroupState(3),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
vertical
|
||||||
|
/>
|
||||||
|
<ButtonGroup
|
||||||
|
buttonsProps={[
|
||||||
|
{
|
||||||
|
text: "Try me!",
|
||||||
|
active: verticalButtonGroupState === 0,
|
||||||
|
onClick: () => setVerticalButtonGroupState(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ad_units",
|
||||||
|
text: "Label",
|
||||||
|
active: verticalButtonGroupState === 1,
|
||||||
|
onClick: () => setVerticalButtonGroupState(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Yet another label",
|
||||||
|
active: verticalButtonGroupState === 2,
|
||||||
|
onClick: () => setVerticalButtonGroupState(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "security",
|
||||||
|
active: verticalButtonGroupState === 3,
|
||||||
|
onClick: () => setVerticalButtonGroupState(3),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
vertical
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TwoThemedSection>
|
</TwoThemedSection>
|
||||||
|
@ -814,6 +956,28 @@ const DesignSystem = (props: Props): JSX.Element => {
|
||||||
<PreviewFolder href="#" title="Disabled, with a longer title" disabled />
|
<PreviewFolder href="#" title="Disabled, with a longer title" disabled />
|
||||||
</div>
|
</div>
|
||||||
</TwoThemedSection>
|
</TwoThemedSection>
|
||||||
|
|
||||||
|
<TwoThemedSection className="grid gap-4" fullWidth>
|
||||||
|
<h3 className="mb-2 text-xl">Audio players</h3>
|
||||||
|
|
||||||
|
<AudioPlayer src="https://resha.re/public-domain/Prelude-No.15-in-G-major-BWV-860.mp3" />
|
||||||
|
<AudioPlayer
|
||||||
|
title="A longer audio track, with a title"
|
||||||
|
src="https://resha.re/public-domain/Muriel-Nguyen-Xuan-Brahms-rhapsody-opus79-1.ogg"
|
||||||
|
/>
|
||||||
|
<AudioPlayer
|
||||||
|
title={`The same audio tack, but this time, an obnoxiously long title that frankly at\
|
||||||
|
this point should stop because who in their right mind would read that much text for a title.`}
|
||||||
|
src="https://resha.re/public-domain/Muriel-Nguyen-Xuan-Brahms-rhapsody-opus79-1.ogg"
|
||||||
|
/>
|
||||||
|
<HorizontalLine />
|
||||||
|
<h3 className="mb-2 text-xl">Video players</h3>
|
||||||
|
<VideoPlayer src={`https://resha.re/public-domain/the_whistler_1944.mp4`} />
|
||||||
|
<VideoPlayer
|
||||||
|
src={`https://resha.re/public-domain/big_buck_bunny_720p_surround.mp4`}
|
||||||
|
title="Big Buck Bunny - Blender Foundation"
|
||||||
|
/>
|
||||||
|
</TwoThemedSection>
|
||||||
</ContentPanel>
|
</ContentPanel>
|
||||||
);
|
);
|
||||||
return <AppLayout {...props} contentPanel={contentPanel} />;
|
return <AppLayout {...props} contentPanel={contentPanel} />;
|
||||||
|
@ -844,10 +1008,15 @@ export const getStaticProps: GetStaticProps = (context) => {
|
||||||
interface ThemedSectionProps {
|
interface ThemedSectionProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TwoThemedSection = ({ children, className }: ThemedSectionProps) => (
|
const TwoThemedSection = ({ children, className, fullWidth }: ThemedSectionProps) => (
|
||||||
<div className="mb-12 grid grid-flow-col drop-shadow-lg shadow-shade">
|
<div
|
||||||
|
className={cJoin(
|
||||||
|
"mb-12 grid grid-flow-col drop-shadow-lg shadow-shade",
|
||||||
|
cIf(fullWidth, "w-full")
|
||||||
|
)}>
|
||||||
<LightThemeSection className={cJoin("rounded-l-xl text-black", className)}>
|
<LightThemeSection className={cJoin("rounded-l-xl text-black", className)}>
|
||||||
{children}
|
{children}
|
||||||
</LightThemeSection>
|
</LightThemeSection>
|
||||||
|
|
|
@ -38,7 +38,7 @@ import {
|
||||||
isDefinedAndNotEmpty,
|
isDefinedAndNotEmpty,
|
||||||
} from "helpers/asserts";
|
} from "helpers/asserts";
|
||||||
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
||||||
import { getScanArchiveURL, isUntangibleGroupItem } from "helpers/libraryItem";
|
import { getScanArchiveURL, getTrackURL, isUntangibleGroupItem } from "helpers/libraryItem";
|
||||||
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
||||||
import { WithLabel } from "components/Inputs/WithLabel";
|
import { WithLabel } from "components/Inputs/WithLabel";
|
||||||
import { cJoin, cIf } from "helpers/className";
|
import { cJoin, cIf } from "helpers/className";
|
||||||
|
@ -53,6 +53,7 @@ import { useFormat } from "hooks/useFormat";
|
||||||
import { getFormat } from "helpers/i18n";
|
import { getFormat } from "helpers/i18n";
|
||||||
import { ElementsSeparator } from "helpers/component";
|
import { ElementsSeparator } from "helpers/component";
|
||||||
import { ToolTip } from "components/ToolTip";
|
import { ToolTip } from "components/ToolTip";
|
||||||
|
import { AudioPlayer } from "components/Player";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
|
@ -69,9 +70,21 @@ const intersectionIds = ["summary", "gallery", "details", "subitems", "contents"
|
||||||
interface Props extends AppLayoutRequired {
|
interface Props extends AppLayoutRequired {
|
||||||
item: NonNullable<NonNullable<GetLibraryItemQuery["libraryItems"]>["data"][number]["attributes"]>;
|
item: NonNullable<NonNullable<GetLibraryItemQuery["libraryItems"]>["data"][number]["attributes"]>;
|
||||||
itemId: NonNullable<GetLibraryItemQuery["libraryItems"]>["data"][number]["id"];
|
itemId: NonNullable<GetLibraryItemQuery["libraryItems"]>["data"][number]["id"];
|
||||||
|
tracks: { id: string; title: string; src: string }[];
|
||||||
|
hasContentScans: boolean;
|
||||||
|
isVariantSet: boolean;
|
||||||
|
hasContentSection: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
const LibrarySlug = ({
|
||||||
|
item,
|
||||||
|
itemId,
|
||||||
|
tracks,
|
||||||
|
hasContentScans,
|
||||||
|
isVariantSet,
|
||||||
|
hasContentSection,
|
||||||
|
...otherProps
|
||||||
|
}: Props): JSX.Element => {
|
||||||
const currency = useAtomGetter(atoms.settings.currency);
|
const currency = useAtomGetter(atoms.settings.currency);
|
||||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||||
const { format, formatLibraryItemType } = useFormat();
|
const { format, formatLibraryItemType } = useFormat();
|
||||||
|
@ -79,6 +92,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
||||||
|
|
||||||
const isContentPanelAtLeast3xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast3xl);
|
const isContentPanelAtLeast3xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast3xl);
|
||||||
const isContentPanelAtLeastSm = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastSm);
|
const isContentPanelAtLeastSm = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastSm);
|
||||||
|
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||||
|
|
||||||
const hoverable = useDeviceSupportsHover();
|
const hoverable = useDeviceSupportsHover();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -91,19 +105,6 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
||||||
useScrollTopOnChange(Ids.ContentPanel, [itemId]);
|
useScrollTopOnChange(Ids.ContentPanel, [itemId]);
|
||||||
const currentIntersection = useIntersectionList(intersectionIds);
|
const currentIntersection = useIntersectionList(intersectionIds);
|
||||||
|
|
||||||
const isVariantSet =
|
|
||||||
item.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
|
|
||||||
item.metadata[0].subtype?.data?.attributes?.slug === "variant-set";
|
|
||||||
|
|
||||||
const hasContentScans = item.contents?.data.some(
|
|
||||||
(content) => content.attributes?.scan_set && content.attributes.scan_set.length > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasContentSection =
|
|
||||||
(item.contents && item.contents.data.length > 0) || item.download_available;
|
|
||||||
|
|
||||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
|
||||||
|
|
||||||
const subPanel = (
|
const subPanel = (
|
||||||
<SubPanel>
|
<SubPanel>
|
||||||
<ElementsSeparator>
|
<ElementsSeparator>
|
||||||
|
@ -157,7 +158,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{item.contents && item.contents.data.length > 0 && (
|
{hasContentSection && (
|
||||||
<NavOption
|
<NavOption
|
||||||
title={format("contents")}
|
title={format("contents")}
|
||||||
url={`#${intersectionIds[4]}`}
|
url={`#${intersectionIds[4]}`}
|
||||||
|
@ -546,6 +547,9 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-full gap-4">
|
<div className="grid w-full gap-4">
|
||||||
|
{tracks.map(({ id, title, src }) => (
|
||||||
|
<AudioPlayer key={id} src={src} title={title} />
|
||||||
|
))}
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
"grid items-center",
|
"grid items-center",
|
||||||
|
@ -641,10 +645,36 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tracks: Props["tracks"] = ((attributes) => {
|
||||||
|
const metadata = attributes.metadata?.[0];
|
||||||
|
if (metadata?.__typename !== "ComponentMetadataAudio" || !metadata.tracks) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return filterDefined(metadata.tracks).map((track, index) => ({
|
||||||
|
id: track.slug,
|
||||||
|
src: getTrackURL(attributes.slug, track.slug),
|
||||||
|
title: `${index + 1}. ${track.title}`,
|
||||||
|
}));
|
||||||
|
})(item.libraryItems.data[0].attributes);
|
||||||
|
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
item: item.libraryItems.data[0].attributes,
|
item: item.libraryItems.data[0].attributes,
|
||||||
itemId: item.libraryItems.data[0].id,
|
itemId: item.libraryItems.data[0].id,
|
||||||
|
tracks,
|
||||||
openGraph: getOpenGraph(format, title, description, thumbnail?.data?.attributes),
|
openGraph: getOpenGraph(format, title, description, thumbnail?.data?.attributes),
|
||||||
|
isVariantSet:
|
||||||
|
item.libraryItems.data[0].attributes.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
|
||||||
|
item.libraryItems.data[0].attributes.metadata[0].subtype?.data?.attributes?.slug ===
|
||||||
|
"variant-set",
|
||||||
|
hasContentScans:
|
||||||
|
item.libraryItems.data[0].attributes.contents?.data.some(
|
||||||
|
(content) => content.attributes?.scan_set && content.attributes.scan_set.length > 0
|
||||||
|
) ?? false,
|
||||||
|
hasContentSection:
|
||||||
|
(item.libraryItems.data[0].attributes.contents &&
|
||||||
|
item.libraryItems.data[0].attributes.contents.data.length > 0) ||
|
||||||
|
item.libraryItems.data[0].attributes.download_available ||
|
||||||
|
tracks.length > 0,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { Terminal } from "components/Cli/Terminal";
|
||||||
import { PostWithTranslations } from "types/types";
|
import { PostWithTranslations } from "types/types";
|
||||||
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
||||||
import { prettyTerminalBoxedTitle } from "helpers/terminal";
|
import { prettyTerminalBoxedTitle } from "helpers/terminal";
|
||||||
import { prettyMarkdown } from "helpers/description";
|
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
import { useAtomGetter } from "helpers/atoms";
|
import { useAtomGetter } from "helpers/atoms";
|
||||||
import { useFormat } from "hooks/useFormat";
|
import { useFormat } from "hooks/useFormat";
|
||||||
|
@ -89,11 +88,11 @@ const terminalPostPage = (post: PostWithTranslations, router: NextRouter): strin
|
||||||
result += prettyTerminalBoxedTitle(selectedTranslation.title);
|
result += prettyTerminalBoxedTitle(selectedTranslation.title);
|
||||||
if (isDefinedAndNotEmpty(selectedTranslation.excerpt)) {
|
if (isDefinedAndNotEmpty(selectedTranslation.excerpt)) {
|
||||||
result += "\n\n";
|
result += "\n\n";
|
||||||
result += prettyMarkdown(selectedTranslation.excerpt);
|
result += selectedTranslation.excerpt;
|
||||||
}
|
}
|
||||||
if (isDefinedAndNotEmpty(selectedTranslation.body)) {
|
if (isDefinedAndNotEmpty(selectedTranslation.body)) {
|
||||||
result += "\n\n";
|
result += "\n\n";
|
||||||
result += prettyMarkdown(selectedTranslation.body);
|
result += selectedTranslation.body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,3 +67,9 @@ textarea:disabled {
|
||||||
textarea {
|
textarea {
|
||||||
@apply scrollbar-none;
|
@apply scrollbar-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* VIDEO CC */
|
||||||
|
|
||||||
|
video::cue {
|
||||||
|
@apply bg-[black]/70 font-body;
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.rc-slider-rail {
|
.rc-slider-rail {
|
||||||
@apply h-2 rounded-full bg-mid/80;
|
@apply h-2 rounded-full bg-mid;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue