fmt-lint
This commit is contained in:
parent
67541bef79
commit
aeeed1cb65
|
@ -1,4 +1,4 @@
|
||||||
name: 'deploy'
|
name: "deploy"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -6,6 +6,7 @@ on:
|
||||||
- main
|
- main
|
||||||
- dev
|
- dev
|
||||||
- feature/email-templates
|
- feature/email-templates
|
||||||
|
- feature/biome
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
@ -17,10 +18,10 @@ jobs:
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: "18"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Run check
|
- name: Run check
|
||||||
run: npm run check
|
run: npm run check
|
||||||
|
@ -40,13 +41,33 @@ jobs:
|
||||||
- name: Run Biome
|
- name: Run Biome
|
||||||
run: biome ci .
|
run: biome ci .
|
||||||
|
|
||||||
|
test_with_playwright:
|
||||||
|
needs: install_dependencies
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright
|
||||||
|
run: npm install playwright
|
||||||
|
|
||||||
|
- name: Run Playwright Test
|
||||||
|
run: npx playwright test/discoursio-webapp.check.js
|
||||||
|
|
||||||
push:
|
push:
|
||||||
|
needs: test_with_playwright
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
uses: ad-m/github-push-action@master
|
uses: ad-m/github-push-action@master
|
||||||
with:
|
|
||||||
branch: ${{ github.head_ref }}
|
|
||||||
|
|
||||||
update_mailgun_template:
|
update_mailgun_template:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -57,34 +78,34 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: 'Email confirmation template'
|
- name: "Email confirmation template"
|
||||||
uses: gyto/mailgun-template-action@v2
|
uses: gyto/mailgun-template-action@v2
|
||||||
with:
|
with:
|
||||||
html-file: './templates/authorizer_email_confirmation.html'
|
html-file: "./templates/authorizer_email_confirmation.html"
|
||||||
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
||||||
mailgun-domain: 'discours.io'
|
mailgun-domain: "discours.io"
|
||||||
mailgun-template: 'authorizer_email_confirmation'
|
mailgun-template: "authorizer_email_confirmation"
|
||||||
|
|
||||||
- name: 'Password reset template'
|
- name: "Password reset template"
|
||||||
uses: gyto/mailgun-template-action@v2
|
uses: gyto/mailgun-template-action@v2
|
||||||
with:
|
with:
|
||||||
html-file: './templates/authorizer_password_reset.html'
|
html-file: "./templates/authorizer_password_reset.html"
|
||||||
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
||||||
mailgun-domain: 'discours.io'
|
mailgun-domain: "discours.io"
|
||||||
mailgun-template: 'authorizer_password_reset'
|
mailgun-template: "authorizer_password_reset"
|
||||||
|
|
||||||
- name: 'First publication notification'
|
- name: "First publication notification"
|
||||||
uses: gyto/mailgun-template-action@v2
|
uses: gyto/mailgun-template-action@v2
|
||||||
with:
|
with:
|
||||||
html-file: './templates/first_publication_notification.html'
|
html-file: "./templates/first_publication_notification.html"
|
||||||
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
||||||
mailgun-domain: 'discours.io'
|
mailgun-domain: "discours.io"
|
||||||
mailgun-template: 'first_publication_notification'
|
mailgun-template: "first_publication_notification"
|
||||||
|
|
||||||
- name: 'New comment notification template'
|
- name: "New comment notification template"
|
||||||
uses: gyto/mailgun-template-action@v2
|
uses: gyto/mailgun-template-action@v2
|
||||||
with:
|
with:
|
||||||
html-file: './templates/new_comment_notification.html'
|
html-file: "./templates/new_comment_notification.html"
|
||||||
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
||||||
mailgun-domain: 'discours.io'
|
mailgun-domain: "discours.io"
|
||||||
mailgun-template: 'new_comment_notification'
|
mailgun-template: "new_comment_notification"
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
---
|
|
||||||
stages:
|
|
||||||
- deploy
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
image:
|
|
||||||
name: alpine/git
|
|
||||||
entrypoint: [""]
|
|
||||||
stage: deploy
|
|
||||||
environment:
|
|
||||||
name: production
|
|
||||||
url: https://new.discours.io
|
|
||||||
only:
|
|
||||||
- main
|
|
||||||
script:
|
|
||||||
- mkdir ~/.ssh
|
|
||||||
- echo "${HOST_KEY}" > ~/.ssh/known_hosts
|
|
||||||
- echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa
|
|
||||||
- chmod 0400 ~/.ssh/id_rsa
|
|
||||||
- git remote add github git@github.com:Discours/discoursio-webapp.git
|
|
||||||
- git push github HEAD:main
|
|
|
@ -1,5 +1,5 @@
|
||||||
"lint-staged": {
|
{
|
||||||
"*.{js,ts,cjs,mjs,d.mts,jsx,tsx,json,jsonc}": ["biome check --apply --no-errors-on-unmatched"],
|
'*.{js,ts,cjs,mjs,d.mts,jsx,tsx,json,jsonc}': [ 'biome check --apply --no-errors-on-unmatched' ],
|
||||||
"package.json": "sort-package-json",
|
"package.json": "sort-package-json",
|
||||||
"public/locales/**/*.json": "sort-json"
|
"public/locales/**/*.json": "sort-json"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"htmlWhitespaceSensitivity": "ignore",
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"proseWrap": "always",
|
|
||||||
"printWidth": 108,
|
|
||||||
"plugins": [],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": "*.ts",
|
|
||||||
"options": {
|
|
||||||
"parser": "typescript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"extends": [
|
|
||||||
"stylelint-config-standard-scss"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"stylelint-order",
|
|
||||||
"stylelint-scss"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"selector-class-pattern": null,
|
|
||||||
"no-descending-specificity": null,
|
|
||||||
"scss/function-no-unknown": null,
|
|
||||||
"scss/no-global-function-names": null,
|
|
||||||
"function-url-quotes": null,
|
|
||||||
"font-family-no-missing-generic-family-keyword": null,
|
|
||||||
"order/order": [
|
|
||||||
"custom-properties",
|
|
||||||
"declarations"
|
|
||||||
],
|
|
||||||
"scss/dollar-variable-pattern": ["^[a-z][a-zA-Z]+$", {
|
|
||||||
"ignore": "global"
|
|
||||||
}],
|
|
||||||
"selector-pseudo-class-no-unknown": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
"ignorePseudoClasses": ["global", "export"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"defaultSeverity": "warning"
|
|
||||||
}
|
|
10
README.md
10
README.md
|
@ -3,12 +3,6 @@
|
||||||
npm install
|
npm install
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
with different backends
|
|
||||||
```
|
|
||||||
npm run start:local
|
|
||||||
npm run start:production
|
|
||||||
npm run start:staging
|
|
||||||
```
|
|
||||||
|
|
||||||
## Useful commands
|
## Useful commands
|
||||||
run checks
|
run checks
|
||||||
|
@ -23,10 +17,10 @@ npm run typecheck:watch
|
||||||
|
|
||||||
generate new SolidJS component:
|
generate new SolidJS component:
|
||||||
```
|
```
|
||||||
npx hygen component new NewComponentName
|
npm run hygen component new NewComponentName
|
||||||
```
|
```
|
||||||
|
|
||||||
generate new SolidJS context:
|
generate new SolidJS context:
|
||||||
```
|
```
|
||||||
npx hygen context new NewContextName
|
npm run hygen context new NewContextName
|
||||||
```
|
```
|
||||||
|
|
22
biome.json
22
biome.json
|
@ -21,18 +21,32 @@
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"all": true,
|
"recommended": true,
|
||||||
"complexity": {
|
"complexity": {
|
||||||
"noForEach": "off"
|
"noForEach": "off",
|
||||||
|
"useOptionalChain": "warn"
|
||||||
|
},
|
||||||
|
"a11y": {
|
||||||
|
"useKeyWithClickEvents": "off",
|
||||||
|
"useKeyWithMouseEvents": "off",
|
||||||
|
"useAnchorContent": "off",
|
||||||
|
"useValidAnchor": "off",
|
||||||
|
"useMediaCaption": "off",
|
||||||
|
"useAltText": "off",
|
||||||
|
"useButtonType": "off",
|
||||||
|
"noRedundantAlt": "off",
|
||||||
|
"noSvgWithoutTitle": "off"
|
||||||
},
|
},
|
||||||
"nursery": {
|
"nursery": {
|
||||||
"useImportRestrictions": "off"
|
"useImportRestrictions": "off"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"useNamingConvention": "off"
|
"useNamingConvention": "off",
|
||||||
|
"noUnusedTemplateLiteral": "off"
|
||||||
},
|
},
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noConsoleLog": "off"
|
"noConsoleLog": "off",
|
||||||
|
"noAssignInExpressions": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs.t
|
to: gen/<%= name %>/<%= action || 'new' %>/hello.ejs.t
|
||||||
---
|
---
|
||||||
---
|
---
|
||||||
to: app/hello.js
|
to: app/hello.js
|
||||||
|
@ -14,5 +14,3 @@ https://github.com/jondot/hygen
|
||||||
```
|
```
|
||||||
|
|
||||||
console.log(hello)
|
console.log(hello)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs.t
|
to: gen/<%= name %>/<%= action || 'new' %>/hello.ejs.t
|
||||||
---
|
---
|
||||||
---
|
---
|
||||||
to: app/hello.js
|
to: app/hello.js
|
||||||
|
@ -14,5 +14,3 @@ https://github.com/jondot/hygen
|
||||||
```
|
```
|
||||||
|
|
||||||
console.log(hello)
|
console.log(hello)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
to: _templates/<%= name %>/<%= action || 'new' %>/prompt.js
|
to: gen/<%= name %>/<%= action || 'new' %>/prompt.js
|
||||||
---
|
---
|
||||||
|
|
||||||
// see types of prompts:
|
// see types of prompts:
|
900
package-lock.json
generated
900
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "discoursio-webapp",
|
"name": "discoursio-webapp",
|
||||||
"version": "0.9.1",
|
"version": "0.9.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -12,6 +12,7 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"fix": "npm run lint:code:fix && npm run lint:styles:fix",
|
"fix": "npm run lint:code:fix && npm run lint:styles:fix",
|
||||||
"format": "npx @biomejs/biome format src/. --write",
|
"format": "npx @biomejs/biome format src/. --write",
|
||||||
|
"hygen": "HYGEN_TMPLS=gen hygen",
|
||||||
"postinstall": "npm run codegen",
|
"postinstall": "npm run codegen",
|
||||||
"lint": "npm run lint:code && npm run lint:styles",
|
"lint": "npm run lint:code && npm run lint:styles",
|
||||||
"lint:code": "npx @biomejs/biome lint src --log-kind=pretty --verbose",
|
"lint:code": "npx @biomejs/biome lint src --log-kind=pretty --verbose",
|
||||||
|
@ -23,9 +24,6 @@
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"start:local": "cross-env PUBLIC_API_URL=http://127.0.0.1:8080 vite",
|
|
||||||
"start:production": "cross-env PUBLIC_API_URL=https://v2.discours.io vite",
|
|
||||||
"start:staging": "cross-env PUBLIC_API_URL=https://testapi.discours.io vite",
|
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"typecheck:watch": "tsc --noEmit --watch"
|
"typecheck:watch": "tsc --noEmit --watch"
|
||||||
},
|
},
|
||||||
|
@ -96,8 +94,6 @@
|
||||||
"@tiptap/extension-youtube": "2.0.3",
|
"@tiptap/extension-youtube": "2.0.3",
|
||||||
"@types/js-cookie": "3.0.6",
|
"@types/js-cookie": "3.0.6",
|
||||||
"@types/node": "20.9.0",
|
"@types/node": "20.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "6.10.0",
|
|
||||||
"@typescript-eslint/parser": "6.10.0",
|
|
||||||
"@urql/core": "3.2.2",
|
"@urql/core": "3.2.2",
|
||||||
"@urql/devtools": "2.0.3",
|
"@urql/devtools": "2.0.3",
|
||||||
"babel-preset-solid": "1.8.4",
|
"babel-preset-solid": "1.8.4",
|
||||||
|
|
|
@ -22,8 +22,8 @@ const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const x = (a?.stat && a.stat.rating) || 0
|
const x = a.stat?.rating || 0
|
||||||
const y = (b?.stat && b.stat.rating) || 0
|
const y = b.stat?.rating || 0
|
||||||
|
|
||||||
if (x > y) {
|
if (x > y) {
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -88,9 +88,8 @@ export const FullArticle = (props: Props) => {
|
||||||
if (mt) {
|
if (mt) {
|
||||||
mt.title = lang() === 'en' ? capitalize(mt.slug.replace(/-/, ' ')) : mt.title
|
mt.title = lang() === 'en' ? capitalize(mt.slug.replace(/-/, ' ')) : mt.title
|
||||||
return mt
|
return mt
|
||||||
} else {
|
|
||||||
return props.article.topics[0]
|
|
||||||
}
|
}
|
||||||
|
return props.article.topics[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
const canEdit = () => props.article.authors?.some((a) => Boolean(a) && a?.slug === author()?.slug)
|
const canEdit = () => props.article.authors?.some((a) => Boolean(a) && a?.slug === author()?.slug)
|
||||||
|
@ -284,7 +283,7 @@ export const FullArticle = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleArticleBodyClick = (event) => {
|
const handleArticleBodyClick = (event) => {
|
||||||
if (event.target.tagName === 'IMG' && !event.target.dataset['disableLightbox']) {
|
if (event.target.tagName === 'IMG' && !event.target.dataset.disableLightbox) {
|
||||||
const src = event.target.src
|
const src = event.target.src
|
||||||
openLightbox(getImageUrl(src))
|
openLightbox(getImageUrl(src))
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const Userpic = (props: Props) => {
|
||||||
const letters = () => {
|
const letters = () => {
|
||||||
if (!props.name) return
|
if (!props.name) return
|
||||||
const names = props.name ? props.name.split(' ') : []
|
const names = props.name ? props.name.split(' ') : []
|
||||||
return names[0][0 ?? names[0][0]] + '.' + (names.length > 1 ? names[1][0] + '.' : '')
|
return `${names[0][0 ?? names[0][0]]}.${names.length > 1 ? `${names[1][0]}.` : ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarSize = createMemo(() => {
|
const avatarSize = createMemo(() => {
|
||||||
|
@ -48,7 +48,7 @@ export const Userpic = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], {
|
class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], {
|
||||||
['cursorPointer']: props.onClick,
|
cursorPointer: props.onClick,
|
||||||
})}
|
})}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
|
|
|
@ -29,8 +29,7 @@ export const Donate = () => {
|
||||||
} = useSnackbar()
|
} = useSnackbar()
|
||||||
|
|
||||||
const initiated = () => {
|
const initiated = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const CloudPayments = window.cp // Checkout(cpOptions)
|
||||||
const CloudPayments = window['cp'] // Checkout(cpOptions)
|
|
||||||
setWidget(new CloudPayments())
|
setWidget(new CloudPayments())
|
||||||
console.log('[donate] payments initiated')
|
console.log('[donate] payments initiated')
|
||||||
setCustomerReciept({
|
setCustomerReciept({
|
||||||
|
@ -60,7 +59,6 @@ export const Donate = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
script.type = 'text/javascript'
|
script.type = 'text/javascript'
|
||||||
script.src = 'https://widget.cloudpayments.ru/bundles/cloudpayments.js'
|
script.src = 'https://widget.cloudpayments.ru/bundles/cloudpayments.js'
|
||||||
|
@ -76,8 +74,8 @@ export const Donate = () => {
|
||||||
const choice: HTMLInputElement | undefined | null =
|
const choice: HTMLInputElement | undefined | null =
|
||||||
amountSwitchElement?.querySelector('input[type=radio]:checked')
|
amountSwitchElement?.querySelector('input[type=radio]:checked')
|
||||||
setAmount(Number.parseInt(customAmountElement?.value || choice?.value || '0'))
|
setAmount(Number.parseInt(customAmountElement?.value || choice?.value || '0'))
|
||||||
console.log('[donate] input amount ' + amount)
|
console.log(`[donate] input amount ${amount}`)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// biome-ignore lint/suspicious/noExplicitAny: it's a widget!
|
||||||
;(widget() as any).charge(
|
;(widget() as any).charge(
|
||||||
{
|
{
|
||||||
// options
|
// options
|
||||||
|
@ -105,7 +103,7 @@ export const Donate = () => {
|
||||||
console.debug('[donate] options', opts)
|
console.debug('[donate] options', opts)
|
||||||
showModal('thank')
|
showModal('thank')
|
||||||
},
|
},
|
||||||
function (reason: string, options) {
|
(reason: string, options) => {
|
||||||
// fail
|
// fail
|
||||||
// действие при неуспешной оплате
|
// действие при неуспешной оплате
|
||||||
console.debug('[donate] options', options)
|
console.debug('[donate] options', options)
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const Footer = () => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
|
|
||||||
const changeLangTitle = createMemo(() => (lang() === 'ru' ? 'English' : 'Русский'))
|
const changeLangTitle = createMemo(() => (lang() === 'ru' ? 'English' : 'Русский'))
|
||||||
const changeLangLink = createMemo(() => '?lng=' + (lang() === 'ru' ? 'en' : 'ru'))
|
const changeLangLink = createMemo(() => `?lng=${lang() === 'ru' ? 'en' : 'ru'}`)
|
||||||
const links = createMemo(() => [
|
const links = createMemo(() => [
|
||||||
{
|
{
|
||||||
header: 'About the project',
|
header: 'About the project',
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const InsertLinkForm = (props: Props) => {
|
||||||
const currentUrl = createEditorTransaction(
|
const currentUrl = createEditorTransaction(
|
||||||
() => props.editor,
|
() => props.editor,
|
||||||
(ed) => {
|
(ed) => {
|
||||||
return (ed && ed.getAttributes('link').href) || ''
|
return ed?.getAttributes('link').href || ''
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const handleClearLinkForm = () => {
|
const handleClearLinkForm = () => {
|
||||||
|
|
|
@ -173,7 +173,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
createEditorTransaction(
|
createEditorTransaction(
|
||||||
() => editor(),
|
() => editor(),
|
||||||
(ed) => {
|
(ed) => {
|
||||||
return ed && ed.isActive(name)
|
return ed?.isActive(name)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
const isActive = (name: string, attributes?: unknown) =>
|
const isActive = (name: string, attributes?: unknown) =>
|
||||||
createEditorTransaction(
|
createEditorTransaction(
|
||||||
() => props.editor,
|
() => props.editor,
|
||||||
(editor) => editor && editor.isActive(name, attributes),
|
(editor) => editor?.isActive(name, attributes),
|
||||||
)
|
)
|
||||||
|
|
||||||
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
|
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
|
||||||
|
|
|
@ -41,9 +41,8 @@ export const ToggleTextWrap = Extension.create({
|
||||||
if (changesApplied) {
|
if (changesApplied) {
|
||||||
dispatch(tr)
|
dispatch(tr)
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -36,7 +36,7 @@ const CreateModalContent = (props: Props) => {
|
||||||
return user.selected === true
|
return user.selected === true
|
||||||
})
|
})
|
||||||
.map((user) => {
|
.map((user) => {
|
||||||
return user['id']
|
return user.id
|
||||||
})
|
})
|
||||||
return [...s]
|
return [...s]
|
||||||
})
|
})
|
||||||
|
|
|
@ -26,8 +26,8 @@ type DialogProps = {
|
||||||
|
|
||||||
const DialogCard = (props: DialogProps) => {
|
const DialogCard = (props: DialogProps) => {
|
||||||
const { t, formatTime } = useLocalize()
|
const { t, formatTime } = useLocalize()
|
||||||
const companions = createMemo(
|
const companions = createMemo(() =>
|
||||||
() => props.members && props.members.filter((member: ChatMember) => member.id !== props.ownId),
|
props.members?.filter((member: ChatMember) => member.id !== props.ownId),
|
||||||
)
|
)
|
||||||
|
|
||||||
const names = createMemo<string>(() => (companions() || []).map((companion) => companion.name).join(', '))
|
const names = createMemo<string>(() => (companions() || []).map((companion) => companion.name).join(', '))
|
||||||
|
|
|
@ -68,7 +68,7 @@ export const ChangePasswordForm = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Show when={validationErrors()}>
|
<Show when={validationErrors()}>
|
||||||
<div>{validationErrors()['password']}</div>
|
<div>{validationErrors().password}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<PasswordField
|
<PasswordField
|
||||||
errorMessage={(err) => setPasswordError(err)}
|
errorMessage={(err) => setPasswordError(err)}
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const ForgotPasswordForm = () => {
|
||||||
redirect_uri: window.location.origin,
|
redirect_uri: window.location.origin,
|
||||||
})
|
})
|
||||||
console.debug('[ForgotPasswordForm] authorizer response:', data)
|
console.debug('[ForgotPasswordForm] authorizer response:', data)
|
||||||
if (errors && errors.some((error) => error.message.includes('bad user credentials'))) {
|
if (errors?.some((error) => error.message.includes('bad user credentials'))) {
|
||||||
setIsUserNotFound(true)
|
setIsUserNotFound(true)
|
||||||
}
|
}
|
||||||
if (data.message) setMessage(data.message)
|
if (data.message) setMessage(data.message)
|
||||||
|
|
|
@ -50,7 +50,7 @@ export const PasswordField = (props: Props) => {
|
||||||
on(
|
on(
|
||||||
() => error(),
|
() => error(),
|
||||||
() => {
|
() => {
|
||||||
props.errorMessage && props.errorMessage(error())
|
props.errorMessage?.(error())
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
|
|
|
@ -113,7 +113,7 @@ export const RegisterForm = () => {
|
||||||
redirect_uri: window.location.origin,
|
redirect_uri: window.location.origin,
|
||||||
}
|
}
|
||||||
const { errors } = await signUp(opts)
|
const { errors } = await signUp(opts)
|
||||||
if (errors && errors.some((error) => error.message.includes('has already signed up'))) {
|
if (errors?.some((error) => error.message.includes('has already signed up'))) {
|
||||||
setValidationErrors((prev) => ({
|
setValidationErrors((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
email: (
|
email: (
|
||||||
|
|
|
@ -35,8 +35,10 @@
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a, button {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facebook,
|
.facebook,
|
||||||
|
|
|
@ -20,9 +20,9 @@ export const SocialProviders = () => {
|
||||||
<div class={styles.social}>
|
<div class={styles.social}>
|
||||||
<For each={PROVIDERS}>
|
<For each={PROVIDERS}>
|
||||||
{(provider) => (
|
{(provider) => (
|
||||||
<a href="#" class={styles[provider]} onClick={(_e) => oauth(provider)}>
|
<button class={styles[provider]} onClick={(_e) => oauth(provider)}>
|
||||||
<Icon name={provider} />
|
<Icon name={provider} />
|
||||||
</a>
|
</button>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -135,7 +135,7 @@ export const Header = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let timer
|
let timer: string | number | NodeJS.Timeout
|
||||||
|
|
||||||
const clearTimer = () => {
|
const clearTimer = () => {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
|
@ -264,7 +264,7 @@ export const Header = (props: Props) => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h4 innerHTML={t('Subscribe us')} />
|
<h4>{t('Subscribe us')}</h4>
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li class={styles.mainNavigationSocial}>
|
<li class={styles.mainNavigationSocial}>
|
||||||
<a href="https://www.instagram.com/discoursio/">
|
<a href="https://www.instagram.com/discoursio/">
|
||||||
|
@ -358,14 +358,14 @@ export const Header = (props: Props) => {
|
||||||
<Icon name="comment" class={styles.icon} />
|
<Icon name="comment" class={styles.icon} />
|
||||||
<Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} />
|
<Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||||
</div>
|
</div>
|
||||||
<a href="#" class={styles.control} onClick={handleCreateButtonClick}>
|
<button class={styles.control} onClick={handleCreateButtonClick}>
|
||||||
<Icon name="pencil-outline" class={styles.icon} />
|
<Icon name="pencil-outline" class={styles.icon} />
|
||||||
<Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} />
|
<Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||||
</a>
|
</button>
|
||||||
<a href="#" class={styles.control} onClick={handleBookmarkButtonClick}>
|
<button class={styles.control} onClick={handleBookmarkButtonClick}>
|
||||||
<Icon name="bookmark" class={styles.icon} />
|
<Icon name="bookmark" class={styles.icon} />
|
||||||
<Icon name="bookmark-hover" class={clsx(styles.icon, styles.iconHover)} />
|
<Icon name="bookmark-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
@ -417,7 +417,7 @@ export const Header = (props: Props) => {
|
||||||
<a href="/podcasts">{t('Podcasts')}</a>
|
<a href="/podcasts">{t('Podcasts')}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="item">
|
<li class="item">
|
||||||
<a href="">{t('Special Projects')}</a>
|
<a href="/about/projects">{t('Special Projects')}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/topic/interview">#{t('Interview')}</a>
|
<a href="/topic/interview">#{t('Interview')}</a>
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const Modal = (props: Props) => {
|
||||||
const handleHide = () => {
|
const handleHide = () => {
|
||||||
if (modal()) {
|
if (modal()) {
|
||||||
if (allowClose()) {
|
if (allowClose()) {
|
||||||
props.onClose && props.onClose()
|
props.onClose?.()
|
||||||
} else {
|
} else {
|
||||||
redirectPage(router, 'home')
|
redirectPage(router, 'home')
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ export const Modal = (props: Props) => {
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.modal, {
|
class={clsx(styles.modal, {
|
||||||
[styles.narrow]: props.variant === 'narrow',
|
[styles.narrow]: props.variant === 'narrow',
|
||||||
['col-auto col-md-20 offset-md-2 col-lg-14 offset-lg-5']: props.variant === 'medium',
|
'col-auto col-md-20 offset-md-2 col-lg-14 offset-lg-5': props.variant === 'medium',
|
||||||
[styles.noPadding]: props.noPadding,
|
[styles.noPadding]: props.noPadding,
|
||||||
[styles.maxHeight]: props.maxHeight,
|
[styles.maxHeight]: props.maxHeight,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const Topics = () => {
|
||||||
<a href="/podcasts">{t('Podcasts')}</a>
|
<a href="/podcasts">{t('Podcasts')}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class={styles.item}>
|
<li class={styles.item}>
|
||||||
<a href="">{t('Special Projects')}</a>
|
<a href="/about/projects">{t('Special Projects')}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class={styles.item}>
|
<li class={styles.item}>
|
||||||
<a href="/topic/interview">#{t('Interview')}</a>
|
<a href="/topic/interview">#{t('Interview')}</a>
|
||||||
|
|
|
@ -25,7 +25,7 @@ const getTitle = (title: string) => {
|
||||||
const shoutTitleWords = title.split(' ')
|
const shoutTitleWords = title.split(' ')
|
||||||
|
|
||||||
while (shoutTitle.length <= 30 && i < shoutTitleWords.length) {
|
while (shoutTitle.length <= 30 && i < shoutTitleWords.length) {
|
||||||
shoutTitle += shoutTitleWords[i] + ' '
|
shoutTitle += `${shoutTitleWords[i]} `
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const AllAuthorsView = (props: Props) => {
|
||||||
setOffsetByFollowers((o) => o + PAGE_SIZE)
|
setOffsetByFollowers((o) => o + PAGE_SIZE)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStatsLoaded = createMemo(() => sortedAuthors() && sortedAuthors().some((author) => author.stat))
|
const isStatsLoaded = createMemo(() => sortedAuthors()?.some((author) => author.stat))
|
||||||
|
|
||||||
createEffect(async () => {
|
createEffect(async () => {
|
||||||
if (!isStatsLoaded()) {
|
if (!isStatsLoaded()) {
|
||||||
|
|
|
@ -59,7 +59,7 @@ export const AuthorView = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (author() && author().id && !author().stat) {
|
if (author()?.id && !author().stat) {
|
||||||
const a = loadAuthor({ slug: '', author_id: author().id })
|
const a = loadAuthor({ slug: '', author_id: author().id })
|
||||||
console.debug(`[AuthorView] loaded author:`, a)
|
console.debug(`[AuthorView] loaded author:`, a)
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,7 +176,7 @@ export const EditView = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let autoSaveTimeOutId
|
let autoSaveTimeOutId: number | string | NodeJS.Timeout
|
||||||
|
|
||||||
const autoSaveRecursive = () => {
|
const autoSaveRecursive = () => {
|
||||||
autoSaveTimeOutId = setTimeout(async () => {
|
autoSaveTimeOutId = setTimeout(async () => {
|
||||||
|
|
|
@ -129,11 +129,11 @@ export const InboxView = (props: Props) => {
|
||||||
})
|
})
|
||||||
if (sortByPerToPer()) {
|
if (sortByPerToPer()) {
|
||||||
return sorted.filter((chat) => (chat.title || '').trim().length === 0)
|
return sorted.filter((chat) => (chat.title || '').trim().length === 0)
|
||||||
} else if (sortByGroup()) {
|
|
||||||
return sorted.filter((chat) => (chat.title || '').trim().length > 0)
|
|
||||||
} else {
|
|
||||||
return sorted
|
|
||||||
}
|
}
|
||||||
|
if (sortByGroup()) {
|
||||||
|
return sorted.filter((chat) => (chat.title || '').trim().length > 0)
|
||||||
|
}
|
||||||
|
return sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
const findToReply = (messageId: number) => {
|
const findToReply = (messageId: number) => {
|
||||||
|
|
|
@ -67,7 +67,7 @@ export const SearchView = (props: Props) => {
|
||||||
name="q"
|
name="q"
|
||||||
ref={searchEl}
|
ref={searchEl}
|
||||||
onInput={handleQueryChange}
|
onInput={handleQueryChange}
|
||||||
placeholder={query() || t('Enter text') + '...'}
|
placeholder={query() || `${t('Enter text')}...`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const DarkModeToggle = (props: Props) => {
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
setEditorDarkMode(false)
|
setEditorDarkMode(false)
|
||||||
delete document.documentElement.dataset.editorDarkMode
|
document.documentElement.dataset.editorDarkMode = undefined
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ export const DropArea = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.DropArea, props.class, props.isSquare && styles['square'])}>
|
<div class={clsx(styles.DropArea, props.class, props.isSquare && styles.square)}>
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.field, { [styles.active]: dragActive() })}
|
class={clsx(styles.field, { [styles.active]: dragActive() })}
|
||||||
onDragEnter={handleDrag}
|
onDragEnter={handleDrag}
|
||||||
|
|
|
@ -89,9 +89,9 @@ export const Lightbox = (props: Props) => {
|
||||||
|
|
||||||
useEscKeyDownHandler(closeLightbox)
|
useEscKeyDownHandler(closeLightbox)
|
||||||
|
|
||||||
let startX: number = 0
|
let startX = 0
|
||||||
let startY: number = 0
|
let startY = 0
|
||||||
let isDragging: boolean = false
|
let isDragging = false
|
||||||
|
|
||||||
const onMouseDown: (event: MouseEvent) => void = (event) => {
|
const onMouseDown: (event: MouseEvent) => void = (event) => {
|
||||||
startX = event.clientX - translateX()
|
startX = event.clientX - translateX()
|
||||||
|
@ -125,7 +125,7 @@ export const Lightbox = (props: Props) => {
|
||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
let fadeTimer
|
let fadeTimer: string | number | NodeJS.Timeout
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
|
@ -163,7 +163,7 @@ export const Lightbox = (props: Props) => {
|
||||||
<button class={clsx(styles.control, styles.controlDefault)} onClick={(event) => zoomReset(event)}>
|
<button class={clsx(styles.control, styles.controlDefault)} onClick={(event) => zoomReset(event)}>
|
||||||
1:1
|
1:1
|
||||||
</button>
|
</button>
|
||||||
<button class={styles.control} onClick={(event) => zoomIn(event)}>
|
<button type="button" class={styles.control} onClick={(event) => zoomIn(event)}>
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,8 +11,10 @@ type Kebab<T extends string, A extends string = ''> = T extends `${infer F}${inf
|
||||||
* @link https://swiperjs.com/element#parameters-as-attributes
|
* @link https://swiperjs.com/element#parameters-as-attributes
|
||||||
*/
|
*/
|
||||||
type KebabObjectKeys<T> = {
|
type KebabObjectKeys<T> = {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// biome-ignore lint/suspicious/noExplicitAny: TODO: <explanation>
|
||||||
[key in keyof T as Kebab<key & string>]: T[key] extends Object ? KebabObjectKeys<T[key]> : T[key]
|
[key in keyof T as Kebab<key & string>]: T[key] extends Record<string, any>
|
||||||
|
? KebabObjectKeys<T[key]>
|
||||||
|
: T[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -28,14 +28,14 @@ export const VideoPlayer = (props: Props) => {
|
||||||
if (isYoutube) {
|
if (isYoutube) {
|
||||||
if (props.videoUrl.includes('youtube.com')) {
|
if (props.videoUrl.includes('youtube.com')) {
|
||||||
const videoIdMatch = props.videoUrl.match(/watch=(\w+)/)
|
const videoIdMatch = props.videoUrl.match(/watch=(\w+)/)
|
||||||
setVideoId(videoIdMatch && videoIdMatch[1])
|
setVideoId(videoIdMatch?.[1])
|
||||||
} else {
|
} else {
|
||||||
const videoIdMatch = props.videoUrl.match(/youtu.be\/(\w+)/)
|
const videoIdMatch = props.videoUrl.match(/youtu.be\/(\w+)/)
|
||||||
setVideoId(videoIdMatch && videoIdMatch[1])
|
setVideoId(videoIdMatch?.[1])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const videoIdMatch = props.videoUrl.match(/vimeo.com\/(\d+)/)
|
const videoIdMatch = props.videoUrl.match(/vimeo.com\/(\d+)/)
|
||||||
setVideoId(videoIdMatch && videoIdMatch[1])
|
setVideoId(videoIdMatch?.[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ export const VideoPlayer = (props: Props) => {
|
||||||
<Match when={isVimeo()}>
|
<Match when={isVimeo()}>
|
||||||
<div class={styles.videoContainer}>
|
<div class={styles.videoContainer}>
|
||||||
<iframe
|
<iframe
|
||||||
|
title={props.title}
|
||||||
src={`https://player.vimeo.com/video/${videoId()}`}
|
src={`https://player.vimeo.com/video/${videoId()}`}
|
||||||
width="640"
|
width="640"
|
||||||
height="360"
|
height="360"
|
||||||
|
@ -69,6 +70,7 @@ export const VideoPlayer = (props: Props) => {
|
||||||
<Match when={!isVimeo()}>
|
<Match when={!isVimeo()}>
|
||||||
<div class={styles.videoContainer}>
|
<div class={styles.videoContainer}>
|
||||||
<iframe
|
<iframe
|
||||||
|
title={props.title}
|
||||||
width="560"
|
width="560"
|
||||||
height="315"
|
height="315"
|
||||||
src={`https://www.youtube.com/embed/${videoId()}`}
|
src={`https://www.youtube.com/embed/${videoId()}`}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import type { Accessor, JSX } from 'solid-js'
|
import type { Accessor, JSX } from 'solid-js'
|
||||||
|
|
||||||
|
import type { Author, Topic, Reaction, Shout } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'
|
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'
|
||||||
import { createContext, useContext, createSignal, createEffect } from 'solid-js'
|
import { createContext, useContext, createSignal, createEffect } from 'solid-js'
|
||||||
|
|
||||||
|
@ -11,8 +13,7 @@ export interface SSEMessage {
|
||||||
id: string
|
id: string
|
||||||
entity: string // follower | shout | reaction
|
entity: string // follower | shout | reaction
|
||||||
action: string // create | delete | update | join | follow | seen
|
action: string // create | delete | update | join | follow | seen
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
payload: Partial<Author | Shout | Topic | Reaction>
|
||||||
payload: any // Author Shout Message Reaction Chat
|
|
||||||
created_at?: number // unixtime x1000
|
created_at?: number // unixtime x1000
|
||||||
seen?: boolean
|
seen?: boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,11 +49,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
|
||||||
const { addHandler } = useConnect()
|
const { addHandler } = useConnect()
|
||||||
addHandler(handleMessage)
|
addHandler(handleMessage)
|
||||||
|
|
||||||
const loadMessages = async (
|
const loadMessages = async (by: MessagesBy, limit = 50, offset = 0): Promise<Array<Message>> => {
|
||||||
by: MessagesBy,
|
|
||||||
limit: number = 50,
|
|
||||||
offset: number = 0,
|
|
||||||
): Promise<Array<Message>> => {
|
|
||||||
if (inboxClient.private) {
|
if (inboxClient.private) {
|
||||||
const msgs = await inboxClient.loadChatMessages({ by, limit, offset })
|
const msgs = await inboxClient.loadChatMessages({ by, limit, offset })
|
||||||
setMessages((mmm) => [...new Set([...mmm, ...msgs])])
|
setMessages((mmm) => [...new Set([...mmm, ...msgs])])
|
||||||
|
|
|
@ -23,7 +23,7 @@ export function useProfileForm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const userpicUrl = (userpic: string) => {
|
const userpicUrl = (userpic: string) => {
|
||||||
if (userpic && userpic.includes('assets.discours.io')) {
|
if (userpic?.includes('assets.discours.io')) {
|
||||||
return userpic.replace('100x', '500x500')
|
return userpic.replace('100x', '500x500')
|
||||||
}
|
}
|
||||||
return userpic
|
return userpic
|
||||||
|
|
|
@ -67,7 +67,7 @@ export type SessionContextType = {
|
||||||
params: ForgotPasswordInput,
|
params: ForgotPasswordInput,
|
||||||
) => Promise<{ data: ForgotPasswordResponse; errors: Error[] }>
|
) => Promise<{ data: ForgotPasswordResponse; errors: Error[] }>
|
||||||
changePassword: (password: string, token: string) => void
|
changePassword: (password: string, token: string) => void
|
||||||
confirmEmail: (input: VerifyEmailInput) => Promise<AuthToken | void> // email confirm callback is in auth.discours.io
|
confirmEmail: (input: VerifyEmailInput) => Promise<AuthToken> // email confirm callback is in auth.discours.io
|
||||||
setIsSessionLoaded: (loaded: boolean) => void
|
setIsSessionLoaded: (loaded: boolean) => void
|
||||||
authorizer: () => Authorizer
|
authorizer: () => Authorizer
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,12 @@ export const SessionProvider = (props: {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const token = searchParams()?.token
|
const token = searchParams()?.token
|
||||||
const access_token = searchParams()?.access_token
|
const access_token = searchParams()?.access_token
|
||||||
if (access_token) changeSearchParams({ mode: 'confirm-email', modal: 'auth', access_token })
|
if (access_token)
|
||||||
|
changeSearchParams({
|
||||||
|
mode: 'confirm-email',
|
||||||
|
modal: 'auth',
|
||||||
|
access_token,
|
||||||
|
})
|
||||||
else if (token) changeSearchParams({ mode: 'change-password', modal: 'auth', token })
|
else if (token) changeSearchParams({ mode: 'change-password', modal: 'auth', token })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -143,15 +148,14 @@ export const SessionProvider = (props: {
|
||||||
setIsSessionLoaded(true)
|
setIsSessionLoaded(true)
|
||||||
|
|
||||||
return s.data
|
return s.data
|
||||||
} else {
|
|
||||||
console.info('[context.session] cannot refresh session', s.errors)
|
|
||||||
setAuthError(s.errors.pop().message)
|
|
||||||
|
|
||||||
// Set the session loaded flag even if there's an error
|
|
||||||
setIsSessionLoaded(true)
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
console.info('[context.session] cannot refresh session', s.errors)
|
||||||
|
setAuthError(s.errors.pop().message)
|
||||||
|
|
||||||
|
// Set the session loaded flag even if there's an error
|
||||||
|
setIsSessionLoaded(true)
|
||||||
|
|
||||||
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.info('[context.session] cannot refresh session', error)
|
console.info('[context.session] cannot refresh session', error)
|
||||||
setAuthError(error)
|
setAuthError(error)
|
||||||
|
@ -232,7 +236,11 @@ export const SessionProvider = (props: {
|
||||||
// initial effect
|
// initial effect
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const metaRes = await authorizer().getMetaData()
|
const metaRes = await authorizer().getMetaData()
|
||||||
setConfig({ ...defaultConfig, ...metaRes, redirectURL: window.location.origin })
|
setConfig({
|
||||||
|
...defaultConfig,
|
||||||
|
...metaRes,
|
||||||
|
redirectURL: window.location.origin,
|
||||||
|
})
|
||||||
let s: AuthToken
|
let s: AuthToken
|
||||||
try {
|
try {
|
||||||
s = await loadSession()
|
s = await loadSession()
|
||||||
|
@ -297,7 +305,11 @@ export const SessionProvider = (props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const changePassword = async (password: string, token: string) => {
|
const changePassword = async (password: string, token: string) => {
|
||||||
const resp = await authorizer().resetPassword({ password, token, confirm_password: password })
|
const resp = await authorizer().resetPassword({
|
||||||
|
password,
|
||||||
|
token,
|
||||||
|
confirm_password: password,
|
||||||
|
})
|
||||||
console.debug('[context.session] change password response:', resp)
|
console.debug('[context.session] change password response:', resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,9 +326,8 @@ export const SessionProvider = (props: {
|
||||||
if (at?.data) {
|
if (at?.data) {
|
||||||
setSession(at.data)
|
setSession(at.data)
|
||||||
return at.data
|
return at.data
|
||||||
} else {
|
|
||||||
console.warn(at?.errors)
|
|
||||||
}
|
}
|
||||||
|
console.warn(at?.errors)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
}
|
}
|
||||||
|
@ -325,15 +336,7 @@ export const SessionProvider = (props: {
|
||||||
const oauth = async (oauthProvider: string) => {
|
const oauth = async (oauthProvider: string) => {
|
||||||
console.debug(`[context.session] calling authorizer's oauth for`)
|
console.debug(`[context.session] calling authorizer's oauth for`)
|
||||||
try {
|
try {
|
||||||
// const data: GraphqlQueryInput = {}
|
await authorizer().oauthLogin(oauthProvider, [], window.location.origin, oauthState())
|
||||||
// await authorizer().graphqlQuery(data)
|
|
||||||
const ar: AuthorizeResponse | void = await authorizer().oauthLogin(
|
|
||||||
oauthProvider,
|
|
||||||
[],
|
|
||||||
window.location.origin,
|
|
||||||
oauthState(),
|
|
||||||
)
|
|
||||||
console.debug(ar)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ if (isDev) {
|
||||||
exchanges.unshift(devtoolsExchange)
|
exchanges.unshift(devtoolsExchange)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createGraphQLClient = (serviceName: string, token: string = '') => {
|
export const createGraphQLClient = (serviceName: string, token = '') => {
|
||||||
const options: ClientOptions = {
|
const options: ClientOptions = {
|
||||||
url: `https://${serviceName}.discours.io`,
|
url: `https://${serviceName}.discours.io`,
|
||||||
maskTypename: true,
|
maskTypename: true,
|
||||||
|
|
|
@ -57,7 +57,7 @@ export const DogmaPage = () => {
|
||||||
<b>Всегда исправляем ошибки, если мы их допустили.</b>
|
<b>Всегда исправляем ошибки, если мы их допустили.</b>
|
||||||
Никто не безгрешен, иногда и мы ошибаемся. Заметили ошибку — отправьте{' '}
|
Никто не безгрешен, иногда и мы ошибаемся. Заметили ошибку — отправьте{' '}
|
||||||
<a href="/about/guide#editing">ремарку</a> автору или напишите нам на{' '}
|
<a href="/about/guide#editing">ремарку</a> автору или напишите нам на{' '}
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
|
|
@ -136,7 +136,7 @@ export const GuidePage = () => {
|
||||||
вы хотите обсудить текст, прежде чем загрузить материал в интернет-редакцию —
|
вы хотите обсудить текст, прежде чем загрузить материал в интернет-редакцию —
|
||||||
разместите его в google-документе, откройте доступ к редактированию по ссылке
|
разместите его в google-документе, откройте доступ к редактированию по ссылке
|
||||||
и напишите нам на
|
и напишите нам на
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
@ -144,11 +144,11 @@ export const GuidePage = () => {
|
||||||
<p>
|
<p>
|
||||||
Если у вас возникают трудности с тем, чтобы подобрать к своему материалу
|
Если у вас возникают трудности с тем, чтобы подобрать к своему материалу
|
||||||
иллюстрации, тоже пишите на
|
иллюстрации, тоже пишите на
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||||
почту
|
почту
|
||||||
</a>
|
</a>
|
||||||
— наши коллеги-художники могут вам помочь{' '}
|
— наши коллеги-художники могут вам помочь{' '}
|
||||||
<a href="/create?collab" target="_blank">
|
<a href="/create?collab" target="_blank" rel="noreferrer">
|
||||||
в режиме совместного редактирования
|
в режиме совместного редактирования
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
@ -177,7 +177,7 @@ export const GuidePage = () => {
|
||||||
на мероприятия, базу контактов, юридическую поддержку, ознакомление с книжными,
|
на мероприятия, базу контактов, юридическую поддержку, ознакомление с книжными,
|
||||||
кино- и музыкальными новинками до их выхода в свет. Если что-то
|
кино- и музыкальными новинками до их выхода в свет. Если что-то
|
||||||
из этого вам понадобится, пишите на почту{' '}
|
из этого вам понадобится, пишите на почту{' '}
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
</a>
|
</a>
|
||||||
— поможем.
|
— поможем.
|
||||||
|
@ -219,15 +219,15 @@ export const GuidePage = () => {
|
||||||
<p>
|
<p>
|
||||||
За свежими публикациями Дискурса можно следить не только на сайте,
|
За свежими публикациями Дискурса можно следить не только на сайте,
|
||||||
но и на страницах в
|
но и на страницах в
|
||||||
<a href="https://facebook.com/discoursio/" target="_blank">
|
<a href="https://facebook.com/discoursio/" target="_blank" rel="noreferrer">
|
||||||
Фейсбуке
|
Фейсбуке
|
||||||
</a>
|
</a>
|
||||||
,{' '}
|
,{' '}
|
||||||
<a href="https://vk.com/discoursio" target="_blank">
|
<a href="https://vk.com/discoursio" target="_blank" rel="noreferrer">
|
||||||
ВКонтакте
|
ВКонтакте
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
и
|
и
|
||||||
<a href="https://t.me/discoursio" target="_blank">
|
<a href="https://t.me/discoursio" target="_blank" rel="noreferrer">
|
||||||
Телеграме
|
Телеграме
|
||||||
</a>
|
</a>
|
||||||
. А ещё раз в месяц мы отправляем <a href="#subscribe">почтовую рассылку</a>{' '}
|
. А ещё раз в месяц мы отправляем <a href="#subscribe">почтовую рассылку</a>{' '}
|
||||||
|
@ -236,7 +236,7 @@ export const GuidePage = () => {
|
||||||
<p>
|
<p>
|
||||||
Если вы хотите сотрудничать, что-то обсудить или предложить — пожалуйста, пишите
|
Если вы хотите сотрудничать, что-то обсудить или предложить — пожалуйста, пишите
|
||||||
на
|
на
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
</a>
|
</a>
|
||||||
. Мы обязательно ответим.
|
. Мы обязательно ответим.
|
||||||
|
|
|
@ -111,7 +111,7 @@ export const HelpPage = () => {
|
||||||
<h3 id="trustee">Войдите в попечительский совет Дискурса</h3>
|
<h3 id="trustee">Войдите в попечительский совет Дискурса</h3>
|
||||||
<p>
|
<p>
|
||||||
Вы хотите сделать крупное пожертвование? Станьте попечителем Дискурса —{' '}
|
Вы хотите сделать крупное пожертвование? Станьте попечителем Дискурса —{' '}
|
||||||
<a class="black-link" href="mailto:welcome@discours.io" target="_blank">
|
<a class="black-link" href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||||
напишите нам
|
напишите нам
|
||||||
</a>
|
</a>
|
||||||
, мы будем рады единомышленникам.
|
, мы будем рады единомышленникам.
|
||||||
|
@ -128,7 +128,7 @@ export const HelpPage = () => {
|
||||||
<p>
|
<p>
|
||||||
Если вы хотите помочь проекту, но у вас возникли вопросы, напишите нам письмо
|
Если вы хотите помочь проекту, но у вас возникли вопросы, напишите нам письмо
|
||||||
по адресу{' '}
|
по адресу{' '}
|
||||||
<a class="black-link" href="mailto:welcome@discours.io" target="_blank">
|
<a class="black-link" href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
|
|
@ -86,7 +86,11 @@ export const TermsOfUsePage = () => {
|
||||||
<p class="ng-binding">
|
<p class="ng-binding">
|
||||||
Обнародование контента осуществляется Издательством в соответствии с условиями
|
Обнародование контента осуществляется Издательством в соответствии с условиями
|
||||||
лицензии{' '}
|
лицензии{' '}
|
||||||
<a href="https://creativecommons.org/licenses/by-nc-nd/4.0/deed.ru" target="_blank">
|
<a
|
||||||
|
href="https://creativecommons.org/licenses/by-nc-nd/4.0/deed.ru"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
Creative Commons BY-NC-ND 4.0
|
Creative Commons BY-NC-ND 4.0
|
||||||
</a>
|
</a>
|
||||||
. Все материалы сайта предназначены исключительно для личного некоммерческого использования.
|
. Все материалы сайта предназначены исключительно для личного некоммерческого использования.
|
||||||
|
@ -99,7 +103,7 @@ export const TermsOfUsePage = () => {
|
||||||
и используются только в образовательных и информационных целях. Если
|
и используются только в образовательных и информационных целях. Если
|
||||||
вы являетесь собственником того или иного произведения и не согласны с его
|
вы являетесь собственником того или иного произведения и не согласны с его
|
||||||
размещением на сайте, пожалуйста, напишите на
|
размещением на сайте, пожалуйста, напишите на
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
@ -196,7 +200,7 @@ export const TermsOfUsePage = () => {
|
||||||
<p class="ng-binding">
|
<p class="ng-binding">
|
||||||
По желанию пользователя Издательство готово удалить любую информацию о нем,
|
По желанию пользователя Издательство готово удалить любую информацию о нем,
|
||||||
собранную автоматическим путем. Для этого следует написать на адрес электронной почты{' '}
|
собранную автоматическим путем. Для этого следует написать на адрес электронной почты{' '}
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
@ -210,11 +214,11 @@ export const TermsOfUsePage = () => {
|
||||||
</p>
|
</p>
|
||||||
<p class="ng-binding">
|
<p class="ng-binding">
|
||||||
Общедоступные видео на сайте могут транслироваться с YouTube и регулируются{' '}
|
Общедоступные видео на сайте могут транслироваться с YouTube и регулируются{' '}
|
||||||
<a href="https://policies.google.com/privacy" target="_blank">
|
<a href="https://policies.google.com/privacy" target="_blank" rel="noreferrer">
|
||||||
политикой конфиденциальности Google
|
политикой конфиденциальности Google
|
||||||
</a>
|
</a>
|
||||||
. Загрузка видео на сайт также означает согласие с
|
. Загрузка видео на сайт также означает согласие с
|
||||||
<a href="https://www.youtube.com/t/terms" target="_blank">
|
<a href="https://www.youtube.com/t/terms" target="_blank" rel="noreferrer">
|
||||||
Условиями использования YouTube
|
Условиями использования YouTube
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
@ -231,7 +235,7 @@ export const TermsOfUsePage = () => {
|
||||||
<p class="ng-binding">
|
<p class="ng-binding">
|
||||||
Любые вопросы и предложения по поводу функционирования сайта можно направить
|
Любые вопросы и предложения по поводу функционирования сайта можно направить
|
||||||
по электронной почте{' '}
|
по электронной почте{' '}
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
или через форму <a href="/connect">«предложить идею»</a>.
|
или через форму <a href="/connect">«предложить идею»</a>.
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { Author, Topic } from '../graphql/schema/core.gen'
|
import type { Author, Topic } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { isAuthor } from './isAuthor'
|
import { isAuthor } from './isAuthor'
|
||||||
import { translit } from './ru2en'
|
import { translit } from './ru2en'
|
||||||
|
|
||||||
const prepareQuery = (searchQuery, lang) => {
|
const prepareQuery = (searchQuery: string, lang: string) => {
|
||||||
const q = searchQuery.toLowerCase()
|
const q = searchQuery.toLowerCase()
|
||||||
if (q.length === 0) return ''
|
if (q.length === 0) return ''
|
||||||
return lang === 'ru' ? translit(q) : q
|
return lang === 'ru' ? translit(q) : q
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringMatches = (str, q, lang) => {
|
const stringMatches = (str: string, q: string, lang: string) => {
|
||||||
const preparedStr = lang === 'ru' ? translit(str.toLowerCase()) : str.toLowerCase()
|
const preparedStr = lang === 'ru' ? translit(str.toLowerCase()) : str.toLowerCase()
|
||||||
return preparedStr.split(' ').some((word) => word.startsWith(q))
|
return preparedStr.split(' ').some((word) => word.startsWith(q))
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ export const dummyFilter = <T extends Topic | Author>(
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.filter((item) => {
|
return data.filter((item) => {
|
||||||
const slugMatches = item.slug && item.slug.split('-').some((w) => w.startsWith(q))
|
const slugMatches = item.slug?.split('-').some((w) => w.startsWith(q))
|
||||||
if (slugMatches) return true
|
if (slugMatches) return true
|
||||||
|
|
||||||
if ('title' in item) {
|
if ('title' in item) {
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const getOpenGraphImageUrl = (
|
||||||
)}','${encodeURIComponent(options.title)}')/`
|
)}','${encodeURIComponent(options.title)}')/`
|
||||||
|
|
||||||
if (src.startsWith(thumborUrl)) {
|
if (src.startsWith(thumborUrl)) {
|
||||||
const thumborKey = src.replace(thumborUrl + '/unsafe', '')
|
const thumborKey = src.replace(`${thumborUrl}/unsafe`, '')
|
||||||
return `${thumborUrl}/unsafe/${sizeUrlPart}${filtersPart}${thumborKey}`
|
return `${thumborUrl}/unsafe/${sizeUrlPart}${filtersPart}${thumborKey}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export let resolveHydrationPromise
|
export let resolveHydrationPromise: () => void
|
||||||
|
|
||||||
export const hydrationPromise = new Promise((resolve) => {
|
export const hydrationPromise: Promise<void> = new Promise((resolve) => {
|
||||||
resolveHydrationPromise = resolve
|
resolveHydrationPromise = resolve
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const getDescription = (body: string): string => {
|
||||||
let description = ''
|
let description = ''
|
||||||
let i = 0
|
let i = 0
|
||||||
while (i < descriptionWordsArray.length && description.length < MAX_DESCRIPTION_LENGTH) {
|
while (i < descriptionWordsArray.length && description.length < MAX_DESCRIPTION_LENGTH) {
|
||||||
description += descriptionWordsArray[i] + ' '
|
description += `${descriptionWordsArray[i]} `
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
return description.trim()
|
return description.trim()
|
||||||
|
|
|
@ -42,10 +42,10 @@ export const profileSocialLinks = (socialLinks: string[]): Link[] => {
|
||||||
return processedLinks.sort((a, b) => {
|
return processedLinks.sort((a, b) => {
|
||||||
if (a.isPlaceholder && !b.isPlaceholder) {
|
if (a.isPlaceholder && !b.isPlaceholder) {
|
||||||
return 1
|
return 1
|
||||||
} else if (!a.isPlaceholder && b.isPlaceholder) {
|
|
||||||
return -1
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
if (!a.isPlaceholder && b.isPlaceholder) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const restoreScrollPosition = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const scrollHandler = (elemId: string, offset = -100) => {
|
export const scrollHandler = (elemId: string, offset = -100) => {
|
||||||
const anchor = document.querySelector('#' + elemId)
|
const anchor = document.querySelector(`#${elemId}`)
|
||||||
|
|
||||||
if (anchor) {
|
if (anchor) {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
|
|
|
@ -26,8 +26,8 @@ export const byLength = (
|
||||||
|
|
||||||
export const byStat = (metric: keyof Stat | keyof TopicStat) => {
|
export const byStat = (metric: keyof Stat | keyof TopicStat) => {
|
||||||
return (a, b) => {
|
return (a, b) => {
|
||||||
const x = (a?.stat && a.stat[metric]) || 0
|
const x = a.stat?.[metric] || 0
|
||||||
const y = (b?.stat && b.stat[metric]) || 0
|
const y = b.stat?.[metric] || 0
|
||||||
if (x > y) return -1
|
if (x > y) return -1
|
||||||
if (x < y) return 1
|
if (x < y) return 1
|
||||||
return 0
|
return 0
|
||||||
|
@ -36,8 +36,8 @@ export const byStat = (metric: keyof Stat | keyof TopicStat) => {
|
||||||
|
|
||||||
export const byTopicStatDesc = (metric: keyof TopicStat) => {
|
export const byTopicStatDesc = (metric: keyof TopicStat) => {
|
||||||
return (a: Topic, b: Topic) => {
|
return (a: Topic, b: Topic) => {
|
||||||
const x = (a?.stat && a.stat[metric]) || 0
|
const x = a.stat?.[metric] || 0
|
||||||
const y = (b?.stat && b.stat[metric]) || 0
|
const y = b.stat?.[metric] || 0
|
||||||
if (x > y) return -1
|
if (x > y) return -1
|
||||||
if (x < y) return 1
|
if (x < y) return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
Loading…
Reference in New Issue
Block a user