Merge branch 'create-shout-2' into 'dev'

Create shout new flow WIP

See merge request discoursio/discoursio-webapp!59
This commit is contained in:
Igor 2023-05-03 16:46:26 +00:00
commit 821fb428de
106 changed files with 2098 additions and 6735 deletions

View File

@ -68,7 +68,7 @@ module.exports = {
'unicorn/prefer-dom-node-append': 'off', // FIXME 'unicorn/prefer-dom-node-append': 'off', // FIXME
'unicorn/prefer-top-level-await': 'warn', 'unicorn/prefer-top-level-await': 'warn',
'unicorn/consistent-function-scoping': 'warn', 'unicorn/consistent-function-scoping': 'warn',
'sonarjs/no-duplicate-string': 'warn', 'sonarjs/no-duplicate-string': ['warn', 5],
// Promise // Promise
// 'promise/catch-or-return': 'off', // Should be enabled // 'promise/catch-or-return': 'off', // Should be enabled

View File

@ -19,7 +19,19 @@
], ],
"scss/dollar-variable-pattern": ["^[a-z][a-zA-Z]+$", { "scss/dollar-variable-pattern": ["^[a-z][a-zA-Z]+$", {
"ignore": "global" "ignore": "global"
}] }],
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global", "export"]
}
],
"property-no-vendor-prefix": [
true,
{
"ignoreProperties": ["box-decoration-break"]
}
]
}, },
"defaultSeverity": "warning" "defaultSeverity": "warning"
} }

31
.stylelintrc.bak Normal file
View File

@ -0,0 +1,31 @@
{
"extends": [
"stylelint-config-standard-scss"
],
"plugins": [
"stylelint-order",
"stylelint-scss"
],
"rules": {
"selector-class-pattern": null,
"no-descending-specificity": null,
"scss/function-no-unknown": null,
"scss/no-global-function-names": null,
"function-url-quotes": null,
"font-family-no-missing-generic-family-keyword": null,
"order/order": [
"custom-properties",
"declarations"
],
"scss/dollar-variable-pattern": ["^[a-z][a-zA-Z]+$", {
"ignore": "global"
}],
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global", "export"]
}
]
},
"defaultSeverity": "warning"
}

View File

@ -1,6 +1,6 @@
overwrite: true overwrite: true
#schema: 'http://localhost:8080' schema: 'http://127.0.0.1:8080'
schema: 'https://v2.discours.io' #schema: 'https://v2.discours.io'
generates: generates:
src/graphql/introspec.gen.ts: src/graphql/introspec.gen.ts:
plugins: plugins:

7254
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,69 +32,68 @@
"@aws-sdk/abort-controller": "3.303.0", "@aws-sdk/abort-controller": "3.303.0",
"@aws-sdk/client-s3": "3.303.0", "@aws-sdk/client-s3": "3.303.0",
"@aws-sdk/lib-storage": "3.303.0", "@aws-sdk/lib-storage": "3.303.0",
"@hocuspocus/provider": "2.0.1", "@hocuspocus/provider": "2.0.6",
"formidable": "2.1.1", "formidable": "2.1.1",
"html-to-json-parser": "1.1.0", "i18next": "22.4.15",
"i18next": "22.4.13",
"mailgun.js": "8.2.1", "mailgun.js": "8.2.1",
"node-fetch": "3.3.1" "node-fetch": "3.3.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.21.3", "@babel/core": "7.21.8",
"@graphql-codegen/cli": "3.2.2", "@graphql-codegen/cli": "3.2.2",
"@graphql-codegen/typescript": "3.0.2", "@graphql-codegen/typescript": "3.0.4",
"@graphql-codegen/typescript-operations": "3.0.2", "@graphql-codegen/typescript-operations": "3.0.4",
"@graphql-codegen/typescript-urql": "3.7.3", "@graphql-codegen/typescript-urql": "3.7.3",
"@graphql-codegen/urql-introspection": "2.2.1", "@graphql-codegen/urql-introspection": "2.2.1",
"@graphql-tools/url-loader": "7.17.14", "@graphql-tools/url-loader": "7.17.18",
"@graphql-typed-document-node/core": "3.2.0", "@graphql-typed-document-node/core": "3.2.0",
"@nanostores/router": "0.8.3", "@nanostores/router": "0.8.3",
"@nanostores/solid": "0.3.2", "@nanostores/solid": "0.3.2",
"@popperjs/core": "2.11.7", "@popperjs/core": "2.11.7",
"@solid-primitives/memo": "1.2.3", "@solid-primitives/memo": "1.2.4",
"@solid-primitives/share": "2.0.4", "@solid-primitives/share": "2.0.4",
"@solid-primitives/storage": "1.3.8", "@solid-primitives/storage": "1.3.9",
"@solid-primitives/upload": "0.0.110", "@solid-primitives/upload": "0.0.110",
"@solidjs/meta": "0.28.2", "@solidjs/meta": "0.28.2",
"@thisbeyond/solid-select": "0.13.0", "@thisbeyond/solid-select": "0.13.0",
"@tiptap/core": "2.0.1", "@tiptap/core": "2.0.3",
"@tiptap/extension-blockquote": "2.0.1", "@tiptap/extension-blockquote": "2.0.3",
"@tiptap/extension-bold": "2.0.1", "@tiptap/extension-bold": "2.0.3",
"@tiptap/extension-bubble-menu": "2.0.1", "@tiptap/extension-bubble-menu": "2.0.3",
"@tiptap/extension-bullet-list": "2.0.1", "@tiptap/extension-bullet-list": "2.0.3",
"@tiptap/extension-character-count": "2.0.1", "@tiptap/extension-character-count": "2.0.3",
"@tiptap/extension-collaboration": "2.0.1", "@tiptap/extension-collaboration": "2.0.3",
"@tiptap/extension-collaboration-cursor": "2.0.1", "@tiptap/extension-collaboration-cursor": "2.0.3",
"@tiptap/extension-document": "2.0.1", "@tiptap/extension-document": "2.0.3",
"@tiptap/extension-dropcursor": "2.0.1", "@tiptap/extension-dropcursor": "2.0.3",
"@tiptap/extension-floating-menu": "2.0.1", "@tiptap/extension-floating-menu": "2.0.3",
"@tiptap/extension-focus": "2.0.1", "@tiptap/extension-focus": "2.0.3",
"@tiptap/extension-gapcursor": "2.0.1", "@tiptap/extension-gapcursor": "2.0.3",
"@tiptap/extension-hard-break": "2.0.1", "@tiptap/extension-hard-break": "2.0.3",
"@tiptap/extension-heading": "2.0.1", "@tiptap/extension-heading": "2.0.3",
"@tiptap/extension-highlight": "2.0.1", "@tiptap/extension-highlight": "2.0.3",
"@tiptap/extension-history": "2.0.1", "@tiptap/extension-history": "2.0.3",
"@tiptap/extension-horizontal-rule": "2.0.1", "@tiptap/extension-horizontal-rule": "2.0.3",
"@tiptap/extension-image": "2.0.1", "@tiptap/extension-image": "2.0.3",
"@tiptap/extension-italic": "2.0.1", "@tiptap/extension-italic": "2.0.3",
"@tiptap/extension-link": "2.0.1", "@tiptap/extension-link": "2.0.3",
"@tiptap/extension-list-item": "2.0.1", "@tiptap/extension-list-item": "2.0.3",
"@tiptap/extension-ordered-list": "2.0.1", "@tiptap/extension-ordered-list": "2.0.3",
"@tiptap/extension-paragraph": "2.0.1", "@tiptap/extension-paragraph": "2.0.3",
"@tiptap/extension-placeholder": "2.0.1", "@tiptap/extension-placeholder": "2.0.3",
"@tiptap/extension-strike": "2.0.1", "@tiptap/extension-strike": "2.0.3",
"@tiptap/extension-text": "2.0.1", "@tiptap/extension-text": "2.0.3",
"@tiptap/extension-underline": "2.0.1", "@tiptap/extension-underline": "2.0.3",
"@tiptap/extension-youtube": "2.0.1", "@tiptap/extension-youtube": "2.0.3",
"@types/express": "4.17.17", "@types/express": "4.17.17",
"@types/node": "18.15.11", "@types/node": "18.16.3",
"@types/uuid": "9.0.1", "@types/uuid": "9.0.1",
"@typescript-eslint/eslint-plugin": "5.57.0", "@typescript-eslint/eslint-plugin": "5.59.2",
"@typescript-eslint/parser": "5.57.0", "@typescript-eslint/parser": "5.59.2",
"@urql/core": "3.2.2", "@urql/core": "3.2.2",
"@urql/devtools": "2.0.3", "@urql/devtools": "2.0.3",
"@urql/exchange-graphcache": "5.2.0", "@urql/exchange-graphcache": "5.2.0",
"babel-preset-solid": "1.7.0", "babel-preset-solid": "1.7.4",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bootstrap": "5.2.3", "bootstrap": "5.2.3",
"clsx": "1.2.1", "clsx": "1.2.1",
@ -102,27 +101,27 @@
"cookie-signature": "1.2.1", "cookie-signature": "1.2.1",
"cosmiconfig-toml-loader": "1.0.0", "cosmiconfig-toml-loader": "1.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.37.0", "eslint": "8.39.0",
"eslint-config-stylelint": "18.0.0", "eslint-config-stylelint": "18.0.0",
"eslint-import-resolver-typescript": "3.5.4", "eslint-import-resolver-typescript": "3.5.5",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-solid": "0.12.0", "eslint-plugin-solid": "0.12.1",
"eslint-plugin-sonarjs": "0.19.0", "eslint-plugin-sonarjs": "0.19.0",
"eslint-plugin-unicorn": "46.0.0", "eslint-plugin-unicorn": "46.0.0",
"graphql": "16.6.0", "graphql": "16.6.0",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",
"graphql-ws": "5.12.0", "graphql-ws": "5.12.1",
"hast-util-select": "5.0.5", "hast-util-select": "5.0.5",
"html-to-json-parser": "1.1.0",
"husky": "8.0.3", "husky": "8.0.3",
"hygen": "6.2.11", "hygen": "6.2.11",
"i18next-http-backend": "2.2.0", "i18next-http-backend": "2.2.0",
"idb": "7.1.1", "idb": "7.1.1",
"install": "0.13.0",
"jest": "29.5.0", "jest": "29.5.0",
"js-cookie": "3.0.1", "js-cookie": "3.0.5",
"lint-staged": "13.2.0", "lint-staged": "13.2.2",
"loglevel": "1.8.1", "loglevel": "1.8.1",
"loglevel-plugin-prefix": "0.8.4", "loglevel-plugin-prefix": "0.8.4",
"markdown-it": "13.0.1", "markdown-it": "13.0.1",
@ -131,9 +130,8 @@
"markdown-it-mark": "3.0.1", "markdown-it-mark": "3.0.1",
"markdown-it-replace-link": "1.2.0", "markdown-it-replace-link": "1.2.0",
"nanostores": "0.7.4", "nanostores": "0.7.4",
"npm": "9.6.3",
"orderedmap": "2.1.0", "orderedmap": "2.1.0",
"prettier": "2.8.7", "prettier": "2.8.8",
"prettier-eslint": "15.0.1", "prettier-eslint": "15.0.1",
"prosemirror-commands": "1.5.1", "prosemirror-commands": "1.5.1",
"prosemirror-dropcursor": "1.8.0", "prosemirror-dropcursor": "1.8.0",
@ -148,43 +146,43 @@
"prosemirror-schema-list": "1.2.2", "prosemirror-schema-list": "1.2.2",
"prosemirror-state": "1.4.2", "prosemirror-state": "1.4.2",
"prosemirror-view": "1.30.2", "prosemirror-view": "1.30.2",
"rollup": "3.20.2", "rollup": "3.21.3",
"rollup-plugin-visualizer": "5.9.0", "rollup-plugin-visualizer": "5.9.0",
"sass": "1.60.0", "sass": "1.62.1",
"solid-js": "1.7.0", "solid-js": "1.7.3",
"solid-tiptap": "0.5.1", "solid-tiptap": "0.6.0",
"solid-transition-group": "0.2.2", "solid-transition-group": "0.2.2",
"sort-package-json": "2.4.1", "sort-package-json": "2.4.1",
"stylelint": "15.3.0", "stylelint": "15.6.1",
"stylelint-config-standard-scss": "7.0.1", "stylelint-config-standard-scss": "9.0.0",
"stylelint-order": "6.0.3", "stylelint-order": "6.0.3",
"stylelint-scss": "4.6.0", "stylelint-scss": "5.0.0",
"swiper": "8.4.7", "swiper": "8.4.7",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"typescript": "5.0.3", "typescript": "5.0.4",
"undici": "5.21.0", "undici": "5.21.0",
"uniqolor": "1.1.0", "uniqolor": "1.1.0",
"unique-names-generator": "4.7.1", "unique-names-generator": "4.7.1",
"uuid": "9.0.0", "uuid": "9.0.0",
"vite": "4.2.1", "vite": "4.3.4",
"vite-plugin-sass-dts": "1.3.2", "vite-plugin-sass-dts": "1.3.4",
"vite-plugin-solid": "2.6.1", "vite-plugin-solid": "2.7.0",
"vite-plugin-ssr": "0.4.108", "vite-plugin-ssr": "0.4.121",
"wonka": "6.3.1", "wonka": "6.3.1",
"ws": "8.13.0", "ws": "8.13.0",
"y-indexeddb": "9.0.10", "y-indexeddb": "9.0.10",
"y-prosemirror": "1.2.0", "y-prosemirror": "1.2.1",
"y-protocols": "1.0.5", "y-protocols": "1.0.5",
"y-webrtc": "10.2.5", "y-webrtc": "10.2.5",
"y-websocket": "1.5.0", "y-websocket": "1.5.0",
"yjs": "13.5.51" "yjs": "13.6.0"
}, },
"overrides": { "overrides": {
"@tiptap/extension-collaboration": { "@tiptap/extension-collaboration": {
"y-prosemirror": "1.2.0" "y-prosemirror": "1.2.1"
}, },
"@tiptap/extension-collaboration-cursor": { "@tiptap/extension-collaboration-cursor": {
"y-prosemirror": "1.2.0" "y-prosemirror": "1.2.1"
} }
} }
} }

View File

@ -67,8 +67,8 @@
"Enter your new password": "Введите новый пароль", "Enter your new password": "Введите новый пароль",
"Error": "Ошибка", "Error": "Ошибка",
"Everything is ok, please give us your email address": "Ничего страшного, просто укажите свою почту, чтобы получить ссылку для сброса пароля.", "Everything is ok, please give us your email address": "Ничего страшного, просто укажите свою почту, чтобы получить ссылку для сброса пароля.",
"FAQ": "Советы и предложения",
"Favorite": "Избранное", "Favorite": "Избранное",
"FAQ": "Советы и предложения",
"Favorite topics": "Избранные темы", "Favorite topics": "Избранные темы",
"Feed settings": "Настройки ленты", "Feed settings": "Настройки ленты",
"Feedback": "Обратная связь", "Feedback": "Обратная связь",
@ -102,8 +102,8 @@
"Independant magazine with an open horizontal cooperation about culture, science and society": "Независимый журнал с открытой горизонтальной редакцией о культуре, науке и обществе", "Independant magazine with an open horizontal cooperation about culture, science and society": "Независимый журнал с открытой горизонтальной редакцией о культуре, науке и обществе",
"Introduce": "Представление", "Introduce": "Представление",
"Invalid email": "Проверьте правильность ввода почты", "Invalid email": "Проверьте правильность ввода почты",
"Invalid url format": "Неверный формат ссылки",
"Invite co-authors": "Пригласить соавторов", "Invite co-authors": "Пригласить соавторов",
"Invalid url format": "Неверный формат ссылки",
"Invite experts": "Пригласить экспертов", "Invite experts": "Пригласить экспертов",
"Invite to collab": "Пригласить к участию", "Invite to collab": "Пригласить к участию",
"It does not look like url": "Это не похоже на ссылку", "It does not look like url": "Это не похоже на ссылку",
@ -154,11 +154,13 @@
"Please, confirm email": "Пожалуйста, подтвердите электронную почту", "Please, confirm email": "Пожалуйста, подтвердите электронную почту",
"Popular": "Популярное", "Popular": "Популярное",
"Popular authors": "Популярные авторы", "Popular authors": "Популярные авторы",
"Preview": "Предпросмотр",
"Principles": "Принципы сообщества", "Principles": "Принципы сообщества",
"Profile": "Профиль", "Profile": "Профиль",
"Profile settings": "Настройки профиля", "Profile settings": "Настройки профиля",
"Profile successfully saved": "Профиль успешно сохранён", "Profile successfully saved": "Профиль успешно сохранён",
"Publications": "Публикации", "Publications": "Публикации",
"Publication settings": "Настройки публикации",
"Publish": "Опубликовать", "Publish": "Опубликовать",
"Quit": "Выйти", "Quit": "Выйти",
"Quotes": "Цитаты", "Quotes": "Цитаты",

View File

@ -27,6 +27,7 @@ import { ProjectsPage } from '../pages/about/projects.page'
import { TermsOfUsePage } from '../pages/about/termsOfUse.page' import { TermsOfUsePage } from '../pages/about/termsOfUse.page'
import { ThanksPage } from '../pages/about/thanks.page' import { ThanksPage } from '../pages/about/thanks.page'
import { CreatePage } from '../pages/create.page' import { CreatePage } from '../pages/create.page'
import { EditPage } from '../pages/edit.page'
import { ConnectPage } from '../pages/connect.page' import { ConnectPage } from '../pages/connect.page'
import { InboxPage } from '../pages/inbox.page' import { InboxPage } from '../pages/inbox.page'
import { LayoutShoutsPage } from '../pages/layoutShouts.page' import { LayoutShoutsPage } from '../pages/layoutShouts.page'
@ -34,6 +35,7 @@ import { SessionProvider } from '../context/session'
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page' import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page' import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page' import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
import { DraftsPage } from '../pages/drafts.page'
import { SnackbarProvider } from '../context/snackbar' import { SnackbarProvider } from '../context/snackbar'
import { LocalizeProvider } from '../context/localize' import { LocalizeProvider } from '../context/localize'
import { EditorProvider } from '../context/editor' import { EditorProvider } from '../context/editor'
@ -46,7 +48,9 @@ const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
expo: LayoutShoutsPage, expo: LayoutShoutsPage,
connect: ConnectPage, connect: ConnectPage,
create: CreatePage, create: CreatePage,
createSettings: CreatePage, edit: EditPage,
editSettings: EditPage,
drafts: DraftsPage,
home: HomePage, home: HomePage,
topics: AllTopicsPage, topics: AllTopicsPage,
topic: TopicPage, topic: TopicPage,

View File

@ -101,9 +101,9 @@ img {
} }
.writeComment { .writeComment {
border: 2px solid #f6f6f6;
@include font-size(1.7rem); @include font-size(1.7rem);
border: 2px solid #f6f6f6;
outline: none; outline: none;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
width: 100%; width: 100%;
@ -114,9 +114,9 @@ img {
} }
.commentWarning { .commentWarning {
background: #f6f6f6;
@include font-size(2.2rem); @include font-size(2.2rem);
background: #f6f6f6;
margin-bottom: 1em; margin-bottom: 1em;
padding: 2.4rem 1.8rem; padding: 2.4rem 1.8rem;
} }
@ -173,6 +173,7 @@ img {
a { a {
border: none; border: none;
text-decoration: none; text-decoration: none;
&:hover { &:hover {
background: unset; background: unset;
color: #000; color: #000;
@ -185,9 +186,9 @@ img {
} }
.shoutStatsItemInner { .shoutStatsItemInner {
cursor: pointer;
margin: -0.3em -0.3em 0; margin: -0.3em -0.3em 0;
padding: 0.3em; padding: 0.3em;
cursor: pointer;
.icon { .icon {
margin-right: 0; margin-right: 0;
@ -199,7 +200,7 @@ img {
&:hover { &:hover {
background: #000; background: #000;
cursor: pointer;
img { img {
filter: invert(1); filter: invert(1);
} }

View File

@ -14,7 +14,7 @@ export default (props: { shout: Shout }) => {
return [] return []
}) })
let audioRef: HTMLAudioElement let audioRef: HTMLAudioElement
const [currentTrack, setCurrentTrack] = createSignal(media()[0]) const [currentTrack] = createSignal(media()[0])
const [paused, setPaused] = createSignal(true) const [paused, setPaused] = createSignal(true)
const togglePlayPause = () => setPaused(!paused()) const togglePlayPause = () => setPaused(!paused())
const playMedia = (m: MediaItem) => { const playMedia = (m: MediaItem) => {

View File

@ -7,7 +7,7 @@
&.isNew { &.isNew {
border-radius: 6px; border-radius: 6px;
background: rgba(38, 56, 217, 0.05); background: rgb(38 56 217 / 5%);
} }
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
@ -178,6 +178,7 @@
font-size: 1.2rem; font-size: 1.2rem;
margin-bottom: 4px; margin-bottom: 4px;
color: rgb(0 0 0 / 30%); color: rgb(0 0 0 / 30%);
@include font-size(1.2rem); @include font-size(1.2rem);
.date { .date {
@ -189,6 +190,7 @@
margin-right: 0.5rem; margin-right: 0.5rem;
vertical-align: middle; vertical-align: middle;
} }
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
margin-left: 1rem; margin-left: 1rem;
} }

View File

@ -1,7 +1,7 @@
import styles from './Comment.module.scss' import styles from './Comment.module.scss'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/AuthorCard'
import { Show, createMemo, createSignal, For, lazy, Suspense, createEffect } from 'solid-js' import { Show, createMemo, createSignal, For, lazy, Suspense } from 'solid-js'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import type { Author, Reaction } from '../../graphql/types.gen' import type { Author, Reaction } from '../../graphql/types.gen'
import MD from './MD' import MD from './MD'

View File

@ -9,6 +9,7 @@
font-size: 1.2rem; font-size: 1.2rem;
cursor: pointer; cursor: pointer;
transition: opacity 0.3s ease-in-out; transition: opacity 0.3s ease-in-out;
&:hover { &:hover {
opacity: 0.5; opacity: 0.5;
} }
@ -31,6 +32,7 @@
.commentRatingControlUp { .commentRatingControlUp {
border-bottom: 8px solid rgb(0 0 0 / 40%); border-bottom: 8px solid rgb(0 0 0 / 40%);
&.voted { &.voted {
border-bottom-color: #2bb452; border-bottom-color: #2bb452;
} }
@ -38,6 +40,7 @@
.commentRatingControlDown { .commentRatingControlDown {
border-top: 8px solid rgb(0 0 0 / 40%); border-top: 8px solid rgb(0 0 0 / 40%);
&.voted { &.voted {
border-top-color: #d00820; border-top-color: #d00820;
} }

View File

@ -122,6 +122,7 @@ export const CommentsTree = (props: Props) => {
onClick={() => { onClick={() => {
setCommentsOrder('newOnly') setCommentsOrder('newOnly')
}} }}
class={styles.commentsViewSwitcherButton}
/> />
</li> </li>
</Show> </Show>
@ -132,6 +133,7 @@ export const CommentsTree = (props: Props) => {
onClick={() => { onClick={() => {
setCommentsOrder('createdAt') setCommentsOrder('createdAt')
}} }}
class={styles.commentsViewSwitcherButton}
/> />
</li> </li>
<li classList={{ selected: commentsOrder() === 'rating' }}> <li classList={{ selected: commentsOrder() === 'rating' }}>
@ -141,6 +143,7 @@ export const CommentsTree = (props: Props) => {
onClick={() => { onClick={() => {
setCommentsOrder('rating') setCommentsOrder('rating')
}} }}
class={styles.commentsViewSwitcherButton}
/> />
</li> </li>
</ul> </ul>

View File

@ -1,6 +1,6 @@
import { capitalize, formatDate } from '../../utils' import { capitalize, formatDate } from '../../utils'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/AuthorCard'
import { createEffect, createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js' import { createEffect, createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js'
import type { Author, Shout } from '../../graphql/types.gen' import type { Author, Shout } from '../../graphql/types.gen'
import MD from './MD' import MD from './MD'
@ -77,7 +77,8 @@ export const FullArticle = (props: ArticleProps) => {
const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug) const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
const bookmark = (ev) => { // eslint-disable-next-line unicorn/consistent-function-scoping
const handleBookmarkButtonClick = (ev) => {
// TODO: implement bookmark clicked // TODO: implement bookmark clicked
ev.preventDefault() ev.preventDefault()
} }
@ -97,14 +98,14 @@ export const FullArticle = (props: ArticleProps) => {
behavior: 'smooth' behavior: 'smooth'
}) })
} }
const { searchParams } = useRouter() const { searchParams, changeSearchParam } = useRouter()
createEffect(() => { createEffect(() => {
if (props.scrollToComments) { if (props.scrollToComments) {
scrollToComments() scrollToComments()
} }
}) })
const { changeSearchParam } = useRouter()
createEffect(() => { createEffect(() => {
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) { if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
scrollToComments() scrollToComments()
@ -228,15 +229,17 @@ export const FullArticle = (props: ArticleProps) => {
/> />
</div> </div>
<div class={styles.shoutStatsItem} onClick={bookmark}> <div class={styles.shoutStatsItem} onClick={handleBookmarkButtonClick}>
<div class={styles.shoutStatsItemInner}> <div class={styles.shoutStatsItemInner}>
<Icon name="bookmark" class={styles.icon} /> <Icon name="bookmark" class={styles.icon} />
</div> </div>
</div> </div>
<Show when={canEdit()}> <Show when={canEdit()}>
<div class={styles.shoutStatsItem}> <div class={styles.shoutStatsItem}>
<a href="/edit" class={styles.shoutStatsItemInner}> <a
href={getPagePath(router, 'edit', { shoutSlug: props.article.slug })}
class={styles.shoutStatsItemInner}
>
<Icon name="edit" class={clsx(styles.icon, styles.iconEdit)} /> <Icon name="edit" class={clsx(styles.icon, styles.iconEdit)} />
{t('Edit')} {t('Edit')}
</a> </a>

View File

@ -1,6 +1,6 @@
import styles from './ShoutRatingControl.module.scss' import styles from './ShoutRatingControl.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, For } from 'solid-js' import { createMemo } from 'solid-js'
import { ReactionKind, Shout } from '../../graphql/types.gen' import { ReactionKind, Shout } from '../../graphql/types.gen'
import { loadShout } from '../../stores/zine/articles' import { loadShout } from '../../stores/zine/articles'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'

View File

@ -169,12 +169,12 @@
} }
.buttonWrite { .buttonWrite {
@include font-size(1.5rem);
color: #000; color: #000;
display: inline-flex; display: inline-flex;
transition: background-color 0.3s, color 0.3s; transition: background-color 0.3s, color 0.3s;
@include font-size(1.5rem);
&:hover { &:hover {
background: #000; background: #000;
color: #fff; color: #fff;
@ -208,8 +208,9 @@
} }
.authorAbout { .authorAbout {
color: #696969;
@include font-size(1.7rem); @include font-size(1.7rem);
color: #696969;
} }
.authorSubscribe { .authorSubscribe {

View File

@ -1,7 +1,7 @@
import type { Author, User } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import Userpic from './Userpic' import Userpic from './Userpic'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import styles from './Card.module.scss' import styles from './AuthorCard.module.scss'
import { createMemo, createSignal, For, Show } from 'solid-js' import { createMemo, createSignal, For, Show } from 'solid-js'
import { translit } from '../../utils/ru2en' import { translit } from '../../utils/ru2en'
import { follow, unfollow } from '../../stores/zine/common' import { follow, unfollow } from '../../stores/zine/common'

View File

@ -11,6 +11,7 @@ export const AuthorRatingControl = (props: AuthorRatingControlProps) => {
const isUpvoted = false const isUpvoted = false
const isDownvoted = false const isDownvoted = false
// eslint-disable-next-line unicorn/consistent-function-scoping
const handleRatingChange = (isUpvote: boolean) => { const handleRatingChange = (isUpvote: boolean) => {
console.log('handleRatingChange', { isUpvote }) console.log('handleRatingChange', { isUpvote })
} }

View File

@ -1,5 +1,5 @@
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { AuthorCard } from './Card' import { AuthorCard } from './AuthorCard'
import './Full.scss' import './Full.scss'
export const AuthorFull = (props: { author: Author }) => { export const AuthorFull = (props: { author: Author }) => {

View File

@ -1,10 +1,10 @@
import { Show } from 'solid-js' import { Show } from 'solid-js'
import type { Author } from '../../graphql/types.gen' import type { Author, User } from '../../graphql/types.gen'
import styles from './Userpic.module.scss' import styles from './Userpic.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
interface UserpicProps { interface UserpicProps {
user: Author user: Author | User
hasLink?: boolean hasLink?: boolean
isBig?: boolean isBig?: boolean
class?: string class?: string

View File

@ -1,8 +1,8 @@
.discoursFooter { .discoursFooter {
background: #000;
color: rgb(255 255 255 / 64%);
@include font-size(1.7rem); @include font-size(1.7rem);
background: #000;
color: rgb(255 255 255 / 64%);
padding: 2.4rem 0 4.2rem; padding: 2.4rem 0 4.2rem;
a { a {
@ -48,10 +48,10 @@
} }
.footerCopyright { .footerCopyright {
border-top: 5px solid #404040;
color: #696969;
@include font-size(1.5rem); @include font-size(1.5rem);
border-top: 5px solid #404040;
color: #696969;
padding-top: 1.6rem; padding-top: 1.6rem;
a { a {

View File

@ -1,8 +1,8 @@
.about-discours { .about-discours {
background: #000;
color: #fff;
@include font-size(1.7rem); @include font-size(1.7rem);
background: #000;
color: #fff;
font-weight: 400; font-weight: 400;
margin-bottom: 6.4rem; margin-bottom: 6.4rem;
padding: 3.6rem 0; padding: 3.6rem 0;

View File

@ -1,5 +1,6 @@
.navigationHeader { .navigationHeader {
@include font-size(1.8rem); @include font-size(1.8rem);
font-weight: bold; font-weight: bold;
margin-top: 1.1em; margin-top: 1.1em;
} }

View File

@ -11,13 +11,13 @@
} }
input { input {
@include font-size(2rem);
background: none; background: none;
border: none; border: none;
border-bottom: 1px solid; border-bottom: 1px solid;
color: #fff; color: #fff;
font-family: inherit; font-family: inherit;
@include font-size(2rem);
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -30,13 +30,13 @@
} }
a { a {
@include font-size(1.5rem);
align-items: center; align-items: center;
background: #fff; background: #fff;
border: none; border: none;
color: #000; color: #000;
display: flex; display: flex;
@include font-size(1.5rem);
padding: 0 0.5em; padding: 0 0.5em;
} }

View File

@ -29,6 +29,7 @@ import { TrailingNode } from './extensions/TrailingNode'
import { EditorBubbleMenu } from './EditorBubbleMenu/EditorBubbleMenu' import { EditorBubbleMenu } from './EditorBubbleMenu/EditorBubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu' import { EditorFloatingMenu } from './EditorFloatingMenu'
import * as Y from 'yjs' import * as Y from 'yjs'
// import { WebrtcProvider } from 'y-webrtc'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor' import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
import { Collaboration } from '@tiptap/extension-collaboration' import { Collaboration } from '@tiptap/extension-collaboration'
import './Prosemirror.scss' import './Prosemirror.scss'
@ -40,7 +41,7 @@ import { Embed } from './extensions/embed'
import { useEditorContext } from '../../context/editor' import { useEditorContext } from '../../context/editor'
type EditorProps = { type EditorProps = {
shoutId: number shoutSlug: string
initialContent?: string initialContent?: string
onChange: (text: string) => void onChange: (text: string) => void
} }
@ -53,7 +54,7 @@ export const Editor = (props: EditorProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { user } = useSession() const { user } = useSession()
const docName = `shout-${props.shoutId}` const docName = `shout-${props.shoutSlug}`
if (!providers[docName]) { if (!providers[docName]) {
providers[docName] = new HocuspocusProvider({ providers[docName] = new HocuspocusProvider({
@ -88,6 +89,8 @@ export const Editor = (props: EditorProps) => {
const editor = createTiptapEditor(() => ({ const editor = createTiptapEditor(() => ({
element: editorElRef.current, element: editorElRef.current,
content: props.initialContent,
//onTransaction: handleEditorTransaction,
extensions: [ extensions: [
Document, Document,
Text, Text,
@ -105,18 +108,10 @@ export const Editor = (props: EditorProps) => {
Heading.configure({ Heading.configure({
levels: [1, 2, 3] levels: [1, 2, 3]
}), }),
BubbleMenu.configure({
element: bubbleMenuRef.current
}),
FloatingMenu.configure({
tippyOptions: {
placement: 'left'
},
element: floatingMenuRef.current
}),
BulletList, BulletList,
OrderedList, OrderedList,
ListItem, ListItem,
CharacterCount,
Collaboration.configure({ Collaboration.configure({
document: yDoc document: yDoc
}), }),
@ -135,10 +130,17 @@ export const Editor = (props: EditorProps) => {
HardBreak, HardBreak,
Highlight, Highlight,
Image, Image,
TrailingNode,
Embed, Embed,
TrailingNode, TrailingNode,
CharacterCount BubbleMenu.configure({
element: bubbleMenuRef.current
}),
FloatingMenu.configure({
tippyOptions: {
placement: 'left'
},
element: floatingMenuRef.current
})
] ]
})) }))

View File

@ -40,8 +40,7 @@
position: relative; position: relative;
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
flex-direction: row; flex-flow: row nowrap;
flex-wrap: nowrap;
align-items: center; align-items: center;
.dropDown { .dropDown {
@ -50,7 +49,7 @@
top: calc(100% + 8px); top: calc(100% + 8px);
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25); box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
background: #fff; background: #fff;
color: #898c94; color: #898c94;

View File

@ -19,7 +19,7 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
const [listBubbleOpen, setListBubbleOpen] = createSignal<boolean>(false) const [listBubbleOpen, setListBubbleOpen] = createSignal<boolean>(false)
const [linkEditorOpen, setLinkEditorOpen] = createSignal<boolean>(false) const [linkEditorOpen, setLinkEditorOpen] = createSignal<boolean>(false)
const isActive = (name: string, attributes?: any) => const isActive = (name: string, attributes?: unknown) =>
createEditorTransaction( createEditorTransaction(
() => props.editor, () => props.editor,
(editor) => { (editor) => {

View File

@ -0,0 +1,46 @@
.LinkForm {
position: relative;
.form {
display: flex;
flex-flow: row nowrap;
padding: 6px 11px;
input {
margin: 0 12px 0 0;
padding: 0;
flex: 1;
border: none;
min-width: 200px;
display: block;
&::placeholder {
color: rgba(#000, 0.3);
}
&:focus {
outline: none;
}
}
}
.linkError {
padding: 6px 11px;
color: red;
font-size: 0.7em;
position: absolute;
bottom: -3rem;
left: 0;
right: 0;
height: 0;
background: #fff;
box-shadow: 0 4px 10px rgba(#000, 0.25);
opacity: 0;
transition: height 0.3s ease-in-out, opacity 0.3s ease-in-out;
&.visible {
height: 32px;
opacity: 1;
}
}
}

View File

@ -0,0 +1,85 @@
import styles from './LinkForm.module.scss'
import { Icon } from '../../../_shared/Icon'
import { createEditorTransaction } from 'solid-tiptap'
import validateUrl from '../../../../utils/validateUrl'
import type { Editor } from '@tiptap/core'
import { createSignal } from 'solid-js'
import { useLocalize } from '../../../../context/localize'
import { clsx } from 'clsx'
type Props = {
editor: Editor
onClose: () => void
}
export const LinkForm = (props: Props) => {
const { t } = useLocalize()
const [url, setUrl] = createSignal('')
const [linkError, setLinkError] = createSignal('')
const currentUrl = createEditorTransaction(
() => props.editor,
(editor) => {
return (editor && editor.getAttributes('link').href) || ''
}
)
const clearLinkForm = () => {
if (currentUrl()) {
props.editor.chain().focus().unsetLink().run()
}
setUrl('')
props.onClose()
}
const handleUrlInput = (value) => {
setUrl(value)
}
const handleSaveButtonClick = () => {
if (!validateUrl(url())) {
setLinkError(t('Invalid url format'))
return
}
props.editor.chain().focus().setLink({ href: url() }).run()
props.onClose()
}
const handleKeyPress = (event) => {
event.preventDefault()
setLinkError('')
const key = event.key
if (key === 'Enter') {
handleSaveButtonClick()
}
if (key === 'Esc') {
clearLinkForm()
}
}
return (
<div class={styles.LinkForm}>
<div class={styles.form}>
<input
type="text"
placeholder={t('Enter URL address')}
autofocus
value={currentUrl()}
onKeyPress={(e) => handleKeyPress(e)}
onInput={(e) => handleUrlInput(e.currentTarget.value)}
/>
<button type="button" onClick={handleSaveButtonClick} disabled={linkError() !== ''}>
<Icon name="status-done" />
</button>
<button type="button" onClick={() => clearLinkForm()}>
{currentUrl() ? 'Ж' : <Icon name="status-cancel" />}
</button>
</div>
<div class={clsx(styles.linkError, { [styles.visible]: Boolean(linkError()) })}>{linkError()}</div>
</div>
)
}

View File

@ -0,0 +1 @@
export { LinkForm } from './LinkForm'

View File

@ -12,25 +12,35 @@ type FloatingMenuProps = {
const embedData = async (data) => { const embedData = async (data) => {
const result = await HTMLParser(data, false) const result = await HTMLParser(data, false)
if (typeof result === 'string') {
return
}
if (result && 'type' in result && result.type === 'iframe') { if (result && 'type' in result && result.type === 'iframe') {
return result.attributes return result.attributes
} }
} }
const validateEmbed = async (value: string): Promise<string> => {
const iframeData = await HTMLParser(value, false)
if (typeof iframeData === 'string') {
return
}
if (iframeData && iframeData.type !== 'iframe') {
return
}
}
export const EditorFloatingMenu = (props: FloatingMenuProps) => { export const EditorFloatingMenu = (props: FloatingMenuProps) => {
const [inlineEditorOpen, setInlineEditorOpen] = createSignal<boolean>(false) const [inlineEditorOpen, setInlineEditorOpen] = createSignal<boolean>(false)
const handleEmbedFormSubmit = async (value: string) => { const handleEmbedFormSubmit = async (value: string) => {
// TODO: add support instagram embed (blockquote) // TODO: add support instagram embed (blockquote)
const emb = await embedData(value) const { src } = (await embedData(value)) as { src: string }
props.editor.chain().focus().setIframe(emb).run() props.editor.chain().focus().setIframe({ src }).run()
}
const validateEmbed = async (value) => {
const iframeData = await HTMLParser(value, false)
if (iframeData && iframeData.type !== 'iframe') {
return
}
} }
return ( return (

View File

@ -2,7 +2,7 @@
position: relative; position: relative;
&.inBubble { &.inBubble {
//... // ...
} }
&.inFloating { &.inFloating {
@ -15,6 +15,7 @@
button { button {
opacity: 1; opacity: 1;
&:disabled, &:disabled,
&:disabled:hover { &:disabled:hover {
opacity: 0.3; opacity: 0.3;
@ -24,8 +25,7 @@
.form { .form {
display: flex; display: flex;
flex-direction: row; flex-flow: row nowrap;
flex-wrap: nowrap;
padding: 6px 11px; padding: 6px 11px;
input { input {
@ -39,6 +39,7 @@
&::placeholder { &::placeholder {
color: rgba(#000, 0.3); color: rgba(#000, 0.3);
} }
&:focus { &:focus {
outline: none; outline: none;
} }

View File

@ -1,7 +1,5 @@
import styles from './InlineForm.module.scss' import styles from './InlineForm.module.scss'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { createEditorTransaction } from 'solid-tiptap'
import type { Editor } from '@tiptap/core'
import { createSignal, Show } from 'solid-js' import { createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { clsx } from 'clsx' import { clsx } from 'clsx'
@ -50,7 +48,7 @@ export const InlineForm = (props: Props) => {
return ( return (
<div <div
class={clsx(styles.InlineForm, { class={clsx(styles.InlineForm, {
[styles.inBubble]: props.variant === 'inBubble', // [styles.inBubble]: props.variant === 'inBubble',
[styles.inFloating]: props.variant === 'inFloating' [styles.inFloating]: props.variant === 'inFloating'
})} })}
> >

View File

@ -1,6 +1,6 @@
.Panel { .Panel {
background: #1f1f1f; background: #1f1f1f;
color: rgb(255 255 255 / 0.35); color: rgb(255 255 255 / 35%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-size: 1.7rem; font-size: 1.7rem;
@ -29,7 +29,7 @@
} }
section { section {
border-bottom: 2px solid rgb(255 255 255 / 0.1); border-bottom: 2px solid rgb(255 255 255 / 10%);
padding: 1.8rem 0; padding: 1.8rem 0;
&:first-child { &:first-child {
@ -56,18 +56,8 @@
} }
} }
.buttonWithIcon { .linkWithIcon {
margin-left: -1.6rem; margin-left: -1.6rem;
.icon {
filter: invert(0.5);
margin-right: 0.3em;
width: 1em;
}
img {
vertical-align: middle;
}
} }
.stats { .stats {
@ -79,7 +69,7 @@
} }
a { a {
color: rgb(255 255 255 / 0.35); color: rgb(255 255 255 / 35%);
font-weight: normal !important; font-weight: normal !important;
&:hover { &:hover {
@ -87,7 +77,19 @@
color: #fff; color: #fff;
} }
} }
&.hidden { &.hidden {
transform: translateX(100%); transform: translateX(100%);
} }
.icon {
display: inline-block;
filter: invert(0.5);
margin-right: 0.3em;
width: 1em;
img {
vertical-align: bottom;
}
}
} }

View File

@ -1,16 +1,19 @@
import { Show } from 'solid-js'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import styles from './Panel.module.scss' import styles from './Panel.module.scss'
import { useEditorContext } from '../../../context/editor' import { useEditorContext } from '../../../context/editor'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
import { getPagePath } from '@nanostores/router'
import { router } from '../../../stores/router'
type Props = { type PanelProps = {
// isVisible: boolean shoutSlug: string
} }
export const Panel = (props: Props) => { export const Panel = (props: PanelProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { const {
isEditorPanelVisible, isEditorPanelVisible,
@ -18,8 +21,27 @@ export const Panel = (props: Props) => {
actions: { toggleEditorPanel } actions: { toggleEditorPanel }
} = useEditorContext() } = useEditorContext()
const containerRef: { current: HTMLElement } = {
current: null
}
useOutsideClickHandler({
containerRef,
predicate: () => isEditorPanelVisible(),
handler: () => toggleEditorPanel()
})
useEscKeyDownHandler(() => {
if (isEditorPanelVisible()) {
toggleEditorPanel()
}
})
return ( return (
<aside class={clsx('col-md-6', styles.Panel, { [styles.hidden]: !isEditorPanelVisible() })}> <aside
ref={(el) => (containerRef.current = el)}
class={clsx('col-md-6', styles.Panel, { [styles.hidden]: !isEditorPanelVisible() })}
>
<div class={styles.actionsHolder}> <div class={styles.actionsHolder}>
<Button <Button
value={<Icon name="close" />} value={<Icon name="close" />}
@ -30,47 +52,50 @@ export const Panel = (props: Props) => {
</div> </div>
<div class={clsx(styles.actionsHolder, styles.scrolled)}> <div class={clsx(styles.actionsHolder, styles.scrolled)}>
<section> <section>
<Button value={t('Publish')} variant={'inline'} class={styles.button} /> <p>
<Button value={t('Save draft')} variant={'inline'} class={styles.button} /> <a>{t('Publish')}</a>
</p>
<p>
<a>{t('Save draft')}</a>
</p>
</section> </section>
<section> <section>
<Button <p>
value={ <a class={styles.linkWithIcon}>
<>
<Icon name="eye" class={styles.icon} /> <Icon name="eye" class={styles.icon} />
{t('Preview')} {t('Preview')}
</> </a>
} </p>
variant={'inline'} <p>
class={clsx(styles.button, styles.buttonWithIcon)} <a
/> class={styles.linkWithIcon}
<Button href={getPagePath(router, 'edit', { shoutSlug: props.shoutSlug })}
value={ >
<>
<Icon name="pencil-outline" class={styles.icon} /> <Icon name="pencil-outline" class={styles.icon} />
{t('Editing')} {t('Editing')}
</> </a>
} </p>
variant={'inline'} <p>
class={clsx(styles.button, styles.buttonWithIcon)} <a class={styles.linkWithIcon}>
/>
<Button
value={
<>
<Icon name="feed-discussion" class={styles.icon} /> <Icon name="feed-discussion" class={styles.icon} />
{t('FAQ')} {t('FAQ')}
</> </a>
} </p>
variant={'inline'}
class={clsx(styles.button, styles.buttonWithIcon)}
/>
</section> </section>
<section> <section>
<Button value={t('Invite co-authors')} variant={'inline'} class={styles.button} /> <p>
<Button value={t('Publication settings')} variant={'inline'} class={styles.button} /> <a>{t('Invite co-authors')}</a>
<Button value={t('Corrections history')} variant={'inline'} class={styles.button} /> </p>
<p>
<a href={getPagePath(router, 'editSettings', { shoutSlug: props.shoutSlug })}>
{t('Publication settings')}
</a>
</p>
<p>
<a>{t('Corrections history')}</a>
</p>
</section> </section>
<section> <section>
@ -92,14 +117,9 @@ export const Panel = (props: Props) => {
<div> <div>
{t('Words')}: <em>{wordCounter().words}</em> {t('Words')}: <em>{wordCounter().words}</em>
</div> </div>
<Show when={wordCounter().paragraphs}> {/*<div>*/}
<div> {/* {t('Last rev.')}: <em>22.03.22 в 18:20</em>*/}
{t('Paragraphs')}: <em>{wordCounter().paragraphs}</em> {/*</div>*/}
</div>
</Show>
<div>
{t('Last rev.')}: <em>22.03.22 в 18:20</em>
</div>
</div> </div>
</div> </div>
</aside> </aside>

View File

@ -2,9 +2,9 @@
outline: none; outline: none;
blockquote { blockquote {
border-left: 2px solid;
@include font-size(1.6rem); @include font-size(1.6rem);
border-left: 2px solid;
margin: 1.5em 0; margin: 1.5em 0;
padding-left: 1.6em; padding-left: 1.6em;
} }

View File

@ -3,7 +3,6 @@ import { createOptions, Select } from '@thisbeyond/solid-select'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import '@thisbeyond/solid-select/style.css' import '@thisbeyond/solid-select/style.css'
import './TopicSelect.scss' import './TopicSelect.scss'
import { clone } from '../../../utils/clone'
type TopicSelectProps = { type TopicSelectProps = {
topics: Topic[] topics: Topic[]

View File

@ -1,7 +1,4 @@
import { mergeAttributes, Node } from '@tiptap/core' import { mergeAttributes, Node } from '@tiptap/core'
import { NodeRange } from 'prosemirror-model'
import { insert } from 'solid-js/web'
import { TextSelection } from 'prosemirror-state'
export interface IframeOptions { export interface IframeOptions {
allowFullscreen: boolean allowFullscreen: boolean
@ -48,7 +45,7 @@ export const Embed = Node.create<IframeOptions>({
const iframe = document.createElement('iframe') const iframe = document.createElement('iframe')
iframe.width = node.attrs.width iframe.width = node.attrs.width
iframe.height = node.attrs.height iframe.height = node.attrs.height
iframe.allowfullscreen = node.attrs.allowfullscreen iframe.allowFullscreen = node.attrs.allowFullscreen
iframe.src = node.attrs.src iframe.src = node.attrs.src
div.append(iframe) div.append(iframe)
return { return {

View File

@ -5,10 +5,10 @@
} }
.sidebarContainer { .sidebarContainer {
display: none; // режим отладки
color: rgb(255 255 255 / 50%);
@include font-size(1.6rem); @include font-size(1.6rem);
display: none; // режим отладки
color: rgb(255 255 255 / 50%);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
top: 0; top: 0;

View File

@ -85,9 +85,9 @@
} }
blockquote { blockquote {
border-left: 2px solid;
@include font-size(1.6rem); @include font-size(1.6rem);
border-left: 2px solid;
margin: 1.5em 0; margin: 1.5em 0;
padding-left: 1.6em; padding-left: 1.6em;
} }
@ -287,10 +287,7 @@ li.ProseMirror-selectednode {
li.ProseMirror-selectednode::after { li.ProseMirror-selectednode::after {
content: ''; content: '';
position: absolute; position: absolute;
left: -32px; inset: -2px -2px -2px -32px;
right: -2px;
top: -2px;
bottom: -2px;
border: 2px solid #8cf; border: 2px solid #8cf;
pointer-events: none; pointer-events: none;
} }

View File

@ -132,9 +132,9 @@
} }
.shoutCardSubtitle { .shoutCardSubtitle {
color: #696969;
@include font-size(1.7rem); @include font-size(1.7rem);
color: #696969;
font-weight: 400; font-weight: 400;
line-height: 1.3; line-height: 1.3;
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
@ -263,9 +263,9 @@
.shoutCardTitle, .shoutCardTitle,
.shoutCardSubtitle { .shoutCardSubtitle {
display: inline;
@include font-size(2.6rem); @include font-size(2.6rem);
display: inline;
line-height: 1.2; line-height: 1.2;
} }
@ -554,24 +554,25 @@
.shoutCardBigTitle { .shoutCardBigTitle {
.shoutCardTitle { .shoutCardTitle {
display: block;
@include font-size(3.2rem); @include font-size(3.2rem);
display: block;
padding-right: 0; padding-right: 0;
} }
.shoutCardSubtitle { .shoutCardSubtitle {
color: #696969;
@include font-size(2.4rem); @include font-size(2.4rem);
color: #696969;
} }
} }
.shoutCardCompact { .shoutCardCompact {
.shoutCardTitle, .shoutCardTitle,
.shoutCardSubtitle { .shoutCardSubtitle {
display: inline;
@include font-size(2.6rem); @include font-size(2.6rem);
display: inline;
line-height: 1.2; line-height: 1.2;
} }
@ -669,9 +670,10 @@
} }
.shoutCardSubtitle { .shoutCardSubtitle {
@include font-size(2.4rem);
color: #696969; color: #696969;
flex: 1; flex: 1;
@include font-size(2.4rem);
} }
} }

View File

@ -3,7 +3,7 @@ import type { Shout } from '../../graphql/types.gen'
import { capitalize } from '../../utils' import { capitalize } from '../../utils'
import { translit } from '../../utils/ru2en' import { translit } from '../../utils/ru2en'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import styles from './Card.module.scss' import styles from './ArticleCard.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { CardTopic } from './CardTopic' import { CardTopic } from './CardTopic'
import { ShoutRatingControl } from '../Article/ShoutRatingControl' import { ShoutRatingControl } from '../Article/ShoutRatingControl'
@ -37,7 +37,6 @@ interface ArticleCardProps {
isBeside?: boolean isBeside?: boolean
} }
article: Shout article: Shout
scrollTo: 'comments'
} }
const getTitleAndSubtitle = (article: Shout): { title: string; subtitle: string } => { const getTitleAndSubtitle = (article: Shout): { title: string; subtitle: string } => {

View File

@ -66,9 +66,9 @@
} }
a { a {
border: none;
@include font-size(1.5rem); @include font-size(1.5rem);
border: none;
font-weight: 500; font-weight: 500;
padding-right: 0.3em; padding-right: 0.3em;
white-space: nowrap; white-space: nowrap;

View File

@ -1,8 +1,8 @@
// TODO: additional entities list column + article // TODO: additional entities list column + article
import { For, Show } from 'solid-js' import { For, Show } from 'solid-js'
import { ArticleCard } from './Card' import { ArticleCard } from './ArticleCard'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/AuthorCard'
import { TopicCard } from '../Topic/Card' import { TopicCard } from '../Topic/Card'
import styles from './Beside.module.scss' import styles from './Beside.module.scss'
import type { Author, Shout, Topic, User } from '../../graphql/types.gen' import type { Author, Shout, Topic, User } from '../../graphql/types.gen'

View File

@ -1,7 +1,7 @@
import type { JSX } from 'solid-js/jsx-runtime' import type { JSX } from 'solid-js/jsx-runtime'
import { For, Show } from 'solid-js' import { For, Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './Card' import { ArticleCard } from './ArticleCard'
import './Group.scss' import './Group.scss'
interface GroupProps { interface GroupProps {

View File

@ -1,8 +0,0 @@
.article-preview {
background: center url('loading.gif') no-repeat;
margin: auto;
vertical-align: middle;
text-align: center;
/* TODO: centered full viewport */
}

View File

@ -1,56 +0,0 @@
import { Row1 } from './Row1'
import { Row2 } from './Row2'
import { Row3 } from './Row3'
import { shuffle } from '../../utils'
import { createMemo, createSignal, For, Suspense } from 'solid-js'
import type { JSX } from 'solid-js'
import type { Shout } from '../../graphql/types.gen'
import './List.scss'
import { useLocalize } from '../../context/localize'
export const Block6 = (props: { articles: Shout[] }) => {
const dice = createMemo(() => shuffle([Row1, Row2, Row3]))
return (
<>
<For each={dice()}>{(c: (ppp: Shout[]) => JSX.Element) => c(props.articles)}</For>
</>
)
}
interface ArticleListProps {
articles: Shout[]
page: number
size: number
}
export default (props: ArticleListProps) => {
const { t } = useLocalize()
const [articles, setArticles] = createSignal(
props.articles.slice(props.page * props.size, props.size * (props.page + 1)) || []
)
const [loadingMore, setLoadingMore] = createSignal(false)
// const [, { more }] = useZine()
const handleMore = () => {
setArticles(props.articles.slice(props.page * props.size, props.size * (props.page + 1)))
if (props.size * (props.page + 1) > props.articles.length) {
console.log('[article-list] load more')
setLoadingMore(true)
// TODO: more()
setLoadingMore(false)
}
}
const x: number = Math.floor(articles().length / 6)
// eslint-disable-next-line unicorn/new-for-builtins
const numbers: number[] = [...Array(x).keys()]
return (
<Suspense fallback={<div class="article-preview">{t('Loading')}</div>}>
<For each={numbers}>
{() => <Block6 articles={articles().slice(0, Math.min(6, articles().length))} />}
</For>
<a href={''} onClick={handleMore} classList={{ disabled: loadingMore() }}>
{loadingMore() ? '...' : t('More')}
</a>
</Suspense>
)
}

View File

@ -1,6 +1,6 @@
import { Show } from 'solid-js' import { Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './Card' import { ArticleCard } from './ArticleCard'
export const Row1 = (props: { article: Shout; nodate?: boolean }) => ( export const Row1 = (props: { article: Shout; nodate?: boolean }) => (
<Show when={!!props.article}> <Show when={!!props.article}>

View File

@ -1,6 +1,6 @@
import { createComputed, createSignal, Show, For } from 'solid-js' import { createComputed, createSignal, Show, For } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './Card' import { ArticleCard } from './ArticleCard'
const x = [ const x = [
['12', '12'], ['12', '12'],

View File

@ -1,7 +1,7 @@
import type { JSX } from 'solid-js/jsx-runtime' import type { JSX } from 'solid-js/jsx-runtime'
import { For } from 'solid-js' import { For } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './Card' import { ArticleCard } from './ArticleCard'
export const Row3 = (props: { articles: Shout[]; header?: JSX.Element; nodate?: boolean }) => { export const Row3 = (props: { articles: Shout[]; header?: JSX.Element; nodate?: boolean }) => {
return ( return (

View File

@ -1,5 +1,5 @@
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './Card' import { ArticleCard } from './ArticleCard'
export const Row5 = (props: { articles: Shout[]; nodate?: boolean }) => { export const Row5 = (props: { articles: Shout[]; nodate?: boolean }) => {
return ( return (

View File

@ -1,6 +1,6 @@
import { For } from 'solid-js' import { For } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './Card' import { ArticleCard } from './ArticleCard'
export default (props: { articles: Shout[] }) => ( export default (props: { articles: Shout[] }) => (
<div class="floor floor--7"> <div class="floor floor--7">

View File

@ -19,6 +19,7 @@
.counter { .counter {
@include font-size(1.2rem); @include font-size(1.2rem);
align-items: center; align-items: center;
align-self: flex-start; align-self: flex-start;
background: #f6f6f6; background: #f6f6f6;

View File

@ -84,8 +84,8 @@ export const Sidebar = (props: FeedSidebarProps) => {
<div class={styles.sidebar}> <div class={styles.sidebar}>
<ul> <ul>
<For each={menuItems}> <For each={menuItems}>
{(item: ListItem, index) => ( {(item: ListItem) => (
<li key={index}> <li>
<a href="#"> <a href="#">
<span class={styles.sidebarItemName}> <span class={styles.sidebarItemName}>
{item.icon && <Icon name={item.icon} class={styles.icon} />} {item.icon && <Icon name={item.icon} class={styles.icon} />}

View File

@ -5,11 +5,11 @@ import { useLocalize } from '../../context/localize'
export type MessageActionType = 'reply' | 'copy' | 'pin' | 'forward' | 'select' | 'delete' export type MessageActionType = 'reply' | 'copy' | 'pin' | 'forward' | 'select' | 'delete'
type MessageActionsPopup = { type MessageActionsPopupProps = {
actionSelect?: (selectedAction) => void actionSelect?: (selectedAction) => void
} & Omit<PopupProps, 'children'> } & Omit<PopupProps, 'children'>
export const MessageActionsPopup = (props: MessageActionsPopup) => { export const MessageActionsPopup = (props: MessageActionsPopupProps) => {
const [selectedAction, setSelectedAction] = createSignal<MessageActionType | null>(null) const [selectedAction, setSelectedAction] = createSignal<MessageActionType | null>(null)
const { t } = useLocalize() const { t } = useLocalize()
const actions: { name: string; action: MessageActionType }[] = [ const actions: { name: string; action: MessageActionType }[] = [

View File

@ -33,12 +33,12 @@
} }
.authImage { .authImage {
@include font-size(1.5rem);
background: #141414 url('/auth-page.jpg') center no-repeat; background: #141414 url('/auth-page.jpg') center no-repeat;
background-size: cover; background-size: cover;
color: #fff; color: #fff;
display: flex; display: flex;
@include font-size(1.5rem);
padding: 3em; padding: 3em;
position: relative; position: relative;
@ -81,8 +81,9 @@
} }
.disclaimer { .disclaimer {
color: #9fa1a7;
@include font-size(1.2rem); @include font-size(1.2rem);
color: #9fa1a7;
} }
.authActions { .authActions {

View File

@ -130,10 +130,11 @@
display: inline-flex; display: inline-flex;
font-weight: 500; font-weight: 500;
position: relative; position: relative;
//flex: 1 100% !important;
// flex: 1 100% !important;
// replace row > * selector to remove !important // replace row > * selector to remove !important
//width: auto !important; // width: auto !important;
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
flex: 1; flex: 1;
@ -142,9 +143,10 @@
} }
.mainNavigationWrapper { .mainNavigationWrapper {
@include font-size(1.7rem);
padding-left: 0; padding-left: 0;
position: relative; position: relative;
@include font-size(1.7rem);
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
display: none; display: none;
@ -513,8 +515,9 @@
} }
.textLabel { .textLabel {
// padding: 0 1.2rem;
display: inline; display: inline;
//padding: 0 1.2rem;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }

View File

@ -3,11 +3,10 @@ import { clsx } from 'clsx'
import { router, useRouter } from '../../stores/router' import { router, useRouter } from '../../stores/router'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { createSignal, Show } from 'solid-js' import { createMemo, createSignal, Show } from 'solid-js'
import Notifications from './Notifications' import Notifications from './Notifications'
import { ProfilePopup } from './ProfilePopup' import { ProfilePopup } from './ProfilePopup'
import Userpic from '../Author/Userpic' import Userpic from '../Author/Userpic'
import type { Author } from '../../graphql/types.gen'
import { showModal, useWarningsStore } from '../../stores/ui' import { showModal, useWarningsStore } from '../../stores/ui'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
@ -44,12 +43,24 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
toggleWarnings() toggleWarnings()
} }
const isEditorPage = createMemo(
() => page().route === 'create' || page().route === 'edit' || page().route === 'editSettings'
)
const showNotifications = createMemo(() => isAuthenticated() && !isEditorPage())
const showSaveButton = createMemo(() => isAuthenticated() && isEditorPage())
const showCreatePostButton = createMemo(() => isAuthenticated() && !isEditorPage())
const handleBurgerButtonClick = () => {
toggleEditorPanel()
}
return ( return (
<ShowOnlyOnClient> <ShowOnlyOnClient>
<Show when={isSessionLoaded()} keyed={true}> <Show when={isSessionLoaded()} keyed={true}>
<div class={clsx(styles.usernav, 'col')}> <div class={clsx(styles.usernav, 'col')}>
<div class={clsx(styles.userControl, styles.userControl, 'col')}> <div class={clsx(styles.userControl, styles.userControl, 'col')}>
<Show when={page().route !== 'create'}> <Show when={showCreatePostButton()}>
<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>
@ -58,7 +69,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
</div> </div>
</Show> </Show>
<Show when={isAuthenticated() && page().route !== 'create'}> <Show when={showNotifications()}>
<div class={styles.userControlItem}> <div class={styles.userControlItem}>
<a href="#" onClick={handleBellIconClick}> <a href="#" onClick={handleBellIconClick}>
<div> <div>
@ -68,7 +79,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
</div> </div>
</Show> </Show>
<Show when={isAuthenticated() && page().route === 'create'}> <Show when={showSaveButton()}>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}> <div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
<Button <Button
value={ value={
@ -97,7 +108,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
<Button <Button
value={<Icon name="burger" />} value={<Icon name="burger" />}
variant={'outline'} variant={'outline'}
onClick={() => toggleEditorPanel()} onClick={handleBurgerButtonClick}
/> />
</div> </div>
</Show> </Show>
@ -136,7 +147,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
<div class={styles.userControlItem}> <div class={styles.userControlItem}>
<button class={styles.button}> <button class={styles.button}>
<div classList={{ entered: page().path === `/${session().user?.slug}` }}> <div classList={{ entered: page().path === `/${session().user?.slug}` }}>
<Userpic user={session().user as Author} class={styles.userpic} /> <Userpic user={session().user} class={styles.userpic} />
</div> </div>
</button> </button>
</div> </div>

View File

@ -45,7 +45,7 @@
width: 100%; width: 100%;
} }
@media (min-width: 800px) and (max-width: 991px) { @media (width >= 800px) and (width <= 991px) {
// top: 11em; // top: 11em;
} }
} }
@ -53,7 +53,8 @@
&.narrow { &.narrow {
max-width: 460px; max-width: 460px;
width: 50%; width: 50%;
@media (min-width: 800px) and (max-width: 991px) {
@media (width >= 800px) and (width <= 991px) {
width: 80%; width: 80%;
} }

View File

@ -1,4 +1,4 @@
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/AuthorCard'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { translit } from '../../utils/ru2en' import { translit } from '../../utils/ru2en'

View File

@ -24,9 +24,9 @@
} }
.topicTitle { .topicTitle {
font-weight: bold;
@include font-size(2.2rem); @include font-size(2.2rem);
font-weight: bold;
margin-bottom: 1.2rem; margin-bottom: 1.2rem;
margin-top: 0.5rem !important; margin-top: 0.5rem !important;
} }

View File

@ -37,7 +37,7 @@ export const FullTopic = (props: Props) => {
{t('Unfollow the topic')} {t('Unfollow the topic')}
</button> </button>
</Show> </Show>
<a href={`/create/${props.topic.slug}`}>{t('Write about the topic')}</a> <a href={`/create/?topicId=${props.topic.id}`}>{t('Write about the topic')}</a>
</div> </div>
<Show when={props.topic.pic}> <Show when={props.topic.pic}>
<img src={props.topic.pic} alt={props.topic.title} /> <img src={props.topic.pic} alt={props.topic.title} />

View File

@ -3,7 +3,7 @@ import type { Author } from '../../graphql/types.gen'
import { setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors' import { setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/AuthorCard'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { translit } from '../../utils/ru2en' import { translit } from '../../utils/ru2en'

View File

@ -14,7 +14,7 @@ import stylesArticle from '../Article/Article.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import Userpic from '../Author/Userpic' import Userpic from '../Author/Userpic'
import { Popup } from '../_shared/Popup' import { Popup } from '../_shared/Popup'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/AuthorCard'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import { Comment } from '../Article/Comment' import { Comment } from '../Article/Comment'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'

View File

@ -0,0 +1,99 @@
:global(.main-content) {
position: static;
}
.articlePreview {
border: 2px solid #e8e8e8;
min-height: 10em;
padding: 1rem 1.2rem;
}
.formHolder {
padding: 0 4rem;
}
.container {
.titleInput,
.subtitleInput {
border: 0;
outline: 0;
padding: 0;
font-size: 36px;
&::placeholder {
opacity: 0.3;
color: #000;
}
}
.titleInput {
font-weight: 700;
}
}
.editSettings,
.edit {
display: none;
&.visible {
display: block;
}
}
.asidePanel {
background: #1f1f1f;
color: rgb(255 255 255 / 35%);
display: flex;
flex-direction: column;
font-size: 1.7rem;
justify-content: flex-start;
height: 100%;
line-height: 1.4;
padding: $grid-gutter-width;
position: fixed;
transition: transform 0.3s;
right: 0;
top: 0;
z-index: 10;
.close {
filter: invert(1);
margin: -1.6rem 0 0 -1.6rem;
}
section {
border-bottom: 2px solid rgb(255 255 255 / 10%);
// margin-bottom: 1.8rem;
margin-top: 1.8rem;
padding-bottom: 1.8rem;
p {
margin: 0.6em 0;
&:last-child {
margin-bottom: 0;
}
}
}
.button {
font-weight: normal;
margin-left: -1.6rem;
&:hover {
color: #fff;
text-decoration: none;
}
}
a {
color: rgb(255 255 255 / 35%);
font-weight: normal !important;
&:hover {
background: none;
color: #fff;
}
}
}

View File

@ -1,14 +1,14 @@
import { createSignal, onMount, Show } from 'solid-js' import { createSignal, onMount, Show } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './Create.module.scss' import styles from './Edit.module.scss'
import { Title } from '@solidjs/meta' import { Title } from '@solidjs/meta'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import type { Topic } from '../../graphql/types.gen' import type { Shout, Topic } from '../../graphql/types.gen'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import { TopicSelect } from '../Editor/TopicSelect/TopicSelect' import { TopicSelect } from '../Editor/TopicSelect/TopicSelect'
import { router, useRouter } from '../../stores/router' import { router, useRouter } from '../../stores/router'
import { getPagePath } from '@nanostores/router' import { openPage } from '@nanostores/router'
import { translit } from '../../utils/ru2en' import { translit } from '../../utils/ru2en'
import { Editor } from '../Editor/Editor' import { Editor } from '../Editor/Editor'
import { Panel } from '../Editor/Panel' import { Panel } from '../Editor/Panel'
@ -18,12 +18,16 @@ type ShoutForm = {
title: string title: string
subtitle: string subtitle: string
selectedTopics: Topic[] selectedTopics: Topic[]
mainTopic: Topic mainTopic: string
body: string body: string
coverImageUrl: string coverImageUrl: string
} }
export const CreateView = () => { type EditViewProps = {
shout: Shout
}
export const EditView = (props: EditViewProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const [topics, setTopics] = createSignal<Topic[]>(null) const [topics, setTopics] = createSignal<Topic[]>(null)
@ -32,13 +36,13 @@ export const CreateView = () => {
const [isSlugChanged, setIsSlugChanged] = createSignal(false) const [isSlugChanged, setIsSlugChanged] = createSignal(false)
const [form, setForm] = createStore<ShoutForm>({ const [form, setForm] = createStore<ShoutForm>({
slug: '', slug: props.shout.slug,
title: '', title: props.shout.title,
subtitle: '', subtitle: props.shout.subtitle,
selectedTopics: [], selectedTopics: props.shout.topics,
mainTopic: null, mainTopic: props.shout.mainTopic,
body: '', body: props.shout.body,
coverImageUrl: '' coverImageUrl: props.shout.cover
}) })
onMount(async () => { onMount(async () => {
@ -49,18 +53,9 @@ export const CreateView = () => {
const handleFormSubmit = async (e) => { const handleFormSubmit = async (e) => {
e.preventDefault() e.preventDefault()
const newShout = await apiClient.createArticle({ const article = await apiClient.publishDraft()
article: {
slug: form.slug,
title: form.title,
subtitle: form.subtitle,
body: form.body,
topics: form.selectedTopics.map((topic) => topic.slug),
mainTopic: form.selectedTopics[0].slug
}
})
router.open(getPagePath(router, 'article', { slug: newShout.slug })) openPage(router, 'article', { slug: article.slug })
} }
const handleTitleInputChange = (e) => { const handleTitleInputChange = (e) => {
@ -82,17 +77,36 @@ export const CreateView = () => {
setForm('slug', slug) setForm('slug', slug)
} }
const handleSaveButtonClick = async (e) => {
e.preventDefault()
await apiClient.updateArticle({
slug: props.shout.slug,
article: {
slug: form.slug,
title: form.title,
subtitle: form.subtitle,
body: form.body,
topics: form.selectedTopics.map((topic) => topic.slug),
mainTopic: form.selectedTopics[0].slug
}
})
openPage(router, 'drafts')
}
return ( return (
<> <>
<div class={styles.container}> <div class={styles.container}>
<Title>{t('Write an article')}</Title> <Title>{t('Write an article')}</Title>
<form onSubmit={handleFormSubmit}> <form onSubmit={handleFormSubmit}>
<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">
<div <div
class={clsx(styles.create, { class={clsx(styles.edit, {
[styles.visible]: page().route === 'create' [styles.visible]: page().route === 'edit'
})} })}
> >
<input <input
@ -115,16 +129,15 @@ export const CreateView = () => {
onChange={(e) => setForm('subtitle', e.currentTarget.value)} onChange={(e) => setForm('subtitle', e.currentTarget.value)}
/> />
<Editor shoutId={42} onChange={(body) => setForm('body', body)} /> <Editor
shoutSlug={props.shout.slug}
<div class={styles.saveBlock}> initialContent={props.shout.body}
{/*<button class={clsx('button button--outline', styles.button)}>Сохранить</button>*/} onChange={(body) => setForm('body', body)}
<a href={getPagePath(router, 'createSettings')}>Настройки</a> />
</div>
</div> </div>
<div <div
class={clsx(styles.createSettings, { class={clsx(styles.editSettings, {
[styles.visible]: page().route === 'createSettings' [styles.visible]: page().route === 'editSettings'
})} })}
> >
<h1>Настройки публикации</h1> <h1>Настройки публикации</h1>
@ -199,27 +212,15 @@ export const CreateView = () => {
выглядеть на&nbsp;главной странице выглядеть на&nbsp;главной странице
</p> </p>
<div class={styles.articlePreview} /> <div class={styles.articlePreview} />
<div class={styles.saveBlock}>
<p>
Проверьте ещё раз введённые данные, если всё верно, вы&nbsp;можете сохранить или
опубликовать ваш текст
</p>
{/*<button class={clsx('button button--outline', styles.button)}>Сохранить</button>*/}
<a href={getPagePath(router, 'create')}>Назад</a>
<button type="submit" class={clsx('button button--submit', styles.button)}>
Опубликовать
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
<Panel /> <Panel shoutSlug={props.shout.slug} />
</> </>
) )
} }
export default CreateView export default EditView

View File

@ -1,5 +1,6 @@
.feedNavigation { .feedNavigation {
@include font-size(1.5rem); @include font-size(1.5rem);
font-weight: 500; font-weight: 500;
h4 { h4 {

View File

@ -1,8 +1,8 @@
import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js' import { createEffect, createSignal, For, onMount, Show } from 'solid-js'
import '../../styles/Feed.scss' import '../../styles/Feed.scss'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { ArticleCard } from '../Feed/Card' import { ArticleCard } from '../Feed/ArticleCard'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/AuthorCard'
import { Sidebar } from '../Feed/Sidebar' import { Sidebar } from '../Feed/Sidebar'
import { loadShouts, useArticlesStore } from '../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'

View File

@ -22,7 +22,7 @@ import {
import { useTopAuthorsStore } from '../../stores/zine/topAuthors' import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages' import { splitToPages } from '../../utils/splitToPages'
import { ArticleCard } from '../Feed/Card' import { ArticleCard } from '../Feed/ArticleCard'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
type HomeProps = { type HomeProps = {

View File

@ -23,12 +23,14 @@ type InboxSearchParams = {
initChat: string initChat: string
chat: string chat: string
} }
// const userSearch = (array: Author[], keyword: string) => {
// const searchTerm = keyword.toLowerCase() const userSearch = (array: Author[], keyword: string) => {
// return array.filter((value) => { return array.filter((value) => new RegExp(keyword.trim(), 'gi').test(value.name))
// return value.name.toLowerCase().match(new RegExp(searchTerm, 'g')) }
// })
// } const handleOpenInviteModal = () => {
showModal('inviteToChat')
}
export const InboxView = () => { export const InboxView = () => {
const { t } = useLocalize() const { t } = useLocalize()
@ -46,14 +48,15 @@ export const InboxView = () => {
const [messageToReply, setMessageToReply] = createSignal<MessageType | null>(null) const [messageToReply, setMessageToReply] = createSignal<MessageType | null>(null)
const { session } = useSession() const { session } = useSession()
const currentUserId = createMemo(() => session()?.user.id) const currentUserId = createMemo(() => session()?.user.id)
// Поиск по диалогам // Поиск по диалогам
const getQuery = (query) => { const getQuery = (query) => {
// if (query().length >= 2) { if (query().length >= 2) {
// const match = userSearch(recipients(), query()) const match = userSearch(recipients(), query())
// setRecipients(match) setRecipients(match)
// } else { } else {
// setRecipients(cashedRecipients()) // setRecipients(cashedRecipients())
// } }
} }
let chatWindow let chatWindow
@ -136,10 +139,6 @@ export const InboxView = () => {
} }
}) })
const handleOpenInviteModal = () => {
showModal('inviteToChat')
}
const chatsToShow = () => { const chatsToShow = () => {
const sorted = chats().sort((a, b) => { const sorted = chats().sort((a, b) => {
return b.updatedAt - a.updatedAt return b.updatedAt - a.updatedAt
@ -158,7 +157,10 @@ export const InboxView = () => {
} }
const handleKeyDown = async (event) => { const handleKeyDown = async (event) => {
if (event.keyCode === 13 && event.shiftKey) return if (event.keyCode === 13 && event.shiftKey) {
return
}
if (event.keyCode === 13 && !event.shiftKey && postMessageText()?.trim().length > 0) { if (event.keyCode === 13 && !event.shiftKey && postMessageText()?.trim().length > 0) {
event.preventDefault() event.preventDefault()
handleSubmit() handleSubmit()

View File

@ -1,7 +1,7 @@
import { Show, For, createSignal } from 'solid-js' import { Show, For, createSignal } from 'solid-js'
import '../../styles/Search.scss' import '../../styles/Search.scss'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from '../Feed/Card' import { ArticleCard } from '../Feed/ArticleCard'
import { loadShouts, useArticlesStore } from '../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'

View File

@ -15,7 +15,7 @@ import { splitToPages } from '../../utils/splitToPages'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import Slider from '../_shared/Slider' import Slider from '../_shared/Slider'
import { Row1 } from '../Feed/Row1' import { Row1 } from '../Feed/Row1'
import { ArticleCard } from '../Feed/Card' import { ArticleCard } from '../Feed/ArticleCard'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
type TopicsPageSearchParams = { type TopicsPageSearchParams = {

View File

@ -5,16 +5,15 @@
max-height: 360px; max-height: 360px;
overflow: auto; overflow: auto;
} }
.item { .item {
display: flex; display: flex;
flex-direction: row; flex-flow: row nowrap;
flex-wrap: nowrap;
align-items: center; align-items: center;
.user { .user {
display: flex; display: flex;
flex-direction: row; flex-flow: row nowrap;
flex-wrap: nowrap;
align-items: center; align-items: center;
margin-right: 1.2rem; margin-right: 1.2rem;

View File

@ -4,7 +4,6 @@ import { Accessor, createContext, createSignal, useContext } from 'solid-js'
type WordCounter = { type WordCounter = {
characters: number characters: number
words: number words: number
paragraphs?: number
} }
type EditorContextType = { type EditorContextType = {
@ -23,12 +22,12 @@ export function useEditorContext() {
} }
export const EditorProvider = (props: { children: JSX.Element }) => { export const EditorProvider = (props: { children: JSX.Element }) => {
const [isEditorPanelVisible, setEditorPanelVisible] = createSignal<boolean>(false) const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
const [wordCounter, setWordCounter] = createSignal<WordCounter>({ const [wordCounter, setWordCounter] = createSignal<WordCounter>({
characters: 0, characters: 0,
words: 0 words: 0
}) })
const toggleEditorPanel = () => setEditorPanelVisible(!isEditorPanelVisible()) const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value)
const countWords = (value) => setWordCounter(value) const countWords = (value) => setWordCounter(value)
const actions = { const actions = {
toggleEditorPanel, toggleEditorPanel,

View File

@ -2,7 +2,7 @@ import type { i18n } from 'i18next'
import type { Accessor, JSX } from 'solid-js' import type { Accessor, JSX } from 'solid-js'
import { createContext, createEffect, createSignal, Show, useContext } from 'solid-js' import { createContext, createEffect, createSignal, Show, useContext } from 'solid-js'
import { useRouter } from '../stores/router' import { useRouter } from '../stores/router'
import i18next from 'i18next' import i18next, { changeLanguage, t } from 'i18next'
import Cookie from 'js-cookie' import Cookie from 'js-cookie'
type LocalizeContextType = { type LocalizeContextType = {
@ -30,13 +30,13 @@ export const LocalizeProvider = (props: { children: JSX.Element }) => {
const lng: Language = searchParams().lng === 'en' ? 'en' : 'ru' const lng: Language = searchParams().lng === 'en' ? 'en' : 'ru'
i18next.changeLanguage(lng) changeLanguage(lng)
setLang(lng) setLang(lng)
Cookie.set('lng', lng) Cookie.set('lng', lng)
changeSearchParam('lng', null) changeSearchParam('lng', null)
}) })
const value: LocalizeContextType = { t: i18next.t, lang, setLang } const value: LocalizeContextType = { t, lang, setLang }
return ( return (
<LocalizeContext.Provider value={value}> <LocalizeContext.Provider value={value}>

View File

@ -54,7 +54,6 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
initialValue: null initialValue: null
}) })
const userSlug = createMemo(() => session()?.user?.slug)
const user = createMemo(() => session()?.user) const user = createMemo(() => session()?.user)
const isAuthenticated = createMemo(() => Boolean(session()?.user?.slug)) const isAuthenticated = createMemo(() => Boolean(session()?.user?.slug))

View File

@ -5,23 +5,11 @@ export default gql`
createShout(inp: $shout) { createShout(inp: $shout) {
error error
shout { shout {
id _id: slug
slug slug
title title
subtitle subtitle
body body
topics {
id
title
slug
}
authors {
id
name
slug
userpic
caption
}
} }
} }
} }

View File

@ -0,0 +1,16 @@
import { gql } from '@urql/core'
export default gql`
mutation UpdateShoutMutation($slug: String!) {
publishShout(slug: $slug) {
error
shout {
id
slug
title
subtitle
body
}
}
}
`

View File

@ -1,29 +1,15 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
mutation UpdateShoutMutation($shout: Shout!) { mutation UpdateShoutMutation($slug: String!, $shout: ShoutInput!) {
updateShout(input: $shout) { updateShout(slug: $slug, inp: $shout) {
error error
shout { shout {
_id: slug id
slug slug
title title
subtitle subtitle
image
body body
topics {
# id
title
slug
image
}
authors {
id
name
slug
userpic
caption
}
} }
} }
} }

View File

@ -1,28 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation DraftCreateMutation($draft: DraftInput!) {
createDraft(draft: $draft) {
error
draft {
id
slug
title
subtitle
body
topics {
# id
title
slug
}
authors {
id
name
slug
userpic
caption
}
}
}
}
`

View File

@ -1,9 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation DraftDestroyMutation($draft: Int!) {
deleteDraft(draft: $draft) {
error
}
}
`

View File

@ -1,28 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation ShoutFromDraftMutation($draft: Int!) {
draftToShout(draft: $draft) {
error
shout {
_id: slug
slug
title
subtitle
body
topics {
# id
title
slug
}
authors {
id
name
slug
userpic
caption
}
}
}
}
`

View File

@ -1,28 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation DraftUpdateMutation($draft: DraftInput!) {
updateDraft(draft: $draft) {
error
draft {
id
slug
title
subtitle
body
topics {
# id
title
slug
}
authors {
id
name
slug
userpic
caption
}
}
}
}
`

View File

@ -0,0 +1,36 @@
import { gql } from '@urql/core'
export default gql`
query LoadDraftsQuery($options: LoadShoutsOptions) {
loadDrafts(options: $options) {
id
title
subtitle
slug
layout
cover
# community
mainTopic
topics {
# id
title
body
slug
stat {
_id: shouts
shouts
authors
followers
}
}
authors {
id
name
slug
userpic
}
createdAt
publishedAt
}
}
`

View File

@ -1,17 +0,0 @@
import { gql } from '@urql/core'
export default gql`
query MyDraftsQuery {
loadDrafts {
authors {
id
slug
name
pic
}
createdAt
body
title
}
}
`

View File

@ -105,30 +105,6 @@ export type Community = {
slug: Scalars['String'] slug: Scalars['String']
} }
export type DraftCollab = {
authors: Array<Maybe<Scalars['Int']>>
body?: Maybe<Scalars['String']>
chat?: Maybe<Chat>
cover?: Maybe<Scalars['String']>
createdAt: Scalars['Int']
layout?: Maybe<Scalars['String']>
slug?: Maybe<Scalars['String']>
subtitle?: Maybe<Scalars['String']>
title?: Maybe<Scalars['String']>
topics?: Maybe<Array<Maybe<Scalars['String']>>>
updatedAt?: Maybe<Scalars['Int']>
}
export type DraftInput = {
authors?: InputMaybe<Array<InputMaybe<Scalars['Int']>>>
body?: InputMaybe<Scalars['String']>
cover?: InputMaybe<Scalars['String']>
slug?: InputMaybe<Scalars['String']>
subtitle?: InputMaybe<Scalars['String']>
title?: InputMaybe<Scalars['String']>
topics?: InputMaybe<Array<InputMaybe<Scalars['Int']>>>
}
export enum FollowingEntity { export enum FollowingEntity {
Author = 'AUTHOR', Author = 'AUTHOR',
Community = 'COMMUNITY', Community = 'COMMUNITY',
@ -185,29 +161,24 @@ export type MessagesBy = {
export type Mutation = { export type Mutation = {
confirmEmail: AuthResult confirmEmail: AuthResult
createChat: Result createChat: Result
createDraft: Result
createMessage: Result createMessage: Result
createReaction: Result createReaction: Result
createShout: Result createShout: Result
createTopic: Result createTopic: Result
deleteChat: Result deleteChat: Result
deleteDraft: Result
deleteMessage: Result deleteMessage: Result
deleteReaction: Result deleteReaction: Result
deleteShout: Result deleteShout: Result
destroyTopic: Result destroyTopic: Result
draftToShout: Result
follow: Result follow: Result
getSession: AuthResult getSession: AuthResult
inviteAccept: Result
inviteAuthor: Result
markAsRead: Result markAsRead: Result
publishShout: Result
rateUser: Result rateUser: Result
registerUser: AuthResult registerUser: AuthResult
sendLink: Result sendLink: Result
unfollow: Result unfollow: Result
updateChat: Result updateChat: Result
updateDraft: Result
updateMessage: Result updateMessage: Result
updateOnlineStatus: Result updateOnlineStatus: Result
updateProfile: Result updateProfile: Result
@ -225,10 +196,6 @@ export type MutationCreateChatArgs = {
title?: InputMaybe<Scalars['String']> title?: InputMaybe<Scalars['String']>
} }
export type MutationCreateDraftArgs = {
draft: DraftInput
}
export type MutationCreateMessageArgs = { export type MutationCreateMessageArgs = {
body: Scalars['String'] body: Scalars['String']
chat: Scalars['String'] chat: Scalars['String']
@ -251,10 +218,6 @@ export type MutationDeleteChatArgs = {
chatId: Scalars['String'] chatId: Scalars['String']
} }
export type MutationDeleteDraftArgs = {
draft: Scalars['Int']
}
export type MutationDeleteMessageArgs = { export type MutationDeleteMessageArgs = {
chatId: Scalars['String'] chatId: Scalars['String']
id: Scalars['Int'] id: Scalars['Int']
@ -272,29 +235,21 @@ export type MutationDestroyTopicArgs = {
slug: Scalars['String'] slug: Scalars['String']
} }
export type MutationDraftToShoutArgs = {
draft: Scalars['Int']
}
export type MutationFollowArgs = { export type MutationFollowArgs = {
slug: Scalars['String'] slug: Scalars['String']
what: FollowingEntity what: FollowingEntity
} }
export type MutationInviteAcceptArgs = {
draft: Scalars['Int']
}
export type MutationInviteAuthorArgs = {
author: Scalars['Int']
draft: Scalars['Int']
}
export type MutationMarkAsReadArgs = { export type MutationMarkAsReadArgs = {
chatId: Scalars['String'] chatId: Scalars['String']
ids: Array<InputMaybe<Scalars['Int']>> ids: Array<InputMaybe<Scalars['Int']>>
} }
export type MutationPublishShoutArgs = {
inp: ShoutInput
slug: Scalars['String']
}
export type MutationRateUserArgs = { export type MutationRateUserArgs = {
slug: Scalars['String'] slug: Scalars['String']
value: Scalars['Int'] value: Scalars['Int']
@ -321,10 +276,6 @@ export type MutationUpdateChatArgs = {
chat: ChatInput chat: ChatInput
} }
export type MutationUpdateDraftArgs = {
draft: DraftInput
}
export type MutationUpdateMessageArgs = { export type MutationUpdateMessageArgs = {
body: Scalars['String'] body: Scalars['String']
chatId: Scalars['String'] chatId: Scalars['String']
@ -342,6 +293,7 @@ export type MutationUpdateReactionArgs = {
export type MutationUpdateShoutArgs = { export type MutationUpdateShoutArgs = {
inp: ShoutInput inp: ShoutInput
slug: Scalars['String']
} }
export type MutationUpdateTopicArgs = { export type MutationUpdateTopicArgs = {
@ -380,7 +332,7 @@ export type Query = {
isEmailUsed: Scalars['Boolean'] isEmailUsed: Scalars['Boolean']
loadAuthorsBy: Array<Maybe<Author>> loadAuthorsBy: Array<Maybe<Author>>
loadChats: Result loadChats: Result
loadDrafts: Array<Maybe<DraftCollab>> loadDrafts: Array<Maybe<Shout>>
loadMessagesBy: Result loadMessagesBy: Result
loadReactionsBy: Array<Maybe<Reaction>> loadReactionsBy: Array<Maybe<Reaction>>
loadRecipients: Result loadRecipients: Result
@ -388,6 +340,7 @@ export type Query = {
loadShouts: Array<Maybe<Shout>> loadShouts: Array<Maybe<Shout>>
markdownBody: Scalars['String'] markdownBody: Scalars['String']
myFeed?: Maybe<Array<Maybe<Shout>>> myFeed?: Maybe<Array<Maybe<Shout>>>
publishShout: Array<Maybe<Shout>>
searchMessages: Result searchMessages: Result
searchRecipients: Result searchRecipients: Result
signIn: AuthResult signIn: AuthResult
@ -424,6 +377,10 @@ export type QueryLoadChatsArgs = {
offset?: InputMaybe<Scalars['Int']> offset?: InputMaybe<Scalars['Int']>
} }
export type QueryLoadDraftsArgs = {
options?: InputMaybe<LoadShoutsOptions>
}
export type QueryLoadMessagesByArgs = { export type QueryLoadMessagesByArgs = {
by: MessagesBy by: MessagesBy
limit?: InputMaybe<Scalars['Int']> limit?: InputMaybe<Scalars['Int']>
@ -457,6 +414,10 @@ export type QueryMyFeedArgs = {
options?: InputMaybe<LoadShoutsOptions> options?: InputMaybe<LoadShoutsOptions>
} }
export type QueryPublishShoutArgs = {
slug: Scalars['String']
}
export type QuerySearchMessagesArgs = { export type QuerySearchMessagesArgs = {
by: MessagesBy by: MessagesBy
limit?: InputMaybe<Scalars['Int']> limit?: InputMaybe<Scalars['Int']>
@ -583,8 +544,6 @@ export type Result = {
chats?: Maybe<Array<Maybe<Chat>>> chats?: Maybe<Array<Maybe<Chat>>>
communities?: Maybe<Array<Maybe<Community>>> communities?: Maybe<Array<Maybe<Community>>>
community?: Maybe<Community> community?: Maybe<Community>
draft?: Maybe<DraftCollab>
drafts?: Maybe<Array<Maybe<DraftCollab>>>
error?: Maybe<Scalars['String']> error?: Maybe<Scalars['String']>
members?: Maybe<Array<Maybe<ChatMember>>> members?: Maybe<Array<Maybe<ChatMember>>>
message?: Maybe<Message> message?: Maybe<Message>
@ -596,7 +555,6 @@ export type Result = {
slugs?: Maybe<Array<Maybe<Scalars['String']>>> slugs?: Maybe<Array<Maybe<Scalars['String']>>>
topic?: Maybe<Topic> topic?: Maybe<Topic>
topics?: Maybe<Array<Maybe<Topic>>> topics?: Maybe<Array<Maybe<Topic>>>
uids?: Maybe<Array<Maybe<Scalars['String']>>>
} }
export type Role = { export type Role = {
@ -634,7 +592,7 @@ export type Shout = {
export type ShoutInput = { export type ShoutInput = {
authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>> authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
body: Scalars['String'] body?: InputMaybe<Scalars['String']>
community?: InputMaybe<Scalars['Int']> community?: InputMaybe<Scalars['Int']>
mainTopic?: InputMaybe<Scalars['String']> mainTopic?: InputMaybe<Scalars['String']>
slug?: InputMaybe<Scalars['String']> slug?: InputMaybe<Scalars['String']>

View File

@ -1,22 +1,19 @@
import { lazy, Show, Suspense } from 'solid-js'
import { PageLayout } from '../components/_shared/PageLayout' import { PageLayout } from '../components/_shared/PageLayout'
import { Loading } from '../components/_shared/Loading' import { Loading } from '../components/_shared/Loading'
import { useSession } from '../context/session' import { onMount } from 'solid-js'
import { apiClient } from '../utils/apiClient'
const CreateView = lazy(() => import('../components/Views/Create')) import { router } from '../stores/router'
import { redirectPage } from '@nanostores/router'
export const CreatePage = () => { export const CreatePage = () => {
const { isAuthenticated, isSessionLoaded } = useSession() onMount(async () => {
const shout = await apiClient.createArticle({ article: {} })
redirectPage(router, 'edit', { shoutSlug: shout.slug })
})
return ( return (
<PageLayout> <PageLayout>
<Show when={isSessionLoaded()}> <Loading />
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
<Suspense fallback={<Loading />}>
<CreateView />
</Suspense>
</Show>
</Show>
</PageLayout> </PageLayout>
) )
} }

View File

@ -1,4 +1,4 @@
import { ROUTES } from '../stores/router' import { ROUTES } from '../stores/router'
import { getServerRoute } from '../utils/getServerRoute' import { getServerRoute } from '../utils/getServerRoute'
export default getServerRoute(ROUTES.createSettings) export default getServerRoute(ROUTES.drafts)

34
src/pages/drafts.page.tsx Normal file
View File

@ -0,0 +1,34 @@
import { createSignal, For, onMount, Show } from 'solid-js'
import { PageLayout } from '../components/_shared/PageLayout'
import { useSession } from '../context/session'
import { Shout } from '../graphql/types.gen'
import { apiClient } from '../utils/apiClient'
export const DraftsPage = () => {
const { isAuthenticated, isSessionLoaded, user } = useSession()
const [drafts, setDrafts] = createSignal<Shout[]>([])
onMount(async () => {
const loadedDrafts = await apiClient.getShouts({
filters: {
author: user().slug,
visibility: 'owner'
},
limit: 9999
})
setDrafts(loadedDrafts)
})
return (
<PageLayout>
<Show when={isSessionLoaded()}>
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
<For each={drafts()}>{(draft) => <div>{draft.title}</div>}</For>
</Show>
</Show>
</PageLayout>
)
}
export const Page = DraftsPage

View File

@ -0,0 +1,4 @@
import { ROUTES } from '../stores/router'
import { getServerRoute } from '../utils/getServerRoute'
export default getServerRoute(ROUTES.edit)

40
src/pages/edit.page.tsx Normal file
View File

@ -0,0 +1,40 @@
import { createMemo, createSignal, lazy, onMount, Show, Suspense } from 'solid-js'
import { PageLayout } from '../components/_shared/PageLayout'
import { Loading } from '../components/_shared/Loading'
import { useSession } from '../context/session'
import { Shout } from '../graphql/types.gen'
import { useRouter } from '../stores/router'
import { apiClient } from '../utils/apiClient'
const EditView = lazy(() => import('../components/Views/Edit'))
export const EditPage = () => {
const { isAuthenticated, isSessionLoaded } = useSession()
const { page } = useRouter()
const shoutSlug = createMemo(() => (page().params as Record<'shoutSlug', string>).shoutSlug)
const [shout, setShout] = createSignal<Shout>(null)
onMount(async () => {
const loadedShout = await apiClient.getShout(shoutSlug())
setShout(loadedShout)
})
return (
<PageLayout>
<Show when={isSessionLoaded()}>
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
<Show when={shout()}>
<Suspense fallback={<Loading />}>
<EditView shout={shout()} />
</Suspense>
</Show>
</Show>
</Show>
</PageLayout>
)
}
export const Page = EditPage

View File

@ -0,0 +1,4 @@
import { ROUTES } from '../stores/router'
import { getServerRoute } from '../utils/getServerRoute'
export default getServerRoute(ROUTES.editSettings)

View File

@ -16,7 +16,7 @@ import { Beside } from '../components/Feed/Beside'
import Slider from '../components/_shared/Slider' import Slider from '../components/_shared/Slider'
import { Row1 } from '../components/Feed/Row1' import { Row1 } from '../components/Feed/Row1'
import styles from '../styles/Topic.module.scss' import styles from '../styles/Topic.module.scss'
import { ArticleCard } from '../components/Feed/Card' import { ArticleCard } from '../components/Feed/ArticleCard'
import { useLocalize } from '../context/localize' import { useLocalize } from '../context/localize'
export const PRERENDERED_ARTICLES_COUNT = 21 export const PRERENDERED_ARTICLES_COUNT = 21
@ -27,14 +27,16 @@ export const LayoutShoutsPage = (props: PageProps) => {
const getLayout = createMemo<LayoutType>(() => { const getLayout = createMemo<LayoutType>(() => {
const { page: getPage } = useRouter() const { page: getPage } = useRouter()
const page = getPage() const page = getPage()
if (page.route !== 'expo') throw new Error('ts guard') if (page.route !== 'expo') {
throw new Error('ts guard')
}
const { layout } = page.params const { layout } = page.params
return layout as LayoutType return layout as LayoutType
}) })
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const { sortedLayoutShouts, loadLayoutShoutsBy } = useLayoutsStore(getLayout(), props.layoutShouts) const { sortedLayoutShouts, loadLayoutShoutsBy } = useLayoutsStore(getLayout(), props.layoutShouts)
const sortedArticles = createMemo<Shout[]>(() => sortedLayoutShouts().get(getLayout()) || []) const sortedArticles = createMemo<Shout[]>(() => sortedLayoutShouts().get(getLayout()) || [])
const loadMoreLayout = async (kind: LayoutType) => { const loadMoreLayout = async (_kind: LayoutType) => {
saveScrollPosition() saveScrollPosition()
const { hasMore } = await loadLayoutShoutsBy({ const { hasMore } = await loadLayoutShoutsBy({
// filters: { layout: kind }, // filters: { layout: kind },

View File

@ -4,7 +4,7 @@ import { hydrate } from 'solid-js/web'
import type { PageContextBuiltInClient } from 'vite-plugin-ssr/client/router' import type { PageContextBuiltInClient } from 'vite-plugin-ssr/client/router'
import type { PageContext } from './types' import type { PageContext } from './types'
import { MetaProvider } from '@solidjs/meta' import { MetaProvider } from '@solidjs/meta'
import i18next from 'i18next' import { use as useI18next, init as initI18next } from 'i18next'
import HttpApi from 'i18next-http-backend' import HttpApi from 'i18next-http-backend'
let layoutReady = false let layoutReady = false
@ -12,8 +12,8 @@ let layoutReady = false
export const render = async (pageContext: PageContextBuiltInClient & PageContext) => { export const render = async (pageContext: PageContextBuiltInClient & PageContext) => {
const { lng, pageProps } = pageContext const { lng, pageProps } = pageContext
i18next.use(HttpApi) useI18next(HttpApi)
await i18next.init({ await initI18next({
// debug: true, // debug: true,
supportedLngs: ['ru', 'en'], supportedLngs: ['ru', 'en'],
fallbackLng: lng, fallbackLng: lng,

View File

@ -4,7 +4,7 @@ import { App } from '../components/App'
import { initRouter } from '../stores/router' import { initRouter } from '../stores/router'
import type { PageContext } from './types' import type { PageContext } from './types'
import { MetaProvider, renderTags } from '@solidjs/meta' import { MetaProvider, renderTags } from '@solidjs/meta'
import i18next from 'i18next' import i18next, { changeLanguage, init as initI18next } from 'i18next'
import ru from '../../public/locales/ru/translation.json' import ru from '../../public/locales/ru/translation.json'
import en from '../../public/locales/en/translation.json' import en from '../../public/locales/en/translation.json'
import type { Language } from '../context/localize' import type { Language } from '../context/localize'
@ -31,7 +31,8 @@ export const render = async (pageContext: PageContext) => {
const lng = getLng(pageContext) const lng = getLng(pageContext)
if (!i18next.isInitialized) { if (!i18next.isInitialized) {
i18next.init({ // eslint-disable-next-line import/no-named-as-default-member
await initI18next({
// debug: true, // debug: true,
supportedLngs: ['ru', 'en'], supportedLngs: ['ru', 'en'],
fallbackLng: lng, fallbackLng: lng,
@ -43,7 +44,7 @@ export const render = async (pageContext: PageContext) => {
} }
}) })
} else if (i18next.language !== lng) { } else if (i18next.language !== lng) {
await i18next.changeLanguage(lng) await changeLanguage(lng)
} }
initRouter(pageContext.urlParsed.pathname, pageContext.urlParsed.search) initRouter(pageContext.urlParsed.pathname, pageContext.urlParsed.search)

View File

@ -1,33 +0,0 @@
import { createStorageSignal } from '@solid-primitives/storage'
import type { Reaction } from '../graphql/types.gen'
import { createSignal } from 'solid-js'
// TODO: store drafts
// import type { Draft } from '../components/EditorExample/store/context'
interface Collab {
authors: string[] // slugs
invites?: string[]
createdAt: Date
body?: string
title?: string
}
export const drafts = createStorageSignal<{ [key: string]: string }>('drafts', {}) // save drafts on device
export const [collabs, setCollabs] = createSignal<Collab[]>([]) // save collabs in backend or in p2p network
export const [editorReactions, setReactions] = createSignal<Reaction[]>([])
/*
TODO: approvals and proposals derived stores
const approvals = computed(
reactions,
(rdict) => Object.values(rdict)
.filter((r: Reaction) => r.kind === ReactionKind.Accept)
)
const proposals = computed<Reaction[], typeof reactions>(
reactions,
(rdict) => Object.values(rdict)
.filter((r: Reaction) => r.kind === ReactionKind.Propose)
)
*/

View File

@ -8,7 +8,9 @@ export const ROUTES = {
inbox: '/inbox', inbox: '/inbox',
connect: '/connect', connect: '/connect',
create: '/create', create: '/create',
createSettings: '/create/settings', edit: '/edit/:shoutSlug',
editSettings: '/edit/:shoutSlug/settings',
drafts: '/drafts',
topics: '/topics', topics: '/topics',
topic: '/topic/:slug', topic: '/topic/:slug',
authors: '/authors', authors: '/authors',

View File

@ -25,9 +25,9 @@
} }
.comment__details { .comment__details {
display: flex;
@include font-size(1.2rem); @include font-size(1.2rem);
display: flex;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
white-space: nowrap; white-space: nowrap;

View File

@ -76,10 +76,7 @@ main {
scroll-behavior: smooth; scroll-behavior: smooth;
display: flex; display: flex;
position: absolute; position: absolute;
top: 0; inset: 0;
bottom: 0;
left: 0;
right: 0;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
flex-direction: column; flex-direction: column;

Some files were not shown because too many files have changed in this diff Show More