Merge branch 'hotfix/editor-permission' into feature/glitchtip
This commit is contained in:
commit
27a9662143
5
.github/workflows/node-ci.yml
vendored
5
.github/workflows/node-ci.yml
vendored
|
@ -15,7 +15,10 @@ jobs:
|
||||||
- name: Check types
|
- name: Check types
|
||||||
run: npm run check:types
|
run: npm run check:types
|
||||||
|
|
||||||
- name: Check styles
|
- name: Lint with Biome
|
||||||
|
run: npm run check:code
|
||||||
|
|
||||||
|
- name: Lint styles
|
||||||
run: npm run lint:styles
|
run: npm run lint:styles
|
||||||
|
|
||||||
- name: Test production build
|
- name: Test production build
|
||||||
|
|
49
biome.json
49
biome.json
|
@ -1,8 +1,21 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
||||||
"files": {
|
"files": {
|
||||||
"include": ["*.tsx", "*.ts", "*.js", "*.json"],
|
"include": [
|
||||||
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.d.ts"]
|
"*.tsx",
|
||||||
|
"*.ts",
|
||||||
|
"*.js",
|
||||||
|
"*.json"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"./dist",
|
||||||
|
"./node_modules",
|
||||||
|
".husky",
|
||||||
|
"docs",
|
||||||
|
"gen",
|
||||||
|
"*.gen.ts",
|
||||||
|
"*.d.ts"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"defaultBranch": "dev",
|
"defaultBranch": "dev",
|
||||||
|
@ -10,13 +23,19 @@
|
||||||
},
|
},
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"ignore": ["./api", "./gen"]
|
"ignore": [
|
||||||
|
"./api",
|
||||||
|
"./gen"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
"indentWidth": 2,
|
"indentWidth": 2,
|
||||||
"lineWidth": 108,
|
"lineWidth": 108,
|
||||||
"ignore": ["./src/graphql/schema", "./gen"]
|
"ignore": [
|
||||||
|
"./src/graphql/schema",
|
||||||
|
"./gen"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"javascript": {
|
"javascript": {
|
||||||
"formatter": {
|
"formatter": {
|
||||||
|
@ -29,14 +48,21 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"ignore": ["*.scss", "*.md", ".DS_Store", "*.svg", "*.d.ts"],
|
"ignore": [
|
||||||
|
"*.scss",
|
||||||
|
"*.md",
|
||||||
|
".DS_Store",
|
||||||
|
"*.svg",
|
||||||
|
"*.d.ts"
|
||||||
|
],
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"all": true,
|
"all": true,
|
||||||
"complexity": {
|
"complexity": {
|
||||||
"noForEach": "off",
|
"noForEach": "off",
|
||||||
"useOptionalChain": "warn",
|
"useOptionalChain": "warn",
|
||||||
"useLiteralKeys": "off"
|
"useLiteralKeys": "off",
|
||||||
|
"noExcessiveCognitiveComplexity": "off"
|
||||||
},
|
},
|
||||||
"correctness": {
|
"correctness": {
|
||||||
"useHookAtTopLevel": "off"
|
"useHookAtTopLevel": "off"
|
||||||
|
@ -54,15 +80,18 @@
|
||||||
"noSvgWithoutTitle": "off"
|
"noSvgWithoutTitle": "off"
|
||||||
},
|
},
|
||||||
"nursery": {
|
"nursery": {
|
||||||
"useImportRestrictions": "off",
|
"useImportRestrictions": "off"
|
||||||
"useImportType": "off",
|
},
|
||||||
"useFilenamingConvention": "off"
|
"performance": {
|
||||||
|
"noBarrelFile": "off"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"useBlockStatements": "off",
|
"useBlockStatements": "off",
|
||||||
"noImplicitBoolean": "off",
|
"noImplicitBoolean": "off",
|
||||||
"useNamingConvention": "off",
|
"useNamingConvention": "off",
|
||||||
"noDefaultExport": "off"
|
"useImportType": "off",
|
||||||
|
"noDefaultExport": "off",
|
||||||
|
"useFilenamingConvention": "off"
|
||||||
},
|
},
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noConsoleLog": "off",
|
"noConsoleLog": "off",
|
||||||
|
|
5478
package-lock.json
generated
5478
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
|
@ -11,13 +11,12 @@
|
||||||
"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=chromium",
|
||||||
"fix": "npm run check:code:fix && stylelint **/*.{scss,css} --fix",
|
"fix": "npm run lint: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",
|
"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 src --log-kind=compact",
|
"check:code:fix": "npx @biomejs/biome lint src --log-kind=compact",
|
||||||
"check:types": "tsc --noEmit",
|
|
||||||
"lint": "npm run lint:code && stylelint **/*.{scss,css}",
|
"lint": "npm run lint:code && stylelint **/*.{scss,css}",
|
||||||
"lint:code": "npx @biomejs/biome lint src --log-kind=compact --verbose",
|
"lint:code": "npx @biomejs/biome lint src --log-kind=compact --verbose",
|
||||||
"lint:code:fix": "npx @biomejs/biome lint src --apply-unsafe --log-kind=compact --verbose",
|
"lint:code:fix": "npx @biomejs/biome lint src --apply-unsafe --log-kind=compact --verbose",
|
||||||
|
@ -36,7 +35,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@authorizerdev/authorizer-js": "2.0.0",
|
"@authorizerdev/authorizer-js": "2.0.0",
|
||||||
"@babel/core": "7.23.3",
|
"@babel/core": "7.23.3",
|
||||||
"@biomejs/biome": "^1.5.3",
|
"@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",
|
||||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
"@graphql-codegen/typescript-operations": "^4.0.1",
|
||||||
|
@ -130,8 +129,9 @@
|
||||||
"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.8",
|
"vite": "5.2.10",
|
||||||
"vite-plugin-mkcert": "^1.17.3",
|
"vite-plugin-mkcert": "^1.17.3",
|
||||||
|
"vite-plugin-node-polyfills": "0.21.0",
|
||||||
"vite-plugin-sass-dts": "^1.3.17",
|
"vite-plugin-sass-dts": "^1.3.17",
|
||||||
"vite-plugin-solid": "2.10.1",
|
"vite-plugin-solid": "2.10.1",
|
||||||
"y-prosemirror": "1.2.2",
|
"y-prosemirror": "1.2.2",
|
||||||
|
@ -140,5 +140,8 @@
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"y-prosemirror": "1.2.2",
|
"y-prosemirror": "1.2.2",
|
||||||
"yjs": "13.6.12"
|
"yjs": "13.6.12"
|
||||||
}
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"@biomejs/biome"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M8.25 4.125C7.14583 4.125 6.13281 4.6901 5.60937 5.60156C5.40365 5.95573 5.16406 6.53385 5.03125 6.91927C4.91146 7.2474 3.07813 11.349 1.95313 13.8568L1.95833 13.8594C1.66667 14.4349 1.5 15.0755 1.5 15.75C1.5 18.2318 3.6875 20.25 6.375 20.25C9.0625 20.25 11.25 18.2318 11.25 15.75V14.3724C11.4505 14.3099 11.7109 14.25 12 14.25C12.2891 14.25 12.5495 14.3099 12.75 14.3724V15.75C12.75 18.2318 14.9375 20.25 17.625 20.25C20.3125 20.25 22.5 18.2318 22.5 15.75C22.5 15.0755 22.3333 14.4349 22.0417 13.8594L22.0469 13.8568C20.9219 11.349 19.0885 7.2474 18.9688 6.92448C18.8359 6.53646 18.5964 5.95833 18.3906 5.60417C17.8672 4.6901 16.8542 4.125 15.75 4.125C14.1354 4.125 12.8177 5.32813 12.7552 6.82813C12.526 6.78125 12.2734 6.75 12 6.75C11.7266 6.75 11.474 6.78125 11.2448 6.82813C11.1823 5.32813 9.86458 4.125 8.25 4.125ZM8.25 5.625C9.07813 5.625 9.75 6.21354 9.75 6.9375V12.5104C8.8724 11.7318 7.6849 11.25 6.375 11.25C5.75781 11.25 5.16927 11.362 4.625 11.5547C5.48177 9.64063 6.36458 7.65365 6.45052 7.40885C6.57292 7.04688 6.77604 6.58333 6.90885 6.35156C7.16667 5.90365 7.67969 5.625 8.25 5.625ZM15.75 5.625C16.3203 5.625 16.8333 5.90365 17.0911 6.35156C17.224 6.58333 17.4271 7.04948 17.5495 7.40885C17.6354 7.65365 18.5182 9.64063 19.3724 11.5547C18.8307 11.362 18.2422 11.25 17.625 11.25C16.3151 11.25 15.1276 11.7318 14.25 12.5104V6.9375C14.25 6.21354 14.9219 5.625 15.75 5.625ZM12 8.25C12.2891 8.25 12.5495 8.3099 12.75 8.3724V9.82552C12.5208 9.78125 12.2708 9.75 12 9.75C11.7292 9.75 11.4792 9.78125 11.25 9.82552V8.3724C11.4505 8.3099 11.7109 8.25 12 8.25ZM12 11.25C12.2891 11.25 12.5495 11.3099 12.75 11.3724V12.8255C12.5208 12.7812 12.2708 12.75 12 12.75C11.7292 12.75 11.4792 12.7812 11.25 12.8255V11.3724C11.4505 11.3099 11.7109 11.25 12 11.25ZM6.375 12.75C8.23698 12.75 9.75 14.0964 9.75 15.75C9.75 17.4036 8.23698 18.75 6.375 18.75C4.51302 18.75 3 17.4036 3 15.75C3 14.0964 4.51302 12.75 6.375 12.75ZM17.625 12.75C19.487 12.75 21 14.0964 21 15.75C21 17.4036 19.487 18.75 17.625 18.75C15.763 18.75 14.25 17.4036 14.25 15.75C14.25 14.0964 15.763 12.75 17.625 12.75Z" fill="#141414"/>
|
<path d="M8.625 4.5C7.59115 4.5 6.75 5.34115 6.75 6.375V8.25H5.625C4.59115 8.25 3.75 9.09115 3.75 10.125V17.25C3.75 18.4896 4.76042 19.5 6 19.5H18C19.2396 19.5 20.25 18.4896 20.25 17.25V6.375C20.25 5.34115 19.4089 4.5 18.375 4.5H8.625ZM8.625 6H18.375C18.5807 6 18.75 6.16927 18.75 6.375V17.25C18.75 17.6641 18.4141 18 18 18H8.1224C8.20313 17.7656 8.25 17.513 8.25 17.25V6.375C8.25 6.16927 8.41927 6 8.625 6ZM10.125 7.5C9.71094 7.5 9.375 7.83594 9.375 8.25C9.375 8.66406 9.71094 9 10.125 9H16.875C17.2891 9 17.625 8.66406 17.625 8.25C17.625 7.83594 17.2891 7.5 16.875 7.5H10.125ZM5.625 9.75H6.75V17.25C6.75 17.6641 6.41406 18 6 18C5.58594 18 5.25 17.6641 5.25 17.25V10.125C5.25 9.91927 5.41927 9.75 5.625 9.75ZM10.125 10.125C9.71094 10.125 9.375 10.4609 9.375 10.875C9.375 11.2891 9.71094 11.625 10.125 11.625H16.875C17.2891 11.625 17.625 11.2891 17.625 10.875C17.625 10.4609 17.2891 10.125 16.875 10.125H10.125ZM10.125 12.75C9.71094 12.75 9.375 13.0859 9.375 13.5V16.125C9.375 16.5391 9.71094 16.875 10.125 16.875H12.375C12.7891 16.875 13.125 16.5391 13.125 16.125V13.5C13.125 13.0859 12.7891 12.75 12.375 12.75H10.125ZM15 12.75C14.5859 12.75 14.25 13.0859 14.25 13.5C14.25 13.9141 14.5859 14.25 15 14.25H16.875C17.2891 14.25 17.625 13.9141 17.625 13.5C17.625 13.0859 17.2891 12.75 16.875 12.75H15ZM15 15.375C14.5859 15.375 14.25 15.7109 14.25 16.125C14.25 16.5391 14.5859 16.875 15 16.875H16.875C17.2891 16.875 17.625 16.5391 17.625 16.125C17.625 15.7109 17.2891 15.375 16.875 15.375H15Z" fill="black"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.6 KiB |
3
public/icons/toggle-arrow.svg
Normal file
3
public/icons/toggle-arrow.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="18" height="10" viewBox="0 0 18 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17.1042 9.90633C16.9063 9.90633 16.7136 9.82821 16.5626 9.67716L8.98965 1.91675L1.43236 9.66154C1.1459 9.95841 0.671944 9.96362 0.375069 9.67716C0.0781948 9.3855 0.0729868 8.91154 0.359444 8.61467L8.45319 0.322998C8.73965 0.0313314 9.24486 0.0313314 9.53132 0.322998L17.6407 8.63029C17.9272 8.92716 17.9219 9.40112 17.6251 9.69279C17.4792 9.83342 17.2917 9.90633 17.1042 9.90633Z" fill="#9FA1A7"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 511 B |
|
@ -528,5 +528,7 @@
|
||||||
"yesterday": "yesterday",
|
"yesterday": "yesterday",
|
||||||
"Failed to delete comment": "Failed to delete comment",
|
"Failed to delete comment": "Failed to delete comment",
|
||||||
"It's OK. Just enter your email to receive a link to change your password": "It's OK. Just enter your email to receive a link to change your password",
|
"It's OK. Just enter your email to receive a link to change your password": "It's OK. Just enter your email to receive a link to change your password",
|
||||||
"Restore password": "Restore password"
|
"Restore password": "Restore password",
|
||||||
|
"Subscribing...": "Subscribing...",
|
||||||
|
"Unsubscribing...": "Unsubscribing..."
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
"All posts rating": "Рейтинг всех постов",
|
"All posts rating": "Рейтинг всех постов",
|
||||||
"All posts": "Все публикации",
|
"All posts": "Все публикации",
|
||||||
"All topics": "Все темы",
|
"All topics": "Все темы",
|
||||||
"All": "Все",
|
"All": "Общая лента",
|
||||||
"Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.",
|
"Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.",
|
||||||
"Are you sure you want to delete this comment?": "Уверены, что хотите удалить этот комментарий?",
|
"Are you sure you want to delete this comment?": "Уверены, что хотите удалить этот комментарий?",
|
||||||
"Are you sure you want to delete this draft?": "Уверены, что хотите удалить этот черновик?",
|
"Are you sure you want to delete this draft?": "Уверены, что хотите удалить этот черновик?",
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
"FAQ": "Советы и предложения",
|
"FAQ": "Советы и предложения",
|
||||||
"Favorite topics": "Избранные темы",
|
"Favorite topics": "Избранные темы",
|
||||||
"Favorite": "Избранное",
|
"Favorite": "Избранное",
|
||||||
"Feed settings": "Настройки ленты",
|
"Feed settings": "Настроить ленту",
|
||||||
"Feed": "Лента",
|
"Feed": "Лента",
|
||||||
"Feedback": "Обратная связь",
|
"Feedback": "Обратная связь",
|
||||||
"Fill email": "Введите почту",
|
"Fill email": "Введите почту",
|
||||||
|
@ -555,5 +555,7 @@
|
||||||
"yesterday": "вчера",
|
"yesterday": "вчера",
|
||||||
"Failed to delete comment": "Не удалось удалить комментарий",
|
"Failed to delete comment": "Не удалось удалить комментарий",
|
||||||
"It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля",
|
"It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля",
|
||||||
"Restore password": "Восстановить пароль"
|
"Restore password": "Восстановить пароль",
|
||||||
|
"Subscribing...": "Подписываем...",
|
||||||
|
"Unsubscribing...": "Отписываем..."
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,10 +44,7 @@ import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.
|
||||||
import { SearchPage } from '../pages/search.page'
|
import { SearchPage } from '../pages/search.page'
|
||||||
import { TopicPage } from '../pages/topic.page'
|
import { TopicPage } from '../pages/topic.page'
|
||||||
import { ROUTES, useRouter } from '../stores/router'
|
import { ROUTES, useRouter } from '../stores/router'
|
||||||
import { MODALS, hideModal, showModal } from '../stores/ui'
|
import { MODALS, showModal } from '../stores/ui'
|
||||||
|
|
||||||
// TODO: lazy load
|
|
||||||
// const SomePage = lazy(() => import('./Pages/SomePage'))
|
|
||||||
|
|
||||||
const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
|
const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
|
||||||
author: AuthorPage,
|
author: AuthorPage,
|
||||||
|
|
|
@ -22,6 +22,7 @@ img {
|
||||||
.articleContent {
|
.articleContent {
|
||||||
img:not([data-disable-lightbox='true']) {
|
img:not([data-disable-lightbox='true']) {
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -179,6 +179,10 @@
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.commentAuthor {
|
||||||
|
margin-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.articleAuthor {
|
.articleAuthor {
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,11 @@
|
||||||
|
|
||||||
color: var(--secondary-color);
|
color: var(--secondary-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
justify-content: center;
|
||||||
justify-content: flex-start;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: .5rem;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
margin-bottom: .5rem;
|
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
|
@ -14,7 +14,7 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CommentDate = (props: Props) => {
|
export const CommentDate = (props: Props) => {
|
||||||
const { t, formatDate } = useLocalize()
|
const { formatDate } = useLocalize()
|
||||||
|
|
||||||
const formattedDate = (date: number) => {
|
const formattedDate = (date: number) => {
|
||||||
const formatDateOptions: Intl.DateTimeFormatOptions = props.isShort
|
const formatDateOptions: Intl.DateTimeFormatOptions = props.isShort
|
||||||
|
|
|
@ -54,12 +54,13 @@ type IframeSize = {
|
||||||
export type ArticlePageSearchParams = {
|
export type ArticlePageSearchParams = {
|
||||||
scrollTo: 'comments'
|
scrollTo: 'comments'
|
||||||
commentId: string
|
commentId: string
|
||||||
|
slide?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollTo = (el: HTMLElement) => {
|
const scrollTo = (el: HTMLElement) => {
|
||||||
const { top } = el.getBoundingClientRect()
|
const { top } = el.getBoundingClientRect()
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: top - DEFAULT_HEADER_OFFSET,
|
top: top + window.scrollY - DEFAULT_HEADER_OFFSET,
|
||||||
left: 0,
|
left: 0,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
})
|
})
|
||||||
|
@ -74,7 +75,7 @@ export const FullArticle = (props: Props) => {
|
||||||
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
||||||
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
||||||
const { t, formatDate, lang } = useLocalize()
|
const { t, formatDate, lang } = useLocalize()
|
||||||
const { author, session, isAuthenticated, requireAuthentication } = useSession()
|
const { author, session, requireAuthentication } = useSession()
|
||||||
|
|
||||||
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000)))
|
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000)))
|
||||||
|
|
||||||
|
@ -151,19 +152,6 @@ export const FullArticle = (props: Props) => {
|
||||||
current: HTMLDivElement
|
current: HTMLDivElement
|
||||||
} = { current: null }
|
} = { current: null }
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (props.scrollToComments) {
|
|
||||||
scrollTo(commentsRef.current)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
|
|
||||||
requestAnimationFrame(() => scrollTo(commentsRef.current))
|
|
||||||
changeSearchParams({ scrollTo: null })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (searchParams().commentId && isReactionsLoaded()) {
|
if (searchParams().commentId && isReactionsLoaded()) {
|
||||||
const commentElement = document.querySelector<HTMLElement>(
|
const commentElement = document.querySelector<HTMLElement>(
|
||||||
|
@ -319,6 +307,19 @@ export const FullArticle = (props: Props) => {
|
||||||
window?.addEventListener('resize', updateIframeSizes)
|
window?.addEventListener('resize', updateIframeSizes)
|
||||||
|
|
||||||
onCleanup(() => window.removeEventListener('resize', updateIframeSizes))
|
onCleanup(() => window.removeEventListener('resize', updateIframeSizes))
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.scrollToComments) {
|
||||||
|
scrollTo(commentsRef.current)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
|
||||||
|
requestAnimationFrame(() => scrollTo(commentsRef.current))
|
||||||
|
changeSearchParams({ scrollTo: null })
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const cover = props.article.cover ?? 'production/image/logo_image.png'
|
const cover = props.article.cover ?? 'production/image/logo_image.png'
|
||||||
|
@ -329,7 +330,7 @@ export const FullArticle = (props: Props) => {
|
||||||
width: 1200,
|
width: 1200,
|
||||||
})
|
})
|
||||||
|
|
||||||
const description = getDescription(props.article.description || body())
|
const description = getDescription(props.article.description || body() || media()[0]?.body)
|
||||||
const ogTitle = props.article.title
|
const ogTitle = props.article.title
|
||||||
const keywords = getKeywords(props.article)
|
const keywords = getKeywords(props.article)
|
||||||
const shareUrl = getShareUrl({ pathname: `/${props.article.slug}` })
|
const shareUrl = getShareUrl({ pathname: `/${props.article.slug}` })
|
||||||
|
@ -560,7 +561,7 @@ export const FullArticle = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={isAuthenticated() && !canEdit()}>
|
<Show when={author()?.id && !canEdit()}>
|
||||||
<div class={styles.help}>
|
<div class={styles.help}>
|
||||||
<button class="button">{t('Cooperate')}</button>
|
<button class="button">{t('Cooperate')}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,7 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthGuard = (props: Props) => {
|
export const AuthGuard = (props: Props) => {
|
||||||
const { isAuthenticated, isSessionLoaded } = useSession()
|
const { author, isSessionLoaded } = useSession()
|
||||||
const { changeSearchParams } = useRouter<RootSearchParams & AuthModalSearchParams>()
|
const { changeSearchParams } = useRouter<RootSearchParams & AuthModalSearchParams>()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
@ -20,7 +20,7 @@ export const AuthGuard = (props: Props) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isSessionLoaded()) {
|
if (isSessionLoaded()) {
|
||||||
if (isAuthenticated()) {
|
if (author()?.id) {
|
||||||
hideModal()
|
hideModal()
|
||||||
} else {
|
} else {
|
||||||
changeSearchParams(
|
changeSearchParams(
|
||||||
|
@ -37,5 +37,5 @@ export const AuthGuard = (props: Props) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return <Show when={(isSessionLoaded() && isAuthenticated()) || props.disabled}>{props.children}</Show>
|
return <Show when={(isSessionLoaded() && author()?.id) || props.disabled}>{props.children}</Show>
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,8 +115,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionButtonLabelHovered {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, on } from 'solid-js'
|
import { Match, Show, Switch, createEffect, createMemo, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useFollowing } from '../../../context/following'
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
@ -10,14 +10,12 @@ 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 { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Userpic } from '../Userpic'
|
import { Userpic } from '../Userpic'
|
||||||
|
|
||||||
import { FollowedInfo } from '../../../pages/types'
|
|
||||||
import stylesButton from '../../_shared/Button/Button.module.scss'
|
|
||||||
import styles from './AuthorBadge.module.scss'
|
import styles from './AuthorBadge.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -29,19 +27,25 @@ type Props = {
|
||||||
inviteView?: boolean
|
inviteView?: boolean
|
||||||
onInvite?: (id: number) => void
|
onInvite?: (id: number) => void
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
isFollowed?: FollowedInfo
|
|
||||||
}
|
}
|
||||||
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 [isMobileView, setIsMobileView] = createSignal(false)
|
const [isMobileView, setIsMobileView] = createSignal(false)
|
||||||
const [isFollowed, setIsFollowed] = createSignal<boolean>()
|
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!(subscriptions && props.author)) return
|
||||||
|
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
|
||||||
|
setIsSubscribed(subscribed)
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setIsMobileView(!mediaMatches.sm)
|
setIsMobileView(!mediaMatches.sm)
|
||||||
})
|
})
|
||||||
|
|
||||||
const { setFollowing } = useFollowing()
|
// const { setFollowing } = useFollowing()
|
||||||
const { changeSearchParams } = useRouter()
|
const { changeSearchParams } = useRouter()
|
||||||
const { t, formatDate, lang } = useLocalize()
|
const { t, formatDate, lang } = useLocalize()
|
||||||
|
|
||||||
|
@ -67,20 +71,11 @@ export const AuthorBadge = (props: Props) => {
|
||||||
return props.author.name
|
return props.author.name
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => props.isFollowed,
|
|
||||||
() => {
|
|
||||||
setIsFollowed(props.isFollowed?.value)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleFollowClick = () => {
|
const handleFollowClick = () => {
|
||||||
const value = !isFollowed()
|
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
setIsFollowed(value)
|
isSubscribed()
|
||||||
setFollowing(FollowingEntity.Author, props.author.slug, value)
|
? unfollow(FollowingEntity.Author, props.author.slug)
|
||||||
|
: follow(FollowingEntity.Author, props.author.slug)
|
||||||
}, 'subscribe')
|
}, 'subscribe')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +119,9 @@ export const AuthorBadge = (props: Props) => {
|
||||||
<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('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={props.author?.stat.comments > 0}>
|
||||||
|
<div>{t('CommentsWithCount', { count: props.author.stat?.comments ?? 0 })}</div>
|
||||||
|
</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('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -134,55 +132,13 @@ 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}>
|
||||||
<Show
|
<BadgeSubscribeButton
|
||||||
when={!props.minimizeSubscribeButton}
|
action={() => handleFollowClick()}
|
||||||
fallback={<CheckButton text={t('Follow')} checked={isFollowed()} onClick={handleFollowClick} />}
|
isSubscribed={isSubscribed()}
|
||||||
>
|
actionMessageType={
|
||||||
<Show
|
subscribeInAction()?.slug === props.author.slug ? subscribeInAction().type : undefined
|
||||||
when={isFollowed()}
|
}
|
||||||
fallback={
|
/>
|
||||||
<Button
|
|
||||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
|
||||||
size="S"
|
|
||||||
value={
|
|
||||||
<Show when={props.iconButtons} fallback={t('Subscribe')}>
|
|
||||||
<Icon name="author-subscribe" class={stylesButton.icon} />
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
onClick={handleFollowClick}
|
|
||||||
isSubscribeButton={true}
|
|
||||||
class={clsx(styles.actionButton, {
|
|
||||||
[styles.iconed]: props.iconButtons,
|
|
||||||
[stylesButton.subscribed]: isFollowed(),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
|
||||||
size="S"
|
|
||||||
value={
|
|
||||||
<Show
|
|
||||||
when={props.iconButtons}
|
|
||||||
fallback={
|
|
||||||
<>
|
|
||||||
<span class={styles.actionButtonLabel}>{t('Following')}</span>
|
|
||||||
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon name="author-unsubscribe" class={stylesButton.icon} />
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
onClick={handleFollowClick}
|
|
||||||
isSubscribeButton={true}
|
|
||||||
class={clsx(styles.actionButton, {
|
|
||||||
[styles.iconed]: props.iconButtons,
|
|
||||||
[stylesButton.subscribed]: isFollowed(),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
<Show when={props.showMessageButton}>
|
<Show when={props.showMessageButton}>
|
||||||
<Button
|
<Button
|
||||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||||
|
|
|
@ -34,16 +34,18 @@ export const AuthorCard = (props: Props) => {
|
||||||
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 [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
||||||
const [isFollowed, setIsFollowed] = createSignal<boolean>()
|
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
|
||||||
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
|
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
|
||||||
const { setFollowing, isOwnerSubscribed } = useFollowing()
|
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setAuthorSubs(props.following)
|
setAuthorSubs(props.following)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setIsFollowed(isOwnerSubscribed(props.author?.id))
|
if (!(subscriptions && props.author)) return
|
||||||
|
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
|
||||||
|
setIsSubscribed(subscribed)
|
||||||
})
|
})
|
||||||
|
|
||||||
const name = createMemo(() => {
|
const name = createMemo(() => {
|
||||||
|
@ -83,15 +85,19 @@ export const AuthorCard = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleFollowClick = () => {
|
const handleFollowClick = () => {
|
||||||
const value = !isFollowed()
|
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
setIsFollowed(value)
|
isSubscribed()
|
||||||
setFollowing(FollowingEntity.Author, props.author.slug, value)
|
? unfollow(FollowingEntity.Author, props.author.slug)
|
||||||
|
: follow(FollowingEntity.Author, props.author.slug)
|
||||||
}, 'subscribe')
|
}, 'subscribe')
|
||||||
}
|
}
|
||||||
|
|
||||||
const followButtonText = createMemo(() => {
|
const followButtonText = createMemo(() => {
|
||||||
if (isOwnerSubscribed(props.author?.id)) {
|
if (subscribeInAction()?.slug === props.author.slug) {
|
||||||
|
return subscribeInAction().type === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSubscribed()) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
||||||
|
@ -119,12 +125,7 @@ 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
|
<Show when={props.followers?.length > 0 || props.following?.length > 0}>
|
||||||
when={
|
|
||||||
(props.followers && props.followers.length > 0) ||
|
|
||||||
(props.following && props.following.length > 0)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class={styles.subscribersContainer}>
|
<div class={styles.subscribersContainer}>
|
||||||
<Show when={props.followers && props.followers.length > 0}>
|
<Show when={props.followers && props.followers.length > 0}>
|
||||||
<a href="?m=followers" class={styles.subscribers}>
|
<a href="?m=followers" class={styles.subscribers}>
|
||||||
|
@ -204,13 +205,14 @@ export const AuthorCard = (props: Props) => {
|
||||||
when={isProfileOwner()}
|
when={isProfileOwner()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={styles.authorActions}>
|
<div class={styles.authorActions}>
|
||||||
<Show when={authorSubs().length}>
|
<Show when={authorSubs()?.length}>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleFollowClick}
|
onClick={handleFollowClick}
|
||||||
|
disabled={Boolean(subscribeInAction())}
|
||||||
value={followButtonText()}
|
value={followButtonText()}
|
||||||
isSubscribeButton={true}
|
isSubscribeButton={true}
|
||||||
class={clsx({
|
class={clsx({
|
||||||
[stylesButton.subscribed]: isFollowed(),
|
[stylesButton.subscribed]: isSubscribed(),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -255,15 +257,7 @@ export const AuthorCard = (props: Props) => {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-24">
|
<div class="col-24">
|
||||||
<For each={props.followers}>
|
<For each={props.followers}>
|
||||||
{(follower: Author) => (
|
{(follower: Author) => <AuthorBadge author={follower} />}
|
||||||
<AuthorBadge
|
|
||||||
author={follower}
|
|
||||||
isFollowed={{
|
|
||||||
loaded: Boolean(authorSubs()),
|
|
||||||
value: isOwnerSubscribed(follower.id),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -318,21 +312,9 @@ export const AuthorCard = (props: Props) => {
|
||||||
<For each={authorSubs()}>
|
<For each={authorSubs()}>
|
||||||
{(subscription) =>
|
{(subscription) =>
|
||||||
isAuthor(subscription) ? (
|
isAuthor(subscription) ? (
|
||||||
<AuthorBadge
|
<AuthorBadge author={subscription} />
|
||||||
isFollowed={{
|
|
||||||
loaded: Boolean(authorSubs()),
|
|
||||||
value: isOwnerSubscribed(subscription.id),
|
|
||||||
}}
|
|
||||||
author={subscription}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<TopicBadge
|
<TopicBadge topic={subscription} />
|
||||||
isFollowed={{
|
|
||||||
loaded: Boolean(authorSubs()),
|
|
||||||
value: isOwnerSubscribed(subscription.id),
|
|
||||||
}}
|
|
||||||
topic={subscription}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createSignal, on, onMount } from 'solid-js'
|
import { For, Show, createEffect, createSignal, on } from 'solid-js'
|
||||||
import { useFollowing } from '../../context/following'
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { apiClient } from '../../graphql/client/core'
|
import { apiClient } from '../../graphql/client/core'
|
||||||
import { Author } from '../../graphql/schema/core.gen'
|
|
||||||
import { setAuthorsByFollowers, setAuthorsByShouts, useAuthorsStore } from '../../stores/zine/authors'
|
import { setAuthorsByFollowers, setAuthorsByShouts, useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||||
import { InlineLoader } from '../InlineLoader'
|
import { InlineLoader } from '../InlineLoader'
|
||||||
|
@ -21,7 +19,6 @@ const PAGE_SIZE = 20
|
||||||
|
|
||||||
export const AuthorsList = (props: Props) => {
|
export const AuthorsList = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { isOwnerSubscribed } = useFollowing()
|
|
||||||
const { authorsByShouts, authorsByFollowers } = useAuthorsStore()
|
const { authorsByShouts, authorsByFollowers } = useAuthorsStore()
|
||||||
const [loading, setLoading] = createSignal(false)
|
const [loading, setLoading] = createSignal(false)
|
||||||
const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 })
|
const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 })
|
||||||
|
@ -83,13 +80,7 @@ export const AuthorsList = (props: Props) => {
|
||||||
{(author) => (
|
{(author) => (
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-20 col-xl-18">
|
<div class="col-lg-20 col-xl-18">
|
||||||
<AuthorBadge
|
<AuthorBadge author={author} />
|
||||||
author={author}
|
|
||||||
isFollowed={{
|
|
||||||
loaded: !loading(),
|
|
||||||
value: isOwnerSubscribed(author.id),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { showModal } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
import { AuthModalSearchParams } from '../Nav/AuthModal/types'
|
import type { AuthModalSearchParams } from '../Nav/AuthModal/types'
|
||||||
|
|
||||||
import styles from './Hero.module.scss'
|
import styles from './Hero.module.scss'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { Buffer } from 'buffer'
|
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show } from 'solid-js'
|
import { Show } from 'solid-js'
|
||||||
|
|
||||||
|
@ -9,6 +7,7 @@ import { composeMediaItems } from '../../../utils/composeMediaItems'
|
||||||
import { AudioPlayer } from '../../Article/AudioPlayer'
|
import { AudioPlayer } from '../../Article/AudioPlayer'
|
||||||
import { DropArea } from '../../_shared/DropArea'
|
import { DropArea } from '../../_shared/DropArea'
|
||||||
|
|
||||||
|
// import { Buffer } from 'node:buffer'
|
||||||
import styles from './AudioUploader.module.scss'
|
import styles from './AudioUploader.module.scss'
|
||||||
|
|
||||||
window.Buffer = Buffer
|
window.Buffer = Buffer
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import type { Doc } from 'yjs/dist/src/utils/Doc'
|
|
||||||
|
|
||||||
import { HocuspocusProvider } from '@hocuspocus/provider'
|
import { HocuspocusProvider } from '@hocuspocus/provider'
|
||||||
import { isTextSelection } from '@tiptap/core'
|
import { isTextSelection } from '@tiptap/core'
|
||||||
import { Bold } from '@tiptap/extension-bold'
|
import { Bold } from '@tiptap/extension-bold'
|
||||||
|
@ -30,7 +28,7 @@ import { Underline } from '@tiptap/extension-underline'
|
||||||
import { createEffect, createSignal, onCleanup } from 'solid-js'
|
import { createEffect, createSignal, onCleanup } from 'solid-js'
|
||||||
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||||
import uniqolor from 'uniqolor'
|
import uniqolor from 'uniqolor'
|
||||||
import * as Y from 'yjs'
|
import { Doc } from 'yjs'
|
||||||
|
|
||||||
import { useEditorContext } from '../../context/editor'
|
import { useEditorContext } from '../../context/editor'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
@ -85,7 +83,7 @@ export const Editor = (props: Props) => {
|
||||||
const docName = `shout-${props.shoutId}`
|
const docName = `shout-${props.shoutId}`
|
||||||
|
|
||||||
if (!yDocs[docName]) {
|
if (!yDocs[docName]) {
|
||||||
yDocs[docName] = new Y.Doc()
|
yDocs[docName] = new Doc()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!providers[docName]) {
|
if (!providers[docName]) {
|
||||||
|
|
|
@ -29,8 +29,10 @@ const embedData = (data) => {
|
||||||
const result: { src: string; width?: string; height?: string } = { src: '' }
|
const result: { src: string; width?: string; height?: string } = { src: '' }
|
||||||
|
|
||||||
for (let i = 0; i < attributes.length; i++) {
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
const attribute = attributes[i]
|
const attribute = attributes.item(i)
|
||||||
result[attribute.name] = attribute.value
|
if (attribute) {
|
||||||
|
result[attribute.name] = attribute.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -13,8 +13,6 @@ import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
import { DarkModeToggle } from '../../_shared/DarkModeToggle'
|
import { DarkModeToggle } from '../../_shared/DarkModeToggle'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
|
||||||
import { useSnackbar } from '../../../context/snackbar'
|
|
||||||
import styles from './Panel.module.scss'
|
import styles from './Panel.module.scss'
|
||||||
|
|
||||||
const typograf = new Typograf({ locale: ['ru', 'en-US'] })
|
const typograf = new Typograf({ locale: ['ru', 'en-US'] })
|
||||||
|
@ -25,8 +23,16 @@ type Props = {
|
||||||
|
|
||||||
export const Panel = (props: Props) => {
|
export const Panel = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { isEditorPanelVisible, wordCounter, editorRef, form, toggleEditorPanel, saveShout, publishShout } =
|
const {
|
||||||
useEditorContext()
|
isEditorPanelVisible,
|
||||||
|
wordCounter,
|
||||||
|
editorRef,
|
||||||
|
form,
|
||||||
|
toggleEditorPanel,
|
||||||
|
saveShout,
|
||||||
|
saveDraft,
|
||||||
|
publishShout,
|
||||||
|
} = useEditorContext()
|
||||||
|
|
||||||
const containerRef: { current: HTMLElement } = { current: null }
|
const containerRef: { current: HTMLElement } = { current: null }
|
||||||
const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false)
|
const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false)
|
||||||
|
@ -45,7 +51,12 @@ export const Panel = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSaveClick = () => {
|
const handleSaveClick = () => {
|
||||||
saveShout(form)
|
const hasTopics = form.selectedTopics?.length > 0
|
||||||
|
if (hasTopics) {
|
||||||
|
saveShout(form)
|
||||||
|
} else {
|
||||||
|
saveDraft(form)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = useEditorHTML(() => editorRef.current())
|
const html = useEditorHTML(() => editorRef.current())
|
||||||
|
|
|
@ -4,8 +4,6 @@ import type { Author, Shout, Topic } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show } from 'solid-js'
|
import { For, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useFollowing } from '../../context/following'
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||||
import { TopicCard } from '../Topic/Card'
|
import { TopicCard } from '../Topic/Card'
|
||||||
|
@ -30,7 +28,6 @@ type Props = {
|
||||||
|
|
||||||
export const Beside = (props: Props) => {
|
export const Beside = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { isOwnerSubscribed } = useFollowing()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={!!props.beside?.slug && props.values?.length > 0}>
|
<Show when={!!props.beside?.slug && props.values?.length > 0}>
|
||||||
|
@ -86,12 +83,7 @@ export const Beside = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.wrapper === 'author'}>
|
<Show when={props.wrapper === 'author'}>
|
||||||
<AuthorBadge
|
<AuthorBadge author={value as Author} />
|
||||||
author={value as Author}
|
|
||||||
isFollowed={{
|
|
||||||
value: isOwnerSubscribed(value.id),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.wrapper === 'article' && value?.slug}>
|
<Show when={props.wrapper === 'article' && value?.slug}>
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
|
|
|
@ -84,6 +84,10 @@
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
right: 2px;
|
right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsLabel {
|
.settingsLabel {
|
||||||
|
@ -136,20 +140,22 @@
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::after {
|
.icon {
|
||||||
content: '+';
|
margin: 0;
|
||||||
font-size: 1.6em;
|
min-width: 1.8rem;
|
||||||
line-height: 1;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 2.5rem;
|
right: 1.7rem;
|
||||||
top: -0.2em;
|
top: 50%;
|
||||||
|
transform: translateY(-50%) rotate(180deg);
|
||||||
|
transform-origin: center;
|
||||||
transition: transform 0.3s;
|
transition: transform 0.3s;
|
||||||
|
width: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.opened {
|
&.opened {
|
||||||
&::after {
|
.icon {
|
||||||
right: 0.9rem;
|
right: 0;
|
||||||
transform: rotate(45deg);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,7 @@ export const Sidebar = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('My subscriptions')}
|
{t('My subscriptions')}
|
||||||
|
<Icon name="toggle-arrow" class={styles.icon} />
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}>
|
<ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { clsx } from 'clsx'
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { Loading } from '../_shared/Loading'
|
import { Loading } from '../_shared/Loading'
|
||||||
import styles from './InlineLoader.module.scss'
|
import styles from './InlineLoader.module.scss'
|
||||||
|
@ -7,7 +6,7 @@ type Props = {
|
||||||
class?: string
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InlineLoader = (props: Props) => {
|
export const InlineLoader = (_props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
return (
|
return (
|
||||||
<div class={styles.InlineLoader}>
|
<div class={styles.InlineLoader}>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { JSX, Show, createEffect, createSignal } from 'solid-js'
|
import { JSX, Show, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
@ -31,8 +31,8 @@ export const LoginForm = () => {
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
// FIXME: use signal or remove
|
||||||
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
const [_isLinkSent, setIsLinkSent] = createSignal(false)
|
||||||
const authFormRef: { current: HTMLFormElement } = { current: null }
|
const authFormRef: { current: HTMLFormElement } = { current: null }
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
const { signIn } = useSession()
|
const { signIn } = useSession()
|
||||||
|
|
|
@ -32,7 +32,8 @@ export const RegisterForm = () => {
|
||||||
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { signUp, isRegistered, resendVerifyEmail } = useSession()
|
const { signUp, isRegistered, resendVerifyEmail } = useSession()
|
||||||
const [submitError, setSubmitError] = createSignal('')
|
// FIXME: use submit error data or remove signal
|
||||||
|
const [_submitError, setSubmitError] = createSignal('')
|
||||||
const [fullName, setFullName] = createSignal('')
|
const [fullName, setFullName] = createSignal('')
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
|
@ -116,7 +117,7 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
const handleCheckEmailStatus = (status: EmailStatus | string) => {
|
const handleCheckEmailStatus = (status: EmailStatus | string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'not verified':
|
case 'not verified': {
|
||||||
setValidationErrors((prev) => ({
|
setValidationErrors((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
email: (
|
email: (
|
||||||
|
@ -129,8 +130,9 @@ export const RegisterForm = () => {
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
break
|
break
|
||||||
case 'verified':
|
}
|
||||||
setValidationErrors((prev) => ({
|
case 'verified': {
|
||||||
|
setValidationErrors((_prev) => ({
|
||||||
email: (
|
email: (
|
||||||
<>
|
<>
|
||||||
{t('This email is registered')}. {t('try')}
|
{t('This email is registered')}. {t('try')}
|
||||||
|
@ -142,7 +144,8 @@ export const RegisterForm = () => {
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
break
|
break
|
||||||
case 'registered':
|
}
|
||||||
|
case 'registered': {
|
||||||
setValidationErrors((prev) => ({
|
setValidationErrors((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
email: (
|
email: (
|
||||||
|
@ -156,9 +159,11 @@ export const RegisterForm = () => {
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
break
|
break
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
console.info('[RegisterForm] email is not registered')
|
console.info('[RegisterForm] email is not registered')
|
||||||
break
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,14 +32,14 @@ const MD_WIDTH_BREAKPOINT = 992
|
||||||
export const HeaderAuth = (props: Props) => {
|
export const HeaderAuth = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const { session, author, isAuthenticated, isSessionLoaded } = useSession()
|
const { session, author, isSessionLoaded } = useSession()
|
||||||
const { unreadNotificationsCount, showNotificationsPanel } = useNotifications()
|
const { unreadNotificationsCount, showNotificationsPanel } = useNotifications()
|
||||||
const { form, toggleEditorPanel, saveShout, publishShout } = useEditorContext()
|
const { form, toggleEditorPanel, saveShout, saveDraft, publishShout } = useEditorContext()
|
||||||
|
|
||||||
const handleBellIconClick = (event: Event) => {
|
const handleBellIconClick = (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
if (!isAuthenticated()) {
|
if (!author()?.id) {
|
||||||
showModal('auth')
|
showModal('auth')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -48,19 +48,22 @@ export const HeaderAuth = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings')
|
const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings')
|
||||||
const isNotificationsVisible = createMemo(() => isAuthenticated() && !isEditorPage())
|
const isNotificationsVisible = createMemo(() => author()?.id && !isEditorPage())
|
||||||
const isSaveButtonVisible = createMemo(() => isAuthenticated() && isEditorPage())
|
const isSaveButtonVisible = createMemo(() => author()?.id && isEditorPage())
|
||||||
const isCreatePostButtonVisible = createMemo(() => !isEditorPage())
|
const isCreatePostButtonVisible = createMemo(() => !isEditorPage())
|
||||||
const isAuthenticatedControlsVisible = createMemo(
|
const isAuthenticatedControlsVisible = createMemo(() => author()?.id && session()?.user?.email_verified)
|
||||||
() => isAuthenticated() && session()?.user?.email_verified,
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleBurgerButtonClick = () => {
|
const handleBurgerButtonClick = () => {
|
||||||
toggleEditorPanel()
|
toggleEditorPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveButtonClick = () => {
|
const handleSaveClick = () => {
|
||||||
saveShout(form)
|
const hasTopics = form.selectedTopics?.length > 0
|
||||||
|
if (hasTopics) {
|
||||||
|
saveShout(form)
|
||||||
|
} else {
|
||||||
|
saveDraft(form)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [width, setWidth] = createSignal(0)
|
const [width, setWidth] = createSignal(0)
|
||||||
|
@ -106,7 +109,7 @@ export const HeaderAuth = (props: Props) => {
|
||||||
<Show when={isSessionLoaded()} keyed={true}>
|
<Show when={isSessionLoaded()} keyed={true}>
|
||||||
<div class={clsx('col-auto col-lg-7', styles.usernav)}>
|
<div class={clsx('col-auto col-lg-7', styles.usernav)}>
|
||||||
<div class={styles.userControl}>
|
<div class={styles.userControl}>
|
||||||
<Show when={isCreatePostButtonVisible() && isAuthenticated()}>
|
<Show when={isCreatePostButtonVisible() && author()?.id}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
<a href={getPagePath(router, 'create')}>
|
<a href={getPagePath(router, 'create')}>
|
||||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||||
|
@ -214,7 +217,7 @@ export const HeaderAuth = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={isCreatePostButtonVisible() && !isAuthenticated()}>
|
<Show when={isCreatePostButtonVisible() && !author()?.id}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
<a href={getPagePath(router, 'create')}>
|
<a href={getPagePath(router, 'create')}>
|
||||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||||
|
@ -227,29 +230,42 @@ export const HeaderAuth = (props: Props) => {
|
||||||
<Show
|
<Show
|
||||||
when={isAuthenticatedControlsVisible()}
|
when={isAuthenticatedControlsVisible()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
<Show when={!author()?.id}>
|
||||||
<a href="?m=auth&mode=login">
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
||||||
<span class={styles.textLabel}>{t('Enter')}</span>
|
<a href="?m=auth&mode=login">
|
||||||
<Icon name="key" class={styles.icon} />
|
<span class={styles.textLabel}>{t('Enter')}</span>
|
||||||
{/*<Icon name="user-default" class={clsx(styles.icon, styles.iconHover)} />*/}
|
<Icon name="key" class={styles.icon} />
|
||||||
</a>
|
{/*<Icon name="user-default" class={clsx(styles.icon, styles.iconHover)} />*/}
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show when={!isSaveButtonVisible()}>
|
<Show
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
when={isSaveButtonVisible()}
|
||||||
<a href={getPagePath(router, 'inbox')}>
|
fallback={
|
||||||
<div classList={{ entered: page().path === '/inbox' }}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
||||||
<Icon name="inbox-white" class={styles.icon} />
|
<a href={getPagePath(router, 'inbox')}>
|
||||||
<Icon name="inbox-white-hover" class={clsx(styles.icon, styles.iconHover)} />
|
<div classList={{ entered: page().path === '/inbox' }}>
|
||||||
</div>
|
<Icon name="inbox-white" class={styles.icon} />
|
||||||
</a>
|
<Icon name="inbox-white-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
|
<button onClick={handleSaveClick}>
|
||||||
|
<span class={styles.textLabel}>{t('Save')}</span>
|
||||||
|
<Icon name="save" class={styles.icon} />
|
||||||
|
<Icon name="save" class={clsx(styles.icon, styles.iconHover)} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={isAuthenticated()}>
|
<Show when={author()?.id}>
|
||||||
<ProfilePopup
|
<ProfilePopup
|
||||||
onVisibilityChange={(isVisible) => {
|
onVisibilityChange={(isVisible) => {
|
||||||
props.setIsProfilePopupVisible(isVisible)
|
props.setIsProfilePopupVisible(isVisible)
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const NotificationGroup = (props: NotificationGroupProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<For each={props.notifications}>
|
<For each={props.notifications}>
|
||||||
{(n: Group, index) => (
|
{(n: Group, _index) => (
|
||||||
<>
|
<>
|
||||||
{t(threadCaption(n.thread), { commentsCount: n.reactions.length })}{' '}
|
{t(threadCaption(n.thread), { commentsCount: n.reactions.length })}{' '}
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -46,7 +46,7 @@ const isEarlier = (date: Date) => {
|
||||||
|
|
||||||
export const NotificationsPanel = (props: Props) => {
|
export const NotificationsPanel = (props: Props) => {
|
||||||
const [isLoading, setIsLoading] = createSignal(false)
|
const [isLoading, setIsLoading] = createSignal(false)
|
||||||
const { isAuthenticated } = useSession()
|
const { author } = useSession()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const {
|
const {
|
||||||
after,
|
after,
|
||||||
|
@ -150,16 +150,13 @@ export const NotificationsPanel = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(author, async (a) => {
|
||||||
() => isAuthenticated(),
|
if (a?.id) {
|
||||||
async () => {
|
setIsLoading(true)
|
||||||
if (isAuthenticated()) {
|
await loadNextPage()
|
||||||
setIsLoading(true)
|
setIsLoading(false)
|
||||||
await loadNextPage()
|
}
|
||||||
setIsLoading(false)
|
}),
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -195,7 +195,7 @@ export const ProfileSettings = () => {
|
||||||
<div class="col-md-20 col-lg-18 col-xl-16">
|
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||||
<h1>{t('Profile settings')}</h1>
|
<h1>{t('Profile settings')}</h1>
|
||||||
<p class="description">{t('Here you can customize your profile the way you want.')}</p>
|
<p class="description">{t('Here you can customize your profile the way you want.')}</p>
|
||||||
<form enctype="multipart/form-data">
|
<form enctype="multipart/form-data" autocomplete="off">
|
||||||
<h4>{t('Userpic')}</h4>
|
<h4>{t('Userpic')}</h4>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<div
|
<div
|
||||||
|
@ -262,14 +262,16 @@ export const ProfileSettings = () => {
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="username"
|
name="nameOfUser"
|
||||||
id="username"
|
id="nameOfUser"
|
||||||
|
data-lpignore="true"
|
||||||
|
autocomplete="one-time-code"
|
||||||
placeholder={t('Name')}
|
placeholder={t('Name')}
|
||||||
onInput={(event) => updateFormField('name', event.currentTarget.value)}
|
onInput={(event) => updateFormField('name', event.currentTarget.value)}
|
||||||
value={form.name}
|
value={form.name}
|
||||||
ref={(el) => (nameInputRef.current = el)}
|
ref={(el) => (nameInputRef.current = el)}
|
||||||
/>
|
/>
|
||||||
<label for="username">{t('Name')}</label>
|
<label for="nameOfUser">{t('Name')}</label>
|
||||||
<Show when={nameError()}>
|
<Show when={nameError()}>
|
||||||
<div
|
<div
|
||||||
style={{ position: 'absolute', 'margin-top': '-4px' }}
|
style={{ position: 'absolute', 'margin-top': '-4px' }}
|
||||||
|
@ -289,6 +291,8 @@ export const ProfileSettings = () => {
|
||||||
type="text"
|
type="text"
|
||||||
name="user-address"
|
name="user-address"
|
||||||
id="user-address"
|
id="user-address"
|
||||||
|
data-lpignore="true"
|
||||||
|
autocomplete="one-time-code2"
|
||||||
onInput={(event) => updateFormField('slug', event.currentTarget.value)}
|
onInput={(event) => updateFormField('slug', event.currentTarget.value)}
|
||||||
value={form.slug}
|
value={form.slug}
|
||||||
ref={(el) => (slugInputRef.current = el)}
|
ref={(el) => (slugInputRef.current = el)}
|
||||||
|
|
|
@ -17,7 +17,7 @@ interface Props {
|
||||||
|
|
||||||
const isInViewport = (el: Element): boolean => {
|
const isInViewport = (el: Element): boolean => {
|
||||||
const rect = el.getBoundingClientRect()
|
const rect = el.getBoundingClientRect()
|
||||||
return rect.top <= DEFAULT_HEADER_OFFSET
|
return rect.top <= DEFAULT_HEADER_OFFSET + 24 // default offset + 1.5em (default header margin-top)
|
||||||
}
|
}
|
||||||
const scrollToHeader = (element) => {
|
const scrollToHeader = (element) => {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createMemo, createSignal } from 'solid-js'
|
import { Show, createEffect, createMemo, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useFollowing } from '../../context/following'
|
import { useFollowing } from '../../context/following'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
@ -38,14 +38,20 @@ 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 { setFollowing, loading: subLoading } = useFollowing()
|
const [isSubscribed, setIsSubscribed] = createSignal()
|
||||||
const [followed, setFollowed] = createSignal()
|
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!(subscriptions && props.topic)) return
|
||||||
|
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
|
||||||
|
setIsSubscribed(subscribed)
|
||||||
|
})
|
||||||
|
|
||||||
const handleFollowClick = () => {
|
const handleFollowClick = () => {
|
||||||
const value = !followed()
|
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
setFollowed(value)
|
isSubscribed()
|
||||||
setFollowing(FollowingEntity.Topic, props.topic.slug, value)
|
? unfollow(FollowingEntity.Topic, props.topic.slug)
|
||||||
|
: follow(FollowingEntity.Topic, props.topic.slug)
|
||||||
}, 'subscribe')
|
}, 'subscribe')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,12 +59,12 @@ export const TopicCard = (props: TopicProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={props.iconButton}>
|
<Show when={props.iconButton}>
|
||||||
<Show when={followed()} fallback="+">
|
<Show when={isSubscribed()} fallback="+">
|
||||||
<Icon name="check-subscribed" />
|
<Icon name="check-subscribed" />
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!props.iconButton}>
|
<Show when={!props.iconButton}>
|
||||||
<Show when={followed()} fallback={t('Follow')}>
|
<Show when={isSubscribed()} fallback={t('Follow')}>
|
||||||
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
|
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
|
||||||
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -130,7 +136,7 @@ export const TopicCard = (props: TopicProps) => {
|
||||||
fallback={
|
fallback={
|
||||||
<CheckButton
|
<CheckButton
|
||||||
text={t('Follow')}
|
text={t('Follow')}
|
||||||
checked={Boolean(followed())}
|
checked={Boolean(isSubscribed())}
|
||||||
onClick={handleFollowClick}
|
onClick={handleFollowClick}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -142,10 +148,10 @@ export const TopicCard = (props: TopicProps) => {
|
||||||
onClick={handleFollowClick}
|
onClick={handleFollowClick}
|
||||||
isSubscribeButton={true}
|
isSubscribeButton={true}
|
||||||
class={clsx(styles.actionButton, {
|
class={clsx(styles.actionButton, {
|
||||||
[styles.isSubscribing]: subLoading(),
|
[styles.isSubscribing]:
|
||||||
[stylesButton.subscribed]: followed(),
|
subscribeInAction()?.slug === props.topic.slug ? subscribeInAction().type : undefined,
|
||||||
|
[stylesButton.subscribed]: isSubscribed(),
|
||||||
})}
|
})}
|
||||||
// disabled={subLoading()}
|
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createEffect, createSignal, on } from 'solid-js'
|
import { Show, createEffect, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useFollowing } from '../../../context/following'
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
@ -8,16 +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 { Button } from '../../_shared/Button'
|
import { BadgeSubscribeButton } from '../../_shared/BadgeSubscribeButton'
|
||||||
import { CheckButton } from '../../_shared/CheckButton'
|
|
||||||
|
|
||||||
import { FollowedInfo } from '../../../pages/types'
|
|
||||||
import styles from './TopicBadge.module.scss'
|
import styles from './TopicBadge.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
minimizeSubscribeButton?: boolean
|
minimizeSubscribeButton?: boolean
|
||||||
isFollowed?: FollowedInfo
|
|
||||||
showStat?: boolean
|
showStat?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,14 +22,20 @@ 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 { setFollowing, loading: subLoading } = useFollowing()
|
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
|
||||||
const [isFollowed, setIsFollowed] = createSignal<boolean>()
|
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!(subscriptions && props.topic)) return
|
||||||
|
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
|
||||||
|
setIsSubscribed(subscribed)
|
||||||
|
})
|
||||||
|
|
||||||
const handleFollowClick = () => {
|
const handleFollowClick = () => {
|
||||||
const value = !isFollowed()
|
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
setIsFollowed(value)
|
isSubscribed()
|
||||||
setFollowing(FollowingEntity.Topic, props.topic.slug, value)
|
? follow(FollowingEntity.Topic, props.topic.slug)
|
||||||
|
: unfollow(FollowingEntity.Topic, props.topic.slug)
|
||||||
}, 'subscribe')
|
}, 'subscribe')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,15 +43,6 @@ export const TopicBadge = (props: Props) => {
|
||||||
setIsMobileView(!mediaMatches.sm)
|
setIsMobileView(!mediaMatches.sm)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => props.isFollowed,
|
|
||||||
() => {
|
|
||||||
setIsFollowed(props.isFollowed.value)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const title = () =>
|
const title = () =>
|
||||||
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
|
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
|
||||||
|
|
||||||
|
@ -83,35 +76,14 @@ export const TopicBadge = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
<Show
|
<BadgeSubscribeButton
|
||||||
when={!props.minimizeSubscribeButton}
|
isSubscribed={isSubscribed()}
|
||||||
fallback={
|
action={handleFollowClick}
|
||||||
<CheckButton text={t('Follow')} checked={Boolean(isFollowed())} onClick={handleFollowClick} />
|
actionMessageType={
|
||||||
|
subscribeInAction()?.slug === props.topic.slug ? subscribeInAction().type : undefined
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<Show
|
|
||||||
when={isFollowed()}
|
|
||||||
fallback={
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="S"
|
|
||||||
value={subLoading() ? t('subscribing...') : t('Subscribe')}
|
|
||||||
onClick={handleFollowClick}
|
|
||||||
class={styles.subscribeButton}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={handleFollowClick}
|
|
||||||
variant="bordered"
|
|
||||||
size="S"
|
|
||||||
value={t('Following')}
|
|
||||||
class={styles.subscribeButton}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.stats}>
|
<div class={styles.stats}>
|
||||||
|
|
|
@ -2,11 +2,11 @@ import type { Author } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '@solidjs/meta'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
|
import { For, Show, createMemo, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import { setAuthorsSort, useAuthorsStore } from '../../../stores/zine/authors'
|
import { useAuthorsStore } from '../../../stores/zine/authors'
|
||||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
import { scrollHandler } from '../../../utils/scroll'
|
import { scrollHandler } from '../../../utils/scroll'
|
||||||
import { authorLetterReduce, translateAuthor } from '../../../utils/translate'
|
import { authorLetterReduce, translateAuthor } from '../../../utils/translate'
|
||||||
|
@ -33,7 +33,7 @@ export const AllAuthors = (props: Props) => {
|
||||||
const [searchQuery, setSearchQuery] = createSignal('')
|
const [searchQuery, setSearchQuery] = createSignal('')
|
||||||
const ALPHABET =
|
const ALPHABET =
|
||||||
lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ@'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ@']
|
lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ@'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ@']
|
||||||
const { searchParams, changeSearchParams } = useRouter<AllAuthorsPageSearchParams>()
|
const { searchParams } = useRouter<AllAuthorsPageSearchParams>()
|
||||||
const { sortedAuthors } = useAuthorsStore({
|
const { sortedAuthors } = useAuthorsStore({
|
||||||
authors: props.authors,
|
authors: props.authors,
|
||||||
sortBy: searchParams().by || 'name',
|
sortBy: searchParams().by || 'name',
|
||||||
|
|
|
@ -3,8 +3,6 @@ import type { Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '@solidjs/meta'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useFollowing } from '../../../context/following'
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import { setTopicsSort, useTopicsStore } from '../../../stores/zine/topics'
|
import { setTopicsSort, useTopicsStore } from '../../../stores/zine/topics'
|
||||||
|
@ -74,8 +72,6 @@ export const AllTopics = (props: Props) => {
|
||||||
return keys
|
return keys
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isOwnerSubscribed } = useFollowing()
|
|
||||||
|
|
||||||
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
||||||
const [searchQuery, setSearchQuery] = createSignal('')
|
const [searchQuery, setSearchQuery] = createSignal('')
|
||||||
const filteredResults = createMemo(() => {
|
const filteredResults = createMemo(() => {
|
||||||
|
@ -188,14 +184,7 @@ export const AllTopics = (props: Props) => {
|
||||||
<For each={filteredResults().slice(0, limit())}>
|
<For each={filteredResults().slice(0, limit())}>
|
||||||
{(topic) => (
|
{(topic) => (
|
||||||
<>
|
<>
|
||||||
<TopicBadge
|
<TopicBadge topic={topic} showStat={true} />
|
||||||
topic={topic}
|
|
||||||
isFollowed={{
|
|
||||||
loaded: filteredResults().length > 0,
|
|
||||||
value: isOwnerSubscribed(topic.slug),
|
|
||||||
}}
|
|
||||||
showStat={true}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/cor
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { Meta, Title } from '@solidjs/meta'
|
import { Meta, Title } from '@solidjs/meta'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, 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'
|
||||||
|
@ -11,7 +11,7 @@ import { useSession } from '../../../context/session'
|
||||||
import { apiClient } from '../../../graphql/client/core'
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
||||||
import { loadAuthor, useAuthorsStore } 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'
|
||||||
|
@ -39,10 +39,9 @@ const LOAD_MORE_PAGE_SIZE = 9
|
||||||
|
|
||||||
export const AuthorView = (props: Props) => {
|
export const AuthorView = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { subscriptions, followers: myFollowers, loadSubscriptions } = useFollowing()
|
const { followers: myFollowers } = useFollowing()
|
||||||
const { session } = useSession()
|
const { session } = useSession()
|
||||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||||
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
|
||||||
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)
|
||||||
|
@ -53,50 +52,41 @@ export const AuthorView = (props: Props) => {
|
||||||
const [commented, setCommented] = createSignal<Reaction[]>()
|
const [commented, setCommented] = createSignal<Reaction[]>()
|
||||||
const modal = MODALS[searchParams().m]
|
const modal = MODALS[searchParams().m]
|
||||||
|
|
||||||
// current author
|
const [sessionChecked, setSessionChecked] = createSignal(false)
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.authorSlug) {
|
if (
|
||||||
if (session()?.user?.app_data?.profile?.slug === props.authorSlug) {
|
!sessionChecked() &&
|
||||||
console.info('my own profile')
|
props.authorSlug &&
|
||||||
const { profile, authors, topics } = session().user.app_data
|
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)
|
setFollowers(myFollowers)
|
||||||
setAuthor(profile)
|
setAuthor(profile)
|
||||||
setFollowing([...authors, ...topics])
|
setFollowing([...authors, ...topics])
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const a = authorEntities()[props.authorSlug]
|
|
||||||
setAuthor(a)
|
|
||||||
// TODO: add following data retrieval
|
|
||||||
console.debug('[Author] expecting following data fetched')
|
|
||||||
} catch (error) {
|
|
||||||
console.debug(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(async () => {
|
|
||||||
if (author()?.id && !author().stat) {
|
|
||||||
const a = await loadAuthor({ slug: '', author_id: author().id })
|
|
||||||
console.debug('[AuthorView] loaded author:', a)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const bioContainerRef: { current: HTMLDivElement } = { current: null }
|
const bioContainerRef: { current: HTMLDivElement } = { current: null }
|
||||||
const bioWrapperRef: { current: HTMLDivElement } = { current: null }
|
const bioWrapperRef: { current: HTMLDivElement } = { current: null }
|
||||||
|
|
||||||
const fetchData = async (slug) => {
|
const fetchData = async (slug: string) => {
|
||||||
try {
|
try {
|
||||||
const [subscriptionsResult, followersResult] = await Promise.all([
|
const [subscriptionsResult, followersResult, authorResult] = await Promise.all([
|
||||||
apiClient.getAuthorFollows({ slug }),
|
apiClient.getAuthorFollows({ slug }),
|
||||||
apiClient.getAuthorFollowers({ slug }),
|
apiClient.getAuthorFollowers({ slug }),
|
||||||
|
loadAuthor({ slug }),
|
||||||
])
|
])
|
||||||
|
|
||||||
const { authors, topics } = subscriptionsResult
|
const { authors, topics } = subscriptionsResult
|
||||||
|
setAuthor(authorResult)
|
||||||
setFollowing([...(authors || []), ...(topics || [])])
|
setFollowing([...(authors || []), ...(topics || [])])
|
||||||
setFollowers(followersResult || [])
|
setFollowers(followersResult || [])
|
||||||
|
|
||||||
console.info('[components.Author] following data loaded')
|
console.debug('[components.Author] following data loaded', subscriptionsResult)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[components.Author] fetch error', error)
|
console.error('[components.Author] fetch error', error)
|
||||||
}
|
}
|
||||||
|
@ -108,14 +98,6 @@ export const AuthorView = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
fetchData(props.authorSlug)
|
|
||||||
|
|
||||||
if (!modal) {
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
saveScrollPosition()
|
saveScrollPosition()
|
||||||
const { hasMore } = await loadShouts({
|
const { hasMore } = await loadShouts({
|
||||||
|
@ -128,13 +110,10 @@ export const AuthorView = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
if (!modal) hideModal()
|
||||||
|
fetchData(props.authorSlug)
|
||||||
checkBioHeight()
|
checkBioHeight()
|
||||||
|
loadMore()
|
||||||
// pagination
|
|
||||||
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
|
||||||
loadMore()
|
|
||||||
loadSubscriptions()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const pages = createMemo<Shout[][]>(() =>
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
|
@ -143,16 +122,22 @@ export const AuthorView = (props: Props) => {
|
||||||
|
|
||||||
const fetchComments = async (commenter: Author) => {
|
const fetchComments = async (commenter: Author) => {
|
||||||
const data = await apiClient.getReactionsBy({
|
const data = await apiClient.getReactionsBy({
|
||||||
by: { comment: false, created_by: commenter.id },
|
by: { comment: true, created_by: commenter.id },
|
||||||
})
|
})
|
||||||
setCommented(data)
|
setCommented(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
const authorSlug = createMemo(() => author()?.slug)
|
||||||
if (author()) {
|
createEffect(
|
||||||
fetchComments(author())
|
on(
|
||||||
}
|
() => authorSlug(),
|
||||||
})
|
() => {
|
||||||
|
fetchData(authorSlug())
|
||||||
|
fetchComments(author())
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const ogImage = createMemo(() =>
|
const ogImage = createMemo(() =>
|
||||||
author()?.pic
|
author()?.pic
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { openPage } from '@nanostores/router'
|
import { openPage } from '@nanostores/router'
|
||||||
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 { useEditorContext } from '../../../context/editor'
|
import { useEditorContext } from '../../../context/editor'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
@ -9,22 +9,24 @@ import { Shout } from '../../../graphql/schema/core.gen'
|
||||||
import { router } from '../../../stores/router'
|
import { router } from '../../../stores/router'
|
||||||
import { Draft } from '../../Draft'
|
import { Draft } from '../../Draft'
|
||||||
|
|
||||||
|
import { Loading } from '../../_shared/Loading'
|
||||||
import styles from './DraftsView.module.scss'
|
import styles from './DraftsView.module.scss'
|
||||||
|
|
||||||
export const DraftsView = () => {
|
export const DraftsView = () => {
|
||||||
const { isAuthenticated, isSessionLoaded } = useSession()
|
const { session } = useSession()
|
||||||
const [drafts, setDrafts] = createSignal<Shout[]>([])
|
const [drafts, setDrafts] = createSignal<Shout[]>([])
|
||||||
|
|
||||||
const loadDrafts = async () => {
|
createEffect(
|
||||||
if (apiClient.private) {
|
on(
|
||||||
const loadedDrafts = await apiClient.getDrafts()
|
() => session(),
|
||||||
setDrafts(loadedDrafts.reverse() || [])
|
async (s) => {
|
||||||
}
|
if (s) {
|
||||||
}
|
const loadedDrafts = await apiClient.getDrafts()
|
||||||
|
setDrafts(loadedDrafts.reverse() || [])
|
||||||
createEffect(() => {
|
}
|
||||||
if (isSessionLoaded()) loadDrafts()
|
},
|
||||||
})
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const { publishShoutById, deleteShout } = useEditorContext()
|
const { publishShoutById, deleteShout } = useEditorContext()
|
||||||
|
|
||||||
|
@ -44,22 +46,20 @@ export const DraftsView = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.DraftsView)}>
|
<div class={clsx(styles.DraftsView)}>
|
||||||
<Show when={isSessionLoaded()}>
|
<Show when={session()?.user?.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">
|
||||||
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
|
<For each={drafts()}>
|
||||||
<For each={drafts()}>
|
{(draft) => (
|
||||||
{(draft) => (
|
<Draft
|
||||||
<Draft
|
class={styles.draft}
|
||||||
class={styles.draft}
|
shout={draft}
|
||||||
shout={draft}
|
onDelete={handleDraftDelete}
|
||||||
onDelete={handleDraftDelete}
|
onPublish={handleDraftPublish}
|
||||||
onPublish={handleDraftPublish}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</For>
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { clsx } from 'clsx'
|
||||||
import deepEqual from 'fast-deep-equal'
|
import deepEqual from 'fast-deep-equal'
|
||||||
import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js'
|
import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
import { throttle } from 'throttle-debounce'
|
||||||
|
|
||||||
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
@ -41,7 +42,9 @@ export const EMPTY_TOPIC: Topic = {
|
||||||
slug: '',
|
slug: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const THROTTLING_INTERVAL = 2000
|
||||||
const AUTO_SAVE_INTERVAL = 5000
|
const AUTO_SAVE_INTERVAL = 5000
|
||||||
|
const AUTO_SAVE_DELAY = 5000
|
||||||
const handleScrollTopButtonClick = (e) => {
|
const handleScrollTopButtonClick = (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
|
@ -65,12 +68,14 @@ export const EditView = (props: Props) => {
|
||||||
} = useEditorContext()
|
} = useEditorContext()
|
||||||
const shoutTopics = props.shout.topics || []
|
const shoutTopics = props.shout.topics || []
|
||||||
|
|
||||||
// TODO: проверить сохранение черновика в local storage (не работает)
|
|
||||||
const draft = getDraftFromLocalStorage(props.shout.id)
|
const draft = getDraftFromLocalStorage(props.shout.id)
|
||||||
|
|
||||||
if (draft) {
|
if (draft) {
|
||||||
setForm(Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id })
|
const draftForm = Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id }
|
||||||
|
setForm(draftForm)
|
||||||
|
console.debug('draft from localstorage: ', draftForm)
|
||||||
} else {
|
} else {
|
||||||
setForm({
|
const draftForm = {
|
||||||
slug: props.shout.slug,
|
slug: props.shout.slug,
|
||||||
shoutId: props.shout.id,
|
shoutId: props.shout.id,
|
||||||
title: props.shout.title,
|
title: props.shout.title,
|
||||||
|
@ -83,7 +88,9 @@ export const EditView = (props: Props) => {
|
||||||
coverImageUrl: props.shout.cover,
|
coverImageUrl: props.shout.cover,
|
||||||
media: props.shout.media,
|
media: props.shout.media,
|
||||||
layout: props.shout.layout,
|
layout: props.shout.layout,
|
||||||
})
|
}
|
||||||
|
setForm(draftForm)
|
||||||
|
console.debug('draft from props data: ', draftForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtitleInput: { current: HTMLTextAreaElement } = { current: null }
|
const subtitleInput: { current: HTMLTextAreaElement } = { current: null }
|
||||||
|
@ -106,9 +113,6 @@ export const EditView = (props: Props) => {
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
window.removeEventListener('scroll', handleScroll)
|
window.removeEventListener('scroll', handleScroll)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||||
const handleBeforeUnload = (event) => {
|
const handleBeforeUnload = (event) => {
|
||||||
if (!deepEqual(prevForm, form)) {
|
if (!deepEqual(prevForm, form)) {
|
||||||
|
@ -180,42 +184,39 @@ export const EditView = (props: Props) => {
|
||||||
|
|
||||||
let autoSaveTimeOutId: number | string | NodeJS.Timeout
|
let autoSaveTimeOutId: number | string | NodeJS.Timeout
|
||||||
|
|
||||||
//TODO: add throttle
|
const autoSave = async () => {
|
||||||
|
const hasChanges = !deepEqual(form, prevForm)
|
||||||
|
const hasTopic = Boolean(form.mainTopic)
|
||||||
|
if (hasChanges || hasTopic) {
|
||||||
|
console.debug('saving draft', form)
|
||||||
|
setSaving(true)
|
||||||
|
saveDraftToLocalStorage(form)
|
||||||
|
await saveDraft(form)
|
||||||
|
setPrevForm(clone(form))
|
||||||
|
setTimeout(() => setSaving(false), AUTO_SAVE_DELAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throttle the autoSave function
|
||||||
|
const throttledAutoSave = throttle(THROTTLING_INTERVAL, autoSave)
|
||||||
|
|
||||||
const autoSaveRecursive = () => {
|
const autoSaveRecursive = () => {
|
||||||
autoSaveTimeOutId = setTimeout(async () => {
|
autoSaveTimeOutId = setTimeout(() => {
|
||||||
const hasChanges = !deepEqual(form, prevForm)
|
throttledAutoSave()
|
||||||
if (hasChanges) {
|
|
||||||
setSaving(true)
|
|
||||||
if (props.shout?.published_at) {
|
|
||||||
saveDraftToLocalStorage(form)
|
|
||||||
} else {
|
|
||||||
await saveDraft(form)
|
|
||||||
}
|
|
||||||
setPrevForm(clone(form))
|
|
||||||
setTimeout(() => {
|
|
||||||
setSaving(false)
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
autoSaveRecursive()
|
autoSaveRecursive()
|
||||||
}, AUTO_SAVE_INTERVAL)
|
}, AUTO_SAVE_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopAutoSave = () => {
|
|
||||||
clearTimeout(autoSaveTimeOutId)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
autoSaveRecursive()
|
autoSaveRecursive()
|
||||||
})
|
onCleanup(() => clearTimeout(autoSaveTimeOutId))
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
stopAutoSave()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const showSubtitleInput = () => {
|
const showSubtitleInput = () => {
|
||||||
setIsSubtitleVisible(true)
|
setIsSubtitleVisible(true)
|
||||||
subtitleInput.current.focus()
|
subtitleInput.current.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
const showLeadInput = () => {
|
const showLeadInput = () => {
|
||||||
setIsLeadVisible(true)
|
setIsLeadVisible(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { apiClient } from '../../../graphql/client/core'
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '../../../graphql/schema/core.gen'
|
import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '../../../graphql/schema/core.gen'
|
||||||
import { LayoutType } from '../../../pages/types'
|
import { LayoutType } from '../../../pages/types'
|
||||||
import { router } from '../../../stores/router'
|
import { router } from '../../../stores/router'
|
||||||
import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles'
|
|
||||||
import { getUnixtime } from '../../../utils/getServerDate'
|
import { getUnixtime } from '../../../utils/getServerDate'
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
|
||||||
import { splitToPages } from '../../../utils/splitToPages'
|
|
||||||
import { ArticleCard } from '../../Feed/ArticleCard'
|
import { ArticleCard } from '../../Feed/ArticleCard'
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
||||||
|
@ -28,19 +26,12 @@ export const PRERENDERED_ARTICLES_COUNT = 36
|
||||||
const LOAD_MORE_PAGE_SIZE = 12
|
const LOAD_MORE_PAGE_SIZE = 12
|
||||||
|
|
||||||
export const Expo = (props: Props) => {
|
export const Expo = (props: Props) => {
|
||||||
const [isLoaded, setIsLoaded] = createSignal<boolean>(Boolean(props.shouts))
|
const { t } = useLocalize()
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
|
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
|
||||||
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
||||||
|
const [articlesEndPage, setArticlesEndPage] = createSignal<number>(PRERENDERED_ARTICLES_COUNT)
|
||||||
const { t } = useLocalize()
|
const [expoShouts, setExpoShouts] = createSignal<Shout[]>([])
|
||||||
|
|
||||||
const { sortedArticles } = useArticlesStore({
|
|
||||||
shouts: isLoaded() ? props.shouts : [],
|
|
||||||
layout: props.layout,
|
|
||||||
})
|
|
||||||
|
|
||||||
const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => {
|
const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => {
|
||||||
const filters = { ...additionalFilters }
|
const filters = { ...additionalFilters }
|
||||||
|
|
||||||
|
@ -58,15 +49,18 @@ export const Expo = (props: Props) => {
|
||||||
const options: LoadShoutsOptions = {
|
const options: LoadShoutsOptions = {
|
||||||
filters: getLoadShoutsFilters(),
|
filters: getLoadShoutsFilters(),
|
||||||
limit: count,
|
limit: count,
|
||||||
offset: sortedArticles().length,
|
offset: expoShouts().length,
|
||||||
}
|
}
|
||||||
|
|
||||||
options.filters = props.layout
|
options.filters = props.layout
|
||||||
? { layouts: [props.layout] }
|
? { layouts: [props.layout] }
|
||||||
: { layouts: ['audio', 'video', 'image', 'literature'] }
|
: { layouts: ['audio', 'video', 'image', 'literature'] }
|
||||||
|
|
||||||
const { hasMore } = await loadShouts(options)
|
const newShouts = await apiClient.getShouts(options)
|
||||||
|
const hasMore = newShouts?.length !== options.limit + 1 && newShouts?.length !== 0
|
||||||
setIsLoadMoreButtonVisible(hasMore)
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
|
|
||||||
|
setExpoShouts((prev) => [...prev, ...newShouts])
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadMoreWithoutScrolling = async (count: number) => {
|
const loadMoreWithoutScrolling = async (count: number) => {
|
||||||
|
@ -100,19 +94,7 @@ export const Expo = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (isLoaded()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
|
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
|
||||||
setIsLoaded(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
|
||||||
loadMore(LOAD_MORE_PAGE_SIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
loadRandomTopArticles()
|
loadRandomTopArticles()
|
||||||
loadRandomTopMonthArticles()
|
loadRandomTopMonthArticles()
|
||||||
})
|
})
|
||||||
|
@ -121,9 +103,11 @@ export const Expo = (props: Props) => {
|
||||||
on(
|
on(
|
||||||
() => props.layout,
|
() => props.layout,
|
||||||
() => {
|
() => {
|
||||||
resetSortedArticles()
|
setExpoShouts([])
|
||||||
|
setIsLoadMoreButtonVisible(false)
|
||||||
setFavoriteTopArticles([])
|
setFavoriteTopArticles([])
|
||||||
setReactedTopMonthArticles([])
|
setReactedTopMonthArticles([])
|
||||||
|
setArticlesEndPage(PRERENDERED_ARTICLES_COUNT)
|
||||||
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
|
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
|
||||||
loadRandomTopArticles()
|
loadRandomTopArticles()
|
||||||
loadRandomTopMonthArticles()
|
loadRandomTopMonthArticles()
|
||||||
|
@ -132,16 +116,17 @@ export const Expo = (props: Props) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
resetSortedArticles()
|
setExpoShouts([])
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleLoadMoreClick = () => {
|
const handleLoadMoreClick = () => {
|
||||||
loadMoreWithoutScrolling(LOAD_MORE_PAGE_SIZE)
|
loadMoreWithoutScrolling(LOAD_MORE_PAGE_SIZE)
|
||||||
|
setArticlesEndPage((prev) => prev + LOAD_MORE_PAGE_SIZE)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.Expo}>
|
<div class={styles.Expo}>
|
||||||
<Show when={sortedArticles()?.length > 0} fallback={<Loading />}>
|
<Show when={expoShouts().length > 0} fallback={<Loading />}>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<ul class={clsx('view-switcher')}>
|
<ul class={clsx('view-switcher')}>
|
||||||
<li class={clsx({ 'view-switcher__item--selected': !props.layout })}>
|
<li class={clsx({ 'view-switcher__item--selected': !props.layout })}>
|
||||||
|
@ -194,7 +179,7 @@ export const Expo = (props: Props) => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<For each={sortedArticles().slice(0, LOAD_MORE_PAGE_SIZE)}>
|
<For each={expoShouts()?.slice(0, LOAD_MORE_PAGE_SIZE)}>
|
||||||
{(shout) => (
|
{(shout) => (
|
||||||
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
|
@ -209,7 +194,7 @@ export const Expo = (props: Props) => {
|
||||||
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
|
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
|
||||||
<ArticleCardSwiper title={t('Top month articles')} slides={reactedTopMonthArticles()} />
|
<ArticleCardSwiper title={t('Top month articles')} slides={reactedTopMonthArticles()} />
|
||||||
</Show>
|
</Show>
|
||||||
<For each={sortedArticles().slice(LOAD_MORE_PAGE_SIZE, LOAD_MORE_PAGE_SIZE * 2)}>
|
<For each={expoShouts().slice(LOAD_MORE_PAGE_SIZE, LOAD_MORE_PAGE_SIZE * 2)}>
|
||||||
{(shout) => (
|
{(shout) => (
|
||||||
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
|
@ -224,7 +209,7 @@ export const Expo = (props: Props) => {
|
||||||
<Show when={favoriteTopArticles()?.length > 0} keyed={true}>
|
<Show when={favoriteTopArticles()?.length > 0} keyed={true}>
|
||||||
<ArticleCardSwiper title={t('Favorite')} slides={favoriteTopArticles()} />
|
<ArticleCardSwiper title={t('Favorite')} slides={favoriteTopArticles()} />
|
||||||
</Show>
|
</Show>
|
||||||
<For each={sortedArticles().slice(LOAD_MORE_PAGE_SIZE * 2)}>
|
<For each={expoShouts().slice(LOAD_MORE_PAGE_SIZE * 2, articlesEndPage())}>
|
||||||
{(shout) => (
|
{(shout) => (
|
||||||
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
|
|
|
@ -170,7 +170,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentArticleTitle {
|
.comment .commentArticleTitle {
|
||||||
line-clamp: 1;
|
line-clamp: 1;
|
||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
|
|
||||||
|
|
|
@ -49,11 +49,18 @@ type VisibilityItem = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeedSearchParams = {
|
type FeedSearchParams = {
|
||||||
by: 'publish_date' | 'likes' | 'comments'
|
by: 'publish_date' | 'likes' | 'last_comment'
|
||||||
period: FeedPeriod
|
period: FeedPeriod
|
||||||
visibility: VisibilityMode
|
visibility: VisibilityMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loadShouts: (options: LoadShoutsOptions) => Promise<{
|
||||||
|
hasMore: boolean
|
||||||
|
newShouts: Shout[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
const getFromDate = (period: FeedPeriod): number => {
|
const getFromDate = (period: FeedPeriod): number => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
let d: Date = now
|
let d: Date = now
|
||||||
|
@ -74,18 +81,10 @@ const getFromDate = (period: FeedPeriod): number => {
|
||||||
return Math.floor(d.getTime() / 1000)
|
return Math.floor(d.getTime() / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
|
||||||
loadShouts: (options: LoadShoutsOptions) => Promise<{
|
|
||||||
hasMore: boolean
|
|
||||||
newShouts: Shout[]
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FeedView = (props: Props) => {
|
export const FeedView = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const monthPeriod: PeriodItem = { value: 'month', title: t('This month') }
|
const monthPeriod: PeriodItem = { value: 'month', title: t('This month') }
|
||||||
const visibilityAll = { value: 'featured', title: t('All') }
|
|
||||||
|
|
||||||
const periods: PeriodItem[] = [
|
const periods: PeriodItem[] = [
|
||||||
{ value: 'week', title: t('This week') },
|
{ value: 'week', title: t('This week') },
|
||||||
|
@ -121,7 +120,7 @@ export const FeedView = (props: Props) => {
|
||||||
const currentVisibility = createMemo(() => {
|
const currentVisibility = createMemo(() => {
|
||||||
const visibility = visibilities.find((v) => v.value === searchParams().visibility)
|
const visibility = visibilities.find((v) => v.value === searchParams().visibility)
|
||||||
if (!visibility) {
|
if (!visibility) {
|
||||||
return visibilityAll
|
return visibilities[0]
|
||||||
}
|
}
|
||||||
return visibility
|
return visibility
|
||||||
})
|
})
|
||||||
|
@ -172,6 +171,7 @@ export const FeedView = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibilityMode = searchParams().visibility
|
const visibilityMode = searchParams().visibility
|
||||||
|
|
||||||
if (visibilityMode === 'all') {
|
if (visibilityMode === 'all') {
|
||||||
options.filters = { ...options.filters }
|
options.filters = { ...options.filters }
|
||||||
} else if (visibilityMode) {
|
} else if (visibilityMode) {
|
||||||
|
@ -185,6 +185,7 @@ export const FeedView = (props: Props) => {
|
||||||
const period = searchParams().period || 'month'
|
const period = searchParams().period || 'month'
|
||||||
options.filters = { after: getFromDate(period) }
|
options.filters = { after: getFromDate(period) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.loadShouts(options)
|
return props.loadShouts(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,10 +258,10 @@ export const FeedView = (props: Props) => {
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class={clsx({
|
class={clsx({
|
||||||
'view-switcher__item--selected': searchParams().by === 'comments',
|
'view-switcher__item--selected': searchParams().by === 'last_comment',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span class="link" onClick={() => changeSearchParams({ by: 'comments' })}>
|
<span class="link" onClick={() => changeSearchParams({ by: 'last_comment' })}>
|
||||||
{t('Most commented')}
|
{t('Most commented')}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { For, Show, batch, createMemo, createSignal, onMount } from 'solid-js'
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { apiClient } from '../../graphql/client/core'
|
|
||||||
import { Shout, Topic } from '../../graphql/schema/core.gen'
|
import { Shout, Topic } from '../../graphql/schema/core.gen'
|
||||||
import { router } from '../../stores/router'
|
import { router } from '../../stores/router'
|
||||||
import {
|
import {
|
||||||
|
@ -52,8 +51,8 @@ export const HomeView = (props: Props) => {
|
||||||
const { topAuthors } = useTopAuthorsStore()
|
const { topAuthors } = useTopAuthorsStore()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const [randomTopic, setRandomTopic] = createSignal<Topic>(null)
|
const [randomTopic, _setRandomTopic] = createSignal<Topic>(null)
|
||||||
const [randomTopicArticles, setRandomTopicArticles] = createSignal<Shout[]>([])
|
const [randomTopicArticles, _setRandomTopicArticles] = createSignal<Shout[]>([])
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
loadTopArticles()
|
loadTopArticles()
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createSignal, onMount } from 'solid-js'
|
import { For, Show, createEffect, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useFollowing } from '../../../context/following'
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
|
||||||
import { apiClient } from '../../../graphql/client/core'
|
|
||||||
import { Author, Topic } from '../../../graphql/schema/core.gen'
|
import { Author, Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { SubscriptionFilter } from '../../../pages/types'
|
import { SubscriptionFilter } from '../../../pages/types'
|
||||||
import { dummyFilter } from '../../../utils/dummyFilter'
|
import { dummyFilter } from '../../../utils/dummyFilter'
|
||||||
|
@ -21,7 +19,6 @@ import stylesSettings from '../../../styles/FeedSettings.module.scss'
|
||||||
|
|
||||||
export const ProfileSubscriptions = () => {
|
export const ProfileSubscriptions = () => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const { author, session } = useSession()
|
|
||||||
const { subscriptions } = useFollowing()
|
const { subscriptions } = useFollowing()
|
||||||
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
|
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
|
||||||
const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([])
|
const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([])
|
||||||
|
|
|
@ -127,10 +127,10 @@ export const PublishSettings = (props: Props) => {
|
||||||
}
|
}
|
||||||
const handlePublishSubmit = () => {
|
const handlePublishSubmit = () => {
|
||||||
const shoutData = { ...props.form, ...settingsForm }
|
const shoutData = { ...props.form, ...settingsForm }
|
||||||
if (!shoutData?.mainTopic) {
|
if (shoutData?.mainTopic) {
|
||||||
showSnackbar({ body: t('Please, set the main topic first') })
|
|
||||||
} else {
|
|
||||||
publishShout(shoutData)
|
publishShout(shoutData)
|
||||||
|
} else {
|
||||||
|
showSnackbar({ body: t('Please, set the main topic first') })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleSaveDraft = () => {
|
const handleSaveDraft = () => {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
.actionButton {
|
||||||
|
border-radius: 0.8rem !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
width: 9em;
|
||||||
|
|
||||||
|
&.iconed {
|
||||||
|
padding: 6px !important;
|
||||||
|
min-width: 4rem;
|
||||||
|
width: unset;
|
||||||
|
|
||||||
|
&:hover img {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.actionButtonLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButtonLabelHovered {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButtonLabelHovered {
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import { Show, createMemo } from 'solid-js'
|
||||||
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { Button } from '../Button'
|
||||||
|
import stylesButton from '../Button/Button.module.scss'
|
||||||
|
import { CheckButton } from '../CheckButton'
|
||||||
|
import { Icon } from '../Icon'
|
||||||
|
import styles from './BadgeDubscribeButton.module.scss'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
class?: string
|
||||||
|
isSubscribed: boolean
|
||||||
|
minimizeSubscribeButton?: boolean
|
||||||
|
action: () => void
|
||||||
|
iconButtons?: boolean
|
||||||
|
actionMessageType?: 'subscribe' | 'unsubscribe'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BadgeSubscribeButton = (props: Props) => {
|
||||||
|
const { t } = useLocalize()
|
||||||
|
|
||||||
|
const inActionText = createMemo(() => {
|
||||||
|
return props.actionMessageType === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...')
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={props.class}>
|
||||||
|
<Show
|
||||||
|
when={!props.minimizeSubscribeButton}
|
||||||
|
fallback={<CheckButton text={t('Follow')} checked={props.isSubscribed} onClick={props.action} />}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={props.isSubscribed}
|
||||||
|
fallback={
|
||||||
|
<Button
|
||||||
|
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||||
|
size="S"
|
||||||
|
value={
|
||||||
|
<Show
|
||||||
|
when={props.iconButtons}
|
||||||
|
fallback={props.actionMessageType ? inActionText() : t('Subscribe')}
|
||||||
|
>
|
||||||
|
<Icon name="author-subscribe" class={stylesButton.icon} />
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
onClick={props.action}
|
||||||
|
isSubscribeButton={true}
|
||||||
|
class={clsx(styles.actionButton, {
|
||||||
|
[styles.iconed]: props.iconButtons,
|
||||||
|
[stylesButton.subscribed]: props.isSubscribed,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||||
|
size="S"
|
||||||
|
value={
|
||||||
|
<Show
|
||||||
|
when={props.iconButtons}
|
||||||
|
fallback={
|
||||||
|
props.actionMessageType ? (
|
||||||
|
inActionText()
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span class={styles.actionButtonLabel}>{t('Following')}</span>
|
||||||
|
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon name="author-unsubscribe" class={stylesButton.icon} />
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
onClick={props.action}
|
||||||
|
isSubscribeButton={true}
|
||||||
|
class={clsx(styles.actionButton, {
|
||||||
|
[styles.iconed]: props.iconButtons,
|
||||||
|
[stylesButton.subscribed]: props.isSubscribed,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/_shared/BadgeSubscribeButton/index.ts
Normal file
1
src/components/_shared/BadgeSubscribeButton/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { BadgeSubscribeButton } from './BadgeSubscribeButton'
|
|
@ -1 +1 @@
|
||||||
export * from './Icon'
|
export { Icon } from './Icon'
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { useAuthorsStore } from '../../../stores/zine/authors'
|
||||||
import { AuthorBadge } from '../../Author/AuthorBadge'
|
import { AuthorBadge } from '../../Author/AuthorBadge'
|
||||||
import { Button } from '../Button'
|
import { Button } from '../Button'
|
||||||
import { DropdownSelect } from '../DropdownSelect'
|
import { DropdownSelect } from '../DropdownSelect'
|
||||||
import { Loading } from '../Loading'
|
|
||||||
|
|
||||||
import { InlineLoader } from '../../InlineLoader'
|
import { InlineLoader } from '../../InlineLoader'
|
||||||
import styles from './InviteMembers.module.scss'
|
import styles from './InviteMembers.module.scss'
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export * from './Popup'
|
export { Popup, type PopupProps } from './Popup'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
|
import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||||
import SwiperCore from 'swiper'
|
import SwiperCore from 'swiper'
|
||||||
import { Manipulation, Navigation, Pagination } from 'swiper/modules'
|
import { HashNavigation, Manipulation, Navigation, Pagination } from 'swiper/modules'
|
||||||
import { throttle } from 'throttle-debounce'
|
import { throttle } from 'throttle-debounce'
|
||||||
|
|
||||||
import { MediaItem } from '../../../pages/types'
|
import { MediaItem } from '../../../pages/types'
|
||||||
|
@ -12,6 +12,8 @@ import { Lightbox } from '../Lightbox'
|
||||||
|
|
||||||
import { SwiperRef } from './swiper'
|
import { SwiperRef } from './swiper'
|
||||||
|
|
||||||
|
import { useRouter } from '../../../stores/router'
|
||||||
|
import { ArticlePageSearchParams } from '../../Article/FullArticle'
|
||||||
import styles from './Swiper.module.scss'
|
import styles from './Swiper.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -31,10 +33,13 @@ export const ImageSwiper = (props: Props) => {
|
||||||
const [slideIndex, setSlideIndex] = createSignal(0)
|
const [slideIndex, setSlideIndex] = createSignal(0)
|
||||||
const [isMobileView, setIsMobileView] = createSignal(false)
|
const [isMobileView, setIsMobileView] = createSignal(false)
|
||||||
const [selectedImage, setSelectedImage] = createSignal('')
|
const [selectedImage, setSelectedImage] = createSignal('')
|
||||||
|
const { searchParams, changeSearchParams } = useRouter<ArticlePageSearchParams>()
|
||||||
|
|
||||||
const handleSlideChange = () => {
|
const handleSlideChange = () => {
|
||||||
thumbSwipeRef.current.swiper.slideTo(mainSwipeRef.current.swiper.activeIndex)
|
const activeIndex = mainSwipeRef.current.swiper.activeIndex
|
||||||
setSlideIndex(mainSwipeRef.current.swiper.activeIndex)
|
thumbSwipeRef.current.swiper.slideTo(activeIndex)
|
||||||
|
setSlideIndex(activeIndex)
|
||||||
|
changeSearchParams({ slide: `${activeIndex + 1}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
|
@ -51,8 +56,19 @@ export const ImageSwiper = (props: Props) => {
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const { register } = await import('swiper/element/bundle')
|
const { register } = await import('swiper/element/bundle')
|
||||||
register()
|
register()
|
||||||
SwiperCore.use([Pagination, Navigation, Manipulation])
|
SwiperCore.use([Pagination, Navigation, Manipulation, HashNavigation])
|
||||||
mainSwipeRef.current?.swiper?.on('slideChange', handleSlideChange)
|
while (!mainSwipeRef.current?.swiper) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10)) // wait 10 ms
|
||||||
|
}
|
||||||
|
mainSwipeRef.current.swiper.on('slideChange', handleSlideChange)
|
||||||
|
const initialSlide = Number.parseInt(searchParams().slide) - 1
|
||||||
|
if (initialSlide && !Number.isNaN(initialSlide) && initialSlide < props.images.length) {
|
||||||
|
mainSwipeRef.current.swiper.slideTo(initialSlide, 0)
|
||||||
|
} else {
|
||||||
|
changeSearchParams({ slide: '1' })
|
||||||
|
}
|
||||||
|
|
||||||
|
mainSwipeRef.current.swiper.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -103,6 +119,9 @@ export const ImageSwiper = (props: Props) => {
|
||||||
watch-slides-visibility={true}
|
watch-slides-visibility={true}
|
||||||
direction={'horizontal'}
|
direction={'horizontal'}
|
||||||
slides-per-group-auto={true}
|
slides-per-group-auto={true}
|
||||||
|
hash-navigation={{
|
||||||
|
watchState: true,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<For each={props.images}>
|
<For each={props.images}>
|
||||||
{(slide, index) => (
|
{(slide, index) => (
|
||||||
|
@ -149,7 +168,7 @@ export const ImageSwiper = (props: Props) => {
|
||||||
{(slide, index) => (
|
{(slide, index) => (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<swiper-slide lazy="true" virtual-index={index()}>
|
<swiper-slide lazy="true" virtual-index={index()} data-hash={index() + 1}>
|
||||||
<div class={styles.image} onClick={handleImageClick}>
|
<div class={styles.image} onClick={handleImageClick}>
|
||||||
<Image src={slide.url} alt={slide.title} width={800} />
|
<Image src={slide.url} alt={slide.title} width={800} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -135,9 +135,13 @@
|
||||||
.counter {
|
.counter {
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
top: 477px;
|
||||||
|
}
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
top: 477px;
|
top: 276px;
|
||||||
right: 0;
|
right: 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0.2rem 0.8rem;
|
padding: 0.2rem 0.8rem;
|
||||||
|
|
|
@ -2,20 +2,29 @@ import { Accessor, JSX, createContext, createEffect, createSignal, useContext }
|
||||||
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, FollowingEntity } from '../graphql/schema/core.gen'
|
import { Author, AuthorFollowsResult, Community, FollowingEntity, Topic } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { useSession } from './session'
|
import { useSession } from './session'
|
||||||
|
|
||||||
|
export type SubscriptionsData = {
|
||||||
|
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<Array<Author>>
|
followers: Accessor<Author[]>
|
||||||
subscriptions: AuthorFollowsResult
|
subscriptions: AuthorFollowsResult
|
||||||
setSubscriptions: (subscriptions: AuthorFollowsResult) => void
|
setSubscriptions: (subscriptions: AuthorFollowsResult) => void
|
||||||
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
||||||
loadSubscriptions: () => void
|
loadSubscriptions: () => 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>
|
||||||
isOwnerSubscribed: (id: number | string) => boolean
|
// followers: Accessor<Author[]>
|
||||||
|
subscribeInAction?: Accessor<SubscribeAction>
|
||||||
}
|
}
|
||||||
|
|
||||||
const FollowingContext = createContext<FollowingContextType>()
|
const FollowingContext = createContext<FollowingContextType>()
|
||||||
|
@ -32,7 +41,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<Array<Author>>([])
|
const [followers, setFollowers] = createSignal<Author[]>([])
|
||||||
const [subscriptions, setSubscriptions] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS)
|
const [subscriptions, setSubscriptions] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS)
|
||||||
const { author, session } = useSession()
|
const { author, session } = useSession()
|
||||||
|
|
||||||
|
@ -43,7 +52,6 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
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)
|
setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
|
||||||
console.info('[context.following] subs:', subscriptions)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.info('[context.following] cannot get subs', error)
|
console.info('[context.following] cannot get subs', error)
|
||||||
|
@ -52,38 +60,50 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
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' })
|
||||||
try {
|
try {
|
||||||
await apiClient.follow({ what, slug })
|
const subscriptionData = await apiClient.follow({ what, slug })
|
||||||
setSubscriptions((prevSubscriptions) => {
|
setSubscriptions((prevSubscriptions) => {
|
||||||
const updatedSubs = { ...prevSubscriptions }
|
if (!prevSubscriptions[what]) prevSubscriptions[what] = []
|
||||||
if (!updatedSubs[what]) updatedSubs[what] = []
|
prevSubscriptions[what].push(subscriptionData)
|
||||||
const exists = updatedSubs[what]?.some((entity) => entity.slug === slug)
|
return prevSubscriptions
|
||||||
if (!exists) updatedSubs[what].push(slug)
|
|
||||||
return updatedSubs
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setSubscribeInAction() // Сбрасываем состояние действия подписки.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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' })
|
||||||
try {
|
try {
|
||||||
await apiClient.unfollow({ what, slug })
|
await apiClient.unfollow({ what, slug })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setSubscribeInAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (author()) {
|
if (author()) {
|
||||||
try {
|
try {
|
||||||
const { authors, followers, topics } = session().user.app_data
|
const appdata = session()?.user.app_data
|
||||||
setSubscriptions({ authors, topics })
|
if (appdata) {
|
||||||
setFollowers(followers)
|
const { authors, followers, topics } = appdata
|
||||||
if (!authors) fetchData()
|
setSubscriptions({ authors, topics })
|
||||||
|
setFollowers(followers)
|
||||||
|
if (!authors) fetchData()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
@ -111,23 +131,17 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOwnerSubscribed = (id?: number | string) => {
|
|
||||||
if (!author() || !subscriptions) return
|
|
||||||
const isAuthorSubscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === id)
|
|
||||||
const isTopicSubscribed = subscriptions.topics?.some((topicEntity) => topicEntity.slug === id)
|
|
||||||
return !!isAuthorSubscribed || !!isTopicSubscribed
|
|
||||||
}
|
|
||||||
|
|
||||||
const value: FollowingContextType = {
|
const value: FollowingContextType = {
|
||||||
loading,
|
loading,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
setSubscriptions,
|
setSubscriptions,
|
||||||
isOwnerSubscribed,
|
|
||||||
setFollowing,
|
setFollowing,
|
||||||
followers,
|
followers,
|
||||||
loadSubscriptions: fetchData,
|
loadSubscriptions: 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>
|
||||||
|
|
|
@ -40,11 +40,11 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
||||||
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
|
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
|
||||||
const [notificationEntities, setNotificationEntities] = createStore<Record<string, NotificationGroup>>({})
|
const [notificationEntities, setNotificationEntities] = createStore<Record<string, NotificationGroup>>({})
|
||||||
const { isAuthenticated } = useSession()
|
const { author } = useSession()
|
||||||
const { addHandler } = useConnect()
|
const { addHandler } = useConnect()
|
||||||
|
|
||||||
const loadNotificationsGrouped = async (options: { after: number; limit?: number; offset?: number }) => {
|
const loadNotificationsGrouped = async (options: { after: number; limit?: number; offset?: number }) => {
|
||||||
if (isAuthenticated() && notifierClient?.private) {
|
if (author()?.id && notifierClient?.private) {
|
||||||
const notificationsResult = await notifierClient.getNotifications(options)
|
const notificationsResult = await notifierClient.getNotifications(options)
|
||||||
const groups = notificationsResult?.notifications || []
|
const groups = notificationsResult?.notifications || []
|
||||||
const total = notificationsResult?.total || 0
|
const total = notificationsResult?.total || 0
|
||||||
|
@ -74,7 +74,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
addHandler((data: SSEMessage) => {
|
addHandler((data: SSEMessage) => {
|
||||||
if (data.entity === 'reaction' && isAuthenticated()) {
|
if (data.entity === 'reaction' && author()?.id) {
|
||||||
console.info('[context.notifications] event', data)
|
console.info('[context.notifications] event', data)
|
||||||
loadNotificationsGrouped({ after: after(), limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
|
loadNotificationsGrouped({ after: after(), limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
|
||||||
}
|
}
|
||||||
|
@ -91,14 +91,14 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const markSeenAll = async () => {
|
const markSeenAll = async () => {
|
||||||
if (isAuthenticated() && notifierClient.private) {
|
if (author()?.id && notifierClient.private) {
|
||||||
await notifierClient.markSeenAfter({ after: after() })
|
await notifierClient.markSeenAfter({ after: after() })
|
||||||
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
|
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const markSeen = async (notification_id: number) => {
|
const markSeen = async (notification_id: number) => {
|
||||||
if (isAuthenticated() && notifierClient.private) {
|
if (author()?.id && notifierClient.private) {
|
||||||
await notifierClient.markSeen(notification_id)
|
await notifierClient.markSeen(notification_id)
|
||||||
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
|
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Accessor, JSX, Resource } from 'solid-js'
|
import type { Accessor, JSX, Resource } from 'solid-js'
|
||||||
import type { AuthModalSource } from '../components/Nav/AuthModal/types'
|
import type { AuthModalSearchParams, AuthModalSource } from '../components/Nav/AuthModal/types'
|
||||||
import type { Author } from '../graphql/schema/core.gen'
|
import type { Author } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -29,7 +29,6 @@ import {
|
||||||
|
|
||||||
import { inboxClient } from '../graphql/client/chat'
|
import { inboxClient } from '../graphql/client/chat'
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { notifierClient } from '../graphql/client/notifier'
|
|
||||||
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 } from '../stores/zine/authors'
|
||||||
|
@ -49,7 +48,6 @@ export type SessionContextType = {
|
||||||
author: Resource<Author | null>
|
author: Resource<Author | null>
|
||||||
authError: Accessor<string>
|
authError: Accessor<string>
|
||||||
isSessionLoaded: Accessor<boolean>
|
isSessionLoaded: Accessor<boolean>
|
||||||
isAuthenticated: 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>
|
loadAuthor: (info?: unknown) => Author | Promise<Author>
|
||||||
|
@ -73,6 +71,7 @@ export type SessionContextType = {
|
||||||
resendVerifyEmail: (params: ResendVerifyEmailInput) => Promise<GenericResponse>
|
resendVerifyEmail: (params: ResendVerifyEmailInput) => Promise<GenericResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noEmptyBlockStatements: <explanation>
|
||||||
const noop = () => {}
|
const noop = () => {}
|
||||||
|
|
||||||
const SessionContext = createContext<SessionContextType>()
|
const SessionContext = createContext<SessionContextType>()
|
||||||
|
@ -136,6 +135,7 @@ export const SessionProvider = (props: {
|
||||||
|
|
||||||
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
|
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
|
||||||
const [authError, setAuthError] = createSignal('')
|
const [authError, setAuthError] = createSignal('')
|
||||||
|
const { clearSearchParams } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
// Function to load session data
|
// Function to load session data
|
||||||
const sessionData = async () => {
|
const sessionData = async () => {
|
||||||
|
@ -143,7 +143,7 @@ export const SessionProvider = (props: {
|
||||||
const s: ApiResponse<AuthToken> = await authorizer().getSession()
|
const s: ApiResponse<AuthToken> = await authorizer().getSession()
|
||||||
if (s?.data) {
|
if (s?.data) {
|
||||||
console.info('[context.session] loading session', s)
|
console.info('[context.session] loading session', s)
|
||||||
|
clearSearchParams()
|
||||||
// Set session expiration time in local storage
|
// Set session expiration time in local storage
|
||||||
const expires_at = new Date(Date.now() + s.data.expires_in * 1000)
|
const expires_at = new Date(Date.now() + s.data.expires_in * 1000)
|
||||||
localStorage.setItem('expires_at', `${expires_at.getTime()}`)
|
localStorage.setItem('expires_at', `${expires_at.getTime()}`)
|
||||||
|
@ -220,10 +220,13 @@ export const SessionProvider = (props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { profile } = session().user.app_data
|
const appdata = session()?.user.app_data
|
||||||
setAuthor(profile)
|
if (appdata) {
|
||||||
addAuthors([profile])
|
const { profile } = appdata
|
||||||
if (!profile) loadAuthor()
|
setAuthor(profile)
|
||||||
|
addAuthors([profile])
|
||||||
|
if (!profile) loadAuthor()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
@ -267,12 +270,9 @@ export const SessionProvider = (props: {
|
||||||
|
|
||||||
// callback state updater
|
// callback state updater
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on([() => props.onStateChangeCallback, session], ([_, ses]) => {
|
||||||
() => props.onStateChangeCallback,
|
ses?.user?.id && props.onStateChangeCallback(ses)
|
||||||
() => {
|
}),
|
||||||
props.onStateChangeCallback(session())
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const [authCallback, setAuthCallback] = createSignal<() => void>(noop)
|
const [authCallback, setAuthCallback] = createSignal<() => void>(noop)
|
||||||
|
@ -374,8 +374,6 @@ export const SessionProvider = (props: {
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAuthenticated = createMemo(() => Boolean(author()))
|
|
||||||
const actions = {
|
const actions = {
|
||||||
loadSession,
|
loadSession,
|
||||||
requireAuthentication,
|
requireAuthentication,
|
||||||
|
@ -400,7 +398,6 @@ export const SessionProvider = (props: {
|
||||||
isSessionLoaded,
|
isSessionLoaded,
|
||||||
author,
|
author,
|
||||||
...actions,
|
...actions,
|
||||||
isAuthenticated,
|
|
||||||
resendVerifyEmail,
|
resendVerifyEmail,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ const setupIndexedDB = async () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTopicsFromIndexedDB = async (db) => {
|
const getTopicsFromIndexedDB = (db) => {
|
||||||
const tx = db.transaction(STORE_NAME, 'readonly')
|
const tx = db.transaction(STORE_NAME, 'readonly')
|
||||||
const store = tx.objectStore(STORE_NAME)
|
const store = tx.objectStore(STORE_NAME)
|
||||||
return store.getAll()
|
return store.getAll()
|
||||||
|
@ -44,7 +44,7 @@ export const TopicsProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const db = await setupIndexedDB()
|
const db = await setupIndexedDB()
|
||||||
let topics = await getTopicsFromIndexedDB(db)
|
let topics = getTopicsFromIndexedDB(db)
|
||||||
|
|
||||||
if (topics.length === 0) {
|
if (topics.length === 0) {
|
||||||
topics = await apiClient.getAllTopics()
|
topics = await apiClient.getAllTopics()
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { createGraphQLClient } from '../createGraphQLClient'
|
|
||||||
import markSeenMutation from '../mutation/notifier/mark-seen'
|
import markSeenMutation from '../mutation/notifier/mark-seen'
|
||||||
import markSeenAfterMutation from '../mutation/notifier/mark-seen-after'
|
import markSeenAfterMutation from '../mutation/notifier/mark-seen-after'
|
||||||
import markThreadSeenMutation from '../mutation/notifier/mark-seen-thread'
|
import markThreadSeenMutation from '../mutation/notifier/mark-seen-thread'
|
||||||
|
|
|
@ -4,6 +4,10 @@ export default gql`
|
||||||
mutation FollowMutation($what: FollowingEntity!, $slug: String!) {
|
mutation FollowMutation($what: FollowingEntity!, $slug: String!) {
|
||||||
follow(what: $what, slug: $slug) {
|
follow(what: $what, slug: $slug) {
|
||||||
error
|
error
|
||||||
|
authors {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { PageProps } from './types'
|
import type { PageProps } from './types'
|
||||||
|
|
||||||
import { createEffect, createSignal, onMount } from 'solid-js'
|
import { createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { AllAuthors } from '../components/Views/AllAuthors/'
|
import { AllAuthors } from '../components/Views/AllAuthors/'
|
||||||
import { PAGE_SIZE } from '../components/Views/AllTopics/AllTopics'
|
import { PAGE_SIZE } from '../components/Views/AllTopics/AllTopics'
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { Show, Suspense, createMemo, createSignal, lazy, onMount } from 'solid-js'
|
import { Show, Suspense, createEffect, createMemo, createSignal, lazy, on, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { AuthGuard } from '../components/AuthGuard'
|
import { AuthGuard } from '../components/AuthGuard'
|
||||||
import { Loading } from '../components/_shared/Loading'
|
import { Loading } from '../components/_shared/Loading'
|
||||||
import { PageLayout } from '../components/_shared/PageLayout'
|
import { PageLayout } from '../components/_shared/PageLayout'
|
||||||
import { useLocalize } from '../context/localize'
|
import { useLocalize } from '../context/localize'
|
||||||
|
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 { useRouter } from '../stores/router'
|
|
||||||
import { router } from '../stores/router'
|
import { router } from '../stores/router'
|
||||||
|
|
||||||
import { redirectPage } from '@nanostores/router'
|
import { redirectPage } from '@nanostores/router'
|
||||||
|
@ -15,68 +15,70 @@ import { LayoutType } from './types'
|
||||||
|
|
||||||
const EditView = lazy(() => import('../components/Views/EditView/EditView'))
|
const EditView = lazy(() => import('../components/Views/EditView/EditView'))
|
||||||
|
|
||||||
export const EditPage = () => {
|
const getContentTypeTitle = (layout: LayoutType) => {
|
||||||
const { page } = useRouter()
|
switch (layout) {
|
||||||
const snackbar = useSnackbar()
|
case 'audio':
|
||||||
const { t } = useLocalize()
|
return 'Publish Album'
|
||||||
|
case 'image':
|
||||||
|
return 'Create gallery'
|
||||||
|
case 'video':
|
||||||
|
return 'Create video'
|
||||||
|
case 'literature':
|
||||||
|
return 'New literary work'
|
||||||
|
default:
|
||||||
|
return 'Write an article'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [shout, setShout] = createSignal<Shout>(null)
|
export const EditPage = () => {
|
||||||
const loadMyShout = async (shout_id: number) => {
|
const { t } = useLocalize()
|
||||||
if (shout_id) {
|
const { session } = useSession()
|
||||||
const { shout: loadedShout, error } = await apiClient.getMyShout(shout_id)
|
const snackbar = useSnackbar()
|
||||||
console.log(loadedShout)
|
|
||||||
if (error) {
|
const fail = async (error: string) => {
|
||||||
await snackbar?.showSnackbar({ type: 'error', body: t('This content is not published yet') })
|
console.error(error)
|
||||||
redirectPage(router, 'drafts')
|
await snackbar?.showSnackbar({ type: 'error', body: t(error) })
|
||||||
} else {
|
redirectPage(router, 'drafts')
|
||||||
setShout(loadedShout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
const [shoutId, setShoutId] = createSignal<number>(0)
|
||||||
const shout_id = window.location.pathname.split('/').pop()
|
const [shout, setShout] = createSignal<Shout>()
|
||||||
if (shout_id) {
|
|
||||||
try {
|
onMount(() => {
|
||||||
await loadMyShout(parseInt(shout_id, 10))
|
const shoutId = window.location.pathname.split('/').pop()
|
||||||
} catch (e) {
|
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
|
||||||
console.error(e)
|
console.debug(`editing shout ${shoutIdFromUrl}`)
|
||||||
}
|
if (shoutIdFromUrl) setShoutId(shoutIdFromUrl)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on([session, shout, shoutId], async ([ses, sh, shid]) => {
|
||||||
|
if (ses?.user && !sh && shid) {
|
||||||
|
const { shout: loadedShout, error } = await apiClient.getMyShout(shid)
|
||||||
|
if (error) {
|
||||||
|
fail(error)
|
||||||
|
} else {
|
||||||
|
setShout(loadedShout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
if (!shout()) {
|
if (!shout()) {
|
||||||
return t('Create post')
|
return t('Create post')
|
||||||
}
|
}
|
||||||
|
return t(getContentTypeTitle(shout()?.layout as LayoutType))
|
||||||
switch (shout().layout as LayoutType) {
|
|
||||||
case 'audio': {
|
|
||||||
return t('Publish Album')
|
|
||||||
}
|
|
||||||
case 'image': {
|
|
||||||
return t('Create gallery')
|
|
||||||
}
|
|
||||||
case 'video': {
|
|
||||||
return t('Create video')
|
|
||||||
}
|
|
||||||
case 'literature': {
|
|
||||||
return t('New literary work')
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return t('Write an article')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title={title()}>
|
<PageLayout title={title()}>
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
<Show when={shout()}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Suspense fallback={<Loading />}>
|
<Show when={shout()} fallback={<Loading />}>
|
||||||
<EditView shout={shout()} />
|
<EditView shout={shout() as Shout} />
|
||||||
</Suspense>
|
</Show>
|
||||||
</Show>
|
</Suspense>
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -320,3 +320,14 @@ h5 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// disable last pass extention
|
||||||
|
|
||||||
|
div[data-lastpass-icon-root="true"] {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[data-lastpass-infield="true"] {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,9 +53,4 @@ export type UploadedFile = {
|
||||||
originalFilename?: string
|
originalFilename?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FollowedInfo = {
|
|
||||||
value?: boolean
|
|
||||||
loaded?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SubscriptionFilter = 'all' | 'authors' | 'topics' | 'communities'
|
export type SubscriptionFilter = 'all' | 'authors' | 'topics' | 'communities'
|
||||||
|
|
|
@ -69,7 +69,8 @@ const checkOpenOnClient = (link: HTMLAnchorElement, event) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToHash = (hash: string) => {
|
// TODO: use scrollToHash or remove
|
||||||
|
const _scrollToHash = (hash: string) => {
|
||||||
let selector = hash
|
let selector = hash
|
||||||
|
|
||||||
if (/^#\d+/.test(selector)) {
|
if (/^#\d+/.test(selector)) {
|
||||||
|
@ -114,8 +115,8 @@ const handleClientRouteLinkClick = async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.hash) {
|
if (url.hash) {
|
||||||
scrollToHash(url.hash)
|
// scrollToHash(url.hash)
|
||||||
return
|
// return
|
||||||
}
|
}
|
||||||
|
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
|
export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
|
||||||
type SortedAuthorsSetter = (prev: Author[]) => Author[]
|
type SortedAuthorsSetter = (prev: Author[]) => Author[]
|
||||||
|
// FIXME: use signal or remove
|
||||||
const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('name')
|
const [_sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('name')
|
||||||
|
|
||||||
export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy)
|
export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy)
|
||||||
|
|
||||||
|
|
|
@ -588,6 +588,7 @@ figure {
|
||||||
display: block;
|
display: block;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,31 +1,41 @@
|
||||||
import { cdnUrl, thumborUrl } from './config'
|
import { cdnUrl, thumborUrl } from './config'
|
||||||
|
|
||||||
const getSizeUrlPart = (options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {}) => {
|
const URL_CONFIG = {
|
||||||
const widthString = options.width ? options.width.toString() : ''
|
cdnUrl: cdnUrl,
|
||||||
const heightString = options.height ? options.height.toString() : ''
|
thumborUrl: `${thumborUrl}/unsafe/`,
|
||||||
|
audioSubfolder: 'audio',
|
||||||
|
imageSubfolder: 'image',
|
||||||
|
productionFolder: 'production/',
|
||||||
|
}
|
||||||
|
|
||||||
if (!(widthString || heightString) || options.noSizeUrlPart) {
|
const AUDIO_EXTENSIONS = new Set(['wav', 'mp3', 'ogg', 'aif', 'flac'])
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${widthString}x${heightString}/`
|
const isAudioFile = (filename: string): boolean => {
|
||||||
|
const extension = filename.split('.').pop()?.toLowerCase()
|
||||||
|
return AUDIO_EXTENSIONS.has(extension ?? '')
|
||||||
|
}
|
||||||
|
const getLastSegment = (url: string): string => url.toLowerCase().split('/').pop() || ''
|
||||||
|
|
||||||
|
const buildSizePart = (width?: number, height?: number, includeSize = true): string => {
|
||||||
|
if (!includeSize) return ''
|
||||||
|
const widthPart = width ? width.toString() : ''
|
||||||
|
const heightPart = height ? height.toString() : ''
|
||||||
|
return widthPart || heightPart ? `${widthPart}x${heightPart}/` : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getImageUrl = (
|
export const getImageUrl = (
|
||||||
src: string,
|
src: string,
|
||||||
options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {},
|
options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {},
|
||||||
) => {
|
): string => {
|
||||||
if (!src.includes('discours.io') && src.includes('http')) {
|
if (!src.includes('discours.io') && src.includes('http')) {
|
||||||
return src
|
return src
|
||||||
}
|
}
|
||||||
const filename = src.toLowerCase().split('/').pop()
|
const filename = getLastSegment(src)
|
||||||
const ext = filename.split('.').pop()
|
const base = isAudioFile(filename) ? URL_CONFIG.cdnUrl : URL_CONFIG.thumborUrl
|
||||||
const isAudio = ext in ['wav', 'mp3', 'ogg', 'aif', 'flac']
|
const suffix = options.noSizeUrlPart ? '' : buildSizePart(options.width, options.height)
|
||||||
const base = isAudio ? cdnUrl : `${thumborUrl}/unsafe/`
|
const subfolder = isAudioFile(filename) ? URL_CONFIG.audioSubfolder : URL_CONFIG.imageSubfolder
|
||||||
const suffix = isAudio || options.noSizeUrlPart ? '' : getSizeUrlPart(options)
|
|
||||||
const subfolder = isAudio ? 'audio' : 'image'
|
|
||||||
|
|
||||||
return `${base}${suffix}production/${subfolder}/${filename}`
|
return `${base}${suffix}${URL_CONFIG.productionFolder}${subfolder}/${filename}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOpenGraphImageUrl = (
|
export const getOpenGraphImageUrl = (
|
||||||
|
@ -37,17 +47,16 @@ export const getOpenGraphImageUrl = (
|
||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
},
|
},
|
||||||
) => {
|
): string => {
|
||||||
const sizeUrlPart = getSizeUrlPart(options)
|
const sizeUrlPart = buildSizePart(options.width, options.height)
|
||||||
|
|
||||||
const filtersPart = `filters:discourstext('${encodeURIComponent(options.topic)}','${encodeURIComponent(
|
const filtersPart = `filters:discourstext('${encodeURIComponent(options.topic)}','${encodeURIComponent(
|
||||||
options.author,
|
options.author,
|
||||||
)}','${encodeURIComponent(options.title)}')/`
|
)}','${encodeURIComponent(options.title)}')/`
|
||||||
|
|
||||||
if (src.startsWith(thumborUrl)) {
|
if (src.startsWith(URL_CONFIG.thumborUrl)) {
|
||||||
const thumborKey = src.replace(`${thumborUrl}/unsafe`, '')
|
const thumborKey = src.replace(URL_CONFIG.thumborUrl, '')
|
||||||
return `${thumborUrl}/unsafe/${sizeUrlPart}${filtersPart}${thumborKey}`
|
return `${URL_CONFIG.thumborUrl}${sizeUrlPart}${filtersPart}${thumborKey}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${thumborUrl}/unsafe/${sizeUrlPart}${filtersPart}${src}`
|
return `${URL_CONFIG.thumborUrl}${sizeUrlPart}${filtersPart}${src}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { RANDOM_TOPICS_COUNT } from '../components/Views/Home'
|
||||||
import { Topic } from '../graphql/schema/core.gen'
|
import { Topic } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
export const getRandomTopicsFromArray = (topics: Topic[], count: number = RANDOM_TOPICS_COUNT): Topic[] => {
|
export const getRandomTopicsFromArray = (topics: Topic[], count: number = RANDOM_TOPICS_COUNT): Topic[] => {
|
||||||
|
if (!Array.isArray(topics)) return []
|
||||||
const shuffledTopics = [...topics].sort(() => 0.5 - Math.random())
|
const shuffledTopics = [...topics].sort(() => 0.5 - Math.random())
|
||||||
return shuffledTopics.slice(0, count)
|
return shuffledTopics.slice(0, count)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import ssrPlugin from 'vike/plugin'
|
import ssrPlugin from 'vike/plugin'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import mkcert from 'vite-plugin-mkcert'
|
import mkcert from 'vite-plugin-mkcert'
|
||||||
|
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||||
import sassDts from 'vite-plugin-sass-dts'
|
import sassDts from 'vite-plugin-sass-dts'
|
||||||
import solidPlugin from 'vite-plugin-solid'
|
import solidPlugin from 'vite-plugin-solid'
|
||||||
|
|
||||||
|
@ -39,6 +40,19 @@ export default defineConfig(({ mode, command }) => {
|
||||||
ssrPlugin({ includeAssetsImportedByServer: true }),
|
ssrPlugin({ includeAssetsImportedByServer: true }),
|
||||||
sassDts(),
|
sassDts(),
|
||||||
cssModuleHMR(),
|
cssModuleHMR(),
|
||||||
|
nodePolyfills({
|
||||||
|
include: ['path', 'stream', 'util'],
|
||||||
|
exclude: ['http'],
|
||||||
|
globals: {
|
||||||
|
Buffer: true,
|
||||||
|
//global: true,
|
||||||
|
//process: true,
|
||||||
|
},
|
||||||
|
overrides: {
|
||||||
|
fs: 'memfs',
|
||||||
|
},
|
||||||
|
protocolImports: true,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
if (command === 'serve') {
|
if (command === 'serve') {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user