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
|
- name: Test production build
|
||||||
run: npm run 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:
|
email-templates:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Update templates on Mailgun
|
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
|
- uses: actions/setup-node@v4
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
run: npm i
|
||||||
|
|
||||||
|
- name: Install CI checks
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Check types
|
- name: Check types
|
||||||
|
@ -23,20 +26,3 @@ jobs:
|
||||||
|
|
||||||
- name: Test production build
|
- name: Test production build
|
||||||
run: npm run 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:
|
fix styles, imports, formatting and autofixable linting errors:
|
||||||
```
|
```
|
||||||
npm run fix
|
npm run fix
|
||||||
```
|
npm run format
|
||||||
## Code generation
|
|
||||||
|
|
||||||
generate new SolidJS component:
|
|
||||||
```
|
|
||||||
npm run hygen component new NewComponentName
|
|
||||||
```
|
|
||||||
|
|
||||||
generate new SolidJS context:
|
|
||||||
```
|
|
||||||
npm run hygen context new NewContextName
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -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": {
|
"files": {
|
||||||
"include": ["*.tsx", "*.ts", "*.js", "*.json"],
|
"include": ["*.tsx", "*.ts", "*.js", "*.json"],
|
||||||
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.gen.ts", "*.d.ts"]
|
"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",
|
"codegen": "graphql-codegen",
|
||||||
"deploy": "graphql-codegen && npm run typecheck && vite build && vercel",
|
"deploy": "graphql-codegen && npm run typecheck && vite build && vercel",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"e2e": "npx playwright test --project=chromium",
|
"e2e": "npx playwright test --project=webkit",
|
||||||
"fix": "npm run check:code:fix && stylelint **/*.{scss,css} --fix",
|
"fix": "npm run check:code:fix && stylelint **/*.{scss,css} --fix",
|
||||||
"format": "npx @biomejs/biome format src/. --write",
|
"format": "npx @biomejs/biome format src/. --write",
|
||||||
"hygen": "HYGEN_TMPLS=gen hygen",
|
|
||||||
"postinstall": "npm run codegen && npx patch-package",
|
"postinstall": "npm run codegen && npx patch-package",
|
||||||
"check:code": "npx @biomejs/biome check src --log-kind=compact --verbose",
|
"check:code": "npx @biomejs/biome check src --log-kind=compact --verbose",
|
||||||
"check:code:fix": "npx @biomejs/biome check . --apply",
|
"check:code:fix": "npx @biomejs/biome check . --apply",
|
||||||
|
@ -33,8 +32,8 @@
|
||||||
"mailgun.js": "10.1.0"
|
"mailgun.js": "10.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@authorizerdev/authorizer-js": "2.0.0",
|
"@authorizerdev/authorizer-js": "^2.0.0",
|
||||||
"@babel/core": "7.23.3",
|
"@babel/core": "^7.24.5",
|
||||||
"@biomejs/biome": "^1.7.2",
|
"@biomejs/biome": "^1.7.2",
|
||||||
"@graphql-codegen/cli": "^5.0.0",
|
"@graphql-codegen/cli": "^5.0.0",
|
||||||
"@graphql-codegen/typescript": "^4.0.1",
|
"@graphql-codegen/typescript": "^4.0.1",
|
||||||
|
@ -45,7 +44,7 @@
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
"@nanostores/router": "0.13.0",
|
"@nanostores/router": "0.13.0",
|
||||||
"@nanostores/solid": "0.4.2",
|
"@nanostores/solid": "0.4.2",
|
||||||
"@playwright/test": "1.41.2",
|
"@playwright/test": "^1.44.0",
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
"@sentry/browser": "^7.113.0",
|
"@sentry/browser": "^7.113.0",
|
||||||
"@solid-primitives/media": "2.2.3",
|
"@solid-primitives/media": "2.2.3",
|
||||||
|
@ -55,90 +54,87 @@
|
||||||
"@solid-primitives/storage": "^3.5.0",
|
"@solid-primitives/storage": "^3.5.0",
|
||||||
"@solid-primitives/upload": "0.0.115",
|
"@solid-primitives/upload": "0.0.115",
|
||||||
"@thisbeyond/solid-select": "0.14.0",
|
"@thisbeyond/solid-select": "0.14.0",
|
||||||
"@tiptap/core": "2.2.3",
|
"@tiptap/core": "2.4.0",
|
||||||
"@tiptap/extension-blockquote": "2.2.3",
|
"@tiptap/extension-blockquote": "2.4.0",
|
||||||
"@tiptap/extension-bold": "2.2.3",
|
"@tiptap/extension-bold": "2.4.0",
|
||||||
"@tiptap/extension-bubble-menu": "2.2.3",
|
"@tiptap/extension-bubble-menu": "2.4.0",
|
||||||
"@tiptap/extension-bullet-list": "2.2.3",
|
"@tiptap/extension-bullet-list": "2.4.0",
|
||||||
"@tiptap/extension-character-count": "2.2.3",
|
"@tiptap/extension-character-count": "2.4.0",
|
||||||
"@tiptap/extension-collaboration": "2.2.3",
|
"@tiptap/extension-collaboration": "2.4.0",
|
||||||
"@tiptap/extension-collaboration-cursor": "2.2.3",
|
"@tiptap/extension-collaboration-cursor": "2.4.0",
|
||||||
"@tiptap/extension-document": "2.2.3",
|
"@tiptap/extension-document": "2.4.0",
|
||||||
"@tiptap/extension-dropcursor": "2.2.3",
|
"@tiptap/extension-dropcursor": "2.4.0",
|
||||||
"@tiptap/extension-floating-menu": "2.2.3",
|
"@tiptap/extension-floating-menu": "2.4.0",
|
||||||
"@tiptap/extension-focus": "2.2.3",
|
"@tiptap/extension-focus": "2.4.0",
|
||||||
"@tiptap/extension-gapcursor": "2.2.3",
|
"@tiptap/extension-gapcursor": "2.4.0",
|
||||||
"@tiptap/extension-hard-break": "2.2.3",
|
"@tiptap/extension-hard-break": "2.4.0",
|
||||||
"@tiptap/extension-heading": "2.2.3",
|
"@tiptap/extension-heading": "2.4.0",
|
||||||
"@tiptap/extension-highlight": "2.2.3",
|
"@tiptap/extension-highlight": "2.4.0",
|
||||||
"@tiptap/extension-history": "2.2.3",
|
"@tiptap/extension-history": "2.4.0",
|
||||||
"@tiptap/extension-horizontal-rule": "2.2.3",
|
"@tiptap/extension-horizontal-rule": "2.4.0",
|
||||||
"@tiptap/extension-image": "2.2.3",
|
"@tiptap/extension-image": "2.4.0",
|
||||||
"@tiptap/extension-italic": "2.2.3",
|
"@tiptap/extension-italic": "2.4.0",
|
||||||
"@tiptap/extension-link": "2.2.3",
|
"@tiptap/extension-link": "2.4.0",
|
||||||
"@tiptap/extension-list-item": "2.2.3",
|
"@tiptap/extension-list-item": "2.4.0",
|
||||||
"@tiptap/extension-ordered-list": "2.2.3",
|
"@tiptap/extension-ordered-list": "2.4.0",
|
||||||
"@tiptap/extension-paragraph": "2.2.3",
|
"@tiptap/extension-paragraph": "2.4.0",
|
||||||
"@tiptap/extension-placeholder": "2.2.3",
|
"@tiptap/extension-placeholder": "2.4.0",
|
||||||
"@tiptap/extension-strike": "2.2.3",
|
"@tiptap/extension-strike": "2.4.0",
|
||||||
"@tiptap/extension-text": "2.2.3",
|
"@tiptap/extension-text": "2.4.0",
|
||||||
"@tiptap/extension-underline": "2.2.3",
|
"@tiptap/extension-underline": "2.4.0",
|
||||||
"@tiptap/extension-youtube": "2.2.3",
|
"@tiptap/extension-youtube": "2.4.0",
|
||||||
"@types/js-cookie": "3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
"@urql/core": "4.2.3",
|
"@urql/core": "4.2.3",
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "^2.0.3",
|
||||||
"babel-preset-solid": "1.8.4",
|
"babel-preset-solid": "1.8.17",
|
||||||
"bootstrap": "5.3.2",
|
"bootstrap": "5.3.2",
|
||||||
"clsx": "2.0.0",
|
"clsx": "2.0.0",
|
||||||
"cropperjs": "1.6.1",
|
"cropperjs": "1.6.1",
|
||||||
"cross-env": "7.0.3",
|
|
||||||
"fast-deep-equal": "3.1.3",
|
"fast-deep-equal": "3.1.3",
|
||||||
"ga-gtag": "1.2.0",
|
"ga-gtag": "1.2.0",
|
||||||
"graphql": "16.8.1",
|
"graphql": "16.8.1",
|
||||||
"graphql-tag": "2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"hygen": "6.2.11",
|
|
||||||
"i18next": "22.4.15",
|
"i18next": "22.4.15",
|
||||||
"i18next-http-backend": "2.2.0",
|
"i18next-http-backend": "2.2.0",
|
||||||
"i18next-icu": "2.3.0",
|
"i18next-icu": "2.3.0",
|
||||||
"intl-messageformat": "10.5.3",
|
"intl-messageformat": "^10.5.14",
|
||||||
"javascript-time-ago": "2.5.9",
|
"javascript-time-ago": "^2.5.10",
|
||||||
"js-cookie": "3.0.5",
|
"js-cookie": "3.0.5",
|
||||||
"lint-staged": "15.1.0",
|
"loglevel": "^1.9.1",
|
||||||
"loglevel": "1.8.1",
|
"loglevel-plugin-prefix": "^0.8.4",
|
||||||
"loglevel-plugin-prefix": "0.8.4",
|
"nanostores": "^0.9.0",
|
||||||
"nanostores": "0.9.5",
|
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"prosemirror-history": "1.3.2",
|
"prosemirror-history": "1.3.2",
|
||||||
"prosemirror-trailing-node": "2.0.7",
|
"prosemirror-trailing-node": "2.0.7",
|
||||||
"prosemirror-view": "1.32.7",
|
"prosemirror-view": "1.32.7",
|
||||||
"rollup": "4.17.2",
|
"rollup": "4.17.2",
|
||||||
"sass": "1.69.5",
|
"sass": "1.77.2",
|
||||||
"solid-js": "1.8.17",
|
"solid-js": "1.8.17",
|
||||||
"solid-popper": "0.3.0",
|
"solid-popper": "0.3.0",
|
||||||
"solid-tiptap": "0.7.0",
|
"solid-tiptap": "0.7.0",
|
||||||
"solid-transition-group": "0.2.3",
|
"solid-transition-group": "0.2.3",
|
||||||
"stylelint": "^16.0.0",
|
"stylelint": "^16.5.0",
|
||||||
"stylelint-config-standard-scss": "^13.0.0",
|
"stylelint-config-standard-scss": "^13.1.0",
|
||||||
"stylelint-order": "^6.0.3",
|
"stylelint-order": "^6.0.3",
|
||||||
"stylelint-scss": "^6.1.0",
|
"stylelint-scss": "^6.1.0",
|
||||||
"swiper": "11.0.5",
|
"swiper": "11.0.5",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.4.5",
|
||||||
"typograf": "7.3.0",
|
"typograf": "7.3.0",
|
||||||
"uniqolor": "1.1.0",
|
"uniqolor": "1.1.0",
|
||||||
"vike": "0.4.148",
|
"vike": "0.4.148",
|
||||||
"vite": "5.2.11",
|
"vite": "5.2.11",
|
||||||
"vite-plugin-mkcert": "^1.17.3",
|
"vite-plugin-mkcert": "^1.17.5",
|
||||||
"vite-plugin-node-polyfills": "0.21.0",
|
"vite-plugin-node-polyfills": "^0.22.0",
|
||||||
"vite-plugin-sass-dts": "^1.3.17",
|
"vite-plugin-sass-dts": "^1.3.22",
|
||||||
"vite-plugin-solid": "2.10.1",
|
"vite-plugin-solid": "^2.10.2",
|
||||||
"y-prosemirror": "1.2.2",
|
"y-prosemirror": "1.2.5",
|
||||||
"yjs": "13.6.12"
|
"yjs": "13.6.15"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"y-prosemirror": "1.2.2",
|
"y-prosemirror": "1.2.5",
|
||||||
"yjs": "13.6.12"
|
"yjs": "13.6.15"
|
||||||
},
|
},
|
||||||
"trustedDependencies": ["@biomejs/biome"]
|
"trustedDependencies": ["@biomejs/biome"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default defineConfig({
|
||||||
use: { ...devices['Desktop Safari'] },
|
use: { ...devices['Desktop Safari'] },
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Test against mobile viewports. */
|
/* Test against many viewports.
|
||||||
// {
|
// {
|
||||||
// name: 'Mobile Chrome',
|
// name: 'Mobile Chrome',
|
||||||
// use: { ...devices['Pixel 5'] },
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
@ -68,10 +68,10 @@ export default defineConfig({
|
||||||
// },
|
// },
|
||||||
],
|
],
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run local dev server before starting the tests */
|
||||||
// webServer: {
|
//webServer: {
|
||||||
// command: 'npm run start',
|
// command: 'npm run dev',
|
||||||
// url: 'http://127.0.0.1:3000',
|
// url: 'https://localhost:3000',
|
||||||
// reuseExistingServer: !process.env.CI,
|
// 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 [isPlaying, setIsPlaying] = createSignal(false)
|
||||||
|
|
||||||
const currentTack = createMemo(() => props.media[currentTrackIndex()])
|
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) => {
|
const handlePlayMedia = async (trackIndex: number) => {
|
||||||
setIsPlaying(!isPlaying() || trackIndex !== currentTrackIndex())
|
setIsPlaying(!isPlaying() || trackIndex !== currentTrackIndex())
|
||||||
|
|
|
@ -48,7 +48,7 @@ export const Comment = (props: Props) => {
|
||||||
const canEdit = createMemo(
|
const canEdit = createMemo(
|
||||||
() =>
|
() =>
|
||||||
Boolean(author()?.id) &&
|
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() || ''))
|
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}>
|
<Show when={props.article.stat?.viewed}>
|
||||||
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}>
|
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}>
|
||||||
{t('viewsWithCount', { count: props.article.stat?.viewed })}
|
{t('some views', { count: props.article.stat?.viewed })}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { openPage } from '@nanostores/router'
|
import { openPage } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
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 { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
@ -10,17 +10,17 @@ import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { translit } from '../../../utils/ru2en'
|
import { translit } from '../../../utils/ru2en'
|
||||||
import { isCyrillic } from '../../../utils/translate'
|
import { isCyrillic } from '../../../utils/translate'
|
||||||
import { BadgeSubscribeButton } from '../../_shared/BadgeSubscribeButton'
|
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
import { CheckButton } from '../../_shared/CheckButton'
|
import { CheckButton } from '../../_shared/CheckButton'
|
||||||
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
||||||
|
import { FollowingButton } from '../../_shared/FollowingButton'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Userpic } from '../Userpic'
|
import { Userpic } from '../Userpic'
|
||||||
import styles from './AuthorBadge.module.scss'
|
import styles from './AuthorBadge.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
author: Author
|
author: Author
|
||||||
minimizeSubscribeButton?: boolean
|
minimize?: boolean
|
||||||
showMessageButton?: boolean
|
showMessageButton?: boolean
|
||||||
iconButtons?: boolean
|
iconButtons?: boolean
|
||||||
nameOnly?: boolean
|
nameOnly?: boolean
|
||||||
|
@ -32,19 +32,21 @@ type Props = {
|
||||||
export const AuthorBadge = (props: Props) => {
|
export const AuthorBadge = (props: Props) => {
|
||||||
const { mediaMatches } = useMediaQuery()
|
const { mediaMatches } = useMediaQuery()
|
||||||
const { author, requireAuthentication } = useSession()
|
const { author, requireAuthentication } = useSession()
|
||||||
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
|
const { follow, unfollow, follows, following } = useFollowing()
|
||||||
const [isMobileView, setIsMobileView] = createSignal(false)
|
const [isMobileView, setIsMobileView] = createSignal(false)
|
||||||
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
|
const [isFollowed, setIsFollowed] = createSignal<boolean>(
|
||||||
|
follows?.authors?.some((authorEntity) => authorEntity.id === props.author?.id),
|
||||||
createEffect(() => {
|
)
|
||||||
if (!(subscriptions && props.author)) return
|
createEffect(() => setIsMobileView(!mediaMatches.sm))
|
||||||
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
|
createEffect(
|
||||||
setIsSubscribed(subscribed)
|
on(
|
||||||
})
|
[() => follows?.authors, () => props.author, following],
|
||||||
|
([followingAuthors, currentAuthor, _]) => {
|
||||||
createEffect(() => {
|
setIsFollowed(followingAuthors?.some((followedAuthor) => followedAuthor.id === currentAuthor?.id))
|
||||||
setIsMobileView(!mediaMatches.sm)
|
},
|
||||||
})
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const { changeSearchParams } = useRouter()
|
const { changeSearchParams } = useRouter()
|
||||||
const { t, formatDate, lang } = useLocalize()
|
const { t, formatDate, lang } = useLocalize()
|
||||||
|
@ -72,11 +74,10 @@ export const AuthorBadge = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleFollowClick = () => {
|
const handleFollowClick = () => {
|
||||||
requireAuthentication(() => {
|
requireAuthentication(async () => {
|
||||||
isSubscribed()
|
const handle = isFollowed() ? unfollow : follow
|
||||||
? unfollow(FollowingEntity.Author, props.author.slug)
|
await handle(FollowingEntity.Author, props.author.slug)
|
||||||
: follow(FollowingEntity.Author, props.author.slug)
|
}, 'follow')
|
||||||
}, 'subscribe')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -117,13 +118,13 @@ export const AuthorBadge = (props: Props) => {
|
||||||
<Show when={props.author?.stat && !props.subscriptionsMode}>
|
<Show when={props.author?.stat && !props.subscriptionsMode}>
|
||||||
<div class={styles.bio}>
|
<div class={styles.bio}>
|
||||||
<Show when={props.author?.stat.shouts > 0}>
|
<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>
|
||||||
<Show when={props.author?.stat.comments > 0}>
|
<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>
|
||||||
<Show when={props.author?.stat.followers > 0}>
|
<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>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -132,12 +133,10 @@ export const AuthorBadge = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.author.slug !== author()?.slug && !props.nameOnly}>
|
<Show when={props.author.slug !== author()?.slug && !props.nameOnly}>
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
<BadgeSubscribeButton
|
<FollowingButton
|
||||||
action={() => handleFollowClick()}
|
action={handleFollowClick}
|
||||||
isSubscribed={isSubscribed()}
|
isFollowed={isFollowed()}
|
||||||
actionMessageType={
|
actionMessageType={following()?.slug === props.author.slug ? following().type : undefined}
|
||||||
subscribeInAction()?.slug === props.author.slug ? subscribeInAction().type : undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Show when={props.showMessageButton}>
|
<Show when={props.showMessageButton}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
@include font-size(4rem);
|
@include font-size(4rem);
|
||||||
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 0.2em;
|
margin-bottom: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorAbout {
|
.authorAbout {
|
||||||
|
@ -429,64 +429,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listWrapper {
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
.subscribersContainer {
|
.subscribersContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
margin-top: 1.5rem;
|
gap: 1rem;
|
||||||
|
margin-top: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(md) {
|
||||||
justify-content: center;
|
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 { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
|
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 { router, useRouter } from '../../../stores/router'
|
||||||
import { isAuthor } from '../../../utils/isAuthor'
|
import { isAuthor } from '../../../utils/isAuthor'
|
||||||
import { translit } from '../../../utils/ru2en'
|
import { translit } from '../../../utils/ru2en'
|
||||||
|
@ -17,6 +17,7 @@ import { SharePopup, getShareUrl } from '../../Article/SharePopup'
|
||||||
import { Modal } from '../../Nav/Modal'
|
import { Modal } from '../../Nav/Modal'
|
||||||
import { TopicBadge } from '../../Topic/TopicBadge'
|
import { TopicBadge } from '../../Topic/TopicBadge'
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
|
import { FollowingCounters } from '../../_shared/FollowingCounters/FollowingCounters'
|
||||||
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
|
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
|
||||||
import { AuthorBadge } from '../AuthorBadge'
|
import { AuthorBadge } from '../AuthorBadge'
|
||||||
import { Userpic } from '../Userpic'
|
import { Userpic } from '../Userpic'
|
||||||
|
@ -27,25 +28,25 @@ import styles from './AuthorCard.module.scss'
|
||||||
type Props = {
|
type Props = {
|
||||||
author: Author
|
author: Author
|
||||||
followers?: Author[]
|
followers?: Author[]
|
||||||
following?: Array<Author | Topic>
|
flatFollows?: Array<Author | Topic>
|
||||||
}
|
}
|
||||||
export const AuthorCard = (props: Props) => {
|
export const AuthorCard = (props: Props) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const { author, isSessionLoaded, requireAuthentication } = useSession()
|
const { author, isSessionLoaded, requireAuthentication } = useSession()
|
||||||
const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([])
|
const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([])
|
||||||
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
const [followsFilter, setFollowsFilter] = createSignal<FollowsFilter>('all')
|
||||||
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
|
const [isFollowed, setIsFollowed] = createSignal<boolean>()
|
||||||
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
|
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
|
||||||
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
|
const { follow, unfollow, follows, following } = useFollowing()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setAuthorSubs(props.following)
|
setAuthorSubs(props.flatFollows)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!(subscriptions && props.author)) return
|
if (!(follows && props.author)) return
|
||||||
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
|
const followed = follows?.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
|
||||||
setIsSubscribed(subscribed)
|
setIsFollowed(followed)
|
||||||
})
|
})
|
||||||
|
|
||||||
const name = createMemo(() => {
|
const name = createMemo(() => {
|
||||||
|
@ -71,33 +72,33 @@ export const AuthorCard = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.following) {
|
if (props.flatFollows) {
|
||||||
if (subscriptionFilter() === 'authors') {
|
if (followsFilter() === 'authors') {
|
||||||
setAuthorSubs(props.following.filter((s) => 'name' in s))
|
setAuthorSubs(props.flatFollows.filter((s) => 'name' in s))
|
||||||
} else if (subscriptionFilter() === 'topics') {
|
} else if (followsFilter() === 'topics') {
|
||||||
setAuthorSubs(props.following.filter((s) => 'title' in s))
|
setAuthorSubs(props.flatFollows.filter((s) => 'title' in s))
|
||||||
} else if (subscriptionFilter() === 'communities') {
|
} else if (followsFilter() === 'communities') {
|
||||||
setAuthorSubs(props.following.filter((s) => 'title' in s))
|
setAuthorSubs(props.flatFollows.filter((s) => 'title' in s))
|
||||||
} else {
|
} else {
|
||||||
setAuthorSubs(props.following)
|
setAuthorSubs(props.flatFollows)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleFollowClick = () => {
|
const handleFollowClick = () => {
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
isSubscribed()
|
isFollowed()
|
||||||
? unfollow(FollowingEntity.Author, props.author.slug)
|
? unfollow(FollowingEntity.Author, props.author.slug)
|
||||||
: follow(FollowingEntity.Author, props.author.slug)
|
: follow(FollowingEntity.Author, props.author.slug)
|
||||||
}, 'subscribe')
|
}, 'follow')
|
||||||
}
|
}
|
||||||
|
|
||||||
const followButtonText = createMemo(() => {
|
const followButtonText = createMemo(() => {
|
||||||
if (subscribeInAction()?.slug === props.author.slug) {
|
if (following()?.slug === props.author.slug) {
|
||||||
return subscribeInAction().type === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...')
|
return following().type === 'follow' ? t('Following...') : t('Unfollowing...')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSubscribed()) {
|
if (isFollowed()) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
||||||
|
@ -108,6 +109,73 @@ export const AuthorCard = (props: Props) => {
|
||||||
return t('Follow')
|
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 (
|
return (
|
||||||
<div class={clsx(styles.author, 'row')}>
|
<div class={clsx(styles.author, 'row')}>
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
|
@ -125,59 +193,14 @@ export const AuthorCard = (props: Props) => {
|
||||||
<Show when={props.author.bio}>
|
<Show when={props.author.bio}>
|
||||||
<div class={styles.authorAbout} innerHTML={props.author.bio} />
|
<div class={styles.authorAbout} innerHTML={props.author.bio} />
|
||||||
</Show>
|
</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}>
|
<div class={styles.subscribersContainer}>
|
||||||
<Show when={props.followers && props.followers.length > 0}>
|
<FollowingCounters
|
||||||
<a href="?m=followers" class={styles.subscribers}>
|
followers={props.followers}
|
||||||
<For each={props.followers.slice(0, 3)}>
|
followersAmount={props.author?.stat?.followers}
|
||||||
{(f) => (
|
following={props.flatFollows}
|
||||||
<Userpic size={'XS'} name={f.name} userpic={f.pic} class={styles.subscribersItem} />
|
followingAmount={props.flatFollows.length}
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
@ -208,11 +231,11 @@ export const AuthorCard = (props: Props) => {
|
||||||
<Show when={authorSubs()?.length}>
|
<Show when={authorSubs()?.length}>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleFollowClick}
|
onClick={handleFollowClick}
|
||||||
disabled={Boolean(subscribeInAction())}
|
disabled={Boolean(following())}
|
||||||
value={followButtonText()}
|
value={followButtonText()}
|
||||||
isSubscribeButton={true}
|
isSubscribeButton={true}
|
||||||
class={clsx({
|
class={clsx({
|
||||||
[stylesButton.subscribed]: isSubscribed(),
|
[stylesButton.followed]: isFollowed(),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -251,77 +274,12 @@ export const AuthorCard = (props: Props) => {
|
||||||
</ShowOnlyOnClient>
|
</ShowOnlyOnClient>
|
||||||
<Show when={props.followers}>
|
<Show when={props.followers}>
|
||||||
<Modal variant="medium" isResponsive={true} name="followers" maxHeight>
|
<Modal variant="medium" isResponsive={true} name="followers" maxHeight>
|
||||||
<>
|
<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>
|
|
||||||
</>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.following}>
|
<Show when={props.flatFollows}>
|
||||||
<Modal variant="medium" isResponsive={true} name="following" maxHeight>
|
<Modal variant="medium" isResponsive={true} name="following" maxHeight>
|
||||||
<>
|
<FollowingModalView />
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { For, createMemo } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { Subscribe } from '../_shared/Subscribe'
|
import { Newsletter } from '../_shared/Newsletter'
|
||||||
|
|
||||||
import styles from './Footer.module.scss'
|
import styles from './Footer.module.scss'
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ export const Footer = () => {
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h5>{t('Subscription')}</h5>
|
<h5>{t('Subscription')}</h5>
|
||||||
<p>{t('Join our maillist')}</p>
|
<p>{t('Join our maillist')}</p>
|
||||||
<Subscribe />
|
<Newsletter />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ export const AudioUploader = (props: Props) => {
|
||||||
|
|
||||||
const handleChangeIndex = (direction: 'up' | 'down', index: number) => {
|
const handleChangeIndex = (direction: 'up' | 'down', index: number) => {
|
||||||
const media = [...props.audio]
|
const media = [...props.audio]
|
||||||
|
if (media?.length > 0) {
|
||||||
if (direction === 'up' && index > 0) {
|
if (direction === 'up' && index > 0) {
|
||||||
const copy = media.splice(index, 1)[0]
|
const copy = media.splice(index, 1)[0]
|
||||||
media.splice(index - 1, 0, copy)
|
media.splice(index - 1, 0, copy)
|
||||||
|
@ -41,6 +42,7 @@ export const AudioUploader = (props: Props) => {
|
||||||
const copy = media.splice(index, 1)[0]
|
const copy = media.splice(index, 1)[0]
|
||||||
media.splice(index + 1, 0, copy)
|
media.splice(index + 1, 0, copy)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
props.onAudioSorted(media)
|
props.onAudioSorted(media)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,6 @@ declare module '@tiptap/core' {
|
||||||
|
|
||||||
export default Node.create({
|
export default Node.create({
|
||||||
name: 'article',
|
name: 'article',
|
||||||
defaultOptions: {
|
|
||||||
HTMLAttributes: {
|
|
||||||
'data-type': 'incut',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
group: 'block',
|
group: 'block',
|
||||||
content: 'block+',
|
content: 'block+',
|
||||||
|
|
||||||
|
@ -32,6 +27,12 @@ export default Node.create({
|
||||||
return ['article', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
return ['article', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
'data-type': 'incut',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
'data-float': {
|
'data-float': {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Blockquote } from '@tiptap/extension-blockquote'
|
import { Blockquote, BlockquoteOptions } from '@tiptap/extension-blockquote'
|
||||||
|
|
||||||
export type QuoteTypes = 'quote' | 'punchline'
|
export type QuoteTypes = 'quote' | 'punchline'
|
||||||
|
|
||||||
|
@ -13,11 +13,13 @@ declare module '@tiptap/core' {
|
||||||
|
|
||||||
export const CustomBlockquote = Blockquote.extend({
|
export const CustomBlockquote = Blockquote.extend({
|
||||||
name: 'blockquote',
|
name: 'blockquote',
|
||||||
defaultOptions: {
|
|
||||||
HTMLAttributes: {},
|
|
||||||
},
|
|
||||||
group: 'block',
|
group: 'block',
|
||||||
content: 'block+',
|
content: 'block+',
|
||||||
|
|
||||||
|
addOptions(): BlockquoteOptions {
|
||||||
|
return {} as BlockquoteOptions
|
||||||
|
},
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
'data-float': {
|
'data-float': {
|
||||||
|
@ -34,14 +36,12 @@ export const CustomBlockquote = Blockquote.extend({
|
||||||
return {
|
return {
|
||||||
toggleBlockquote:
|
toggleBlockquote:
|
||||||
(type) =>
|
(type) =>
|
||||||
({ commands }) => {
|
({ commands }) =>
|
||||||
return commands.toggleWrap(this.name, { 'data-type': type })
|
commands.toggleWrap(this.name, { 'data-type': type }),
|
||||||
},
|
|
||||||
setBlockQuoteFloat:
|
setBlockQuoteFloat:
|
||||||
(value) =>
|
(value) =>
|
||||||
({ commands }) => {
|
({ commands }) =>
|
||||||
return commands.updateAttributes(this.name, { 'data-float': value })
|
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 = () => {
|
export const Sidebar = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { seen } = useSeen()
|
const { seen } = useSeen()
|
||||||
const { subscriptions } = useFollowing()
|
const { follows } = useFollowing()
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const { articlesByTopic, articlesByAuthor } = useArticlesStore()
|
const { articlesByTopic, articlesByAuthor } = useArticlesStore()
|
||||||
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
|
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
|
||||||
|
@ -83,35 +83,9 @@ export const Sidebar = () => {
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
|
|
||||||
<Show when={subscriptions.authors.length > 0 || subscriptions.topics.length > 0}>
|
<Show when={follows?.authors?.length > 0 || follows?.topics?.length > 0}>
|
||||||
<h4
|
<h4
|
||||||
classList={{ [styles.opened]: isSubscriptionsVisible() }}
|
classList={{ [styles.opened]: isSubscriptionsVisible() }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -123,7 +97,7 @@ export const Sidebar = () => {
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}>
|
<ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}>
|
||||||
<For each={subscriptions.authors}>
|
<For each={follows.authors}>
|
||||||
{(a: Author) => (
|
{(a: Author) => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/author/${a.slug}`} classList={{ [styles.unread]: checkAuthorIsSeen(a.slug) }}>
|
<a href={`/author/${a.slug}`} classList={{ [styles.unread]: checkAuthorIsSeen(a.slug) }}>
|
||||||
|
@ -135,7 +109,7 @@ export const Sidebar = () => {
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<For each={subscriptions.topics}>
|
<For each={follows.topics}>
|
||||||
{(topic) => (
|
{(topic) => (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -63,18 +63,8 @@ export const PasswordField = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(
|
createEffect(on(error, (er) => er && props.errorMessage?.(er), { defer: true }))
|
||||||
on(
|
createEffect(() => setError(props.setError))
|
||||||
() => error(),
|
|
||||||
() => {
|
|
||||||
props.errorMessage?.(error())
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
createEffect(() => {
|
|
||||||
setError(props.setError)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.PassportField, props.class)}>
|
<div class={clsx(styles.PassportField, props.class)}>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { useModalStore } from '../../../stores/ui'
|
||||||
import { getDescription } from '../../../utils/meta'
|
import { getDescription } from '../../../utils/meta'
|
||||||
import { SharePopup, getShareUrl } from '../../Article/SharePopup'
|
import { SharePopup, getShareUrl } from '../../Article/SharePopup'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Subscribe } from '../../_shared/Subscribe'
|
import { Newsletter } from '../../_shared/Newsletter'
|
||||||
import { AuthModal } from '../AuthModal'
|
import { AuthModal } from '../AuthModal'
|
||||||
import { ConfirmModal } from '../ConfirmModal'
|
import { ConfirmModal } from '../ConfirmModal'
|
||||||
import { HeaderAuth } from '../HeaderAuth'
|
import { HeaderAuth } from '../HeaderAuth'
|
||||||
|
@ -301,7 +301,7 @@ export const Header = (props: Props) => {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h4>{t('Newsletter')}</h4>
|
<h4>{t('Newsletter')}</h4>
|
||||||
<Subscribe variant={'mobileSubscription'} />
|
<Newsletter variant={'mobileSubscription'} />
|
||||||
|
|
||||||
<h4>{t('Language')}</h4>
|
<h4>{t('Language')}</h4>
|
||||||
<select
|
<select
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
<Popup {...props} horizontalAnchor="right" popupCssClass={styles.profilePopup}>
|
<Popup {...props} horizontalAnchor="right" popupCssClass={styles.profilePopup}>
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<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} />
|
<Icon name="profile" class={styles.icon} />
|
||||||
{t('Profile')}
|
{t('Profile')}
|
||||||
</a>
|
</a>
|
||||||
|
@ -35,7 +35,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class={styles.action}
|
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} />
|
<Icon name="feed-all" class={styles.icon} />
|
||||||
{t('Subscriptions')}
|
{t('Subscriptions')}
|
||||||
|
@ -44,7 +44,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class={styles.action}
|
class={styles.action}
|
||||||
href={`${getPagePath(router, 'authorComments', { slug: author().slug })}`}
|
href={`${getPagePath(router, 'authorComments', { slug: author()?.slug })}`}
|
||||||
>
|
>
|
||||||
<Icon name="comment" class={styles.icon} />
|
<Icon name="comment" class={styles.icon} />
|
||||||
{t('Comments')}
|
{t('Comments')}
|
||||||
|
|
|
@ -57,7 +57,7 @@ export const ProfileSettings = () => {
|
||||||
const [nameError, setNameError] = createSignal<string>()
|
const [nameError, setNameError] = createSignal<string>()
|
||||||
const { form, submit, updateFormField, setForm } = useProfileForm()
|
const { form, submit, updateFormField, setForm } = useProfileForm()
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
const { loadAuthor, session } = useSession()
|
const { loadSession, session } = useSession()
|
||||||
const { showConfirm } = useConfirm()
|
const { showConfirm } = useConfirm()
|
||||||
const [clearAbout, setClearAbout] = createSignal(false)
|
const [clearAbout, setClearAbout] = createSignal(false)
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ export const ProfileSettings = () => {
|
||||||
setIsSaving(false)
|
setIsSaving(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadAuthor() // renews author's profile
|
setTimeout(loadSession, 5000) // renews author's profile
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
|
|
|
@ -62,7 +62,7 @@ export const TableOfContents = (props: Props) => {
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.body,
|
() => props.body,
|
||||||
() => debouncedUpdateHeadings(),
|
(_) => debouncedUpdateHeadings(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -123,12 +123,12 @@
|
||||||
width: 9em;
|
width: 9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.isSubscribing {
|
.isFollowing {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
.isSubscribed {
|
.isFollowed {
|
||||||
background: #000;
|
background: #000;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
transition:
|
transition:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { clsx } from 'clsx'
|
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 { useFollowing } from '../../context/following'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
@ -7,18 +7,16 @@ import { useSession } from '../../context/session'
|
||||||
import { FollowingEntity, type Topic } from '../../graphql/schema/core.gen'
|
import { FollowingEntity, type Topic } from '../../graphql/schema/core.gen'
|
||||||
import { capitalize } from '../../utils/capitalize'
|
import { capitalize } from '../../utils/capitalize'
|
||||||
import { CardTopic } from '../Feed/CardTopic'
|
import { CardTopic } from '../Feed/CardTopic'
|
||||||
import { Button } from '../_shared/Button'
|
|
||||||
import { CheckButton } from '../_shared/CheckButton'
|
import { CheckButton } from '../_shared/CheckButton'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { FollowingButton } from '../_shared/FollowingButton'
|
||||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||||
|
|
||||||
import stylesButton from '../_shared/Button/Button.module.scss'
|
|
||||||
import styles from './Card.module.scss'
|
import styles from './Card.module.scss'
|
||||||
|
|
||||||
interface TopicProps {
|
interface TopicProps {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
subscribed?: boolean
|
followed?: boolean
|
||||||
shortDescription?: boolean
|
shortDescription?: boolean
|
||||||
subscribeButtonBottom?: boolean
|
subscribeButtonBottom?: boolean
|
||||||
additionalClass?: string
|
additionalClass?: string
|
||||||
|
@ -27,7 +25,7 @@ interface TopicProps {
|
||||||
showPublications?: boolean
|
showPublications?: boolean
|
||||||
showDescription?: boolean
|
showDescription?: boolean
|
||||||
isCardMode?: boolean
|
isCardMode?: boolean
|
||||||
minimizeSubscribeButton?: boolean
|
minimize?: boolean
|
||||||
isNarrow?: boolean
|
isNarrow?: boolean
|
||||||
withIcon?: boolean
|
withIcon?: boolean
|
||||||
}
|
}
|
||||||
|
@ -38,39 +36,23 @@ export const TopicCard = (props: TopicProps) => {
|
||||||
capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''),
|
capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''),
|
||||||
)
|
)
|
||||||
const { author, requireAuthentication } = useSession()
|
const { author, requireAuthentication } = useSession()
|
||||||
const [isSubscribed, setIsSubscribed] = createSignal()
|
const { follow, unfollow, follows } = useFollowing()
|
||||||
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
|
const [isFollowed, setIsFollowed] = createSignal(false)
|
||||||
|
createEffect(
|
||||||
createEffect(() => {
|
on([() => follows, () => props.topic], ([flws, tpc]) => {
|
||||||
if (!(subscriptions && props.topic)) return
|
if (flws && tpc) {
|
||||||
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
|
const followed = follows?.topics?.some((topics) => topics.id === props.topic?.id)
|
||||||
setIsSubscribed(subscribed)
|
setIsFollowed(followed)
|
||||||
})
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const handleFollowClick = () => {
|
const handleFollowClick = () => {
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
isSubscribed()
|
isFollowed()
|
||||||
? unfollow(FollowingEntity.Topic, props.topic.slug)
|
? unfollow(FollowingEntity.Topic, props.topic.slug)
|
||||||
: follow(FollowingEntity.Topic, props.topic.slug)
|
: follow(FollowingEntity.Topic, props.topic.slug)
|
||||||
}, 'subscribe')
|
}, 'follow')
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -132,27 +114,12 @@ export const TopicCard = (props: TopicProps) => {
|
||||||
<ShowOnlyOnClient>
|
<ShowOnlyOnClient>
|
||||||
<Show when={author()}>
|
<Show when={author()}>
|
||||||
<Show
|
<Show
|
||||||
when={!props.minimizeSubscribeButton}
|
when={!props.minimize}
|
||||||
fallback={
|
fallback={
|
||||||
<CheckButton
|
<CheckButton text={t('Follow')} checked={isFollowed()} onClick={handleFollowClick} />
|
||||||
text={t('Follow')}
|
|
||||||
checked={Boolean(isSubscribed())}
|
|
||||||
onClick={handleFollowClick}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<FollowingButton action={handleFollowClick} isFollowed={isFollowed()} />
|
||||||
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(),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</ShowOnlyOnClient>
|
</ShowOnlyOnClient>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
.topicHeader {
|
.topicHeader {
|
||||||
@include font-size(1.7rem);
|
font-weight: 500;
|
||||||
|
|
||||||
padding: 2.8rem $container-padding-x 0;
|
padding: 2.8rem $container-padding-x 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
@ -12,10 +11,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topicDescription {
|
||||||
|
@include font-size(1.8rem);
|
||||||
|
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 1rem 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.topicActions {
|
.topicActions {
|
||||||
margin-top: 2.8rem;
|
margin-top: 2.8rem;
|
||||||
|
|
||||||
.write {
|
.writeControl {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -23,13 +29,38 @@
|
||||||
min-width: 64px;
|
min-width: 64px;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background: var(--background-color-invert);
|
border: 1px solid #f7f7f7;
|
||||||
color: var(--default-color-invert);
|
background: #f7f7f7;
|
||||||
border: none;
|
color: var(--default-color);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: 2px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0 1.2rem 1em;
|
margin: 0 1.2rem 1em;
|
||||||
white-space: nowrap;
|
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 { clsx } from 'clsx'
|
||||||
import { Show, createEffect, createSignal } from 'solid-js'
|
import { Show, createEffect, createSignal } from 'solid-js'
|
||||||
|
@ -9,22 +9,25 @@ import { useSession } from '../../context/session'
|
||||||
import { FollowingEntity } from '../../graphql/schema/core.gen'
|
import { FollowingEntity } from '../../graphql/schema/core.gen'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
|
|
||||||
|
import { FollowingCounters } from '../_shared/FollowingCounters/FollowingCounters'
|
||||||
|
import { Icon } from '../_shared/Icon'
|
||||||
import styles from './Full.module.scss'
|
import styles from './Full.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
|
followers?: Author[]
|
||||||
|
authors?: Author[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FullTopic = (props: Props) => {
|
export const FullTopic = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { subscriptions, setFollowing } = useFollowing()
|
const { follows, changeFollowing } = useFollowing()
|
||||||
const { requireAuthentication } = useSession()
|
const { requireAuthentication } = useSession()
|
||||||
const [followed, setFollowed] = createSignal()
|
const [followed, setFollowed] = createSignal()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const subs = subscriptions
|
if (follows?.topics.length !== 0) {
|
||||||
if (subs?.topics.length !== 0) {
|
const items = follows.topics || []
|
||||||
const items = subs.topics || []
|
|
||||||
setFollowed(items.some((x: Topic) => x?.slug === props.topic?.slug))
|
setFollowed(items.some((x: Topic) => x?.slug === props.topic?.slug))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -33,26 +36,46 @@ export const FullTopic = (props: Props) => {
|
||||||
const really = !followed()
|
const really = !followed()
|
||||||
setFollowed(really)
|
setFollowed(really)
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
setFollowing(FollowingEntity.Topic, props.topic.slug, really)
|
changeFollowing(FollowingEntity.Topic, props.topic.slug, really)
|
||||||
}, 'follow')
|
}, 'follow')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
|
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
|
||||||
<h1>#{props.topic?.title}</h1>
|
<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)}>
|
<div class={clsx(styles.topicActions)}>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handleFollowClick}
|
onClick={handleFollowClick}
|
||||||
value={followed() ? t('Unfollow the topic') : t('Follow the topic')}
|
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')}
|
{t('Write about the topic')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.topic?.pic}>
|
<Show when={props.topic?.pic}>
|
||||||
<img src={props.topic.pic} alt={props.topic?.title} />
|
<img src={props.topic?.pic} alt={props.topic?.title} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
// display: flex;
|
// display: flex;
|
||||||
|
@ -62,11 +63,13 @@
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@include font-size(2.2rem);
|
@include font-size(2.2rem);
|
||||||
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
@include font-size(1.6rem);
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin: 0.8rem 0;
|
margin: 0.8rem 0;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
@ -104,6 +107,7 @@
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
color: var(--blue-500);
|
color: var(--blue-500);
|
||||||
|
@ -111,8 +115,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
color: var(--black-400);
|
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
|
|
||||||
|
color: var(--black-400);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { clsx } from 'clsx'
|
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 { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
@ -8,12 +8,12 @@ import { useSession } from '../../../context/session'
|
||||||
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
|
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { capitalize } from '../../../utils/capitalize'
|
import { capitalize } from '../../../utils/capitalize'
|
||||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
import { BadgeSubscribeButton } from '../../_shared/BadgeSubscribeButton'
|
import { FollowingButton } from '../../_shared/FollowingButton'
|
||||||
import styles from './TopicBadge.module.scss'
|
import styles from './TopicBadge.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
minimizeSubscribeButton?: boolean
|
minimize?: boolean
|
||||||
showStat?: boolean
|
showStat?: boolean
|
||||||
subscriptionsMode?: boolean
|
subscriptionsMode?: boolean
|
||||||
}
|
}
|
||||||
|
@ -23,18 +23,21 @@ export const TopicBadge = (props: Props) => {
|
||||||
const { mediaMatches } = useMediaQuery()
|
const { mediaMatches } = useMediaQuery()
|
||||||
const [isMobileView, setIsMobileView] = createSignal(false)
|
const [isMobileView, setIsMobileView] = createSignal(false)
|
||||||
const { requireAuthentication } = useSession()
|
const { requireAuthentication } = useSession()
|
||||||
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
|
const [isFollowed, setIsFollowed] = createSignal<boolean>()
|
||||||
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
|
const { follow, unfollow, follows, following } = useFollowing()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(
|
||||||
if (!(subscriptions && props.topic)) return
|
on([() => follows, () => props.topic], ([flws, tpc]) => {
|
||||||
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
|
if (flws && tpc) {
|
||||||
setIsSubscribed(subscribed)
|
const followed = follows?.topics?.some((topics) => topics.id === props.topic?.id)
|
||||||
})
|
setIsFollowed(followed)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const handleFollowClick = () => {
|
const handleFollowClick = () => {
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
isSubscribed()
|
isFollowed()
|
||||||
? follow(FollowingEntity.Topic, props.topic.slug)
|
? follow(FollowingEntity.Topic, props.topic.slug)
|
||||||
: unfollow(FollowingEntity.Topic, props.topic.slug)
|
: unfollow(FollowingEntity.Topic, props.topic.slug)
|
||||||
}, 'subscribe')
|
}, 'subscribe')
|
||||||
|
@ -73,7 +76,7 @@ export const TopicBadge = (props: Props) => {
|
||||||
when={props.topic.body}
|
when={props.topic.body}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={styles.description}>
|
<div class={styles.description}>
|
||||||
{t('PublicationsWithCount', { count: props.topic?.stat?.shouts ?? 0 })}
|
{t('some posts', { count: props.topic?.stat?.shouts ?? 0 })}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -82,28 +85,24 @@ export const TopicBadge = (props: Props) => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
<BadgeSubscribeButton
|
<FollowingButton
|
||||||
isSubscribed={isSubscribed()}
|
isFollowed={isFollowed()}
|
||||||
action={handleFollowClick}
|
action={handleFollowClick}
|
||||||
actionMessageType={
|
actionMessageType={following()?.slug === props.topic.slug ? following().type : undefined}
|
||||||
subscribeInAction()?.slug === props.topic.slug ? subscribeInAction().type : undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!props.subscriptionsMode}>
|
<Show when={!props.subscriptionsMode}>
|
||||||
<div class={styles.stats}>
|
<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}>
|
<span class={styles.statsItem}>
|
||||||
{t('authorsWithCount', { count: props.topic?.stat?.authors })}
|
{t('some followers', { count: props.topic?.stat?.followers })}
|
||||||
</span>
|
|
||||||
<span class={styles.statsItem}>
|
|
||||||
{t('FollowersWithCount', { count: props.topic?.stat?.followers })}
|
|
||||||
</span>
|
</span>
|
||||||
<Show when={props.topic?.stat?.comments}>
|
<Show when={props.topic?.stat?.comments}>
|
||||||
<span class={styles.statsItem}>
|
<span class={styles.statsItem}>
|
||||||
{t('CommentsWithCount', { count: props.topic?.stat?.comments ?? 0 })}
|
{t('some comments', { count: props.topic?.stat?.comments ?? 0 })}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,32 +1,30 @@
|
||||||
import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/core.gen'
|
|
||||||
|
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { useFollowing } from '../../../context/following'
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Meta, Title } from '../../../context/meta'
|
import { Meta, Title } from '../../../context/meta'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { apiClient } from '../../../graphql/client/core'
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
|
import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
|
import { MODALS, hideModal } from '../../../stores/ui'
|
||||||
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
||||||
import { loadAuthor } from '../../../stores/zine/authors'
|
import { loadAuthor } from '../../../stores/zine/authors'
|
||||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
import { getDescription } from '../../../utils/meta'
|
import { getDescription } from '../../../utils/meta'
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
|
||||||
|
import { byCreated } from '../../../utils/sortby'
|
||||||
import { splitToPages } from '../../../utils/splitToPages'
|
import { splitToPages } from '../../../utils/splitToPages'
|
||||||
|
import stylesArticle from '../../Article/Article.module.scss'
|
||||||
import { Comment } from '../../Article/Comment'
|
import { Comment } from '../../Article/Comment'
|
||||||
import { AuthorCard } from '../../Author/AuthorCard'
|
import { AuthorCard } from '../../Author/AuthorCard'
|
||||||
import { AuthorShoutsRating } from '../../Author/AuthorShoutsRating'
|
import { AuthorShoutsRating } from '../../Author/AuthorShoutsRating'
|
||||||
|
import { Placeholder } from '../../Feed/Placeholder'
|
||||||
import { Row1 } from '../../Feed/Row1'
|
import { Row1 } from '../../Feed/Row1'
|
||||||
import { Row2 } from '../../Feed/Row2'
|
import { Row2 } from '../../Feed/Row2'
|
||||||
import { Row3 } from '../../Feed/Row3'
|
import { Row3 } from '../../Feed/Row3'
|
||||||
import { Loading } from '../../_shared/Loading'
|
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'
|
import styles from './Author.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -34,71 +32,26 @@ type Props = {
|
||||||
shouts?: Shout[]
|
shouts?: Shout[]
|
||||||
author?: Author
|
author?: Author
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PRERENDERED_ARTICLES_COUNT = 12
|
export const PRERENDERED_ARTICLES_COUNT = 12
|
||||||
const LOAD_MORE_PAGE_SIZE = 9
|
const LOAD_MORE_PAGE_SIZE = 9
|
||||||
|
|
||||||
export const AuthorView = (props: Props) => {
|
export const AuthorView = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { followers: myFollowers } = useFollowing()
|
const { followers: myFollowers, follows: myFollows } = useFollowing()
|
||||||
const { session } = useSession()
|
const { author: me } = useSession()
|
||||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||||
const { page: getPage, searchParams } = useRouter()
|
const { page: getPage, searchParams } = useRouter()
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
||||||
const [author, setAuthor] = createSignal<Author>()
|
const [author, setAuthor] = createSignal<Author>(props.author)
|
||||||
const [followers, setFollowers] = createSignal([])
|
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 [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
|
||||||
const [commented, setCommented] = createSignal<Reaction[]>()
|
const [commented, setCommented] = createSignal<Reaction[]>()
|
||||||
const modal = MODALS[searchParams().m]
|
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 () => {
|
const loadMore = async () => {
|
||||||
saveScrollPosition()
|
saveScrollPosition()
|
||||||
const { hasMore } = await loadShouts({
|
const { hasMore } = await loadShouts({
|
||||||
|
@ -110,36 +63,72 @@ export const AuthorView = (props: Props) => {
|
||||||
restoreScrollPosition()
|
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(() => {
|
onMount(() => {
|
||||||
if (!modal) hideModal()
|
if (!modal) hideModal()
|
||||||
fetchData(props.authorSlug)
|
|
||||||
checkBioHeight()
|
checkBioHeight()
|
||||||
loadMore()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const pages = createMemo<Shout[][]>(() =>
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE),
|
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(() =>
|
const ogImage = createMemo(() =>
|
||||||
author()?.pic
|
author()?.pic
|
||||||
? getImageUrl(author()?.pic, { width: 1200 })
|
? getImageUrl(author()?.pic, { width: 1200 })
|
||||||
|
@ -168,16 +157,12 @@ export const AuthorView = (props: Props) => {
|
||||||
<Show when={author()} fallback={<Loading />}>
|
<Show when={author()} fallback={<Loading />}>
|
||||||
<>
|
<>
|
||||||
<div class={styles.authorHeader}>
|
<div class={styles.authorHeader}>
|
||||||
<AuthorCard author={author()} followers={followers() || []} following={following() || []} />
|
<AuthorCard author={author()} followers={followers() || []} flatFollows={following() || []} />
|
||||||
</div>
|
</div>
|
||||||
<div class={clsx(styles.groupControls, 'row')}>
|
<div class={clsx(styles.groupControls, 'row')}>
|
||||||
<div class="col-md-16">
|
<div class="col-md-16">
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li
|
<li classList={{ 'view-switcher__item--selected': getPage().route === 'author' }}>
|
||||||
classList={{
|
|
||||||
'view-switcher__item--selected': getPage().route === 'author',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
href={getPagePath(router, 'author', {
|
href={getPagePath(router, 'author', {
|
||||||
slug: props.authorSlug,
|
slug: props.authorSlug,
|
||||||
|
@ -189,11 +174,7 @@ export const AuthorView = (props: Props) => {
|
||||||
<span class="view-switcher__counter">{author().stat.shouts}</span>
|
<span class="view-switcher__counter">{author().stat.shouts}</span>
|
||||||
</Show>
|
</Show>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li classList={{ 'view-switcher__item--selected': getPage().route === 'authorComments' }}>
|
||||||
classList={{
|
|
||||||
'view-switcher__item--selected': getPage().route === 'authorComments',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
href={getPagePath(router, 'authorComments', {
|
href={getPagePath(router, 'authorComments', {
|
||||||
slug: props.authorSlug,
|
slug: props.authorSlug,
|
||||||
|
@ -205,11 +186,7 @@ export const AuthorView = (props: Props) => {
|
||||||
<span class="view-switcher__counter">{author().stat.comments}</span>
|
<span class="view-switcher__counter">{author().stat.comments}</span>
|
||||||
</Show>
|
</Show>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li classList={{ 'view-switcher__item--selected': getPage().route === 'authorAbout' }}>
|
||||||
classList={{
|
|
||||||
'view-switcher__item--selected': getPage().route === 'authorAbout',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
onClick={() => checkBioHeight()}
|
onClick={() => checkBioHeight()}
|
||||||
href={getPagePath(router, 'authorAbout', {
|
href={getPagePath(router, 'authorAbout', {
|
||||||
|
@ -260,6 +237,12 @@ export const AuthorView = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={getPage().route === 'authorComments'}>
|
<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="wide-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-20 col-lg-18">
|
<div class="col-md-20 col-lg-18">
|
||||||
|
@ -280,26 +263,24 @@ export const AuthorView = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={getPage().route === 'author'}>
|
<Match when={getPage().route === 'author'}>
|
||||||
<Show when={sortedArticles().length === 1}>
|
<Show when={me()?.slug === props.authorSlug && !me().stat?.shouts}>
|
||||||
<Row1 article={sortedArticles()[0]} noauthor={true} nodate={true} />
|
<div class="wide-container">
|
||||||
|
<Placeholder type={getPage().route} mode="profile" />
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={sortedArticles().length === 2}>
|
<Show when={sortedArticles().length > 0}>
|
||||||
|
<Row1 article={sortedArticles()[0]} noauthor={true} nodate={true} />
|
||||||
|
|
||||||
|
<Show when={sortedArticles().length > 1}>
|
||||||
|
<Switch>
|
||||||
|
<Match when={sortedArticles().length === 2}>
|
||||||
<Row2 articles={sortedArticles()} isEqual={true} noauthor={true} nodate={true} />
|
<Row2 articles={sortedArticles()} isEqual={true} noauthor={true} nodate={true} />
|
||||||
</Show>
|
</Match>
|
||||||
|
<Match when={sortedArticles().length === 3}>
|
||||||
<Show when={sortedArticles().length === 3}>
|
|
||||||
<Row3 articles={sortedArticles()} noauthor={true} nodate={true} />
|
<Row3 articles={sortedArticles()} noauthor={true} nodate={true} />
|
||||||
</Show>
|
</Match>
|
||||||
|
<Match when={sortedArticles().length > 3}>
|
||||||
<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()}>
|
<For each={pages()}>
|
||||||
{(page) => (
|
{(page) => (
|
||||||
<>
|
<>
|
||||||
|
@ -312,6 +293,8 @@ export const AuthorView = (props: Props) => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={isLoadMoreButtonVisible()}>
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
|
@ -321,6 +304,7 @@ export const AuthorView = (props: Props) => {
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,20 +15,24 @@ import styles from './DraftsView.module.scss'
|
||||||
export const DraftsView = () => {
|
export const DraftsView = () => {
|
||||||
const { author, loadSession } = useSession()
|
const { author, loadSession } = useSession()
|
||||||
const [drafts, setDrafts] = createSignal<Shout[]>([])
|
const [drafts, setDrafts] = createSignal<Shout[]>([])
|
||||||
|
const [loading, setLoading] = createSignal(false)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => author(),
|
() => author(),
|
||||||
async (a) => {
|
async (a) => {
|
||||||
if (a) {
|
if (a) {
|
||||||
|
setLoading(true)
|
||||||
const { shouts: loadedDrafts, error } = await apiClient.getDrafts()
|
const { shouts: loadedDrafts, error } = await apiClient.getDrafts()
|
||||||
if (error) {
|
if (error) {
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
await loadSession()
|
await loadSession()
|
||||||
}
|
}
|
||||||
setDrafts(loadedDrafts || [])
|
setDrafts(loadedDrafts || [])
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ defer: true },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,7 +54,7 @@ export const DraftsView = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.DraftsView)}>
|
<div class={clsx(styles.DraftsView)}>
|
||||||
<Show when={author()?.id} fallback={<Loading />}>
|
<Show when={!loading() && author()?.id} fallback={<Loading />}>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
|
<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 { PublishSettings } from '../PublishSettings'
|
||||||
|
|
||||||
|
import { Loading } from '../../_shared/Loading'
|
||||||
import styles from './EditView.module.scss'
|
import styles from './EditView.module.scss'
|
||||||
|
|
||||||
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
|
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
|
||||||
|
@ -145,7 +146,7 @@ export const EditView = (props: Props) => {
|
||||||
|
|
||||||
const handleMediaDelete = (index) => {
|
const handleMediaDelete = (index) => {
|
||||||
const copy = [...mediaItems()]
|
const copy = [...mediaItems()]
|
||||||
copy.splice(index, 1)
|
if (copy?.length > 0) copy.splice(index, 1)
|
||||||
handleInputChange('media', JSON.stringify(copy))
|
handleInputChange('media', JSON.stringify(copy))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,7 +404,7 @@ export const EditView = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={page().route === 'edit'}>
|
<Show when={page().route === 'edit' && form?.shoutId} fallback={<Loading />}>
|
||||||
<Editor
|
<Editor
|
||||||
shoutId={form.shoutId}
|
shoutId={form.shoutId}
|
||||||
initialContent={form.body}
|
initialContent={form.body}
|
||||||
|
|
|
@ -175,6 +175,7 @@
|
||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
border: none;
|
||||||
color: rgb(0 0 0 / 65%);
|
color: rgb(0 0 0 / 65%);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { getShareUrl } from '../../Article/SharePopup'
|
||||||
import { AuthorBadge } from '../../Author/AuthorBadge'
|
import { AuthorBadge } from '../../Author/AuthorBadge'
|
||||||
import { AuthorLink } from '../../Author/AuthorLink'
|
import { AuthorLink } from '../../Author/AuthorLink'
|
||||||
import { ArticleCard } from '../../Feed/ArticleCard'
|
import { ArticleCard } from '../../Feed/ArticleCard'
|
||||||
|
import { Placeholder } from '../../Feed/Placeholder'
|
||||||
import { Sidebar } from '../../Feed/Sidebar'
|
import { Sidebar } from '../../Feed/Sidebar'
|
||||||
import { Modal } from '../../Nav/Modal'
|
import { Modal } from '../../Nav/Modal'
|
||||||
import { DropDown } from '../../_shared/DropDown'
|
import { DropDown } from '../../_shared/DropDown'
|
||||||
|
@ -100,7 +101,7 @@ export const FeedView = (props: Props) => {
|
||||||
const { page, searchParams, changeSearchParams } = useRouter<FeedSearchParams>()
|
const { page, searchParams, changeSearchParams } = useRouter<FeedSearchParams>()
|
||||||
const [isLoading, setIsLoading] = createSignal(false)
|
const [isLoading, setIsLoading] = createSignal(false)
|
||||||
const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false)
|
const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false)
|
||||||
const { session } = useSession()
|
const { author, session } = useSession()
|
||||||
const { loadReactionsBy } = useReactions()
|
const { loadReactionsBy } = useReactions()
|
||||||
const { sortedArticles } = useArticlesStore()
|
const { sortedArticles } = useArticlesStore()
|
||||||
const { topTopics } = useTopics()
|
const { topTopics } = useTopics()
|
||||||
|
@ -143,16 +144,20 @@ export const FeedView = (props: Props) => {
|
||||||
Promise.all([loadTopComments()]).finally(() => setIsRightColumnLoaded(true))
|
Promise.all([loadTopComments()]).finally(() => setIsRightColumnLoaded(true))
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(
|
||||||
if (session()?.access_token && !unratedArticles()) {
|
on(
|
||||||
loadUnratedArticles()
|
[() => session(), unratedArticles],
|
||||||
}
|
([s, seen]) => {
|
||||||
})
|
if (s?.access_token && !(seen?.length > 0)) loadUnratedArticles()
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => page().route + searchParams().by + searchParams().period + searchParams().visibility,
|
[page, searchParams],
|
||||||
() => {
|
(_, _p) => {
|
||||||
resetSortedArticles()
|
resetSortedArticles()
|
||||||
loadMore()
|
loadMore()
|
||||||
},
|
},
|
||||||
|
@ -234,6 +239,11 @@ export const FeedView = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-12 offset-xl-1">
|
<div class="col-md-12 offset-xl-1">
|
||||||
|
<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}>
|
<div class={styles.filtersContainer}>
|
||||||
<ul class={clsx('view-switcher', styles.feedFilter)}>
|
<ul class={clsx('view-switcher', styles.feedFilter)}>
|
||||||
<li
|
<li
|
||||||
|
@ -337,6 +347,7 @@ export const FeedView = (props: Props) => {
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class={clsx('col-md-7 col-xl-6 offset-xl-1', styles.feedAside)}>
|
<aside class={clsx('col-md-7 col-xl-6 offset-xl-1', styles.feedAside)}>
|
||||||
|
|
|
@ -136,9 +136,7 @@ export const InboxView = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(messages, () => {
|
||||||
() => messages(),
|
|
||||||
() => {
|
|
||||||
if (!messagesContainerRef.current) {
|
if (!messagesContainerRef.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -149,8 +147,7 @@ export const InboxView = (props: Props) => {
|
||||||
top: messagesContainerRef.current.scrollHeight,
|
top: messagesContainerRef.current.scrollHeight,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
})
|
})
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
)
|
)
|
||||||
const handleScrollMessageContainer = () => {
|
const handleScrollMessageContainer = () => {
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { clsx } from 'clsx'
|
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 { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Author, Topic } from '../../../graphql/schema/core.gen'
|
import { Author, Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { SubscriptionFilter } from '../../../pages/types'
|
import { FollowsFilter } from '../../../pages/types'
|
||||||
import { dummyFilter } from '../../../utils/dummyFilter'
|
import { dummyFilter } from '../../../utils/dummyFilter'
|
||||||
// TODO: refactor styles
|
|
||||||
import { isAuthor } from '../../../utils/isAuthor'
|
import { isAuthor } from '../../../utils/isAuthor'
|
||||||
import { AuthorBadge } from '../../Author/AuthorBadge'
|
import { AuthorBadge } from '../../Author/AuthorBadge'
|
||||||
import { ProfileSettingsNavigation } from '../../Nav/ProfileSettingsNavigation'
|
import { ProfileSettingsNavigation } from '../../Nav/ProfileSettingsNavigation'
|
||||||
|
@ -19,30 +18,30 @@ import stylesSettings from '../../../styles/FeedSettings.module.scss'
|
||||||
|
|
||||||
export const ProfileSubscriptions = () => {
|
export const ProfileSubscriptions = () => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const { subscriptions } = useFollowing()
|
const { follows } = useFollowing()
|
||||||
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
|
const [flatFollows, setFlatFollows] = createSignal<Array<Author | Topic>>([])
|
||||||
const [filtered, setFiltered] = 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('')
|
const [searchQuery, setSearchQuery] = createSignal('')
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => setFlatFollows([...(follows?.authors || []), ...(follows?.topics || [])]))
|
||||||
const { authors, topics } = subscriptions
|
|
||||||
if (authors || topics) {
|
createEffect(
|
||||||
const fdata = [...(authors || []), ...(topics || [])]
|
on([flatFollows, followsFilter], ([flat, mode]) => {
|
||||||
setFollowing(fdata)
|
if (mode === 'authors') {
|
||||||
if (subscriptionFilter() === 'authors') {
|
setFiltered(flat.filter((s) => 'name' in s))
|
||||||
setFiltered(fdata.filter((s) => 'name' in s))
|
} else if (mode === 'topics') {
|
||||||
} else if (subscriptionFilter() === 'topics') {
|
setFiltered(flat.filter((s) => 'title' in s))
|
||||||
setFiltered(fdata.filter((s) => 'title' in s))
|
|
||||||
} else {
|
} else {
|
||||||
setFiltered(fdata)
|
setFiltered(flat)
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
})
|
{ defer: true },
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (searchQuery()) {
|
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">
|
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||||
<h1>{t('My subscriptions')}</h1>
|
<h1>{t('My subscriptions')}</h1>
|
||||||
<p class="description">{t('Here you can manage all your Discours subscriptions')}</p>
|
<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">
|
<ul class="view-switcher">
|
||||||
<li
|
<li
|
||||||
class={clsx({
|
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')}
|
{t('All')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class={clsx({
|
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')}
|
{t('Authors')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class={clsx({
|
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')}
|
{t('Topics')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -104,9 +103,9 @@ export const ProfileSubscriptions = () => {
|
||||||
{(followingItem) => (
|
{(followingItem) => (
|
||||||
<div>
|
<div>
|
||||||
{isAuthor(followingItem) ? (
|
{isAuthor(followingItem) ? (
|
||||||
<AuthorBadge minimizeSubscribeButton={true} author={followingItem} />
|
<AuthorBadge minimize={true} author={followingItem} />
|
||||||
) : (
|
) : (
|
||||||
<TopicBadge minimizeSubscribeButton={true} topic={followingItem} />
|
<TopicBadge minimize={true} topic={followingItem} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -40,7 +40,18 @@ const EMPTY_TOPIC: Topic = {
|
||||||
id: -1,
|
id: -1,
|
||||||
slug: '',
|
slug: '',
|
||||||
}
|
}
|
||||||
const emptyConfig = {
|
|
||||||
|
interface FormConfig {
|
||||||
|
coverImageUrl?: string
|
||||||
|
mainTopic?: Topic
|
||||||
|
slug?: string
|
||||||
|
title?: string
|
||||||
|
subtitle?: string
|
||||||
|
description?: string
|
||||||
|
selectedTopics?: Topic[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyConfig: FormConfig = {
|
||||||
coverImageUrl: '',
|
coverImageUrl: '',
|
||||||
mainTopic: EMPTY_TOPIC,
|
mainTopic: EMPTY_TOPIC,
|
||||||
slug: '',
|
slug: '',
|
||||||
|
@ -78,7 +89,7 @@ export const PublishSettings = (props: Props) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const [settingsForm, setSettingsForm] = createStore(emptyConfig)
|
const [settingsForm, setSettingsForm] = createStore<FormConfig>(emptyConfig)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setSettingsForm(initialData())
|
setSettingsForm(initialData())
|
||||||
|
@ -96,12 +107,12 @@ export const PublishSettings = (props: Props) => {
|
||||||
setSettingsForm('coverImageUrl', '')
|
setSettingsForm('coverImageUrl', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTopicSelectChange = (newSelectedTopics) => {
|
const handleTopicSelectChange = (newSelectedTopics: Topic[]) => {
|
||||||
if (
|
if (
|
||||||
props.form.selectedTopics.length === 0 ||
|
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 {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
mainTopic: newSelectedTopics[0],
|
mainTopic: newSelectedTopics[0],
|
||||||
|
@ -193,7 +204,8 @@ export const PublishSettings = (props: Props) => {
|
||||||
fieldName={t('Header')}
|
fieldName={t('Header')}
|
||||||
placeholder={t('Come up with a title for your story')}
|
placeholder={t('Come up with a title for your story')}
|
||||||
initialValue={settingsForm.title}
|
initialValue={settingsForm.title}
|
||||||
value={(value) => setSettingsForm('title', value)}
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
value={(value: any) => setSettingsForm('title', value)}
|
||||||
allowEnterKey={false}
|
allowEnterKey={false}
|
||||||
maxLength={100}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
|
@ -203,7 +215,8 @@ export const PublishSettings = (props: Props) => {
|
||||||
fieldName={t('Subheader')}
|
fieldName={t('Subheader')}
|
||||||
placeholder={t('Come up with a subtitle for your story')}
|
placeholder={t('Come up with a subtitle for your story')}
|
||||||
initialValue={settingsForm.subtitle || ''}
|
initialValue={settingsForm.subtitle || ''}
|
||||||
value={(value) => setSettingsForm('subtitle', value)}
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
value={(value: any) => setSettingsForm('subtitle', value)}
|
||||||
allowEnterKey={false}
|
allowEnterKey={false}
|
||||||
maxLength={100}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
|
@ -214,7 +227,8 @@ export const PublishSettings = (props: Props) => {
|
||||||
placeholder={t('Write a short introduction')}
|
placeholder={t('Write a short introduction')}
|
||||||
label={t('Description')}
|
label={t('Description')}
|
||||||
initialContent={composeDescription()}
|
initialContent={composeDescription()}
|
||||||
onChange={(value) => setForm('description', value)}
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
onChange={(value: any) => setForm('description', value)}
|
||||||
maxLength={DESCRIPTION_MAX_LENGTH}
|
maxLength={DESCRIPTION_MAX_LENGTH}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||||
|
@ -33,6 +33,7 @@ interface Props {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
shouts: Shout[]
|
shouts: Shout[]
|
||||||
topicSlug: string
|
topicSlug: string
|
||||||
|
followers?: Author[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PRERENDERED_ARTICLES_COUNT = 28
|
export const PRERENDERED_ARTICLES_COUNT = 28
|
||||||
|
@ -49,13 +50,30 @@ export const TopicView = (props: Props) => {
|
||||||
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
||||||
|
|
||||||
const [topic, setTopic] = createSignal<Topic>()
|
const [topic, setTopic] = createSignal<Topic>()
|
||||||
|
createEffect(
|
||||||
createEffect(() => {
|
on([() => props.topicSlug, topic, topicEntities], async ([slug, t, ttt]) => {
|
||||||
const topics = topicEntities()
|
if (slug && !t && ttt) {
|
||||||
if (props.topicSlug && !topic() && topics) {
|
const current = ttt[slug]
|
||||||
setTopic(topics[props.topicSlug])
|
console.debug(current)
|
||||||
|
setTopic(current)
|
||||||
|
await loadTopicFollowers()
|
||||||
|
await loadTopicAuthors()
|
||||||
|
await loadRandom()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
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 loadFavoriteTopArticles = async (topic: string) => {
|
||||||
const options: LoadShoutsOptions = {
|
const options: LoadShoutsOptions = {
|
||||||
|
@ -87,14 +105,6 @@ export const TopicView = (props: Props) => {
|
||||||
loadReactedTopMonthArticles(topic()?.slug)
|
loadReactedTopMonthArticles(topic()?.slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => topic(),
|
|
||||||
() => loadRandom(),
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const title = createMemo(
|
const title = createMemo(
|
||||||
() =>
|
() =>
|
||||||
`#${capitalize(
|
`#${capitalize(
|
||||||
|
@ -158,7 +168,7 @@ export const TopicView = (props: Props) => {
|
||||||
<Meta name="twitter:card" content="summary_large_image" />
|
<Meta name="twitter:card" content="summary_large_image" />
|
||||||
<Meta name="twitter:title" content={title()} />
|
<Meta name="twitter:title" content={title()} />
|
||||||
<Meta name="twitter:description" content={description()} />
|
<Meta name="twitter:description" content={description()} />
|
||||||
<FullTopic topic={topic()} />
|
<FullTopic topic={topic()} followers={followers()} authors={topicAuthors()} />
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class={clsx(styles.groupControls, 'row group__controls')}>
|
<div class={clsx(styles.groupControls, 'row group__controls')}>
|
||||||
<div class="col-md-16">
|
<div class="col-md-16">
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export { BadgeSubscribeButton } from './BadgeSubscribeButton'
|
|
|
@ -175,7 +175,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.subscribed {
|
&.followed {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
|
|
||||||
|
|
|
@ -2,35 +2,36 @@ import { clsx } from 'clsx'
|
||||||
import { Show, createMemo } from 'solid-js'
|
import { Show, createMemo } from 'solid-js'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Button } from '../Button'
|
import { Button } from '../Button'
|
||||||
import stylesButton from '../Button/Button.module.scss'
|
|
||||||
import { CheckButton } from '../CheckButton'
|
import { CheckButton } from '../CheckButton'
|
||||||
import { Icon } from '../Icon'
|
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 = {
|
type Props = {
|
||||||
class?: string
|
class?: string
|
||||||
isSubscribed: boolean
|
isFollowed: boolean
|
||||||
minimizeSubscribeButton?: boolean
|
minimize?: boolean
|
||||||
action: () => void
|
action: () => void
|
||||||
iconButtons?: boolean
|
iconButtons?: boolean
|
||||||
actionMessageType?: 'subscribe' | 'unsubscribe'
|
actionMessageType?: 'follow' | 'unfollow'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BadgeSubscribeButton = (props: Props) => {
|
export const FollowingButton = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const inActionText = createMemo(() => {
|
const inActionText = createMemo(() => {
|
||||||
return props.actionMessageType === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...')
|
return props.actionMessageType === 'follow' ? t('Following...') : t('Unfollowing...')
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={props.class}>
|
<div class={props.class}>
|
||||||
<Show
|
<Show
|
||||||
when={!props.minimizeSubscribeButton}
|
when={!props.minimize}
|
||||||
fallback={<CheckButton text={t('Follow')} checked={props.isSubscribed} onClick={props.action} />}
|
fallback={<CheckButton text={t('Follow')} checked={props.isFollowed} onClick={props.action} />}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={props.isSubscribed}
|
when={props.isFollowed}
|
||||||
fallback={
|
fallback={
|
||||||
<Button
|
<Button
|
||||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||||
|
@ -38,7 +39,7 @@ export const BadgeSubscribeButton = (props: Props) => {
|
||||||
value={
|
value={
|
||||||
<Show
|
<Show
|
||||||
when={props.iconButtons}
|
when={props.iconButtons}
|
||||||
fallback={props.actionMessageType ? inActionText() : t('Subscribe')}
|
fallback={props.actionMessageType ? inActionText() : t('Follow')}
|
||||||
>
|
>
|
||||||
<Icon name="author-subscribe" class={stylesButton.icon} />
|
<Icon name="author-subscribe" class={stylesButton.icon} />
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -47,7 +48,7 @@ export const BadgeSubscribeButton = (props: Props) => {
|
||||||
isSubscribeButton={true}
|
isSubscribeButton={true}
|
||||||
class={clsx(styles.actionButton, {
|
class={clsx(styles.actionButton, {
|
||||||
[styles.iconed]: props.iconButtons,
|
[styles.iconed]: props.iconButtons,
|
||||||
[stylesButton.subscribed]: props.isSubscribed,
|
[stylesButton.followed]: props.isFollowed,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -76,7 +77,7 @@ export const BadgeSubscribeButton = (props: Props) => {
|
||||||
isSubscribeButton={true}
|
isSubscribeButton={true}
|
||||||
class={clsx(styles.actionButton, {
|
class={clsx(styles.actionButton, {
|
||||||
[styles.iconed]: props.iconButtons,
|
[styles.iconed]: props.iconButtons,
|
||||||
[stylesButton.subscribed]: props.isSubscribed,
|
[stylesButton.followed]: props.isFollowed,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</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(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => sortedAuthors(),
|
sortedAuthors,
|
||||||
(currentAuthors) => {
|
(currentAuthors) => {
|
||||||
setAuthorsToInvite(currentAuthors.map((author) => ({ ...author, selected: false })))
|
setAuthorsToInvite(currentAuthors.map((author) => ({ ...author, selected: false })))
|
||||||
},
|
},
|
||||||
|
|
|
@ -129,8 +129,8 @@ export const Lightbox = (props: Props) => {
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => zoomLevel(),
|
zoomLevel,
|
||||||
() => {
|
(_) => {
|
||||||
clearTimeout(fadeTimer)
|
clearTimeout(fadeTimer)
|
||||||
|
|
||||||
fadeTimer = setTimeout(() => {
|
fadeTimer = setTimeout(() => {
|
||||||
|
|
|
@ -6,12 +6,12 @@ import { validateEmail } from '../../../utils/validateEmail'
|
||||||
import { Button } from '../Button'
|
import { Button } from '../Button'
|
||||||
import { Icon } from '../Icon'
|
import { Icon } from '../Icon'
|
||||||
|
|
||||||
import styles from './Subscribe.module.scss'
|
import styles from './Newsletter.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
variant?: 'mobileSubscription'
|
variant?: 'mobileSubscription'
|
||||||
}
|
}
|
||||||
export const Subscribe = (props: Props) => {
|
export const Newsletter = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const [title, setTitle] = createSignal('')
|
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(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.images.length,
|
() => props.images.length,
|
||||||
() => {
|
(_) => {
|
||||||
mainSwipeRef.current?.swiper.update()
|
mainSwipeRef.current?.swiper.update()
|
||||||
thumbSwipeRef.current?.swiper.update()
|
thumbSwipeRef.current?.swiper.update()
|
||||||
},
|
},
|
||||||
|
@ -121,6 +121,7 @@ export const EditorSwiper = (props: Props) => {
|
||||||
|
|
||||||
const handleChangeIndex = (direction: 'left' | 'right', index: number) => {
|
const handleChangeIndex = (direction: 'left' | 'right', index: number) => {
|
||||||
const images = [...props.images]
|
const images = [...props.images]
|
||||||
|
if (images?.length > 0) {
|
||||||
if (direction === 'left' && index > 0) {
|
if (direction === 'left' && index > 0) {
|
||||||
const copy = images.splice(index, 1)[0]
|
const copy = images.splice(index, 1)[0]
|
||||||
images.splice(index - 1, 0, copy)
|
images.splice(index - 1, 0, copy)
|
||||||
|
@ -133,6 +134,7 @@ export const EditorSwiper = (props: Props) => {
|
||||||
mainSwipeRef.current.swiper.slideTo(direction === 'left' ? index - 1 : index + 1)
|
mainSwipeRef.current.swiper.slideTo(direction === 'left' ? index - 1 : index + 1)
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSaveBeforeSlideChange = () => {
|
const handleSaveBeforeSlideChange = () => {
|
||||||
handleSlideDescriptionChange(slideIndex(), 'body', slideBody())
|
handleSlideDescriptionChange(slideIndex(), 'body', slideBody())
|
||||||
|
|
|
@ -45,7 +45,7 @@ export const ImageSwiper = (props: Props) => {
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.images.length,
|
() => props.images.length,
|
||||||
() => {
|
(_) => {
|
||||||
mainSwipeRef.current?.swiper.update()
|
mainSwipeRef.current?.swiper.update()
|
||||||
thumbSwipeRef.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 type { Author, Reaction, Shout, Topic } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'
|
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 { Chat, Message } from '../graphql/schema/chat.gen'
|
||||||
|
import { sseUrl } from '../utils/config'
|
||||||
import { useSession } from './session'
|
import { useSession } from './session'
|
||||||
|
|
||||||
const RECONNECT_TIMES = 2
|
const RECONNECT_TIMES = 2
|
||||||
|
@ -38,16 +39,20 @@ export const ConnectProvider = (props: { children: JSX.Element }) => {
|
||||||
setHandlers((hhh) => [...hhh, handler])
|
setHandlers((hhh) => [...hhh, handler])
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(async () => {
|
createEffect(
|
||||||
const token = session()?.access_token
|
on(
|
||||||
if (token && !connected() && retried() <= RECONNECT_TIMES) {
|
() => session()?.access_token,
|
||||||
console.info('[context.connect] init SSE connection')
|
async (tkn) => {
|
||||||
|
if (!sseUrl) return
|
||||||
|
if (!tkn) return
|
||||||
|
if (!connected() && retried() <= RECONNECT_TIMES) {
|
||||||
|
console.info('[context.connect] got token, init SSE connection')
|
||||||
try {
|
try {
|
||||||
await fetchEventSource('https://connect.discours.io', {
|
await fetchEventSource(sseUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: token,
|
Authorization: tkn,
|
||||||
},
|
},
|
||||||
onmessage(event) {
|
onmessage(event) {
|
||||||
const m: SSEMessage = JSON.parse(event.data || '{}')
|
const m: SSEMessage = JSON.parse(event.data || '{}')
|
||||||
|
@ -61,14 +66,14 @@ export const ConnectProvider = (props: { children: JSX.Element }) => {
|
||||||
setRetried(0)
|
setRetried(0)
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
return Promise.reject(`SSE: cannot connect to real-time updates, status: ${response.status}`)
|
return Promise.reject(`SSE: cannot connect to real-time updates: ${response.status}`)
|
||||||
},
|
},
|
||||||
onclose() {
|
onclose() {
|
||||||
console.log('[context.connect] SSE connection closed by server')
|
console.log('[context.connect] SSE connection closed by server')
|
||||||
setConnected(false)
|
setConnected(false)
|
||||||
if (retried() < RECONNECT_TIMES) {
|
if (retried() < RECONNECT_TIMES) {
|
||||||
setRetried((r) => r + 1)
|
setRetried((r) => r + 1)
|
||||||
}
|
} else throw Error('closed by server')
|
||||||
},
|
},
|
||||||
onerror(err) {
|
onerror(err) {
|
||||||
console.error('[context.connect] SSE connection error:', err)
|
console.error('[context.connect] SSE connection error:', err)
|
||||||
|
@ -82,7 +87,9 @@ export const ConnectProvider = (props: { children: JSX.Element }) => {
|
||||||
console.error('[context.connect] SSE connection failed:', error)
|
console.error('[context.connect] SSE connection failed:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const value: ConnectContextType = { addHandler, connected }
|
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 { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
import { apiClient } from '../graphql/client/core'
|
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'
|
import { useSession } from './session'
|
||||||
|
|
||||||
export type SubscriptionsData = {
|
type FollowingData = { slug: string; type: 'follow' | 'unfollow' }
|
||||||
topics?: Topic[]
|
|
||||||
authors?: Author[]
|
|
||||||
communities?: Community[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubscribeAction = { slug: string; type: 'subscribe' | 'unsubscribe' }
|
|
||||||
|
|
||||||
interface FollowingContextType {
|
interface FollowingContextType {
|
||||||
loading: Accessor<boolean>
|
loading: Accessor<boolean>
|
||||||
|
|
||||||
followers: Accessor<Author[]>
|
followers: Accessor<Author[]>
|
||||||
subscriptions: AuthorFollowsResult
|
setFollows: (follows: AuthorFollowsResult) => void
|
||||||
setSubscriptions: (subscriptions: AuthorFollowsResult) => void
|
|
||||||
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
following: Accessor<FollowingData>
|
||||||
loadSubscriptions: () => void
|
changeFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
||||||
|
|
||||||
|
follows: AuthorFollowsResult
|
||||||
|
loadFollows: () => void
|
||||||
|
|
||||||
follow: (what: FollowingEntity, slug: string) => Promise<void>
|
follow: (what: FollowingEntity, slug: string) => Promise<void>
|
||||||
unfollow: (what: FollowingEntity, slug: string) => Promise<void>
|
unfollow: (what: FollowingEntity, slug: string) => Promise<void>
|
||||||
// followers: Accessor<Author[]>
|
|
||||||
subscribeInAction?: Accessor<SubscribeAction>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const FollowingContext = createContext<FollowingContextType>()
|
const FollowingContext = createContext<FollowingContextType>()
|
||||||
|
@ -42,7 +39,7 @@ const EMPTY_SUBSCRIPTIONS: AuthorFollowsResult = {
|
||||||
export const FollowingProvider = (props: { children: JSX.Element }) => {
|
export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
const [loading, setLoading] = createSignal<boolean>(false)
|
const [loading, setLoading] = createSignal<boolean>(false)
|
||||||
const [followers, setFollowers] = createSignal<Author[]>([])
|
const [followers, setFollowers] = createSignal<Author[]>([])
|
||||||
const [subscriptions, setSubscriptions] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS)
|
const [follows, setFollows] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS)
|
||||||
const { author, session } = useSession()
|
const { author, session } = useSession()
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
@ -51,68 +48,67 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
if (apiClient.private) {
|
if (apiClient.private) {
|
||||||
console.debug('[context.following] fetching subs data...')
|
console.debug('[context.following] fetching subs data...')
|
||||||
const result = await apiClient.getAuthorFollows({ user: session()?.user.id })
|
const result = await apiClient.getAuthorFollows({ user: session()?.user.id })
|
||||||
setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
|
setFollows(result || EMPTY_SUBSCRIPTIONS)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.info('[context.following] cannot get subs', error)
|
console.warn('[context.following] cannot get subs', error)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
const [following, setFollowing] = createSignal<FollowingData>()
|
||||||
console.info('[context.following] subs:', subscriptions)
|
|
||||||
})
|
|
||||||
|
|
||||||
const [subscribeInAction, setSubscribeInAction] = createSignal<SubscribeAction>()
|
|
||||||
const follow = async (what: FollowingEntity, slug: string) => {
|
const follow = async (what: FollowingEntity, slug: string) => {
|
||||||
if (!author()) return
|
if (!author()) return
|
||||||
setSubscribeInAction({ slug, type: 'subscribe' })
|
setFollowing({ slug, type: 'follow' })
|
||||||
try {
|
try {
|
||||||
const subscriptionData = await apiClient.follow({ what, slug })
|
const result = await apiClient.follow({ what, slug })
|
||||||
setSubscriptions((prevSubscriptions) => {
|
setFollows((subs) => {
|
||||||
if (!prevSubscriptions[what]) prevSubscriptions[what] = []
|
if (result.authors) subs['authors'] = result.authors || []
|
||||||
prevSubscriptions[what].push(subscriptionData)
|
if (result.topics) subs['topics'] = result.topics || []
|
||||||
return prevSubscriptions
|
return subs
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
setSubscribeInAction() // Сбрасываем состояние действия подписки.
|
setFollowing() // Сбрасываем состояние действия подписки.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unfollow = async (what: FollowingEntity, slug: string) => {
|
const unfollow = async (what: FollowingEntity, slug: string) => {
|
||||||
if (!author()) return
|
if (!author()) return
|
||||||
setSubscribeInAction({ slug: slug, type: 'unsubscribe' })
|
setFollowing({ slug: slug, type: 'unfollow' })
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
setSubscribeInAction()
|
setFollowing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(
|
||||||
if (author()) {
|
on(
|
||||||
try {
|
() => session()?.user.app_data,
|
||||||
const appdata = session()?.user.app_data
|
(appdata) => {
|
||||||
if (appdata) {
|
if (appdata) {
|
||||||
const { authors, followers, topics } = appdata
|
const { authors, followers, topics } = appdata
|
||||||
setSubscriptions({ authors, topics })
|
setFollows({ authors, topics })
|
||||||
setFollowers(followers)
|
setFollowers(followers)
|
||||||
if (!authors) fetchData()
|
if (!authors) fetchData()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
},
|
||||||
console.error(e)
|
),
|
||||||
}
|
)
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const setFollowing = (what: FollowingEntity, slug: string, value = true) => {
|
const changeFollowing = (what: FollowingEntity, slug: string, value = true) => {
|
||||||
setSubscriptions((prevSubscriptions) => {
|
setFollows((fff) => {
|
||||||
const updatedSubs = { ...prevSubscriptions }
|
const updatedSubs = { ...fff }
|
||||||
if (!updatedSubs[what]) updatedSubs[what] = []
|
if (!updatedSubs[what]) updatedSubs[what] = []
|
||||||
if (value) {
|
if (value) {
|
||||||
const exists = updatedSubs[what]?.some((entity) => entity.slug === slug)
|
const exists = updatedSubs[what]?.some((entity) => entity.slug === slug)
|
||||||
|
@ -133,15 +129,14 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
const value: FollowingContextType = {
|
const value: FollowingContextType = {
|
||||||
loading,
|
loading,
|
||||||
subscriptions,
|
follows,
|
||||||
setSubscriptions,
|
setFollows,
|
||||||
setFollowing,
|
following,
|
||||||
|
changeFollowing,
|
||||||
followers,
|
followers,
|
||||||
loadSubscriptions: fetchData,
|
loadFollows: fetchData,
|
||||||
follow,
|
follow,
|
||||||
unfollow,
|
unfollow,
|
||||||
// followers,
|
|
||||||
subscribeInAction,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <FollowingContext.Provider value={value}>{props.children}</FollowingContext.Provider>
|
return <FollowingContext.Provider value={value}>{props.children}</FollowingContext.Provider>
|
||||||
|
|
|
@ -184,7 +184,7 @@ function initServerProvider() {
|
||||||
const index = tags.findIndex(
|
const index = tags.findIndex(
|
||||||
(prev) => prev.tag === tagDesc.tag && getTagKey(prev, properties) === tagDescKey,
|
(prev) => prev.tag === tagDesc.tag && getTagKey(prev, properties) === tagDescKey,
|
||||||
)
|
)
|
||||||
if (index !== -1) {
|
if (index !== -1 && tags?.length > 0) {
|
||||||
tags.splice(index, 1)
|
tags.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,27 +32,26 @@ import { inboxClient } from '../graphql/client/chat'
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { useRouter } from '../stores/router'
|
import { useRouter } from '../stores/router'
|
||||||
import { showModal } from '../stores/ui'
|
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 { useLocalize } from './localize'
|
||||||
import { useSnackbar } from './snackbar'
|
import { useSnackbar } from './snackbar'
|
||||||
|
|
||||||
const defaultConfig: ConfigType = {
|
const defaultConfig: ConfigType = {
|
||||||
authorizerURL: 'https://auth.discours.io',
|
authorizerURL: authApiUrl.replace('/graphql', ''),
|
||||||
redirectURL: 'https://testing.discours.io',
|
redirectURL: 'https://testing.discours.io',
|
||||||
clientID: 'b9038a34-ca59-41ae-a105-c7fbea603e24', // FIXME: use env?
|
clientID: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionContextType = {
|
export type SessionContextType = {
|
||||||
config: Accessor<ConfigType>
|
config: Accessor<ConfigType>
|
||||||
session: Resource<AuthToken>
|
session: Resource<AuthToken>
|
||||||
author: Resource<Author | null>
|
author: Accessor<Author>
|
||||||
authError: Accessor<string>
|
authError: Accessor<string>
|
||||||
isSessionLoaded: Accessor<boolean>
|
isSessionLoaded: Accessor<boolean>
|
||||||
loadSession: () => AuthToken | Promise<AuthToken>
|
loadSession: () => AuthToken | Promise<AuthToken>
|
||||||
setSession: (token: AuthToken | null) => void // setSession
|
setSession: (token: AuthToken | null) => void // setSession
|
||||||
loadAuthor: (info?: unknown) => Author | Promise<Author>
|
|
||||||
setAuthor: (a: Author) => void
|
|
||||||
requireAuthentication: (
|
requireAuthentication: (
|
||||||
callback: (() => Promise<void>) | (() => void),
|
callback: (() => Promise<void>) | (() => void),
|
||||||
modalSource: AuthModalSource,
|
modalSource: AuthModalSource,
|
||||||
|
@ -66,16 +65,39 @@ export type SessionContextType = {
|
||||||
params: ForgotPasswordInput,
|
params: ForgotPasswordInput,
|
||||||
) => Promise<{ data: ForgotPasswordResponse; errors: Error[] }>
|
) => Promise<{ data: ForgotPasswordResponse; errors: Error[] }>
|
||||||
changePassword: (password: string, token: string) => void
|
changePassword: (password: string, token: string) => void
|
||||||
confirmEmail: (input: VerifyEmailInput) => Promise<AuthToken> // email confirm callback is in auth.discours.io
|
confirmEmail: (input: VerifyEmailInput) => Promise<AuthToken> // email confirm callback is in authorizer
|
||||||
setIsSessionLoaded: (loaded: boolean) => void
|
setIsSessionLoaded: (loaded: boolean) => void
|
||||||
authorizer: () => Authorizer
|
authorizer: () => Authorizer
|
||||||
isRegistered: (email: string) => Promise<string>
|
isRegistered: (email: string) => Promise<string>
|
||||||
resendVerifyEmail: (params: ResendVerifyEmailInput) => Promise<GenericResponse>
|
resendVerifyEmail: (params: ResendVerifyEmailInput) => Promise<GenericResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: <explanation>
|
const noop = () => null
|
||||||
const noop = () => {}
|
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>()
|
const SessionContext = createContext<SessionContextType>()
|
||||||
|
|
||||||
export function useSession() {
|
export function useSession() {
|
||||||
|
@ -96,15 +118,15 @@ export const SessionProvider = (props: {
|
||||||
// handle auth state callback
|
// handle auth state callback
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => searchParams()?.state,
|
searchParams,
|
||||||
(state) => {
|
(params) => {
|
||||||
if (state) {
|
if (params?.state) {
|
||||||
setOauthState((_s) => state)
|
setOauthState((_s) => params?.state)
|
||||||
const scope = searchParams()?.scope
|
const scope = params?.scope
|
||||||
? searchParams()?.scope?.toString().split(' ')
|
? params?.scope?.toString().split(' ')
|
||||||
: ['openid', 'profile', 'email']
|
: ['openid', 'profile', 'email']
|
||||||
if (scope) console.info(`[context.session] scope: ${scope}`)
|
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] }))
|
setConfig((c: ConfigType) => ({ ...c, redirectURL: url.split('?')[0] }))
|
||||||
changeSearchParams({ mode: 'confirm-email', m: 'auth' }, true)
|
changeSearchParams({ mode: 'confirm-email', m: 'auth' }, true)
|
||||||
}
|
}
|
||||||
|
@ -202,75 +224,56 @@ export const SessionProvider = (props: {
|
||||||
|
|
||||||
onCleanup(() => clearTimeout(minuteLater))
|
onCleanup(() => clearTimeout(minuteLater))
|
||||||
|
|
||||||
const authorData = async () => {
|
const [author, setAuthor] = createSignal<Author>()
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
// when session is loaded
|
// when session is loaded
|
||||||
createEffect(() => {
|
createEffect(
|
||||||
if (session()) {
|
on(
|
||||||
const token = session()?.access_token
|
() => session(),
|
||||||
if (token) {
|
async (s: AuthToken) => {
|
||||||
if (!inboxClient.private) {
|
if (s) {
|
||||||
|
const token = s?.access_token
|
||||||
|
const profile = s?.user?.app_data?.profile
|
||||||
|
if (token && !inboxClient.private) {
|
||||||
apiClient.connect(token)
|
apiClient.connect(token)
|
||||||
inboxClient.connect(token)
|
inboxClient.connect(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const appdata = session()?.user.app_data
|
|
||||||
if (appdata) {
|
|
||||||
const { profile } = appdata
|
|
||||||
if (profile?.id) {
|
if (profile?.id) {
|
||||||
setAuthor(profile)
|
|
||||||
addAuthors([profile])
|
addAuthors([profile])
|
||||||
} else {
|
setAuthor(profile)
|
||||||
setTimeout(loadAuthor, 15)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSessionLoaded(true)
|
setIsSessionLoaded(true)
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// when author is loaded
|
|
||||||
createEffect(() => {
|
|
||||||
if (author()) {
|
|
||||||
addAuthors([author()])
|
|
||||||
} else {
|
} else {
|
||||||
reset()
|
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 {
|
||||||
|
console.warn(s)
|
||||||
const reset = () => {
|
|
||||||
setIsSessionLoaded(true)
|
|
||||||
setSession(null)
|
setSession(null)
|
||||||
setAuthor(null)
|
setAuthor(null)
|
||||||
|
setIsSessionLoaded(true)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// initial effect
|
// initial effect
|
||||||
onMount(async () => {
|
onMount(() => {
|
||||||
const metaRes = await authorizer().getMetaData()
|
|
||||||
setConfig({
|
setConfig({
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
...metaRes,
|
...metaRes,
|
||||||
redirectURL: window.location.origin,
|
redirectURL: window.location.origin,
|
||||||
})
|
})
|
||||||
let s: AuthToken
|
loadSession()
|
||||||
try {
|
|
||||||
s = await loadSession()
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[context.session] load session failed', error)
|
|
||||||
}
|
|
||||||
if (!s) reset()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// callback state updater
|
// callback state updater
|
||||||
|
@ -316,8 +319,10 @@ export const SessionProvider = (props: {
|
||||||
const signOut = async () => {
|
const signOut = async () => {
|
||||||
const authResult: ApiResponse<GenericResponse> = await authorizer().logout()
|
const authResult: ApiResponse<GenericResponse> = await authorizer().logout()
|
||||||
console.debug(authResult)
|
console.debug(authResult)
|
||||||
reset()
|
setSession(null)
|
||||||
|
setIsSessionLoaded(true)
|
||||||
showSnackbar({ body: t("You've successfully logged out") })
|
showSnackbar({ body: t("You've successfully logged out") })
|
||||||
|
console.debug(session())
|
||||||
}
|
}
|
||||||
|
|
||||||
const changePassword = async (password: string, token: string) => {
|
const changePassword = async (password: string, token: string) => {
|
||||||
|
@ -391,9 +396,7 @@ export const SessionProvider = (props: {
|
||||||
updateProfile,
|
updateProfile,
|
||||||
setIsSessionLoaded,
|
setIsSessionLoaded,
|
||||||
setSession,
|
setSession,
|
||||||
setAuthor,
|
|
||||||
authorizer,
|
authorizer,
|
||||||
loadAuthor,
|
|
||||||
forgotPassword,
|
forgotPassword,
|
||||||
changePassword,
|
changePassword,
|
||||||
oauth,
|
oauth,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
||||||
LoadShoutsOptions,
|
LoadShoutsOptions,
|
||||||
MutationDelete_ShoutArgs,
|
MutationDelete_ShoutArgs,
|
||||||
ProfileInput,
|
ProfileInput,
|
||||||
|
QueryGet_Topic_FollowersArgs,
|
||||||
QueryLoad_Authors_ByArgs,
|
QueryLoad_Authors_ByArgs,
|
||||||
QueryLoad_Shouts_Random_TopArgs,
|
QueryLoad_Shouts_Random_TopArgs,
|
||||||
QueryLoad_Shouts_SearchArgs,
|
QueryLoad_Shouts_SearchArgs,
|
||||||
|
@ -39,11 +40,11 @@ import loadShoutsUnrated from '../query/core/articles-load-unrated'
|
||||||
import authorBy from '../query/core/author-by'
|
import authorBy from '../query/core/author-by'
|
||||||
import authorFollowers from '../query/core/author-followers'
|
import authorFollowers from '../query/core/author-followers'
|
||||||
import authorFollows from '../query/core/author-follows'
|
import authorFollows from '../query/core/author-follows'
|
||||||
import authorId from '../query/core/author-id'
|
|
||||||
import authorsAll from '../query/core/authors-all'
|
import authorsAll from '../query/core/authors-all'
|
||||||
import authorsLoadBy from '../query/core/authors-load-by'
|
import authorsLoadBy from '../query/core/authors-load-by'
|
||||||
import reactionsLoadBy from '../query/core/reactions-load-by'
|
import reactionsLoadBy from '../query/core/reactions-load-by'
|
||||||
import topicBySlug from '../query/core/topic-by-slug'
|
import topicBySlug from '../query/core/topic-by-slug'
|
||||||
|
import topicFollowers from '../query/core/topic-followers'
|
||||||
import topicsAll from '../query/core/topics-all'
|
import topicsAll from '../query/core/topics-all'
|
||||||
import topicsRandomQuery from '../query/core/topics-random'
|
import topicsRandomQuery from '../query/core/topics-random'
|
||||||
|
|
||||||
|
@ -119,16 +120,16 @@ export const apiClient = {
|
||||||
return response.data.get_author
|
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[]> => {
|
getAuthorFollowers: async ({ slug }: { slug: string }): Promise<Author[]> => {
|
||||||
const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise()
|
const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise()
|
||||||
return response.data.get_author_followers
|
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: {
|
getAuthorFollows: async (params: {
|
||||||
slug?: string
|
slug?: string
|
||||||
author_id?: number
|
author_id?: number
|
||||||
|
|
|
@ -6,7 +6,24 @@ export default gql`
|
||||||
error
|
error
|
||||||
authors {
|
authors {
|
||||||
id
|
id
|
||||||
|
name
|
||||||
slug
|
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!) {
|
mutation UnfollowMutation($what: FollowingEntity!, $slug: String!) {
|
||||||
unfollow(what: $what, slug: $slug) {
|
unfollow(what: $what, slug: $slug) {
|
||||||
error
|
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'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
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) {
|
get_author_followers(slug: $slug, user: $user, author_id: $author_id) {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query GetAuthorId($user: String!) {
|
query TopicFollowersQuery($slug: String) {
|
||||||
get_author_id(user: $user) {
|
get_topic_followers(slug: $slug) {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
bio
|
bio
|
||||||
about
|
about
|
||||||
pic
|
pic
|
||||||
|
# communities
|
||||||
links
|
links
|
||||||
created_at
|
created_at
|
||||||
last_seen
|
last_seen
|
||||||
|
@ -18,8 +19,6 @@ export default gql`
|
||||||
followers
|
followers
|
||||||
rating
|
rating
|
||||||
comments
|
comments
|
||||||
rating_shouts
|
|
||||||
rating_comments
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -40,7 +40,7 @@ export const DiscussionRulesPage = () => {
|
||||||
людей рождается истина.
|
людей рождается истина.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>За что можно получить дырку в карме и выиграть бан в сообществе</h3>
|
<h3 id="ban">За что можно получить дырку в карме и выиграть бан в сообществе</h3>
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Feedback } from '../../components/Discours/Feedback'
|
||||||
import { Modal } from '../../components/Nav/Modal'
|
import { Modal } from '../../components/Nav/Modal'
|
||||||
import Opener from '../../components/Nav/Modal/Opener'
|
import Opener from '../../components/Nav/Modal/Opener'
|
||||||
import { StaticPage } from '../../components/Views/StaticPage'
|
import { StaticPage } from '../../components/Views/StaticPage'
|
||||||
import { Subscribe } from '../../components/_shared/Subscribe'
|
import { Newsletter } from '../../components/_shared/Newsletter'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { getImageUrl } from '../../utils/getImageUrl'
|
import { getImageUrl } from '../../utils/getImageUrl'
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export const ManifestPage = () => {
|
||||||
<Feedback />
|
<Feedback />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal variant="wide" name="subscribe">
|
<Modal variant="wide" name="subscribe">
|
||||||
<Subscribe />
|
<Newsletter />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Meta name="descprition" content={description} />
|
<Meta name="descprition" content={description} />
|
||||||
<Meta name="keywords" content={t('keywords')} />
|
<Meta name="keywords" content={t('keywords')} />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { PageProps } from './types'
|
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 { AuthorView, PRERENDERED_ARTICLES_COUNT } from '../components/Views/Author'
|
||||||
import { Loading } from '../components/_shared/Loading'
|
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(),
|
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(
|
createEffect(
|
||||||
on(
|
on(slug, async (s) => {
|
||||||
() => slug(),
|
if (s) {
|
||||||
async () => {
|
|
||||||
setIsLoaded(false)
|
setIsLoaded(false)
|
||||||
resetSortedArticles()
|
resetSortedArticles()
|
||||||
await preload()
|
await loadShouts({
|
||||||
|
filters: { author: s, featured: false },
|
||||||
|
limit: PRERENDERED_ARTICLES_COUNT,
|
||||||
|
})
|
||||||
|
await loadAuthor({ slug: s })
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
},
|
}
|
||||||
{ defer: true },
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
onCleanup(() => resetSortedArticles())
|
onCleanup(() => resetSortedArticles())
|
||||||
|
|
|
@ -17,6 +17,7 @@ import styles from '../styles/Create.module.scss'
|
||||||
|
|
||||||
const handleCreate = async (layout: LayoutType) => {
|
const handleCreate = async (layout: LayoutType) => {
|
||||||
const shout = await apiClient.createArticle({ article: { layout: layout } })
|
const shout = await apiClient.createArticle({ article: { layout: layout } })
|
||||||
|
shout?.id &&
|
||||||
redirectPage(router, 'edit', {
|
redirectPage(router, 'edit', {
|
||||||
shoutId: shout?.id.toString(),
|
shoutId: shout?.id.toString(),
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { useLocalize } from '../context/localize'
|
||||||
import { useSession } from '../context/session'
|
import { useSession } from '../context/session'
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { Shout } from '../graphql/schema/core.gen'
|
import { Shout } from '../graphql/schema/core.gen'
|
||||||
import { router, useRouter } from '../stores/router'
|
import { router } from '../stores/router'
|
||||||
|
|
||||||
import { redirectPage } from '@nanostores/router'
|
import { redirectPage } from '@nanostores/router'
|
||||||
import { useSnackbar } from '../context/snackbar'
|
import { useSnackbar } from '../context/snackbar'
|
||||||
|
@ -33,7 +33,6 @@ const getContentTypeTitle = (layout: LayoutType) => {
|
||||||
export const EditPage = () => {
|
export const EditPage = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { session } = useSession()
|
const { session } = useSession()
|
||||||
const { page } = useRouter()
|
|
||||||
const snackbar = useSnackbar()
|
const snackbar = useSnackbar()
|
||||||
|
|
||||||
const fail = async (error: string) => {
|
const fail = async (error: string) => {
|
||||||
|
@ -48,18 +47,21 @@ export const EditPage = () => {
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => page(),
|
() => window?.location.pathname,
|
||||||
(p) => {
|
(p) => {
|
||||||
if (p?.path) {
|
if (p) {
|
||||||
console.debug(p?.path)
|
console.debug(p)
|
||||||
const shoutId = p?.path.split('/').pop()
|
const shoutId = p.split('/').pop()
|
||||||
|
if (shoutId) {
|
||||||
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
|
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
|
||||||
console.debug(`editing shout ${shoutIdFromUrl}`)
|
console.debug(`editing shout ${shoutIdFromUrl}`)
|
||||||
if (shoutIdFromUrl) {
|
if (shoutIdFromUrl) {
|
||||||
setShoutId(shoutIdFromUrl)
|
setShoutId(shoutIdFromUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
{ defer: true },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,9 @@ import { LayoutType } from '../types'
|
||||||
export const ExpoPage = (props: PageProps) => {
|
export const ExpoPage = (props: PageProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const getLayout = createMemo<LayoutType>(() => page().params['layout'] as LayoutType)
|
const layout = createMemo(() => page().params['layout'] as LayoutType)
|
||||||
|
const title = createMemo(() => {
|
||||||
const getTitle = () => {
|
switch (layout()) {
|
||||||
switch (getLayout()) {
|
|
||||||
case 'audio': {
|
case 'audio': {
|
||||||
return t('Audio')
|
return t('Audio')
|
||||||
}
|
}
|
||||||
|
@ -32,22 +31,14 @@ export const ExpoPage = (props: PageProps) => {
|
||||||
return t('Art')
|
return t('Art')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(on(title, (t) => (document.title = t), { defer: true }))
|
||||||
on(
|
|
||||||
() => getLayout(),
|
|
||||||
() => {
|
|
||||||
document.title = getTitle()
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout withPadding={true} zeroBottomPadding={true} title={getTitle()}>
|
<PageLayout withPadding={true} zeroBottomPadding={true} title={title()}>
|
||||||
<Topics />
|
<Topics />
|
||||||
<Expo shouts={props.expoShouts} layout={getLayout()} />
|
<Expo shouts={props.expoShouts} layout={layout()} />
|
||||||
</PageLayout>
|
</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 { Feed } from '../components/Views/Feed'
|
||||||
import { PageLayout } from '../components/_shared/PageLayout'
|
import { PageLayout } from '../components/_shared/PageLayout'
|
||||||
import { useLocalize } from '../context/localize'
|
import { useLocalize } from '../context/localize'
|
||||||
|
@ -25,34 +24,14 @@ const handleMyFeedLoadShouts = (options: LoadShoutsOptions) => {
|
||||||
|
|
||||||
export const FeedPage = () => {
|
export const FeedPage = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
onCleanup(() => resetSortedArticles())
|
|
||||||
|
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
|
createEffect(on(page, (_) => resetSortedArticles(), { defer: true }))
|
||||||
createEffect(
|
onCleanup(() => resetSortedArticles())
|
||||||
on(
|
|
||||||
() => page().route,
|
|
||||||
() => {
|
|
||||||
resetSortedArticles()
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title={t('Feed')}>
|
<PageLayout title={t('Feed')}>
|
||||||
<ReactionsProvider>
|
<ReactionsProvider>
|
||||||
<Switch fallback={<Feed loadShouts={handleFeedLoadShouts} />}>
|
<Feed loadShouts={page().route === 'feedMy' ? handleMyFeedLoadShouts : handleFeedLoadShouts} />
|
||||||
<Match when={page().route === 'feed'}>
|
|
||||||
<Feed loadShouts={handleFeedLoadShouts} />
|
|
||||||
</Match>
|
|
||||||
<Match when={page().route === 'feedMy'}>
|
|
||||||
<AuthGuard>
|
|
||||||
<Feed loadShouts={handleMyFeedLoadShouts} />
|
|
||||||
</AuthGuard>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</ReactionsProvider>
|
</ReactionsProvider>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -44,10 +44,10 @@ export const ProfileSecurityPage = () => {
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => session()?.user?.email,
|
() => session()?.user?.email,
|
||||||
() => {
|
(email) => {
|
||||||
setFormData((prevData) => ({
|
setFormData((prevData) => ({
|
||||||
...prevData,
|
...prevData,
|
||||||
['email']: session()?.user?.email,
|
email,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -37,24 +37,22 @@ export const TopicPage = (props: PageProps) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(slug, async (s) => {
|
||||||
() => slug(),
|
if (s) {
|
||||||
async () => {
|
|
||||||
setIsLoaded(false)
|
setIsLoaded(false)
|
||||||
resetSortedArticles()
|
resetSortedArticles()
|
||||||
await preload()
|
await preload()
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
},
|
}
|
||||||
{ defer: true },
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
onCleanup(() => resetSortedArticles())
|
onCleanup(resetSortedArticles)
|
||||||
|
|
||||||
const usePrerenderedData = props.topic?.slug === slug()
|
const usePrerenderedData = props.topic?.slug === slug()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title={props.seo.title}>
|
<PageLayout title={props.seo?.title || props.topic?.title}>
|
||||||
<ReactionsProvider>
|
<ReactionsProvider>
|
||||||
<Show when={isLoaded()} fallback={<Loading />}>
|
<Show when={isLoaded()} fallback={<Loading />}>
|
||||||
<TopicView
|
<TopicView
|
||||||
|
|
|
@ -53,4 +53,4 @@ export type UploadedFile = {
|
||||||
originalFilename?: string
|
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 { App } from '../components/App'
|
||||||
import { initRouter } from '../stores/router'
|
import { initRouter } from '../stores/router'
|
||||||
import { GLITCHTIP_DSN } from '../utils/config'
|
import { reportDsn } from '../utils/config'
|
||||||
import { resolveHydrationPromise } from '../utils/hydrationPromise'
|
import { resolveHydrationPromise } from '../utils/hydrationPromise'
|
||||||
|
|
||||||
let layoutReady = false
|
let layoutReady = false
|
||||||
|
@ -22,7 +22,7 @@ export const render = async (pageContext: PageContextBuiltInClientWithClientRout
|
||||||
initRouter(pathname, searchParams)
|
initRouter(pathname, searchParams)
|
||||||
|
|
||||||
SentryInit({
|
SentryInit({
|
||||||
dsn: GLITCHTIP_DSN,
|
dsn: reportDsn,
|
||||||
tracesSampleRate: 0.01,
|
tracesSampleRate: 0.01,
|
||||||
integrations: [replayIntegration()],
|
integrations: [replayIntegration()],
|
||||||
// Session Replay
|
// Session Replay
|
||||||
|
|
|
@ -1,20 +1,8 @@
|
||||||
export const isDev = import.meta.env.MODE === 'development'
|
export const isDev = import.meta.env.MODE === 'development'
|
||||||
|
|
||||||
const defaultThumborUrl = 'https://images.discours.io'
|
|
||||||
export const cdnUrl = 'https://cdn.discours.io'
|
export const cdnUrl = 'https://cdn.discours.io'
|
||||||
export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || defaultThumborUrl
|
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 SENTRY_DSN = import.meta.env.PUBLIC_SENTRY_DSN || ''
|
export const coreApiUrl = import.meta.env.PUBLIC_API_BASE || 'https://core.discours.io'
|
||||||
export const GLITCHTIP_DSN = import.meta.env.PUBLIC_GLITCHTIP_DSN || ''
|
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'
|
||||||
const defaultSearchUrl = 'https://search.discours.io'
|
export const sseUrl = import.meta.env.PUBLIC_REALTIME_EVENTS || 'https://connect.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
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { UploadFile } from '@solid-primitives/upload'
|
import { UploadFile } from '@solid-primitives/upload'
|
||||||
|
|
||||||
import { UploadedFile } from '../pages/types'
|
import { UploadedFile } from '../pages/types'
|
||||||
|
import { coreApiUrl } from './config'
|
||||||
|
|
||||||
const apiBaseUrl = 'https://core.discours.io'
|
const apiUrl = `${coreApiUrl}/upload`
|
||||||
const apiUrl = `${apiBaseUrl}/upload`
|
|
||||||
|
|
||||||
export const handleFileUpload = async (uploadFile: UploadFile, token: string): Promise<UploadedFile> => {
|
export const handleFileUpload = async (uploadFile: UploadFile, token: string): Promise<UploadedFile> => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
|
|
||||||
const baseHost = process.env.BASE_URL
|
const baseHost = process.env.BASE_HOST || 'https://localhost:3000'
|
||||||
|
|
||||||
const pagesTitles = {
|
const pagesTitles = {
|
||||||
'/': /Дискурс/,
|
'/': /Дискурс/,
|
||||||
'/feed': /Дискурс/,
|
'/feed': /Лента/,
|
||||||
'/create': /Дискурс/,
|
'/create': /Выберите тип публикации/,
|
||||||
'/about/donate': /Дискурс/,
|
'/about/help': /Поддержите Дискурс/,
|
||||||
'/authors': /Дискурс/,
|
'/authors': /Авторы/,
|
||||||
'/topics': /Дискурс/,
|
'/topics': /Темы и сюжеты/,
|
||||||
'/inbox': /Дискурс/,
|
'/inbox': /Входящие/,
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(pagesTitles).forEach((res: string) => {
|
Object.keys(pagesTitles).forEach((res: string) => {
|
||||||
|
|
|
@ -5,13 +5,13 @@ import { chromium } from 'playwright'
|
||||||
|
|
||||||
// Define the URLs to visit
|
// Define the URLs to visit
|
||||||
const pagesToVisit = [
|
const pagesToVisit = [
|
||||||
'http://localhost:3000/',
|
'https://localhost:3000/',
|
||||||
'http://localhost:3000/feed',
|
'https://localhost:3000/feed',
|
||||||
'http://localhost:3000/create',
|
'https://localhost:3000/create',
|
||||||
'http://localhost:3000/about/donate',
|
'https://localhost:3000/about/donate',
|
||||||
'http://localhost:3000/authors',
|
'https://localhost:3000/authors',
|
||||||
'http://localhost:3000/topics',
|
'https://localhost:3000/topics',
|
||||||
'http://localhost:3000/inbox',
|
'https://localhost:3000/inbox',
|
||||||
]
|
]
|
||||||
|
|
||||||
// Loop through the pages and visit each one
|
// Loop through the pages and visit each one
|
||||||
|
|
|
@ -14,8 +14,9 @@ const cssModuleHMR = () => {
|
||||||
const { modules } = context
|
const { modules } = context
|
||||||
|
|
||||||
modules.forEach((module) => {
|
modules.forEach((module) => {
|
||||||
if (module.id.includes('.module.scss')) {
|
if (module.id.includes('.scss') || module.id.includes('.css')) {
|
||||||
module.isSelfAccepting = true
|
module.isSelfAccepting = true
|
||||||
|
// module.accept()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue
Block a user