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

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

View File

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

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
@ -14,5 +14,3 @@ https://github.com/jondot/hygen
```
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
@ -14,5 +14,3 @@ https://github.com/jondot/hygen
```
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:

900
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -173,7 +173,7 @@ const SimplifiedEditor = (props: Props) => {
createEditorTransaction(
() => editor(),
(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) =>
createEditorTransaction(
() => props.editor,
(editor) => editor && editor.isActive(name, attributes),
(editor) => editor?.isActive(name, attributes),
)
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)

View File

@ -41,9 +41,8 @@ export const ToggleTextWrap = Extension.create({
if (changesApplied) {
dispatch(tr)
return true
} else {
return false
}
return false
},
}
},

View File

@ -36,7 +36,7 @@ const CreateModalContent = (props: Props) => {
return user.selected === true
})
.map((user) => {
return user['id']
return user.id
})
return [...s]
})

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ export const PasswordField = (props: Props) => {
on(
() => error(),
() => {
props.errorMessage && props.errorMessage(error())
props.errorMessage?.(error())
},
{ defer: true },
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -176,7 +176,7 @@ export const EditView = (props: Props) => {
}
}
let autoSaveTimeOutId
let autoSaveTimeOutId: number | string | NodeJS.Timeout
const autoSaveRecursive = () => {
autoSaveTimeOutId = setTimeout(async () => {

View File

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

View File

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

View File

@ -33,7 +33,7 @@ export const DarkModeToggle = (props: Props) => {
onCleanup(() => {
setEditorDarkMode(false)
delete document.documentElement.dataset.editorDarkMode
document.documentElement.dataset.editorDarkMode = undefined
})
})

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,7 +86,11 @@ export const TermsOfUsePage = () => {
<p class="ng-binding">
Обнародование контента осуществляется Издательством в&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
</a>
. Все материалы сайта предназначены исключительно для личного некоммерческого использования.
@ -99,7 +103,7 @@ export const TermsOfUsePage = () => {
и&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
</a>
.
@ -196,7 +200,7 @@ export const TermsOfUsePage = () => {
<p class="ng-binding">
По&nbsp;желанию пользователя Издательство готово удалить любую информацию о&nbsp;нем,
собранную автоматическим путем. Для этого следует написать на&nbsp;адрес электронной почты{' '}
<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">
Общедоступные видео на&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
</a>
. Загрузка видео на&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
</a>
.
@ -231,7 +235,7 @@ export const TermsOfUsePage = () => {
<p class="ng-binding">
Любые вопросы и&nbsp;предложения по&nbsp;поводу функционирования сайта можно направить
по&nbsp;электронной почте{' '}
<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">&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 { 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) {

View File

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

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

View File

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

View File

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

View File

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

View File

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