Merge pull request #466 from Discours/hotfix/following
hotfix following status update
This commit is contained in:
commit
ab61c1e35a
|
@ -29,6 +29,14 @@ jobs:
|
|||
- name: Test production build
|
||||
run: npm run build
|
||||
|
||||
- name: Install Playwright
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run e2e
|
||||
env:
|
||||
BASE_URL: ${{ github.event.deployment_status.target_url }}
|
||||
|
||||
email-templates:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update templates on Mailgun
|
||||
|
|
20
.github/workflows/node-ci.yml
vendored
20
.github/workflows/node-ci.yml
vendored
|
@ -10,6 +10,9 @@ jobs:
|
|||
- uses: actions/setup-node@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Install CI checks
|
||||
run: npm ci
|
||||
|
||||
- name: Check types
|
||||
|
@ -23,20 +26,3 @@ jobs:
|
|||
|
||||
- name: Test production build
|
||||
run: npm run build
|
||||
|
||||
e2e:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.deployment_status.state == 'success'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
env:
|
||||
BASE_URL: ${{ github.event.deployment_status.target_url }}
|
||||
|
|
12
README.md
12
README.md
|
@ -16,15 +16,5 @@ npm run typecheck:watch
|
|||
fix styles, imports, formatting and autofixable linting errors:
|
||||
```
|
||||
npm run fix
|
||||
```
|
||||
## Code generation
|
||||
|
||||
generate new SolidJS component:
|
||||
```
|
||||
npm run hygen component new NewComponentName
|
||||
```
|
||||
|
||||
generate new SolidJS context:
|
||||
```
|
||||
npm run hygen context new NewContextName
|
||||
npm run format
|
||||
```
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/1.7.2/schema.json",
|
||||
"files": {
|
||||
"include": ["*.tsx", "*.ts", "*.js", "*.json"],
|
||||
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.gen.ts", "*.d.ts"]
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
to: src/components/<%= h.changeCase.pascal(name) %>/<%= h.changeCase.pascal(name) %>.tsx
|
||||
---
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import styles from './<%= h.changeCase.pascal(name) %>.module.scss'
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
}
|
||||
|
||||
export const <%= h.changeCase.pascal(name) %> = (props: Props) => {
|
||||
return (
|
||||
<div class={clsx(styles.<%= h.changeCase.pascal(name) %>, props.class)}>
|
||||
<%= h.changeCase.pascal(name) %>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
to: src/components/<%= h.changeCase.pascal(name) %>/index.ts
|
||||
---
|
||||
export { <%= h.changeCase.pascal(name) %> } from './<%= h.changeCase.pascal(name) %>'
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
to: src/components/<%= h.changeCase.pascal(name) %>/<%= h.changeCase.pascal(name) %>.module.scss
|
||||
---
|
||||
|
||||
.<%= h.changeCase.pascal(name) %> {
|
||||
display: block;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
---
|
||||
to: src/context/<%= h.changeCase.camel(name) %>.tsx
|
||||
---
|
||||
import type { Accessor, JSX } from 'solid-js'
|
||||
import { createContext, createSignal, useContext } from 'solid-js'
|
||||
|
||||
type <%= h.changeCase.pascal(name) %>ContextType = {
|
||||
|
||||
}
|
||||
|
||||
const <%= h.changeCase.pascal(name) %>Context = createContext<<%= h.changeCase.pascal(name) %>ContextType>()
|
||||
|
||||
export function use<%= h.changeCase.pascal(name) %>() {
|
||||
return useContext(<%= h.changeCase.pascal(name) %>Context)
|
||||
}
|
||||
|
||||
export const <%= h.changeCase.pascal(name) %>Provider = (props: { children: JSX.Element }) => {
|
||||
const actions = {
|
||||
}
|
||||
|
||||
const value: <%= h.changeCase.pascal(name) %>ContextType = { ...actions }
|
||||
|
||||
return <<%= h.changeCase.pascal(name) %>Context.Provider value={value}>{props.children}</<%= h.changeCase.pascal(name) %>Context.Provider>
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
message: |
|
||||
hygen {bold generator new} --name [NAME] --action [ACTION]
|
||||
hygen {bold generator with-prompt} --name [NAME] --action [ACTION]
|
||||
---
|
|
@ -1,16 +0,0 @@
|
|||
---
|
||||
to: gen/<%= name %>/<%= action || 'new' %>/hello.ejs.t
|
||||
---
|
||||
---
|
||||
to: app/hello.js
|
||||
---
|
||||
const hello = ```
|
||||
Hello!
|
||||
This is your first hygen template.
|
||||
|
||||
Learn what it can do here:
|
||||
|
||||
https://github.com/jondot/hygen
|
||||
```
|
||||
|
||||
console.log(hello)
|
|
@ -1,16 +0,0 @@
|
|||
---
|
||||
to: gen/<%= name %>/<%= action || 'new' %>/hello.ejs.t
|
||||
---
|
||||
---
|
||||
to: app/hello.js
|
||||
---
|
||||
const hello = ```
|
||||
Hello!
|
||||
This is your first prompt based hygen template.
|
||||
|
||||
Learn what it can do here:
|
||||
|
||||
https://github.com/jondot/hygen
|
||||
```
|
||||
|
||||
console.log(hello)
|
|
@ -1,14 +0,0 @@
|
|||
---
|
||||
to: gen/<%= name %>/<%= action || 'new' %>/prompt.js
|
||||
---
|
||||
|
||||
// see types of prompts:
|
||||
// https://github.com/enquirer/enquirer/tree/master/examples
|
||||
//
|
||||
module.exports = [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'message',
|
||||
message: "What's your message?"
|
||||
}
|
||||
]
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
setup: <%= name %>
|
||||
force: true # this is because mostly, people init into existing folders is safe
|
||||
---
|
2762
package-lock.json
generated
2762
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
110
package.json
110
package.json
|
@ -10,10 +10,9 @@
|
|||
"codegen": "graphql-codegen",
|
||||
"deploy": "graphql-codegen && npm run typecheck && vite build && vercel",
|
||||
"dev": "vite",
|
||||
"e2e": "npx playwright test --project=chromium",
|
||||
"e2e": "npx playwright test --project=webkit",
|
||||
"fix": "npm run check:code:fix && stylelint **/*.{scss,css} --fix",
|
||||
"format": "npx @biomejs/biome format src/. --write",
|
||||
"hygen": "HYGEN_TMPLS=gen hygen",
|
||||
"postinstall": "npm run codegen && npx patch-package",
|
||||
"check:code": "npx @biomejs/biome check src --log-kind=compact --verbose",
|
||||
"check:code:fix": "npx @biomejs/biome check . --apply",
|
||||
|
@ -33,8 +32,8 @@
|
|||
"mailgun.js": "10.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@authorizerdev/authorizer-js": "2.0.0",
|
||||
"@babel/core": "7.23.3",
|
||||
"@authorizerdev/authorizer-js": "^2.0.0",
|
||||
"@babel/core": "^7.24.5",
|
||||
"@biomejs/biome": "^1.7.2",
|
||||
"@graphql-codegen/cli": "^5.0.0",
|
||||
"@graphql-codegen/typescript": "^4.0.1",
|
||||
|
@ -45,7 +44,7 @@
|
|||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@nanostores/router": "0.13.0",
|
||||
"@nanostores/solid": "0.4.2",
|
||||
"@playwright/test": "1.41.2",
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@sentry/browser": "^7.113.0",
|
||||
"@solid-primitives/media": "2.2.3",
|
||||
|
@ -55,90 +54,87 @@
|
|||
"@solid-primitives/storage": "^3.5.0",
|
||||
"@solid-primitives/upload": "0.0.115",
|
||||
"@thisbeyond/solid-select": "0.14.0",
|
||||
"@tiptap/core": "2.2.3",
|
||||
"@tiptap/extension-blockquote": "2.2.3",
|
||||
"@tiptap/extension-bold": "2.2.3",
|
||||
"@tiptap/extension-bubble-menu": "2.2.3",
|
||||
"@tiptap/extension-bullet-list": "2.2.3",
|
||||
"@tiptap/extension-character-count": "2.2.3",
|
||||
"@tiptap/extension-collaboration": "2.2.3",
|
||||
"@tiptap/extension-collaboration-cursor": "2.2.3",
|
||||
"@tiptap/extension-document": "2.2.3",
|
||||
"@tiptap/extension-dropcursor": "2.2.3",
|
||||
"@tiptap/extension-floating-menu": "2.2.3",
|
||||
"@tiptap/extension-focus": "2.2.3",
|
||||
"@tiptap/extension-gapcursor": "2.2.3",
|
||||
"@tiptap/extension-hard-break": "2.2.3",
|
||||
"@tiptap/extension-heading": "2.2.3",
|
||||
"@tiptap/extension-highlight": "2.2.3",
|
||||
"@tiptap/extension-history": "2.2.3",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.3",
|
||||
"@tiptap/extension-image": "2.2.3",
|
||||
"@tiptap/extension-italic": "2.2.3",
|
||||
"@tiptap/extension-link": "2.2.3",
|
||||
"@tiptap/extension-list-item": "2.2.3",
|
||||
"@tiptap/extension-ordered-list": "2.2.3",
|
||||
"@tiptap/extension-paragraph": "2.2.3",
|
||||
"@tiptap/extension-placeholder": "2.2.3",
|
||||
"@tiptap/extension-strike": "2.2.3",
|
||||
"@tiptap/extension-text": "2.2.3",
|
||||
"@tiptap/extension-underline": "2.2.3",
|
||||
"@tiptap/extension-youtube": "2.2.3",
|
||||
"@types/js-cookie": "3.0.6",
|
||||
"@tiptap/core": "2.4.0",
|
||||
"@tiptap/extension-blockquote": "2.4.0",
|
||||
"@tiptap/extension-bold": "2.4.0",
|
||||
"@tiptap/extension-bubble-menu": "2.4.0",
|
||||
"@tiptap/extension-bullet-list": "2.4.0",
|
||||
"@tiptap/extension-character-count": "2.4.0",
|
||||
"@tiptap/extension-collaboration": "2.4.0",
|
||||
"@tiptap/extension-collaboration-cursor": "2.4.0",
|
||||
"@tiptap/extension-document": "2.4.0",
|
||||
"@tiptap/extension-dropcursor": "2.4.0",
|
||||
"@tiptap/extension-floating-menu": "2.4.0",
|
||||
"@tiptap/extension-focus": "2.4.0",
|
||||
"@tiptap/extension-gapcursor": "2.4.0",
|
||||
"@tiptap/extension-hard-break": "2.4.0",
|
||||
"@tiptap/extension-heading": "2.4.0",
|
||||
"@tiptap/extension-highlight": "2.4.0",
|
||||
"@tiptap/extension-history": "2.4.0",
|
||||
"@tiptap/extension-horizontal-rule": "2.4.0",
|
||||
"@tiptap/extension-image": "2.4.0",
|
||||
"@tiptap/extension-italic": "2.4.0",
|
||||
"@tiptap/extension-link": "2.4.0",
|
||||
"@tiptap/extension-list-item": "2.4.0",
|
||||
"@tiptap/extension-ordered-list": "2.4.0",
|
||||
"@tiptap/extension-paragraph": "2.4.0",
|
||||
"@tiptap/extension-placeholder": "2.4.0",
|
||||
"@tiptap/extension-strike": "2.4.0",
|
||||
"@tiptap/extension-text": "2.4.0",
|
||||
"@tiptap/extension-underline": "2.4.0",
|
||||
"@tiptap/extension-youtube": "2.4.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^20.11.0",
|
||||
"@urql/core": "4.2.3",
|
||||
"@urql/devtools": "^2.0.3",
|
||||
"babel-preset-solid": "1.8.4",
|
||||
"babel-preset-solid": "1.8.17",
|
||||
"bootstrap": "5.3.2",
|
||||
"clsx": "2.0.0",
|
||||
"cropperjs": "1.6.1",
|
||||
"cross-env": "7.0.3",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"ga-gtag": "1.2.0",
|
||||
"graphql": "16.8.1",
|
||||
"graphql-tag": "2.12.6",
|
||||
"hygen": "6.2.11",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"i18next": "22.4.15",
|
||||
"i18next-http-backend": "2.2.0",
|
||||
"i18next-icu": "2.3.0",
|
||||
"intl-messageformat": "10.5.3",
|
||||
"javascript-time-ago": "2.5.9",
|
||||
"intl-messageformat": "^10.5.14",
|
||||
"javascript-time-ago": "^2.5.10",
|
||||
"js-cookie": "3.0.5",
|
||||
"lint-staged": "15.1.0",
|
||||
"loglevel": "1.8.1",
|
||||
"loglevel-plugin-prefix": "0.8.4",
|
||||
"nanostores": "0.9.5",
|
||||
"loglevel": "^1.9.1",
|
||||
"loglevel-plugin-prefix": "^0.8.4",
|
||||
"nanostores": "^0.9.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"prosemirror-history": "1.3.2",
|
||||
"prosemirror-trailing-node": "2.0.7",
|
||||
"prosemirror-view": "1.32.7",
|
||||
"rollup": "4.17.2",
|
||||
"sass": "1.69.5",
|
||||
"sass": "1.77.2",
|
||||
"solid-js": "1.8.17",
|
||||
"solid-popper": "0.3.0",
|
||||
"solid-tiptap": "0.7.0",
|
||||
"solid-transition-group": "0.2.3",
|
||||
"stylelint": "^16.0.0",
|
||||
"stylelint-config-standard-scss": "^13.0.0",
|
||||
"stylelint": "^16.5.0",
|
||||
"stylelint-config-standard-scss": "^13.1.0",
|
||||
"stylelint-order": "^6.0.3",
|
||||
"stylelint-scss": "^6.1.0",
|
||||
"swiper": "11.0.5",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"typescript": "5.2.2",
|
||||
"typescript": "5.4.5",
|
||||
"typograf": "7.3.0",
|
||||
"uniqolor": "1.1.0",
|
||||
"vike": "0.4.148",
|
||||
"vite": "5.2.11",
|
||||
"vite-plugin-mkcert": "^1.17.3",
|
||||
"vite-plugin-node-polyfills": "0.21.0",
|
||||
"vite-plugin-sass-dts": "^1.3.17",
|
||||
"vite-plugin-solid": "2.10.1",
|
||||
"y-prosemirror": "1.2.2",
|
||||
"yjs": "13.6.12"
|
||||
"vite-plugin-mkcert": "^1.17.5",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-sass-dts": "^1.3.22",
|
||||
"vite-plugin-solid": "^2.10.2",
|
||||
"y-prosemirror": "1.2.5",
|
||||
"yjs": "13.6.15"
|
||||
},
|
||||
"overrides": {
|
||||
"y-prosemirror": "1.2.2",
|
||||
"yjs": "13.6.12"
|
||||
"y-prosemirror": "1.2.5",
|
||||
"yjs": "13.6.15"
|
||||
},
|
||||
"trustedDependencies": ["@biomejs/biome"]
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export default defineConfig({
|
|||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
/* Test against many viewports.
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
|
@ -68,10 +68,10 @@ export default defineConfig({
|
|||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
/* Run local dev server before starting the tests */
|
||||
//webServer: {
|
||||
// command: 'npm run dev',
|
||||
// url: 'https://localhost:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
//},
|
||||
})
|
||||
|
|
5
public/icons/arrow-right-2.svg
Normal file
5
public/icons/arrow-right-2.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.125 12.75H4.5C4.08854 12.75 3.75 12.4115 3.75 12C3.75 11.5885 4.08854 11.25 4.5 11.25H19.125C19.5365 11.25 19.875 11.5885 19.875 12C19.875 12.4115 19.5365 12.75 19.125 12.75Z" fill="currentColor"/>
|
||||
<path
|
||||
d="M14.0678 18.3593C13.8803 18.3593 13.6928 18.2916 13.547 18.151C13.2501 17.8593 13.2397 17.3853 13.5314 17.0885L18.4584 11.9999L13.5314 6.91137C13.2397 6.6145 13.2501 6.14054 13.547 5.84887C13.8439 5.56241 14.3178 5.57283 14.6043 5.8697L20.0366 11.4791C20.3178 11.7707 20.3178 12.2291 20.0366 12.5207L14.6043 18.1301C14.4584 18.2864 14.2657 18.3593 14.0678 18.3593Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 713 B |
BIN
public/placeholder-discussions.webp
Normal file
BIN
public/placeholder-discussions.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 379 KiB |
BIN
public/placeholder-experts.webp
Normal file
BIN
public/placeholder-experts.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 157 KiB |
BIN
public/placeholder-feed.webp
Normal file
BIN
public/placeholder-feed.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 220 KiB |
BIN
public/placeholder-join.webp
Normal file
BIN
public/placeholder-join.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 192 KiB |
|
@ -31,16 +31,7 @@ export const AudioPlayer = (props: Props) => {
|
|||
const [isPlaying, setIsPlaying] = createSignal(false)
|
||||
|
||||
const currentTack = createMemo(() => props.media[currentTrackIndex()])
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => currentTrackIndex(),
|
||||
() => {
|
||||
setCurrentTrackDuration(0)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
createEffect(on(currentTrackIndex, () => setCurrentTrackDuration(0), { defer: true }))
|
||||
|
||||
const handlePlayMedia = async (trackIndex: number) => {
|
||||
setIsPlaying(!isPlaying() || trackIndex !== currentTrackIndex())
|
||||
|
|
|
@ -48,7 +48,7 @@ export const Comment = (props: Props) => {
|
|||
const canEdit = createMemo(
|
||||
() =>
|
||||
Boolean(author()?.id) &&
|
||||
(props.comment?.created_by?.slug === author().slug || session()?.user?.roles.includes('editor')),
|
||||
(props.comment?.created_by?.slug === author()?.slug || session()?.user?.roles.includes('editor')),
|
||||
)
|
||||
|
||||
const body = createMemo(() => (editedBody() ? editedBody().trim() : props.comment.body.trim() || ''))
|
||||
|
|
|
@ -487,7 +487,7 @@ export const FullArticle = (props: Props) => {
|
|||
|
||||
<Show when={props.article.stat?.viewed}>
|
||||
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}>
|
||||
{t('viewsWithCount', { count: props.article.stat?.viewed })}
|
||||
{t('some views', { count: props.article.stat?.viewed })}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { openPage } from '@nanostores/router'
|
||||
import { clsx } from 'clsx'
|
||||
import { Match, Show, Switch, createEffect, createMemo, createSignal } from 'solid-js'
|
||||
import { Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js'
|
||||
|
||||
import { useFollowing } from '../../../context/following'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
@ -10,17 +10,17 @@ import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
|
|||
import { router, useRouter } from '../../../stores/router'
|
||||
import { translit } from '../../../utils/ru2en'
|
||||
import { isCyrillic } from '../../../utils/translate'
|
||||
import { BadgeSubscribeButton } from '../../_shared/BadgeSubscribeButton'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { CheckButton } from '../../_shared/CheckButton'
|
||||
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
||||
import { FollowingButton } from '../../_shared/FollowingButton'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Userpic } from '../Userpic'
|
||||
import styles from './AuthorBadge.module.scss'
|
||||
|
||||
type Props = {
|
||||
author: Author
|
||||
minimizeSubscribeButton?: boolean
|
||||
minimize?: boolean
|
||||
showMessageButton?: boolean
|
||||
iconButtons?: boolean
|
||||
nameOnly?: boolean
|
||||
|
@ -32,19 +32,21 @@ type Props = {
|
|||
export const AuthorBadge = (props: Props) => {
|
||||
const { mediaMatches } = useMediaQuery()
|
||||
const { author, requireAuthentication } = useSession()
|
||||
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
|
||||
const { follow, unfollow, follows, following } = useFollowing()
|
||||
const [isMobileView, setIsMobileView] = createSignal(false)
|
||||
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
|
||||
|
||||
createEffect(() => {
|
||||
if (!(subscriptions && props.author)) return
|
||||
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
|
||||
setIsSubscribed(subscribed)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
setIsMobileView(!mediaMatches.sm)
|
||||
})
|
||||
const [isFollowed, setIsFollowed] = createSignal<boolean>(
|
||||
follows?.authors?.some((authorEntity) => authorEntity.id === props.author?.id),
|
||||
)
|
||||
createEffect(() => setIsMobileView(!mediaMatches.sm))
|
||||
createEffect(
|
||||
on(
|
||||
[() => follows?.authors, () => props.author, following],
|
||||
([followingAuthors, currentAuthor, _]) => {
|
||||
setIsFollowed(followingAuthors?.some((followedAuthor) => followedAuthor.id === currentAuthor?.id))
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const { changeSearchParams } = useRouter()
|
||||
const { t, formatDate, lang } = useLocalize()
|
||||
|
@ -72,11 +74,10 @@ export const AuthorBadge = (props: Props) => {
|
|||
})
|
||||
|
||||
const handleFollowClick = () => {
|
||||
requireAuthentication(() => {
|
||||
isSubscribed()
|
||||
? unfollow(FollowingEntity.Author, props.author.slug)
|
||||
: follow(FollowingEntity.Author, props.author.slug)
|
||||
}, 'subscribe')
|
||||
requireAuthentication(async () => {
|
||||
const handle = isFollowed() ? unfollow : follow
|
||||
await handle(FollowingEntity.Author, props.author.slug)
|
||||
}, 'follow')
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -117,13 +118,13 @@ export const AuthorBadge = (props: Props) => {
|
|||
<Show when={props.author?.stat && !props.subscriptionsMode}>
|
||||
<div class={styles.bio}>
|
||||
<Show when={props.author?.stat.shouts > 0}>
|
||||
<div>{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}</div>
|
||||
<div>{t('some posts', { count: props.author.stat?.shouts ?? 0 })}</div>
|
||||
</Show>
|
||||
<Show when={props.author?.stat.comments > 0}>
|
||||
<div>{t('CommentsWithCount', { count: props.author.stat?.comments ?? 0 })}</div>
|
||||
<div>{t('some comments', { count: props.author.stat?.comments ?? 0 })}</div>
|
||||
</Show>
|
||||
<Show when={props.author?.stat.followers > 0}>
|
||||
<div>{t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}</div>
|
||||
<div>{t('some followers', { count: props.author.stat?.followers ?? 0 })}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
@ -132,12 +133,10 @@ export const AuthorBadge = (props: Props) => {
|
|||
</div>
|
||||
<Show when={props.author.slug !== author()?.slug && !props.nameOnly}>
|
||||
<div class={styles.actions}>
|
||||
<BadgeSubscribeButton
|
||||
action={() => handleFollowClick()}
|
||||
isSubscribed={isSubscribed()}
|
||||
actionMessageType={
|
||||
subscribeInAction()?.slug === props.author.slug ? subscribeInAction().type : undefined
|
||||
}
|
||||
<FollowingButton
|
||||
action={handleFollowClick}
|
||||
isFollowed={isFollowed()}
|
||||
actionMessageType={following()?.slug === props.author.slug ? following().type : undefined}
|
||||
/>
|
||||
<Show when={props.showMessageButton}>
|
||||
<Button
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
@include font-size(4rem);
|
||||
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.2em;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.authorAbout {
|
||||
|
@ -429,64 +429,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.listWrapper {
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.subscribersContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 1.4rem;
|
||||
margin-top: 1.5rem;
|
||||
gap: 1rem;
|
||||
margin-top: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.subscribers {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
margin: 0 2% 1rem;
|
||||
vertical-align: top;
|
||||
border-bottom: unset !important;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.subscribersItem {
|
||||
position: relative;
|
||||
|
||||
&:nth-child(1) {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: -4px;
|
||||
box-shadow: 0 0 0 1px var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.subscribersCounter {
|
||||
font-weight: 500;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: none !important;
|
||||
|
||||
.subscribersCounter {
|
||||
background: var(--background-color-invert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listWrapper {
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { useFollowing } from '../../../context/following'
|
|||
import { useLocalize } from '../../../context/localize'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
|
||||
import { SubscriptionFilter } from '../../../pages/types'
|
||||
import { FollowsFilter } from '../../../pages/types'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { isAuthor } from '../../../utils/isAuthor'
|
||||
import { translit } from '../../../utils/ru2en'
|
||||
|
@ -17,6 +17,7 @@ import { SharePopup, getShareUrl } from '../../Article/SharePopup'
|
|||
import { Modal } from '../../Nav/Modal'
|
||||
import { TopicBadge } from '../../Topic/TopicBadge'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { FollowingCounters } from '../../_shared/FollowingCounters/FollowingCounters'
|
||||
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
|
||||
import { AuthorBadge } from '../AuthorBadge'
|
||||
import { Userpic } from '../Userpic'
|
||||
|
@ -27,25 +28,25 @@ import styles from './AuthorCard.module.scss'
|
|||
type Props = {
|
||||
author: Author
|
||||
followers?: Author[]
|
||||
following?: Array<Author | Topic>
|
||||
flatFollows?: Array<Author | Topic>
|
||||
}
|
||||
export const AuthorCard = (props: Props) => {
|
||||
const { t, lang } = useLocalize()
|
||||
const { author, isSessionLoaded, requireAuthentication } = useSession()
|
||||
const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([])
|
||||
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
||||
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
|
||||
const [followsFilter, setFollowsFilter] = createSignal<FollowsFilter>('all')
|
||||
const [isFollowed, setIsFollowed] = createSignal<boolean>()
|
||||
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
|
||||
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
|
||||
const { follow, unfollow, follows, following } = useFollowing()
|
||||
|
||||
onMount(() => {
|
||||
setAuthorSubs(props.following)
|
||||
setAuthorSubs(props.flatFollows)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!(subscriptions && props.author)) return
|
||||
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
|
||||
setIsSubscribed(subscribed)
|
||||
if (!(follows && props.author)) return
|
||||
const followed = follows?.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
|
||||
setIsFollowed(followed)
|
||||
})
|
||||
|
||||
const name = createMemo(() => {
|
||||
|
@ -71,33 +72,33 @@ export const AuthorCard = (props: Props) => {
|
|||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (props.following) {
|
||||
if (subscriptionFilter() === 'authors') {
|
||||
setAuthorSubs(props.following.filter((s) => 'name' in s))
|
||||
} else if (subscriptionFilter() === 'topics') {
|
||||
setAuthorSubs(props.following.filter((s) => 'title' in s))
|
||||
} else if (subscriptionFilter() === 'communities') {
|
||||
setAuthorSubs(props.following.filter((s) => 'title' in s))
|
||||
if (props.flatFollows) {
|
||||
if (followsFilter() === 'authors') {
|
||||
setAuthorSubs(props.flatFollows.filter((s) => 'name' in s))
|
||||
} else if (followsFilter() === 'topics') {
|
||||
setAuthorSubs(props.flatFollows.filter((s) => 'title' in s))
|
||||
} else if (followsFilter() === 'communities') {
|
||||
setAuthorSubs(props.flatFollows.filter((s) => 'title' in s))
|
||||
} else {
|
||||
setAuthorSubs(props.following)
|
||||
setAuthorSubs(props.flatFollows)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleFollowClick = () => {
|
||||
requireAuthentication(() => {
|
||||
isSubscribed()
|
||||
isFollowed()
|
||||
? unfollow(FollowingEntity.Author, props.author.slug)
|
||||
: follow(FollowingEntity.Author, props.author.slug)
|
||||
}, 'subscribe')
|
||||
}, 'follow')
|
||||
}
|
||||
|
||||
const followButtonText = createMemo(() => {
|
||||
if (subscribeInAction()?.slug === props.author.slug) {
|
||||
return subscribeInAction().type === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...')
|
||||
if (following()?.slug === props.author.slug) {
|
||||
return following().type === 'follow' ? t('Following...') : t('Unfollowing...')
|
||||
}
|
||||
|
||||
if (isSubscribed()) {
|
||||
if (isFollowed()) {
|
||||
return (
|
||||
<>
|
||||
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
||||
|
@ -108,6 +109,73 @@ export const AuthorCard = (props: Props) => {
|
|||
return t('Follow')
|
||||
})
|
||||
|
||||
const FollowersModalView = () => (
|
||||
<>
|
||||
<h2>{t('Followers')}</h2>
|
||||
<div class={styles.listWrapper}>
|
||||
<div class="row">
|
||||
<div class="col-24">
|
||||
<For each={props.followers}>{(follower: Author) => <AuthorBadge author={follower} />}</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
const FollowingModalView = () => (
|
||||
<>
|
||||
<h2>{t('Subscriptions')}</h2>
|
||||
<ul class="view-switcher">
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': followsFilter() === 'all',
|
||||
})}
|
||||
>
|
||||
<button type="button" onClick={() => setFollowsFilter('all')}>
|
||||
{t('All')}
|
||||
</button>
|
||||
<span class="view-switcher__counter">{props.flatFollows.length}</span>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': followsFilter() === 'authors',
|
||||
})}
|
||||
>
|
||||
<button type="button" onClick={() => setFollowsFilter('authors')}>
|
||||
{t('Authors')}
|
||||
</button>
|
||||
<span class="view-switcher__counter">{props.flatFollows.filter((s) => 'name' in s).length}</span>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': followsFilter() === 'topics',
|
||||
})}
|
||||
>
|
||||
<button type="button" onClick={() => setFollowsFilter('topics')}>
|
||||
{t('Topics')}
|
||||
</button>
|
||||
<span class="view-switcher__counter">{props.flatFollows.filter((s) => 'title' in s).length}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<div class={styles.listWrapper}>
|
||||
<div class="row">
|
||||
<div class="col-24">
|
||||
<For each={authorSubs()}>
|
||||
{(subscription) =>
|
||||
isAuthor(subscription) ? (
|
||||
<AuthorBadge author={subscription} subscriptionsMode={true} />
|
||||
) : (
|
||||
<TopicBadge topic={subscription} subscriptionsMode={true} />
|
||||
)
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.author, 'row')}>
|
||||
<div class="col-md-5">
|
||||
|
@ -125,59 +193,14 @@ export const AuthorCard = (props: Props) => {
|
|||
<Show when={props.author.bio}>
|
||||
<div class={styles.authorAbout} innerHTML={props.author.bio} />
|
||||
</Show>
|
||||
<Show when={props.followers?.length > 0 || props.following?.length > 0}>
|
||||
<Show when={props.followers?.length > 0 || props.flatFollows?.length > 0}>
|
||||
<div class={styles.subscribersContainer}>
|
||||
<Show when={props.followers && props.followers.length > 0}>
|
||||
<a href="?m=followers" class={styles.subscribers}>
|
||||
<For each={props.followers.slice(0, 3)}>
|
||||
{(f) => (
|
||||
<Userpic size={'XS'} name={f.name} userpic={f.pic} class={styles.subscribersItem} />
|
||||
)}
|
||||
</For>
|
||||
<div class={styles.subscribersCounter}>
|
||||
{t('SubscriberWithCount', {
|
||||
count: props.followers.length ?? 0,
|
||||
})}
|
||||
</div>
|
||||
</a>
|
||||
</Show>
|
||||
|
||||
<Show when={props.following && props.following.length > 0}>
|
||||
<a href="?m=following" class={styles.subscribers}>
|
||||
<For each={props.following.slice(0, 3)}>
|
||||
{(f) => {
|
||||
if ('name' in f) {
|
||||
return (
|
||||
<Userpic
|
||||
size={'XS'}
|
||||
name={f.name}
|
||||
userpic={f.pic}
|
||||
class={styles.subscribersItem}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if ('title' in f) {
|
||||
return (
|
||||
<Userpic
|
||||
size={'XS'}
|
||||
name={f.title}
|
||||
userpic={f.pic}
|
||||
class={styles.subscribersItem}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}}
|
||||
</For>
|
||||
<div class={styles.subscribersCounter}>
|
||||
{t('SubscriptionWithCount', {
|
||||
count: props?.following.length ?? 0,
|
||||
})}
|
||||
</div>
|
||||
</a>
|
||||
</Show>
|
||||
<FollowingCounters
|
||||
followers={props.followers}
|
||||
followersAmount={props.author?.stat?.followers}
|
||||
following={props.flatFollows}
|
||||
followingAmount={props.flatFollows.length}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
@ -208,11 +231,11 @@ export const AuthorCard = (props: Props) => {
|
|||
<Show when={authorSubs()?.length}>
|
||||
<Button
|
||||
onClick={handleFollowClick}
|
||||
disabled={Boolean(subscribeInAction())}
|
||||
disabled={Boolean(following())}
|
||||
value={followButtonText()}
|
||||
isSubscribeButton={true}
|
||||
class={clsx({
|
||||
[stylesButton.subscribed]: isSubscribed(),
|
||||
[stylesButton.followed]: isFollowed(),
|
||||
})}
|
||||
/>
|
||||
</Show>
|
||||
|
@ -251,77 +274,12 @@ export const AuthorCard = (props: Props) => {
|
|||
</ShowOnlyOnClient>
|
||||
<Show when={props.followers}>
|
||||
<Modal variant="medium" isResponsive={true} name="followers" maxHeight>
|
||||
<>
|
||||
<h2>{t('Followers')}</h2>
|
||||
<div class={styles.listWrapper}>
|
||||
<div class="row">
|
||||
<div class="col-24">
|
||||
<For each={props.followers}>
|
||||
{(follower: Author) => <AuthorBadge author={follower} />}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<FollowersModalView />
|
||||
</Modal>
|
||||
</Show>
|
||||
<Show when={props.following}>
|
||||
<Show when={props.flatFollows}>
|
||||
<Modal variant="medium" isResponsive={true} name="following" maxHeight>
|
||||
<>
|
||||
<h2>{t('Subscriptions')}</h2>
|
||||
<ul class="view-switcher">
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': subscriptionFilter() === 'all',
|
||||
})}
|
||||
>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
||||
{t('All')}
|
||||
</button>
|
||||
<span class="view-switcher__counter">{props.following.length}</span>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': subscriptionFilter() === 'authors',
|
||||
})}
|
||||
>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('authors')}>
|
||||
{t('Authors')}
|
||||
</button>
|
||||
<span class="view-switcher__counter">
|
||||
{props.following.filter((s) => 'name' in s).length}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': subscriptionFilter() === 'topics',
|
||||
})}
|
||||
>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
||||
{t('Topics')}
|
||||
</button>
|
||||
<span class="view-switcher__counter">
|
||||
{props.following.filter((s) => 'title' in s).length}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<div class={styles.listWrapper}>
|
||||
<div class="row">
|
||||
<div class="col-24">
|
||||
<For each={authorSubs()}>
|
||||
{(subscription) =>
|
||||
isAuthor(subscription) ? (
|
||||
<AuthorBadge author={subscription} subscriptionsMode={true} />
|
||||
) : (
|
||||
<TopicBadge topic={subscription} subscriptionsMode={true} />
|
||||
)
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<FollowingModalView />
|
||||
</Modal>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { For, createMemo } from 'solid-js'
|
|||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { Subscribe } from '../_shared/Subscribe'
|
||||
import { Newsletter } from '../_shared/Newsletter'
|
||||
|
||||
import styles from './Footer.module.scss'
|
||||
|
||||
|
@ -133,7 +133,7 @@ export const Footer = () => {
|
|||
<div class="col-md-6">
|
||||
<h5>{t('Subscription')}</h5>
|
||||
<p>{t('Join our maillist')}</p>
|
||||
<Subscribe />
|
||||
<Newsletter />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -34,12 +34,14 @@ export const AudioUploader = (props: Props) => {
|
|||
|
||||
const handleChangeIndex = (direction: 'up' | 'down', index: number) => {
|
||||
const media = [...props.audio]
|
||||
if (direction === 'up' && index > 0) {
|
||||
const copy = media.splice(index, 1)[0]
|
||||
media.splice(index - 1, 0, copy)
|
||||
} else if (direction === 'down' && index < media.length - 1) {
|
||||
const copy = media.splice(index, 1)[0]
|
||||
media.splice(index + 1, 0, copy)
|
||||
if (media?.length > 0) {
|
||||
if (direction === 'up' && index > 0) {
|
||||
const copy = media.splice(index, 1)[0]
|
||||
media.splice(index - 1, 0, copy)
|
||||
} else if (direction === 'down' && index < media.length - 1) {
|
||||
const copy = media.splice(index, 1)[0]
|
||||
media.splice(index + 1, 0, copy)
|
||||
}
|
||||
}
|
||||
props.onAudioSorted(media)
|
||||
}
|
||||
|
|
|
@ -12,11 +12,6 @@ declare module '@tiptap/core' {
|
|||
|
||||
export default Node.create({
|
||||
name: 'article',
|
||||
defaultOptions: {
|
||||
HTMLAttributes: {
|
||||
'data-type': 'incut',
|
||||
},
|
||||
},
|
||||
group: 'block',
|
||||
content: 'block+',
|
||||
|
||||
|
@ -32,6 +27,12 @@ export default Node.create({
|
|||
return ['article', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
'data-type': 'incut',
|
||||
}
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
'data-float': {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||
import { Blockquote, BlockquoteOptions } from '@tiptap/extension-blockquote'
|
||||
|
||||
export type QuoteTypes = 'quote' | 'punchline'
|
||||
|
||||
|
@ -13,11 +13,13 @@ declare module '@tiptap/core' {
|
|||
|
||||
export const CustomBlockquote = Blockquote.extend({
|
||||
name: 'blockquote',
|
||||
defaultOptions: {
|
||||
HTMLAttributes: {},
|
||||
},
|
||||
group: 'block',
|
||||
content: 'block+',
|
||||
|
||||
addOptions(): BlockquoteOptions {
|
||||
return {} as BlockquoteOptions
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
'data-float': {
|
||||
|
@ -34,14 +36,12 @@ export const CustomBlockquote = Blockquote.extend({
|
|||
return {
|
||||
toggleBlockquote:
|
||||
(type) =>
|
||||
({ commands }) => {
|
||||
return commands.toggleWrap(this.name, { 'data-type': type })
|
||||
},
|
||||
({ commands }) =>
|
||||
commands.toggleWrap(this.name, { 'data-type': type }),
|
||||
setBlockQuoteFloat:
|
||||
(value) =>
|
||||
({ commands }) => {
|
||||
return commands.updateAttributes(this.name, { 'data-float': value })
|
||||
},
|
||||
({ commands }) =>
|
||||
commands.updateAttributes(this.name, { 'data-float': value }),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
268
src/components/Feed/Placeholder/Placeholder.module.scss
Normal file
268
src/components/Feed/Placeholder/Placeholder.module.scss
Normal file
|
@ -0,0 +1,268 @@
|
|||
.placeholder {
|
||||
border-radius: 2.2rem;
|
||||
display: flex;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
h3 {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
align-items: center;
|
||||
border-radius: 1.2rem;
|
||||
display: flex;
|
||||
|
||||
@include font-size(1.5rem);
|
||||
|
||||
gap: 0.6rem;
|
||||
justify-content: center;
|
||||
margin-top: 3rem;
|
||||
padding: 1rem 2rem;
|
||||
width: 100%;
|
||||
|
||||
.icon {
|
||||
height: 2.4rem;
|
||||
width: 2.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder--feed-mode {
|
||||
flex-direction: column;
|
||||
min-height: 40rem;
|
||||
text-align: center;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
aspect-ratio: 1 / 0.8;
|
||||
}
|
||||
|
||||
.placeholderCover {
|
||||
flex: 1 100%;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
bottom: 0;
|
||||
content: '';
|
||||
height: 20%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&.placeholder--feedMy .placeholderCover::after {
|
||||
background: linear-gradient(to top, #171032, rgb(23 16 50 / 0%));
|
||||
}
|
||||
|
||||
&.placeholder--feedCollaborations .placeholderCover::after {
|
||||
background: linear-gradient(to top, #070709, rgb(7 7 9 / 0%));
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder--profile-mode {
|
||||
min-height: 40rem;
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
max-height: 30rem;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.placeholderCover {
|
||||
flex: 1;
|
||||
padding: 1.6rem;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
order: 2;
|
||||
position: static;
|
||||
}
|
||||
|
||||
img {
|
||||
aspect-ratio: 16/10;
|
||||
min-width: 40rem;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
object-position: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.placeholderContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.2;
|
||||
min-width: 60%;
|
||||
padding: 0 2rem 2rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: 1.6rem;
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
@include font-size(3.8rem);
|
||||
}
|
||||
|
||||
.button {
|
||||
background: var(--background-color-invert);
|
||||
bottom: 2rem;
|
||||
color: var(--default-color-invert);
|
||||
font-size: 1.6rem;
|
||||
left: 2rem;
|
||||
right: 2rem;
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
left: auto;
|
||||
position: absolute;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.icon {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.placeholderCover {
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
left: 0;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholderContent {
|
||||
padding: 1.6rem;
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
br {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder--feedMy,
|
||||
.placeholder--feedCollaborations {
|
||||
color: var(--default-color-invert);
|
||||
|
||||
button,
|
||||
.button {
|
||||
background: var(--background-color);
|
||||
color: var(--default-color);
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder--feedMy {
|
||||
background: #171032;
|
||||
|
||||
.placeholderCover {
|
||||
img {
|
||||
object-position: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder--feedCollaborations {
|
||||
background: #070709;
|
||||
|
||||
.placeholderCover {
|
||||
img {
|
||||
object-position: bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder--feedDiscussions {
|
||||
background: #E9E9EE;
|
||||
|
||||
.placeholderCover {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
height: 90%;
|
||||
mix-blend-mode: multiply;
|
||||
object-fit: contain;
|
||||
top: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
background: var(--background-color-invert);
|
||||
color: var(--default-color-invert);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.placeholder--author {
|
||||
background: #E58B72;
|
||||
}
|
||||
|
||||
.placeholder--authorComments {
|
||||
background: #E9E9EE;
|
||||
|
||||
.placeholderCover {
|
||||
img {
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottomLinks {
|
||||
display: flex;
|
||||
|
||||
@include font-size(1.6rem);
|
||||
|
||||
gap: 4rem;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
flex-direction: column;
|
||||
gap: 1.4rem;
|
||||
}
|
||||
|
||||
a {
|
||||
border: none !important;
|
||||
padding-left: 2.6rem;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.icon {
|
||||
filter: invert(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
filter: invert(1);
|
||||
height: 1.8rem;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
transition: filter 0.2s;
|
||||
width: 1.8rem;
|
||||
}
|
||||
}
|
120
src/components/Feed/Placeholder/Placeholder.tsx
Normal file
120
src/components/Feed/Placeholder/Placeholder.tsx
Normal file
|
@ -0,0 +1,120 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { For, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import styles from './Placeholder.module.scss'
|
||||
|
||||
export type PlaceholderProps = {
|
||||
type: string
|
||||
mode: 'feed' | 'profile'
|
||||
}
|
||||
|
||||
export const Placeholder = (props: PlaceholderProps) => {
|
||||
const { t } = useLocalize()
|
||||
const { author } = useSession()
|
||||
|
||||
const data = {
|
||||
feedMy: {
|
||||
image: 'placeholder-feed.webp',
|
||||
header: t('Feed settings'),
|
||||
text: t('Placeholder feed'),
|
||||
buttonLabel: author() ? t('Popular authors') : t('Create own feed'),
|
||||
href: '/authors?by=followers',
|
||||
},
|
||||
feedCollaborations: {
|
||||
image: 'placeholder-experts.webp',
|
||||
header: t('Find collaborators'),
|
||||
text: t('Placeholder feedCollaborations'),
|
||||
buttonLabel: t('Find co-authors'),
|
||||
href: '/authors?by=name',
|
||||
},
|
||||
feedDiscussions: {
|
||||
image: 'placeholder-discussions.webp',
|
||||
header: t('Participate in discussions'),
|
||||
text: t('Placeholder feedDiscussions'),
|
||||
buttonLabel: author() ? t('Current discussions') : t('Enter'),
|
||||
href: '/feed?by=last_comment',
|
||||
},
|
||||
author: {
|
||||
image: 'placeholder-join.webp',
|
||||
header: t('Join our team of authors'),
|
||||
text: t('Join our team of authors text'),
|
||||
buttonLabel: t('Create post'),
|
||||
href: '/create',
|
||||
profileLinks: [
|
||||
{
|
||||
href: '/how-to-write-a-good-article',
|
||||
label: t('How to write a good article'),
|
||||
},
|
||||
],
|
||||
},
|
||||
authorComments: {
|
||||
image: 'placeholder-discussions.webp',
|
||||
header: t('Join discussions'),
|
||||
text: t('Placeholder feedDiscussions'),
|
||||
buttonLabel: t('Go to discussions'),
|
||||
href: '/feed?by=last_comment',
|
||||
profileLinks: [
|
||||
{
|
||||
href: '/about/discussion-rules',
|
||||
label: t('Discussion rules'),
|
||||
},
|
||||
{
|
||||
href: '/about/discussion-rules#ban',
|
||||
label: t('Block rules'),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class={clsx(
|
||||
styles.placeholder,
|
||||
styles[`placeholder--${props.type}`],
|
||||
styles[`placeholder--${props.mode}-mode`],
|
||||
)}
|
||||
>
|
||||
<div class={styles.placeholderCover}>
|
||||
<img src={`/${data[props.type].image}`} />
|
||||
</div>
|
||||
<div class={styles.placeholderContent}>
|
||||
<div>
|
||||
<h3 innerHTML={data[props.type].header} />
|
||||
<p innerHTML={data[props.type].text} />
|
||||
</div>
|
||||
|
||||
<Show when={data[props.type].profileLinks}>
|
||||
<div class={styles.bottomLinks}>
|
||||
<For each={data[props.type].profileLinks}>
|
||||
{(link) => (
|
||||
<a href={link.href}>
|
||||
<Icon name="link-white" class={styles.icon} />
|
||||
{link.label}
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={author()}
|
||||
fallback={
|
||||
<a class={styles.button} href="?m=auth&mode=login">
|
||||
{data[props.type].buttonLabel}
|
||||
</a>
|
||||
}
|
||||
>
|
||||
<a class={styles.button} href={data[props.type].href}>
|
||||
{data[props.type].buttonLabel}
|
||||
<Show when={props.mode === 'profile'}>
|
||||
<Icon name="arrow-right-2" class={styles.icon} />
|
||||
</Show>
|
||||
</a>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/Feed/Placeholder/index.ts
Normal file
1
src/components/Feed/Placeholder/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { Placeholder } from './Placeholder'
|
|
@ -15,7 +15,7 @@ import styles from './Sidebar.module.scss'
|
|||
export const Sidebar = () => {
|
||||
const { t } = useLocalize()
|
||||
const { seen } = useSeen()
|
||||
const { subscriptions } = useFollowing()
|
||||
const { follows } = useFollowing()
|
||||
const { page } = useRouter()
|
||||
const { articlesByTopic, articlesByAuthor } = useArticlesStore()
|
||||
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
|
||||
|
@ -83,35 +83,9 @@ export const Sidebar = () => {
|
|||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getPagePath(router, 'feedBookmarks')}
|
||||
class={clsx({
|
||||
[styles.selected]: page().route === 'feedBookmarks',
|
||||
})}
|
||||
>
|
||||
<span class={styles.sidebarItemName}>
|
||||
<Icon name="bookmark" class={styles.icon} />
|
||||
{t('Bookmarks')}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getPagePath(router, 'feedNotifications')}
|
||||
class={clsx({
|
||||
[styles.selected]: page().route === 'feedNotifications',
|
||||
})}
|
||||
>
|
||||
<span class={styles.sidebarItemName}>
|
||||
<Icon name="feed-notifications" class={styles.icon} />
|
||||
{t('Notifications')}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Show when={subscriptions.authors.length > 0 || subscriptions.topics.length > 0}>
|
||||
<Show when={follows?.authors?.length > 0 || follows?.topics?.length > 0}>
|
||||
<h4
|
||||
classList={{ [styles.opened]: isSubscriptionsVisible() }}
|
||||
onClick={() => {
|
||||
|
@ -123,7 +97,7 @@ export const Sidebar = () => {
|
|||
</h4>
|
||||
|
||||
<ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}>
|
||||
<For each={subscriptions.authors}>
|
||||
<For each={follows.authors}>
|
||||
{(a: Author) => (
|
||||
<li>
|
||||
<a href={`/author/${a.slug}`} classList={{ [styles.unread]: checkAuthorIsSeen(a.slug) }}>
|
||||
|
@ -135,7 +109,7 @@ export const Sidebar = () => {
|
|||
</li>
|
||||
)}
|
||||
</For>
|
||||
<For each={subscriptions.topics}>
|
||||
<For each={follows.topics}>
|
||||
{(topic) => (
|
||||
<li>
|
||||
<a
|
||||
|
|
|
@ -63,18 +63,8 @@ export const PasswordField = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => error(),
|
||||
() => {
|
||||
props.errorMessage?.(error())
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
createEffect(() => {
|
||||
setError(props.setError)
|
||||
})
|
||||
createEffect(on(error, (er) => er && props.errorMessage?.(er), { defer: true }))
|
||||
createEffect(() => setError(props.setError))
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.PassportField, props.class)}>
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useModalStore } from '../../../stores/ui'
|
|||
import { getDescription } from '../../../utils/meta'
|
||||
import { SharePopup, getShareUrl } from '../../Article/SharePopup'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Subscribe } from '../../_shared/Subscribe'
|
||||
import { Newsletter } from '../../_shared/Newsletter'
|
||||
import { AuthModal } from '../AuthModal'
|
||||
import { ConfirmModal } from '../ConfirmModal'
|
||||
import { HeaderAuth } from '../HeaderAuth'
|
||||
|
@ -301,7 +301,7 @@ export const Header = (props: Props) => {
|
|||
</ul>
|
||||
|
||||
<h4>{t('Newsletter')}</h4>
|
||||
<Subscribe variant={'mobileSubscription'} />
|
||||
<Newsletter variant={'mobileSubscription'} />
|
||||
|
||||
<h4>{t('Language')}</h4>
|
||||
<select
|
||||
|
|
|
@ -21,7 +21,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
|||
<Popup {...props} horizontalAnchor="right" popupCssClass={styles.profilePopup}>
|
||||
<ul class="nodash">
|
||||
<li>
|
||||
<a class={styles.action} href={getPagePath(router, 'author', { slug: author().slug })}>
|
||||
<a class={styles.action} href={getPagePath(router, 'author', { slug: author()?.slug })}>
|
||||
<Icon name="profile" class={styles.icon} />
|
||||
{t('Profile')}
|
||||
</a>
|
||||
|
@ -35,7 +35,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
|||
<li>
|
||||
<a
|
||||
class={styles.action}
|
||||
href={`${getPagePath(router, 'author', { slug: author().slug })}?m=following`}
|
||||
href={`${getPagePath(router, 'author', { slug: author()?.slug })}?m=following`}
|
||||
>
|
||||
<Icon name="feed-all" class={styles.icon} />
|
||||
{t('Subscriptions')}
|
||||
|
@ -44,7 +44,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
|||
<li>
|
||||
<a
|
||||
class={styles.action}
|
||||
href={`${getPagePath(router, 'authorComments', { slug: author().slug })}`}
|
||||
href={`${getPagePath(router, 'authorComments', { slug: author()?.slug })}`}
|
||||
>
|
||||
<Icon name="comment" class={styles.icon} />
|
||||
{t('Comments')}
|
||||
|
|
|
@ -57,7 +57,7 @@ export const ProfileSettings = () => {
|
|||
const [nameError, setNameError] = createSignal<string>()
|
||||
const { form, submit, updateFormField, setForm } = useProfileForm()
|
||||
const { showSnackbar } = useSnackbar()
|
||||
const { loadAuthor, session } = useSession()
|
||||
const { loadSession, session } = useSession()
|
||||
const { showConfirm } = useConfirm()
|
||||
const [clearAbout, setClearAbout] = createSignal(false)
|
||||
|
||||
|
@ -112,7 +112,7 @@ export const ProfileSettings = () => {
|
|||
setIsSaving(false)
|
||||
}
|
||||
|
||||
await loadAuthor() // renews author's profile
|
||||
setTimeout(loadSession, 5000) // renews author's profile
|
||||
}
|
||||
|
||||
const handleCancel = async () => {
|
||||
|
|
|
@ -62,7 +62,7 @@ export const TableOfContents = (props: Props) => {
|
|||
createEffect(
|
||||
on(
|
||||
() => props.body,
|
||||
() => debouncedUpdateHeadings(),
|
||||
(_) => debouncedUpdateHeadings(),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
@ -123,12 +123,12 @@
|
|||
width: 9em;
|
||||
}
|
||||
|
||||
.isSubscribing {
|
||||
.isFollowing {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/*
|
||||
.isSubscribed {
|
||||
.isFollowed {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
transition:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { Show, createEffect, createMemo, createSignal } from 'solid-js'
|
||||
import { Show, createEffect, createMemo, createSignal, on } from 'solid-js'
|
||||
|
||||
import { useFollowing } from '../../context/following'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
|
@ -7,18 +7,16 @@ import { useSession } from '../../context/session'
|
|||
import { FollowingEntity, type Topic } from '../../graphql/schema/core.gen'
|
||||
import { capitalize } from '../../utils/capitalize'
|
||||
import { CardTopic } from '../Feed/CardTopic'
|
||||
import { Button } from '../_shared/Button'
|
||||
import { CheckButton } from '../_shared/CheckButton'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { FollowingButton } from '../_shared/FollowingButton'
|
||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||
|
||||
import stylesButton from '../_shared/Button/Button.module.scss'
|
||||
import styles from './Card.module.scss'
|
||||
|
||||
interface TopicProps {
|
||||
topic: Topic
|
||||
compact?: boolean
|
||||
subscribed?: boolean
|
||||
followed?: boolean
|
||||
shortDescription?: boolean
|
||||
subscribeButtonBottom?: boolean
|
||||
additionalClass?: string
|
||||
|
@ -27,7 +25,7 @@ interface TopicProps {
|
|||
showPublications?: boolean
|
||||
showDescription?: boolean
|
||||
isCardMode?: boolean
|
||||
minimizeSubscribeButton?: boolean
|
||||
minimize?: boolean
|
||||
isNarrow?: boolean
|
||||
withIcon?: boolean
|
||||
}
|
||||
|
@ -38,39 +36,23 @@ export const TopicCard = (props: TopicProps) => {
|
|||
capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''),
|
||||
)
|
||||
const { author, requireAuthentication } = useSession()
|
||||
const [isSubscribed, setIsSubscribed] = createSignal()
|
||||
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
|
||||
|
||||
createEffect(() => {
|
||||
if (!(subscriptions && props.topic)) return
|
||||
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
|
||||
setIsSubscribed(subscribed)
|
||||
})
|
||||
const { follow, unfollow, follows } = useFollowing()
|
||||
const [isFollowed, setIsFollowed] = createSignal(false)
|
||||
createEffect(
|
||||
on([() => follows, () => props.topic], ([flws, tpc]) => {
|
||||
if (flws && tpc) {
|
||||
const followed = follows?.topics?.some((topics) => topics.id === props.topic?.id)
|
||||
setIsFollowed(followed)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const handleFollowClick = () => {
|
||||
requireAuthentication(() => {
|
||||
isSubscribed()
|
||||
isFollowed()
|
||||
? unfollow(FollowingEntity.Topic, props.topic.slug)
|
||||
: follow(FollowingEntity.Topic, props.topic.slug)
|
||||
}, 'subscribe')
|
||||
}
|
||||
|
||||
const subscribeValue = () => {
|
||||
return (
|
||||
<>
|
||||
<Show when={props.iconButton}>
|
||||
<Show when={isSubscribed()} fallback="+">
|
||||
<Icon name="check-subscribed" />
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={!props.iconButton}>
|
||||
<Show when={isSubscribed()} fallback={t('Follow')}>
|
||||
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
|
||||
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
||||
</Show>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}, 'follow')
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -132,27 +114,12 @@ export const TopicCard = (props: TopicProps) => {
|
|||
<ShowOnlyOnClient>
|
||||
<Show when={author()}>
|
||||
<Show
|
||||
when={!props.minimizeSubscribeButton}
|
||||
when={!props.minimize}
|
||||
fallback={
|
||||
<CheckButton
|
||||
text={t('Follow')}
|
||||
checked={Boolean(isSubscribed())}
|
||||
onClick={handleFollowClick}
|
||||
/>
|
||||
<CheckButton text={t('Follow')} checked={isFollowed()} onClick={handleFollowClick} />
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="M"
|
||||
value={subscribeValue()}
|
||||
onClick={handleFollowClick}
|
||||
isSubscribeButton={true}
|
||||
class={clsx(styles.actionButton, {
|
||||
[styles.isSubscribing]:
|
||||
subscribeInAction()?.slug === props.topic.slug ? subscribeInAction().type : undefined,
|
||||
[stylesButton.subscribed]: isSubscribed(),
|
||||
})}
|
||||
/>
|
||||
<FollowingButton action={handleFollowClick} isFollowed={isFollowed()} />
|
||||
</Show>
|
||||
</Show>
|
||||
</ShowOnlyOnClient>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.topicHeader {
|
||||
@include font-size(1.7rem);
|
||||
|
||||
font-weight: 500;
|
||||
padding: 2.8rem $container-padding-x 0;
|
||||
text-align: center;
|
||||
|
||||
|
@ -12,10 +11,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.topicDescription {
|
||||
@include font-size(1.8rem);
|
||||
|
||||
line-height: 1.4;
|
||||
margin: 1rem 0 2rem;
|
||||
}
|
||||
|
||||
.topicActions {
|
||||
margin-top: 2.8rem;
|
||||
|
||||
.write {
|
||||
.writeControl {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -23,13 +29,38 @@
|
|||
min-width: 64px;
|
||||
font-size: 17px;
|
||||
padding: 8px 16px;
|
||||
background: var(--background-color-invert);
|
||||
color: var(--default-color-invert);
|
||||
border: none;
|
||||
border: 1px solid #f7f7f7;
|
||||
background: #f7f7f7;
|
||||
color: var(--default-color);
|
||||
font-weight: 500;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
margin: 0 1.2rem 1em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.followControl,
|
||||
.writeControl {
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.topicDetails {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 1.4rem;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.topicDetailsItem {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-right: 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.topicDetailsIcon {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Topic } from '../../graphql/schema/core.gen'
|
||||
import type { Author, Topic } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { Show, createEffect, createSignal } from 'solid-js'
|
||||
|
@ -9,22 +9,25 @@ import { useSession } from '../../context/session'
|
|||
import { FollowingEntity } from '../../graphql/schema/core.gen'
|
||||
import { Button } from '../_shared/Button'
|
||||
|
||||
import { FollowingCounters } from '../_shared/FollowingCounters/FollowingCounters'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import styles from './Full.module.scss'
|
||||
|
||||
type Props = {
|
||||
topic: Topic
|
||||
followers?: Author[]
|
||||
authors?: Author[]
|
||||
}
|
||||
|
||||
export const FullTopic = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const { subscriptions, setFollowing } = useFollowing()
|
||||
const { follows, changeFollowing } = useFollowing()
|
||||
const { requireAuthentication } = useSession()
|
||||
const [followed, setFollowed] = createSignal()
|
||||
|
||||
createEffect(() => {
|
||||
const subs = subscriptions
|
||||
if (subs?.topics.length !== 0) {
|
||||
const items = subs.topics || []
|
||||
if (follows?.topics.length !== 0) {
|
||||
const items = follows.topics || []
|
||||
setFollowed(items.some((x: Topic) => x?.slug === props.topic?.slug))
|
||||
}
|
||||
})
|
||||
|
@ -33,26 +36,46 @@ export const FullTopic = (props: Props) => {
|
|||
const really = !followed()
|
||||
setFollowed(really)
|
||||
requireAuthentication(() => {
|
||||
setFollowing(FollowingEntity.Topic, props.topic.slug, really)
|
||||
changeFollowing(FollowingEntity.Topic, props.topic.slug, really)
|
||||
}, 'follow')
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
|
||||
<h1>#{props.topic?.title}</h1>
|
||||
<p innerHTML={props.topic?.body} />
|
||||
<p class={styles.topicDescription} innerHTML={props.topic?.body} />
|
||||
|
||||
<div class={styles.topicDetails}>
|
||||
<Show when={props.topic?.stat}>
|
||||
<div class={styles.topicDetailsItem}>
|
||||
<Icon name="feed-all" class={styles.topicDetailsIcon} />
|
||||
{t('some posts', {
|
||||
count: props.topic?.stat.shouts ?? 0,
|
||||
})}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<FollowingCounters
|
||||
followers={props.followers}
|
||||
followersAmount={props.topic?.stat?.followers}
|
||||
authors={props.authors}
|
||||
authorsAmount={props.topic?.stat?.authors || props.authors?.length || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={clsx(styles.topicActions)}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleFollowClick}
|
||||
value={followed() ? t('Unfollow the topic') : t('Follow the topic')}
|
||||
class={styles.followControl}
|
||||
/>
|
||||
<a class={styles.write} href={`/create/?topicId=${props.topic?.id}`}>
|
||||
<a class={styles.writeControl} href={`/create/?topicId=${props.topic?.id}`}>
|
||||
{t('Write about the topic')}
|
||||
</a>
|
||||
</div>
|
||||
<Show when={props.topic?.pic}>
|
||||
<img src={props.topic.pic} alt={props.topic?.title} />
|
||||
<img src={props.topic?.pic} alt={props.topic?.title} />
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
|
||||
.info {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
border: none;
|
||||
|
||||
// display: flex;
|
||||
|
@ -62,11 +63,13 @@
|
|||
|
||||
.title {
|
||||
@include font-size(2.2rem);
|
||||
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
line-height: 1.4;
|
||||
margin: 0.8rem 0;
|
||||
-webkit-line-clamp: 2;
|
||||
|
@ -104,6 +107,7 @@
|
|||
|
||||
.title {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
font-weight: 500;
|
||||
line-height: 1em;
|
||||
color: var(--blue-500);
|
||||
|
@ -111,8 +115,9 @@
|
|||
}
|
||||
|
||||
.description {
|
||||
color: var(--black-400);
|
||||
@include font-size(1.2rem);
|
||||
|
||||
color: var(--black-400);
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { Show, createEffect, createSignal } from 'solid-js'
|
||||
import { Show, createEffect, createSignal, on } from 'solid-js'
|
||||
|
||||
import { useFollowing } from '../../../context/following'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
@ -8,12 +8,12 @@ import { useSession } from '../../../context/session'
|
|||
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
|
||||
import { capitalize } from '../../../utils/capitalize'
|
||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||
import { BadgeSubscribeButton } from '../../_shared/BadgeSubscribeButton'
|
||||
import { FollowingButton } from '../../_shared/FollowingButton'
|
||||
import styles from './TopicBadge.module.scss'
|
||||
|
||||
type Props = {
|
||||
topic: Topic
|
||||
minimizeSubscribeButton?: boolean
|
||||
minimize?: boolean
|
||||
showStat?: boolean
|
||||
subscriptionsMode?: boolean
|
||||
}
|
||||
|
@ -23,18 +23,21 @@ export const TopicBadge = (props: Props) => {
|
|||
const { mediaMatches } = useMediaQuery()
|
||||
const [isMobileView, setIsMobileView] = createSignal(false)
|
||||
const { requireAuthentication } = useSession()
|
||||
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
|
||||
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
|
||||
const [isFollowed, setIsFollowed] = createSignal<boolean>()
|
||||
const { follow, unfollow, follows, following } = useFollowing()
|
||||
|
||||
createEffect(() => {
|
||||
if (!(subscriptions && props.topic)) return
|
||||
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
|
||||
setIsSubscribed(subscribed)
|
||||
})
|
||||
createEffect(
|
||||
on([() => follows, () => props.topic], ([flws, tpc]) => {
|
||||
if (flws && tpc) {
|
||||
const followed = follows?.topics?.some((topics) => topics.id === props.topic?.id)
|
||||
setIsFollowed(followed)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const handleFollowClick = () => {
|
||||
requireAuthentication(() => {
|
||||
isSubscribed()
|
||||
isFollowed()
|
||||
? follow(FollowingEntity.Topic, props.topic.slug)
|
||||
: unfollow(FollowingEntity.Topic, props.topic.slug)
|
||||
}, 'subscribe')
|
||||
|
@ -73,7 +76,7 @@ export const TopicBadge = (props: Props) => {
|
|||
when={props.topic.body}
|
||||
fallback={
|
||||
<div class={styles.description}>
|
||||
{t('PublicationsWithCount', { count: props.topic?.stat?.shouts ?? 0 })}
|
||||
{t('some posts', { count: props.topic?.stat?.shouts ?? 0 })}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
@ -82,28 +85,24 @@ export const TopicBadge = (props: Props) => {
|
|||
</a>
|
||||
</div>
|
||||
<div class={styles.actions}>
|
||||
<BadgeSubscribeButton
|
||||
isSubscribed={isSubscribed()}
|
||||
<FollowingButton
|
||||
isFollowed={isFollowed()}
|
||||
action={handleFollowClick}
|
||||
actionMessageType={
|
||||
subscribeInAction()?.slug === props.topic.slug ? subscribeInAction().type : undefined
|
||||
}
|
||||
actionMessageType={following()?.slug === props.topic.slug ? following().type : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={!props.subscriptionsMode}>
|
||||
<div class={styles.stats}>
|
||||
<span class={styles.statsItem}>{t('shoutsWithCount', { count: props.topic?.stat?.shouts })}</span>
|
||||
<span class={styles.statsItem}>{t('some shouts', { count: props.topic?.stat?.shouts })}</span>
|
||||
<span class={styles.statsItem}>{t('some authors', { count: props.topic?.stat?.authors })}</span>
|
||||
<span class={styles.statsItem}>
|
||||
{t('authorsWithCount', { count: props.topic?.stat?.authors })}
|
||||
</span>
|
||||
<span class={styles.statsItem}>
|
||||
{t('FollowersWithCount', { count: props.topic?.stat?.followers })}
|
||||
{t('some followers', { count: props.topic?.stat?.followers })}
|
||||
</span>
|
||||
<Show when={props.topic?.stat?.comments}>
|
||||
<span class={styles.statsItem}>
|
||||
{t('CommentsWithCount', { count: props.topic?.stat?.comments ?? 0 })}
|
||||
{t('some comments', { count: props.topic?.stat?.comments ?? 0 })}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -1,32 +1,30 @@
|
|||
import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/core.gen'
|
||||
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import { clsx } from 'clsx'
|
||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||
|
||||
import { useFollowing } from '../../../context/following'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Meta, Title } from '../../../context/meta'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { apiClient } from '../../../graphql/client/core'
|
||||
import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/core.gen'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { MODALS, hideModal } from '../../../stores/ui'
|
||||
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
||||
import { loadAuthor } from '../../../stores/zine/authors'
|
||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||
import { getDescription } from '../../../utils/meta'
|
||||
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
|
||||
import { byCreated } from '../../../utils/sortby'
|
||||
import { splitToPages } from '../../../utils/splitToPages'
|
||||
import stylesArticle from '../../Article/Article.module.scss'
|
||||
import { Comment } from '../../Article/Comment'
|
||||
import { AuthorCard } from '../../Author/AuthorCard'
|
||||
import { AuthorShoutsRating } from '../../Author/AuthorShoutsRating'
|
||||
import { Placeholder } from '../../Feed/Placeholder'
|
||||
import { Row1 } from '../../Feed/Row1'
|
||||
import { Row2 } from '../../Feed/Row2'
|
||||
import { Row3 } from '../../Feed/Row3'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
|
||||
import { MODALS, hideModal } from '../../../stores/ui'
|
||||
import { byCreated } from '../../../utils/sortby'
|
||||
import stylesArticle from '../../Article/Article.module.scss'
|
||||
import styles from './Author.module.scss'
|
||||
|
||||
type Props = {
|
||||
|
@ -34,71 +32,26 @@ type Props = {
|
|||
shouts?: Shout[]
|
||||
author?: Author
|
||||
}
|
||||
|
||||
export const PRERENDERED_ARTICLES_COUNT = 12
|
||||
const LOAD_MORE_PAGE_SIZE = 9
|
||||
|
||||
export const AuthorView = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const { followers: myFollowers } = useFollowing()
|
||||
const { session } = useSession()
|
||||
const { followers: myFollowers, follows: myFollows } = useFollowing()
|
||||
const { author: me } = useSession()
|
||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||
const { page: getPage, searchParams } = useRouter()
|
||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
||||
const [author, setAuthor] = createSignal<Author>()
|
||||
const [author, setAuthor] = createSignal<Author>(props.author)
|
||||
const [followers, setFollowers] = createSignal([])
|
||||
const [following, setFollowing] = createSignal<Array<Author | Topic>>([]) // flat AuthorFollowsResult
|
||||
const [following, changeFollowing] = createSignal<Array<Author | Topic>>([]) // flat AuthorFollowsResult
|
||||
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
|
||||
const [commented, setCommented] = createSignal<Reaction[]>()
|
||||
const modal = MODALS[searchParams().m]
|
||||
|
||||
const [sessionChecked, setSessionChecked] = createSignal(false)
|
||||
createEffect(() => {
|
||||
if (
|
||||
!sessionChecked() &&
|
||||
props.authorSlug &&
|
||||
session()?.user?.app_data?.profile?.slug === props.authorSlug
|
||||
) {
|
||||
setSessionChecked(true)
|
||||
const appdata = session()?.user.app_data
|
||||
if (appdata) {
|
||||
console.info('preloaded my own profile')
|
||||
const { authors, profile, topics } = appdata
|
||||
setFollowers(myFollowers)
|
||||
setAuthor(profile)
|
||||
setFollowing([...(authors || []), ...(topics || [])])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const bioContainerRef: { current: HTMLDivElement } = { current: null }
|
||||
const bioWrapperRef: { current: HTMLDivElement } = { current: null }
|
||||
|
||||
const fetchData = async (slug: string) => {
|
||||
if (author()?.stat.followers || author()?.stat.followers === (followers() || [])?.length) return
|
||||
try {
|
||||
const [subscriptionsResult, followersResult, authorResult] = await Promise.all([
|
||||
apiClient.getAuthorFollows({ slug }),
|
||||
apiClient.getAuthorFollowers({ slug }),
|
||||
loadAuthor({ slug }),
|
||||
])
|
||||
const { authors, topics } = subscriptionsResult
|
||||
setAuthor(authorResult)
|
||||
setFollowing([...(authors || []), ...(topics || [])])
|
||||
setFollowers(followersResult || [])
|
||||
|
||||
console.info('[components.Author] data loaded')
|
||||
} catch (error) {
|
||||
console.error('[components.Author] fetch error', error)
|
||||
}
|
||||
}
|
||||
|
||||
const checkBioHeight = () => {
|
||||
if (bioContainerRef.current) {
|
||||
setShowExpandBioControl(bioContainerRef.current.offsetHeight > bioWrapperRef.current.offsetHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// пагинация загрузки ленты постов
|
||||
const loadMore = async () => {
|
||||
saveScrollPosition()
|
||||
const { hasMore } = await loadShouts({
|
||||
|
@ -110,36 +63,72 @@ export const AuthorView = (props: Props) => {
|
|||
restoreScrollPosition()
|
||||
}
|
||||
|
||||
// загружает профиль и подписки
|
||||
const [isFetching, setIsFetching] = createSignal(false)
|
||||
const fetchData = async (slug) => {
|
||||
setIsFetching(true)
|
||||
const authorResult = await loadAuthor({ slug })
|
||||
setAuthor(authorResult)
|
||||
console.info(`[Author] profile for @${slug} fetched`)
|
||||
|
||||
const followsResult = await apiClient.getAuthorFollows({ slug })
|
||||
const { authors, topics } = followsResult
|
||||
changeFollowing([...(authors || []), ...(topics || [])])
|
||||
console.info(`[Author] follows for @${slug} fetched`)
|
||||
|
||||
const followersResult = await apiClient.getAuthorFollowers({ slug })
|
||||
setFollowers(followersResult || [])
|
||||
console.info(`[Author] followers for @${slug} fetched`)
|
||||
setIsFetching(false)
|
||||
}
|
||||
|
||||
// проверяет не собственный ли это профиль, иначе - загружает
|
||||
createEffect(
|
||||
on([() => me(), () => props.authorSlug], ([myProfile, slug]) => {
|
||||
const my = slug && myProfile?.slug === slug
|
||||
if (my) {
|
||||
console.debug('[Author] my profile precached')
|
||||
myProfile && setAuthor(myProfile)
|
||||
setFollowers(myFollowers() || [])
|
||||
changeFollowing([...(myFollows?.authors || []), ...(myFollows?.topics || [])])
|
||||
} else if (slug && !isFetching()) {
|
||||
fetchData(slug)
|
||||
}
|
||||
}),
|
||||
{ defer: true },
|
||||
)
|
||||
|
||||
// догружает ленту и комментарии
|
||||
createEffect(
|
||||
on(author, async (profile) => {
|
||||
if (!commented() && profile) {
|
||||
await loadMore()
|
||||
|
||||
const ccc = await apiClient.getReactionsBy({
|
||||
by: { comment: true, created_by: profile.id },
|
||||
})
|
||||
setCommented(ccc)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const bioContainerRef: { current: HTMLDivElement } = { current: null }
|
||||
const bioWrapperRef: { current: HTMLDivElement } = { current: null }
|
||||
const checkBioHeight = () => {
|
||||
if (bioContainerRef.current) {
|
||||
setShowExpandBioControl(bioContainerRef.current.offsetHeight > bioWrapperRef.current.offsetHeight)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!modal) hideModal()
|
||||
fetchData(props.authorSlug)
|
||||
checkBioHeight()
|
||||
loadMore()
|
||||
})
|
||||
|
||||
const pages = createMemo<Shout[][]>(() =>
|
||||
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE),
|
||||
)
|
||||
|
||||
const fetchComments = async (commenter: Author) => {
|
||||
const data = await apiClient.getReactionsBy({
|
||||
by: { comment: true, created_by: commenter.id },
|
||||
})
|
||||
setCommented(data)
|
||||
}
|
||||
|
||||
const authorSlug = createMemo(() => author()?.slug)
|
||||
createEffect(
|
||||
on(
|
||||
() => authorSlug(),
|
||||
() => {
|
||||
fetchData(authorSlug())
|
||||
fetchComments(author())
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const ogImage = createMemo(() =>
|
||||
author()?.pic
|
||||
? getImageUrl(author()?.pic, { width: 1200 })
|
||||
|
@ -168,16 +157,12 @@ export const AuthorView = (props: Props) => {
|
|||
<Show when={author()} fallback={<Loading />}>
|
||||
<>
|
||||
<div class={styles.authorHeader}>
|
||||
<AuthorCard author={author()} followers={followers() || []} following={following() || []} />
|
||||
<AuthorCard author={author()} followers={followers() || []} flatFollows={following() || []} />
|
||||
</div>
|
||||
<div class={clsx(styles.groupControls, 'row')}>
|
||||
<div class="col-md-16">
|
||||
<ul class="view-switcher">
|
||||
<li
|
||||
classList={{
|
||||
'view-switcher__item--selected': getPage().route === 'author',
|
||||
}}
|
||||
>
|
||||
<li classList={{ 'view-switcher__item--selected': getPage().route === 'author' }}>
|
||||
<a
|
||||
href={getPagePath(router, 'author', {
|
||||
slug: props.authorSlug,
|
||||
|
@ -189,11 +174,7 @@ export const AuthorView = (props: Props) => {
|
|||
<span class="view-switcher__counter">{author().stat.shouts}</span>
|
||||
</Show>
|
||||
</li>
|
||||
<li
|
||||
classList={{
|
||||
'view-switcher__item--selected': getPage().route === 'authorComments',
|
||||
}}
|
||||
>
|
||||
<li classList={{ 'view-switcher__item--selected': getPage().route === 'authorComments' }}>
|
||||
<a
|
||||
href={getPagePath(router, 'authorComments', {
|
||||
slug: props.authorSlug,
|
||||
|
@ -205,11 +186,7 @@ export const AuthorView = (props: Props) => {
|
|||
<span class="view-switcher__counter">{author().stat.comments}</span>
|
||||
</Show>
|
||||
</li>
|
||||
<li
|
||||
classList={{
|
||||
'view-switcher__item--selected': getPage().route === 'authorAbout',
|
||||
}}
|
||||
>
|
||||
<li classList={{ 'view-switcher__item--selected': getPage().route === 'authorAbout' }}>
|
||||
<a
|
||||
onClick={() => checkBioHeight()}
|
||||
href={getPagePath(router, 'authorAbout', {
|
||||
|
@ -260,6 +237,12 @@ export const AuthorView = (props: Props) => {
|
|||
</div>
|
||||
</Match>
|
||||
<Match when={getPage().route === 'authorComments'}>
|
||||
<Show when={me()?.slug === props.authorSlug && !me().stat?.comments}>
|
||||
<div class="wide-container">
|
||||
<Placeholder type={getPage().route} mode="profile" />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<div class="col-md-20 col-lg-18">
|
||||
|
@ -280,46 +263,47 @@ export const AuthorView = (props: Props) => {
|
|||
</div>
|
||||
</Match>
|
||||
<Match when={getPage().route === 'author'}>
|
||||
<Show when={sortedArticles().length === 1}>
|
||||
<Show when={me()?.slug === props.authorSlug && !me().stat?.shouts}>
|
||||
<div class="wide-container">
|
||||
<Placeholder type={getPage().route} mode="profile" />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={sortedArticles().length > 0}>
|
||||
<Row1 article={sortedArticles()[0]} noauthor={true} nodate={true} />
|
||||
</Show>
|
||||
|
||||
<Show when={sortedArticles().length === 2}>
|
||||
<Row2 articles={sortedArticles()} isEqual={true} noauthor={true} nodate={true} />
|
||||
</Show>
|
||||
<Show when={sortedArticles().length > 1}>
|
||||
<Switch>
|
||||
<Match when={sortedArticles().length === 2}>
|
||||
<Row2 articles={sortedArticles()} isEqual={true} noauthor={true} nodate={true} />
|
||||
</Match>
|
||||
<Match when={sortedArticles().length === 3}>
|
||||
<Row3 articles={sortedArticles()} noauthor={true} nodate={true} />
|
||||
</Match>
|
||||
<Match when={sortedArticles().length > 3}>
|
||||
<For each={pages()}>
|
||||
{(page) => (
|
||||
<>
|
||||
<Row1 article={page[0]} noauthor={true} nodate={true} />
|
||||
<Row2 articles={page.slice(1, 3)} isEqual={true} noauthor={true} />
|
||||
<Row1 article={page[3]} noauthor={true} nodate={true} />
|
||||
<Row2 articles={page.slice(4, 6)} isEqual={true} noauthor={true} />
|
||||
<Row1 article={page[6]} noauthor={true} nodate={true} />
|
||||
<Row2 articles={page.slice(7, 9)} isEqual={true} noauthor={true} />
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
|
||||
<Show when={sortedArticles().length === 3}>
|
||||
<Row3 articles={sortedArticles()} noauthor={true} nodate={true} />
|
||||
</Show>
|
||||
|
||||
<Show when={sortedArticles().length > 3}>
|
||||
<Row1 article={sortedArticles()[0]} noauthor={true} nodate={true} />
|
||||
<Row2 articles={sortedArticles().slice(1, 3)} isEqual={true} noauthor={true} />
|
||||
<Row1 article={sortedArticles()[3]} noauthor={true} nodate={true} />
|
||||
<Row2 articles={sortedArticles().slice(4, 6)} isEqual={true} noauthor={true} />
|
||||
<Row1 article={sortedArticles()[6]} noauthor={true} nodate={true} />
|
||||
<Row2 articles={sortedArticles().slice(7, 9)} isEqual={true} noauthor={true} />
|
||||
|
||||
<For each={pages()}>
|
||||
{(page) => (
|
||||
<>
|
||||
<Row1 article={page[0]} noauthor={true} nodate={true} />
|
||||
<Row2 articles={page.slice(1, 3)} isEqual={true} noauthor={true} />
|
||||
<Row1 article={page[3]} noauthor={true} nodate={true} />
|
||||
<Row2 articles={page.slice(4, 6)} isEqual={true} noauthor={true} />
|
||||
<Row1 article={page[6]} noauthor={true} nodate={true} />
|
||||
<Row2 articles={page.slice(7, 9)} isEqual={true} noauthor={true} />
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
<Show when={isLoadMoreButtonVisible()}>
|
||||
<p class="load-more-container">
|
||||
<button class="button" onClick={loadMore}>
|
||||
{t('Load more')}
|
||||
</button>
|
||||
</p>
|
||||
<Show when={isLoadMoreButtonVisible()}>
|
||||
<p class="load-more-container">
|
||||
<button class="button" onClick={loadMore}>
|
||||
{t('Load more')}
|
||||
</button>
|
||||
</p>
|
||||
</Show>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
|
|
@ -15,20 +15,24 @@ import styles from './DraftsView.module.scss'
|
|||
export const DraftsView = () => {
|
||||
const { author, loadSession } = useSession()
|
||||
const [drafts, setDrafts] = createSignal<Shout[]>([])
|
||||
const [loading, setLoading] = createSignal(false)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => author(),
|
||||
async (a) => {
|
||||
if (a) {
|
||||
setLoading(true)
|
||||
const { shouts: loadedDrafts, error } = await apiClient.getDrafts()
|
||||
if (error) {
|
||||
console.warn(error)
|
||||
await loadSession()
|
||||
}
|
||||
setDrafts(loadedDrafts || [])
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -50,7 +54,7 @@ export const DraftsView = () => {
|
|||
|
||||
return (
|
||||
<div class={clsx(styles.DraftsView)}>
|
||||
<Show when={author()?.id} fallback={<Loading />}>
|
||||
<Show when={!loading() && author()?.id} fallback={<Loading />}>
|
||||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
|
||||
|
|
|
@ -27,6 +27,7 @@ import { EditorSwiper } from '../../_shared/SolidSwiper'
|
|||
|
||||
import { PublishSettings } from '../PublishSettings'
|
||||
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
import styles from './EditView.module.scss'
|
||||
|
||||
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
|
||||
|
@ -145,7 +146,7 @@ export const EditView = (props: Props) => {
|
|||
|
||||
const handleMediaDelete = (index) => {
|
||||
const copy = [...mediaItems()]
|
||||
copy.splice(index, 1)
|
||||
if (copy?.length > 0) copy.splice(index, 1)
|
||||
handleInputChange('media', JSON.stringify(copy))
|
||||
}
|
||||
|
||||
|
@ -403,7 +404,7 @@ export const EditView = (props: Props) => {
|
|||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={page().route === 'edit'}>
|
||||
<Show when={page().route === 'edit' && form?.shoutId} fallback={<Loading />}>
|
||||
<Editor
|
||||
shoutId={form.shoutId}
|
||||
initialContent={form.body}
|
||||
|
|
|
@ -175,6 +175,7 @@
|
|||
-webkit-line-clamp: 1;
|
||||
|
||||
a {
|
||||
border: none;
|
||||
color: rgb(0 0 0 / 65%);
|
||||
|
||||
&:hover {
|
||||
|
|
|
@ -20,6 +20,7 @@ import { getShareUrl } from '../../Article/SharePopup'
|
|||
import { AuthorBadge } from '../../Author/AuthorBadge'
|
||||
import { AuthorLink } from '../../Author/AuthorLink'
|
||||
import { ArticleCard } from '../../Feed/ArticleCard'
|
||||
import { Placeholder } from '../../Feed/Placeholder'
|
||||
import { Sidebar } from '../../Feed/Sidebar'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
import { DropDown } from '../../_shared/DropDown'
|
||||
|
@ -100,7 +101,7 @@ export const FeedView = (props: Props) => {
|
|||
const { page, searchParams, changeSearchParams } = useRouter<FeedSearchParams>()
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false)
|
||||
const { session } = useSession()
|
||||
const { author, session } = useSession()
|
||||
const { loadReactionsBy } = useReactions()
|
||||
const { sortedArticles } = useArticlesStore()
|
||||
const { topTopics } = useTopics()
|
||||
|
@ -143,16 +144,20 @@ export const FeedView = (props: Props) => {
|
|||
Promise.all([loadTopComments()]).finally(() => setIsRightColumnLoaded(true))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (session()?.access_token && !unratedArticles()) {
|
||||
loadUnratedArticles()
|
||||
}
|
||||
})
|
||||
createEffect(
|
||||
on(
|
||||
[() => session(), unratedArticles],
|
||||
([s, seen]) => {
|
||||
if (s?.access_token && !(seen?.length > 0)) loadUnratedArticles()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => page().route + searchParams().by + searchParams().period + searchParams().visibility,
|
||||
() => {
|
||||
[page, searchParams],
|
||||
(_, _p) => {
|
||||
resetSortedArticles()
|
||||
loadMore()
|
||||
},
|
||||
|
@ -234,107 +239,113 @@ export const FeedView = (props: Props) => {
|
|||
</div>
|
||||
|
||||
<div class="col-md-12 offset-xl-1">
|
||||
<div class={styles.filtersContainer}>
|
||||
<ul class={clsx('view-switcher', styles.feedFilter)}>
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected':
|
||||
searchParams().by === 'publish_date' || !searchParams().by,
|
||||
})}
|
||||
>
|
||||
<a href={getPagePath(router, page().route)}>{t('Recent')}</a>
|
||||
</li>
|
||||
{/*<li>*/}
|
||||
{/* <a href="/feed/?by=views">{t('Most read')}</a>*/}
|
||||
{/*</li>*/}
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': searchParams().by === 'likes',
|
||||
})}
|
||||
>
|
||||
<span class="link" onClick={() => changeSearchParams({ by: 'likes' })}>
|
||||
{t('Top rated')}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': searchParams().by === 'last_comment',
|
||||
})}
|
||||
>
|
||||
<span class="link" onClick={() => changeSearchParams({ by: 'last_comment' })}>
|
||||
{t('Most commented')}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class={styles.dropdowns}>
|
||||
<Show when={searchParams().by && searchParams().by !== 'publish_date'}>
|
||||
<Show when={!author() && page().route !== 'feed'}>
|
||||
<Placeholder type={page().route} mode="feed" />
|
||||
</Show>
|
||||
|
||||
<Show when={(author() || page().route === 'feed') && sortedArticles().length}>
|
||||
<div class={styles.filtersContainer}>
|
||||
<ul class={clsx('view-switcher', styles.feedFilter)}>
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected':
|
||||
searchParams().by === 'publish_date' || !searchParams().by,
|
||||
})}
|
||||
>
|
||||
<a href={getPagePath(router, page().route)}>{t('Recent')}</a>
|
||||
</li>
|
||||
{/*<li>*/}
|
||||
{/* <a href="/feed/?by=views">{t('Most read')}</a>*/}
|
||||
{/*</li>*/}
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': searchParams().by === 'likes',
|
||||
})}
|
||||
>
|
||||
<span class="link" onClick={() => changeSearchParams({ by: 'likes' })}>
|
||||
{t('Top rated')}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': searchParams().by === 'last_comment',
|
||||
})}
|
||||
>
|
||||
<span class="link" onClick={() => changeSearchParams({ by: 'last_comment' })}>
|
||||
{t('Most commented')}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class={styles.dropdowns}>
|
||||
<Show when={searchParams().by && searchParams().by !== 'publish_date'}>
|
||||
<DropDown
|
||||
popupProps={{ horizontalAnchor: 'right' }}
|
||||
options={periods}
|
||||
currentOption={currentPeriod()}
|
||||
triggerCssClass={styles.periodSwitcher}
|
||||
onChange={(period: PeriodItem) => changeSearchParams({ period: period.value })}
|
||||
/>
|
||||
</Show>
|
||||
<DropDown
|
||||
popupProps={{ horizontalAnchor: 'right' }}
|
||||
options={periods}
|
||||
currentOption={currentPeriod()}
|
||||
options={visibilities}
|
||||
currentOption={currentVisibility()}
|
||||
triggerCssClass={styles.periodSwitcher}
|
||||
onChange={(period: PeriodItem) => changeSearchParams({ period: period.value })}
|
||||
onChange={(visibility: VisibilityItem) =>
|
||||
changeSearchParams({ visibility: visibility.value })
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
<DropDown
|
||||
popupProps={{ horizontalAnchor: 'right' }}
|
||||
options={visibilities}
|
||||
currentOption={currentVisibility()}
|
||||
triggerCssClass={styles.periodSwitcher}
|
||||
onChange={(visibility: VisibilityItem) =>
|
||||
changeSearchParams({ visibility: visibility.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={!isLoading()} fallback={<Loading />}>
|
||||
<Show when={sortedArticles().length > 0}>
|
||||
<For each={sortedArticles().slice(0, 4)}>
|
||||
{(article) => (
|
||||
<ArticleCard
|
||||
onShare={(shared) => handleShare(shared)}
|
||||
onInvite={() => showModal('inviteMembers')}
|
||||
article={article}
|
||||
settings={{ isFeedMode: true }}
|
||||
desktopCoverSize="M"
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<Show when={!isLoading()} fallback={<Loading />}>
|
||||
<Show when={sortedArticles().length > 0}>
|
||||
<For each={sortedArticles().slice(0, 4)}>
|
||||
{(article) => (
|
||||
<ArticleCard
|
||||
onShare={(shared) => handleShare(shared)}
|
||||
onInvite={() => showModal('inviteMembers')}
|
||||
article={article}
|
||||
settings={{ isFeedMode: true }}
|
||||
desktopCoverSize="M"
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<div class={styles.asideSection}>
|
||||
<div class={stylesBeside.besideColumnTitle}>
|
||||
<h4>{t('Popular authors')}</h4>
|
||||
<a href="/authors">
|
||||
{t('All authors')}
|
||||
<Icon name="arrow-right" class={stylesBeside.icon} />
|
||||
</a>
|
||||
<div class={styles.asideSection}>
|
||||
<div class={stylesBeside.besideColumnTitle}>
|
||||
<h4>{t('Popular authors')}</h4>
|
||||
<a href="/authors">
|
||||
{t('All authors')}
|
||||
<Icon name="arrow-right" class={stylesBeside.icon} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class={stylesBeside.besideColumn}>
|
||||
<For each={topAuthors().slice(0, 5)}>
|
||||
{(author) => (
|
||||
<li>
|
||||
<AuthorBadge author={author} />
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class={stylesBeside.besideColumn}>
|
||||
<For each={topAuthors().slice(0, 5)}>
|
||||
{(author) => (
|
||||
<li>
|
||||
<AuthorBadge author={author} />
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
<For each={sortedArticles().slice(4)}>
|
||||
{(article) => (
|
||||
<ArticleCard article={article} settings={{ isFeedMode: true }} desktopCoverSize="M" />
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
<For each={sortedArticles().slice(4)}>
|
||||
{(article) => (
|
||||
<ArticleCard article={article} settings={{ isFeedMode: true }} desktopCoverSize="M" />
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
<Show when={isLoadMoreButtonVisible()}>
|
||||
<p class="load-more-container">
|
||||
<button class="button" onClick={loadMore}>
|
||||
{t('Load more')}
|
||||
</button>
|
||||
</p>
|
||||
<Show when={isLoadMoreButtonVisible()}>
|
||||
<p class="load-more-container">
|
||||
<button class="button" onClick={loadMore}>
|
||||
{t('Load more')}
|
||||
</button>
|
||||
</p>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -136,21 +136,18 @@ export const InboxView = (props: Props) => {
|
|||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => messages(),
|
||||
() => {
|
||||
if (!messagesContainerRef.current) {
|
||||
return
|
||||
}
|
||||
if (messagesContainerRef.current.scrollTop >= messagesContainerRef.current.scrollHeight) {
|
||||
return
|
||||
}
|
||||
messagesContainerRef.current.scroll({
|
||||
top: messagesContainerRef.current.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
},
|
||||
),
|
||||
on(messages, () => {
|
||||
if (!messagesContainerRef.current) {
|
||||
return
|
||||
}
|
||||
if (messagesContainerRef.current.scrollTop >= messagesContainerRef.current.scrollHeight) {
|
||||
return
|
||||
}
|
||||
messagesContainerRef.current.scroll({
|
||||
top: messagesContainerRef.current.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}),
|
||||
{ defer: true },
|
||||
)
|
||||
const handleScrollMessageContainer = () => {
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { For, Show, createEffect, createSignal } from 'solid-js'
|
||||
import { For, Show, createEffect, createSignal, on } from 'solid-js'
|
||||
|
||||
import { useFollowing } from '../../../context/following'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Author, Topic } from '../../../graphql/schema/core.gen'
|
||||
import { SubscriptionFilter } from '../../../pages/types'
|
||||
import { FollowsFilter } from '../../../pages/types'
|
||||
import { dummyFilter } from '../../../utils/dummyFilter'
|
||||
// TODO: refactor styles
|
||||
import { isAuthor } from '../../../utils/isAuthor'
|
||||
import { AuthorBadge } from '../../Author/AuthorBadge'
|
||||
import { ProfileSettingsNavigation } from '../../Nav/ProfileSettingsNavigation'
|
||||
|
@ -19,30 +18,30 @@ import stylesSettings from '../../../styles/FeedSettings.module.scss'
|
|||
|
||||
export const ProfileSubscriptions = () => {
|
||||
const { t, lang } = useLocalize()
|
||||
const { subscriptions } = useFollowing()
|
||||
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
|
||||
const { follows } = useFollowing()
|
||||
const [flatFollows, setFlatFollows] = createSignal<Array<Author | Topic>>([])
|
||||
const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([])
|
||||
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
||||
const [followsFilter, setFollowsFilter] = createSignal<FollowsFilter>('all')
|
||||
const [searchQuery, setSearchQuery] = createSignal('')
|
||||
|
||||
createEffect(() => {
|
||||
const { authors, topics } = subscriptions
|
||||
if (authors || topics) {
|
||||
const fdata = [...(authors || []), ...(topics || [])]
|
||||
setFollowing(fdata)
|
||||
if (subscriptionFilter() === 'authors') {
|
||||
setFiltered(fdata.filter((s) => 'name' in s))
|
||||
} else if (subscriptionFilter() === 'topics') {
|
||||
setFiltered(fdata.filter((s) => 'title' in s))
|
||||
createEffect(() => setFlatFollows([...(follows?.authors || []), ...(follows?.topics || [])]))
|
||||
|
||||
createEffect(
|
||||
on([flatFollows, followsFilter], ([flat, mode]) => {
|
||||
if (mode === 'authors') {
|
||||
setFiltered(flat.filter((s) => 'name' in s))
|
||||
} else if (mode === 'topics') {
|
||||
setFiltered(flat.filter((s) => 'title' in s))
|
||||
} else {
|
||||
setFiltered(fdata)
|
||||
setFiltered(flat)
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
{ defer: true },
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (searchQuery()) {
|
||||
setFiltered(dummyFilter(following(), searchQuery(), lang()))
|
||||
setFiltered(dummyFilter(flatFollows(), searchQuery(), lang()))
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -60,32 +59,32 @@ export const ProfileSubscriptions = () => {
|
|||
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||
<h1>{t('My subscriptions')}</h1>
|
||||
<p class="description">{t('Here you can manage all your Discours subscriptions')}</p>
|
||||
<Show when={following()} fallback={<Loading />}>
|
||||
<Show when={flatFollows()} fallback={<Loading />}>
|
||||
<ul class="view-switcher">
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': subscriptionFilter() === 'all',
|
||||
'view-switcher__item--selected': followsFilter() === 'all',
|
||||
})}
|
||||
>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
||||
<button type="button" onClick={() => setFollowsFilter('all')}>
|
||||
{t('All')}
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': subscriptionFilter() === 'authors',
|
||||
'view-switcher__item--selected': followsFilter() === 'authors',
|
||||
})}
|
||||
>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('authors')}>
|
||||
<button type="button" onClick={() => setFollowsFilter('authors')}>
|
||||
{t('Authors')}
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({
|
||||
'view-switcher__item--selected': subscriptionFilter() === 'topics',
|
||||
'view-switcher__item--selected': followsFilter() === 'topics',
|
||||
})}
|
||||
>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
||||
<button type="button" onClick={() => setFollowsFilter('topics')}>
|
||||
{t('Topics')}
|
||||
</button>
|
||||
</li>
|
||||
|
@ -104,9 +103,9 @@ export const ProfileSubscriptions = () => {
|
|||
{(followingItem) => (
|
||||
<div>
|
||||
{isAuthor(followingItem) ? (
|
||||
<AuthorBadge minimizeSubscribeButton={true} author={followingItem} />
|
||||
<AuthorBadge minimize={true} author={followingItem} />
|
||||
) : (
|
||||
<TopicBadge minimizeSubscribeButton={true} topic={followingItem} />
|
||||
<TopicBadge minimize={true} topic={followingItem} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -40,7 +40,18 @@ const EMPTY_TOPIC: Topic = {
|
|||
id: -1,
|
||||
slug: '',
|
||||
}
|
||||
const emptyConfig = {
|
||||
|
||||
interface FormConfig {
|
||||
coverImageUrl?: string
|
||||
mainTopic?: Topic
|
||||
slug?: string
|
||||
title?: string
|
||||
subtitle?: string
|
||||
description?: string
|
||||
selectedTopics?: Topic[]
|
||||
}
|
||||
|
||||
const emptyConfig: FormConfig = {
|
||||
coverImageUrl: '',
|
||||
mainTopic: EMPTY_TOPIC,
|
||||
slug: '',
|
||||
|
@ -78,7 +89,7 @@ export const PublishSettings = (props: Props) => {
|
|||
}
|
||||
})
|
||||
|
||||
const [settingsForm, setSettingsForm] = createStore(emptyConfig)
|
||||
const [settingsForm, setSettingsForm] = createStore<FormConfig>(emptyConfig)
|
||||
|
||||
onMount(() => {
|
||||
setSettingsForm(initialData())
|
||||
|
@ -96,12 +107,12 @@ export const PublishSettings = (props: Props) => {
|
|||
setSettingsForm('coverImageUrl', '')
|
||||
}
|
||||
|
||||
const handleTopicSelectChange = (newSelectedTopics) => {
|
||||
const handleTopicSelectChange = (newSelectedTopics: Topic[]) => {
|
||||
if (
|
||||
props.form.selectedTopics.length === 0 ||
|
||||
newSelectedTopics.every((topic) => topic.id !== props.form.mainTopic?.id)
|
||||
newSelectedTopics.every((topic: Topic) => topic.id !== props.form.mainTopic?.id)
|
||||
) {
|
||||
setSettingsForm((prev) => {
|
||||
setSettingsForm((prev: Topic) => {
|
||||
return {
|
||||
...prev,
|
||||
mainTopic: newSelectedTopics[0],
|
||||
|
@ -193,7 +204,8 @@ export const PublishSettings = (props: Props) => {
|
|||
fieldName={t('Header')}
|
||||
placeholder={t('Come up with a title for your story')}
|
||||
initialValue={settingsForm.title}
|
||||
value={(value) => setSettingsForm('title', value)}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
value={(value: any) => setSettingsForm('title', value)}
|
||||
allowEnterKey={false}
|
||||
maxLength={100}
|
||||
/>
|
||||
|
@ -203,7 +215,8 @@ export const PublishSettings = (props: Props) => {
|
|||
fieldName={t('Subheader')}
|
||||
placeholder={t('Come up with a subtitle for your story')}
|
||||
initialValue={settingsForm.subtitle || ''}
|
||||
value={(value) => setSettingsForm('subtitle', value)}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
value={(value: any) => setSettingsForm('subtitle', value)}
|
||||
allowEnterKey={false}
|
||||
maxLength={100}
|
||||
/>
|
||||
|
@ -214,7 +227,8 @@ export const PublishSettings = (props: Props) => {
|
|||
placeholder={t('Write a short introduction')}
|
||||
label={t('Description')}
|
||||
initialContent={composeDescription()}
|
||||
onChange={(value) => setForm('description', value)}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
onChange={(value: any) => setForm('description', value)}
|
||||
maxLength={DESCRIPTION_MAX_LENGTH}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { LoadShoutsOptions, Shout, Topic } from '../../graphql/schema/core.gen'
|
||||
import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||
|
@ -33,6 +33,7 @@ interface Props {
|
|||
topic: Topic
|
||||
shouts: Shout[]
|
||||
topicSlug: string
|
||||
followers?: Author[]
|
||||
}
|
||||
|
||||
export const PRERENDERED_ARTICLES_COUNT = 28
|
||||
|
@ -49,13 +50,30 @@ export const TopicView = (props: Props) => {
|
|||
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
||||
|
||||
const [topic, setTopic] = createSignal<Topic>()
|
||||
createEffect(
|
||||
on([() => props.topicSlug, topic, topicEntities], async ([slug, t, ttt]) => {
|
||||
if (slug && !t && ttt) {
|
||||
const current = ttt[slug]
|
||||
console.debug(current)
|
||||
setTopic(current)
|
||||
await loadTopicFollowers()
|
||||
await loadTopicAuthors()
|
||||
await loadRandom()
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const topics = topicEntities()
|
||||
if (props.topicSlug && !topic() && topics) {
|
||||
setTopic(topics[props.topicSlug])
|
||||
}
|
||||
})
|
||||
const [followers, setFollowers] = createSignal<Author[]>(props.followers || [])
|
||||
const loadTopicFollowers = async () => {
|
||||
const flwrs = await apiClient.getTopicFollowers({ slug: props.topicSlug })
|
||||
setFollowers(flwrs)
|
||||
}
|
||||
const [topicAuthors, setTopicAuthors] = createSignal<Author[]>([])
|
||||
const loadTopicAuthors = async () => {
|
||||
const by: AuthorsBy = { topic: props.topicSlug }
|
||||
const authors = await apiClient.loadAuthorsBy({ by, limit: 10, offset: 0 })
|
||||
setTopicAuthors(authors)
|
||||
}
|
||||
|
||||
const loadFavoriteTopArticles = async (topic: string) => {
|
||||
const options: LoadShoutsOptions = {
|
||||
|
@ -87,14 +105,6 @@ export const TopicView = (props: Props) => {
|
|||
loadReactedTopMonthArticles(topic()?.slug)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => topic(),
|
||||
() => loadRandom(),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const title = createMemo(
|
||||
() =>
|
||||
`#${capitalize(
|
||||
|
@ -158,7 +168,7 @@ export const TopicView = (props: Props) => {
|
|||
<Meta name="twitter:card" content="summary_large_image" />
|
||||
<Meta name="twitter:title" content={title()} />
|
||||
<Meta name="twitter:description" content={description()} />
|
||||
<FullTopic topic={topic()} />
|
||||
<FullTopic topic={topic()} followers={followers()} authors={topicAuthors()} />
|
||||
<div class="wide-container">
|
||||
<div class={clsx(styles.groupControls, 'row group__controls')}>
|
||||
<div class="col-md-16">
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export { BadgeSubscribeButton } from './BadgeSubscribeButton'
|
|
@ -175,7 +175,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.subscribed {
|
||||
&.followed {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
|
||||
|
|
|
@ -2,35 +2,36 @@ import { clsx } from 'clsx'
|
|||
import { Show, createMemo } from 'solid-js'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Button } from '../Button'
|
||||
import stylesButton from '../Button/Button.module.scss'
|
||||
import { CheckButton } from '../CheckButton'
|
||||
import { Icon } from '../Icon'
|
||||
import styles from './BadgeDubscribeButton.module.scss'
|
||||
|
||||
import stylesButton from '../Button/Button.module.scss'
|
||||
import styles from './FollowingButton.module.scss'
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
isSubscribed: boolean
|
||||
minimizeSubscribeButton?: boolean
|
||||
isFollowed: boolean
|
||||
minimize?: boolean
|
||||
action: () => void
|
||||
iconButtons?: boolean
|
||||
actionMessageType?: 'subscribe' | 'unsubscribe'
|
||||
actionMessageType?: 'follow' | 'unfollow'
|
||||
}
|
||||
|
||||
export const BadgeSubscribeButton = (props: Props) => {
|
||||
export const FollowingButton = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
|
||||
const inActionText = createMemo(() => {
|
||||
return props.actionMessageType === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...')
|
||||
return props.actionMessageType === 'follow' ? t('Following...') : t('Unfollowing...')
|
||||
})
|
||||
|
||||
return (
|
||||
<div class={props.class}>
|
||||
<Show
|
||||
when={!props.minimizeSubscribeButton}
|
||||
fallback={<CheckButton text={t('Follow')} checked={props.isSubscribed} onClick={props.action} />}
|
||||
when={!props.minimize}
|
||||
fallback={<CheckButton text={t('Follow')} checked={props.isFollowed} onClick={props.action} />}
|
||||
>
|
||||
<Show
|
||||
when={props.isSubscribed}
|
||||
when={props.isFollowed}
|
||||
fallback={
|
||||
<Button
|
||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||
|
@ -38,7 +39,7 @@ export const BadgeSubscribeButton = (props: Props) => {
|
|||
value={
|
||||
<Show
|
||||
when={props.iconButtons}
|
||||
fallback={props.actionMessageType ? inActionText() : t('Subscribe')}
|
||||
fallback={props.actionMessageType ? inActionText() : t('Follow')}
|
||||
>
|
||||
<Icon name="author-subscribe" class={stylesButton.icon} />
|
||||
</Show>
|
||||
|
@ -47,7 +48,7 @@ export const BadgeSubscribeButton = (props: Props) => {
|
|||
isSubscribeButton={true}
|
||||
class={clsx(styles.actionButton, {
|
||||
[styles.iconed]: props.iconButtons,
|
||||
[stylesButton.subscribed]: props.isSubscribed,
|
||||
[stylesButton.followed]: props.isFollowed,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
|
@ -76,7 +77,7 @@ export const BadgeSubscribeButton = (props: Props) => {
|
|||
isSubscribeButton={true}
|
||||
class={clsx(styles.actionButton, {
|
||||
[styles.iconed]: props.iconButtons,
|
||||
[stylesButton.subscribed]: props.isSubscribed,
|
||||
[stylesButton.followed]: props.isFollowed,
|
||||
})}
|
||||
/>
|
||||
</Show>
|
1
src/components/_shared/FollowingButton/index.ts
Normal file
1
src/components/_shared/FollowingButton/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { FollowingButton } from './FollowingButton'
|
|
@ -0,0 +1,50 @@
|
|||
.subscribers {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
margin: 0 1rem 0 0;
|
||||
vertical-align: top;
|
||||
border-bottom: unset !important;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.subscribersItem {
|
||||
position: relative;
|
||||
|
||||
&:nth-child(1) {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: -4px;
|
||||
box-shadow: 0 0 0 1px var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.subscribersCounter {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: none !important;
|
||||
|
||||
.subscribersCounter {
|
||||
background: var(--background-color-invert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subscribersList {
|
||||
display: flex;
|
||||
margin-right: 0.6rem;
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import { For, Show, createMemo } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
||||
import { Author, Topic } from '../../../graphql/schema/core.gen'
|
||||
import { Userpic } from '../../Author/Userpic'
|
||||
|
||||
import styles from './FollowingCounters.module.scss'
|
||||
|
||||
type Props = {
|
||||
followers?: Author[]
|
||||
followersAmount?: number
|
||||
following?: Array<Author | Topic>
|
||||
followingAmount?: number
|
||||
authors?: Author[]
|
||||
authorsAmount?: number
|
||||
topics?: Topic[]
|
||||
topicsAmount?: number
|
||||
}
|
||||
|
||||
const UserpicList = (props: { items: Array<Author | Topic> }) => (
|
||||
<div class={styles.subscribersList}>
|
||||
<For each={props.items.slice(0, 3)}>
|
||||
{(item) => (
|
||||
<Userpic
|
||||
size="XS"
|
||||
name={'name' in item ? item.name : 'title' in item ? item.title : ''}
|
||||
userpic={item.pic}
|
||||
class={styles.subscribersItem}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
|
||||
const Counter = (props: { count: number; label: string }) => (
|
||||
<div class={styles.subscribersCounter}>{props.label}</div>
|
||||
)
|
||||
|
||||
export const FollowingCounters = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
|
||||
const getFollowersCount = createMemo(() => props.followersAmount || props.followers?.length || 0)
|
||||
const getFollowingCount = createMemo(() => props.followingAmount || props.following?.length || 0)
|
||||
const getAuthorsCount = createMemo(() => props.authorsAmount || props.authors?.length || 0)
|
||||
const getTopicsCount = createMemo(() => props.topicsAmount || props.topics?.length || 0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<a href="?m=followers" class={styles.subscribers}>
|
||||
<Show when={getFollowersCount() > 0}>
|
||||
<UserpicList items={props.followers || []} />
|
||||
</Show>
|
||||
<Counter count={getFollowersCount()} label={t('some followers', { count: getFollowersCount() })} />
|
||||
</a>
|
||||
|
||||
<a href="?m=following" class={styles.subscribers}>
|
||||
<Show when={getFollowingCount() > 0}>
|
||||
<UserpicList items={props.following || []} />
|
||||
</Show>
|
||||
<Show
|
||||
when={getFollowingCount() > 0}
|
||||
fallback={
|
||||
<>
|
||||
<Show when={getAuthorsCount() > 0}>
|
||||
<UserpicList items={props.authors || []} />
|
||||
<Counter
|
||||
count={getAuthorsCount()}
|
||||
label={t('some authors', { count: getAuthorsCount() })}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={getTopicsCount() > 0}>
|
||||
<Counter count={getTopicsCount()} label={t('some topics', { count: getTopicsCount() })} />
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Counter
|
||||
count={getFollowingCount()}
|
||||
label={t('some followings', { count: getFollowingCount() })}
|
||||
/>
|
||||
</Show>
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
1
src/components/_shared/FollowingCounters/index.ts
Normal file
1
src/components/_shared/FollowingCounters/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { FollowingCounters } from './FollowingCounters'
|
|
@ -66,7 +66,7 @@ export const InviteMembers = (props: Props) => {
|
|||
|
||||
createEffect(
|
||||
on(
|
||||
() => sortedAuthors(),
|
||||
sortedAuthors,
|
||||
(currentAuthors) => {
|
||||
setAuthorsToInvite(currentAuthors.map((author) => ({ ...author, selected: false })))
|
||||
},
|
||||
|
|
|
@ -129,8 +129,8 @@ export const Lightbox = (props: Props) => {
|
|||
|
||||
createEffect(
|
||||
on(
|
||||
() => zoomLevel(),
|
||||
() => {
|
||||
zoomLevel,
|
||||
(_) => {
|
||||
clearTimeout(fadeTimer)
|
||||
|
||||
fadeTimer = setTimeout(() => {
|
||||
|
|
|
@ -6,12 +6,12 @@ import { validateEmail } from '../../../utils/validateEmail'
|
|||
import { Button } from '../Button'
|
||||
import { Icon } from '../Icon'
|
||||
|
||||
import styles from './Subscribe.module.scss'
|
||||
import styles from './Newsletter.module.scss'
|
||||
|
||||
type Props = {
|
||||
variant?: 'mobileSubscription'
|
||||
}
|
||||
export const Subscribe = (props: Props) => {
|
||||
export const Newsletter = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
|
||||
const [title, setTitle] = createSignal('')
|
1
src/components/_shared/Newsletter/index.ts
Normal file
1
src/components/_shared/Newsletter/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { Newsletter } from './Newsletter'
|
|
@ -61,7 +61,7 @@ export const EditorSwiper = (props: Props) => {
|
|||
createEffect(
|
||||
on(
|
||||
() => props.images.length,
|
||||
() => {
|
||||
(_) => {
|
||||
mainSwipeRef.current?.swiper.update()
|
||||
thumbSwipeRef.current?.swiper.update()
|
||||
},
|
||||
|
@ -121,17 +121,19 @@ export const EditorSwiper = (props: Props) => {
|
|||
|
||||
const handleChangeIndex = (direction: 'left' | 'right', index: number) => {
|
||||
const images = [...props.images]
|
||||
if (direction === 'left' && index > 0) {
|
||||
const copy = images.splice(index, 1)[0]
|
||||
images.splice(index - 1, 0, copy)
|
||||
} else if (direction === 'right' && index < images.length - 1) {
|
||||
const copy = images.splice(index, 1)[0]
|
||||
images.splice(index + 1, 0, copy)
|
||||
if (images?.length > 0) {
|
||||
if (direction === 'left' && index > 0) {
|
||||
const copy = images.splice(index, 1)[0]
|
||||
images.splice(index - 1, 0, copy)
|
||||
} else if (direction === 'right' && index < images.length - 1) {
|
||||
const copy = images.splice(index, 1)[0]
|
||||
images.splice(index + 1, 0, copy)
|
||||
}
|
||||
props.onImagesSorted(images)
|
||||
setTimeout(() => {
|
||||
mainSwipeRef.current.swiper.slideTo(direction === 'left' ? index - 1 : index + 1)
|
||||
}, 0)
|
||||
}
|
||||
props.onImagesSorted(images)
|
||||
setTimeout(() => {
|
||||
mainSwipeRef.current.swiper.slideTo(direction === 'left' ? index - 1 : index + 1)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const handleSaveBeforeSlideChange = () => {
|
||||
|
|
|
@ -45,7 +45,7 @@ export const ImageSwiper = (props: Props) => {
|
|||
createEffect(
|
||||
on(
|
||||
() => props.images.length,
|
||||
() => {
|
||||
(_) => {
|
||||
mainSwipeRef.current?.swiper.update()
|
||||
thumbSwipeRef.current?.swiper.update()
|
||||
},
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export { Subscribe } from './Subscribe'
|
|
@ -3,9 +3,10 @@ import type { Accessor, JSX } from 'solid-js'
|
|||
import type { Author, Reaction, Shout, Topic } from '../graphql/schema/core.gen'
|
||||
|
||||
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'
|
||||
import { createContext, createEffect, createSignal, useContext } from 'solid-js'
|
||||
import { createContext, createEffect, createSignal, on, useContext } from 'solid-js'
|
||||
|
||||
import { Chat, Message } from '../graphql/schema/chat.gen'
|
||||
import { sseUrl } from '../utils/config'
|
||||
import { useSession } from './session'
|
||||
|
||||
const RECONNECT_TIMES = 2
|
||||
|
@ -38,51 +39,57 @@ export const ConnectProvider = (props: { children: JSX.Element }) => {
|
|||
setHandlers((hhh) => [...hhh, handler])
|
||||
}
|
||||
|
||||
createEffect(async () => {
|
||||
const token = session()?.access_token
|
||||
if (token && !connected() && retried() <= RECONNECT_TIMES) {
|
||||
console.info('[context.connect] init SSE connection')
|
||||
try {
|
||||
await fetchEventSource('https://connect.discours.io', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token,
|
||||
},
|
||||
onmessage(event) {
|
||||
const m: SSEMessage = JSON.parse(event.data || '{}')
|
||||
console.log('[context.connect] Received message:', m)
|
||||
messageHandlers().forEach((handler) => handler(m))
|
||||
},
|
||||
onopen: (response) => {
|
||||
console.log('[context.connect] SSE connection opened', response)
|
||||
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
||||
setConnected(true)
|
||||
setRetried(0)
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.reject(`SSE: cannot connect to real-time updates, status: ${response.status}`)
|
||||
},
|
||||
onclose() {
|
||||
console.log('[context.connect] SSE connection closed by server')
|
||||
setConnected(false)
|
||||
if (retried() < RECONNECT_TIMES) {
|
||||
setRetried((r) => r + 1)
|
||||
}
|
||||
},
|
||||
onerror(err) {
|
||||
console.error('[context.connect] SSE connection error:', err)
|
||||
setConnected(false)
|
||||
if (retried() < RECONNECT_TIMES) {
|
||||
setRetried((r) => r + 1)
|
||||
} else throw Error(err)
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[context.connect] SSE connection failed:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
createEffect(
|
||||
on(
|
||||
() => session()?.access_token,
|
||||
async (tkn) => {
|
||||
if (!sseUrl) return
|
||||
if (!tkn) return
|
||||
if (!connected() && retried() <= RECONNECT_TIMES) {
|
||||
console.info('[context.connect] got token, init SSE connection')
|
||||
try {
|
||||
await fetchEventSource(sseUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: tkn,
|
||||
},
|
||||
onmessage(event) {
|
||||
const m: SSEMessage = JSON.parse(event.data || '{}')
|
||||
console.log('[context.connect] Received message:', m)
|
||||
messageHandlers().forEach((handler) => handler(m))
|
||||
},
|
||||
onopen: (response) => {
|
||||
console.log('[context.connect] SSE connection opened', response)
|
||||
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
||||
setConnected(true)
|
||||
setRetried(0)
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.reject(`SSE: cannot connect to real-time updates: ${response.status}`)
|
||||
},
|
||||
onclose() {
|
||||
console.log('[context.connect] SSE connection closed by server')
|
||||
setConnected(false)
|
||||
if (retried() < RECONNECT_TIMES) {
|
||||
setRetried((r) => r + 1)
|
||||
} else throw Error('closed by server')
|
||||
},
|
||||
onerror(err) {
|
||||
console.error('[context.connect] SSE connection error:', err)
|
||||
setConnected(false)
|
||||
if (retried() < RECONNECT_TIMES) {
|
||||
setRetried((r) => r + 1)
|
||||
} else throw Error(err)
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[context.connect] SSE connection failed:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const value: ConnectContextType = { addHandler, connected }
|
||||
|
||||
|
|
|
@ -1,30 +1,27 @@
|
|||
import { Accessor, JSX, createContext, createEffect, createSignal, useContext } from 'solid-js'
|
||||
import { Accessor, JSX, createContext, createEffect, createSignal, on, useContext } from 'solid-js'
|
||||
import { createStore } from 'solid-js/store'
|
||||
|
||||
import { apiClient } from '../graphql/client/core'
|
||||
import { Author, AuthorFollowsResult, Community, FollowingEntity, Topic } from '../graphql/schema/core.gen'
|
||||
import { Author, AuthorFollowsResult, FollowingEntity } from '../graphql/schema/core.gen'
|
||||
|
||||
import { useSession } from './session'
|
||||
|
||||
export type SubscriptionsData = {
|
||||
topics?: Topic[]
|
||||
authors?: Author[]
|
||||
communities?: Community[]
|
||||
}
|
||||
|
||||
type SubscribeAction = { slug: string; type: 'subscribe' | 'unsubscribe' }
|
||||
type FollowingData = { slug: string; type: 'follow' | 'unfollow' }
|
||||
|
||||
interface FollowingContextType {
|
||||
loading: Accessor<boolean>
|
||||
|
||||
followers: Accessor<Author[]>
|
||||
subscriptions: AuthorFollowsResult
|
||||
setSubscriptions: (subscriptions: AuthorFollowsResult) => void
|
||||
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
||||
loadSubscriptions: () => void
|
||||
setFollows: (follows: AuthorFollowsResult) => void
|
||||
|
||||
following: Accessor<FollowingData>
|
||||
changeFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
||||
|
||||
follows: AuthorFollowsResult
|
||||
loadFollows: () => void
|
||||
|
||||
follow: (what: FollowingEntity, slug: string) => Promise<void>
|
||||
unfollow: (what: FollowingEntity, slug: string) => Promise<void>
|
||||
// followers: Accessor<Author[]>
|
||||
subscribeInAction?: Accessor<SubscribeAction>
|
||||
}
|
||||
|
||||
const FollowingContext = createContext<FollowingContextType>()
|
||||
|
@ -42,7 +39,7 @@ const EMPTY_SUBSCRIPTIONS: AuthorFollowsResult = {
|
|||
export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||
const [loading, setLoading] = createSignal<boolean>(false)
|
||||
const [followers, setFollowers] = createSignal<Author[]>([])
|
||||
const [subscriptions, setSubscriptions] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS)
|
||||
const [follows, setFollows] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS)
|
||||
const { author, session } = useSession()
|
||||
|
||||
const fetchData = async () => {
|
||||
|
@ -51,68 +48,67 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
|
|||
if (apiClient.private) {
|
||||
console.debug('[context.following] fetching subs data...')
|
||||
const result = await apiClient.getAuthorFollows({ user: session()?.user.id })
|
||||
setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
|
||||
setFollows(result || EMPTY_SUBSCRIPTIONS)
|
||||
}
|
||||
} catch (error) {
|
||||
console.info('[context.following] cannot get subs', error)
|
||||
console.warn('[context.following] cannot get subs', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
console.info('[context.following] subs:', subscriptions)
|
||||
})
|
||||
|
||||
const [subscribeInAction, setSubscribeInAction] = createSignal<SubscribeAction>()
|
||||
const [following, setFollowing] = createSignal<FollowingData>()
|
||||
const follow = async (what: FollowingEntity, slug: string) => {
|
||||
if (!author()) return
|
||||
setSubscribeInAction({ slug, type: 'subscribe' })
|
||||
setFollowing({ slug, type: 'follow' })
|
||||
try {
|
||||
const subscriptionData = await apiClient.follow({ what, slug })
|
||||
setSubscriptions((prevSubscriptions) => {
|
||||
if (!prevSubscriptions[what]) prevSubscriptions[what] = []
|
||||
prevSubscriptions[what].push(subscriptionData)
|
||||
return prevSubscriptions
|
||||
const result = await apiClient.follow({ what, slug })
|
||||
setFollows((subs) => {
|
||||
if (result.authors) subs['authors'] = result.authors || []
|
||||
if (result.topics) subs['topics'] = result.topics || []
|
||||
return subs
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
setSubscribeInAction() // Сбрасываем состояние действия подписки.
|
||||
setFollowing() // Сбрасываем состояние действия подписки.
|
||||
}
|
||||
}
|
||||
|
||||
const unfollow = async (what: FollowingEntity, slug: string) => {
|
||||
if (!author()) return
|
||||
setSubscribeInAction({ slug: slug, type: 'unsubscribe' })
|
||||
setFollowing({ slug: slug, type: 'unfollow' })
|
||||
try {
|
||||
await apiClient.unfollow({ what, slug })
|
||||
const result = await apiClient.unfollow({ what, slug })
|
||||
setFollows((subs) => {
|
||||
if (result.authors) subs['authors'] = result.authors || []
|
||||
if (result.topics) subs['topics'] = result.topics || []
|
||||
return subs
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
setSubscribeInAction()
|
||||
setFollowing()
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (author()) {
|
||||
try {
|
||||
const appdata = session()?.user.app_data
|
||||
createEffect(
|
||||
on(
|
||||
() => session()?.user.app_data,
|
||||
(appdata) => {
|
||||
if (appdata) {
|
||||
const { authors, followers, topics } = appdata
|
||||
setSubscriptions({ authors, topics })
|
||||
setFollows({ authors, topics })
|
||||
setFollowers(followers)
|
||||
if (!authors) fetchData()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const setFollowing = (what: FollowingEntity, slug: string, value = true) => {
|
||||
setSubscriptions((prevSubscriptions) => {
|
||||
const updatedSubs = { ...prevSubscriptions }
|
||||
const changeFollowing = (what: FollowingEntity, slug: string, value = true) => {
|
||||
setFollows((fff) => {
|
||||
const updatedSubs = { ...fff }
|
||||
if (!updatedSubs[what]) updatedSubs[what] = []
|
||||
if (value) {
|
||||
const exists = updatedSubs[what]?.some((entity) => entity.slug === slug)
|
||||
|
@ -133,15 +129,14 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
|
|||
|
||||
const value: FollowingContextType = {
|
||||
loading,
|
||||
subscriptions,
|
||||
setSubscriptions,
|
||||
setFollowing,
|
||||
follows,
|
||||
setFollows,
|
||||
following,
|
||||
changeFollowing,
|
||||
followers,
|
||||
loadSubscriptions: fetchData,
|
||||
loadFollows: fetchData,
|
||||
follow,
|
||||
unfollow,
|
||||
// followers,
|
||||
subscribeInAction,
|
||||
}
|
||||
|
||||
return <FollowingContext.Provider value={value}>{props.children}</FollowingContext.Provider>
|
||||
|
|
|
@ -184,7 +184,7 @@ function initServerProvider() {
|
|||
const index = tags.findIndex(
|
||||
(prev) => prev.tag === tagDesc.tag && getTagKey(prev, properties) === tagDescKey,
|
||||
)
|
||||
if (index !== -1) {
|
||||
if (index !== -1 && tags?.length > 0) {
|
||||
tags.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,27 +32,26 @@ import { inboxClient } from '../graphql/client/chat'
|
|||
import { apiClient } from '../graphql/client/core'
|
||||
import { useRouter } from '../stores/router'
|
||||
import { showModal } from '../stores/ui'
|
||||
import { addAuthors } from '../stores/zine/authors'
|
||||
import { addAuthors, loadAuthor } from '../stores/zine/authors'
|
||||
|
||||
import { authApiUrl } from '../utils/config'
|
||||
import { useLocalize } from './localize'
|
||||
import { useSnackbar } from './snackbar'
|
||||
|
||||
const defaultConfig: ConfigType = {
|
||||
authorizerURL: 'https://auth.discours.io',
|
||||
authorizerURL: authApiUrl.replace('/graphql', ''),
|
||||
redirectURL: 'https://testing.discours.io',
|
||||
clientID: 'b9038a34-ca59-41ae-a105-c7fbea603e24', // FIXME: use env?
|
||||
clientID: '',
|
||||
}
|
||||
|
||||
export type SessionContextType = {
|
||||
config: Accessor<ConfigType>
|
||||
session: Resource<AuthToken>
|
||||
author: Resource<Author | null>
|
||||
author: Accessor<Author>
|
||||
authError: Accessor<string>
|
||||
isSessionLoaded: Accessor<boolean>
|
||||
loadSession: () => AuthToken | Promise<AuthToken>
|
||||
setSession: (token: AuthToken | null) => void // setSession
|
||||
loadAuthor: (info?: unknown) => Author | Promise<Author>
|
||||
setAuthor: (a: Author) => void
|
||||
requireAuthentication: (
|
||||
callback: (() => Promise<void>) | (() => void),
|
||||
modalSource: AuthModalSource,
|
||||
|
@ -66,16 +65,39 @@ export type SessionContextType = {
|
|||
params: ForgotPasswordInput,
|
||||
) => Promise<{ data: ForgotPasswordResponse; errors: Error[] }>
|
||||
changePassword: (password: string, token: string) => void
|
||||
confirmEmail: (input: VerifyEmailInput) => Promise<AuthToken> // email confirm callback is in auth.discours.io
|
||||
confirmEmail: (input: VerifyEmailInput) => Promise<AuthToken> // email confirm callback is in authorizer
|
||||
setIsSessionLoaded: (loaded: boolean) => void
|
||||
authorizer: () => Authorizer
|
||||
isRegistered: (email: string) => Promise<string>
|
||||
resendVerifyEmail: (params: ResendVerifyEmailInput) => Promise<GenericResponse>
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: <explanation>
|
||||
const noop = () => {}
|
||||
|
||||
const noop = () => null
|
||||
const metaRes = {
|
||||
data: {
|
||||
meta: {
|
||||
version: 'latest',
|
||||
// client_id: 'b9038a34-ca59-41ae-a105-c7fbea603e24',
|
||||
is_google_login_enabled: true,
|
||||
is_facebook_login_enabled: true,
|
||||
is_github_login_enabled: true,
|
||||
is_linkedin_login_enabled: false,
|
||||
is_apple_login_enabled: false,
|
||||
is_twitter_login_enabled: true,
|
||||
is_microsoft_login_enabled: false,
|
||||
is_twitch_login_enabled: false,
|
||||
is_roblox_login_enabled: false,
|
||||
is_email_verification_enabled: true,
|
||||
is_basic_authentication_enabled: true,
|
||||
is_magic_link_login_enabled: true,
|
||||
is_sign_up_enabled: true,
|
||||
is_strong_password_enabled: false,
|
||||
is_multi_factor_auth_enabled: true,
|
||||
is_mobile_basic_authentication_enabled: true,
|
||||
is_phone_verification_enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
const SessionContext = createContext<SessionContextType>()
|
||||
|
||||
export function useSession() {
|
||||
|
@ -96,15 +118,15 @@ export const SessionProvider = (props: {
|
|||
// handle auth state callback
|
||||
createEffect(
|
||||
on(
|
||||
() => searchParams()?.state,
|
||||
(state) => {
|
||||
if (state) {
|
||||
setOauthState((_s) => state)
|
||||
const scope = searchParams()?.scope
|
||||
? searchParams()?.scope?.toString().split(' ')
|
||||
searchParams,
|
||||
(params) => {
|
||||
if (params?.state) {
|
||||
setOauthState((_s) => params?.state)
|
||||
const scope = params?.scope
|
||||
? params?.scope?.toString().split(' ')
|
||||
: ['openid', 'profile', 'email']
|
||||
if (scope) console.info(`[context.session] scope: ${scope}`)
|
||||
const url = searchParams()?.redirect_uri || searchParams()?.redirectURL || window.location.href
|
||||
const url = params?.redirect_uri || params?.redirectURL || window.location.href
|
||||
setConfig((c: ConfigType) => ({ ...c, redirectURL: url.split('?')[0] }))
|
||||
changeSearchParams({ mode: 'confirm-email', m: 'auth' }, true)
|
||||
}
|
||||
|
@ -202,75 +224,56 @@ export const SessionProvider = (props: {
|
|||
|
||||
onCleanup(() => clearTimeout(minuteLater))
|
||||
|
||||
const authorData = async () => {
|
||||
const u = session()?.user
|
||||
return u ? (await apiClient.getAuthorId({ user: u.id.trim() })) || null : null
|
||||
}
|
||||
const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource<Author | null>(authorData, {
|
||||
ssrLoadFrom: 'initial',
|
||||
initialValue: null,
|
||||
})
|
||||
|
||||
const [author, setAuthor] = createSignal<Author>()
|
||||
// when session is loaded
|
||||
createEffect(() => {
|
||||
if (session()) {
|
||||
const token = session()?.access_token
|
||||
if (token) {
|
||||
if (!inboxClient.private) {
|
||||
apiClient.connect(token)
|
||||
inboxClient.connect(token)
|
||||
}
|
||||
|
||||
try {
|
||||
const appdata = session()?.user.app_data
|
||||
if (appdata) {
|
||||
const { profile } = appdata
|
||||
if (profile?.id) {
|
||||
setAuthor(profile)
|
||||
addAuthors([profile])
|
||||
createEffect(
|
||||
on(
|
||||
() => session(),
|
||||
async (s: AuthToken) => {
|
||||
if (s) {
|
||||
const token = s?.access_token
|
||||
const profile = s?.user?.app_data?.profile
|
||||
if (token && !inboxClient.private) {
|
||||
apiClient.connect(token)
|
||||
inboxClient.connect(token)
|
||||
}
|
||||
if (profile?.id) {
|
||||
addAuthors([profile])
|
||||
setAuthor(profile)
|
||||
setIsSessionLoaded(true)
|
||||
} else {
|
||||
console.warn('app_data is empty')
|
||||
if (s?.user) {
|
||||
try {
|
||||
console.info('Loading author:', s?.user?.nickname)
|
||||
const a = await loadAuthor({ slug: s?.user?.nickname })
|
||||
addAuthors([a])
|
||||
setAuthor(a)
|
||||
s.user.app_data.profile = a
|
||||
} catch (error) {
|
||||
console.error('Error loading author:', error)
|
||||
}
|
||||
} else {
|
||||
setTimeout(loadAuthor, 15)
|
||||
console.warn(s)
|
||||
setSession(null)
|
||||
setAuthor(null)
|
||||
setIsSessionLoaded(true)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
setIsSessionLoaded(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// when author is loaded
|
||||
createEffect(() => {
|
||||
if (author()) {
|
||||
addAuthors([author()])
|
||||
} else {
|
||||
reset()
|
||||
}
|
||||
})
|
||||
|
||||
const reset = () => {
|
||||
setIsSessionLoaded(true)
|
||||
setSession(null)
|
||||
setAuthor(null)
|
||||
}
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
// initial effect
|
||||
onMount(async () => {
|
||||
const metaRes = await authorizer().getMetaData()
|
||||
onMount(() => {
|
||||
setConfig({
|
||||
...defaultConfig,
|
||||
...metaRes,
|
||||
redirectURL: window.location.origin,
|
||||
})
|
||||
let s: AuthToken
|
||||
try {
|
||||
s = await loadSession()
|
||||
} catch (error) {
|
||||
console.warn('[context.session] load session failed', error)
|
||||
}
|
||||
if (!s) reset()
|
||||
loadSession()
|
||||
})
|
||||
|
||||
// callback state updater
|
||||
|
@ -316,8 +319,10 @@ export const SessionProvider = (props: {
|
|||
const signOut = async () => {
|
||||
const authResult: ApiResponse<GenericResponse> = await authorizer().logout()
|
||||
console.debug(authResult)
|
||||
reset()
|
||||
setSession(null)
|
||||
setIsSessionLoaded(true)
|
||||
showSnackbar({ body: t("You've successfully logged out") })
|
||||
console.debug(session())
|
||||
}
|
||||
|
||||
const changePassword = async (password: string, token: string) => {
|
||||
|
@ -391,9 +396,7 @@ export const SessionProvider = (props: {
|
|||
updateProfile,
|
||||
setIsSessionLoaded,
|
||||
setSession,
|
||||
setAuthor,
|
||||
authorizer,
|
||||
loadAuthor,
|
||||
forgotPassword,
|
||||
changePassword,
|
||||
oauth,
|
||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
|||
LoadShoutsOptions,
|
||||
MutationDelete_ShoutArgs,
|
||||
ProfileInput,
|
||||
QueryGet_Topic_FollowersArgs,
|
||||
QueryLoad_Authors_ByArgs,
|
||||
QueryLoad_Shouts_Random_TopArgs,
|
||||
QueryLoad_Shouts_SearchArgs,
|
||||
|
@ -39,11 +40,11 @@ import loadShoutsUnrated from '../query/core/articles-load-unrated'
|
|||
import authorBy from '../query/core/author-by'
|
||||
import authorFollowers from '../query/core/author-followers'
|
||||
import authorFollows from '../query/core/author-follows'
|
||||
import authorId from '../query/core/author-id'
|
||||
import authorsAll from '../query/core/authors-all'
|
||||
import authorsLoadBy from '../query/core/authors-load-by'
|
||||
import reactionsLoadBy from '../query/core/reactions-load-by'
|
||||
import topicBySlug from '../query/core/topic-by-slug'
|
||||
import topicFollowers from '../query/core/topic-followers'
|
||||
import topicsAll from '../query/core/topics-all'
|
||||
import topicsRandomQuery from '../query/core/topics-random'
|
||||
|
||||
|
@ -119,16 +120,16 @@ export const apiClient = {
|
|||
return response.data.get_author
|
||||
},
|
||||
|
||||
getAuthorId: async (params: { user: string }): Promise<Author> => {
|
||||
const response = await publicGraphQLClient.query(authorId, params).toPromise()
|
||||
return response.data.get_author_id
|
||||
},
|
||||
|
||||
getAuthorFollowers: async ({ slug }: { slug: string }): Promise<Author[]> => {
|
||||
const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise()
|
||||
return response.data.get_author_followers
|
||||
},
|
||||
|
||||
getTopicFollowers: async ({ slug }: QueryGet_Topic_FollowersArgs): Promise<Author[]> => {
|
||||
const response = await publicGraphQLClient.query(topicFollowers, { slug }).toPromise()
|
||||
return response.data.get_topic_followers
|
||||
},
|
||||
|
||||
getAuthorFollows: async (params: {
|
||||
slug?: string
|
||||
author_id?: number
|
||||
|
|
|
@ -6,7 +6,24 @@ export default gql`
|
|||
error
|
||||
authors {
|
||||
id
|
||||
name
|
||||
slug
|
||||
pic
|
||||
bio
|
||||
stat {
|
||||
followers
|
||||
shouts
|
||||
comments
|
||||
}
|
||||
}
|
||||
topics {
|
||||
body
|
||||
slug
|
||||
stat {
|
||||
shouts
|
||||
authors
|
||||
followers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,27 @@ export default gql`
|
|||
mutation UnfollowMutation($what: FollowingEntity!, $slug: String!) {
|
||||
unfollow(what: $what, slug: $slug) {
|
||||
error
|
||||
authors {
|
||||
id
|
||||
name
|
||||
slug
|
||||
pic
|
||||
bio
|
||||
stat {
|
||||
followers
|
||||
shouts
|
||||
comments
|
||||
}
|
||||
}
|
||||
topics {
|
||||
body
|
||||
slug
|
||||
stat {
|
||||
shouts
|
||||
authors
|
||||
followers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
query UserSubscribersQuery($slug: String, $user: String, $author_id: Int) {
|
||||
query UserFollowingCountersQuery($slug: String, $user: String, $author_id: Int) {
|
||||
get_author_followers(slug: $slug, user: $user, author_id: $author_id) {
|
||||
id
|
||||
slug
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
query GetAuthorId($user: String!) {
|
||||
get_author_id(user: $user) {
|
||||
query TopicFollowersQuery($slug: String) {
|
||||
get_topic_followers(slug: $slug) {
|
||||
id
|
||||
slug
|
||||
name
|
||||
bio
|
||||
about
|
||||
pic
|
||||
# communities
|
||||
links
|
||||
created_at
|
||||
last_seen
|
||||
|
@ -18,8 +19,6 @@ export default gql`
|
|||
followers
|
||||
rating
|
||||
comments
|
||||
rating_shouts
|
||||
rating_comments
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ export const DiscussionRulesPage = () => {
|
|||
людей рождается истина.
|
||||
</p>
|
||||
|
||||
<h3>За что можно получить дырку в карме и выиграть бан в сообществе</h3>
|
||||
<h3 id="ban">За что можно получить дырку в карме и выиграть бан в сообществе</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Feedback } from '../../components/Discours/Feedback'
|
|||
import { Modal } from '../../components/Nav/Modal'
|
||||
import Opener from '../../components/Nav/Modal/Opener'
|
||||
import { StaticPage } from '../../components/Views/StaticPage'
|
||||
import { Subscribe } from '../../components/_shared/Subscribe'
|
||||
import { Newsletter } from '../../components/_shared/Newsletter'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { getImageUrl } from '../../utils/getImageUrl'
|
||||
|
||||
|
@ -24,7 +24,7 @@ export const ManifestPage = () => {
|
|||
<Feedback />
|
||||
</Modal>
|
||||
<Modal variant="wide" name="subscribe">
|
||||
<Subscribe />
|
||||
<Newsletter />
|
||||
</Modal>
|
||||
<Meta name="descprition" content={description} />
|
||||
<Meta name="keywords" content={t('keywords')} />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { PageProps } from './types'
|
||||
|
||||
import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||
import { Show, createEffect, createMemo, createSignal, on, onCleanup } from 'solid-js'
|
||||
|
||||
import { AuthorView, PRERENDERED_ARTICLES_COUNT } from '../components/Views/Author'
|
||||
import { Loading } from '../components/_shared/Loading'
|
||||
|
@ -20,38 +20,19 @@ export const AuthorPage = (props: PageProps) => {
|
|||
Boolean(props.authorShouts) && Boolean(props.author) && props.author.slug === slug(),
|
||||
)
|
||||
|
||||
const preload = () => {
|
||||
return Promise.all([
|
||||
loadShouts({
|
||||
filters: { author: slug(), featured: false },
|
||||
limit: PRERENDERED_ARTICLES_COUNT,
|
||||
}),
|
||||
loadAuthor({ slug: slug() }),
|
||||
])
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (isLoaded()) {
|
||||
return
|
||||
}
|
||||
|
||||
resetSortedArticles()
|
||||
await preload()
|
||||
|
||||
setIsLoaded(true)
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => slug(),
|
||||
async () => {
|
||||
on(slug, async (s) => {
|
||||
if (s) {
|
||||
setIsLoaded(false)
|
||||
resetSortedArticles()
|
||||
await preload()
|
||||
await loadShouts({
|
||||
filters: { author: s, featured: false },
|
||||
limit: PRERENDERED_ARTICLES_COUNT,
|
||||
})
|
||||
await loadAuthor({ slug: s })
|
||||
setIsLoaded(true)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
onCleanup(() => resetSortedArticles())
|
||||
|
|
|
@ -17,9 +17,10 @@ import styles from '../styles/Create.module.scss'
|
|||
|
||||
const handleCreate = async (layout: LayoutType) => {
|
||||
const shout = await apiClient.createArticle({ article: { layout: layout } })
|
||||
redirectPage(router, 'edit', {
|
||||
shoutId: shout?.id.toString(),
|
||||
})
|
||||
shout?.id &&
|
||||
redirectPage(router, 'edit', {
|
||||
shoutId: shout?.id.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
export const CreatePage = () => {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useLocalize } from '../context/localize'
|
|||
import { useSession } from '../context/session'
|
||||
import { apiClient } from '../graphql/client/core'
|
||||
import { Shout } from '../graphql/schema/core.gen'
|
||||
import { router, useRouter } from '../stores/router'
|
||||
import { router } from '../stores/router'
|
||||
|
||||
import { redirectPage } from '@nanostores/router'
|
||||
import { useSnackbar } from '../context/snackbar'
|
||||
|
@ -33,7 +33,6 @@ const getContentTypeTitle = (layout: LayoutType) => {
|
|||
export const EditPage = () => {
|
||||
const { t } = useLocalize()
|
||||
const { session } = useSession()
|
||||
const { page } = useRouter()
|
||||
const snackbar = useSnackbar()
|
||||
|
||||
const fail = async (error: string) => {
|
||||
|
@ -48,18 +47,21 @@ export const EditPage = () => {
|
|||
|
||||
createEffect(
|
||||
on(
|
||||
() => page(),
|
||||
() => window?.location.pathname,
|
||||
(p) => {
|
||||
if (p?.path) {
|
||||
console.debug(p?.path)
|
||||
const shoutId = p?.path.split('/').pop()
|
||||
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
|
||||
console.debug(`editing shout ${shoutIdFromUrl}`)
|
||||
if (shoutIdFromUrl) {
|
||||
setShoutId(shoutIdFromUrl)
|
||||
if (p) {
|
||||
console.debug(p)
|
||||
const shoutId = p.split('/').pop()
|
||||
if (shoutId) {
|
||||
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
|
||||
console.debug(`editing shout ${shoutIdFromUrl}`)
|
||||
if (shoutIdFromUrl) {
|
||||
setShoutId(shoutIdFromUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
@ -12,10 +12,9 @@ import { LayoutType } from '../types'
|
|||
export const ExpoPage = (props: PageProps) => {
|
||||
const { t } = useLocalize()
|
||||
const { page } = useRouter()
|
||||
const getLayout = createMemo<LayoutType>(() => page().params['layout'] as LayoutType)
|
||||
|
||||
const getTitle = () => {
|
||||
switch (getLayout()) {
|
||||
const layout = createMemo(() => page().params['layout'] as LayoutType)
|
||||
const title = createMemo(() => {
|
||||
switch (layout()) {
|
||||
case 'audio': {
|
||||
return t('Audio')
|
||||
}
|
||||
|
@ -32,22 +31,14 @@ export const ExpoPage = (props: PageProps) => {
|
|||
return t('Art')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => getLayout(),
|
||||
() => {
|
||||
document.title = getTitle()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
createEffect(on(title, (t) => (document.title = t), { defer: true }))
|
||||
|
||||
return (
|
||||
<PageLayout withPadding={true} zeroBottomPadding={true} title={getTitle()}>
|
||||
<PageLayout withPadding={true} zeroBottomPadding={true} title={title()}>
|
||||
<Topics />
|
||||
<Expo shouts={props.expoShouts} layout={getLayout()} />
|
||||
<Expo shouts={props.expoShouts} layout={layout()} />
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Match, Switch, createEffect, on, onCleanup } from 'solid-js'
|
||||
import { createEffect, on, onCleanup } from 'solid-js'
|
||||
|
||||
import { AuthGuard } from '../components/AuthGuard'
|
||||
import { Feed } from '../components/Views/Feed'
|
||||
import { PageLayout } from '../components/_shared/PageLayout'
|
||||
import { useLocalize } from '../context/localize'
|
||||
|
@ -25,34 +24,14 @@ const handleMyFeedLoadShouts = (options: LoadShoutsOptions) => {
|
|||
|
||||
export const FeedPage = () => {
|
||||
const { t } = useLocalize()
|
||||
|
||||
onCleanup(() => resetSortedArticles())
|
||||
|
||||
const { page } = useRouter()
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => page().route,
|
||||
() => {
|
||||
resetSortedArticles()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
createEffect(on(page, (_) => resetSortedArticles(), { defer: true }))
|
||||
onCleanup(() => resetSortedArticles())
|
||||
|
||||
return (
|
||||
<PageLayout title={t('Feed')}>
|
||||
<ReactionsProvider>
|
||||
<Switch fallback={<Feed loadShouts={handleFeedLoadShouts} />}>
|
||||
<Match when={page().route === 'feed'}>
|
||||
<Feed loadShouts={handleFeedLoadShouts} />
|
||||
</Match>
|
||||
<Match when={page().route === 'feedMy'}>
|
||||
<AuthGuard>
|
||||
<Feed loadShouts={handleMyFeedLoadShouts} />
|
||||
</AuthGuard>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Feed loadShouts={page().route === 'feedMy' ? handleMyFeedLoadShouts : handleFeedLoadShouts} />
|
||||
</ReactionsProvider>
|
||||
</PageLayout>
|
||||
)
|
||||
|
|
|
@ -44,10 +44,10 @@ export const ProfileSecurityPage = () => {
|
|||
createEffect(
|
||||
on(
|
||||
() => session()?.user?.email,
|
||||
() => {
|
||||
(email) => {
|
||||
setFormData((prevData) => ({
|
||||
...prevData,
|
||||
['email']: session()?.user?.email,
|
||||
email,
|
||||
}))
|
||||
},
|
||||
),
|
||||
|
|
|
@ -37,24 +37,22 @@ export const TopicPage = (props: PageProps) => {
|
|||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => slug(),
|
||||
async () => {
|
||||
on(slug, async (s) => {
|
||||
if (s) {
|
||||
setIsLoaded(false)
|
||||
resetSortedArticles()
|
||||
await preload()
|
||||
setIsLoaded(true)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
onCleanup(() => resetSortedArticles())
|
||||
onCleanup(resetSortedArticles)
|
||||
|
||||
const usePrerenderedData = props.topic?.slug === slug()
|
||||
|
||||
return (
|
||||
<PageLayout title={props.seo.title}>
|
||||
<PageLayout title={props.seo?.title || props.topic?.title}>
|
||||
<ReactionsProvider>
|
||||
<Show when={isLoaded()} fallback={<Loading />}>
|
||||
<TopicView
|
||||
|
|
|
@ -53,4 +53,4 @@ export type UploadedFile = {
|
|||
originalFilename?: string
|
||||
}
|
||||
|
||||
export type SubscriptionFilter = 'all' | 'authors' | 'topics' | 'communities'
|
||||
export type FollowsFilter = 'all' | 'authors' | 'topics' | 'communities'
|
||||
|
|
|
@ -9,7 +9,7 @@ import { hydrate } from 'solid-js/web'
|
|||
|
||||
import { App } from '../components/App'
|
||||
import { initRouter } from '../stores/router'
|
||||
import { GLITCHTIP_DSN } from '../utils/config'
|
||||
import { reportDsn } from '../utils/config'
|
||||
import { resolveHydrationPromise } from '../utils/hydrationPromise'
|
||||
|
||||
let layoutReady = false
|
||||
|
@ -22,7 +22,7 @@ export const render = async (pageContext: PageContextBuiltInClientWithClientRout
|
|||
initRouter(pathname, searchParams)
|
||||
|
||||
SentryInit({
|
||||
dsn: GLITCHTIP_DSN,
|
||||
dsn: reportDsn,
|
||||
tracesSampleRate: 0.01,
|
||||
integrations: [replayIntegration()],
|
||||
// Session Replay
|
||||
|
|
|
@ -1,20 +1,8 @@
|
|||
export const isDev = import.meta.env.MODE === 'development'
|
||||
|
||||
const defaultThumborUrl = 'https://images.discours.io'
|
||||
export const cdnUrl = 'https://cdn.discours.io'
|
||||
export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || defaultThumborUrl
|
||||
|
||||
export const SENTRY_DSN = import.meta.env.PUBLIC_SENTRY_DSN || ''
|
||||
export const GLITCHTIP_DSN = import.meta.env.PUBLIC_GLITCHTIP_DSN || ''
|
||||
|
||||
const defaultSearchUrl = 'https://search.discours.io'
|
||||
export const searchUrl = import.meta.env.PUBLIC_SEARCH_URL || defaultSearchUrl
|
||||
|
||||
const defaultCoreUrl = 'https://core.discours.io'
|
||||
export const coreApiUrl = import.meta.env.PUBLIC_CORE_API || defaultCoreUrl
|
||||
|
||||
const defaultChatUrl = 'https://chat.discours.io'
|
||||
export const chatApiUrl = import.meta.env.PUBLIC_CHAT_API || defaultChatUrl
|
||||
|
||||
const defaultAuthUrl = 'https://auth.discours.io'
|
||||
export const authApiUrl = import.meta.env.PUBLIC_AUTH_API || defaultAuthUrl
|
||||
export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || 'https://images.discours.io'
|
||||
export const reportDsn = import.meta.env.PUBLIC_GLITCHTIP_DSN || import.meta.env.PUBLIC_SENTRY_DSN || ''
|
||||
export const coreApiUrl = import.meta.env.PUBLIC_API_BASE || 'https://core.discours.io'
|
||||
export const chatApiUrl = import.meta.env.PUBLIC_CHAT_API || 'https://inbox.discours.io'
|
||||
export const authApiUrl = import.meta.env.PUBLIC_AUTH_API || 'https://auth.discours.io/graphql'
|
||||
export const sseUrl = import.meta.env.PUBLIC_REALTIME_EVENTS || 'https://connect.discours.io'
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { UploadFile } from '@solid-primitives/upload'
|
||||
|
||||
import { UploadedFile } from '../pages/types'
|
||||
import { coreApiUrl } from './config'
|
||||
|
||||
const apiBaseUrl = 'https://core.discours.io'
|
||||
const apiUrl = `${apiBaseUrl}/upload`
|
||||
const apiUrl = `${coreApiUrl}/upload`
|
||||
|
||||
export const handleFileUpload = async (uploadFile: UploadFile, token: string): Promise<UploadedFile> => {
|
||||
const formData = new FormData()
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
const baseHost = process.env.BASE_URL
|
||||
const baseHost = process.env.BASE_HOST || 'https://localhost:3000'
|
||||
|
||||
const pagesTitles = {
|
||||
'/': /Дискурс/,
|
||||
'/feed': /Дискурс/,
|
||||
'/create': /Дискурс/,
|
||||
'/about/donate': /Дискурс/,
|
||||
'/authors': /Дискурс/,
|
||||
'/topics': /Дискурс/,
|
||||
'/inbox': /Дискурс/,
|
||||
'/feed': /Лента/,
|
||||
'/create': /Выберите тип публикации/,
|
||||
'/about/help': /Поддержите Дискурс/,
|
||||
'/authors': /Авторы/,
|
||||
'/topics': /Темы и сюжеты/,
|
||||
'/inbox': /Входящие/,
|
||||
}
|
||||
|
||||
Object.keys(pagesTitles).forEach((res: string) => {
|
||||
|
|
|
@ -5,13 +5,13 @@ import { chromium } from 'playwright'
|
|||
|
||||
// Define the URLs to visit
|
||||
const pagesToVisit = [
|
||||
'http://localhost:3000/',
|
||||
'http://localhost:3000/feed',
|
||||
'http://localhost:3000/create',
|
||||
'http://localhost:3000/about/donate',
|
||||
'http://localhost:3000/authors',
|
||||
'http://localhost:3000/topics',
|
||||
'http://localhost:3000/inbox',
|
||||
'https://localhost:3000/',
|
||||
'https://localhost:3000/feed',
|
||||
'https://localhost:3000/create',
|
||||
'https://localhost:3000/about/donate',
|
||||
'https://localhost:3000/authors',
|
||||
'https://localhost:3000/topics',
|
||||
'https://localhost:3000/inbox',
|
||||
]
|
||||
|
||||
// Loop through the pages and visit each one
|
||||
|
|
|
@ -14,8 +14,9 @@ const cssModuleHMR = () => {
|
|||
const { modules } = context
|
||||
|
||||
modules.forEach((module) => {
|
||||
if (module.id.includes('.module.scss')) {
|
||||
if (module.id.includes('.scss') || module.id.includes('.css')) {
|
||||
module.isSelfAccepting = true
|
||||
// module.accept()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue
Block a user