This commit is contained in:
Untone 2023-11-17 18:08:52 +03:00
commit 8d0a6269e1
245 changed files with 3150 additions and 23268 deletions

View File

@ -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
} }
} }
} };

View File

@ -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"
} }

View File

@ -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
View 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 })
}

View File

@ -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 {

View File

@ -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,
}) })
} }
} }

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to": "In&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to", "In&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to": "In&nbsp;bookmarks, you can save favorite discussions and&nbsp;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"
} }

View File

@ -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&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to": "В&nbsp;закладках можно сохранять избранные дискуссии и&nbsp;материалы, к&nbsp;которым хочется вернуться", "In&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to": "В&nbsp;закладках можно сохранять избранные дискуссии и&nbsp;материалы, к&nbsp;которым хочется вернуться",
"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": "К новым сообщениям"
} }

View File

@ -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>
) )
} }

View File

@ -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;

View File

@ -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

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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;

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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)}

View File

@ -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>

View File

@ -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()} />
}

View File

@ -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 () => {

View File

@ -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')
} }

View File

@ -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,
) )
} }
} }

View File

@ -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'

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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}
> >

View File

@ -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()

View File

@ -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));
}
*/

View File

@ -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&thinsp; 250&thinsp;
</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&thinsp; 500&thinsp;
</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&thinsp; 1000&thinsp;
</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>
) )

View File

@ -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()
} }

View File

@ -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')} &copy; 2015&ndash;{new Date().getFullYear()}{' '} . {t('Discours')} &copy; 2015&ndash;{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>

View File

@ -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',
}) })
}} }}
> >

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(),
}) })
} }
}) })

View File

@ -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) => {

View File

@ -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'

View File

@ -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;

View File

@ -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

View File

@ -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()) {

View File

@ -0,0 +1,4 @@
.LinkBubbleMenu {
background: var(--editor-bubble-menu-background);
box-shadow: 0 4px 10px rgba(#000, 0.25);
}

View File

@ -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>
)
}

View File

@ -0,0 +1 @@
export { LinkBubbleMenuModule } from './LinkBubbleMenu.module'

View File

@ -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(() => {

View File

@ -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>
) )
} }

View File

@ -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()

View File

@ -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)}
> >

View File

@ -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') {

View File

@ -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}

View File

@ -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 })
},
} }
} },
}
}) })

View File

@ -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 })
},
} }
} },
}
}) })

View File

@ -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 })
},
} }
} },
}
}) })

View File

@ -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
},
} }
} },
}
}) })

View File

@ -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)
},
} }
} },
}
}) })

View File

@ -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 })
},
} }
} },
}
}) })

View File

@ -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
},
} }
} },
}
}) })

View File

@ -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 })
} },
} },
}) }),
] ]
} },
}) })

View File

@ -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) {

View File

@ -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 })}>

View File

@ -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}>

View File

@ -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>

View File

@ -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

View File

@ -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,
}} }}
/> />
)} )}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 }) => {

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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,
) )
}) })
} }

View File

@ -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()}` }}
> >

View File

@ -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>

View File

@ -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

View File

@ -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[]
} }

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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 = {

View File

@ -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'}>

View File

@ -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

View File

@ -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&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to' 'In&nbsp;bookmarks, you can save favorite discussions and&nbsp;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&nbsp;ll be able to participate in&nbsp;discussions, rate others' comments and&nbsp;learn about&nbsp;new responses" "You&nbsp;ll be able to participate in&nbsp;discussions, rate others' comments and&nbsp;learn about&nbsp;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&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed' 'This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;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&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed' 'This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;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&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted' 'This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted',
) ),
} }
} }
default: { default: {
return { return {
title: t(title), title: t(title),
description: '' description: '',
} }
} }
} }

View File

@ -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)

View File

@ -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',
}) })
} }
> >

View File

@ -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',
}) })
} }
> >

View File

@ -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',
}) })
} }
> >

View File

@ -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'

View File

@ -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',
)} )}
.&nbsp; .&nbsp;
{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()]} />

View File

@ -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 (

View File

@ -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