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