fmt-lint
This commit is contained in:
parent
67541bef79
commit
aeeed1cb65
|
@ -1,4 +1,4 @@
|
|||
name: 'deploy'
|
||||
name: "deploy"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -6,6 +6,7 @@ on:
|
|||
- main
|
||||
- dev
|
||||
- feature/email-templates
|
||||
- feature/biome
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
@ -17,10 +18,10 @@ jobs:
|
|||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: "18"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
run: npm ci
|
||||
|
||||
- name: Run check
|
||||
run: npm run check
|
||||
|
@ -40,13 +41,33 @@ jobs:
|
|||
- name: Run Biome
|
||||
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:
|
||||
needs: test_with_playwright
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
branch: ${{ github.head_ref }}
|
||||
|
||||
update_mailgun_template:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -57,34 +78,34 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: 'Email confirmation template'
|
||||
- name: "Email confirmation template"
|
||||
uses: gyto/mailgun-template-action@v2
|
||||
with:
|
||||
html-file: './templates/authorizer_email_confirmation.html'
|
||||
html-file: "./templates/authorizer_email_confirmation.html"
|
||||
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
||||
mailgun-domain: 'discours.io'
|
||||
mailgun-template: 'authorizer_email_confirmation'
|
||||
mailgun-domain: "discours.io"
|
||||
mailgun-template: "authorizer_email_confirmation"
|
||||
|
||||
- name: 'Password reset template'
|
||||
- name: "Password reset template"
|
||||
uses: gyto/mailgun-template-action@v2
|
||||
with:
|
||||
html-file: './templates/authorizer_password_reset.html'
|
||||
html-file: "./templates/authorizer_password_reset.html"
|
||||
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
||||
mailgun-domain: 'discours.io'
|
||||
mailgun-template: 'authorizer_password_reset'
|
||||
mailgun-domain: "discours.io"
|
||||
mailgun-template: "authorizer_password_reset"
|
||||
|
||||
- name: 'First publication notification'
|
||||
- name: "First publication notification"
|
||||
uses: gyto/mailgun-template-action@v2
|
||||
with:
|
||||
html-file: './templates/first_publication_notification.html'
|
||||
html-file: "./templates/first_publication_notification.html"
|
||||
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
||||
mailgun-domain: 'discours.io'
|
||||
mailgun-template: 'first_publication_notification'
|
||||
mailgun-domain: "discours.io"
|
||||
mailgun-template: "first_publication_notification"
|
||||
|
||||
- name: 'New comment notification template'
|
||||
- name: "New comment notification template"
|
||||
uses: gyto/mailgun-template-action@v2
|
||||
with:
|
||||
html-file: './templates/new_comment_notification.html'
|
||||
html-file: "./templates/new_comment_notification.html"
|
||||
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
|
||||
mailgun-domain: 'discours.io'
|
||||
mailgun-template: 'new_comment_notification'
|
||||
mailgun-domain: "discours.io"
|
||||
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",
|
||||
"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 start
|
||||
```
|
||||
with different backends
|
||||
```
|
||||
npm run start:local
|
||||
npm run start:production
|
||||
npm run start:staging
|
||||
```
|
||||
|
||||
## Useful commands
|
||||
run checks
|
||||
|
@ -23,10 +17,10 @@ npm run typecheck:watch
|
|||
|
||||
generate new SolidJS component:
|
||||
```
|
||||
npx hygen component new NewComponentName
|
||||
npm run hygen component new NewComponentName
|
||||
```
|
||||
|
||||
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": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"all": true,
|
||||
"recommended": true,
|
||||
"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": {
|
||||
"useImportRestrictions": "off"
|
||||
},
|
||||
"style": {
|
||||
"useNamingConvention": "off"
|
||||
"useNamingConvention": "off",
|
||||
"noUnusedTemplateLiteral": "off"
|
||||
},
|
||||
"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
|
||||
|
@ -14,5 +14,3 @@ https://github.com/jondot/hygen
|
|||
```
|
||||
|
||||
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
|
||||
|
@ -14,5 +14,3 @@ https://github.com/jondot/hygen
|
|||
```
|
||||
|
||||
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:
|
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",
|
||||
"version": "0.9.1",
|
||||
"version": "0.9.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
@ -12,6 +12,7 @@
|
|||
"dev": "vite",
|
||||
"fix": "npm run lint:code:fix && npm run lint:styles:fix",
|
||||
"format": "npx @biomejs/biome format src/. --write",
|
||||
"hygen": "HYGEN_TMPLS=gen hygen",
|
||||
"postinstall": "npm run codegen",
|
||||
"lint": "npm run lint:code && npm run lint:styles",
|
||||
"lint:code": "npx @biomejs/biome lint src --log-kind=pretty --verbose",
|
||||
|
@ -23,9 +24,6 @@
|
|||
"prepare": "husky install",
|
||||
"preview": "vite preview",
|
||||
"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:watch": "tsc --noEmit --watch"
|
||||
},
|
||||
|
@ -96,8 +94,6 @@
|
|||
"@tiptap/extension-youtube": "2.0.3",
|
||||
"@types/js-cookie": "3.0.6",
|
||||
"@types/node": "20.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.10.0",
|
||||
"@typescript-eslint/parser": "6.10.0",
|
||||
"@urql/core": "3.2.2",
|
||||
"@urql/devtools": "2.0.3",
|
||||
"babel-preset-solid": "1.8.4",
|
||||
|
|
|
@ -22,8 +22,8 @@ const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
|
|||
return 0
|
||||
}
|
||||
|
||||
const x = (a?.stat && a.stat.rating) || 0
|
||||
const y = (b?.stat && b.stat.rating) || 0
|
||||
const x = a.stat?.rating || 0
|
||||
const y = b.stat?.rating || 0
|
||||
|
||||
if (x > y) {
|
||||
return 1
|
||||
|
|
|
@ -88,9 +88,8 @@ export const FullArticle = (props: Props) => {
|
|||
if (mt) {
|
||||
mt.title = lang() === 'en' ? capitalize(mt.slug.replace(/-/, ' ')) : mt.title
|
||||
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)
|
||||
|
@ -284,7 +283,7 @@ export const FullArticle = (props: Props) => {
|
|||
}
|
||||
|
||||
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
|
||||
openLightbox(getImageUrl(src))
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ export const Userpic = (props: Props) => {
|
|||
const letters = () => {
|
||||
if (!props.name) return
|
||||
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(() => {
|
||||
|
@ -48,7 +48,7 @@ export const Userpic = (props: Props) => {
|
|||
return (
|
||||
<div
|
||||
class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], {
|
||||
['cursorPointer']: props.onClick,
|
||||
cursorPointer: props.onClick,
|
||||
})}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
|
|
|
@ -29,8 +29,7 @@ export const Donate = () => {
|
|||
} = useSnackbar()
|
||||
|
||||
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())
|
||||
console.log('[donate] payments initiated')
|
||||
setCustomerReciept({
|
||||
|
@ -60,7 +59,6 @@ export const Donate = () => {
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const script = document.createElement('script')
|
||||
script.type = 'text/javascript'
|
||||
script.src = 'https://widget.cloudpayments.ru/bundles/cloudpayments.js'
|
||||
|
@ -76,8 +74,8 @@ export const Donate = () => {
|
|||
const choice: HTMLInputElement | undefined | null =
|
||||
amountSwitchElement?.querySelector('input[type=radio]:checked')
|
||||
setAmount(Number.parseInt(customAmountElement?.value || choice?.value || '0'))
|
||||
console.log('[donate] input amount ' + amount)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
console.log(`[donate] input amount ${amount}`)
|
||||
// biome-ignore lint/suspicious/noExplicitAny: it's a widget!
|
||||
;(widget() as any).charge(
|
||||
{
|
||||
// options
|
||||
|
@ -105,7 +103,7 @@ export const Donate = () => {
|
|||
console.debug('[donate] options', opts)
|
||||
showModal('thank')
|
||||
},
|
||||
function (reason: string, options) {
|
||||
(reason: string, options) => {
|
||||
// fail
|
||||
// действие при неуспешной оплате
|
||||
console.debug('[donate] options', options)
|
||||
|
|
|
@ -11,7 +11,7 @@ export const Footer = () => {
|
|||
const { t, lang } = useLocalize()
|
||||
|
||||
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(() => [
|
||||
{
|
||||
header: 'About the project',
|
||||
|
|
|
@ -24,7 +24,7 @@ export const InsertLinkForm = (props: Props) => {
|
|||
const currentUrl = createEditorTransaction(
|
||||
() => props.editor,
|
||||
(ed) => {
|
||||
return (ed && ed.getAttributes('link').href) || ''
|
||||
return ed?.getAttributes('link').href || ''
|
||||
},
|
||||
)
|
||||
const handleClearLinkForm = () => {
|
||||
|
|
|
@ -173,7 +173,7 @@ const SimplifiedEditor = (props: Props) => {
|
|||
createEditorTransaction(
|
||||
() => editor(),
|
||||
(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) =>
|
||||
createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => editor && editor.isActive(name, attributes),
|
||||
(editor) => editor?.isActive(name, attributes),
|
||||
)
|
||||
|
||||
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
|
||||
|
|
|
@ -41,9 +41,8 @@ export const ToggleTextWrap = Extension.create({
|
|||
if (changesApplied) {
|
||||
dispatch(tr)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
|
@ -36,7 +36,7 @@ const CreateModalContent = (props: Props) => {
|
|||
return user.selected === true
|
||||
})
|
||||
.map((user) => {
|
||||
return user['id']
|
||||
return user.id
|
||||
})
|
||||
return [...s]
|
||||
})
|
||||
|
|
|
@ -26,8 +26,8 @@ type DialogProps = {
|
|||
|
||||
const DialogCard = (props: DialogProps) => {
|
||||
const { t, formatTime } = useLocalize()
|
||||
const companions = createMemo(
|
||||
() => props.members && props.members.filter((member: ChatMember) => member.id !== props.ownId),
|
||||
const companions = createMemo(() =>
|
||||
props.members?.filter((member: ChatMember) => member.id !== props.ownId),
|
||||
)
|
||||
|
||||
const names = createMemo<string>(() => (companions() || []).map((companion) => companion.name).join(', '))
|
||||
|
|
|
@ -68,7 +68,7 @@ export const ChangePasswordForm = () => {
|
|||
)}
|
||||
</div>
|
||||
<Show when={validationErrors()}>
|
||||
<div>{validationErrors()['password']}</div>
|
||||
<div>{validationErrors().password}</div>
|
||||
</Show>
|
||||
<PasswordField
|
||||
errorMessage={(err) => setPasswordError(err)}
|
||||
|
|
|
@ -63,7 +63,7 @@ export const ForgotPasswordForm = () => {
|
|||
redirect_uri: window.location.origin,
|
||||
})
|
||||
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)
|
||||
}
|
||||
if (data.message) setMessage(data.message)
|
||||
|
|
|
@ -50,7 +50,7 @@ export const PasswordField = (props: Props) => {
|
|||
on(
|
||||
() => error(),
|
||||
() => {
|
||||
props.errorMessage && props.errorMessage(error())
|
||||
props.errorMessage?.(error())
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
|
|
|
@ -113,7 +113,7 @@ export const RegisterForm = () => {
|
|||
redirect_uri: window.location.origin,
|
||||
}
|
||||
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) => ({
|
||||
...prev,
|
||||
email: (
|
||||
|
|
|
@ -35,8 +35,10 @@
|
|||
width: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
a, button {
|
||||
border: none !important;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.facebook,
|
||||
|
|
|
@ -20,9 +20,9 @@ export const SocialProviders = () => {
|
|||
<div class={styles.social}>
|
||||
<For each={PROVIDERS}>
|
||||
{(provider) => (
|
||||
<a href="#" class={styles[provider]} onClick={(_e) => oauth(provider)}>
|
||||
<button class={styles[provider]} onClick={(_e) => oauth(provider)}>
|
||||
<Icon name={provider} />
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
|
|
@ -135,7 +135,7 @@ export const Header = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
let timer
|
||||
let timer: string | number | NodeJS.Timeout
|
||||
|
||||
const clearTimer = () => {
|
||||
clearTimeout(timer)
|
||||
|
@ -264,7 +264,7 @@ export const Header = (props: Props) => {
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<h4 innerHTML={t('Subscribe us')} />
|
||||
<h4>{t('Subscribe us')}</h4>
|
||||
<ul class="view-switcher">
|
||||
<li class={styles.mainNavigationSocial}>
|
||||
<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-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||
</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-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||
</a>
|
||||
<a href="#" class={styles.control} onClick={handleBookmarkButtonClick}>
|
||||
</button>
|
||||
<button class={styles.control} onClick={handleBookmarkButtonClick}>
|
||||
<Icon name="bookmark" class={styles.icon} />
|
||||
<Icon name="bookmark-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
@ -417,7 +417,7 @@ export const Header = (props: Props) => {
|
|||
<a href="/podcasts">{t('Podcasts')}</a>
|
||||
</li>
|
||||
<li class="item">
|
||||
<a href="">{t('Special Projects')}</a>
|
||||
<a href="/about/projects">{t('Special Projects')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/topic/interview">#{t('Interview')}</a>
|
||||
|
|
|
@ -32,7 +32,7 @@ export const Modal = (props: Props) => {
|
|||
const handleHide = () => {
|
||||
if (modal()) {
|
||||
if (allowClose()) {
|
||||
props.onClose && props.onClose()
|
||||
props.onClose?.()
|
||||
} else {
|
||||
redirectPage(router, 'home')
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export const Modal = (props: Props) => {
|
|||
<div
|
||||
class={clsx(styles.modal, {
|
||||
[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.maxHeight]: props.maxHeight,
|
||||
})}
|
||||
|
|
|
@ -22,7 +22,7 @@ export const Topics = () => {
|
|||
<a href="/podcasts">{t('Podcasts')}</a>
|
||||
</li>
|
||||
<li class={styles.item}>
|
||||
<a href="">{t('Special Projects')}</a>
|
||||
<a href="/about/projects">{t('Special Projects')}</a>
|
||||
</li>
|
||||
<li class={styles.item}>
|
||||
<a href="/topic/interview">#{t('Interview')}</a>
|
||||
|
|
|
@ -25,7 +25,7 @@ const getTitle = (title: string) => {
|
|||
const shoutTitleWords = title.split(' ')
|
||||
|
||||
while (shoutTitle.length <= 30 && i < shoutTitleWords.length) {
|
||||
shoutTitle += shoutTitleWords[i] + ' '
|
||||
shoutTitle += `${shoutTitleWords[i]} `
|
||||
i++
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ export const AllAuthorsView = (props: Props) => {
|
|||
setOffsetByFollowers((o) => o + PAGE_SIZE)
|
||||
}
|
||||
|
||||
const isStatsLoaded = createMemo(() => sortedAuthors() && sortedAuthors().some((author) => author.stat))
|
||||
const isStatsLoaded = createMemo(() => sortedAuthors()?.some((author) => author.stat))
|
||||
|
||||
createEffect(async () => {
|
||||
if (!isStatsLoaded()) {
|
||||
|
|
|
@ -59,7 +59,7 @@ export const AuthorView = (props: Props) => {
|
|||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (author() && author().id && !author().stat) {
|
||||
if (author()?.id && !author().stat) {
|
||||
const a = loadAuthor({ slug: '', author_id: author().id })
|
||||
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 = () => {
|
||||
autoSaveTimeOutId = setTimeout(async () => {
|
||||
|
|
|
@ -129,11 +129,11 @@ export const InboxView = (props: Props) => {
|
|||
})
|
||||
if (sortByPerToPer()) {
|
||||
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) => {
|
||||
|
|
|
@ -67,7 +67,7 @@ export const SearchView = (props: Props) => {
|
|||
name="q"
|
||||
ref={searchEl}
|
||||
onInput={handleQueryChange}
|
||||
placeholder={query() || t('Enter text') + '...'}
|
||||
placeholder={query() || `${t('Enter text')}...`}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
|
|
|
@ -33,7 +33,7 @@ export const DarkModeToggle = (props: Props) => {
|
|||
|
||||
onCleanup(() => {
|
||||
setEditorDarkMode(false)
|
||||
delete document.documentElement.dataset.editorDarkMode
|
||||
document.documentElement.dataset.editorDarkMode = undefined
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ export const DropArea = (props: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.DropArea, props.class, props.isSquare && styles['square'])}>
|
||||
<div class={clsx(styles.DropArea, props.class, props.isSquare && styles.square)}>
|
||||
<div
|
||||
class={clsx(styles.field, { [styles.active]: dragActive() })}
|
||||
onDragEnter={handleDrag}
|
||||
|
|
|
@ -89,9 +89,9 @@ export const Lightbox = (props: Props) => {
|
|||
|
||||
useEscKeyDownHandler(closeLightbox)
|
||||
|
||||
let startX: number = 0
|
||||
let startY: number = 0
|
||||
let isDragging: boolean = false
|
||||
let startX = 0
|
||||
let startY = 0
|
||||
let isDragging = false
|
||||
|
||||
const onMouseDown: (event: MouseEvent) => void = (event) => {
|
||||
startX = event.clientX - translateX()
|
||||
|
@ -125,7 +125,7 @@ export const Lightbox = (props: Props) => {
|
|||
cursor: 'grab',
|
||||
}))
|
||||
|
||||
let fadeTimer
|
||||
let fadeTimer: string | number | NodeJS.Timeout
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
|
@ -163,7 +163,7 @@ export const Lightbox = (props: Props) => {
|
|||
<button class={clsx(styles.control, styles.controlDefault)} onClick={(event) => zoomReset(event)}>
|
||||
1:1
|
||||
</button>
|
||||
<button class={styles.control} onClick={(event) => zoomIn(event)}>
|
||||
<button type="button" class={styles.control} onClick={(event) => zoomIn(event)}>
|
||||
+
|
||||
</button>
|
||||
</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
|
||||
*/
|
||||
type KebabObjectKeys<T> = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
[key in keyof T as Kebab<key & string>]: T[key] extends Object ? KebabObjectKeys<T[key]> : T[key]
|
||||
// biome-ignore lint/suspicious/noExplicitAny: TODO: <explanation>
|
||||
[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 (props.videoUrl.includes('youtube.com')) {
|
||||
const videoIdMatch = props.videoUrl.match(/watch=(\w+)/)
|
||||
setVideoId(videoIdMatch && videoIdMatch[1])
|
||||
setVideoId(videoIdMatch?.[1])
|
||||
} else {
|
||||
const videoIdMatch = props.videoUrl.match(/youtu.be\/(\w+)/)
|
||||
setVideoId(videoIdMatch && videoIdMatch[1])
|
||||
setVideoId(videoIdMatch?.[1])
|
||||
}
|
||||
} else {
|
||||
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()}>
|
||||
<div class={styles.videoContainer}>
|
||||
<iframe
|
||||
title={props.title}
|
||||
src={`https://player.vimeo.com/video/${videoId()}`}
|
||||
width="640"
|
||||
height="360"
|
||||
|
@ -69,6 +70,7 @@ export const VideoPlayer = (props: Props) => {
|
|||
<Match when={!isVimeo()}>
|
||||
<div class={styles.videoContainer}>
|
||||
<iframe
|
||||
title={props.title}
|
||||
width="560"
|
||||
height="315"
|
||||
src={`https://www.youtube.com/embed/${videoId()}`}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
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 { createContext, useContext, createSignal, createEffect } from 'solid-js'
|
||||
|
||||
|
@ -11,8 +13,7 @@ export interface SSEMessage {
|
|||
id: string
|
||||
entity: string // follower | shout | reaction
|
||||
action: string // create | delete | update | join | follow | seen
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
payload: any // Author Shout Message Reaction Chat
|
||||
payload: Partial<Author | Shout | Topic | Reaction>
|
||||
created_at?: number // unixtime x1000
|
||||
seen?: boolean
|
||||
}
|
||||
|
|
|
@ -49,11 +49,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
|
|||
const { addHandler } = useConnect()
|
||||
addHandler(handleMessage)
|
||||
|
||||
const loadMessages = async (
|
||||
by: MessagesBy,
|
||||
limit: number = 50,
|
||||
offset: number = 0,
|
||||
): Promise<Array<Message>> => {
|
||||
const loadMessages = async (by: MessagesBy, limit = 50, offset = 0): Promise<Array<Message>> => {
|
||||
if (inboxClient.private) {
|
||||
const msgs = await inboxClient.loadChatMessages({ by, limit, offset })
|
||||
setMessages((mmm) => [...new Set([...mmm, ...msgs])])
|
||||
|
|
|
@ -23,7 +23,7 @@ export function useProfileForm() {
|
|||
}
|
||||
|
||||
const userpicUrl = (userpic: string) => {
|
||||
if (userpic && userpic.includes('assets.discours.io')) {
|
||||
if (userpic?.includes('assets.discours.io')) {
|
||||
return userpic.replace('100x', '500x500')
|
||||
}
|
||||
return userpic
|
||||
|
|
|
@ -67,7 +67,7 @@ export type SessionContextType = {
|
|||
params: ForgotPasswordInput,
|
||||
) => Promise<{ data: ForgotPasswordResponse; errors: Error[] }>
|
||||
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
|
||||
authorizer: () => Authorizer
|
||||
}
|
||||
|
@ -114,7 +114,12 @@ export const SessionProvider = (props: {
|
|||
createEffect(() => {
|
||||
const token = searchParams()?.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 })
|
||||
})
|
||||
|
||||
|
@ -143,7 +148,7 @@ export const SessionProvider = (props: {
|
|||
setIsSessionLoaded(true)
|
||||
|
||||
return s.data
|
||||
} else {
|
||||
}
|
||||
console.info('[context.session] cannot refresh session', s.errors)
|
||||
setAuthError(s.errors.pop().message)
|
||||
|
||||
|
@ -151,7 +156,6 @@ export const SessionProvider = (props: {
|
|||
setIsSessionLoaded(true)
|
||||
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.info('[context.session] cannot refresh session', error)
|
||||
setAuthError(error)
|
||||
|
@ -232,7 +236,11 @@ export const SessionProvider = (props: {
|
|||
// initial effect
|
||||
onMount(async () => {
|
||||
const metaRes = await authorizer().getMetaData()
|
||||
setConfig({ ...defaultConfig, ...metaRes, redirectURL: window.location.origin })
|
||||
setConfig({
|
||||
...defaultConfig,
|
||||
...metaRes,
|
||||
redirectURL: window.location.origin,
|
||||
})
|
||||
let s: AuthToken
|
||||
try {
|
||||
s = await loadSession()
|
||||
|
@ -297,7 +305,11 @@ export const SessionProvider = (props: {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -314,9 +326,8 @@ export const SessionProvider = (props: {
|
|||
if (at?.data) {
|
||||
setSession(at.data)
|
||||
return at.data
|
||||
} else {
|
||||
console.warn(at?.errors)
|
||||
}
|
||||
console.warn(at?.errors)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
|
@ -325,15 +336,7 @@ export const SessionProvider = (props: {
|
|||
const oauth = async (oauthProvider: string) => {
|
||||
console.debug(`[context.session] calling authorizer's oauth for`)
|
||||
try {
|
||||
// const data: GraphqlQueryInput = {}
|
||||
// await authorizer().graphqlQuery(data)
|
||||
const ar: AuthorizeResponse | void = await authorizer().oauthLogin(
|
||||
oauthProvider,
|
||||
[],
|
||||
window.location.origin,
|
||||
oauthState(),
|
||||
)
|
||||
console.debug(ar)
|
||||
await authorizer().oauthLogin(oauthProvider, [], window.location.origin, oauthState())
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ if (isDev) {
|
|||
exchanges.unshift(devtoolsExchange)
|
||||
}
|
||||
|
||||
export const createGraphQLClient = (serviceName: string, token: string = '') => {
|
||||
export const createGraphQLClient = (serviceName: string, token = '') => {
|
||||
const options: ClientOptions = {
|
||||
url: `https://${serviceName}.discours.io`,
|
||||
maskTypename: true,
|
||||
|
|
|
@ -57,7 +57,7 @@ export const DogmaPage = () => {
|
|||
<b>Всегда исправляем ошибки, если мы их допустили.</b>
|
||||
Никто не безгрешен, иногда и мы ошибаемся. Заметили ошибку — отправьте{' '}
|
||||
<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
|
||||
</a>
|
||||
.
|
||||
|
|
|
@ -136,7 +136,7 @@ export const GuidePage = () => {
|
|||
вы хотите обсудить текст, прежде чем загрузить материал в интернет-редакцию —
|
||||
разместите его в google-документе, откройте доступ к редактированию по ссылке
|
||||
и напишите нам на
|
||||
<a href="mailto:welcome@discours.io" target="_blank">
|
||||
<a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||
welcome@discours.io
|
||||
</a>
|
||||
.
|
||||
|
@ -144,11 +144,11 @@ export const GuidePage = () => {
|
|||
<p>
|
||||
Если у вас возникают трудности с тем, чтобы подобрать к своему материалу
|
||||
иллюстрации, тоже пишите на
|
||||
<a href="mailto:welcome@discours.io" target="_blank">
|
||||
<a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||
почту
|
||||
</a>
|
||||
— наши коллеги-художники могут вам помочь{' '}
|
||||
<a href="/create?collab" target="_blank">
|
||||
<a href="/create?collab" target="_blank" rel="noreferrer">
|
||||
в режиме совместного редактирования
|
||||
</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
|
||||
</a>
|
||||
— поможем.
|
||||
|
@ -219,15 +219,15 @@ export const GuidePage = () => {
|
|||
<p>
|
||||
За свежими публикациями Дискурса можно следить не только на сайте,
|
||||
но и на страницах в
|
||||
<a href="https://facebook.com/discoursio/" target="_blank">
|
||||
<a href="https://facebook.com/discoursio/" target="_blank" rel="noreferrer">
|
||||
Фейсбуке
|
||||
</a>
|
||||
,{' '}
|
||||
<a href="https://vk.com/discoursio" target="_blank">
|
||||
<a href="https://vk.com/discoursio" target="_blank" rel="noreferrer">
|
||||
ВКонтакте
|
||||
</a>{' '}
|
||||
и
|
||||
<a href="https://t.me/discoursio" target="_blank">
|
||||
<a href="https://t.me/discoursio" target="_blank" rel="noreferrer">
|
||||
Телеграме
|
||||
</a>
|
||||
. А ещё раз в месяц мы отправляем <a href="#subscribe">почтовую рассылку</a>{' '}
|
||||
|
@ -236,7 +236,7 @@ export const GuidePage = () => {
|
|||
<p>
|
||||
Если вы хотите сотрудничать, что-то обсудить или предложить — пожалуйста, пишите
|
||||
на
|
||||
<a href="mailto:welcome@discours.io" target="_blank">
|
||||
<a href="mailto:welcome@discours.io" target="_blank" rel="noreferrer">
|
||||
welcome@discours.io
|
||||
</a>
|
||||
. Мы обязательно ответим.
|
||||
|
|
|
@ -111,7 +111,7 @@ export const HelpPage = () => {
|
|||
<h3 id="trustee">Войдите в попечительский совет Дискурса</h3>
|
||||
<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>
|
||||
, мы будем рады единомышленникам.
|
||||
|
@ -128,7 +128,7 @@ export const HelpPage = () => {
|
|||
<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
|
||||
</a>
|
||||
.
|
||||
|
|
|
@ -86,7 +86,11 @@ export const TermsOfUsePage = () => {
|
|||
<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
|
||||
</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
|
||||
</a>
|
||||
.
|
||||
|
@ -196,7 +200,7 @@ export const TermsOfUsePage = () => {
|
|||
<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
|
||||
</a>
|
||||
.
|
||||
|
@ -210,11 +214,11 @@ export const TermsOfUsePage = () => {
|
|||
</p>
|
||||
<p class="ng-binding">
|
||||
Общедоступные видео на сайте могут транслироваться с YouTube и регулируются{' '}
|
||||
<a href="https://policies.google.com/privacy" target="_blank">
|
||||
<a href="https://policies.google.com/privacy" target="_blank" rel="noreferrer">
|
||||
политикой конфиденциальности Google
|
||||
</a>
|
||||
. Загрузка видео на сайт также означает согласие с
|
||||
<a href="https://www.youtube.com/t/terms" target="_blank">
|
||||
<a href="https://www.youtube.com/t/terms" target="_blank" rel="noreferrer">
|
||||
Условиями использования YouTube
|
||||
</a>
|
||||
.
|
||||
|
@ -231,7 +235,7 @@ export const TermsOfUsePage = () => {
|
|||
<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
|
||||
</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 { translit } from './ru2en'
|
||||
|
||||
const prepareQuery = (searchQuery, lang) => {
|
||||
const prepareQuery = (searchQuery: string, lang: string) => {
|
||||
const q = searchQuery.toLowerCase()
|
||||
if (q.length === 0) return ''
|
||||
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()
|
||||
return preparedStr.split(' ').some((word) => word.startsWith(q))
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export const dummyFilter = <T extends Topic | Author>(
|
|||
}
|
||||
|
||||
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 ('title' in item) {
|
||||
|
|
|
@ -40,7 +40,7 @@ export const getOpenGraphImageUrl = (
|
|||
)}','${encodeURIComponent(options.title)}')/`
|
||||
|
||||
if (src.startsWith(thumborUrl)) {
|
||||
const thumborKey = src.replace(thumborUrl + '/unsafe', '')
|
||||
const thumborKey = src.replace(`${thumborUrl}/unsafe`, '')
|
||||
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
|
||||
})
|
||||
|
|
|
@ -13,7 +13,7 @@ export const getDescription = (body: string): string => {
|
|||
let description = ''
|
||||
let i = 0
|
||||
while (i < descriptionWordsArray.length && description.length < MAX_DESCRIPTION_LENGTH) {
|
||||
description += descriptionWordsArray[i] + ' '
|
||||
description += `${descriptionWordsArray[i]} `
|
||||
i++
|
||||
}
|
||||
return description.trim()
|
||||
|
|
|
@ -42,10 +42,10 @@ export const profileSocialLinks = (socialLinks: string[]): Link[] => {
|
|||
return processedLinks.sort((a, b) => {
|
||||
if (a.isPlaceholder && !b.isPlaceholder) {
|
||||
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) => {
|
||||
const anchor = document.querySelector('#' + elemId)
|
||||
const anchor = document.querySelector(`#${elemId}`)
|
||||
|
||||
if (anchor) {
|
||||
window.scrollTo({
|
||||
|
|
|
@ -26,8 +26,8 @@ export const byLength = (
|
|||
|
||||
export const byStat = (metric: keyof Stat | keyof TopicStat) => {
|
||||
return (a, b) => {
|
||||
const x = (a?.stat && a.stat[metric]) || 0
|
||||
const y = (b?.stat && b.stat[metric]) || 0
|
||||
const x = a.stat?.[metric] || 0
|
||||
const y = b.stat?.[metric] || 0
|
||||
if (x > y) return -1
|
||||
if (x < y) return 1
|
||||
return 0
|
||||
|
@ -36,8 +36,8 @@ export const byStat = (metric: keyof Stat | keyof TopicStat) => {
|
|||
|
||||
export const byTopicStatDesc = (metric: keyof TopicStat) => {
|
||||
return (a: Topic, b: Topic) => {
|
||||
const x = (a?.stat && a.stat[metric]) || 0
|
||||
const y = (b?.stat && b.stat[metric]) || 0
|
||||
const x = a.stat?.[metric] || 0
|
||||
const y = b.stat?.[metric] || 0
|
||||
if (x > y) return -1
|
||||
if (x < y) return 1
|
||||
return 0
|
||||
|
|
Loading…
Reference in New Issue
Block a user