merged
This commit is contained in:
commit
8d0a6269e1
109
.eslintrc.cjs
109
.eslintrc.cjs
|
@ -1,41 +1,41 @@
|
|||
module.exports = {
|
||||
plugins: ['@typescript-eslint', 'import', 'sonarjs', 'unicorn', 'promise', 'solid', 'jest'],
|
||||
plugins: ["@typescript-eslint", "import", "sonarjs", "unicorn", "promise", "solid", "jest"],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'prettier',
|
||||
'plugin:sonarjs/recommended',
|
||||
'plugin:unicorn/recommended',
|
||||
'plugin:promise/recommended',
|
||||
'plugin:solid/recommended',
|
||||
'plugin:jest/recommended'
|
||||
"eslint:recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier",
|
||||
"plugin:sonarjs/recommended",
|
||||
"plugin:unicorn/recommended",
|
||||
"plugin:promise/recommended",
|
||||
"plugin:solid/recommended",
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 2021,
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json'
|
||||
sourceType: "module",
|
||||
project: "./tsconfig.json"
|
||||
},
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended'
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
// Maybe one day...
|
||||
// 'plugin:@typescript-eslint/recommended-requiring-type-checking'
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: '^_'
|
||||
argsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
// TODO: Remove any usage and enable
|
||||
'@typescript-eslint/no-explicit-any': 'off'
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -47,41 +47,60 @@ module.exports = {
|
|||
globals: {},
|
||||
rules: {
|
||||
// Solid
|
||||
'solid/reactivity': 'off', // FIXME
|
||||
'solid/no-innerhtml': 'off',
|
||||
"solid/reactivity": "off", // FIXME
|
||||
"solid/no-innerhtml": "off",
|
||||
|
||||
/** Unicorn **/
|
||||
'unicorn/no-null': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'unicorn/no-array-for-each': 'off',
|
||||
'unicorn/no-array-reduce': 'off',
|
||||
'unicorn/prefer-string-replace-all': 'warn',
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/prefer-module': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
'unicorn/numeric-separators-style': 'off',
|
||||
'unicorn/prefer-node-protocol': 'off',
|
||||
'unicorn/prefer-dom-node-append': 'off', // FIXME
|
||||
'unicorn/prefer-top-level-await': 'warn',
|
||||
'unicorn/consistent-function-scoping': 'warn',
|
||||
'unicorn/no-array-callback-reference': 'warn',
|
||||
'unicorn/no-array-method-this-argument': 'warn',
|
||||
"unicorn/no-null": "off",
|
||||
"unicorn/filename-case": "off",
|
||||
"unicorn/no-array-for-each": "off",
|
||||
"unicorn/no-array-reduce": "off",
|
||||
"unicorn/prefer-string-replace-all": "warn",
|
||||
"unicorn/prevent-abbreviations": "off",
|
||||
"unicorn/prefer-module": "off",
|
||||
"unicorn/import-style": "off",
|
||||
"unicorn/numeric-separators-style": "off",
|
||||
"unicorn/prefer-node-protocol": "off",
|
||||
"unicorn/prefer-dom-node-append": "off", // FIXME
|
||||
"unicorn/prefer-top-level-await": "warn",
|
||||
"unicorn/consistent-function-scoping": "warn",
|
||||
"unicorn/no-array-callback-reference": "warn",
|
||||
"unicorn/no-array-method-this-argument": "warn",
|
||||
"unicorn/no-for-loop": "off",
|
||||
|
||||
'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }],
|
||||
"sonarjs/no-duplicate-string": ["warn", { threshold: 5 }],
|
||||
|
||||
// Promise
|
||||
// 'promise/catch-or-return': 'off', // Should be enabled
|
||||
'promise/always-return': 'off',
|
||||
"promise/always-return": "off",
|
||||
|
||||
eqeqeq: 'error',
|
||||
'no-param-reassign': 'error',
|
||||
'no-nested-ternary': 'error',
|
||||
'no-shadow': 'error'
|
||||
eqeqeq: "error",
|
||||
"no-param-reassign": "error",
|
||||
"no-nested-ternary": "error",
|
||||
"no-shadow": "error",
|
||||
|
||||
"import/order": ["warn", {
|
||||
groups: ["type", "builtin", "external", "internal", "parent", "sibling", "index"],
|
||||
distinctGroup: false,
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: "*.scss",
|
||||
patternOptions: { matchBase: true },
|
||||
group: "index",
|
||||
position: "after"
|
||||
}
|
||||
],
|
||||
"newlines-between": "always",
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
caseInsensitive: true
|
||||
}
|
||||
}]
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
"import/resolver": {
|
||||
typescript: true,
|
||||
node: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"*.{js,mjs,ts,tsx,json,scss,css,html}": "prettier --write",
|
||||
"package.json": "sort-package-json"
|
||||
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write",
|
||||
"package.json": "sort-package-json",
|
||||
"public/locales/**/*.json": "sort-json"
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
"singleQuote": true,
|
||||
"proseWrap": "always",
|
||||
"printWidth": 108,
|
||||
"trailingComma": "none",
|
||||
"plugins": [],
|
||||
"overrides": [
|
||||
{
|
||||
|
|
32
api/edge-ssr.js
Normal file
32
api/edge-ssr.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { renderPage } from 'vike/server'
|
||||
|
||||
export const config = {
|
||||
runtime: 'edge',
|
||||
}
|
||||
export default async function handler(request) {
|
||||
const { url, cookies } = request
|
||||
|
||||
const pageContext = await renderPage({ urlOriginal: url, cookies })
|
||||
|
||||
const { httpResponse, errorWhileRendering, is404 } = pageContext
|
||||
|
||||
if (errorWhileRendering && !is404) {
|
||||
console.error(errorWhileRendering)
|
||||
return new Response('', { status: 500 })
|
||||
}
|
||||
|
||||
if (!httpResponse) {
|
||||
return new Response()
|
||||
}
|
||||
|
||||
const { body, statusCode, headers: headersArray } = httpResponse
|
||||
|
||||
const headers = headersArray.reduce((acc, [name, value]) => {
|
||||
acc[name] = value
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
headers['Cache-Control'] = 's-maxage=1, stale-while-revalidate'
|
||||
|
||||
return new Response(body, { status: statusCode, headers })
|
||||
}
|
|
@ -15,7 +15,7 @@ export default async function handler(req, res) {
|
|||
from: 'Discours Feedback Robot <robot@discours.io>',
|
||||
to: 'welcome@discours.io',
|
||||
subject,
|
||||
text
|
||||
text,
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -13,18 +13,18 @@ export default async (req, res) => {
|
|||
const response = await mg.lists.members.createMember('newsletter@discours.io', {
|
||||
address: email,
|
||||
subscribed: true,
|
||||
upsert: 'yes'
|
||||
upsert: 'yes',
|
||||
})
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Email was added to newsletter list',
|
||||
response: JSON.stringify(response)
|
||||
response: JSON.stringify(response),
|
||||
})
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
message: error.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
26
api/ssr.mjs
26
api/ssr.mjs
|
@ -1,27 +1 @@
|
|||
import { renderPage } from 'vike/server'
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { url, cookies } = req
|
||||
|
||||
const pageContext = await renderPage({ urlOriginal: url, cookies })
|
||||
|
||||
const { httpResponse, errorWhileRendering, is404 } = pageContext
|
||||
|
||||
if (errorWhileRendering && !is404) {
|
||||
console.error(errorWhileRendering)
|
||||
res.statusCode = 500
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
if (!httpResponse) {
|
||||
res.statusCode = 200
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const { body, statusCode, contentType } = httpResponse
|
||||
res.statusCode = statusCode
|
||||
res.setHeader('Content-Type', contentType)
|
||||
res.end(body)
|
||||
}
|
||||
|
|
20520
package-lock.json
generated
20520
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
|
@ -35,11 +35,10 @@
|
|||
"i18next-icu": "2.3.0",
|
||||
"idb": "7.1.1",
|
||||
"intl-messageformat": "10.5.3",
|
||||
"just-throttle": "4.2.0",
|
||||
"mailgun.js": "8.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.21.8",
|
||||
"@babel/core": "7.23.3",
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/typescript": "4.0.1",
|
||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||
|
@ -49,8 +48,8 @@
|
|||
"@graphql-typed-document-node/core": "3.2.0",
|
||||
"@hocuspocus/provider": "2.0.6",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@nanostores/router": "0.8.3",
|
||||
"@nanostores/solid": "0.3.2",
|
||||
"@nanostores/router": "0.11.0",
|
||||
"@nanostores/solid": "0.4.2",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@sentry/browser": "5.30.0",
|
||||
"@solid-primitives/media": "2.2.3",
|
||||
|
@ -58,7 +57,7 @@
|
|||
"@solid-primitives/share": "2.0.4",
|
||||
"@solid-primitives/storage": "1.3.9",
|
||||
"@solid-primitives/upload": "0.0.110",
|
||||
"@solidjs/meta": "0.28.2",
|
||||
"@solidjs/meta": "0.29.1",
|
||||
"@thisbeyond/solid-select": "0.14.0",
|
||||
"@tiptap/core": "2.0.3",
|
||||
"@tiptap/extension-blockquote": "2.0.3",
|
||||
|
@ -89,17 +88,16 @@
|
|||
"@tiptap/extension-text": "2.0.3",
|
||||
"@tiptap/extension-underline": "2.0.3",
|
||||
"@tiptap/extension-youtube": "2.0.3",
|
||||
"@types/js-cookie": "3.0.5",
|
||||
"@types/node": "20.8.10",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||
"@typescript-eslint/parser": "6.9.1",
|
||||
"@types/js-cookie": "3.0.6",
|
||||
"@types/node": "20.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.10.0",
|
||||
"@typescript-eslint/parser": "6.10.0",
|
||||
"@urql/core": "3.2.2",
|
||||
"@urql/devtools": "2.0.3",
|
||||
"babel-preset-solid": "1.8.4",
|
||||
"bootstrap": "5.3.2",
|
||||
"clsx": "2.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"debounce": "1.2.1",
|
||||
"eslint": "8.53.0",
|
||||
"eslint-config-stylelint": "20.0.0",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
|
@ -111,25 +109,19 @@
|
|||
"eslint-plugin-sonarjs": "0.23.0",
|
||||
"eslint-plugin-unicorn": "49.0.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"graphql": "16.6.0",
|
||||
"graphql": "16.8.1",
|
||||
"graphql-tag": "2.12.6",
|
||||
"html-to-json-parser": "1.1.0",
|
||||
"husky": "8.0.3",
|
||||
"hygen": "6.2.11",
|
||||
"i18next-http-backend": "2.2.0",
|
||||
"javascript-time-ago": "2.5.9",
|
||||
"jest": "29.7.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"lint-staged": "15.0.2",
|
||||
"lint-staged": "15.1.0",
|
||||
"loglevel": "1.8.1",
|
||||
"loglevel-plugin-prefix": "0.8.4",
|
||||
"markdown-it": "13.0.1",
|
||||
"markdown-it-container": "3.0.0",
|
||||
"markdown-it-implicit-figures": "0.11.0",
|
||||
"markdown-it-mark": "3.0.1",
|
||||
"markdown-it-replace-link": "1.2.0",
|
||||
"nanostores": "0.7.4",
|
||||
"prettier": "3.0.3",
|
||||
"nanostores": "0.9.5",
|
||||
"prettier": "3.1.0",
|
||||
"prettier-eslint": "16.1.2",
|
||||
"prosemirror-history": "1.3.0",
|
||||
"prosemirror-trailing-node": "2.0.3",
|
||||
|
@ -140,16 +132,18 @@
|
|||
"solid-popper": "0.3.0",
|
||||
"solid-tiptap": "0.6.0",
|
||||
"solid-transition-group": "0.2.3",
|
||||
"sort-json": "2.0.1",
|
||||
"sort-package-json": "2.6.0",
|
||||
"stylelint": "15.11.0",
|
||||
"stylelint-config-standard-scss": "11.1.0",
|
||||
"stylelint-order": "6.0.3",
|
||||
"stylelint-scss": "5.3.0",
|
||||
"stylelint-scss": "5.3.1",
|
||||
"swiper": "9.4.1",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"typescript": "5.2.2",
|
||||
"typograf": "7.1.0",
|
||||
"uniqolor": "1.1.0",
|
||||
"vike": "0.4.144",
|
||||
"vike": "0.4.146",
|
||||
"vite": "4.5.0",
|
||||
"vite-plugin-mkcert": "1.16.0",
|
||||
"vite-plugin-sass-dts": "1.3.11",
|
||||
|
|
|
@ -126,6 +126,7 @@
|
|||
"FAQ": "Tips and suggestions",
|
||||
"Favorite": "Favorites",
|
||||
"Favorite topics": "Favorite topics",
|
||||
"Feed": "Feed",
|
||||
"Feed settings": "Feed settings",
|
||||
"Feedback": "Feedback",
|
||||
"Fill email": "Fill email",
|
||||
|
@ -168,6 +169,7 @@
|
|||
"I know the password": "I know the password",
|
||||
"Image format not supported": "Image format not supported",
|
||||
"In bookmarks, you can save favorite discussions and materials that you want to return to": "In bookmarks, you can save favorite discussions and materials that you want to return to",
|
||||
"Inbox": "Inbox",
|
||||
"Incut": "Incut",
|
||||
"Independant magazine with an open horizontal cooperation about culture, science and society": "Independant magazine with an open horizontal cooperation about culture, science and society",
|
||||
"Insert footnote": "Insert footnote",
|
||||
|
@ -452,6 +454,5 @@
|
|||
"video": "video",
|
||||
"view": "view",
|
||||
"viewsWithCount": "{count} {count, plural, one {view} other {views}}",
|
||||
"yesterday": "yesterday",
|
||||
"To new messages": "To new messages"
|
||||
"yesterday": "yesterday"
|
||||
}
|
||||
|
|
|
@ -130,6 +130,7 @@
|
|||
"FAQ": "Советы и предложения",
|
||||
"Favorite": "Избранное",
|
||||
"Favorite topics": "Избранные темы",
|
||||
"Feed": "Лента",
|
||||
"Feed settings": "Настройки ленты",
|
||||
"Feedback": "Обратная связь",
|
||||
"Fill email": "Введите почту",
|
||||
|
@ -175,6 +176,7 @@
|
|||
"I know the password": "Я знаю пароль!",
|
||||
"Image format not supported": "Тип изображения не поддерживается",
|
||||
"In bookmarks, you can save favorite discussions and materials that you want to return to": "В закладках можно сохранять избранные дискуссии и материалы, к которым хочется вернуться",
|
||||
"Inbox": "Входящие",
|
||||
"Incut": "Подверстка",
|
||||
"Independant magazine with an open horizontal cooperation about culture, science and society": "Независимый журнал с открытой горизонтальной редакцией о культуре, науке и обществе",
|
||||
"Insert footnote": "Вставить сноску",
|
||||
|
@ -476,6 +478,5 @@
|
|||
"video": "видео",
|
||||
"view": "просмотр",
|
||||
"viewsWithCount": "{count} {count, plural, one {просмотр} few {просмотрa} other {просмотров}}",
|
||||
"yesterday": "вчера",
|
||||
"To new messages": "К новым сообщениям"
|
||||
"yesterday": "вчера"
|
||||
}
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
// FIXME: breaks on vercel, research
|
||||
// import 'solid-devtools'
|
||||
import type { PageProps, RootSearchParams } from '../pages/types'
|
||||
|
||||
import { hideModal, MODALS, showModal } from '../stores/ui'
|
||||
import { Meta, MetaProvider } from '@solidjs/meta'
|
||||
import { Component, createEffect, createMemo } from 'solid-js'
|
||||
import { ROUTES, useRouter } from '../stores/router'
|
||||
import { Dynamic } from 'solid-js/web'
|
||||
|
||||
import type { PageProps, RootSearchParams } from '../pages/types'
|
||||
import { HomePage } from '../pages/index.page'
|
||||
import { AllTopicsPage } from '../pages/allTopics.page'
|
||||
import { TopicPage } from '../pages/topic.page'
|
||||
import { AllAuthorsPage } from '../pages/allAuthors.page'
|
||||
import { AuthorPage } from '../pages/author.page'
|
||||
import { FeedPage } from '../pages/feed.page'
|
||||
import { ArticlePage } from '../pages/article.page'
|
||||
import { SearchPage } from '../pages/search.page'
|
||||
import { FourOuFourPage } from '../pages/fourOuFour.page'
|
||||
import { ConfirmProvider } from '../context/confirm'
|
||||
import { EditorProvider } from '../context/editor'
|
||||
import { LocalizeProvider } from '../context/localize'
|
||||
import { NotificationsProvider } from '../context/notifications'
|
||||
import { SessionProvider } from '../context/session'
|
||||
import { SnackbarProvider } from '../context/snackbar'
|
||||
import { DiscussionRulesPage } from '../pages/about/discussionRules.page'
|
||||
import { DogmaPage } from '../pages/about/dogma.page'
|
||||
import { GuidePage } from '../pages/about/guide.page'
|
||||
|
@ -26,21 +20,26 @@ import { PrinciplesPage } from '../pages/about/principles.page'
|
|||
import { ProjectsPage } from '../pages/about/projects.page'
|
||||
import { TermsOfUsePage } from '../pages/about/termsOfUse.page'
|
||||
import { ThanksPage } from '../pages/about/thanks.page'
|
||||
import { CreatePage } from '../pages/create.page'
|
||||
import { EditPage } from '../pages/edit.page'
|
||||
import { AllAuthorsPage } from '../pages/allAuthors.page'
|
||||
import { AllTopicsPage } from '../pages/allTopics.page'
|
||||
import { ArticlePage } from '../pages/article.page'
|
||||
import { AuthorPage } from '../pages/author.page'
|
||||
import { ConnectPage } from '../pages/connect.page'
|
||||
import { InboxPage } from '../pages/inbox.page'
|
||||
import { ExpoPage } from '../pages/expo/expo.page'
|
||||
import { SessionProvider } from '../context/session'
|
||||
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
|
||||
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
|
||||
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
|
||||
import { CreatePage } from '../pages/create.page'
|
||||
import { DraftsPage } from '../pages/drafts.page'
|
||||
import { SnackbarProvider } from '../context/snackbar'
|
||||
import { LocalizeProvider } from '../context/localize'
|
||||
import { ConfirmProvider } from '../context/confirm'
|
||||
import { EditorProvider } from '../context/editor'
|
||||
import { NotificationsProvider } from '../context/notifications'
|
||||
import { EditPage } from '../pages/edit.page'
|
||||
import { ExpoPage } from '../pages/expo/expo.page'
|
||||
import { FeedPage } from '../pages/feed.page'
|
||||
import { FourOuFourPage } from '../pages/fourOuFour.page'
|
||||
import { InboxPage } from '../pages/inbox.page'
|
||||
import { HomePage } from '../pages/index.page'
|
||||
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
|
||||
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
|
||||
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
|
||||
import { SearchPage } from '../pages/search.page'
|
||||
import { TopicPage } from '../pages/topic.page'
|
||||
import { ROUTES, useRouter } from '../stores/router'
|
||||
import { hideModal, MODALS, showModal } from '../stores/ui'
|
||||
|
||||
// TODO: lazy load
|
||||
// const SomePage = lazy(() => import('./Pages/SomePage'))
|
||||
|
@ -82,7 +81,7 @@ const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
|
|||
profileSettings: ProfileSettingsPage,
|
||||
profileSecurity: ProfileSecurityPage,
|
||||
profileSubscriptions: ProfileSubscriptionsPage,
|
||||
fourOuFour: FourOuFourPage
|
||||
fourOuFour: FourOuFourPage,
|
||||
}
|
||||
|
||||
export const App = (props: PageProps) => {
|
||||
|
@ -110,18 +109,21 @@ export const App = (props: PageProps) => {
|
|||
})
|
||||
|
||||
return (
|
||||
<LocalizeProvider>
|
||||
<SnackbarProvider>
|
||||
<ConfirmProvider>
|
||||
<SessionProvider>
|
||||
<NotificationsProvider>
|
||||
<EditorProvider>
|
||||
<Dynamic component={pageComponent()} {...props} />
|
||||
</EditorProvider>
|
||||
</NotificationsProvider>
|
||||
</SessionProvider>
|
||||
</ConfirmProvider>
|
||||
</SnackbarProvider>
|
||||
</LocalizeProvider>
|
||||
<MetaProvider>
|
||||
<Meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<LocalizeProvider>
|
||||
<SnackbarProvider>
|
||||
<ConfirmProvider>
|
||||
<SessionProvider>
|
||||
<NotificationsProvider>
|
||||
<EditorProvider>
|
||||
<Dynamic component={pageComponent()} {...props} />
|
||||
</EditorProvider>
|
||||
</NotificationsProvider>
|
||||
</SessionProvider>
|
||||
</ConfirmProvider>
|
||||
</SnackbarProvider>
|
||||
</LocalizeProvider>
|
||||
</MetaProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -23,22 +23,16 @@ img {
|
|||
}
|
||||
}
|
||||
|
||||
.shoutCover {
|
||||
background-size: cover;
|
||||
height: 0;
|
||||
padding-bottom: 56.2%;
|
||||
.articleContent {
|
||||
img {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
}
|
||||
|
||||
.shoutBody {
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.6;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
blockquote[data-type='punchline'] {
|
||||
clear: both;
|
||||
|
@ -75,6 +69,7 @@ img {
|
|||
&[data-float='left'],
|
||||
&[data-float='right'] {
|
||||
@include font-size(2.2rem);
|
||||
|
||||
line-height: 1.4;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
|
@ -423,7 +418,7 @@ img {
|
|||
}
|
||||
|
||||
.shoutStatsItemViews {
|
||||
color: rgb(0 0 0 / 0.4);
|
||||
color: rgb(0 0 0 / 40%);
|
||||
cursor: default;
|
||||
font-weight: normal;
|
||||
margin-left: auto;
|
||||
|
@ -466,7 +461,8 @@ img {
|
|||
.shoutStatsItemAdditionalDataItem {
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
//margin-left: 2rem;
|
||||
|
||||
// margin-left: 2rem;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
cursor: default;
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './AudioHeader.module.scss'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import { Topic } from '../../../graphql/types.gen'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Image } from '../../_shared/Image'
|
||||
import { CardTopic } from '../../Feed/CardTopic'
|
||||
import { Image } from '../../_shared/Image'
|
||||
|
||||
import styles from './AudioHeader.module.scss'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
cover?: string
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { createEffect, createMemo, createSignal, on, onMount, Show } from 'solid-js'
|
||||
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
|
||||
import { PlayerHeader } from './PlayerHeader'
|
||||
import { PlayerPlaylist } from './PlayerPlaylist'
|
||||
|
||||
import styles from './AudioPlayer.module.scss'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
|
||||
type Props = {
|
||||
media: MediaItem[]
|
||||
|
@ -35,8 +38,8 @@ export const AudioPlayer = (props: Props) => {
|
|||
() => {
|
||||
setCurrentTrackDuration(0)
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const handlePlayMedia = async (trackIndex: number) => {
|
||||
|
@ -131,7 +134,7 @@ export const AudioPlayer = (props: Props) => {
|
|||
<div
|
||||
class={styles.progressFilled}
|
||||
style={{
|
||||
width: `${(currentTime() / currentTrackDuration()) * 100 || 0}%`
|
||||
width: `${(currentTime() / currentTrackDuration()) * 100 || 0}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { createSignal, Show } from 'solid-js'
|
||||
import { clsx } from 'clsx'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
|
||||
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import styles from './AudioPlayer.module.scss'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import styles from './AudioPlayer.module.scss'
|
||||
|
||||
type Props = {
|
||||
onPlayMedia: () => void
|
||||
|
@ -18,7 +18,7 @@ type Props = {
|
|||
|
||||
export const PlayerHeader = (props: Props) => {
|
||||
const volumeContainerRef: { current: HTMLDivElement } = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
|
||||
const [isVolumeBarOpened, setIsVolumeBarOpened] = createSignal(false)
|
||||
|
@ -30,7 +30,7 @@ export const PlayerHeader = (props: Props) => {
|
|||
useOutsideClickHandler({
|
||||
containerRef: volumeContainerRef,
|
||||
predicate: () => isVolumeBarOpened(),
|
||||
handler: () => toggleVolumeBar()
|
||||
handler: () => toggleVolumeBar(),
|
||||
})
|
||||
|
||||
return (
|
||||
|
@ -42,7 +42,7 @@ export const PlayerHeader = (props: Props) => {
|
|||
onClick={props.onPlayMedia}
|
||||
class={clsx(
|
||||
styles.playButton,
|
||||
props.isPlaying ? styles.playButtonInvertPause : styles.playButtonInvertPlay
|
||||
props.isPlaying ? styles.playButtonInvertPause : styles.playButtonInvertPlay,
|
||||
)}
|
||||
aria-label="Play"
|
||||
data-playing="false"
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { createSignal, For, Show } from 'solid-js'
|
||||
import { SharePopup, getShareUrl } from '../SharePopup'
|
||||
import { getDescription } from '../../../utils/meta'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import styles from './AudioPlayer.module.scss'
|
||||
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
|
||||
import MD from '../MD'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
import { getDescription } from '../../../utils/meta'
|
||||
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import SimplifiedEditor from '../../Editor/SimplifiedEditor'
|
||||
import { SharePopup, getShareUrl } from '../SharePopup'
|
||||
|
||||
import styles from './AudioPlayer.module.scss'
|
||||
|
||||
type Props = {
|
||||
media: MediaItem[]
|
||||
|
@ -146,12 +147,12 @@ export const PlayerPlaylist = (props: Props) => {
|
|||
<div class={styles.descriptionBlock}>
|
||||
<Show when={mi.body}>
|
||||
<div class={styles.description}>
|
||||
<MD body={mi.body} />
|
||||
<div innerHTML={mi.body} />
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={mi.lyrics}>
|
||||
<div class={styles.lyrics}>
|
||||
<MD body={mi.lyrics} />
|
||||
<div innerHTML={mi.lyrics} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
transition: background-color 0.3s;
|
||||
position: relative;
|
||||
list-style: none;
|
||||
background: rgb(0 0 0 / 0.1);
|
||||
background: rgb(0 0 0 / 10%);
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
padding-right: 0;
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
import { Show, createMemo, createSignal, For, lazy, Suspense } from 'solid-js'
|
||||
import { clsx } from 'clsx'
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import { clsx } from 'clsx'
|
||||
import { Show, createMemo, createSignal, For, lazy, Suspense } from 'solid-js'
|
||||
|
||||
import MD from '../MD'
|
||||
import { Userpic } from '../../Author/Userpic'
|
||||
import { CommentRatingControl } from '../CommentRatingControl'
|
||||
import { CommentDate } from '../CommentDate'
|
||||
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import { useSession } from '../../../context/session'
|
||||
import { useConfirm } from '../../../context/confirm'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useReactions } from '../../../context/reactions'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { useSnackbar } from '../../../context/snackbar'
|
||||
import { useConfirm } from '../../../context/confirm'
|
||||
|
||||
import { Author, Reaction, ReactionKind } from '../../../graphql/types.gen'
|
||||
import { router } from '../../../stores/router'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
|
||||
import { AuthorLink } from '../../Author/AhtorLink'
|
||||
import { Userpic } from '../../Author/Userpic'
|
||||
import { CommentDate } from '../CommentDate'
|
||||
import { CommentRatingControl } from '../CommentRatingControl'
|
||||
|
||||
import styles from './Comment.module.scss'
|
||||
import { AuthorLink } from '../../Author/AhtorLink'
|
||||
|
@ -44,15 +42,15 @@ export const Comment = (props: Props) => {
|
|||
const { session } = useSession()
|
||||
|
||||
const {
|
||||
actions: { createReaction, deleteReaction, updateReaction }
|
||||
actions: { createReaction, deleteReaction, updateReaction },
|
||||
} = useReactions()
|
||||
|
||||
const {
|
||||
actions: { showConfirm }
|
||||
actions: { showConfirm },
|
||||
} = useConfirm()
|
||||
|
||||
const {
|
||||
actions: { showSnackbar }
|
||||
actions: { showSnackbar },
|
||||
} = useSnackbar()
|
||||
|
||||
const isCommentAuthor = createMemo(() => props.comment.createdBy?.slug === session()?.user?.slug)
|
||||
|
@ -66,7 +64,7 @@ export const Comment = (props: Props) => {
|
|||
confirmBody: t('Are you sure you want to delete this comment?'),
|
||||
confirmButtonLabel: t('Delete'),
|
||||
confirmButtonVariant: 'danger',
|
||||
declineButtonVariant: 'primary'
|
||||
declineButtonVariant: 'primary',
|
||||
})
|
||||
|
||||
if (isConfirmed) {
|
||||
|
@ -87,7 +85,7 @@ export const Comment = (props: Props) => {
|
|||
kind: ReactionKind.Comment,
|
||||
replyTo: props.comment.id,
|
||||
body: value,
|
||||
shout: props.comment.shout.id
|
||||
shout: props.comment.shout.id,
|
||||
})
|
||||
setClearEditor(true)
|
||||
setIsReplyVisible(false)
|
||||
|
@ -108,7 +106,7 @@ export const Comment = (props: Props) => {
|
|||
await updateReaction(props.comment.id, {
|
||||
kind: ReactionKind.Comment,
|
||||
body: value,
|
||||
shout: props.comment.shout.id
|
||||
shout: props.comment.shout.id,
|
||||
})
|
||||
setEditMode(false)
|
||||
setLoading(false)
|
||||
|
@ -123,7 +121,7 @@ export const Comment = (props: Props) => {
|
|||
<li
|
||||
id={`comment_${comment().id}`}
|
||||
class={clsx(styles.comment, props.class, {
|
||||
[styles.isNew]: !isCommentAuthor() && createdAt > props.lastSeen
|
||||
[styles.isNew]: !isCommentAuthor() && createdAt > props.lastSeen,
|
||||
})}
|
||||
>
|
||||
<Show when={!!body()}>
|
||||
|
@ -136,7 +134,7 @@ export const Comment = (props: Props) => {
|
|||
name={comment().createdBy.name}
|
||||
userpic={comment().createdBy.userpic}
|
||||
class={clsx({
|
||||
[styles.compactUserpic]: props.compact
|
||||
[styles.compactUserpic]: props.compact,
|
||||
})}
|
||||
/>
|
||||
<small>
|
||||
|
@ -171,7 +169,7 @@ export const Comment = (props: Props) => {
|
|||
</div>
|
||||
</Show>
|
||||
<div class={styles.commentBody}>
|
||||
<Show when={editMode()} fallback={<MD body={body()} />}>
|
||||
<Show when={editMode()} fallback={<div innerHTML={body()} />}>
|
||||
<Suspense fallback={<p>{t('Loading')}</p>}>
|
||||
<SimplifiedEditor
|
||||
initialContent={comment().body}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { Show } from 'solid-js'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import type { Reaction } from '../../../graphql/types.gen'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import styles from './CommentDate.module.scss'
|
||||
|
||||
type Props = {
|
||||
|
@ -27,7 +30,7 @@ export const CommentDate = (props: Props) => {
|
|||
<div
|
||||
class={clsx(styles.commentDates, {
|
||||
[styles.commentDatesLastInRow]: props.isLastInRow,
|
||||
[styles.showOnHover]: props.showOnHover
|
||||
[styles.showOnHover]: props.showOnHover,
|
||||
})}
|
||||
>
|
||||
<time class={styles.date}>{formattedDate(props.comment.createdAt)}</time>
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './CommentRatingControl.module.scss'
|
||||
import type { Reaction } from '../../graphql/types.gen'
|
||||
import { ReactionKind } from '../../graphql/types.gen'
|
||||
import { useSession } from '../../context/session'
|
||||
import { useReactions } from '../../context/reactions'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { createMemo } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useReactions } from '../../context/reactions'
|
||||
import { useSession } from '../../context/session'
|
||||
import { useSnackbar } from '../../context/snackbar'
|
||||
import { ReactionKind } from '../../graphql/types.gen'
|
||||
import { loadShout } from '../../stores/zine/articles'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useSnackbar } from '../../context/snackbar'
|
||||
import { VotersList } from '../_shared/VotersList'
|
||||
|
||||
import styles from './CommentRatingControl.module.scss'
|
||||
|
||||
type Props = {
|
||||
comment: Reaction
|
||||
}
|
||||
|
@ -19,11 +22,11 @@ export const CommentRatingControl = (props: Props) => {
|
|||
const { t } = useLocalize()
|
||||
const { user } = useSession()
|
||||
const {
|
||||
actions: { showSnackbar }
|
||||
actions: { showSnackbar },
|
||||
} = useSnackbar()
|
||||
const {
|
||||
reactionEntities,
|
||||
actions: { createReaction, deleteReaction, loadReactionsBy }
|
||||
actions: { createReaction, deleteReaction, loadReactionsBy },
|
||||
} = useReactions()
|
||||
|
||||
const checkReaction = (reactionKind: ReactionKind) =>
|
||||
|
@ -32,7 +35,7 @@ export const CommentRatingControl = (props: Props) => {
|
|||
r.kind === reactionKind &&
|
||||
r.createdBy.slug === user()?.slug &&
|
||||
r.shout.id === props.comment.shout.id &&
|
||||
r.replyTo === props.comment.id
|
||||
r.replyTo === props.comment.id,
|
||||
)
|
||||
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
|
||||
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
|
||||
|
@ -43,8 +46,8 @@ export const CommentRatingControl = (props: Props) => {
|
|||
(r) =>
|
||||
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
|
||||
r.shout.id === props.comment.shout.id &&
|
||||
r.replyTo === props.comment.id
|
||||
)
|
||||
r.replyTo === props.comment.id,
|
||||
),
|
||||
)
|
||||
|
||||
const deleteCommentReaction = async (reactionKind: ReactionKind) => {
|
||||
|
@ -53,7 +56,7 @@ export const CommentRatingControl = (props: Props) => {
|
|||
r.kind === reactionKind &&
|
||||
r.createdBy.slug === user()?.slug &&
|
||||
r.shout.id === props.comment.shout.id &&
|
||||
r.replyTo === props.comment.id
|
||||
r.replyTo === props.comment.id,
|
||||
)
|
||||
return deleteReaction(reactionToDelete.id)
|
||||
}
|
||||
|
@ -68,7 +71,7 @@ export const CommentRatingControl = (props: Props) => {
|
|||
await createReaction({
|
||||
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
|
||||
shout: props.comment.shout.id,
|
||||
replyTo: props.comment.id
|
||||
replyTo: props.comment.id,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
|
@ -77,7 +80,7 @@ export const CommentRatingControl = (props: Props) => {
|
|||
|
||||
await loadShout(props.comment.shout.slug)
|
||||
await loadReactionsBy({
|
||||
by: { shout: props.comment.shout.slug }
|
||||
by: { shout: props.comment.shout.slug },
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -88,7 +91,7 @@ export const CommentRatingControl = (props: Props) => {
|
|||
disabled={!canVote() || !user()}
|
||||
onClick={() => handleRatingChange(true)}
|
||||
class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, {
|
||||
[styles.voted]: isUpvoted()
|
||||
[styles.voted]: isUpvoted(),
|
||||
})}
|
||||
/>
|
||||
<Popup
|
||||
|
@ -96,7 +99,7 @@ export const CommentRatingControl = (props: Props) => {
|
|||
<div
|
||||
class={clsx(styles.commentRatingValue, {
|
||||
[styles.commentRatingPositive]: props.comment.stat.rating > 0,
|
||||
[styles.commentRatingNegative]: props.comment.stat.rating < 0
|
||||
[styles.commentRatingNegative]: props.comment.stat.rating < 0,
|
||||
})}
|
||||
>
|
||||
{props.comment.stat.rating || 0}
|
||||
|
@ -114,7 +117,7 @@ export const CommentRatingControl = (props: Props) => {
|
|||
disabled={!canVote() || !user()}
|
||||
onClick={() => handleRatingChange(false)}
|
||||
class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, {
|
||||
[styles.voted]: isDownvoted()
|
||||
[styles.voted]: isDownvoted(),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import { Show, createMemo, createSignal, onMount, For } from 'solid-js'
|
||||
import { Comment } from './Comment'
|
||||
import styles from './Article.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { Author, Reaction, ReactionKind } from '../../graphql/types.gen'
|
||||
import { useSession } from '../../context/session'
|
||||
import { Button } from '../_shared/Button'
|
||||
import { useReactions } from '../../context/reactions'
|
||||
import { byCreated } from '../../utils/sortby'
|
||||
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
||||
import { Show, createMemo, createSignal, onMount, For } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useReactions } from '../../context/reactions'
|
||||
import { useSession } from '../../context/session'
|
||||
import { Author, Reaction, ReactionKind } from '../../graphql/types.gen'
|
||||
import { byCreated } from '../../utils/sortby'
|
||||
import { Button } from '../_shared/Button'
|
||||
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
||||
import SimplifiedEditor from '../Editor/SimplifiedEditor'
|
||||
|
||||
import { Comment } from './Comment'
|
||||
|
||||
import styles from './Article.module.scss'
|
||||
|
||||
type CommentsOrder = 'createdAt' | 'rating' | 'newOnly'
|
||||
|
||||
const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
|
||||
|
@ -48,11 +51,11 @@ export const CommentsTree = (props: Props) => {
|
|||
|
||||
const {
|
||||
reactionEntities,
|
||||
actions: { createReaction }
|
||||
actions: { createReaction },
|
||||
} = useReactions()
|
||||
|
||||
const comments = createMemo(() =>
|
||||
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
|
||||
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT'),
|
||||
)
|
||||
|
||||
const sortedComments = createMemo(() => {
|
||||
|
@ -96,7 +99,7 @@ export const CommentsTree = (props: Props) => {
|
|||
await createReaction({
|
||||
kind: ReactionKind.Comment,
|
||||
body: value,
|
||||
shout: props.shoutId
|
||||
shout: props.shoutId,
|
||||
})
|
||||
setClearEditor(true)
|
||||
} catch (error) {
|
||||
|
@ -154,7 +157,7 @@ export const CommentsTree = (props: Props) => {
|
|||
<Comment
|
||||
sortedComments={sortedComments()}
|
||||
isArticleAuthor={Boolean(
|
||||
props.articleAuthors.some((a) => a.slug === reaction.createdBy.slug)
|
||||
props.articleAuthors.some((a) => a.slug === reaction.createdBy.slug),
|
||||
)}
|
||||
comment={reaction}
|
||||
clickedReply={(id) => setClickedReplyId(id)}
|
||||
|
|
|
@ -1,33 +1,36 @@
|
|||
import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup } from 'solid-js'
|
||||
import { Title } from '@solidjs/meta'
|
||||
import { clsx } from 'clsx'
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import MD from './MD'
|
||||
import type { Author, Shout } from '../../graphql/types.gen'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
import { clsx } from 'clsx'
|
||||
import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useReactions } from '../../context/reactions'
|
||||
import { useSession } from '../../context/session'
|
||||
import { MediaItem } from '../../pages/types'
|
||||
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
|
||||
import { getImageUrl } from '../../utils/getImageUrl'
|
||||
import { getDescription } from '../../utils/meta'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { Image } from '../_shared/Image'
|
||||
import { Lightbox } from '../_shared/Lightbox'
|
||||
import { Popover } from '../_shared/Popover'
|
||||
import { ImageSwiper } from '../_shared/SolidSwiper'
|
||||
import { VideoPlayer } from '../_shared/VideoPlayer'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
import { CardTopic } from '../Feed/CardTopic'
|
||||
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
|
||||
import { TableOfContents } from '../TableOfContents'
|
||||
|
||||
import { AudioHeader } from './AudioHeader'
|
||||
import { AudioPlayer } from './AudioPlayer'
|
||||
import { CommentsTree } from './CommentsTree'
|
||||
import { getShareUrl, SharePopup } from './SharePopup'
|
||||
import { ShoutRatingControl } from './ShoutRatingControl'
|
||||
import { CommentsTree } from './CommentsTree'
|
||||
import stylesHeader from '../Nav/Header/Header.module.scss'
|
||||
import { AudioHeader } from './AudioHeader'
|
||||
import { Popover } from '../_shared/Popover'
|
||||
import { VideoPlayer } from '../_shared/VideoPlayer'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { ImageSwiper } from '../_shared/SolidSwiper'
|
||||
|
||||
import styles from './Article.module.scss'
|
||||
import { CardTopic } from '../Feed/CardTopic'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
import { getImageUrl } from '../../utils/getImageUrl'
|
||||
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
|
||||
import { Lightbox } from '../_shared/Lightbox'
|
||||
import stylesHeader from '../Nav/Header/Header.module.scss'
|
||||
|
||||
type Props = {
|
||||
article: Shout
|
||||
|
@ -45,7 +48,7 @@ const scrollTo = (el: HTMLElement) => {
|
|||
window.scrollTo({
|
||||
top: top + window.scrollY - DEFAULT_HEADER_OFFSET,
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -56,7 +59,7 @@ export const FullArticle = (props: Props) => {
|
|||
const {
|
||||
user,
|
||||
isAuthenticated,
|
||||
actions: { requireAuthentication }
|
||||
actions: { requireAuthentication },
|
||||
} = useSession()
|
||||
|
||||
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
||||
|
@ -66,7 +69,7 @@ export const FullArticle = (props: Props) => {
|
|||
const mainTopic = createMemo(
|
||||
() =>
|
||||
props.article.topics?.find((topic) => topic?.slug === props.article.mainTopic) ||
|
||||
props.article.topics[0]
|
||||
props.article.topics[0],
|
||||
)
|
||||
|
||||
const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
|
||||
|
@ -91,8 +94,13 @@ export const FullArticle = (props: Props) => {
|
|||
}
|
||||
return props.article.body
|
||||
})
|
||||
const media = createMemo(() => {
|
||||
return JSON.parse(props.article.media || '[]')
|
||||
|
||||
const media = createMemo<MediaItem[]>(() => {
|
||||
try {
|
||||
return JSON.parse(props.article.media)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
const commentsRef: {
|
||||
|
@ -115,7 +123,7 @@ export const FullArticle = (props: Props) => {
|
|||
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
|
||||
scrollToComments()
|
||||
changeSearchParam({
|
||||
scrollTo: null
|
||||
scrollTo: null,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -123,7 +131,7 @@ export const FullArticle = (props: Props) => {
|
|||
createEffect(() => {
|
||||
if (searchParams().commentId && isReactionsLoaded()) {
|
||||
const commentElement = document.querySelector<HTMLElement>(
|
||||
`[id='comment_${searchParams().commentId}']`
|
||||
`[id='comment_${searchParams().commentId}']`,
|
||||
)
|
||||
|
||||
changeSearchParam({ commentId: null })
|
||||
|
@ -135,17 +143,21 @@ export const FullArticle = (props: Props) => {
|
|||
})
|
||||
|
||||
const {
|
||||
actions: { loadReactionsBy }
|
||||
actions: { loadReactionsBy },
|
||||
} = useReactions()
|
||||
|
||||
onMount(async () => {
|
||||
await loadReactionsBy({
|
||||
by: { shout: props.article.slug }
|
||||
by: { shout: props.article.slug },
|
||||
})
|
||||
|
||||
setIsReactionsLoaded(true)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
document.title = props.article.title
|
||||
})
|
||||
|
||||
const clickHandlers = []
|
||||
const documentClickHandlers = []
|
||||
|
||||
|
@ -155,7 +167,7 @@ export const FullArticle = (props: Props) => {
|
|||
}
|
||||
|
||||
const tooltipElements: NodeListOf<HTMLElement> = document.querySelectorAll(
|
||||
'[data-toggle="tooltip"], footnote'
|
||||
'[data-toggle="tooltip"], footnote',
|
||||
)
|
||||
if (!tooltipElements) {
|
||||
return
|
||||
|
@ -180,19 +192,19 @@ export const FullArticle = (props: Props) => {
|
|||
modifiers: [
|
||||
{
|
||||
name: 'eventListeners',
|
||||
options: { scroll: false }
|
||||
options: { scroll: false },
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8]
|
||||
}
|
||||
offset: [0, 8],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'flip',
|
||||
options: { fallbackPlacements: ['top'] }
|
||||
}
|
||||
]
|
||||
options: { fallbackPlacements: ['top'] },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
tooltip.style.visibility = 'hidden'
|
||||
|
@ -249,10 +261,12 @@ export const FullArticle = (props: Props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Title>{props.article.title}</Title>
|
||||
<div class="wide-container">
|
||||
<div class="row position-relative">
|
||||
<article class="col-md-16 col-lg-14 col-xl-12 offset-md-5">
|
||||
<article
|
||||
class={clsx('col-md-16 col-lg-14 col-xl-12 offset-md-5', styles.articleContent)}
|
||||
onClick={handleArticleBodyClick}
|
||||
>
|
||||
{/*TODO: Check styles.shoutTopic*/}
|
||||
<Show when={props.article.layout !== 'music'}>
|
||||
<div class={styles.shoutHeader}>
|
||||
|
@ -282,12 +296,7 @@ export const FullArticle = (props: Props) => {
|
|||
props.article.layout !== 'image'
|
||||
}
|
||||
>
|
||||
<div
|
||||
class={styles.shoutCover}
|
||||
style={{
|
||||
'background-image': `url('${getImageUrl(props.article.cover, { width: 1600 })}')`
|
||||
}}
|
||||
/>
|
||||
<Image width={1600} alt={props.article.title} src={props.article.cover} />
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
@ -319,7 +328,7 @@ export const FullArticle = (props: Props) => {
|
|||
description={m.body}
|
||||
/>
|
||||
<Show when={m?.body}>
|
||||
<MD body={m.body} />
|
||||
<div innerHTML={m.body} />
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
|
@ -328,11 +337,7 @@ export const FullArticle = (props: Props) => {
|
|||
</Show>
|
||||
|
||||
<Show when={body()}>
|
||||
<div id="shoutBody" class={styles.shoutBody} onClick={handleArticleBodyClick}>
|
||||
<Show when={!body().startsWith('<')} fallback={<div innerHTML={body()} />}>
|
||||
<MD body={body()} />
|
||||
</Show>
|
||||
</div>
|
||||
<div id="shoutBody" class={styles.shoutBody} innerHTML={body()} />
|
||||
</Show>
|
||||
</article>
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import MD from 'markdown-it'
|
||||
import mdfig from 'markdown-it-implicit-figures'
|
||||
import mdmark from 'markdown-it-mark'
|
||||
import mdcustom from 'markdown-it-container'
|
||||
import mdlinks from 'markdown-it-replace-link'
|
||||
import { createMemo } from 'solid-js'
|
||||
|
||||
const mit = MD({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
})
|
||||
mit.use(mdmark)
|
||||
mit.use(mdcustom)
|
||||
mit.use(mdfig, {
|
||||
dataType: false, // <figure data-type="image">
|
||||
figcaption: true // <figcaption>alternative text</figcaption>
|
||||
})
|
||||
mit.use(mdlinks)
|
||||
|
||||
export default (props: { body: string }) => {
|
||||
const body = createMemo(() => (props.body.startsWith('<') ? props.body : mit.render(props.body)))
|
||||
return <div innerHTML={body()} />
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
import { Icon } from '../_shared/Icon'
|
||||
import type { PopupProps } from '../_shared/Popup'
|
||||
|
||||
import { createSocialShare, TWITTER, VK, FACEBOOK, TELEGRAM } from '@solid-primitives/share'
|
||||
import styles from '../_shared/Popup/Popup.module.scss'
|
||||
import type { PopupProps } from '../_shared/Popup'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { createEffect, createSignal } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useSnackbar } from '../../context/snackbar'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
|
||||
import styles from '../_shared/Popup/Popup.module.scss'
|
||||
|
||||
type SharePopupProps = {
|
||||
title: string
|
||||
|
@ -26,7 +28,7 @@ export const SharePopup = (props: SharePopupProps) => {
|
|||
const { t } = useLocalize()
|
||||
const [isVisible, setIsVisible] = createSignal(false)
|
||||
const {
|
||||
actions: { showSnackbar }
|
||||
actions: { showSnackbar },
|
||||
} = useSnackbar()
|
||||
|
||||
createEffect(() => {
|
||||
|
@ -38,7 +40,7 @@ export const SharePopup = (props: SharePopupProps) => {
|
|||
const [share] = createSocialShare(() => ({
|
||||
title: props.title,
|
||||
url: props.shareUrl,
|
||||
description: props.description
|
||||
description: props.description,
|
||||
}))
|
||||
|
||||
const copyLink = async () => {
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { createMemo, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useReactions } from '../../context/reactions'
|
||||
import { useSession } from '../../context/session'
|
||||
import { ReactionKind, Shout } from '../../graphql/types.gen'
|
||||
import { loadShout } from '../../stores/zine/articles'
|
||||
import { useSession } from '../../context/session'
|
||||
import { useReactions } from '../../context/reactions'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
import { VotersList } from '../_shared/VotersList'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
import styles from './ShoutRatingControl.module.scss'
|
||||
|
||||
interface ShoutRatingControlProps {
|
||||
|
@ -19,12 +21,12 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
|||
const { t } = useLocalize()
|
||||
const {
|
||||
user,
|
||||
actions: { requireAuthentication }
|
||||
actions: { requireAuthentication },
|
||||
} = useSession()
|
||||
|
||||
const {
|
||||
reactionEntities,
|
||||
actions: { createReaction, deleteReaction, loadReactionsBy }
|
||||
actions: { createReaction, deleteReaction, loadReactionsBy },
|
||||
} = useReactions()
|
||||
|
||||
const checkReaction = (reactionKind: ReactionKind) =>
|
||||
|
@ -33,7 +35,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
|||
r.kind === reactionKind &&
|
||||
r.createdBy.slug === user()?.slug &&
|
||||
r.shout.id === props.shout.id &&
|
||||
!r.replyTo
|
||||
!r.replyTo,
|
||||
)
|
||||
|
||||
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
|
||||
|
@ -45,8 +47,8 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
|||
(r) =>
|
||||
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
|
||||
r.shout.id === props.shout.id &&
|
||||
!r.replyTo
|
||||
)
|
||||
!r.replyTo,
|
||||
),
|
||||
)
|
||||
|
||||
const deleteShoutReaction = async (reactionKind: ReactionKind) => {
|
||||
|
@ -55,7 +57,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
|||
r.kind === reactionKind &&
|
||||
r.createdBy.slug === user()?.slug &&
|
||||
r.shout.id === props.shout.id &&
|
||||
!r.replyTo
|
||||
!r.replyTo,
|
||||
)
|
||||
return deleteReaction(reactionToDelete.id)
|
||||
}
|
||||
|
@ -69,13 +71,13 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
|||
} else {
|
||||
await createReaction({
|
||||
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
|
||||
shout: props.shout.id
|
||||
shout: props.shout.id,
|
||||
})
|
||||
}
|
||||
|
||||
loadShout(props.shout.slug)
|
||||
loadReactionsBy({
|
||||
by: { shout: props.shout.slug }
|
||||
by: { shout: props.shout.slug },
|
||||
})
|
||||
}, 'vote')
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { createEffect, JSX, Show } from 'solid-js'
|
||||
|
||||
import { useSession } from '../../context/session'
|
||||
import { hideModal } from '../../stores/ui'
|
||||
import { useRouter } from '../../stores/router'
|
||||
import { RootSearchParams } from '../../pages/types'
|
||||
import { useRouter } from '../../stores/router'
|
||||
import { hideModal } from '../../stores/ui'
|
||||
import { AuthModalSearchParams } from '../Nav/AuthModal/types'
|
||||
|
||||
type Props = {
|
||||
|
@ -25,9 +26,9 @@ export const AuthGuard = (props: Props) => {
|
|||
changeSearchParam(
|
||||
{
|
||||
source: 'authguard',
|
||||
modal: 'auth'
|
||||
modal: 'auth',
|
||||
},
|
||||
true
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './AhtorLink.module.scss'
|
||||
|
||||
import { Author } from '../../../graphql/types.gen'
|
||||
import { Userpic } from '../Userpic'
|
||||
|
||||
import styles from './AhtorLink.module.scss'
|
||||
|
||||
type Props = {
|
||||
author: Author
|
||||
size?: 'XS' | 'M' | 'L'
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import { openPage } from '@nanostores/router'
|
||||
import { clsx } from 'clsx'
|
||||
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { Author, FollowingEntity } from '../../../graphql/types.gen'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { follow, unfollow } from '../../../stores/zine/common'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { CheckButton } from '../../_shared/CheckButton'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Userpic } from '../Userpic'
|
||||
|
||||
import styles from './AuthorBadge.module.scss'
|
||||
import stylesButton from '../../_shared/Button/Button.module.scss'
|
||||
import { Userpic } from '../Userpic'
|
||||
import { Author, FollowingEntity } from '../../../graphql/types.gen'
|
||||
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { follow, unfollow } from '../../../stores/zine/common'
|
||||
import { CheckButton } from '../../_shared/CheckButton'
|
||||
import { openPage } from '@nanostores/router'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
type Props = {
|
||||
author: Author
|
||||
|
@ -25,12 +27,12 @@ export const AuthorBadge = (props: Props) => {
|
|||
const {
|
||||
session,
|
||||
subscriptions,
|
||||
actions: { loadSubscriptions, requireAuthentication }
|
||||
actions: { loadSubscriptions, requireAuthentication },
|
||||
} = useSession()
|
||||
const { changeSearchParam } = useRouter()
|
||||
const { t, formatDate } = useLocalize()
|
||||
const subscribed = createMemo(() =>
|
||||
subscriptions().authors.some((author) => author.slug === props.author.slug)
|
||||
subscriptions().authors.some((author) => author.slug === props.author.slug),
|
||||
)
|
||||
|
||||
const subscribe = async (really = true) => {
|
||||
|
@ -53,29 +55,10 @@ export const AuthorBadge = (props: Props) => {
|
|||
requireAuthentication(() => {
|
||||
openPage(router, `inbox`)
|
||||
changeSearchParam({
|
||||
initChat: props.author.id.toString()
|
||||
initChat: props.author.id.toString(),
|
||||
})
|
||||
}, 'discussions')
|
||||
}
|
||||
const subscribeValue = createMemo(() => {
|
||||
if (props.iconButtons) {
|
||||
return <Icon name="author-subscribe" class={stylesButton.icon} />
|
||||
}
|
||||
return isSubscribing() ? t('subscribing...') : t('Subscribe')
|
||||
})
|
||||
|
||||
const unsubscribeValue = () => {
|
||||
if (props.iconButtons) {
|
||||
return <Icon name="author-unsubscribe" class={stylesButton.icon} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span class={styles.actionButtonLabel}>{t('Following')}</span>
|
||||
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.AuthorBadge, { [styles.nameOnly]: props.nameOnly })}>
|
||||
|
@ -129,12 +112,23 @@ export const AuthorBadge = (props: Props) => {
|
|||
<Button
|
||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||
size="M"
|
||||
value={subscribeValue()}
|
||||
value={
|
||||
<Show
|
||||
when={props.iconButtons}
|
||||
fallback={
|
||||
<Show when={isSubscribing()} fallback={t('Subscribe')}>
|
||||
{t('subscribing...')}
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<Icon name="author-subscribe" class={stylesButton.icon} />
|
||||
</Show>
|
||||
}
|
||||
onClick={() => handleSubscribe(true)}
|
||||
isSubscribeButton={true}
|
||||
class={clsx(styles.actionButton, {
|
||||
[styles.iconed]: props.iconButtons,
|
||||
[stylesButton.subscribed]: subscribed()
|
||||
[stylesButton.subscribed]: subscribed(),
|
||||
})}
|
||||
/>
|
||||
}
|
||||
|
@ -142,12 +136,24 @@ export const AuthorBadge = (props: Props) => {
|
|||
<Button
|
||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||
size="M"
|
||||
value={unsubscribeValue()}
|
||||
value={
|
||||
<Show
|
||||
when={props.iconButtons}
|
||||
fallback={
|
||||
<>
|
||||
<span class={styles.actionButtonLabel}>{t('Following')}</span>
|
||||
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Icon name="author-unsubscribe" class={stylesButton.icon} />
|
||||
</Show>
|
||||
}
|
||||
onClick={() => handleSubscribe(false)}
|
||||
isSubscribeButton={true}
|
||||
class={clsx(styles.actionButton, {
|
||||
[styles.iconed]: props.iconButtons,
|
||||
[stylesButton.subscribed]: subscribed()
|
||||
[stylesButton.subscribed]: subscribed(),
|
||||
})}
|
||||
/>
|
||||
</Show>
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
import type { Author } from '../../../graphql/types.gen'
|
||||
import { Userpic } from '../Userpic'
|
||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||
import { translit } from '../../../utils/ru2en'
|
||||
import { follow, unfollow } from '../../../stores/zine/common'
|
||||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
|
||||
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
|
||||
import { openPage, redirectPage } from '@nanostores/router'
|
||||
import { clsx } from 'clsx'
|
||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
|
||||
import { SubscriptionFilter } from '../../../pages/types'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { follow, unfollow } from '../../../stores/zine/common'
|
||||
import { isAuthor } from '../../../utils/isAuthor'
|
||||
import { AuthorBadge } from '../AuthorBadge'
|
||||
import { TopicBadge } from '../../Topic/TopicBadge'
|
||||
import { translit } from '../../../utils/ru2en'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
|
||||
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
import { TopicBadge } from '../../Topic/TopicBadge'
|
||||
import { AuthorBadge } from '../AuthorBadge'
|
||||
import { Userpic } from '../Userpic'
|
||||
|
||||
import styles from './AuthorCard.module.scss'
|
||||
import stylesButton from '../../_shared/Button/Button.module.scss'
|
||||
|
||||
|
@ -32,7 +35,7 @@ export const AuthorCard = (props: Props) => {
|
|||
session,
|
||||
subscriptions,
|
||||
isSessionLoaded,
|
||||
actions: { loadSubscriptions, requireAuthentication }
|
||||
actions: { loadSubscriptions, requireAuthentication },
|
||||
} = useSession()
|
||||
|
||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
||||
|
@ -40,7 +43,7 @@ export const AuthorCard = (props: Props) => {
|
|||
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
||||
|
||||
const subscribed = createMemo<boolean>(() =>
|
||||
subscriptions().authors.some((author) => author.slug === props.author.slug)
|
||||
subscriptions().authors.some((author) => author.slug === props.author.slug),
|
||||
)
|
||||
|
||||
const subscribe = async (really = true) => {
|
||||
|
@ -74,7 +77,7 @@ export const AuthorCard = (props: Props) => {
|
|||
requireAuthentication(() => {
|
||||
openPage(router, `inbox`)
|
||||
changeSearchParam({
|
||||
initChat: props.author.id.toString()
|
||||
initChat: props.author.id.toString(),
|
||||
})
|
||||
}, 'discussions')
|
||||
}
|
||||
|
@ -97,20 +100,22 @@ export const AuthorCard = (props: Props) => {
|
|||
}
|
||||
})
|
||||
|
||||
const followButtonText = () => {
|
||||
const followButtonText = createMemo(() => {
|
||||
if (isSubscribing()) {
|
||||
return t('subscribing...')
|
||||
} else if (subscribed()) {
|
||||
}
|
||||
|
||||
if (subscribed()) {
|
||||
return (
|
||||
<>
|
||||
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
||||
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return t('Follow')
|
||||
}
|
||||
}
|
||||
|
||||
return t('Follow')
|
||||
})
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.author, 'row')}>
|
||||
|
@ -216,7 +221,7 @@ export const AuthorCard = (props: Props) => {
|
|||
value={followButtonText()}
|
||||
isSubscribeButton={true}
|
||||
class={clsx({
|
||||
[stylesButton.subscribed]: subscribed()
|
||||
[stylesButton.subscribed]: subscribed(),
|
||||
})}
|
||||
/>
|
||||
<Button
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import styles from './AuthorRatingControl.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import styles from './AuthorRatingControl.module.scss'
|
||||
|
||||
interface AuthorRatingControlProps {
|
||||
author: Author
|
||||
class?: string
|
||||
|
@ -20,7 +22,7 @@ export const AuthorRatingControl = (props: AuthorRatingControlProps) => {
|
|||
<div
|
||||
class={clsx(styles.rating, props.class, {
|
||||
[styles.isUpvoted]: isUpvoted,
|
||||
[styles.isDownvoted]: isDownvoted
|
||||
[styles.isDownvoted]: isDownvoted,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { createMemo, Show } from 'solid-js'
|
||||
import styles from './Userpic.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { createMemo, Show } from 'solid-js'
|
||||
|
||||
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
import { Image } from '../../_shared/Image'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
|
||||
import styles from './Userpic.module.scss'
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
|
@ -46,7 +48,7 @@ export const Userpic = (props: Props) => {
|
|||
return (
|
||||
<div
|
||||
class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], {
|
||||
['cursorPointer']: props.onClick
|
||||
['cursorPointer']: props.onClick,
|
||||
})}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import styles from './Banner.module.scss'
|
||||
|
||||
import { showModal } from '../../stores/ui'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { showModal } from '../../stores/ui'
|
||||
|
||||
import styles from './Banner.module.scss'
|
||||
|
||||
export default () => {
|
||||
const { t } = useLocalize()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.donate-form input,
|
||||
.donate-form label,
|
||||
.donate-form .btn {
|
||||
.donateForm input,
|
||||
.donateForm label,
|
||||
.donateForm .btn {
|
||||
font-family: Muller, Arial, Helvetica, sans-serif;
|
||||
border: solid 1px #595959;
|
||||
border-radius: 3px;
|
||||
|
@ -10,7 +10,7 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.donate-form input {
|
||||
.donateForm input {
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
appearance: none;
|
||||
|
@ -48,12 +48,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
.donateForm .btn {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
padding: 5px 10px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
transform: none !important;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
&:last-of-type {
|
||||
|
@ -62,7 +63,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
.btnGroup {
|
||||
input {
|
||||
&:first-child + .btn {
|
||||
border-top-right-radius: 0;
|
||||
|
@ -75,12 +76,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.payment-type {
|
||||
.paymentType {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.donate-buttons-container {
|
||||
.donateButtonsContainer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
|
@ -111,59 +113,34 @@
|
|||
}
|
||||
}
|
||||
|
||||
.donate-input {
|
||||
.donateInput {
|
||||
@include media-breakpoint-down(sm) {
|
||||
flex: 1 100%;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
border: 1px solid #000;
|
||||
.sendBtn {
|
||||
border: 2px solid #000;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
color: #fff !important;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
line-height: 1.8;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-choose {
|
||||
.paymentChoose {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.form-group:not(:first-child) {
|
||||
.formGroup:not(:first-child) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.discours-help .modalwrap__inner {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
.payment-form {
|
||||
padding: 0 !important;
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.delimiter-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.delimiter {
|
||||
left: 100%;
|
||||
line-height: 1;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, calc(-50% - 0.8rem));
|
||||
}
|
||||
*/
|
|
@ -1,8 +1,11 @@
|
|||
import '../../styles/help.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { createSignal, onMount } from 'solid-js'
|
||||
import { showModal } from '../../stores/ui'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useSnackbar } from '../../context/snackbar'
|
||||
import { showModal } from '../../stores/ui'
|
||||
|
||||
import styles from './Donate.module.scss'
|
||||
|
||||
export const Donate = () => {
|
||||
const { t } = useLocalize()
|
||||
|
@ -11,7 +14,7 @@ export const Donate = () => {
|
|||
const cpOptions = {
|
||||
publicId: 'pk_0a37bab30ffc6b77b2f93d65f2aed',
|
||||
description: t('Help discours to grow'),
|
||||
currency: 'RUB'
|
||||
currency: 'RUB',
|
||||
}
|
||||
|
||||
let amountSwitchElement: HTMLDivElement | undefined
|
||||
|
@ -22,13 +25,13 @@ export const Donate = () => {
|
|||
const [period, setPeriod] = createSignal(monthly)
|
||||
const [amount, setAmount] = createSignal(0)
|
||||
const {
|
||||
actions: { showSnackbar }
|
||||
actions: { showSnackbar },
|
||||
} = useSnackbar()
|
||||
|
||||
const initiated = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const {
|
||||
cp: { CloudPayments }
|
||||
cp: { CloudPayments },
|
||||
} = window as any // Checkout(cpOptions)
|
||||
setWidget(new CloudPayments())
|
||||
console.log('[donate] payments initiated')
|
||||
|
@ -42,8 +45,8 @@ export const Donate = () => {
|
|||
amount: amount() || 0, //сумма
|
||||
vat: 20, //ставка НДС
|
||||
method: 0, // тег-1214 признак способа расчета - признак способа расчета
|
||||
object: 0 // тег-1212 признак предмета расчета - признак предмета товара, работы, услуги, платежа, выплаты, иного предмета расчета
|
||||
}
|
||||
object: 0, // тег-1212 признак предмета расчета - признак предмета товара, работы, услуги, платежа, выплаты, иного предмета расчета
|
||||
},
|
||||
],
|
||||
// taxationSystem: 0, //система налогообложения; необязательный, если у вас одна система налогообложения
|
||||
// email: 'user@example.com', //e-mail покупателя, если нужно отправить письмо с чеком
|
||||
|
@ -53,8 +56,8 @@ export const Donate = () => {
|
|||
electronic: amount(), // Сумма оплаты электронными деньгами
|
||||
advancePayment: 0, // Сумма из предоплаты (зачетом аванса) (2 знака после запятой)
|
||||
credit: 0, // Сумма постоплатой(в кредит) (2 знака после запятой)
|
||||
provision: 0 // Сумма оплаты встречным предоставлением (сертификаты, др. мат.ценности) (2 знака после запятой)
|
||||
}
|
||||
provision: 0, // Сумма оплаты встречным предоставлением (сертификаты, др. мат.ценности) (2 знака после запятой)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -93,10 +96,10 @@ export const Donate = () => {
|
|||
recurrent: {
|
||||
interval: period(), // local solid's signal
|
||||
period: 1, // internal widget's
|
||||
CustomerReciept: customerReciept() // чек для регулярных платежей
|
||||
}
|
||||
}
|
||||
}
|
||||
CustomerReciept: customerReciept(), // чек для регулярных платежей
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
(opts) => {
|
||||
// success
|
||||
|
@ -111,34 +114,34 @@ export const Donate = () => {
|
|||
|
||||
showSnackbar({
|
||||
type: 'error',
|
||||
body: reason
|
||||
body: reason,
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form class="discours-form donate-form" action="" method="post">
|
||||
<form class={styles.donateForm} action="" method="post">
|
||||
<input type="hidden" name="shopId" value="156465" />
|
||||
<input value="148805" name="scid" type="hidden" />
|
||||
<input value="0" name="customerNumber" type="hidden" />
|
||||
|
||||
<div class="form-group">
|
||||
<div class="donate-buttons-container" ref={amountSwitchElement}>
|
||||
<div class={styles.formGroup}>
|
||||
<div class={styles.donateButtonsContainer} ref={amountSwitchElement}>
|
||||
<input type="radio" name="amount" id="fix250" value="250" />
|
||||
<label for="fix250" class="btn donate-value-radio">
|
||||
<label for="fix250" class={styles.btn}>
|
||||
250 ₽
|
||||
</label>
|
||||
<input type="radio" name="amount" id="fix500" value="500" checked />
|
||||
<label for="fix500" class="btn donate-value-radio">
|
||||
<label for="fix500" class={styles.btn}>
|
||||
500 ₽
|
||||
</label>
|
||||
<input type="radio" name="amount" id="fix1000" value="1000" />
|
||||
<label for="fix1000" class="btn donate-value-radio">
|
||||
<label for="fix1000" class={styles.btn}>
|
||||
1000 ₽
|
||||
</label>
|
||||
<input
|
||||
class="form-control donate-input"
|
||||
class={styles.donateInput}
|
||||
required
|
||||
ref={customAmountElement}
|
||||
type="number"
|
||||
|
@ -148,8 +151,8 @@ export const Donate = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="payment-type" classList={{ showing: showingPayment() }}>
|
||||
<div class="btn-group payment-choose" data-toggle="buttons">
|
||||
<div class={styles.formGroup} id="payment-type" classList={{ showing: showingPayment() }}>
|
||||
<div class={clsx(styles.btnGroup, styles.paymentChoose)} data-toggle="buttons">
|
||||
<input
|
||||
type="radio"
|
||||
autocomplete="off"
|
||||
|
@ -158,7 +161,11 @@ export const Donate = () => {
|
|||
onClick={() => setPeriod(once)}
|
||||
checked={period() === once}
|
||||
/>
|
||||
<label for="once" class="btn payment-type" classList={{ active: period() === once }}>
|
||||
<label
|
||||
for="once"
|
||||
class={clsx(styles.btn, styles.paymentType)}
|
||||
classList={{ active: period() === once }}
|
||||
>
|
||||
{t('One time')}
|
||||
</label>
|
||||
<input
|
||||
|
@ -169,16 +176,20 @@ export const Donate = () => {
|
|||
onClick={() => setPeriod(monthly)}
|
||||
checked={period() === monthly}
|
||||
/>
|
||||
<label for="monthly" class="btn payment-type" classList={{ active: period() === monthly }}>
|
||||
<label
|
||||
for="monthly"
|
||||
class={clsx(styles.btn, styles.paymentType)}
|
||||
classList={{ active: period() === monthly }}
|
||||
>
|
||||
{t('Every month')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<a href={''} class="btn send-btn donate" onClick={show}>
|
||||
<div class={styles.formGroup}>
|
||||
<button type="button" class={clsx(styles.btn, styles.sendBtn)} onClick={show}>
|
||||
{t('Help discours to grow')}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { hideModal } from '../../stores/ui'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { hideModal } from '../../stores/ui'
|
||||
import { Button } from '../_shared/Button'
|
||||
|
||||
export const Feedback = () => {
|
||||
|
@ -14,9 +14,9 @@ export const Feedback = () => {
|
|||
method,
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json; charset=utf-8'
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
},
|
||||
body: JSON.stringify({ contact: contactElement?.value, message: msgElement?.textContent })
|
||||
body: JSON.stringify({ contact: contactElement?.value, message: msgElement?.textContent }),
|
||||
})
|
||||
hideModal()
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { createMemo, For } from 'solid-js'
|
||||
import styles from './Footer.module.scss'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { Subscribe } from '../_shared/Subscribe'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import styles from './Footer.module.scss'
|
||||
|
||||
export const Footer = () => {
|
||||
const { t, lang } = useLocalize()
|
||||
|
@ -17,25 +18,25 @@ export const Footer = () => {
|
|||
items: [
|
||||
{
|
||||
title: 'Manifest',
|
||||
slug: '/about/manifest'
|
||||
slug: '/about/manifest',
|
||||
},
|
||||
{
|
||||
title: 'How it works',
|
||||
slug: '/about/guide'
|
||||
slug: '/about/guide',
|
||||
},
|
||||
{
|
||||
title: 'Dogma',
|
||||
slug: '/about/dogma'
|
||||
slug: '/about/dogma',
|
||||
},
|
||||
{
|
||||
title: 'Principles',
|
||||
slug: '/about/principles'
|
||||
slug: '/about/principles',
|
||||
},
|
||||
{
|
||||
title: 'How to write an article',
|
||||
slug: '/how-to-write-a-good-article'
|
||||
}
|
||||
]
|
||||
slug: '/how-to-write-a-good-article',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
|
@ -43,25 +44,25 @@ export const Footer = () => {
|
|||
items: [
|
||||
{
|
||||
title: 'Suggest an idea',
|
||||
slug: '/connect'
|
||||
slug: '/connect',
|
||||
},
|
||||
{
|
||||
title: 'Become an author',
|
||||
slug: '/create'
|
||||
slug: '/create',
|
||||
},
|
||||
{
|
||||
title: 'Support us',
|
||||
slug: '/about/help'
|
||||
slug: '/about/help',
|
||||
},
|
||||
{
|
||||
title: 'Feedback',
|
||||
slug: '/#feedback'
|
||||
slug: '/#feedback',
|
||||
},
|
||||
{
|
||||
title: 'Work with us',
|
||||
slug: 'https://docs.google.com/forms/d/e/1FAIpQLSeNNvIzKlXElJtkPkYiXl-jQjlvsL9u4-kpnoRjz1O8Wo40xQ/viewform'
|
||||
}
|
||||
]
|
||||
slug: 'https://docs.google.com/forms/d/e/1FAIpQLSeNNvIzKlXElJtkPkYiXl-jQjlvsL9u4-kpnoRjz1O8Wo40xQ/viewform',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
|
@ -69,46 +70,46 @@ export const Footer = () => {
|
|||
items: [
|
||||
{
|
||||
title: 'Authors',
|
||||
slug: '/authors'
|
||||
slug: '/authors',
|
||||
},
|
||||
{
|
||||
title: 'Communities',
|
||||
slug: '/community'
|
||||
slug: '/community',
|
||||
},
|
||||
{
|
||||
title: 'Partners',
|
||||
slug: '/about/partners'
|
||||
slug: '/about/partners',
|
||||
},
|
||||
{
|
||||
title: 'Special projects',
|
||||
slug: '/about/projects'
|
||||
slug: '/about/projects',
|
||||
},
|
||||
{
|
||||
title: changeLangTitle(),
|
||||
slug: changeLangLink(),
|
||||
rel: 'external'
|
||||
}
|
||||
]
|
||||
}
|
||||
rel: 'external',
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
const SOCIAL = [
|
||||
{
|
||||
name: 'facebook',
|
||||
href: 'https://facebook.com/discoursio'
|
||||
href: 'https://facebook.com/discoursio',
|
||||
},
|
||||
{
|
||||
name: 'vk',
|
||||
href: 'https://vk.com/discoursio'
|
||||
href: 'https://vk.com/discoursio',
|
||||
},
|
||||
{
|
||||
name: 'twitter',
|
||||
href: 'https://twitter.com/discours_io'
|
||||
href: 'https://twitter.com/discours_io',
|
||||
},
|
||||
{
|
||||
name: 'telegram',
|
||||
href: 'https://t.me/discoursio'
|
||||
}
|
||||
href: 'https://t.me/discoursio',
|
||||
},
|
||||
]
|
||||
return (
|
||||
<footer class={styles.discoursFooter}>
|
||||
|
@ -143,7 +144,7 @@ export const Footer = () => {
|
|||
<div class={clsx(styles.footerCopyright, 'row')}>
|
||||
<div class="col-md-18 col-lg-20">
|
||||
{t(
|
||||
'Independant magazine with an open horizontal cooperation about culture, science and society'
|
||||
'Independant magazine with an open horizontal cooperation about culture, science and society',
|
||||
)}
|
||||
. {t('Discours')} © 2015–{new Date().getFullYear()}{' '}
|
||||
<a href="/about/terms-of-use">{t('Terms of use')}</a>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import styles from './Hero.module.scss'
|
||||
|
||||
import { showModal } from '../../stores/ui'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useRouter } from '../../stores/router'
|
||||
import { showModal } from '../../stores/ui'
|
||||
import { AuthModalSearchParams } from '../Nav/AuthModal/types'
|
||||
|
||||
import styles from './Hero.module.scss'
|
||||
|
||||
export default () => {
|
||||
const { t } = useLocalize()
|
||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||
|
@ -17,7 +17,7 @@ export default () => {
|
|||
<h4 innerHTML={t('Horizontal collaborative journalistic platform')} />
|
||||
<p
|
||||
innerHTML={t(
|
||||
'Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects'
|
||||
'Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects',
|
||||
)}
|
||||
/>
|
||||
<div class={styles.aboutDiscoursActions}>
|
||||
|
@ -29,7 +29,7 @@ export default () => {
|
|||
onClick={() => {
|
||||
showModal('auth')
|
||||
changeSearchParam({
|
||||
mode: 'register'
|
||||
mode: 'register',
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './Draft.module.scss'
|
||||
import type { Shout } from '../../graphql/types.gen'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useConfirm } from '../../context/confirm'
|
||||
import { useSnackbar } from '../../context/snackbar'
|
||||
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import { useConfirm } from '../../context/confirm'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useSnackbar } from '../../context/snackbar'
|
||||
import { router } from '../../stores/router'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
import styles from './Draft.module.scss'
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
|
@ -18,11 +21,11 @@ type Props = {
|
|||
export const Draft = (props: Props) => {
|
||||
const { t, formatDate } = useLocalize()
|
||||
const {
|
||||
actions: { showConfirm }
|
||||
actions: { showConfirm },
|
||||
} = useConfirm()
|
||||
|
||||
const {
|
||||
actions: { showSnackbar }
|
||||
actions: { showSnackbar },
|
||||
} = useSnackbar()
|
||||
|
||||
const handlePublishLinkClick = (e) => {
|
||||
|
@ -37,7 +40,7 @@ export const Draft = (props: Props) => {
|
|||
confirmBody: t('Are you sure you want to delete this draft?'),
|
||||
confirmButtonLabel: t('Delete'),
|
||||
confirmButtonVariant: 'danger',
|
||||
declineButtonVariant: 'primary'
|
||||
declineButtonVariant: 'primary',
|
||||
})
|
||||
if (isConfirmed) {
|
||||
props.onDelete(props.shout)
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { Buffer } from 'buffer'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import styles from './AudioUploader.module.scss'
|
||||
import { DropArea } from '../../_shared/DropArea'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
import { composeMediaItems } from '../../../utils/composeMediaItems'
|
||||
import { DropArea } from '../../_shared/DropArea'
|
||||
import { AudioPlayer } from '../../Article/AudioPlayer'
|
||||
import { Buffer } from 'buffer'
|
||||
|
||||
import styles from './AudioUploader.module.scss'
|
||||
|
||||
window.Buffer = Buffer
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './AutoSaveNotice.module.scss'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
|
||||
import styles from './AutoSaveNotice.module.scss'
|
||||
|
||||
type Props = {
|
||||
active: boolean
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import type { Editor } from '@tiptap/core'
|
||||
import styles from './BubbleMenu.module.scss'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
|
||||
import styles from './BubbleMenu.module.scss'
|
||||
|
||||
type Props = {
|
||||
editor: Editor
|
||||
ref: (el: HTMLElement) => void
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import type { Editor } from '@tiptap/core'
|
||||
import styles from './BubbleMenu.module.scss'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import { UploadModalContent } from '../UploadModalContent'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
import { UploadedFile } from '../../../pages/types'
|
||||
import { renderUploadedImage } from '../../../utils/renderUploadedImage'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
import { UploadModalContent } from '../UploadModalContent'
|
||||
|
||||
import styles from './BubbleMenu.module.scss'
|
||||
|
||||
type Props = {
|
||||
editor: Editor
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { createSignal, Show, For } from 'solid-js'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import styles from './BubbleMenu.module.scss'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { createSignal, Show, For } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import styles from './BubbleMenu.module.scss'
|
||||
|
||||
type Props = {
|
||||
editor: Editor
|
||||
|
|
|
@ -1,52 +1,56 @@
|
|||
import { createEffect, createSignal, onCleanup } from 'solid-js'
|
||||
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||
import uniqolor from 'uniqolor'
|
||||
import * as Y from 'yjs'
|
||||
import type { Doc } from 'yjs/dist/src/utils/Doc'
|
||||
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider'
|
||||
import { isTextSelection } from '@tiptap/core'
|
||||
import { Bold } from '@tiptap/extension-bold'
|
||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||
import { Dropcursor } from '@tiptap/extension-dropcursor'
|
||||
import { Italic } from '@tiptap/extension-italic'
|
||||
import { Strike } from '@tiptap/extension-strike'
|
||||
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
|
||||
import { Underline } from '@tiptap/extension-underline'
|
||||
import { FloatingMenu } from '@tiptap/extension-floating-menu'
|
||||
import { BulletList } from '@tiptap/extension-bullet-list'
|
||||
import { OrderedList } from '@tiptap/extension-ordered-list'
|
||||
import { ListItem } from '@tiptap/extension-list-item'
|
||||
import { CharacterCount } from '@tiptap/extension-character-count'
|
||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||
import { Collaboration } from '@tiptap/extension-collaboration'
|
||||
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
|
||||
import { Document } from '@tiptap/extension-document'
|
||||
import { Dropcursor } from '@tiptap/extension-dropcursor'
|
||||
import { FloatingMenu } from '@tiptap/extension-floating-menu'
|
||||
import Focus from '@tiptap/extension-focus'
|
||||
import { Gapcursor } from '@tiptap/extension-gapcursor'
|
||||
import { HardBreak } from '@tiptap/extension-hard-break'
|
||||
import { Heading } from '@tiptap/extension-heading'
|
||||
import { Highlight } from '@tiptap/extension-highlight'
|
||||
import { Link } from '@tiptap/extension-link'
|
||||
import { Document } from '@tiptap/extension-document'
|
||||
import { Text } from '@tiptap/extension-text'
|
||||
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
|
||||
import { isTextSelection } from '@tiptap/core'
|
||||
import { Paragraph } from '@tiptap/extension-paragraph'
|
||||
import Focus from '@tiptap/extension-focus'
|
||||
import { Collaboration } from '@tiptap/extension-collaboration'
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider'
|
||||
import { CustomBlockquote } from './extensions/CustomBlockquote'
|
||||
import { Figure } from './extensions/Figure'
|
||||
import { Figcaption } from './extensions/Figcaption'
|
||||
import { Embed } from './extensions/Embed'
|
||||
import { useSession } from '../../context/session'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useEditorContext } from '../../context/editor'
|
||||
import { TrailingNode } from './extensions/TrailingNode'
|
||||
import Article from './extensions/Article'
|
||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||
import './Prosemirror.scss'
|
||||
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import { Footnote } from './extensions/Footnote'
|
||||
import { Italic } from '@tiptap/extension-italic'
|
||||
import { Link } from '@tiptap/extension-link'
|
||||
import { ListItem } from '@tiptap/extension-list-item'
|
||||
import { OrderedList } from '@tiptap/extension-ordered-list'
|
||||
import { Paragraph } from '@tiptap/extension-paragraph'
|
||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||
import { Strike } from '@tiptap/extension-strike'
|
||||
import { Text } from '@tiptap/extension-text'
|
||||
import { Underline } from '@tiptap/extension-underline'
|
||||
import { createEffect, createSignal, onCleanup } from 'solid-js'
|
||||
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||
import uniqolor from 'uniqolor'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
import { useEditorContext } from '../../context/editor'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useSession } from '../../context/session'
|
||||
import { useSnackbar } from '../../context/snackbar'
|
||||
import { handleImageUpload } from '../../utils/handleImageUpload'
|
||||
|
||||
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||
import Article from './extensions/Article'
|
||||
import { CustomBlockquote } from './extensions/CustomBlockquote'
|
||||
import { Embed } from './extensions/Embed'
|
||||
import { Figcaption } from './extensions/Figcaption'
|
||||
import { Figure } from './extensions/Figure'
|
||||
import { Footnote } from './extensions/Footnote'
|
||||
import { TrailingNode } from './extensions/TrailingNode'
|
||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||
|
||||
import './Prosemirror.scss'
|
||||
|
||||
type Props = {
|
||||
shoutId: number
|
||||
initialContent?: string
|
||||
|
@ -61,7 +65,7 @@ const allowedImageTypes = new Set([
|
|||
'image/png',
|
||||
'image/tiff',
|
||||
'image/webp',
|
||||
'image/x-icon'
|
||||
'image/x-icon',
|
||||
])
|
||||
|
||||
const yDocs: Record<string, Doc> = {}
|
||||
|
@ -75,7 +79,7 @@ export const Editor = (props: Props) => {
|
|||
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
|
||||
|
||||
const {
|
||||
actions: { showSnackbar }
|
||||
actions: { showSnackbar },
|
||||
} = useSnackbar()
|
||||
|
||||
const docName = `shout-${props.shoutId}`
|
||||
|
@ -88,47 +92,47 @@ export const Editor = (props: Props) => {
|
|||
providers[docName] = new HocuspocusProvider({
|
||||
url: 'wss://hocuspocus.discours.io',
|
||||
name: docName,
|
||||
document: yDocs[docName]
|
||||
document: yDocs[docName],
|
||||
})
|
||||
}
|
||||
|
||||
const editorElRef: {
|
||||
current: HTMLDivElement
|
||||
} = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
|
||||
const textBubbleMenuRef: {
|
||||
current: HTMLDivElement
|
||||
} = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
|
||||
const incutBubbleMenuRef: {
|
||||
current: HTMLElement
|
||||
} = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
const figureBubbleMenuRef: {
|
||||
current: HTMLElement
|
||||
} = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
const blockquoteBubbleMenuRef: {
|
||||
current: HTMLElement
|
||||
} = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
|
||||
const floatingMenuRef: {
|
||||
current: HTMLDivElement
|
||||
} = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
|
||||
const ImageFigure = Figure.extend({
|
||||
name: 'capturedImage',
|
||||
content: 'figcaption image'
|
||||
content: 'figcaption image',
|
||||
})
|
||||
|
||||
const handleClipboardPaste = async () => {
|
||||
|
@ -149,7 +153,7 @@ export const Editor = (props: Props) => {
|
|||
source: blob.toString(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
file
|
||||
file,
|
||||
}
|
||||
|
||||
showSnackbar({ body: t('Uploading image') })
|
||||
|
@ -166,17 +170,17 @@ export const Editor = (props: Props) => {
|
|||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: result.originalFilename
|
||||
}
|
||||
]
|
||||
text: result.originalFilename,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
attrs: {
|
||||
src: result.url
|
||||
}
|
||||
}
|
||||
]
|
||||
src: result.url,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.run()
|
||||
} catch (error) {
|
||||
|
@ -190,7 +194,7 @@ export const Editor = (props: Props) => {
|
|||
element: editorElRef.current,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'articleEditor'
|
||||
class: 'articleEditor',
|
||||
},
|
||||
transformPastedHTML(html) {
|
||||
return html.replaceAll(/<img.*?>/g, '')
|
||||
|
@ -198,7 +202,7 @@ export const Editor = (props: Props) => {
|
|||
handlePaste: () => {
|
||||
handleClipboardPaste()
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
extensions: [
|
||||
Document,
|
||||
|
@ -211,31 +215,31 @@ export const Editor = (props: Props) => {
|
|||
Strike,
|
||||
HorizontalRule.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'horizontalRule'
|
||||
}
|
||||
class: 'horizontalRule',
|
||||
},
|
||||
}),
|
||||
Underline,
|
||||
Link.configure({
|
||||
openOnClick: false
|
||||
openOnClick: false,
|
||||
}),
|
||||
Heading.configure({
|
||||
levels: [2, 3, 4]
|
||||
levels: [2, 3, 4],
|
||||
}),
|
||||
BulletList,
|
||||
OrderedList,
|
||||
ListItem,
|
||||
Collaboration.configure({
|
||||
document: yDocs[docName]
|
||||
document: yDocs[docName],
|
||||
}),
|
||||
CollaborationCursor.configure({
|
||||
provider: providers[docName],
|
||||
user: {
|
||||
name: user().name,
|
||||
color: uniqolor(user().slug).color
|
||||
}
|
||||
color: uniqolor(user().slug).color,
|
||||
},
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: t('Add a link or click plus to embed media')
|
||||
placeholder: t('Add a link or click plus to embed media'),
|
||||
}),
|
||||
Focus,
|
||||
Gapcursor,
|
||||
|
@ -243,8 +247,8 @@ export const Editor = (props: Props) => {
|
|||
Highlight.configure({
|
||||
multicolor: true,
|
||||
HTMLAttributes: {
|
||||
class: 'highlight'
|
||||
}
|
||||
class: 'highlight',
|
||||
},
|
||||
}),
|
||||
ImageFigure,
|
||||
Image,
|
||||
|
@ -267,8 +271,8 @@ export const Editor = (props: Props) => {
|
|||
return result
|
||||
},
|
||||
tippyOptions: {
|
||||
sticky: true
|
||||
}
|
||||
sticky: true,
|
||||
},
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
pluginKey: 'blockquoteBubbleMenu',
|
||||
|
@ -286,8 +290,8 @@ export const Editor = (props: Props) => {
|
|||
if (selectedElement) {
|
||||
return selectedElement.getBoundingClientRect()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
pluginKey: 'incutBubbleMenu',
|
||||
|
@ -305,31 +309,31 @@ export const Editor = (props: Props) => {
|
|||
if (selectedElement) {
|
||||
return selectedElement.getBoundingClientRect()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
pluginKey: 'imageBubbleMenu',
|
||||
element: figureBubbleMenuRef.current,
|
||||
shouldShow: ({ editor: e, view }) => {
|
||||
return view.hasFocus() && e.isActive('image')
|
||||
}
|
||||
},
|
||||
}),
|
||||
FloatingMenu.configure({
|
||||
tippyOptions: {
|
||||
placement: 'left'
|
||||
placement: 'left',
|
||||
},
|
||||
element: floatingMenuRef.current
|
||||
element: floatingMenuRef.current,
|
||||
}),
|
||||
TrailingNode,
|
||||
Article
|
||||
Article,
|
||||
],
|
||||
enablePasteRules: [Link],
|
||||
content: initialContent ?? null
|
||||
content: initialContent ?? null,
|
||||
}))
|
||||
|
||||
const {
|
||||
actions: { countWords, setEditor }
|
||||
actions: { countWords, setEditor },
|
||||
} = useEditorContext()
|
||||
|
||||
setEditor(editor)
|
||||
|
@ -341,7 +345,7 @@ export const Editor = (props: Props) => {
|
|||
if (html()) {
|
||||
countWords({
|
||||
characters: editor().storage.characterCount.characters(),
|
||||
words: editor().storage.characterCount.words()
|
||||
words: editor().storage.characterCount.words(),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import { createEffect, createSignal, Show } from 'solid-js'
|
||||
import type { Editor, JSONContent } from '@tiptap/core'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { InlineForm } from '../InlineForm'
|
||||
import styles from './EditorFloatingMenu.module.scss'
|
||||
import HTMLParser from 'html-to-json-parser'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
import { Menu } from './Menu'
|
||||
import type { MenuItem } from './Menu/Menu'
|
||||
import { showModal } from '../../../stores/ui'
|
||||
import { UploadModalContent } from '../UploadModalContent'
|
||||
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
|
||||
import { createEffect, createSignal, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { UploadedFile } from '../../../pages/types'
|
||||
import { showModal } from '../../../stores/ui'
|
||||
import { renderUploadedImage } from '../../../utils/renderUploadedImage'
|
||||
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
import { InlineForm } from '../InlineForm'
|
||||
import { UploadModalContent } from '../UploadModalContent'
|
||||
|
||||
import { Menu } from './Menu'
|
||||
|
||||
import styles from './EditorFloatingMenu.module.scss'
|
||||
|
||||
type FloatingMenuProps = {
|
||||
editor: Editor
|
||||
|
@ -20,10 +23,17 @@ type FloatingMenuProps = {
|
|||
}
|
||||
|
||||
const embedData = async (data) => {
|
||||
const result = (await HTMLParser(data, false)) as JSONContent
|
||||
if ('type' in result && result.type === 'iframe') {
|
||||
return result.attributes
|
||||
const element = document.createRange().createContextualFragment(data)
|
||||
const { attributes } = element.firstChild as HTMLIFrameElement
|
||||
|
||||
const result: { src: string } = { src: '' }
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const attribute = attributes[i]
|
||||
result[attribute.name] = attribute.value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
||||
|
@ -39,8 +49,8 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
|||
}
|
||||
|
||||
const validateEmbed = async (value) => {
|
||||
const iframeData = (await HTMLParser(value, false)) as JSONContent
|
||||
if (iframeData.type !== 'iframe') {
|
||||
const element = document.createRange().createContextualFragment(value)
|
||||
if (element.firstChild?.nodeName !== 'IFRAME') {
|
||||
return t('Error')
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +84,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
|||
if (menuOpen()) {
|
||||
setMenuOpen(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const handleUpload = (image: UploadedFile) => {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import styles from './Menu.module.scss'
|
||||
import { useLocalize } from '../../../../context/localize'
|
||||
import { Icon } from '../../../_shared/Icon'
|
||||
import { Popover } from '../../../_shared/Popover'
|
||||
import { useLocalize } from '../../../../context/localize'
|
||||
|
||||
import styles from './Menu.module.scss'
|
||||
|
||||
export type MenuItem = 'image' | 'embed' | 'horizontal-rule'
|
||||
|
||||
|
|
|
@ -39,7 +39,9 @@
|
|||
border: 1px solid #e9e9ee;
|
||||
border-radius: 2px;
|
||||
opacity: 0;
|
||||
transition: height 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
||||
transition:
|
||||
height 0.3s ease-in-out,
|
||||
opacity 0.3s ease-in-out;
|
||||
|
||||
&.visible {
|
||||
height: 32px;
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import styles from './InlineForm.module.scss'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { createSignal, onMount } from 'solid-js'
|
||||
import { clsx } from 'clsx'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import { createSignal, onMount } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
|
||||
import styles from './InlineForm.module.scss'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Editor } from '@tiptap/core'
|
||||
import { createEditorTransaction } from 'solid-tiptap'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { validateUrl } from '../../../utils/validateUrl'
|
||||
import { InlineForm } from '../InlineForm'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { createEditorTransaction } from 'solid-tiptap'
|
||||
|
||||
type Props = {
|
||||
editor: Editor
|
||||
|
@ -24,7 +25,7 @@ export const InsertLinkForm = (props: Props) => {
|
|||
() => props.editor,
|
||||
(ed) => {
|
||||
return (ed && ed.getAttributes('link').href) || ''
|
||||
}
|
||||
},
|
||||
)
|
||||
const handleClearLinkForm = () => {
|
||||
if (currentUrl()) {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.LinkBubbleMenu {
|
||||
background: var(--editor-bubble-menu-background);
|
||||
box-shadow: 0 4px 10px rgba(#000, 0.25);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import type { Editor } from '@tiptap/core'
|
||||
|
||||
import { InsertLinkForm } from '../InsertLinkForm'
|
||||
|
||||
import styles from './LinkBubbleMenu.module.scss'
|
||||
|
||||
type Props = {
|
||||
editor: Editor
|
||||
ref: (el: HTMLDivElement) => void
|
||||
shouldShow: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const LinkBubbleMenuModule = (props: Props) => {
|
||||
return (
|
||||
<div ref={props.ref} class={styles.LinkBubbleMenu}>
|
||||
<InsertLinkForm editor={props.editor} onClose={props.onClose} />
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/Editor/LinkBubbleMenu/index.ts
Normal file
1
src/components/Editor/LinkBubbleMenu/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { LinkBubbleMenuModule } from './LinkBubbleMenu.module'
|
|
@ -1,17 +1,19 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import styles from './Panel.module.scss'
|
||||
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'
|
||||
import { clsx } from 'clsx'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import { useEditorHTML } from 'solid-tiptap'
|
||||
import Typograf from 'typograf'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
|
||||
import { useEditorContext } from '../../../context/editor'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { router } from '../../../stores/router'
|
||||
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
|
||||
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { DarkModeToggle } from '../../_shared/DarkModeToggle'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import styles from './Panel.module.scss'
|
||||
|
||||
const typograf = new Typograf({ locale: ['ru', 'en-US'] })
|
||||
|
||||
|
@ -26,11 +28,11 @@ export const Panel = (props: Props) => {
|
|||
wordCounter,
|
||||
editorRef,
|
||||
form,
|
||||
actions: { toggleEditorPanel, saveShout, publishShout }
|
||||
actions: { toggleEditorPanel, saveShout, publishShout },
|
||||
} = useEditorContext()
|
||||
|
||||
const containerRef: { current: HTMLElement } = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
|
||||
const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false)
|
||||
|
@ -39,7 +41,7 @@ export const Panel = (props: Props) => {
|
|||
useOutsideClickHandler({
|
||||
containerRef,
|
||||
predicate: () => isEditorPanelVisible(),
|
||||
handler: () => toggleEditorPanel()
|
||||
handler: () => toggleEditorPanel(),
|
||||
})
|
||||
|
||||
useEscKeyDownHandler(() => {
|
||||
|
|
|
@ -1,3 +1,15 @@
|
|||
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||
import { Bold } from '@tiptap/extension-bold'
|
||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||
import { CharacterCount } from '@tiptap/extension-character-count'
|
||||
import { Document } from '@tiptap/extension-document'
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import { Italic } from '@tiptap/extension-italic'
|
||||
import { Link } from '@tiptap/extension-link'
|
||||
import { Paragraph } from '@tiptap/extension-paragraph'
|
||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||
import { Text } from '@tiptap/extension-text'
|
||||
import { clsx } from 'clsx'
|
||||
import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { Portal } from 'solid-js/web'
|
||||
import {
|
||||
|
@ -5,34 +17,25 @@ import {
|
|||
createTiptapEditor,
|
||||
useEditorHTML,
|
||||
useEditorIsEmpty,
|
||||
useEditorIsFocused
|
||||
useEditorIsFocused,
|
||||
} from 'solid-tiptap'
|
||||
|
||||
import { useEditorContext } from '../../context/editor'
|
||||
import { Document } from '@tiptap/extension-document'
|
||||
import { Text } from '@tiptap/extension-text'
|
||||
import { Paragraph } from '@tiptap/extension-paragraph'
|
||||
import { Bold } from '@tiptap/extension-bold'
|
||||
import { Button } from '../_shared/Button'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { UploadedFile } from '../../pages/types'
|
||||
import { hideModal, showModal } from '../../stores/ui'
|
||||
import { Button } from '../_shared/Button'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { Popover } from '../_shared/Popover'
|
||||
import { Italic } from '@tiptap/extension-italic'
|
||||
import { Modal } from '../Nav/Modal'
|
||||
import { hideModal, showModal } from '../../stores/ui'
|
||||
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||
import { UploadModalContent } from './UploadModalContent'
|
||||
import { clsx } from 'clsx'
|
||||
import styles from './SimplifiedEditor.module.scss'
|
||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||
import { InsertLinkForm } from './InsertLinkForm'
|
||||
import { Link } from '@tiptap/extension-link'
|
||||
import { UploadedFile } from '../../pages/types'
|
||||
import { Figure } from './extensions/Figure'
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
|
||||
import { Figcaption } from './extensions/Figcaption'
|
||||
import { Figure } from './extensions/Figure'
|
||||
import { LinkBubbleMenuModule } from './LinkBubbleMenu'
|
||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||
import { CharacterCount } from '@tiptap/extension-character-count'
|
||||
import { UploadModalContent } from './UploadModalContent'
|
||||
|
||||
import styles from './SimplifiedEditor.module.scss'
|
||||
|
||||
type Props = {
|
||||
placeholder: string
|
||||
|
@ -61,33 +64,39 @@ export const MAX_DESCRIPTION_LIMIT = 400
|
|||
const SimplifiedEditor = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const [counter, setCounter] = createSignal<number>()
|
||||
|
||||
const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false)
|
||||
const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false)
|
||||
const wrapperEditorElRef: {
|
||||
current: HTMLElement
|
||||
} = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
|
||||
const editorElRef: {
|
||||
current: HTMLElement
|
||||
} = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
|
||||
const textBubbleMenuRef: {
|
||||
current: HTMLDivElement
|
||||
} = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
|
||||
const linkBubbleMenuRef: {
|
||||
current: HTMLDivElement
|
||||
} = {
|
||||
current: null,
|
||||
}
|
||||
|
||||
const {
|
||||
actions: { setEditor }
|
||||
actions: { setEditor },
|
||||
} = useEditorContext()
|
||||
|
||||
const ImageFigure = Figure.extend({
|
||||
name: 'capturedImage',
|
||||
content: 'figcaption image'
|
||||
content: 'figcaption image',
|
||||
})
|
||||
|
||||
const content = props.initialContent
|
||||
|
@ -95,8 +104,8 @@ const SimplifiedEditor = (props: Props) => {
|
|||
element: editorElRef.current,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: styles.simplifiedEditorField
|
||||
}
|
||||
class: styles.simplifiedEditorField,
|
||||
},
|
||||
},
|
||||
extensions: [
|
||||
Document,
|
||||
|
@ -105,16 +114,16 @@ const SimplifiedEditor = (props: Props) => {
|
|||
Bold,
|
||||
Italic,
|
||||
Link.configure({
|
||||
openOnClick: false
|
||||
openOnClick: false,
|
||||
}),
|
||||
|
||||
CharacterCount.configure({
|
||||
limit: MAX_DESCRIPTION_LIMIT
|
||||
limit: MAX_DESCRIPTION_LIMIT,
|
||||
}),
|
||||
Blockquote.configure({
|
||||
HTMLAttributes: {
|
||||
class: styles.blockQuote
|
||||
}
|
||||
class: styles.blockQuote,
|
||||
},
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
pluginKey: 'textBubbleMenu',
|
||||
|
@ -124,18 +133,27 @@ const SimplifiedEditor = (props: Props) => {
|
|||
const { selection } = state
|
||||
const { empty } = selection
|
||||
return view.hasFocus() && !empty
|
||||
}
|
||||
},
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
pluginKey: 'linkBubbleMenu',
|
||||
element: linkBubbleMenuRef.current,
|
||||
shouldShow: ({ state }) => {
|
||||
const { selection } = state
|
||||
const { empty } = selection
|
||||
return !empty && shouldShowLinkBubbleMenu()
|
||||
},
|
||||
}),
|
||||
ImageFigure,
|
||||
Image,
|
||||
Figcaption,
|
||||
Placeholder.configure({
|
||||
emptyNodeClass: styles.emptyNode,
|
||||
placeholder: props.placeholder
|
||||
})
|
||||
placeholder: props.placeholder,
|
||||
}),
|
||||
],
|
||||
autofocus: props.autoFocus,
|
||||
content: content ?? null
|
||||
content: content ?? null,
|
||||
}))
|
||||
|
||||
setEditor(editor)
|
||||
|
@ -147,7 +165,7 @@ const SimplifiedEditor = (props: Props) => {
|
|||
() => editor(),
|
||||
(ed) => {
|
||||
return ed && ed.isActive(name)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const html = useEditorHTML(() => editor())
|
||||
|
@ -168,17 +186,17 @@ const SimplifiedEditor = (props: Props) => {
|
|||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: image.originalFilename
|
||||
}
|
||||
]
|
||||
text: image.originalFilename,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
attrs: {
|
||||
src: image.url
|
||||
}
|
||||
}
|
||||
]
|
||||
src: image.url,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.run()
|
||||
hideModal()
|
||||
|
@ -210,7 +228,7 @@ const SimplifiedEditor = (props: Props) => {
|
|||
|
||||
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !editor().state.selection.empty) {
|
||||
event.preventDefault()
|
||||
showModal('simplifiedEditorInsertLink')
|
||||
setShouldShowLinkBubbleMenu(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,8 +246,6 @@ const SimplifiedEditor = (props: Props) => {
|
|||
})
|
||||
}
|
||||
|
||||
const handleInsertLink = () => !editor().state.selection.empty && showModal('simplifiedEditorInsertLink')
|
||||
|
||||
createEffect(() => {
|
||||
if (html()) {
|
||||
setCounter(editor().storage.characterCount.characters())
|
||||
|
@ -238,9 +254,13 @@ const SimplifiedEditor = (props: Props) => {
|
|||
|
||||
const maxHeightStyle = {
|
||||
overflow: 'auto',
|
||||
'max-height': `${props.maxHeight}px`
|
||||
'max-height': `${props.maxHeight}px`,
|
||||
}
|
||||
|
||||
const handleShowLinkBubble = () => {
|
||||
editor().chain().focus().run()
|
||||
setShouldShowLinkBubbleMenu(true)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={(el) => (wrapperEditorElRef.current = el)}
|
||||
|
@ -249,7 +269,7 @@ const SimplifiedEditor = (props: Props) => {
|
|||
[styles.minimal]: props.variant === 'minimal',
|
||||
[styles.bordered]: props.variant === 'bordered',
|
||||
[styles.isFocused]: isFocused() || !isEmpty(),
|
||||
[styles.labelVisible]: props.label && counter() > 0
|
||||
[styles.labelVisible]: props.label && counter() > 0,
|
||||
})}
|
||||
>
|
||||
<Show when={props.maxLength && editor()}>
|
||||
|
@ -291,7 +311,7 @@ const SimplifiedEditor = (props: Props) => {
|
|||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
onClick={handleInsertLink}
|
||||
onClick={handleShowLinkBubble}
|
||||
class={clsx(styles.actionButton, { [styles.active]: isLink() })}
|
||||
>
|
||||
<Icon name="editor-link" />
|
||||
|
@ -342,11 +362,6 @@ const SimplifiedEditor = (props: Props) => {
|
|||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<Portal>
|
||||
<Modal variant="narrow" name="simplifiedEditorInsertLink">
|
||||
<InsertLinkForm editor={editor()} onClose={() => hideModal()} />
|
||||
</Modal>
|
||||
</Portal>
|
||||
<Show when={props.imageEnabled}>
|
||||
<Portal>
|
||||
<Modal variant="narrow" name="simplifiedEditorUploadImage">
|
||||
|
@ -366,6 +381,12 @@ const SimplifiedEditor = (props: Props) => {
|
|||
ref={(el) => (textBubbleMenuRef.current = el)}
|
||||
/>
|
||||
</Show>
|
||||
<LinkBubbleMenuModule
|
||||
shouldShow={shouldShowLinkBubbleMenu()}
|
||||
editor={editor()}
|
||||
ref={(el) => (linkBubbleMenuRef.current = el)}
|
||||
onClose={() => setShouldShowLinkBubbleMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { Switch, Match, createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import styles from './TextBubbleMenu.module.scss'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { Switch, Match, createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js'
|
||||
import { createEditorTransaction } from 'solid-tiptap'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import { InsertLinkForm } from '../InsertLinkForm'
|
||||
import SimplifiedEditor from '../SimplifiedEditor'
|
||||
|
||||
import styles from './TextBubbleMenu.module.scss'
|
||||
|
||||
type BubbleMenuProps = {
|
||||
editor: Editor
|
||||
isCommonMarkup: boolean
|
||||
|
@ -22,7 +25,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
const isActive = (name: string, attributes?: unknown) =>
|
||||
createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => editor && editor.isActive(name, attributes)
|
||||
(editor) => editor && editor.isActive(name, attributes),
|
||||
)
|
||||
|
||||
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
|
||||
|
@ -79,7 +82,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
}
|
||||
const value = ed.getAttributes('footnote').value
|
||||
setFootNote(value)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const handleAddFootnote = (footnote) => {
|
||||
|
@ -148,7 +151,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: textSizeBubbleOpen()
|
||||
[styles.bubbleMenuButtonActive]: textSizeBubbleOpen(),
|
||||
})}
|
||||
onClick={toggleTextSizePopup}
|
||||
>
|
||||
|
@ -165,7 +168,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isH1()
|
||||
[styles.bubbleMenuButtonActive]: isH1(),
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
|
@ -182,7 +185,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isH2()
|
||||
[styles.bubbleMenuButtonActive]: isH2(),
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||
|
@ -199,7 +202,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isH3()
|
||||
[styles.bubbleMenuButtonActive]: isH3(),
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleHeading({ level: 4 }).run()
|
||||
|
@ -219,7 +222,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isQuote()
|
||||
[styles.bubbleMenuButtonActive]: isQuote(),
|
||||
})}
|
||||
onClick={handleSetPunchline}
|
||||
>
|
||||
|
@ -233,7 +236,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isPunchLine()
|
||||
[styles.bubbleMenuButtonActive]: isPunchLine(),
|
||||
})}
|
||||
onClick={handleSetQuote}
|
||||
>
|
||||
|
@ -250,7 +253,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isIncut()
|
||||
[styles.bubbleMenuButtonActive]: isIncut(),
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleArticle().run()
|
||||
|
@ -274,7 +277,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isBold()
|
||||
[styles.bubbleMenuButtonActive]: isBold(),
|
||||
})}
|
||||
onClick={() => props.editor.chain().focus().toggleBold().run()}
|
||||
>
|
||||
|
@ -288,7 +291,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isItalic()
|
||||
[styles.bubbleMenuButtonActive]: isItalic(),
|
||||
})}
|
||||
onClick={() => props.editor.chain().focus().toggleItalic().run()}
|
||||
>
|
||||
|
@ -304,7 +307,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isHighlight()
|
||||
[styles.bubbleMenuButtonActive]: isHighlight(),
|
||||
})}
|
||||
onClick={() => props.editor.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()}
|
||||
>
|
||||
|
@ -321,7 +324,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
type="button"
|
||||
onClick={() => setLinkEditorOpen(true)}
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isLink()
|
||||
[styles.bubbleMenuButtonActive]: isLink(),
|
||||
})}
|
||||
>
|
||||
<Icon name="editor-link" />
|
||||
|
@ -336,7 +339,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isFootnote()
|
||||
[styles.bubbleMenuButtonActive]: isFootnote(),
|
||||
})}
|
||||
onClick={handleOpenFootnoteEditor}
|
||||
>
|
||||
|
@ -349,7 +352,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: listBubbleOpen()
|
||||
[styles.bubbleMenuButtonActive]: listBubbleOpen(),
|
||||
})}
|
||||
onClick={toggleListPopup}
|
||||
>
|
||||
|
@ -366,7 +369,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isBulletList()
|
||||
[styles.bubbleMenuButtonActive]: isBulletList(),
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleBulletList().run()
|
||||
|
@ -383,7 +386,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isOrderedList()
|
||||
[styles.bubbleMenuButtonActive]: isOrderedList(),
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleOrderedList().run()
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import type { Topic } from '../../../graphql/types.gen'
|
||||
|
||||
import { createOptions, Select } from '@thisbeyond/solid-select'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import '@thisbeyond/solid-select/style.css'
|
||||
import './TopicSelect.scss'
|
||||
import styles from './TopicSelect.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { createSignal } from 'solid-js'
|
||||
import { slugify } from '../../../utils/slugify'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { clone } from '../../../utils/clone'
|
||||
import { slugify } from '../../../utils/slugify'
|
||||
|
||||
import '@thisbeyond/solid-select/style.css'
|
||||
import './TopicSelect.scss'
|
||||
|
||||
import styles from './TopicSelect.module.scss'
|
||||
|
||||
type TopicSelectProps = {
|
||||
topics: Topic[]
|
||||
|
@ -33,7 +37,7 @@ export const TopicSelect = (props: TopicSelectProps) => {
|
|||
disable: (topic) => {
|
||||
return props.selectedTopics.some((selectedTopic) => selectedTopic.slug === topic.slug)
|
||||
},
|
||||
createable: createValue
|
||||
createable: createValue,
|
||||
})
|
||||
|
||||
const handleChange = (selectedTopics: Topic[]) => {
|
||||
|
@ -57,7 +61,7 @@ export const TopicSelect = (props: TopicSelectProps) => {
|
|||
return (
|
||||
<div
|
||||
class={clsx(styles.selectedItem, {
|
||||
[styles.mainTopic]: isMainTopic
|
||||
[styles.mainTopic]: isMainTopic,
|
||||
})}
|
||||
onClick={() => handleSelectedItemClick(item)}
|
||||
>
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import styles from './UploadModalContent.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import { InlineForm } from '../InlineForm'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { createDropzone, createFileUploader, UploadFile } from '@solid-primitives/upload'
|
||||
import { clsx } from 'clsx'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
import { verifyImg } from '../../../utils/verifyImg'
|
||||
import { UploadedFile } from '../../../pages/types'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { handleImageUpload } from '../../../utils/handleImageUpload'
|
||||
import { verifyImg } from '../../../utils/verifyImg'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
import { InlineForm } from '../InlineForm'
|
||||
|
||||
import styles from './UploadModalContent.module.scss'
|
||||
|
||||
type Props = {
|
||||
onClose: (image?: UploadedFile) => void
|
||||
|
@ -46,7 +48,7 @@ export const UploadModalContent = (props: Props) => {
|
|||
source: blob.toString(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
file: file
|
||||
file: file,
|
||||
}
|
||||
await runUpload(fileToUpload)
|
||||
} catch (error) {
|
||||
|
@ -70,7 +72,7 @@ export const UploadModalContent = (props: Props) => {
|
|||
} else {
|
||||
setDragError(t('Image format not supported'))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
const handleDrag = (event: MouseEvent) => {
|
||||
if (event.type === 'dragenter' || event.type === 'dragover') {
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './VideoUploader.module.scss'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { createDropzone } from '@solid-primitives/upload'
|
||||
import { createSignal, For, Show } from 'solid-js'
|
||||
import { useSnackbar } from '../../../context/snackbar'
|
||||
import { validateUrl } from '../../../utils/validateUrl'
|
||||
import type { MediaItem } from '../../../pages/types'
|
||||
|
||||
import { createDropzone } from '@solid-primitives/upload'
|
||||
import { clsx } from 'clsx'
|
||||
import { createSignal, For, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useSnackbar } from '../../../context/snackbar'
|
||||
import { composeMediaItems } from '../../../utils/composeMediaItems'
|
||||
import { validateUrl } from '../../../utils/validateUrl'
|
||||
import { VideoPlayer } from '../../_shared/VideoPlayer'
|
||||
|
||||
import styles from './VideoUploader.module.scss'
|
||||
|
||||
type Props = {
|
||||
video: MediaItem[]
|
||||
onVideoAdd: (value: MediaItem[]) => void
|
||||
|
@ -22,13 +25,13 @@ export const VideoUploader = (props: Props) => {
|
|||
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
||||
|
||||
const {
|
||||
actions: { showSnackbar }
|
||||
actions: { showSnackbar },
|
||||
} = useSnackbar()
|
||||
|
||||
const urlInput: {
|
||||
current: HTMLInputElement
|
||||
} = {
|
||||
current: null
|
||||
current: null,
|
||||
}
|
||||
|
||||
const { setRef: dropzoneRef, files: droppedFiles } = createDropzone({
|
||||
|
@ -39,13 +42,13 @@ export const VideoUploader = (props: Props) => {
|
|||
} else if (droppedFiles()[0].file.type.startsWith('video/')) {
|
||||
await showSnackbar({
|
||||
body: t(
|
||||
'This functionality is currently not available, we would like to work on this issue. Use the download link.'
|
||||
)
|
||||
'This functionality is currently not available, we would like to work on this issue. Use the download link.',
|
||||
),
|
||||
})
|
||||
} else {
|
||||
setError(t('Video format not supported'))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
const handleDrag = (event) => {
|
||||
if (event.type === 'dragenter' || event.type === 'dragover') {
|
||||
|
@ -84,8 +87,8 @@ export const VideoUploader = (props: Props) => {
|
|||
onClick={() =>
|
||||
showSnackbar({
|
||||
body: t(
|
||||
'This functionality is currently not available, we would like to work on this issue. Use the download link.'
|
||||
)
|
||||
'This functionality is currently not available, we would like to work on this issue. Use the download link.',
|
||||
),
|
||||
})
|
||||
}
|
||||
ref={dropzoneRef}
|
||||
|
|
|
@ -14,8 +14,8 @@ export default Node.create({
|
|||
name: 'article',
|
||||
defaultOptions: {
|
||||
HTMLAttributes: {
|
||||
'data-type': 'incut'
|
||||
}
|
||||
'data-type': 'incut',
|
||||
},
|
||||
},
|
||||
group: 'block',
|
||||
content: 'block+',
|
||||
|
@ -23,8 +23,8 @@ export default Node.create({
|
|||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'article'
|
||||
}
|
||||
tag: 'article',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
@ -35,11 +35,11 @@ export default Node.create({
|
|||
addAttributes() {
|
||||
return {
|
||||
'data-float': {
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
'data-bg': {
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -60,7 +60,7 @@ export default Node.create({
|
|||
(value) =>
|
||||
({ commands }) => {
|
||||
return commands.updateAttributes(this.name, { 'data-bg': value })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -14,18 +14,18 @@ declare module '@tiptap/core' {
|
|||
export const CustomBlockquote = Blockquote.extend({
|
||||
name: 'blockquote',
|
||||
defaultOptions: {
|
||||
HTMLAttributes: {}
|
||||
HTMLAttributes: {},
|
||||
},
|
||||
group: 'block',
|
||||
content: 'block+',
|
||||
addAttributes() {
|
||||
return {
|
||||
'data-float': {
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
'data-type': {
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
@ -41,7 +41,7 @@ export const CustomBlockquote = Blockquote.extend({
|
|||
(value) =>
|
||||
({ commands }) => {
|
||||
return commands.updateAttributes(this.name, { 'data-float': value })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -16,20 +16,20 @@ export const CustomImage = Image.extend({
|
|||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
alt: {
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
width: {
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
height: {
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
'data-float': {
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
addCommands() {
|
||||
|
@ -39,14 +39,14 @@ export const CustomImage = Image.extend({
|
|||
({ commands }) => {
|
||||
return commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: options
|
||||
attrs: options,
|
||||
})
|
||||
},
|
||||
setImageFloat:
|
||||
(value) =>
|
||||
({ commands }) => {
|
||||
return commands.updateAttributes(this.name, { 'data-float': value })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -25,14 +25,14 @@ export const Embed = Node.create<IframeOptions>({
|
|||
return {
|
||||
src: { default: null },
|
||||
width: { default: null },
|
||||
height: { default: null }
|
||||
height: { default: null },
|
||||
}
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'iframe'
|
||||
}
|
||||
tag: 'iframe',
|
||||
},
|
||||
]
|
||||
},
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
|
@ -49,7 +49,7 @@ export const Embed = Node.create<IframeOptions>({
|
|||
iframe.src = node.attrs.src
|
||||
div.append(iframe)
|
||||
return {
|
||||
dom: div
|
||||
dom: div,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -64,7 +64,7 @@ export const Embed = Node.create<IframeOptions>({
|
|||
tr.replaceRangeWith(selection.from, selection.to, node)
|
||||
}
|
||||
return true
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -12,7 +12,7 @@ export const Figcaption = Node.create({
|
|||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
HTMLAttributes: {},
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -25,8 +25,8 @@ export const Figcaption = Node.create({
|
|||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'figcaption'
|
||||
}
|
||||
tag: 'figcaption',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
@ -39,7 +39,7 @@ export const Figcaption = Node.create({
|
|||
(value) =>
|
||||
({ commands }) => {
|
||||
return commands.focus(value)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -12,7 +12,7 @@ export const Figure = Node.create({
|
|||
name: 'figure',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
HTMLAttributes: {},
|
||||
}
|
||||
},
|
||||
group: 'block',
|
||||
|
@ -22,15 +22,15 @@ export const Figure = Node.create({
|
|||
|
||||
addAttributes() {
|
||||
return {
|
||||
'data-float': null
|
||||
'data-float': null,
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: `figure[data-type="${this.name}"]`
|
||||
}
|
||||
tag: `figure[data-type="${this.name}"]`,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
@ -54,10 +54,10 @@ export const Figure = Node.create({
|
|||
event.preventDefault()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
|
@ -67,7 +67,7 @@ export const Figure = Node.create({
|
|||
(value) =>
|
||||
({ commands }) => {
|
||||
return commands.updateAttributes(this.name, { 'data-float': value })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -14,7 +14,7 @@ export const Footnote = Node.create({
|
|||
name: 'footnote',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
HTMLAttributes: {},
|
||||
}
|
||||
},
|
||||
group: 'inline',
|
||||
|
@ -29,18 +29,18 @@ export const Footnote = Node.create({
|
|||
parseHTML: (element) => element.dataset.value || null,
|
||||
renderHTML: (attributes) => {
|
||||
return {
|
||||
'data-value': attributes.value
|
||||
'data-value': attributes.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'footnote'
|
||||
}
|
||||
tag: 'footnote',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
@ -92,7 +92,7 @@ export const Footnote = Node.create({
|
|||
}
|
||||
|
||||
return false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -22,7 +22,7 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
|
|||
addOptions() {
|
||||
return {
|
||||
node: 'paragraph',
|
||||
notAfter: ['paragraph']
|
||||
notAfter: ['paragraph'],
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -61,9 +61,9 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
|
|||
const lastNode = tr.doc.lastChild
|
||||
|
||||
return !nodeEqualsType({ node: lastNode, types: disabledNodes })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -413,6 +413,7 @@
|
|||
}
|
||||
|
||||
swiper-slide & {
|
||||
aspect-ratio: 16/9;
|
||||
margin-bottom: 0;
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
import { createMemo, createSignal, For, Show } from 'solid-js'
|
||||
import type { Shout } from '../../../graphql/types.gen'
|
||||
import { capitalize } from '../../../utils/capitalize'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { clsx } from 'clsx'
|
||||
import { CardTopic } from '../CardTopic'
|
||||
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
|
||||
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
|
||||
import { getDescription } from '../../../utils/meta'
|
||||
import { FeedArticlePopup } from '../FeedArticlePopup'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
||||
import { getPagePath, openPage } from '@nanostores/router'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import { Image } from '../../_shared/Image'
|
||||
import { clsx } from 'clsx'
|
||||
import { createMemo, createSignal, For, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { capitalize } from '../../../utils/capitalize'
|
||||
import { getDescription } from '../../../utils/meta'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Image } from '../../_shared/Image'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
|
||||
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
|
||||
import { AuthorLink } from '../../Author/AhtorLink'
|
||||
import stylesHeader from '../../Nav/Header/Header.module.scss'
|
||||
import { CardTopic } from '../CardTopic'
|
||||
import { FeedArticlePopup } from '../FeedArticlePopup'
|
||||
|
||||
import styles from './ArticleCard.module.scss'
|
||||
import stylesHeader from '../../Nav/Header/Header.module.scss'
|
||||
|
||||
interface ArticleCardProps {
|
||||
settings?: {
|
||||
|
@ -45,7 +48,7 @@ interface ArticleCardProps {
|
|||
}
|
||||
|
||||
const getTitleAndSubtitle = (
|
||||
article: Shout
|
||||
article: Shout,
|
||||
): {
|
||||
title: string
|
||||
subtitle: string
|
||||
|
@ -90,7 +93,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
event.preventDefault()
|
||||
openPage(router, 'article', { slug: props.article.slug })
|
||||
changeSearchParam({
|
||||
scrollTo: 'comments'
|
||||
scrollTo: 'comments',
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -111,7 +114,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
[styles.shoutCardCompact]: props.settings?.isCompact,
|
||||
[styles.shoutCardSingle]: props.settings?.isSingle,
|
||||
[styles.shoutCardBeside]: props.settings?.isBeside,
|
||||
[styles.shoutCardNoImage]: !props.article.cover
|
||||
[styles.shoutCardNoImage]: !props.article.cover,
|
||||
}}
|
||||
>
|
||||
<Show when={!props.settings?.noimage && !props.settings?.isFeedMode}>
|
||||
|
@ -155,7 +158,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
|
||||
<div
|
||||
class={clsx(styles.shoutCardTitlesContainer, {
|
||||
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode
|
||||
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode,
|
||||
})}
|
||||
>
|
||||
<a href={getPagePath(router, 'article', { slug: props.article.slug })}>
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
// TODO: additional entities list column + article
|
||||
|
||||
import { For, Show } from 'solid-js'
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
import { TopicCard } from '../Topic/Card'
|
||||
import styles from './Beside.module.scss'
|
||||
import type { Author, Shout, Topic, User } from '../../graphql/types.gen'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { For, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
import { TopicCard } from '../Topic/Card'
|
||||
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
|
||||
import styles from './Beside.module.scss'
|
||||
|
||||
type Props = {
|
||||
title?: string
|
||||
|
@ -36,7 +40,7 @@ export const Beside = (props: Props) => {
|
|||
'col-lg-8',
|
||||
styles[
|
||||
`besideRatingColumn${props.wrapper.charAt(0).toUpperCase() + props.wrapper.slice(1)}`
|
||||
]
|
||||
],
|
||||
)}
|
||||
>
|
||||
<Show when={!!props.title}>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import { router } from '../../stores/router'
|
||||
|
||||
import styles from './CardTopic.module.scss'
|
||||
|
||||
type CardTopicProps = {
|
||||
|
@ -16,7 +18,7 @@ export const CardTopic = (props: CardTopicProps) => {
|
|||
<div
|
||||
class={clsx(styles.shoutTopic, props.class, {
|
||||
[styles.shoutTopicFloorImportant]: props.isFloorImportant,
|
||||
[styles.shoutTopicFeedMode]: props.isFeedMode
|
||||
[styles.shoutTopicFeedMode]: props.isFeedMode,
|
||||
})}
|
||||
>
|
||||
<a href={getPagePath(router, 'topic', { slug: props.slug })}>{props.title}</a>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import styles from './FeedArticlePopup.module.scss'
|
||||
import type { PopupProps } from '../_shared/Popup'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
|
||||
import { createEffect, createSignal, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
|
||||
import styles from './FeedArticlePopup.module.scss'
|
||||
|
||||
type FeedArticlePopupProps = {
|
||||
title: string
|
||||
shareUrl?: string
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import type { JSX } from 'solid-js/jsx-runtime'
|
||||
import { For, Show } from 'solid-js'
|
||||
import type { Shout } from '../../graphql/types.gen'
|
||||
import type { JSX } from 'solid-js/jsx-runtime'
|
||||
|
||||
import { For, Show } from 'solid-js'
|
||||
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
import './Group.scss'
|
||||
|
||||
|
@ -26,7 +28,7 @@ export default (props: GroupProps) => {
|
|||
noicon: true,
|
||||
isFloorImportant: true,
|
||||
isBigTitle: true,
|
||||
nodate: true
|
||||
nodate: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -59,7 +61,7 @@ export default (props: GroupProps) => {
|
|||
isBigTitle: true,
|
||||
isCompact: true,
|
||||
isFloorImportant: true,
|
||||
nodate: true
|
||||
nodate: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -76,7 +78,7 @@ export default (props: GroupProps) => {
|
|||
isBigTitle: true,
|
||||
isCompact: true,
|
||||
isFloorImportant: true,
|
||||
nodate: true
|
||||
nodate: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { Show } from 'solid-js'
|
||||
import type { Shout } from '../../graphql/types.gen'
|
||||
|
||||
import { Show } from 'solid-js'
|
||||
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
|
||||
export const Row1 = (props: {
|
||||
|
@ -19,7 +21,7 @@ export const Row1 = (props: {
|
|||
isSingle: true,
|
||||
nodate: props.nodate,
|
||||
noAuthorLink: props.noAuthorLink,
|
||||
noauthor: props.noauthor
|
||||
noauthor: props.noauthor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { createComputed, createSignal, Show, For } from 'solid-js'
|
||||
import type { Shout } from '../../graphql/types.gen'
|
||||
|
||||
import { createComputed, createSignal, Show, For } from 'solid-js'
|
||||
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
|
||||
const x = [
|
||||
['12', '12'],
|
||||
['8', '16'],
|
||||
['16', '8']
|
||||
['16', '8'],
|
||||
]
|
||||
|
||||
export const Row2 = (props: {
|
||||
|
@ -35,7 +37,7 @@ export const Row2 = (props: {
|
|||
isWithCover: props.isEqual || x[y()][i()] === '16',
|
||||
nodate: props.isEqual || props.nodate,
|
||||
noAuthorLink: props.noAuthorLink,
|
||||
noauthor: props.noauthor
|
||||
noauthor: props.noauthor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import type { JSX } from 'solid-js/jsx-runtime'
|
||||
import { For, Show } from 'solid-js'
|
||||
import type { Shout } from '../../graphql/types.gen'
|
||||
import type { JSX } from 'solid-js/jsx-runtime'
|
||||
|
||||
import { For, Show } from 'solid-js'
|
||||
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
|
||||
export const Row3 = (props: {
|
||||
|
@ -24,7 +26,7 @@ export const Row3 = (props: {
|
|||
settings={{
|
||||
nodate: props.nodate,
|
||||
noAuthorLink: props.noAuthorLink,
|
||||
noauthor: props.noauthor
|
||||
noauthor: props.noauthor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { Shout } from '../../graphql/types.gen'
|
||||
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
|
||||
export const Row5 = (props: { articles: Shout[]; nodate?: boolean }) => {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { For } from 'solid-js'
|
||||
import type { Shout } from '../../graphql/types.gen'
|
||||
|
||||
import { For } from 'solid-js'
|
||||
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
|
||||
export default (props: { articles: Shout[] }) => (
|
||||
|
@ -16,7 +18,7 @@ export default (props: { articles: Shout[] }) => (
|
|||
isWithCover: true,
|
||||
isBigTitle: true,
|
||||
isVertical: true,
|
||||
nodate: true
|
||||
nodate: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -19,11 +19,16 @@
|
|||
align-items: center;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sidebarItemNameLabel {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.userpic {
|
||||
margin-right: 1.2rem;
|
||||
}
|
||||
|
||||
.userpic {
|
||||
|
@ -109,7 +114,6 @@
|
|||
display: inline-block;
|
||||
line-height: 1;
|
||||
height: 2.4rem;
|
||||
margin-bottom: 0.2rem;
|
||||
margin-right: 0.8rem;
|
||||
min-width: 2.4rem;
|
||||
text-align: center;
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { getPagePath } from '@nanostores/router'
|
||||
import { clsx } from 'clsx'
|
||||
import { createSignal, For, Show } from 'solid-js'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { useArticlesStore } from '../../../stores/zine/articles'
|
||||
import { useSeenStore } from '../../../stores/zine/seen'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import styles from './Sidebar.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Userpic } from '../../Author/Userpic'
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
|
||||
import styles from './Sidebar.module.scss'
|
||||
|
||||
export const Sidebar = () => {
|
||||
const { t } = useLocalize()
|
||||
|
@ -33,7 +35,7 @@ export const Sidebar = () => {
|
|||
<a
|
||||
href={getPagePath(router, 'feed')}
|
||||
class={clsx({
|
||||
[styles.selected]: page().route === 'feed'
|
||||
[styles.selected]: page().route === 'feed',
|
||||
})}
|
||||
>
|
||||
<span class={styles.sidebarItemName}>
|
||||
|
@ -46,7 +48,7 @@ export const Sidebar = () => {
|
|||
<a
|
||||
href={getPagePath(router, 'feedMy')}
|
||||
class={clsx({
|
||||
[styles.selected]: page().route === 'feedMy'
|
||||
[styles.selected]: page().route === 'feedMy',
|
||||
})}
|
||||
>
|
||||
<span class={styles.sidebarItemName}>
|
||||
|
@ -59,7 +61,7 @@ export const Sidebar = () => {
|
|||
<a
|
||||
href={getPagePath(router, 'feedCollaborations')}
|
||||
class={clsx({
|
||||
[styles.selected]: page().route === 'feedCollaborations'
|
||||
[styles.selected]: page().route === 'feedCollaborations',
|
||||
})}
|
||||
>
|
||||
<span class={styles.sidebarItemName}>
|
||||
|
@ -72,7 +74,7 @@ export const Sidebar = () => {
|
|||
<a
|
||||
href={getPagePath(router, 'feedDiscussions')}
|
||||
class={clsx({
|
||||
[styles.selected]: page().route === 'feedDiscussions'
|
||||
[styles.selected]: page().route === 'feedDiscussions',
|
||||
})}
|
||||
>
|
||||
<span class={styles.sidebarItemName}>
|
||||
|
@ -85,7 +87,7 @@ export const Sidebar = () => {
|
|||
<a
|
||||
href={getPagePath(router, 'feedBookmarks')}
|
||||
class={clsx({
|
||||
[styles.selected]: page().route === 'feedBookmarks'
|
||||
[styles.selected]: page().route === 'feedBookmarks',
|
||||
})}
|
||||
>
|
||||
<span class={styles.sidebarItemName}>
|
||||
|
@ -98,7 +100,7 @@ export const Sidebar = () => {
|
|||
<a
|
||||
href={getPagePath(router, 'feedNotifications')}
|
||||
class={clsx({
|
||||
[styles.selected]: page().route === 'feedNotifications'
|
||||
[styles.selected]: page().route === 'feedNotifications',
|
||||
})}
|
||||
>
|
||||
<span class={styles.sidebarItemName}>
|
||||
|
@ -129,7 +131,7 @@ export const Sidebar = () => {
|
|||
>
|
||||
<div class={styles.sidebarItemName}>
|
||||
<Userpic name={author.name} userpic={author.userpic} size="XS" class={styles.userpic} />
|
||||
{author.name}
|
||||
<div class={styles.sidebarItemNameLabel}>{author.name}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -144,7 +146,7 @@ export const Sidebar = () => {
|
|||
>
|
||||
<div class={styles.sidebarItemName}>
|
||||
<Icon name="hash" class={styles.icon} />
|
||||
{topic.title}
|
||||
<div class={styles.sidebarItemNameLabel}>{topic.title}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { createSignal, For, createEffect } from 'solid-js'
|
||||
import styles from './CreateModalContent.module.scss'
|
||||
|
||||
import InviteUser from './InviteUser'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
import { hideModal } from '../../stores/ui'
|
||||
|
||||
import { createSignal, For, createEffect } from 'solid-js'
|
||||
|
||||
import { useInbox } from '../../context/inbox'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { hideModal } from '../../stores/ui'
|
||||
|
||||
import InviteUser from './InviteUser'
|
||||
|
||||
import styles from './CreateModalContent.module.scss'
|
||||
|
||||
type inviteUser = Author & { selected: boolean }
|
||||
type Props = {
|
||||
|
@ -46,7 +49,7 @@ const CreateModalContent = (props: Props) => {
|
|||
const handleClick = (user) => {
|
||||
setCollectionToInvite((userCollection) => {
|
||||
return userCollection.map((clickedUser) =>
|
||||
user.id === clickedUser.id ? { ...clickedUser, selected: !clickedUser.selected } : clickedUser
|
||||
user.id === clickedUser.id ? { ...clickedUser, selected: !clickedUser.selected } : clickedUser,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { Show, createMemo } from 'solid-js'
|
||||
import './DialogCard.module.scss'
|
||||
import styles from './DialogAvatar.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { Show, createMemo } from 'solid-js'
|
||||
|
||||
import { getImageUrl } from '../../utils/getImageUrl'
|
||||
import './DialogCard.module.scss'
|
||||
|
||||
import styles from './DialogAvatar.module.scss'
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
|
@ -25,7 +27,7 @@ const colors = [
|
|||
'#668cff',
|
||||
'#c34cfe',
|
||||
'#e699ff',
|
||||
'#6633ff'
|
||||
'#6633ff',
|
||||
]
|
||||
|
||||
const getById = (letter: string) =>
|
||||
|
@ -42,7 +44,7 @@ const DialogAvatar = (props: Props) => {
|
|||
class={clsx(styles.DialogAvatar, props.class, {
|
||||
[styles.online]: props.online,
|
||||
[styles.bordered]: props.bordered,
|
||||
[styles.small]: props.size === 'small'
|
||||
[styles.small]: props.size === 'small',
|
||||
})}
|
||||
style={{ 'background-color': `${randomBg()}` }}
|
||||
>
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { Show, Switch, Match, createMemo, createEffect } from 'solid-js'
|
||||
import DialogAvatar from './DialogAvatar'
|
||||
import type { ChatMember } from '../../graphql/types.gen'
|
||||
import GroupDialogAvatar from './GroupDialogAvatar'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import styles from './DialogCard.module.scss'
|
||||
import { Show, Switch, Match, createMemo } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
|
||||
import DialogAvatar from './DialogAvatar'
|
||||
import GroupDialogAvatar from './GroupDialogAvatar'
|
||||
|
||||
import styles from './DialogCard.module.scss'
|
||||
|
||||
type DialogProps = {
|
||||
online?: boolean
|
||||
message?: string
|
||||
|
@ -22,14 +26,14 @@ type DialogProps = {
|
|||
const DialogCard = (props: DialogProps) => {
|
||||
const { t, formatTime } = useLocalize()
|
||||
const companions = createMemo(
|
||||
() => props.members && props.members.filter((member) => member.id !== props.ownId)
|
||||
() => props.members && props.members.filter((member) => member.id !== props.ownId),
|
||||
)
|
||||
|
||||
const names = createMemo(
|
||||
() =>
|
||||
companions()
|
||||
?.map((companion) => companion.name)
|
||||
.join(', ')
|
||||
.join(', '),
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -37,24 +41,27 @@ const DialogCard = (props: DialogProps) => {
|
|||
<div
|
||||
class={clsx(styles.DialogCard, {
|
||||
[styles.opened]: props.isOpened,
|
||||
[styles.hovered]: !props.isChatHeader
|
||||
[styles.hovered]: !props.isChatHeader,
|
||||
})}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={props.members.length === 2}>
|
||||
<div class={styles.avatar}>
|
||||
<Show
|
||||
when={props.isChatHeader}
|
||||
fallback={<DialogAvatar name={companions()[0].slug} url={companions()[0].userpic} />}
|
||||
>
|
||||
<AuthorBadge nameOnly={true} author={companions()[0]} />
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
<Switch
|
||||
fallback={
|
||||
<Show
|
||||
when={props.isChatHeader}
|
||||
fallback={
|
||||
<div class={styles.avatar}>
|
||||
<DialogAvatar name={props.members[0].slug} url={props.members[0].userpic} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AuthorBadge nameOnly={true} author={props.members[0]} />
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<Match when={props.members.length >= 3}>
|
||||
<div class={styles.avatar}>
|
||||
<GroupDialogAvatar users={companions()} />
|
||||
<GroupDialogAvatar users={props.members} />
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import type { Chat } from '../../graphql/types.gen'
|
||||
import styles from './DialogHeader.module.scss'
|
||||
|
||||
import DialogCard from './DialogCard'
|
||||
|
||||
import styles from './DialogHeader.module.scss'
|
||||
|
||||
type DialogHeader = {
|
||||
chat: Chat
|
||||
ownId: number
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { For } from 'solid-js'
|
||||
import './DialogCard.module.scss'
|
||||
import styles from './GroupDialogAvatar.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import type { ChatMember } from '../../graphql/types.gen'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { For } from 'solid-js'
|
||||
|
||||
import DialogAvatar from './DialogAvatar'
|
||||
|
||||
import './DialogCard.module.scss'
|
||||
|
||||
import styles from './GroupDialogAvatar.module.scss'
|
||||
|
||||
type Props = {
|
||||
users: ChatMember[]
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import styles from './InviteUser.module.scss'
|
||||
import DialogAvatar from './DialogAvatar'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
import DialogAvatar from './DialogAvatar'
|
||||
|
||||
import styles from './InviteUser.module.scss'
|
||||
|
||||
type DialogProps = {
|
||||
author: Author
|
||||
selected: boolean
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { createSignal, Show } from 'solid-js'
|
||||
import { clsx } from 'clsx'
|
||||
import styles from './Message.module.scss'
|
||||
import DialogAvatar from './DialogAvatar'
|
||||
import type { Message as MessageType, ChatMember } from '../../graphql/types.gen'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
import DialogAvatar from './DialogAvatar'
|
||||
import { MessageActionsPopup } from './MessageActionsPopup'
|
||||
import QuotedMessage from './QuotedMessage'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
|
||||
import styles from './Message.module.scss'
|
||||
|
||||
type Props = {
|
||||
content: MessageType
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { createEffect, createSignal, For } from 'solid-js'
|
||||
import type { PopupProps } from '../_shared/Popup'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
|
||||
import { createEffect, createSignal, For } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
|
||||
export type MessageActionType = 'reply' | 'copy' | 'pin' | 'forward' | 'select' | 'delete'
|
||||
|
||||
|
@ -18,7 +20,7 @@ export const MessageActionsPopup = (props: MessageActionsPopupProps) => {
|
|||
{ name: t('Pin'), action: 'pin' },
|
||||
{ name: t('Forward'), action: 'forward' },
|
||||
{ name: t('Select'), action: 'select' },
|
||||
{ name: t('Delete'), action: 'delete' }
|
||||
{ name: t('Delete'), action: 'delete' },
|
||||
]
|
||||
createEffect(() => {
|
||||
if (props.actionSelect) props.actionSelect(selectedAction())
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Show } from 'solid-js'
|
||||
|
||||
import styles from './MessagesFallback.module.scss'
|
||||
|
||||
type MessagesFallback = {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Show } from 'solid-js'
|
||||
import styles from './QuotedMessage.module.scss'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { clsx } from 'clsx'
|
||||
import { Show } from 'solid-js'
|
||||
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
import styles from './QuotedMessage.module.scss'
|
||||
|
||||
type QuotedMessage = {
|
||||
body: string
|
||||
|
@ -17,7 +19,7 @@ const QuotedMessage = (props: QuotedMessage) => {
|
|||
class={clsx(styles.QuotedMessage, {
|
||||
[styles.reply]: props.variant === 'reply',
|
||||
[styles.inline]: props.variant === 'inline',
|
||||
[styles.own]: props.isOwn
|
||||
[styles.own]: props.isOwn,
|
||||
})}
|
||||
>
|
||||
<Show when={props.variant === 'reply'}>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import styles from './Search.module.scss'
|
||||
import { createSignal } from 'solid-js'
|
||||
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
import styles from './Search.module.scss'
|
||||
|
||||
type Props = {
|
||||
placeholder: string
|
||||
onChange: (value: () => string) => void
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import styles from './AuthModalHeader.module.scss'
|
||||
import { Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../../context/localize'
|
||||
import { useRouter } from '../../../../stores/router'
|
||||
import { AuthModalSearchParams } from '../types'
|
||||
|
||||
import styles from './AuthModalHeader.module.scss'
|
||||
|
||||
type Props = {
|
||||
modalType: 'login' | 'register'
|
||||
}
|
||||
|
@ -14,7 +16,7 @@ export const AuthModalHeader = (props: Props) => {
|
|||
const { source } = searchParams()
|
||||
|
||||
const generateModalTextsFromSource = (
|
||||
modalType: 'login' | 'register'
|
||||
modalType: 'login' | 'register',
|
||||
): { title: string; description: string } => {
|
||||
const title = modalType === 'login' ? 'Welcome to Discours' : 'Create account'
|
||||
|
||||
|
@ -22,53 +24,53 @@ export const AuthModalHeader = (props: Props) => {
|
|||
case 'create': {
|
||||
return {
|
||||
title: t(`${title} to publish articles`),
|
||||
description: ''
|
||||
description: '',
|
||||
}
|
||||
}
|
||||
case 'bookmark': {
|
||||
return {
|
||||
title: t(`${title} to add to your bookmarks`),
|
||||
description: t(
|
||||
'In bookmarks, you can save favorite discussions and materials that you want to return to'
|
||||
)
|
||||
'In bookmarks, you can save favorite discussions and materials that you want to return to',
|
||||
),
|
||||
}
|
||||
}
|
||||
case 'discussions': {
|
||||
return {
|
||||
title: t(`${title} to participate in discussions`),
|
||||
description: t(
|
||||
"You ll be able to participate in discussions, rate others' comments and learn about new responses"
|
||||
)
|
||||
"You ll be able to participate in discussions, rate others' comments and learn about new responses",
|
||||
),
|
||||
}
|
||||
}
|
||||
case 'follow': {
|
||||
return {
|
||||
title: t(`${title} to subscribe`),
|
||||
description: t(
|
||||
'This way you ll be able to subscribe to authors, interesting topics and customize your feed'
|
||||
)
|
||||
'This way you ll be able to subscribe to authors, interesting topics and customize your feed',
|
||||
),
|
||||
}
|
||||
}
|
||||
case 'subscribe': {
|
||||
return {
|
||||
title: t(`${title} to subscribe to new publications`),
|
||||
description: t(
|
||||
'This way you ll be able to subscribe to authors, interesting topics and customize your feed'
|
||||
)
|
||||
'This way you ll be able to subscribe to authors, interesting topics and customize your feed',
|
||||
),
|
||||
}
|
||||
}
|
||||
case 'vote': {
|
||||
return {
|
||||
title: t(`${title} to vote`),
|
||||
description: t(
|
||||
'This way we ll realize that you re a real person and ll take your vote into account. And you ll see how others voted'
|
||||
)
|
||||
'This way we ll realize that you re a real person and ll take your vote into account. And you ll see how others voted',
|
||||
),
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
title: t(title),
|
||||
description: ''
|
||||
description: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import styles from './AuthModal.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import type { ConfirmEmailSearchParams } from './types'
|
||||
import { ApiError } from '../../../utils/apiClient'
|
||||
import { useSession } from '../../../context/session'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { ApiError } from '../../../utils/apiClient'
|
||||
|
||||
import styles from './AuthModal.module.scss'
|
||||
|
||||
export const EmailConfirm = () => {
|
||||
const { t } = useLocalize()
|
||||
const {
|
||||
session,
|
||||
actions: { confirmEmail }
|
||||
actions: { confirmEmail },
|
||||
} = useSession()
|
||||
|
||||
const [isTokenExpired, setIsTokenExpired] = createSignal(false)
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import styles from './AuthModal.module.scss'
|
||||
import type { AuthModalSearchParams } from './types'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { createSignal, JSX, Show } from 'solid-js'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import { email, setEmail } from './sharedLogic'
|
||||
import type { AuthModalSearchParams } from './types'
|
||||
import { ApiError } from '../../../utils/apiClient'
|
||||
import { signSendLink } from '../../../stores/auth'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { signSendLink } from '../../../stores/auth'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import { ApiError } from '../../../utils/apiClient'
|
||||
import { validateEmail } from '../../../utils/validateEmail'
|
||||
|
||||
import { email, setEmail } from './sharedLogic'
|
||||
|
||||
import styles from './AuthModal.module.scss'
|
||||
|
||||
type FormFields = {
|
||||
email: string
|
||||
}
|
||||
|
@ -83,7 +87,7 @@ export const ForgotPasswordForm = () => {
|
|||
|
||||
<div
|
||||
class={clsx('pretty-form__item', {
|
||||
'pretty-form__item--error': validationErrors().email
|
||||
'pretty-form__item--error': validationErrors().email,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
|
@ -116,7 +120,7 @@ export const ForgotPasswordForm = () => {
|
|||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
changeSearchParam({
|
||||
mode: 'register'
|
||||
mode: 'register',
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
@ -138,7 +142,7 @@ export const ForgotPasswordForm = () => {
|
|||
class={styles.authLink}
|
||||
onClick={() =>
|
||||
changeSearchParam({
|
||||
mode: 'login'
|
||||
mode: 'login',
|
||||
})
|
||||
}
|
||||
>
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import styles from './AuthModal.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { SocialProviders } from './SocialProviders'
|
||||
import { ApiError } from '../../../utils/apiClient'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import { email, setEmail } from './sharedLogic'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import type { AuthModalSearchParams } from './types'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { signSendLink } from '../../../stores/auth'
|
||||
import { validateEmail } from '../../../utils/validateEmail'
|
||||
import { useSnackbar } from '../../../context/snackbar'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { useSnackbar } from '../../../context/snackbar'
|
||||
import { signSendLink } from '../../../stores/auth'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { ApiError } from '../../../utils/apiClient'
|
||||
import { validateEmail } from '../../../utils/validateEmail'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import { AuthModalHeader } from './AuthModalHeader'
|
||||
import { email, setEmail } from './sharedLogic'
|
||||
import { SocialProviders } from './SocialProviders'
|
||||
|
||||
import styles from './AuthModal.module.scss'
|
||||
|
||||
type FormFields = {
|
||||
email: string
|
||||
|
@ -36,11 +40,11 @@ export const LoginForm = () => {
|
|||
const authFormRef: { current: HTMLFormElement } = { current: null }
|
||||
|
||||
const {
|
||||
actions: { showSnackbar }
|
||||
actions: { showSnackbar },
|
||||
} = useSnackbar()
|
||||
|
||||
const {
|
||||
actions: { signIn }
|
||||
actions: { signIn },
|
||||
} = useSession()
|
||||
|
||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||
|
@ -144,7 +148,7 @@ export const LoginForm = () => {
|
|||
</Show>
|
||||
<div
|
||||
class={clsx('pretty-form__item', {
|
||||
'pretty-form__item--error': validationErrors().email
|
||||
'pretty-form__item--error': validationErrors().email,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
|
@ -164,7 +168,7 @@ export const LoginForm = () => {
|
|||
|
||||
<div
|
||||
class={clsx('pretty-form__item', {
|
||||
'pretty-form__item--error': validationErrors().password
|
||||
'pretty-form__item--error': validationErrors().password,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
|
@ -198,7 +202,7 @@ export const LoginForm = () => {
|
|||
class="link"
|
||||
onClick={() =>
|
||||
changeSearchParam({
|
||||
mode: 'forgot-password'
|
||||
mode: 'forgot-password',
|
||||
})
|
||||
}
|
||||
>
|
||||
|
@ -215,7 +219,7 @@ export const LoginForm = () => {
|
|||
class={styles.authLink}
|
||||
onClick={() =>
|
||||
changeSearchParam({
|
||||
mode: 'register'
|
||||
mode: 'register',
|
||||
})
|
||||
}
|
||||
>
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
import { Show, createSignal } from 'solid-js'
|
||||
import type { JSX } from 'solid-js'
|
||||
import styles from './AuthModal.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { SocialProviders } from './SocialProviders'
|
||||
import { ApiError } from '../../../utils/apiClient'
|
||||
import { email, setEmail } from './sharedLogic'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import type { AuthModalSearchParams } from './types'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
|
||||
import { register } from '../../../stores/auth'
|
||||
import type { JSX } from 'solid-js'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { Show, createSignal } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { register } from '../../../stores/auth'
|
||||
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { ApiError } from '../../../utils/apiClient'
|
||||
import { validateEmail } from '../../../utils/validateEmail'
|
||||
import { AuthModalHeader } from './AuthModalHeader'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import { AuthModalHeader } from './AuthModalHeader'
|
||||
import { email, setEmail } from './sharedLogic'
|
||||
import { SocialProviders } from './SocialProviders'
|
||||
|
||||
import styles from './AuthModal.module.scss'
|
||||
|
||||
type FormFields = {
|
||||
fullName: string
|
||||
email: string
|
||||
|
@ -126,7 +130,7 @@ export const RegisterForm = () => {
|
|||
await register({
|
||||
name: cleanName,
|
||||
email: cleanEmail,
|
||||
password: password()
|
||||
password: password(),
|
||||
})
|
||||
|
||||
setIsSuccess(true)
|
||||
|
@ -156,7 +160,7 @@ export const RegisterForm = () => {
|
|||
</Show>
|
||||
<div
|
||||
class={clsx('pretty-form__item', {
|
||||
'pretty-form__item--error': validationErrors().fullName
|
||||
'pretty-form__item--error': validationErrors().fullName,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
|
@ -174,7 +178,7 @@ export const RegisterForm = () => {
|
|||
|
||||
<div
|
||||
class={clsx('pretty-form__item', {
|
||||
'pretty-form__item--error': validationErrors().email
|
||||
'pretty-form__item--error': validationErrors().email,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
|
@ -199,7 +203,7 @@ export const RegisterForm = () => {
|
|||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
changeSearchParam({
|
||||
mode: 'login'
|
||||
mode: 'login',
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
@ -211,7 +215,7 @@ export const RegisterForm = () => {
|
|||
|
||||
<div
|
||||
class={clsx('pretty-form__item', {
|
||||
'pretty-form__item--error': validationErrors().password
|
||||
'pretty-form__item--error': validationErrors().password,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
|
@ -252,7 +256,7 @@ export const RegisterForm = () => {
|
|||
class={styles.authLink}
|
||||
onClick={() =>
|
||||
changeSearchParam({
|
||||
mode: 'login'
|
||||
mode: 'login',
|
||||
})
|
||||
}
|
||||
>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Icon } from '../../_shared/Icon'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { apiBaseUrl } from '../../../utils/config'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import styles from './SocialProviders.module.scss'
|
||||
import { apiBaseUrl } from '../../../utils/config'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
||||
type Provider = 'facebook' | 'google' | 'vk' | 'github'
|
||||
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
import { Dynamic } from 'solid-js/web'
|
||||
import { Show, Component, createEffect, createMemo } from 'solid-js'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import { clsx } from 'clsx'
|
||||
import styles from './AuthModal.module.scss'
|
||||
import { LoginForm } from './LoginForm'
|
||||
import { isMobile } from '../../../utils/media-query'
|
||||
import { RegisterForm } from './RegisterForm'
|
||||
import { ForgotPasswordForm } from './ForgotPasswordForm'
|
||||
import { EmailConfirm } from './EmailConfirm'
|
||||
import type { AuthModalMode, AuthModalSearchParams } from './types'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { Show, Component, createEffect, createMemo } from 'solid-js'
|
||||
import { Dynamic } from 'solid-js/web'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { isMobile } from '../../../utils/media-query'
|
||||
|
||||
import { EmailConfirm } from './EmailConfirm'
|
||||
import { ForgotPasswordForm } from './ForgotPasswordForm'
|
||||
import { LoginForm } from './LoginForm'
|
||||
import { RegisterForm } from './RegisterForm'
|
||||
|
||||
import styles from './AuthModal.module.scss'
|
||||
|
||||
const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = {
|
||||
login: LoginForm,
|
||||
register: RegisterForm,
|
||||
'forgot-password': ForgotPasswordForm,
|
||||
'confirm-email': EmailConfirm
|
||||
'confirm-email': EmailConfirm,
|
||||
}
|
||||
|
||||
export const AuthModal = () => {
|
||||
|
@ -40,7 +44,7 @@ export const AuthModal = () => {
|
|||
<div
|
||||
ref={rootRef}
|
||||
class={clsx(styles.view, {
|
||||
row: !source
|
||||
row: !source,
|
||||
})}
|
||||
classList={{ [styles.signUp]: mode() === 'register' || mode() === 'confirm-email' }}
|
||||
>
|
||||
|
@ -54,7 +58,7 @@ export const AuthModal = () => {
|
|||
<h4>{t(`Join the global community of authors!`)}</h4>
|
||||
<p class={styles.authBenefits}>
|
||||
{t(
|
||||
'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine'
|
||||
'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine',
|
||||
)}
|
||||
.
|
||||
{t('New stories every day and even more!')}
|
||||
|
@ -77,7 +81,7 @@ export const AuthModal = () => {
|
|||
</Show>
|
||||
<div
|
||||
class={clsx(styles.auth, {
|
||||
'col-md-12': !source
|
||||
'col-md-12': !source,
|
||||
})}
|
||||
>
|
||||
<Dynamic component={AUTH_MODAL_MODES[mode()]} />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useConfirm } from '../../../context/confirm'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Button } from '../../_shared/Button'
|
||||
|
||||
import styles from './ConfirmModal.module.scss'
|
||||
|
||||
export const ConfirmModal = () => {
|
||||
|
@ -8,7 +9,7 @@ export const ConfirmModal = () => {
|
|||
|
||||
const {
|
||||
confirmMessage,
|
||||
actions: { resolveConfirm }
|
||||
actions: { resolveConfirm },
|
||||
} = useConfirm()
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import './Confirmed.scss'
|
||||
import { onMount } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
|
||||
export const Confirmed = (props: { token?: string }) => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user