This commit is contained in:
Untone 2024-02-04 12:03:15 +03:00
parent 67541bef79
commit aeeed1cb65
66 changed files with 201 additions and 1142 deletions

View File

@ -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"

View File

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

View File

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

View File

@ -1,16 +0,0 @@
{
"htmlWhitespaceSensitivity": "ignore",
"semi": false,
"singleQuote": true,
"proseWrap": "always",
"printWidth": 108,
"plugins": [],
"overrides": [
{
"files": "*.ts",
"options": {
"parser": "typescript"
}
}
]
}

View File

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

View File

@ -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
``` ```

View File

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

View File

@ -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)

View File

@ -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)

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

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

View File

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

View File

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

View File

@ -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)

View File

@ -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',

View File

@ -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 = () => {

View File

@ -173,7 +173,7 @@ const SimplifiedEditor = (props: Props) => {
createEditorTransaction( createEditorTransaction(
() => editor(), () => editor(),
(ed) => { (ed) => {
return ed && ed.isActive(name) return ed?.isActive(name)
}, },
) )

View File

@ -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)

View File

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

View File

@ -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]
}) })

View File

@ -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(', '))

View File

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

View File

@ -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)

View File

@ -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 },
), ),

View File

@ -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: (

View File

@ -35,8 +35,10 @@
width: auto; width: auto;
} }
a { a, button {
border: none !important; border: none !important;
outline: none;
box-shadow: none;
} }
.facebook, .facebook,

View File

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

View File

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

View File

@ -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,
})} })}

View File

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

View File

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

View File

@ -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()) {

View File

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

View File

@ -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 () => {

View File

@ -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) => {

View File

@ -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">

View File

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

View File

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

View File

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

View File

@ -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]
} }
/** /**

View File

@ -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()}`}

View File

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

View File

@ -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])])

View File

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

View File

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

View File

@ -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,

View File

@ -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>
. .

View File

@ -136,7 +136,7 @@ export const GuidePage = () => {
вы&nbsp;хотите обсудить текст, прежде чем загрузить материал в интернет-редакцию&nbsp;&mdash; вы&nbsp;хотите обсудить текст, прежде чем загрузить материал в интернет-редакцию&nbsp;&mdash;
разместите его в&nbsp;google-документе, откройте доступ к&nbsp;редактированию по&nbsp;ссылке разместите его в&nbsp;google-документе, откройте доступ к&nbsp;редактированию по&nbsp;ссылке
и&nbsp;напишите нам на&nbsp; и&nbsp;напишите нам на&nbsp;
<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>
Если у&nbsp;вас возникают трудности с&nbsp;тем, чтобы подобрать к&nbsp;своему материалу Если у&nbsp;вас возникают трудности с&nbsp;тем, чтобы подобрать к&nbsp;своему материалу
иллюстрации, тоже пишите на&nbsp; иллюстрации, тоже пишите на&nbsp;
<a href="mailto:welcome@discours.io" target="_blank"> <a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
почту почту
</a> </a>
&mdash; наши коллеги-художники могут вам помочь{' '} &mdash; наши коллеги-художники могут вам помочь{' '}
<a href="/create?collab" target="_blank"> <a href="/create?collab" target="_blank" rel="noreferrer">
в&nbsp;режиме совместного редактирования в&nbsp;режиме совместного редактирования
</a> </a>
. .
@ -177,7 +177,7 @@ export const GuidePage = () => {
на&nbsp;мероприятия, базу контактов, юридическую поддержку, ознакомление с&nbsp;книжными, на&nbsp;мероприятия, базу контактов, юридическую поддержку, ознакомление с&nbsp;книжными,
кино- и&nbsp;музыкальными новинками до&nbsp;их&nbsp;выхода в&nbsp;свет. Если что-то кино- и&nbsp;музыкальными новинками до&nbsp;их&nbsp;выхода в&nbsp;свет. Если что-то
из&nbsp;этого вам понадобится, пишите на&nbsp;почту{' '} из&nbsp;этого вам понадобится, пишите на&nbsp;почту{' '}
<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>
&nbsp;&mdash; поможем. &nbsp;&mdash; поможем.
@ -219,15 +219,15 @@ export const GuidePage = () => {
<p> <p>
За&nbsp;свежими публикациями Дискурса можно следить не&nbsp;только на&nbsp;сайте, За&nbsp;свежими публикациями Дискурса можно следить не&nbsp;только на&nbsp;сайте,
но&nbsp;и&nbsp;на&nbsp;страницах в&nbsp; но&nbsp;и&nbsp;на&nbsp;страницах в&nbsp;
<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>{' '}
и&nbsp; и&nbsp;
<a href="https://t.me/discoursio" target="_blank"> <a href="https://t.me/discoursio" target="_blank" rel="noreferrer">
Телеграме Телеграме
</a> </a>
. А&nbsp;ещё раз в&nbsp;месяц мы&nbsp;отправляем <a href="#subscribe">почтовую рассылку</a>{' '} . А&nbsp;ещё раз в&nbsp;месяц мы&nbsp;отправляем <a href="#subscribe">почтовую рассылку</a>{' '}
@ -236,7 +236,7 @@ export const GuidePage = () => {
<p> <p>
Если вы&nbsp;хотите сотрудничать, что-то обсудить или предложить &mdash; пожалуйста, пишите Если вы&nbsp;хотите сотрудничать, что-то обсудить или предложить &mdash; пожалуйста, пишите
на&nbsp; на&nbsp;
<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>
. Мы&nbsp;обязательно ответим. . Мы&nbsp;обязательно ответим.

View File

@ -111,7 +111,7 @@ export const HelpPage = () => {
<h3 id="trustee">Войдите в&nbsp;попечительский совет Дискурса</h3> <h3 id="trustee">Войдите в&nbsp;попечительский совет Дискурса</h3>
<p> <p>
Вы&nbsp;хотите сделать крупное пожертвование? Станьте попечителем Дискурса&nbsp;&mdash;{' '} Вы&nbsp;хотите сделать крупное пожертвование? Станьте попечителем Дискурса&nbsp;&mdash;{' '}
<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>
, мы&nbsp;будем рады единомышленникам. , мы&nbsp;будем рады единомышленникам.
@ -128,7 +128,7 @@ export const HelpPage = () => {
<p> <p>
Если вы&nbsp;хотите помочь проекту, но&nbsp;у&nbsp;вас возникли вопросы, напишите нам письмо Если вы&nbsp;хотите помочь проекту, но&nbsp;у&nbsp;вас возникли вопросы, напишите нам письмо
по&nbsp;адресу{' '} по&nbsp;адресу{' '}
<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>
. .

View File

@ -86,7 +86,11 @@ export const TermsOfUsePage = () => {
<p class="ng-binding"> <p class="ng-binding">
Обнародование контента осуществляется Издательством в&nbsp;соответствии с&nbsp;условиями Обнародование контента осуществляется Издательством в&nbsp;соответствии с&nbsp;условиями
лицензии{' '} лицензии{' '}
<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 = () => {
и&nbsp;используются только в&nbsp;образовательных и&nbsp;информационных целях. Если и&nbsp;используются только в&nbsp;образовательных и&nbsp;информационных целях. Если
вы&nbsp;являетесь собственником того или иного произведения и&nbsp;не&nbsp;согласны с&nbsp;его вы&nbsp;являетесь собственником того или иного произведения и&nbsp;не&nbsp;согласны с&nbsp;его
размещением на&nbsp;сайте, пожалуйста, напишите на&nbsp; размещением на&nbsp;сайте, пожалуйста, напишите на&nbsp;
<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">
По&nbsp;желанию пользователя Издательство готово удалить любую информацию о&nbsp;нем, По&nbsp;желанию пользователя Издательство готово удалить любую информацию о&nbsp;нем,
собранную автоматическим путем. Для этого следует написать на&nbsp;адрес электронной почты{' '} собранную автоматическим путем. Для этого следует написать на&nbsp;адрес электронной почты{' '}
<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">
Общедоступные видео на&nbsp;сайте могут транслироваться с&nbsp;YouTube и&nbsp;регулируются{' '} Общедоступные видео на&nbsp;сайте могут транслироваться с&nbsp;YouTube и&nbsp;регулируются{' '}
<a href="https://policies.google.com/privacy" target="_blank"> <a href="https://policies.google.com/privacy" target="_blank" rel="noreferrer">
политикой конфиденциальности Google политикой конфиденциальности Google
</a> </a>
. Загрузка видео на&nbsp;сайт также означает согласие с&nbsp; . Загрузка видео на&nbsp;сайт также означает согласие с&nbsp;
<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">
Любые вопросы и&nbsp;предложения по&nbsp;поводу функционирования сайта можно направить Любые вопросы и&nbsp;предложения по&nbsp;поводу функционирования сайта можно направить
по&nbsp;электронной почте{' '} по&nbsp;электронной почте{' '}
<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">&laquo;предложить идею&raquo;</a>. или через форму <a href="/connect">&laquo;предложить идею&raquo;</a>.

View File

@ -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) {

View File

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

View File

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

View File

@ -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()

View File

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

View File

@ -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({

View File

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