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 = {
plugins: ['@typescript-eslint', 'import', 'sonarjs', 'unicorn', 'promise', 'solid', 'jest'],
plugins: ["@typescript-eslint", "import", "sonarjs", "unicorn", "promise", "solid", "jest"],
extends: [
'eslint:recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'prettier',
'plugin:sonarjs/recommended',
'plugin:unicorn/recommended',
'plugin:promise/recommended',
'plugin:solid/recommended',
'plugin:jest/recommended'
"eslint:recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier",
"plugin:sonarjs/recommended",
"plugin:unicorn/recommended",
"plugin:promise/recommended",
"plugin:solid/recommended",
"plugin:jest/recommended"
],
overrides: [
{
files: ['**/*.ts', '**/*.tsx'],
parser: '@typescript-eslint/parser',
files: ["**/*.ts", "**/*.tsx"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2021,
ecmaFeatures: { jsx: true },
sourceType: 'module',
project: './tsconfig.json'
sourceType: "module",
project: "./tsconfig.json"
},
extends: [
'plugin:@typescript-eslint/recommended'
"plugin:@typescript-eslint/recommended"
// Maybe one day...
// 'plugin:@typescript-eslint/recommended-requiring-type-checking'
],
rules: {
'@typescript-eslint/no-unused-vars': [
'warn',
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: '^_'
argsIgnorePattern: "^_"
}
],
'@typescript-eslint/no-non-null-assertion': 'error',
"@typescript-eslint/no-non-null-assertion": "error",
// TODO: Remove any usage and enable
'@typescript-eslint/no-explicit-any': 'off'
"@typescript-eslint/no-explicit-any": "off"
}
}
],
@ -47,41 +47,60 @@ module.exports = {
globals: {},
rules: {
// Solid
'solid/reactivity': 'off', // FIXME
'solid/no-innerhtml': 'off',
"solid/reactivity": "off", // FIXME
"solid/no-innerhtml": "off",
/** Unicorn **/
'unicorn/no-null': 'off',
'unicorn/filename-case': 'off',
'unicorn/no-array-for-each': 'off',
'unicorn/no-array-reduce': 'off',
'unicorn/prefer-string-replace-all': 'warn',
'unicorn/prevent-abbreviations': 'off',
'unicorn/prefer-module': 'off',
'unicorn/import-style': 'off',
'unicorn/numeric-separators-style': 'off',
'unicorn/prefer-node-protocol': 'off',
'unicorn/prefer-dom-node-append': 'off', // FIXME
'unicorn/prefer-top-level-await': 'warn',
'unicorn/consistent-function-scoping': 'warn',
'unicorn/no-array-callback-reference': 'warn',
'unicorn/no-array-method-this-argument': 'warn',
"unicorn/no-null": "off",
"unicorn/filename-case": "off",
"unicorn/no-array-for-each": "off",
"unicorn/no-array-reduce": "off",
"unicorn/prefer-string-replace-all": "warn",
"unicorn/prevent-abbreviations": "off",
"unicorn/prefer-module": "off",
"unicorn/import-style": "off",
"unicorn/numeric-separators-style": "off",
"unicorn/prefer-node-protocol": "off",
"unicorn/prefer-dom-node-append": "off", // FIXME
"unicorn/prefer-top-level-await": "warn",
"unicorn/consistent-function-scoping": "warn",
"unicorn/no-array-callback-reference": "warn",
"unicorn/no-array-method-this-argument": "warn",
"unicorn/no-for-loop": "off",
'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }],
"sonarjs/no-duplicate-string": ["warn", { threshold: 5 }],
// Promise
// 'promise/catch-or-return': 'off', // Should be enabled
'promise/always-return': 'off',
"promise/always-return": "off",
eqeqeq: 'error',
'no-param-reassign': 'error',
'no-nested-ternary': 'error',
'no-shadow': 'error'
eqeqeq: "error",
"no-param-reassign": "error",
"no-nested-ternary": "error",
"no-shadow": "error",
"import/order": ["warn", {
groups: ["type", "builtin", "external", "internal", "parent", "sibling", "index"],
distinctGroup: false,
pathGroups: [
{
pattern: "*.scss",
patternOptions: { matchBase: true },
group: "index",
position: "after"
}
],
"newlines-between": "always",
alphabetize: {
order: "asc",
caseInsensitive: true
}
}]
},
settings: {
'import/resolver': {
"import/resolver": {
typescript: true,
node: true
}
}
}
};

View File

@ -1,4 +1,5 @@
{
"*.{js,mjs,ts,tsx,json,scss,css,html}": "prettier --write",
"package.json": "sort-package-json"
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write",
"package.json": "sort-package-json",
"public/locales/**/*.json": "sort-json"
}

View File

@ -4,7 +4,6 @@
"singleQuote": true,
"proseWrap": "always",
"printWidth": 108,
"trailingComma": "none",
"plugins": [],
"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>',
to: 'welcome@discours.io',
subject,
text
text,
}
try {

View File

@ -13,18 +13,18 @@ export default async (req, res) => {
const response = await mg.lists.members.createMember('newsletter@discours.io', {
address: email,
subscribed: true,
upsert: 'yes'
upsert: 'yes',
})
return res.status(200).json({
success: true,
message: 'Email was added to newsletter list',
response: JSON.stringify(response)
response: JSON.stringify(response),
})
} catch (error) {
return res.status(400).json({
success: false,
message: error.message
message: error.message,
})
}
}

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",
"idb": "7.1.1",
"intl-messageformat": "10.5.3",
"just-throttle": "4.2.0",
"mailgun.js": "8.2.1"
},
"devDependencies": {
"@babel/core": "7.21.8",
"@babel/core": "7.23.3",
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
@ -49,8 +48,8 @@
"@graphql-typed-document-node/core": "3.2.0",
"@hocuspocus/provider": "2.0.6",
"@microsoft/fetch-event-source": "^2.0.1",
"@nanostores/router": "0.8.3",
"@nanostores/solid": "0.3.2",
"@nanostores/router": "0.11.0",
"@nanostores/solid": "0.4.2",
"@popperjs/core": "2.11.8",
"@sentry/browser": "5.30.0",
"@solid-primitives/media": "2.2.3",
@ -58,7 +57,7 @@
"@solid-primitives/share": "2.0.4",
"@solid-primitives/storage": "1.3.9",
"@solid-primitives/upload": "0.0.110",
"@solidjs/meta": "0.28.2",
"@solidjs/meta": "0.29.1",
"@thisbeyond/solid-select": "0.14.0",
"@tiptap/core": "2.0.3",
"@tiptap/extension-blockquote": "2.0.3",
@ -89,17 +88,16 @@
"@tiptap/extension-text": "2.0.3",
"@tiptap/extension-underline": "2.0.3",
"@tiptap/extension-youtube": "2.0.3",
"@types/js-cookie": "3.0.5",
"@types/node": "20.8.10",
"@typescript-eslint/eslint-plugin": "6.9.1",
"@typescript-eslint/parser": "6.9.1",
"@types/js-cookie": "3.0.6",
"@types/node": "20.9.0",
"@typescript-eslint/eslint-plugin": "6.10.0",
"@typescript-eslint/parser": "6.10.0",
"@urql/core": "3.2.2",
"@urql/devtools": "2.0.3",
"babel-preset-solid": "1.8.4",
"bootstrap": "5.3.2",
"clsx": "2.0.0",
"cross-env": "7.0.3",
"debounce": "1.2.1",
"eslint": "8.53.0",
"eslint-config-stylelint": "20.0.0",
"eslint-import-resolver-typescript": "3.6.1",
@ -111,25 +109,19 @@
"eslint-plugin-sonarjs": "0.23.0",
"eslint-plugin-unicorn": "49.0.0",
"fast-deep-equal": "3.1.3",
"graphql": "16.6.0",
"graphql": "16.8.1",
"graphql-tag": "2.12.6",
"html-to-json-parser": "1.1.0",
"husky": "8.0.3",
"hygen": "6.2.11",
"i18next-http-backend": "2.2.0",
"javascript-time-ago": "2.5.9",
"jest": "29.7.0",
"js-cookie": "3.0.5",
"lint-staged": "15.0.2",
"lint-staged": "15.1.0",
"loglevel": "1.8.1",
"loglevel-plugin-prefix": "0.8.4",
"markdown-it": "13.0.1",
"markdown-it-container": "3.0.0",
"markdown-it-implicit-figures": "0.11.0",
"markdown-it-mark": "3.0.1",
"markdown-it-replace-link": "1.2.0",
"nanostores": "0.7.4",
"prettier": "3.0.3",
"nanostores": "0.9.5",
"prettier": "3.1.0",
"prettier-eslint": "16.1.2",
"prosemirror-history": "1.3.0",
"prosemirror-trailing-node": "2.0.3",
@ -140,16 +132,18 @@
"solid-popper": "0.3.0",
"solid-tiptap": "0.6.0",
"solid-transition-group": "0.2.3",
"sort-json": "2.0.1",
"sort-package-json": "2.6.0",
"stylelint": "15.11.0",
"stylelint-config-standard-scss": "11.1.0",
"stylelint-order": "6.0.3",
"stylelint-scss": "5.3.0",
"stylelint-scss": "5.3.1",
"swiper": "9.4.1",
"throttle-debounce": "5.0.0",
"typescript": "5.2.2",
"typograf": "7.1.0",
"uniqolor": "1.1.0",
"vike": "0.4.144",
"vike": "0.4.146",
"vite": "4.5.0",
"vite-plugin-mkcert": "1.16.0",
"vite-plugin-sass-dts": "1.3.11",

View File

@ -126,6 +126,7 @@
"FAQ": "Tips and suggestions",
"Favorite": "Favorites",
"Favorite topics": "Favorite topics",
"Feed": "Feed",
"Feed settings": "Feed settings",
"Feedback": "Feedback",
"Fill email": "Fill email",
@ -168,6 +169,7 @@
"I know the password": "I know the password",
"Image format not supported": "Image format not supported",
"In&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",
"Independant magazine with an open horizontal cooperation about culture, science and society": "Independant magazine with an open horizontal cooperation about culture, science and society",
"Insert footnote": "Insert footnote",
@ -452,6 +454,5 @@
"video": "video",
"view": "view",
"viewsWithCount": "{count} {count, plural, one {view} other {views}}",
"yesterday": "yesterday",
"To new messages": "To new messages"
"yesterday": "yesterday"
}

View File

@ -130,6 +130,7 @@
"FAQ": "Советы и предложения",
"Favorite": "Избранное",
"Favorite topics": "Избранные темы",
"Feed": "Лента",
"Feed settings": "Настройки ленты",
"Feedback": "Обратная связь",
"Fill email": "Введите почту",
@ -175,6 +176,7 @@
"I know the password": "Я знаю пароль!",
"Image format not supported": "Тип изображения не поддерживается",
"In&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to": "В&nbsp;закладках можно сохранять избранные дискуссии и&nbsp;материалы, к&nbsp;которым хочется вернуться",
"Inbox": "Входящие",
"Incut": "Подверстка",
"Independant magazine with an open horizontal cooperation about culture, science and society": "Независимый журнал с открытой горизонтальной редакцией о культуре, науке и обществе",
"Insert footnote": "Вставить сноску",
@ -476,6 +478,5 @@
"video": "видео",
"view": "просмотр",
"viewsWithCount": "{count} {count, plural, one {просмотр} few {просмотрa} other {просмотров}}",
"yesterday": "вчера",
"To new messages": "К новым сообщениям"
"yesterday": "вчера"
}

View File

@ -1,21 +1,15 @@
// FIXME: breaks on vercel, research
// import 'solid-devtools'
import type { PageProps, RootSearchParams } from '../pages/types'
import { hideModal, MODALS, showModal } from '../stores/ui'
import { Meta, MetaProvider } from '@solidjs/meta'
import { Component, createEffect, createMemo } from 'solid-js'
import { ROUTES, useRouter } from '../stores/router'
import { Dynamic } from 'solid-js/web'
import type { PageProps, RootSearchParams } from '../pages/types'
import { HomePage } from '../pages/index.page'
import { AllTopicsPage } from '../pages/allTopics.page'
import { TopicPage } from '../pages/topic.page'
import { AllAuthorsPage } from '../pages/allAuthors.page'
import { AuthorPage } from '../pages/author.page'
import { FeedPage } from '../pages/feed.page'
import { ArticlePage } from '../pages/article.page'
import { SearchPage } from '../pages/search.page'
import { FourOuFourPage } from '../pages/fourOuFour.page'
import { ConfirmProvider } from '../context/confirm'
import { EditorProvider } from '../context/editor'
import { LocalizeProvider } from '../context/localize'
import { NotificationsProvider } from '../context/notifications'
import { SessionProvider } from '../context/session'
import { SnackbarProvider } from '../context/snackbar'
import { DiscussionRulesPage } from '../pages/about/discussionRules.page'
import { DogmaPage } from '../pages/about/dogma.page'
import { GuidePage } from '../pages/about/guide.page'
@ -26,21 +20,26 @@ import { PrinciplesPage } from '../pages/about/principles.page'
import { ProjectsPage } from '../pages/about/projects.page'
import { TermsOfUsePage } from '../pages/about/termsOfUse.page'
import { ThanksPage } from '../pages/about/thanks.page'
import { CreatePage } from '../pages/create.page'
import { EditPage } from '../pages/edit.page'
import { AllAuthorsPage } from '../pages/allAuthors.page'
import { AllTopicsPage } from '../pages/allTopics.page'
import { ArticlePage } from '../pages/article.page'
import { AuthorPage } from '../pages/author.page'
import { ConnectPage } from '../pages/connect.page'
import { InboxPage } from '../pages/inbox.page'
import { ExpoPage } from '../pages/expo/expo.page'
import { SessionProvider } from '../context/session'
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
import { CreatePage } from '../pages/create.page'
import { DraftsPage } from '../pages/drafts.page'
import { SnackbarProvider } from '../context/snackbar'
import { LocalizeProvider } from '../context/localize'
import { ConfirmProvider } from '../context/confirm'
import { EditorProvider } from '../context/editor'
import { NotificationsProvider } from '../context/notifications'
import { EditPage } from '../pages/edit.page'
import { ExpoPage } from '../pages/expo/expo.page'
import { FeedPage } from '../pages/feed.page'
import { FourOuFourPage } from '../pages/fourOuFour.page'
import { InboxPage } from '../pages/inbox.page'
import { HomePage } from '../pages/index.page'
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
import { SearchPage } from '../pages/search.page'
import { TopicPage } from '../pages/topic.page'
import { ROUTES, useRouter } from '../stores/router'
import { hideModal, MODALS, showModal } from '../stores/ui'
// TODO: lazy load
// const SomePage = lazy(() => import('./Pages/SomePage'))
@ -82,7 +81,7 @@ const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
profileSettings: ProfileSettingsPage,
profileSecurity: ProfileSecurityPage,
profileSubscriptions: ProfileSubscriptionsPage,
fourOuFour: FourOuFourPage
fourOuFour: FourOuFourPage,
}
export const App = (props: PageProps) => {
@ -110,6 +109,8 @@ export const App = (props: PageProps) => {
})
return (
<MetaProvider>
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<LocalizeProvider>
<SnackbarProvider>
<ConfirmProvider>
@ -123,5 +124,6 @@ export const App = (props: PageProps) => {
</ConfirmProvider>
</SnackbarProvider>
</LocalizeProvider>
</MetaProvider>
)
}

View File

@ -23,22 +23,16 @@ img {
}
}
.shoutCover {
background-size: cover;
height: 0;
padding-bottom: 56.2%;
.articleContent {
img {
cursor: zoom-in;
}
}
.shoutBody {
font-size: 1.6rem;
line-height: 1.6;
img {
display: block;
margin-bottom: 0.5em;
cursor: zoom-in;
}
blockquote,
blockquote[data-type='punchline'] {
clear: both;
@ -75,6 +69,7 @@ img {
&[data-float='left'],
&[data-float='right'] {
@include font-size(2.2rem);
line-height: 1.4;
@include media-breakpoint-up(sm) {
@ -423,7 +418,7 @@ img {
}
.shoutStatsItemViews {
color: rgb(0 0 0 / 0.4);
color: rgb(0 0 0 / 40%);
cursor: default;
font-weight: normal;
margin-left: auto;
@ -466,7 +461,8 @@ img {
.shoutStatsItemAdditionalDataItem {
font-weight: normal;
display: inline-block;
//margin-left: 2rem;
// margin-left: 2rem;
margin-right: 0;
margin-bottom: 0;
cursor: default;

View File

@ -1,12 +1,15 @@
import { clsx } from 'clsx'
import styles from './AudioHeader.module.scss'
import { MediaItem } from '../../../pages/types'
import { createSignal, Show } from 'solid-js'
import { Icon } from '../../_shared/Icon'
import { Topic } from '../../../graphql/types.gen'
import { MediaItem } from '../../../pages/types'
import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image'
import { CardTopic } from '../../Feed/CardTopic'
import { Image } from '../../_shared/Image'
import styles from './AudioHeader.module.scss'
type Props = {
title: string
cover?: string

View File

@ -1,8 +1,11 @@
import { createEffect, createMemo, createSignal, on, onMount, Show } from 'solid-js'
import { MediaItem } from '../../../pages/types'
import { PlayerHeader } from './PlayerHeader'
import { PlayerPlaylist } from './PlayerPlaylist'
import styles from './AudioPlayer.module.scss'
import { MediaItem } from '../../../pages/types'
type Props = {
media: MediaItem[]
@ -35,8 +38,8 @@ export const AudioPlayer = (props: Props) => {
() => {
setCurrentTrackDuration(0)
},
{ defer: true }
)
{ defer: true },
),
)
const handlePlayMedia = async (trackIndex: number) => {
@ -131,7 +134,7 @@ export const AudioPlayer = (props: Props) => {
<div
class={styles.progressFilled}
style={{
width: `${(currentTime() / currentTrackDuration()) * 100 || 0}%`
width: `${(currentTime() / currentTrackDuration()) * 100 || 0}%`,
}}
/>
</div>

View File

@ -1,11 +1,11 @@
import { createSignal, Show } from 'solid-js'
import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { Icon } from '../../_shared/Icon'
import styles from './AudioPlayer.module.scss'
import { MediaItem } from '../../../pages/types'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { Icon } from '../../_shared/Icon'
import styles from './AudioPlayer.module.scss'
type Props = {
onPlayMedia: () => void
@ -18,7 +18,7 @@ type Props = {
export const PlayerHeader = (props: Props) => {
const volumeContainerRef: { current: HTMLDivElement } = {
current: null
current: null,
}
const [isVolumeBarOpened, setIsVolumeBarOpened] = createSignal(false)
@ -30,7 +30,7 @@ export const PlayerHeader = (props: Props) => {
useOutsideClickHandler({
containerRef: volumeContainerRef,
predicate: () => isVolumeBarOpened(),
handler: () => toggleVolumeBar()
handler: () => toggleVolumeBar(),
})
return (
@ -42,7 +42,7 @@ export const PlayerHeader = (props: Props) => {
onClick={props.onPlayMedia}
class={clsx(
styles.playButton,
props.isPlaying ? styles.playButtonInvertPause : styles.playButtonInvertPlay
props.isPlaying ? styles.playButtonInvertPause : styles.playButtonInvertPlay,
)}
aria-label="Play"
data-playing="false"

View File

@ -1,14 +1,15 @@
import { createSignal, For, Show } from 'solid-js'
import { SharePopup, getShareUrl } from '../SharePopup'
import { getDescription } from '../../../utils/meta'
import { useLocalize } from '../../../context/localize'
import { Popover } from '../../_shared/Popover'
import { Icon } from '../../_shared/Icon'
import styles from './AudioPlayer.module.scss'
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
import MD from '../MD'
import { MediaItem } from '../../../pages/types'
import { getDescription } from '../../../utils/meta'
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
import { Icon } from '../../_shared/Icon'
import { Popover } from '../../_shared/Popover'
import SimplifiedEditor from '../../Editor/SimplifiedEditor'
import { SharePopup, getShareUrl } from '../SharePopup'
import styles from './AudioPlayer.module.scss'
type Props = {
media: MediaItem[]
@ -146,12 +147,12 @@ export const PlayerPlaylist = (props: Props) => {
<div class={styles.descriptionBlock}>
<Show when={mi.body}>
<div class={styles.description}>
<MD body={mi.body} />
<div innerHTML={mi.body} />
</div>
</Show>
<Show when={mi.lyrics}>
<div class={styles.lyrics}>
<MD body={mi.lyrics} />
<div innerHTML={mi.lyrics} />
</div>
</Show>
</div>

View File

@ -4,7 +4,7 @@
transition: background-color 0.3s;
position: relative;
list-style: none;
background: rgb(0 0 0 / 0.1);
background: rgb(0 0 0 / 10%);
@include media-breakpoint-down(sm) {
padding-right: 0;

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 { clsx } from 'clsx'
import { Show, createMemo, createSignal, For, lazy, Suspense } from 'solid-js'
import MD from '../MD'
import { Userpic } from '../../Author/Userpic'
import { CommentRatingControl } from '../CommentRatingControl'
import { CommentDate } from '../CommentDate'
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
import { Icon } from '../../_shared/Icon'
import { useSession } from '../../../context/session'
import { useConfirm } from '../../../context/confirm'
import { useLocalize } from '../../../context/localize'
import { useReactions } from '../../../context/reactions'
import { useSession } from '../../../context/session'
import { useSnackbar } from '../../../context/snackbar'
import { useConfirm } from '../../../context/confirm'
import { Author, Reaction, ReactionKind } from '../../../graphql/types.gen'
import { router } from '../../../stores/router'
import { Icon } from '../../_shared/Icon'
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
import { AuthorLink } from '../../Author/AhtorLink'
import { Userpic } from '../../Author/Userpic'
import { CommentDate } from '../CommentDate'
import { CommentRatingControl } from '../CommentRatingControl'
import styles from './Comment.module.scss'
import { AuthorLink } from '../../Author/AhtorLink'
@ -44,15 +42,15 @@ export const Comment = (props: Props) => {
const { session } = useSession()
const {
actions: { createReaction, deleteReaction, updateReaction }
actions: { createReaction, deleteReaction, updateReaction },
} = useReactions()
const {
actions: { showConfirm }
actions: { showConfirm },
} = useConfirm()
const {
actions: { showSnackbar }
actions: { showSnackbar },
} = useSnackbar()
const isCommentAuthor = createMemo(() => props.comment.createdBy?.slug === session()?.user?.slug)
@ -66,7 +64,7 @@ export const Comment = (props: Props) => {
confirmBody: t('Are you sure you want to delete this comment?'),
confirmButtonLabel: t('Delete'),
confirmButtonVariant: 'danger',
declineButtonVariant: 'primary'
declineButtonVariant: 'primary',
})
if (isConfirmed) {
@ -87,7 +85,7 @@ export const Comment = (props: Props) => {
kind: ReactionKind.Comment,
replyTo: props.comment.id,
body: value,
shout: props.comment.shout.id
shout: props.comment.shout.id,
})
setClearEditor(true)
setIsReplyVisible(false)
@ -108,7 +106,7 @@ export const Comment = (props: Props) => {
await updateReaction(props.comment.id, {
kind: ReactionKind.Comment,
body: value,
shout: props.comment.shout.id
shout: props.comment.shout.id,
})
setEditMode(false)
setLoading(false)
@ -123,7 +121,7 @@ export const Comment = (props: Props) => {
<li
id={`comment_${comment().id}`}
class={clsx(styles.comment, props.class, {
[styles.isNew]: !isCommentAuthor() && createdAt > props.lastSeen
[styles.isNew]: !isCommentAuthor() && createdAt > props.lastSeen,
})}
>
<Show when={!!body()}>
@ -136,7 +134,7 @@ export const Comment = (props: Props) => {
name={comment().createdBy.name}
userpic={comment().createdBy.userpic}
class={clsx({
[styles.compactUserpic]: props.compact
[styles.compactUserpic]: props.compact,
})}
/>
<small>
@ -171,7 +169,7 @@ export const Comment = (props: Props) => {
</div>
</Show>
<div class={styles.commentBody}>
<Show when={editMode()} fallback={<MD body={body()} />}>
<Show when={editMode()} fallback={<div innerHTML={body()} />}>
<Suspense fallback={<p>{t('Loading')}</p>}>
<SimplifiedEditor
initialContent={comment().body}

View File

@ -1,8 +1,11 @@
import { Show } from 'solid-js'
import { Icon } from '../../_shared/Icon'
import type { Reaction } from '../../../graphql/types.gen'
import { useLocalize } from '../../../context/localize'
import { clsx } from 'clsx'
import { Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon'
import styles from './CommentDate.module.scss'
type Props = {
@ -27,7 +30,7 @@ export const CommentDate = (props: Props) => {
<div
class={clsx(styles.commentDates, {
[styles.commentDatesLastInRow]: props.isLastInRow,
[styles.showOnHover]: props.showOnHover
[styles.showOnHover]: props.showOnHover,
})}
>
<time class={styles.date}>{formattedDate(props.comment.createdAt)}</time>

View File

@ -1,16 +1,19 @@
import { clsx } from 'clsx'
import styles from './CommentRatingControl.module.scss'
import type { Reaction } from '../../graphql/types.gen'
import { ReactionKind } from '../../graphql/types.gen'
import { useSession } from '../../context/session'
import { useReactions } from '../../context/reactions'
import { clsx } from 'clsx'
import { createMemo } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/snackbar'
import { ReactionKind } from '../../graphql/types.gen'
import { loadShout } from '../../stores/zine/articles'
import { Popup } from '../_shared/Popup'
import { useLocalize } from '../../context/localize'
import { useSnackbar } from '../../context/snackbar'
import { VotersList } from '../_shared/VotersList'
import styles from './CommentRatingControl.module.scss'
type Props = {
comment: Reaction
}
@ -19,11 +22,11 @@ export const CommentRatingControl = (props: Props) => {
const { t } = useLocalize()
const { user } = useSession()
const {
actions: { showSnackbar }
actions: { showSnackbar },
} = useSnackbar()
const {
reactionEntities,
actions: { createReaction, deleteReaction, loadReactionsBy }
actions: { createReaction, deleteReaction, loadReactionsBy },
} = useReactions()
const checkReaction = (reactionKind: ReactionKind) =>
@ -32,7 +35,7 @@ export const CommentRatingControl = (props: Props) => {
r.kind === reactionKind &&
r.createdBy.slug === user()?.slug &&
r.shout.id === props.comment.shout.id &&
r.replyTo === props.comment.id
r.replyTo === props.comment.id,
)
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
@ -43,8 +46,8 @@ export const CommentRatingControl = (props: Props) => {
(r) =>
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
r.shout.id === props.comment.shout.id &&
r.replyTo === props.comment.id
)
r.replyTo === props.comment.id,
),
)
const deleteCommentReaction = async (reactionKind: ReactionKind) => {
@ -53,7 +56,7 @@ export const CommentRatingControl = (props: Props) => {
r.kind === reactionKind &&
r.createdBy.slug === user()?.slug &&
r.shout.id === props.comment.shout.id &&
r.replyTo === props.comment.id
r.replyTo === props.comment.id,
)
return deleteReaction(reactionToDelete.id)
}
@ -68,7 +71,7 @@ export const CommentRatingControl = (props: Props) => {
await createReaction({
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
shout: props.comment.shout.id,
replyTo: props.comment.id
replyTo: props.comment.id,
})
}
} catch {
@ -77,7 +80,7 @@ export const CommentRatingControl = (props: Props) => {
await loadShout(props.comment.shout.slug)
await loadReactionsBy({
by: { shout: props.comment.shout.slug }
by: { shout: props.comment.shout.slug },
})
}
@ -88,7 +91,7 @@ export const CommentRatingControl = (props: Props) => {
disabled={!canVote() || !user()}
onClick={() => handleRatingChange(true)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, {
[styles.voted]: isUpvoted()
[styles.voted]: isUpvoted(),
})}
/>
<Popup
@ -96,7 +99,7 @@ export const CommentRatingControl = (props: Props) => {
<div
class={clsx(styles.commentRatingValue, {
[styles.commentRatingPositive]: props.comment.stat.rating > 0,
[styles.commentRatingNegative]: props.comment.stat.rating < 0
[styles.commentRatingNegative]: props.comment.stat.rating < 0,
})}
>
{props.comment.stat.rating || 0}
@ -114,7 +117,7 @@ export const CommentRatingControl = (props: Props) => {
disabled={!canVote() || !user()}
onClick={() => handleRatingChange(false)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, {
[styles.voted]: isDownvoted()
[styles.voted]: isDownvoted(),
})}
/>
</div>

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 { Author, Reaction, ReactionKind } from '../../graphql/types.gen'
import { useSession } from '../../context/session'
import { Button } from '../_shared/Button'
import { useReactions } from '../../context/reactions'
import { byCreated } from '../../utils/sortby'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
import { Show, createMemo, createSignal, onMount, For } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
import { Author, Reaction, ReactionKind } from '../../graphql/types.gen'
import { byCreated } from '../../utils/sortby'
import { Button } from '../_shared/Button'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
import SimplifiedEditor from '../Editor/SimplifiedEditor'
import { Comment } from './Comment'
import styles from './Article.module.scss'
type CommentsOrder = 'createdAt' | 'rating' | 'newOnly'
const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
@ -48,11 +51,11 @@ export const CommentsTree = (props: Props) => {
const {
reactionEntities,
actions: { createReaction }
actions: { createReaction },
} = useReactions()
const comments = createMemo(() =>
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT'),
)
const sortedComments = createMemo(() => {
@ -96,7 +99,7 @@ export const CommentsTree = (props: Props) => {
await createReaction({
kind: ReactionKind.Comment,
body: value,
shout: props.shoutId
shout: props.shoutId,
})
setClearEditor(true)
} catch (error) {
@ -154,7 +157,7 @@ export const CommentsTree = (props: Props) => {
<Comment
sortedComments={sortedComments()}
isArticleAuthor={Boolean(
props.articleAuthors.some((a) => a.slug === reaction.createdBy.slug)
props.articleAuthors.some((a) => a.slug === reaction.createdBy.slug),
)}
comment={reaction}
clickedReply={(id) => setClickedReplyId(id)}

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 { useSession } from '../../context/session'
import { getPagePath } from '@nanostores/router'
import { createPopper } from '@popperjs/core'
import { clsx } from 'clsx'
import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
import { MediaItem } from '../../pages/types'
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
import { getImageUrl } from '../../utils/getImageUrl'
import { getDescription } from '../../utils/meta'
import { Icon } from '../_shared/Icon'
import { Image } from '../_shared/Image'
import { Lightbox } from '../_shared/Lightbox'
import { Popover } from '../_shared/Popover'
import { ImageSwiper } from '../_shared/SolidSwiper'
import { VideoPlayer } from '../_shared/VideoPlayer'
import { AuthorBadge } from '../Author/AuthorBadge'
import { CardTopic } from '../Feed/CardTopic'
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
import { TableOfContents } from '../TableOfContents'
import { AudioHeader } from './AudioHeader'
import { AudioPlayer } from './AudioPlayer'
import { CommentsTree } from './CommentsTree'
import { getShareUrl, SharePopup } from './SharePopup'
import { ShoutRatingControl } from './ShoutRatingControl'
import { CommentsTree } from './CommentsTree'
import stylesHeader from '../Nav/Header/Header.module.scss'
import { AudioHeader } from './AudioHeader'
import { Popover } from '../_shared/Popover'
import { VideoPlayer } from '../_shared/VideoPlayer'
import { Icon } from '../_shared/Icon'
import { ImageSwiper } from '../_shared/SolidSwiper'
import styles from './Article.module.scss'
import { CardTopic } from '../Feed/CardTopic'
import { createPopper } from '@popperjs/core'
import { AuthorBadge } from '../Author/AuthorBadge'
import { getImageUrl } from '../../utils/getImageUrl'
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
import { Lightbox } from '../_shared/Lightbox'
import stylesHeader from '../Nav/Header/Header.module.scss'
type Props = {
article: Shout
@ -45,7 +48,7 @@ const scrollTo = (el: HTMLElement) => {
window.scrollTo({
top: top + window.scrollY - DEFAULT_HEADER_OFFSET,
left: 0,
behavior: 'smooth'
behavior: 'smooth',
})
}
@ -56,7 +59,7 @@ export const FullArticle = (props: Props) => {
const {
user,
isAuthenticated,
actions: { requireAuthentication }
actions: { requireAuthentication },
} = useSession()
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
@ -66,7 +69,7 @@ export const FullArticle = (props: Props) => {
const mainTopic = createMemo(
() =>
props.article.topics?.find((topic) => topic?.slug === props.article.mainTopic) ||
props.article.topics[0]
props.article.topics[0],
)
const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
@ -91,8 +94,13 @@ export const FullArticle = (props: Props) => {
}
return props.article.body
})
const media = createMemo(() => {
return JSON.parse(props.article.media || '[]')
const media = createMemo<MediaItem[]>(() => {
try {
return JSON.parse(props.article.media)
} catch {
return []
}
})
const commentsRef: {
@ -115,7 +123,7 @@ export const FullArticle = (props: Props) => {
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
scrollToComments()
changeSearchParam({
scrollTo: null
scrollTo: null,
})
}
})
@ -123,7 +131,7 @@ export const FullArticle = (props: Props) => {
createEffect(() => {
if (searchParams().commentId && isReactionsLoaded()) {
const commentElement = document.querySelector<HTMLElement>(
`[id='comment_${searchParams().commentId}']`
`[id='comment_${searchParams().commentId}']`,
)
changeSearchParam({ commentId: null })
@ -135,17 +143,21 @@ export const FullArticle = (props: Props) => {
})
const {
actions: { loadReactionsBy }
actions: { loadReactionsBy },
} = useReactions()
onMount(async () => {
await loadReactionsBy({
by: { shout: props.article.slug }
by: { shout: props.article.slug },
})
setIsReactionsLoaded(true)
})
onMount(() => {
document.title = props.article.title
})
const clickHandlers = []
const documentClickHandlers = []
@ -155,7 +167,7 @@ export const FullArticle = (props: Props) => {
}
const tooltipElements: NodeListOf<HTMLElement> = document.querySelectorAll(
'[data-toggle="tooltip"], footnote'
'[data-toggle="tooltip"], footnote',
)
if (!tooltipElements) {
return
@ -180,19 +192,19 @@ export const FullArticle = (props: Props) => {
modifiers: [
{
name: 'eventListeners',
options: { scroll: false }
options: { scroll: false },
},
{
name: 'offset',
options: {
offset: [0, 8]
}
offset: [0, 8],
},
},
{
name: 'flip',
options: { fallbackPlacements: ['top'] }
}
]
options: { fallbackPlacements: ['top'] },
},
],
})
tooltip.style.visibility = 'hidden'
@ -249,10 +261,12 @@ export const FullArticle = (props: Props) => {
return (
<>
<Title>{props.article.title}</Title>
<div class="wide-container">
<div class="row position-relative">
<article class="col-md-16 col-lg-14 col-xl-12 offset-md-5">
<article
class={clsx('col-md-16 col-lg-14 col-xl-12 offset-md-5', styles.articleContent)}
onClick={handleArticleBodyClick}
>
{/*TODO: Check styles.shoutTopic*/}
<Show when={props.article.layout !== 'music'}>
<div class={styles.shoutHeader}>
@ -282,12 +296,7 @@ export const FullArticle = (props: Props) => {
props.article.layout !== 'image'
}
>
<div
class={styles.shoutCover}
style={{
'background-image': `url('${getImageUrl(props.article.cover, { width: 1600 })}')`
}}
/>
<Image width={1600} alt={props.article.title} src={props.article.cover} />
</Show>
</div>
</Show>
@ -319,7 +328,7 @@ export const FullArticle = (props: Props) => {
description={m.body}
/>
<Show when={m?.body}>
<MD body={m.body} />
<div innerHTML={m.body} />
</Show>
</div>
)}
@ -328,11 +337,7 @@ export const FullArticle = (props: Props) => {
</Show>
<Show when={body()}>
<div id="shoutBody" class={styles.shoutBody} onClick={handleArticleBodyClick}>
<Show when={!body().startsWith('<')} fallback={<div innerHTML={body()} />}>
<MD body={body()} />
</Show>
</div>
<div id="shoutBody" class={styles.shoutBody} innerHTML={body()} />
</Show>
</article>

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 styles from '../_shared/Popup/Popup.module.scss'
import type { PopupProps } from '../_shared/Popup'
import { Popup } from '../_shared/Popup'
import { useLocalize } from '../../context/localize'
import { createEffect, createSignal } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useSnackbar } from '../../context/snackbar'
import { Icon } from '../_shared/Icon'
import { Popup } from '../_shared/Popup'
import styles from '../_shared/Popup/Popup.module.scss'
type SharePopupProps = {
title: string
@ -26,7 +28,7 @@ export const SharePopup = (props: SharePopupProps) => {
const { t } = useLocalize()
const [isVisible, setIsVisible] = createSignal(false)
const {
actions: { showSnackbar }
actions: { showSnackbar },
} = useSnackbar()
createEffect(() => {
@ -38,7 +40,7 @@ export const SharePopup = (props: SharePopupProps) => {
const [share] = createSocialShare(() => ({
title: props.title,
url: props.shareUrl,
description: props.description
description: props.description,
}))
const copyLink = async () => {

View File

@ -1,13 +1,15 @@
import { clsx } from 'clsx'
import { createMemo, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
import { ReactionKind, Shout } from '../../graphql/types.gen'
import { loadShout } from '../../stores/zine/articles'
import { useSession } from '../../context/session'
import { useReactions } from '../../context/reactions'
import { Icon } from '../_shared/Icon'
import { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList'
import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon'
import styles from './ShoutRatingControl.module.scss'
interface ShoutRatingControlProps {
@ -19,12 +21,12 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
const { t } = useLocalize()
const {
user,
actions: { requireAuthentication }
actions: { requireAuthentication },
} = useSession()
const {
reactionEntities,
actions: { createReaction, deleteReaction, loadReactionsBy }
actions: { createReaction, deleteReaction, loadReactionsBy },
} = useReactions()
const checkReaction = (reactionKind: ReactionKind) =>
@ -33,7 +35,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
r.kind === reactionKind &&
r.createdBy.slug === user()?.slug &&
r.shout.id === props.shout.id &&
!r.replyTo
!r.replyTo,
)
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
@ -45,8 +47,8 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
(r) =>
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
r.shout.id === props.shout.id &&
!r.replyTo
)
!r.replyTo,
),
)
const deleteShoutReaction = async (reactionKind: ReactionKind) => {
@ -55,7 +57,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
r.kind === reactionKind &&
r.createdBy.slug === user()?.slug &&
r.shout.id === props.shout.id &&
!r.replyTo
!r.replyTo,
)
return deleteReaction(reactionToDelete.id)
}
@ -69,13 +71,13 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
} else {
await createReaction({
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
shout: props.shout.id
shout: props.shout.id,
})
}
loadShout(props.shout.slug)
loadReactionsBy({
by: { shout: props.shout.slug }
by: { shout: props.shout.slug },
})
}, 'vote')
}

View File

@ -1,8 +1,9 @@
import { createEffect, JSX, Show } from 'solid-js'
import { useSession } from '../../context/session'
import { hideModal } from '../../stores/ui'
import { useRouter } from '../../stores/router'
import { RootSearchParams } from '../../pages/types'
import { useRouter } from '../../stores/router'
import { hideModal } from '../../stores/ui'
import { AuthModalSearchParams } from '../Nav/AuthModal/types'
type Props = {
@ -25,9 +26,9 @@ export const AuthGuard = (props: Props) => {
changeSearchParam(
{
source: 'authguard',
modal: 'auth'
modal: 'auth',
},
true
true,
)
}
}

View File

@ -1,8 +1,10 @@
import { clsx } from 'clsx'
import styles from './AhtorLink.module.scss'
import { Author } from '../../../graphql/types.gen'
import { Userpic } from '../Userpic'
import styles from './AhtorLink.module.scss'
type Props = {
author: Author
size?: 'XS' | 'M' | 'L'

View File

@ -1,17 +1,19 @@
import { openPage } from '@nanostores/router'
import { clsx } from 'clsx'
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { Author, FollowingEntity } from '../../../graphql/types.gen'
import { router, useRouter } from '../../../stores/router'
import { follow, unfollow } from '../../../stores/zine/common'
import { Button } from '../../_shared/Button'
import { CheckButton } from '../../_shared/CheckButton'
import { Icon } from '../../_shared/Icon'
import { Userpic } from '../Userpic'
import styles from './AuthorBadge.module.scss'
import stylesButton from '../../_shared/Button/Button.module.scss'
import { Userpic } from '../Userpic'
import { Author, FollowingEntity } from '../../../graphql/types.gen'
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Button } from '../../_shared/Button'
import { useSession } from '../../../context/session'
import { follow, unfollow } from '../../../stores/zine/common'
import { CheckButton } from '../../_shared/CheckButton'
import { openPage } from '@nanostores/router'
import { router, useRouter } from '../../../stores/router'
import { Icon } from '../../_shared/Icon'
type Props = {
author: Author
@ -25,12 +27,12 @@ export const AuthorBadge = (props: Props) => {
const {
session,
subscriptions,
actions: { loadSubscriptions, requireAuthentication }
actions: { loadSubscriptions, requireAuthentication },
} = useSession()
const { changeSearchParam } = useRouter()
const { t, formatDate } = useLocalize()
const subscribed = createMemo(() =>
subscriptions().authors.some((author) => author.slug === props.author.slug)
subscriptions().authors.some((author) => author.slug === props.author.slug),
)
const subscribe = async (really = true) => {
@ -53,29 +55,10 @@ export const AuthorBadge = (props: Props) => {
requireAuthentication(() => {
openPage(router, `inbox`)
changeSearchParam({
initChat: props.author.id.toString()
initChat: props.author.id.toString(),
})
}, 'discussions')
}
const subscribeValue = createMemo(() => {
if (props.iconButtons) {
return <Icon name="author-subscribe" class={stylesButton.icon} />
}
return isSubscribing() ? t('subscribing...') : t('Subscribe')
})
const unsubscribeValue = () => {
if (props.iconButtons) {
return <Icon name="author-unsubscribe" class={stylesButton.icon} />
}
return (
<>
<span class={styles.actionButtonLabel}>{t('Following')}</span>
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
</>
)
}
return (
<div class={clsx(styles.AuthorBadge, { [styles.nameOnly]: props.nameOnly })}>
@ -129,12 +112,23 @@ export const AuthorBadge = (props: Props) => {
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="M"
value={subscribeValue()}
value={
<Show
when={props.iconButtons}
fallback={
<Show when={isSubscribing()} fallback={t('Subscribe')}>
{t('subscribing...')}
</Show>
}
>
<Icon name="author-subscribe" class={stylesButton.icon} />
</Show>
}
onClick={() => handleSubscribe(true)}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: subscribed()
[stylesButton.subscribed]: subscribed(),
})}
/>
}
@ -142,12 +136,24 @@ export const AuthorBadge = (props: Props) => {
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="M"
value={unsubscribeValue()}
value={
<Show
when={props.iconButtons}
fallback={
<>
<span class={styles.actionButtonLabel}>{t('Following')}</span>
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
</>
}
>
<Icon name="author-unsubscribe" class={stylesButton.icon} />
</Show>
}
onClick={() => handleSubscribe(false)}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: subscribed()
[stylesButton.subscribed]: subscribed(),
})}
/>
</Show>

View File

@ -1,22 +1,25 @@
import type { Author } from '../../../graphql/types.gen'
import { Userpic } from '../Userpic'
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
import { translit } from '../../../utils/ru2en'
import { follow, unfollow } from '../../../stores/zine/common'
import { clsx } from 'clsx'
import { useSession } from '../../../context/session'
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
import { router, useRouter } from '../../../stores/router'
import { openPage, redirectPage } from '@nanostores/router'
import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Modal } from '../../Nav/Modal'
import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
import { SubscriptionFilter } from '../../../pages/types'
import { router, useRouter } from '../../../stores/router'
import { follow, unfollow } from '../../../stores/zine/common'
import { isAuthor } from '../../../utils/isAuthor'
import { AuthorBadge } from '../AuthorBadge'
import { TopicBadge } from '../../Topic/TopicBadge'
import { translit } from '../../../utils/ru2en'
import { Button } from '../../_shared/Button'
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { Modal } from '../../Nav/Modal'
import { TopicBadge } from '../../Topic/TopicBadge'
import { AuthorBadge } from '../AuthorBadge'
import { Userpic } from '../Userpic'
import styles from './AuthorCard.module.scss'
import stylesButton from '../../_shared/Button/Button.module.scss'
@ -32,7 +35,7 @@ export const AuthorCard = (props: Props) => {
session,
subscriptions,
isSessionLoaded,
actions: { loadSubscriptions, requireAuthentication }
actions: { loadSubscriptions, requireAuthentication },
} = useSession()
const [isSubscribing, setIsSubscribing] = createSignal(false)
@ -40,7 +43,7 @@ export const AuthorCard = (props: Props) => {
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
const subscribed = createMemo<boolean>(() =>
subscriptions().authors.some((author) => author.slug === props.author.slug)
subscriptions().authors.some((author) => author.slug === props.author.slug),
)
const subscribe = async (really = true) => {
@ -74,7 +77,7 @@ export const AuthorCard = (props: Props) => {
requireAuthentication(() => {
openPage(router, `inbox`)
changeSearchParam({
initChat: props.author.id.toString()
initChat: props.author.id.toString(),
})
}, 'discussions')
}
@ -97,20 +100,22 @@ export const AuthorCard = (props: Props) => {
}
})
const followButtonText = () => {
const followButtonText = createMemo(() => {
if (isSubscribing()) {
return t('subscribing...')
} else if (subscribed()) {
}
if (subscribed()) {
return (
<>
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
</>
)
} else {
}
return t('Follow')
}
}
})
return (
<div class={clsx(styles.author, 'row')}>
@ -216,7 +221,7 @@ export const AuthorCard = (props: Props) => {
value={followButtonText()}
isSubscribeButton={true}
class={clsx({
[stylesButton.subscribed]: subscribed()
[stylesButton.subscribed]: subscribed(),
})}
/>
<Button

View File

@ -1,7 +1,9 @@
import styles from './AuthorRatingControl.module.scss'
import { clsx } from 'clsx'
import type { Author } from '../../graphql/types.gen'
import { clsx } from 'clsx'
import styles from './AuthorRatingControl.module.scss'
interface AuthorRatingControlProps {
author: Author
class?: string
@ -20,7 +22,7 @@ export const AuthorRatingControl = (props: AuthorRatingControlProps) => {
<div
class={clsx(styles.rating, props.class, {
[styles.isUpvoted]: isUpvoted,
[styles.isDownvoted]: isDownvoted
[styles.isDownvoted]: isDownvoted,
})}
>
<button

View File

@ -1,9 +1,11 @@
import { createMemo, Show } from 'solid-js'
import styles from './Userpic.module.scss'
import { clsx } from 'clsx'
import { createMemo, Show } from 'solid-js'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import { Loading } from '../../_shared/Loading'
import { Image } from '../../_shared/Image'
import { Loading } from '../../_shared/Loading'
import styles from './Userpic.module.scss'
type Props = {
name: string
@ -46,7 +48,7 @@ export const Userpic = (props: Props) => {
return (
<div
class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], {
['cursorPointer']: props.onClick
['cursorPointer']: props.onClick,
})}
onClick={props.onClick}
>

View File

@ -1,8 +1,9 @@
import styles from './Banner.module.scss'
import { showModal } from '../../stores/ui'
import { clsx } from 'clsx'
import { useLocalize } from '../../context/localize'
import { showModal } from '../../stores/ui'
import styles from './Banner.module.scss'
export default () => {
const { t } = useLocalize()

View File

@ -1,6 +1,6 @@
.donate-form input,
.donate-form label,
.donate-form .btn {
.donateForm input,
.donateForm label,
.donateForm .btn {
font-family: Muller, Arial, Helvetica, sans-serif;
border: solid 1px #595959;
border-radius: 3px;
@ -10,7 +10,7 @@
text-align: center;
}
.donate-form input {
.donateForm input {
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
@ -48,12 +48,13 @@
}
}
.btn {
.donateForm .btn {
cursor: pointer;
flex: 1;
padding: 5px 10px;
text-align: center;
white-space: nowrap;
transform: none !important;
@include media-breakpoint-down(sm) {
&:last-of-type {
@ -62,7 +63,7 @@
}
}
.btn-group {
.btnGroup {
input {
&:first-child + .btn {
border-top-right-radius: 0;
@ -75,12 +76,13 @@
}
}
.payment-type {
.paymentType {
width: 50%;
}
}
.donate-buttons-container {
.donateButtonsContainer {
align-items: center;
display: flex;
flex: 1;
justify-content: space-between;
@ -111,59 +113,34 @@
}
}
.donate-input {
.donateInput {
@include media-breakpoint-down(sm) {
flex: 1 100%;
margin: 0 !important;
}
}
.send-btn {
border: 1px solid #000;
.sendBtn {
border: 2px solid #000;
background-color: #000;
color: #fff;
color: #fff !important;
display: block;
font-weight: 700;
line-height: 1.8;
letter-spacing: 0.05em;
text-transform: uppercase;
width: 100%;
&:hover {
background-color: #fff !important;
color: #000 !important;
}
}
.payment-choose {
.paymentChoose {
display: flex;
}
.form-group:not(:first-child) {
.formGroup:not(:first-child) {
margin-top: 20px;
}
.discours-help .modalwrap__inner {
max-width: 500px;
}
/*
.payment-form {
padding: 0 !important;
.button {
display: block;
padding-bottom: 1.5rem;
padding-top: 1.5rem;
width: 100%;
}
}
.delimiter-container {
position: relative;
}
.delimiter {
left: 100%;
line-height: 1;
position: absolute;
top: 50%;
transform: translate(-50%, calc(-50% - 0.8rem));
}
*/

View File

@ -1,8 +1,11 @@
import '../../styles/help.scss'
import { clsx } from 'clsx'
import { createSignal, onMount } from 'solid-js'
import { showModal } from '../../stores/ui'
import { useLocalize } from '../../context/localize'
import { useSnackbar } from '../../context/snackbar'
import { showModal } from '../../stores/ui'
import styles from './Donate.module.scss'
export const Donate = () => {
const { t } = useLocalize()
@ -11,7 +14,7 @@ export const Donate = () => {
const cpOptions = {
publicId: 'pk_0a37bab30ffc6b77b2f93d65f2aed',
description: t('Help discours to grow'),
currency: 'RUB'
currency: 'RUB',
}
let amountSwitchElement: HTMLDivElement | undefined
@ -22,13 +25,13 @@ export const Donate = () => {
const [period, setPeriod] = createSignal(monthly)
const [amount, setAmount] = createSignal(0)
const {
actions: { showSnackbar }
actions: { showSnackbar },
} = useSnackbar()
const initiated = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const {
cp: { CloudPayments }
cp: { CloudPayments },
} = window as any // Checkout(cpOptions)
setWidget(new CloudPayments())
console.log('[donate] payments initiated')
@ -42,8 +45,8 @@ export const Donate = () => {
amount: amount() || 0, //сумма
vat: 20, //ставка НДС
method: 0, // тег-1214 признак способа расчета - признак способа расчета
object: 0 // тег-1212 признак предмета расчета - признак предмета товара, работы, услуги, платежа, выплаты, иного предмета расчета
}
object: 0, // тег-1212 признак предмета расчета - признак предмета товара, работы, услуги, платежа, выплаты, иного предмета расчета
},
],
// taxationSystem: 0, //система налогообложения; необязательный, если у вас одна система налогообложения
// email: 'user@example.com', //e-mail покупателя, если нужно отправить письмо с чеком
@ -53,8 +56,8 @@ export const Donate = () => {
electronic: amount(), // Сумма оплаты электронными деньгами
advancePayment: 0, // Сумма из предоплаты (зачетом аванса) (2 знака после запятой)
credit: 0, // Сумма постоплатой(в кредит) (2 знака после запятой)
provision: 0 // Сумма оплаты встречным предоставлением (сертификаты, др. мат.ценности) (2 знака после запятой)
}
provision: 0, // Сумма оплаты встречным предоставлением (сертификаты, др. мат.ценности) (2 знака после запятой)
},
})
}
@ -93,10 +96,10 @@ export const Donate = () => {
recurrent: {
interval: period(), // local solid's signal
period: 1, // internal widget's
CustomerReciept: customerReciept() // чек для регулярных платежей
}
}
}
CustomerReciept: customerReciept(), // чек для регулярных платежей
},
},
},
},
(opts) => {
// success
@ -111,34 +114,34 @@ export const Donate = () => {
showSnackbar({
type: 'error',
body: reason
body: reason,
})
}
},
)
}
return (
<form class="discours-form donate-form" action="" method="post">
<form class={styles.donateForm} action="" method="post">
<input type="hidden" name="shopId" value="156465" />
<input value="148805" name="scid" type="hidden" />
<input value="0" name="customerNumber" type="hidden" />
<div class="form-group">
<div class="donate-buttons-container" ref={amountSwitchElement}>
<div class={styles.formGroup}>
<div class={styles.donateButtonsContainer} ref={amountSwitchElement}>
<input type="radio" name="amount" id="fix250" value="250" />
<label for="fix250" class="btn donate-value-radio">
<label for="fix250" class={styles.btn}>
250&thinsp;
</label>
<input type="radio" name="amount" id="fix500" value="500" checked />
<label for="fix500" class="btn donate-value-radio">
<label for="fix500" class={styles.btn}>
500&thinsp;
</label>
<input type="radio" name="amount" id="fix1000" value="1000" />
<label for="fix1000" class="btn donate-value-radio">
<label for="fix1000" class={styles.btn}>
1000&thinsp;
</label>
<input
class="form-control donate-input"
class={styles.donateInput}
required
ref={customAmountElement}
type="number"
@ -148,8 +151,8 @@ export const Donate = () => {
</div>
</div>
<div class="form-group" id="payment-type" classList={{ showing: showingPayment() }}>
<div class="btn-group payment-choose" data-toggle="buttons">
<div class={styles.formGroup} id="payment-type" classList={{ showing: showingPayment() }}>
<div class={clsx(styles.btnGroup, styles.paymentChoose)} data-toggle="buttons">
<input
type="radio"
autocomplete="off"
@ -158,7 +161,11 @@ export const Donate = () => {
onClick={() => setPeriod(once)}
checked={period() === once}
/>
<label for="once" class="btn payment-type" classList={{ active: period() === once }}>
<label
for="once"
class={clsx(styles.btn, styles.paymentType)}
classList={{ active: period() === once }}
>
{t('One time')}
</label>
<input
@ -169,16 +176,20 @@ export const Donate = () => {
onClick={() => setPeriod(monthly)}
checked={period() === monthly}
/>
<label for="monthly" class="btn payment-type" classList={{ active: period() === monthly }}>
<label
for="monthly"
class={clsx(styles.btn, styles.paymentType)}
classList={{ active: period() === monthly }}
>
{t('Every month')}
</label>
</div>
</div>
<div class="form-group">
<a href={''} class="btn send-btn donate" onClick={show}>
<div class={styles.formGroup}>
<button type="button" class={clsx(styles.btn, styles.sendBtn)} onClick={show}>
{t('Help discours to grow')}
</a>
</button>
</div>
</form>
)

View File

@ -1,5 +1,5 @@
import { hideModal } from '../../stores/ui'
import { useLocalize } from '../../context/localize'
import { hideModal } from '../../stores/ui'
import { Button } from '../_shared/Button'
export const Feedback = () => {
@ -14,9 +14,9 @@ export const Feedback = () => {
method,
headers: {
accept: 'application/json',
'content-type': 'application/json; charset=utf-8'
'content-type': 'application/json; charset=utf-8',
},
body: JSON.stringify({ contact: contactElement?.value, message: msgElement?.textContent })
body: JSON.stringify({ contact: contactElement?.value, message: msgElement?.textContent }),
})
hideModal()
}

View File

@ -1,10 +1,11 @@
import { clsx } from 'clsx'
import { createMemo, For } from 'solid-js'
import styles from './Footer.module.scss'
import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon'
import { Subscribe } from '../_shared/Subscribe'
import { clsx } from 'clsx'
import { useLocalize } from '../../context/localize'
import styles from './Footer.module.scss'
export const Footer = () => {
const { t, lang } = useLocalize()
@ -17,25 +18,25 @@ export const Footer = () => {
items: [
{
title: 'Manifest',
slug: '/about/manifest'
slug: '/about/manifest',
},
{
title: 'How it works',
slug: '/about/guide'
slug: '/about/guide',
},
{
title: 'Dogma',
slug: '/about/dogma'
slug: '/about/dogma',
},
{
title: 'Principles',
slug: '/about/principles'
slug: '/about/principles',
},
{
title: 'How to write an article',
slug: '/how-to-write-a-good-article'
}
]
slug: '/how-to-write-a-good-article',
},
],
},
{
@ -43,25 +44,25 @@ export const Footer = () => {
items: [
{
title: 'Suggest an idea',
slug: '/connect'
slug: '/connect',
},
{
title: 'Become an author',
slug: '/create'
slug: '/create',
},
{
title: 'Support us',
slug: '/about/help'
slug: '/about/help',
},
{
title: 'Feedback',
slug: '/#feedback'
slug: '/#feedback',
},
{
title: 'Work with us',
slug: 'https://docs.google.com/forms/d/e/1FAIpQLSeNNvIzKlXElJtkPkYiXl-jQjlvsL9u4-kpnoRjz1O8Wo40xQ/viewform'
}
]
slug: 'https://docs.google.com/forms/d/e/1FAIpQLSeNNvIzKlXElJtkPkYiXl-jQjlvsL9u4-kpnoRjz1O8Wo40xQ/viewform',
},
],
},
{
@ -69,46 +70,46 @@ export const Footer = () => {
items: [
{
title: 'Authors',
slug: '/authors'
slug: '/authors',
},
{
title: 'Communities',
slug: '/community'
slug: '/community',
},
{
title: 'Partners',
slug: '/about/partners'
slug: '/about/partners',
},
{
title: 'Special projects',
slug: '/about/projects'
slug: '/about/projects',
},
{
title: changeLangTitle(),
slug: changeLangLink(),
rel: 'external'
}
]
}
rel: 'external',
},
],
},
])
const SOCIAL = [
{
name: 'facebook',
href: 'https://facebook.com/discoursio'
href: 'https://facebook.com/discoursio',
},
{
name: 'vk',
href: 'https://vk.com/discoursio'
href: 'https://vk.com/discoursio',
},
{
name: 'twitter',
href: 'https://twitter.com/discours_io'
href: 'https://twitter.com/discours_io',
},
{
name: 'telegram',
href: 'https://t.me/discoursio'
}
href: 'https://t.me/discoursio',
},
]
return (
<footer class={styles.discoursFooter}>
@ -143,7 +144,7 @@ export const Footer = () => {
<div class={clsx(styles.footerCopyright, 'row')}>
<div class="col-md-18 col-lg-20">
{t(
'Independant magazine with an open horizontal cooperation about culture, science and society'
'Independant magazine with an open horizontal cooperation about culture, science and society',
)}
. {t('Discours')} &copy; 2015&ndash;{new Date().getFullYear()}{' '}
<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 { useRouter } from '../../stores/router'
import { showModal } from '../../stores/ui'
import { AuthModalSearchParams } from '../Nav/AuthModal/types'
import styles from './Hero.module.scss'
export default () => {
const { t } = useLocalize()
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
@ -17,7 +17,7 @@ export default () => {
<h4 innerHTML={t('Horizontal collaborative journalistic platform')} />
<p
innerHTML={t(
'Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects'
'Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects',
)}
/>
<div class={styles.aboutDiscoursActions}>
@ -29,7 +29,7 @@ export default () => {
onClick={() => {
showModal('auth')
changeSearchParam({
mode: 'register'
mode: 'register',
})
}}
>

View File

@ -1,12 +1,15 @@
import { clsx } from 'clsx'
import styles from './Draft.module.scss'
import type { Shout } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon'
import { useLocalize } from '../../context/localize'
import { useConfirm } from '../../context/confirm'
import { useSnackbar } from '../../context/snackbar'
import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
import { useConfirm } from '../../context/confirm'
import { useLocalize } from '../../context/localize'
import { useSnackbar } from '../../context/snackbar'
import { router } from '../../stores/router'
import { Icon } from '../_shared/Icon'
import styles from './Draft.module.scss'
type Props = {
class?: string
@ -18,11 +21,11 @@ type Props = {
export const Draft = (props: Props) => {
const { t, formatDate } = useLocalize()
const {
actions: { showConfirm }
actions: { showConfirm },
} = useConfirm()
const {
actions: { showSnackbar }
actions: { showSnackbar },
} = useSnackbar()
const handlePublishLinkClick = (e) => {
@ -37,7 +40,7 @@ export const Draft = (props: Props) => {
confirmBody: t('Are you sure you want to delete this draft?'),
confirmButtonLabel: t('Delete'),
confirmButtonVariant: 'danger',
declineButtonVariant: 'primary'
declineButtonVariant: 'primary',
})
if (isConfirmed) {
props.onDelete(props.shout)

View File

@ -1,12 +1,15 @@
import { Buffer } from 'buffer'
import { clsx } from 'clsx'
import styles from './AudioUploader.module.scss'
import { DropArea } from '../../_shared/DropArea'
import { useLocalize } from '../../../context/localize'
import { Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { MediaItem } from '../../../pages/types'
import { composeMediaItems } from '../../../utils/composeMediaItems'
import { DropArea } from '../../_shared/DropArea'
import { AudioPlayer } from '../../Article/AudioPlayer'
import { Buffer } from 'buffer'
import styles from './AudioUploader.module.scss'
window.Buffer = Buffer

View File

@ -1,7 +1,9 @@
import { clsx } from 'clsx'
import styles from './AutoSaveNotice.module.scss'
import { Loading } from '../../_shared/Loading'
import { useLocalize } from '../../../context/localize'
import { Loading } from '../../_shared/Loading'
import styles from './AutoSaveNotice.module.scss'
type Props = {
active: boolean

View File

@ -1,9 +1,11 @@
import type { Editor } from '@tiptap/core'
import styles from './BubbleMenu.module.scss'
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon'
import { Popover } from '../../_shared/Popover'
import styles from './BubbleMenu.module.scss'
type Props = {
editor: Editor
ref: (el: HTMLElement) => void

View File

@ -1,12 +1,14 @@
import type { Editor } from '@tiptap/core'
import styles from './BubbleMenu.module.scss'
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize'
import { Popover } from '../../_shared/Popover'
import { UploadModalContent } from '../UploadModalContent'
import { Modal } from '../../Nav/Modal'
import { UploadedFile } from '../../../pages/types'
import { renderUploadedImage } from '../../../utils/renderUploadedImage'
import { Icon } from '../../_shared/Icon'
import { Popover } from '../../_shared/Popover'
import { Modal } from '../../Nav/Modal'
import { UploadModalContent } from '../UploadModalContent'
import styles from './BubbleMenu.module.scss'
type Props = {
editor: Editor

View File

@ -1,9 +1,12 @@
import { createSignal, Show, For } from 'solid-js'
import type { Editor } from '@tiptap/core'
import styles from './BubbleMenu.module.scss'
import { clsx } from 'clsx'
import { Icon } from '../../_shared/Icon'
import { createSignal, Show, For } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon'
import styles from './BubbleMenu.module.scss'
type Props = {
editor: Editor

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 { HocuspocusProvider } from '@hocuspocus/provider'
import { isTextSelection } from '@tiptap/core'
import { Bold } from '@tiptap/extension-bold'
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
import { Dropcursor } from '@tiptap/extension-dropcursor'
import { Italic } from '@tiptap/extension-italic'
import { Strike } from '@tiptap/extension-strike'
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
import { Underline } from '@tiptap/extension-underline'
import { FloatingMenu } from '@tiptap/extension-floating-menu'
import { BulletList } from '@tiptap/extension-bullet-list'
import { OrderedList } from '@tiptap/extension-ordered-list'
import { ListItem } from '@tiptap/extension-list-item'
import { CharacterCount } from '@tiptap/extension-character-count'
import { Placeholder } from '@tiptap/extension-placeholder'
import { Collaboration } from '@tiptap/extension-collaboration'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
import { Document } from '@tiptap/extension-document'
import { Dropcursor } from '@tiptap/extension-dropcursor'
import { FloatingMenu } from '@tiptap/extension-floating-menu'
import Focus from '@tiptap/extension-focus'
import { Gapcursor } from '@tiptap/extension-gapcursor'
import { HardBreak } from '@tiptap/extension-hard-break'
import { Heading } from '@tiptap/extension-heading'
import { Highlight } from '@tiptap/extension-highlight'
import { Link } from '@tiptap/extension-link'
import { Document } from '@tiptap/extension-document'
import { Text } from '@tiptap/extension-text'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
import { isTextSelection } from '@tiptap/core'
import { Paragraph } from '@tiptap/extension-paragraph'
import Focus from '@tiptap/extension-focus'
import { Collaboration } from '@tiptap/extension-collaboration'
import { HocuspocusProvider } from '@hocuspocus/provider'
import { CustomBlockquote } from './extensions/CustomBlockquote'
import { Figure } from './extensions/Figure'
import { Figcaption } from './extensions/Figcaption'
import { Embed } from './extensions/Embed'
import { useSession } from '../../context/session'
import { useLocalize } from '../../context/localize'
import { useEditorContext } from '../../context/editor'
import { TrailingNode } from './extensions/TrailingNode'
import Article from './extensions/Article'
import { TextBubbleMenu } from './TextBubbleMenu'
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu'
import './Prosemirror.scss'
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
import { Image } from '@tiptap/extension-image'
import { Footnote } from './extensions/Footnote'
import { Italic } from '@tiptap/extension-italic'
import { Link } from '@tiptap/extension-link'
import { ListItem } from '@tiptap/extension-list-item'
import { OrderedList } from '@tiptap/extension-ordered-list'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Placeholder } from '@tiptap/extension-placeholder'
import { Strike } from '@tiptap/extension-strike'
import { Text } from '@tiptap/extension-text'
import { Underline } from '@tiptap/extension-underline'
import { createEffect, createSignal, onCleanup } from 'solid-js'
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
import uniqolor from 'uniqolor'
import * as Y from 'yjs'
import { useEditorContext } from '../../context/editor'
import { useLocalize } from '../../context/localize'
import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/snackbar'
import { handleImageUpload } from '../../utils/handleImageUpload'
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu'
import Article from './extensions/Article'
import { CustomBlockquote } from './extensions/CustomBlockquote'
import { Embed } from './extensions/Embed'
import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure'
import { Footnote } from './extensions/Footnote'
import { TrailingNode } from './extensions/TrailingNode'
import { TextBubbleMenu } from './TextBubbleMenu'
import './Prosemirror.scss'
type Props = {
shoutId: number
initialContent?: string
@ -61,7 +65,7 @@ const allowedImageTypes = new Set([
'image/png',
'image/tiff',
'image/webp',
'image/x-icon'
'image/x-icon',
])
const yDocs: Record<string, Doc> = {}
@ -75,7 +79,7 @@ export const Editor = (props: Props) => {
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
const {
actions: { showSnackbar }
actions: { showSnackbar },
} = useSnackbar()
const docName = `shout-${props.shoutId}`
@ -88,47 +92,47 @@ export const Editor = (props: Props) => {
providers[docName] = new HocuspocusProvider({
url: 'wss://hocuspocus.discours.io',
name: docName,
document: yDocs[docName]
document: yDocs[docName],
})
}
const editorElRef: {
current: HTMLDivElement
} = {
current: null
current: null,
}
const textBubbleMenuRef: {
current: HTMLDivElement
} = {
current: null
current: null,
}
const incutBubbleMenuRef: {
current: HTMLElement
} = {
current: null
current: null,
}
const figureBubbleMenuRef: {
current: HTMLElement
} = {
current: null
current: null,
}
const blockquoteBubbleMenuRef: {
current: HTMLElement
} = {
current: null
current: null,
}
const floatingMenuRef: {
current: HTMLDivElement
} = {
current: null
current: null,
}
const ImageFigure = Figure.extend({
name: 'capturedImage',
content: 'figcaption image'
content: 'figcaption image',
})
const handleClipboardPaste = async () => {
@ -149,7 +153,7 @@ export const Editor = (props: Props) => {
source: blob.toString(),
name: file.name,
size: file.size,
file
file,
}
showSnackbar({ body: t('Uploading image') })
@ -166,17 +170,17 @@ export const Editor = (props: Props) => {
content: [
{
type: 'text',
text: result.originalFilename
}
]
text: result.originalFilename,
},
],
},
{
type: 'image',
attrs: {
src: result.url
}
}
]
src: result.url,
},
},
],
})
.run()
} catch (error) {
@ -190,7 +194,7 @@ export const Editor = (props: Props) => {
element: editorElRef.current,
editorProps: {
attributes: {
class: 'articleEditor'
class: 'articleEditor',
},
transformPastedHTML(html) {
return html.replaceAll(/<img.*?>/g, '')
@ -198,7 +202,7 @@ export const Editor = (props: Props) => {
handlePaste: () => {
handleClipboardPaste()
return false
}
},
},
extensions: [
Document,
@ -211,31 +215,31 @@ export const Editor = (props: Props) => {
Strike,
HorizontalRule.configure({
HTMLAttributes: {
class: 'horizontalRule'
}
class: 'horizontalRule',
},
}),
Underline,
Link.configure({
openOnClick: false
openOnClick: false,
}),
Heading.configure({
levels: [2, 3, 4]
levels: [2, 3, 4],
}),
BulletList,
OrderedList,
ListItem,
Collaboration.configure({
document: yDocs[docName]
document: yDocs[docName],
}),
CollaborationCursor.configure({
provider: providers[docName],
user: {
name: user().name,
color: uniqolor(user().slug).color
}
color: uniqolor(user().slug).color,
},
}),
Placeholder.configure({
placeholder: t('Add a link or click plus to embed media')
placeholder: t('Add a link or click plus to embed media'),
}),
Focus,
Gapcursor,
@ -243,8 +247,8 @@ export const Editor = (props: Props) => {
Highlight.configure({
multicolor: true,
HTMLAttributes: {
class: 'highlight'
}
class: 'highlight',
},
}),
ImageFigure,
Image,
@ -267,8 +271,8 @@ export const Editor = (props: Props) => {
return result
},
tippyOptions: {
sticky: true
}
sticky: true,
},
}),
BubbleMenu.configure({
pluginKey: 'blockquoteBubbleMenu',
@ -286,8 +290,8 @@ export const Editor = (props: Props) => {
if (selectedElement) {
return selectedElement.getBoundingClientRect()
}
}
}
},
},
}),
BubbleMenu.configure({
pluginKey: 'incutBubbleMenu',
@ -305,31 +309,31 @@ export const Editor = (props: Props) => {
if (selectedElement) {
return selectedElement.getBoundingClientRect()
}
}
}
},
},
}),
BubbleMenu.configure({
pluginKey: 'imageBubbleMenu',
element: figureBubbleMenuRef.current,
shouldShow: ({ editor: e, view }) => {
return view.hasFocus() && e.isActive('image')
}
},
}),
FloatingMenu.configure({
tippyOptions: {
placement: 'left'
placement: 'left',
},
element: floatingMenuRef.current
element: floatingMenuRef.current,
}),
TrailingNode,
Article
Article,
],
enablePasteRules: [Link],
content: initialContent ?? null
content: initialContent ?? null,
}))
const {
actions: { countWords, setEditor }
actions: { countWords, setEditor },
} = useEditorContext()
setEditor(editor)
@ -341,7 +345,7 @@ export const Editor = (props: Props) => {
if (html()) {
countWords({
characters: editor().storage.characterCount.characters(),
words: editor().storage.characterCount.words()
words: editor().storage.characterCount.words(),
})
}
})

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 { showModal } from '../../../stores/ui'
import { UploadModalContent } from '../UploadModalContent'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import type { Editor } from '@tiptap/core'
import { createEffect, createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { UploadedFile } from '../../../pages/types'
import { showModal } from '../../../stores/ui'
import { renderUploadedImage } from '../../../utils/renderUploadedImage'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { Icon } from '../../_shared/Icon'
import { Modal } from '../../Nav/Modal'
import { InlineForm } from '../InlineForm'
import { UploadModalContent } from '../UploadModalContent'
import { Menu } from './Menu'
import styles from './EditorFloatingMenu.module.scss'
type FloatingMenuProps = {
editor: Editor
@ -20,10 +23,17 @@ type FloatingMenuProps = {
}
const embedData = async (data) => {
const result = (await HTMLParser(data, false)) as JSONContent
if ('type' in result && result.type === 'iframe') {
return result.attributes
const element = document.createRange().createContextualFragment(data)
const { attributes } = element.firstChild as HTMLIFrameElement
const result: { src: string } = { src: '' }
for (let i = 0; i < attributes.length; i++) {
const attribute = attributes[i]
result[attribute.name] = attribute.value
}
return result
}
export const EditorFloatingMenu = (props: FloatingMenuProps) => {
@ -39,8 +49,8 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
}
const validateEmbed = async (value) => {
const iframeData = (await HTMLParser(value, false)) as JSONContent
if (iframeData.type !== 'iframe') {
const element = document.createRange().createContextualFragment(value)
if (element.firstChild?.nodeName !== 'IFRAME') {
return t('Error')
}
}
@ -74,7 +84,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
if (menuOpen()) {
setMenuOpen(false)
}
}
},
})
const handleUpload = (image: UploadedFile) => {

View File

@ -1,7 +1,8 @@
import styles from './Menu.module.scss'
import { useLocalize } from '../../../../context/localize'
import { Icon } from '../../../_shared/Icon'
import { Popover } from '../../../_shared/Popover'
import { useLocalize } from '../../../../context/localize'
import styles from './Menu.module.scss'
export type MenuItem = 'image' | 'embed' | 'horizontal-rule'

View File

@ -39,7 +39,9 @@
border: 1px solid #e9e9ee;
border-radius: 2px;
opacity: 0;
transition: height 0.3s ease-in-out, opacity 0.3s ease-in-out;
transition:
height 0.3s ease-in-out,
opacity 0.3s ease-in-out;
&.visible {
height: 32px;

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 { Popover } from '../../_shared/Popover'
import { createSignal, onMount } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon'
import { Popover } from '../../_shared/Popover'
import styles from './InlineForm.module.scss'
type Props = {
onClose: () => void

View File

@ -1,8 +1,9 @@
import { Editor } from '@tiptap/core'
import { createEditorTransaction } from 'solid-tiptap'
import { useLocalize } from '../../../context/localize'
import { validateUrl } from '../../../utils/validateUrl'
import { InlineForm } from '../InlineForm'
import { useLocalize } from '../../../context/localize'
import { createEditorTransaction } from 'solid-tiptap'
type Props = {
editor: Editor
@ -24,7 +25,7 @@ export const InsertLinkForm = (props: Props) => {
() => props.editor,
(ed) => {
return (ed && ed.getAttributes('link').href) || ''
}
},
)
const handleClearLinkForm = () => {
if (currentUrl()) {

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 { router } from '../../../stores/router'
import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js'
import { useEditorHTML } from 'solid-tiptap'
import Typograf from 'typograf'
import { createSignal, Show } from 'solid-js'
import { useEditorContext } from '../../../context/editor'
import { useLocalize } from '../../../context/localize'
import { router } from '../../../stores/router'
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { Button } from '../../_shared/Button'
import { DarkModeToggle } from '../../_shared/DarkModeToggle'
import { Icon } from '../../_shared/Icon'
import styles from './Panel.module.scss'
const typograf = new Typograf({ locale: ['ru', 'en-US'] })
@ -26,11 +28,11 @@ export const Panel = (props: Props) => {
wordCounter,
editorRef,
form,
actions: { toggleEditorPanel, saveShout, publishShout }
actions: { toggleEditorPanel, saveShout, publishShout },
} = useEditorContext()
const containerRef: { current: HTMLElement } = {
current: null
current: null,
}
const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false)
@ -39,7 +41,7 @@ export const Panel = (props: Props) => {
useOutsideClickHandler({
containerRef,
predicate: () => isEditorPanelVisible(),
handler: () => toggleEditorPanel()
handler: () => toggleEditorPanel(),
})
useEscKeyDownHandler(() => {

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 { Portal } from 'solid-js/web'
import {
@ -5,34 +17,25 @@ import {
createTiptapEditor,
useEditorHTML,
useEditorIsEmpty,
useEditorIsFocused
useEditorIsFocused,
} from 'solid-tiptap'
import { useEditorContext } from '../../context/editor'
import { Document } from '@tiptap/extension-document'
import { Text } from '@tiptap/extension-text'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Bold } from '@tiptap/extension-bold'
import { Button } from '../_shared/Button'
import { useLocalize } from '../../context/localize'
import { UploadedFile } from '../../pages/types'
import { hideModal, showModal } from '../../stores/ui'
import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon'
import { Popover } from '../_shared/Popover'
import { Italic } from '@tiptap/extension-italic'
import { Modal } from '../Nav/Modal'
import { hideModal, showModal } from '../../stores/ui'
import { Blockquote } from '@tiptap/extension-blockquote'
import { UploadModalContent } from './UploadModalContent'
import { clsx } from 'clsx'
import styles from './SimplifiedEditor.module.scss'
import { Placeholder } from '@tiptap/extension-placeholder'
import { InsertLinkForm } from './InsertLinkForm'
import { Link } from '@tiptap/extension-link'
import { UploadedFile } from '../../pages/types'
import { Figure } from './extensions/Figure'
import { Image } from '@tiptap/extension-image'
import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure'
import { LinkBubbleMenuModule } from './LinkBubbleMenu'
import { TextBubbleMenu } from './TextBubbleMenu'
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
import { CharacterCount } from '@tiptap/extension-character-count'
import { UploadModalContent } from './UploadModalContent'
import styles from './SimplifiedEditor.module.scss'
type Props = {
placeholder: string
@ -61,33 +64,39 @@ export const MAX_DESCRIPTION_LIMIT = 400
const SimplifiedEditor = (props: Props) => {
const { t } = useLocalize()
const [counter, setCounter] = createSignal<number>()
const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false)
const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false)
const wrapperEditorElRef: {
current: HTMLElement
} = {
current: null
current: null,
}
const editorElRef: {
current: HTMLElement
} = {
current: null
current: null,
}
const textBubbleMenuRef: {
current: HTMLDivElement
} = {
current: null
current: null,
}
const linkBubbleMenuRef: {
current: HTMLDivElement
} = {
current: null,
}
const {
actions: { setEditor }
actions: { setEditor },
} = useEditorContext()
const ImageFigure = Figure.extend({
name: 'capturedImage',
content: 'figcaption image'
content: 'figcaption image',
})
const content = props.initialContent
@ -95,8 +104,8 @@ const SimplifiedEditor = (props: Props) => {
element: editorElRef.current,
editorProps: {
attributes: {
class: styles.simplifiedEditorField
}
class: styles.simplifiedEditorField,
},
},
extensions: [
Document,
@ -105,16 +114,16 @@ const SimplifiedEditor = (props: Props) => {
Bold,
Italic,
Link.configure({
openOnClick: false
openOnClick: false,
}),
CharacterCount.configure({
limit: MAX_DESCRIPTION_LIMIT
limit: MAX_DESCRIPTION_LIMIT,
}),
Blockquote.configure({
HTMLAttributes: {
class: styles.blockQuote
}
class: styles.blockQuote,
},
}),
BubbleMenu.configure({
pluginKey: 'textBubbleMenu',
@ -124,18 +133,27 @@ const SimplifiedEditor = (props: Props) => {
const { selection } = state
const { empty } = selection
return view.hasFocus() && !empty
}
},
}),
BubbleMenu.configure({
pluginKey: 'linkBubbleMenu',
element: linkBubbleMenuRef.current,
shouldShow: ({ state }) => {
const { selection } = state
const { empty } = selection
return !empty && shouldShowLinkBubbleMenu()
},
}),
ImageFigure,
Image,
Figcaption,
Placeholder.configure({
emptyNodeClass: styles.emptyNode,
placeholder: props.placeholder
})
placeholder: props.placeholder,
}),
],
autofocus: props.autoFocus,
content: content ?? null
content: content ?? null,
}))
setEditor(editor)
@ -147,7 +165,7 @@ const SimplifiedEditor = (props: Props) => {
() => editor(),
(ed) => {
return ed && ed.isActive(name)
}
},
)
const html = useEditorHTML(() => editor())
@ -168,17 +186,17 @@ const SimplifiedEditor = (props: Props) => {
content: [
{
type: 'text',
text: image.originalFilename
}
]
text: image.originalFilename,
},
],
},
{
type: 'image',
attrs: {
src: image.url
}
}
]
src: image.url,
},
},
],
})
.run()
hideModal()
@ -210,7 +228,7 @@ const SimplifiedEditor = (props: Props) => {
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !editor().state.selection.empty) {
event.preventDefault()
showModal('simplifiedEditorInsertLink')
setShouldShowLinkBubbleMenu(true)
}
}
@ -228,8 +246,6 @@ const SimplifiedEditor = (props: Props) => {
})
}
const handleInsertLink = () => !editor().state.selection.empty && showModal('simplifiedEditorInsertLink')
createEffect(() => {
if (html()) {
setCounter(editor().storage.characterCount.characters())
@ -238,9 +254,13 @@ const SimplifiedEditor = (props: Props) => {
const maxHeightStyle = {
overflow: 'auto',
'max-height': `${props.maxHeight}px`
'max-height': `${props.maxHeight}px`,
}
const handleShowLinkBubble = () => {
editor().chain().focus().run()
setShouldShowLinkBubbleMenu(true)
}
return (
<div
ref={(el) => (wrapperEditorElRef.current = el)}
@ -249,7 +269,7 @@ const SimplifiedEditor = (props: Props) => {
[styles.minimal]: props.variant === 'minimal',
[styles.bordered]: props.variant === 'bordered',
[styles.isFocused]: isFocused() || !isEmpty(),
[styles.labelVisible]: props.label && counter() > 0
[styles.labelVisible]: props.label && counter() > 0,
})}
>
<Show when={props.maxLength && editor()}>
@ -291,7 +311,7 @@ const SimplifiedEditor = (props: Props) => {
<button
ref={triggerRef}
type="button"
onClick={handleInsertLink}
onClick={handleShowLinkBubble}
class={clsx(styles.actionButton, { [styles.active]: isLink() })}
>
<Icon name="editor-link" />
@ -342,11 +362,6 @@ const SimplifiedEditor = (props: Props) => {
</Show>
</div>
</Show>
<Portal>
<Modal variant="narrow" name="simplifiedEditorInsertLink">
<InsertLinkForm editor={editor()} onClose={() => hideModal()} />
</Modal>
</Portal>
<Show when={props.imageEnabled}>
<Portal>
<Modal variant="narrow" name="simplifiedEditorUploadImage">
@ -366,6 +381,12 @@ const SimplifiedEditor = (props: Props) => {
ref={(el) => (textBubbleMenuRef.current = el)}
/>
</Show>
<LinkBubbleMenuModule
shouldShow={shouldShowLinkBubbleMenu()}
editor={editor()}
ref={(el) => (linkBubbleMenuRef.current = el)}
onClose={() => setShouldShowLinkBubbleMenu(false)}
/>
</div>
)
}

View File

@ -1,14 +1,17 @@
import { Switch, Match, createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js'
import type { Editor } from '@tiptap/core'
import styles from './TextBubbleMenu.module.scss'
import { Icon } from '../../_shared/Icon'
import { clsx } from 'clsx'
import { Switch, Match, createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js'
import { createEditorTransaction } from 'solid-tiptap'
import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon'
import { Popover } from '../../_shared/Popover'
import { InsertLinkForm } from '../InsertLinkForm'
import SimplifiedEditor from '../SimplifiedEditor'
import styles from './TextBubbleMenu.module.scss'
type BubbleMenuProps = {
editor: Editor
isCommonMarkup: boolean
@ -22,7 +25,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
const isActive = (name: string, attributes?: unknown) =>
createEditorTransaction(
() => props.editor,
(editor) => editor && editor.isActive(name, attributes)
(editor) => editor && editor.isActive(name, attributes),
)
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
@ -79,7 +82,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
}
const value = ed.getAttributes('footnote').value
setFootNote(value)
}
},
)
const handleAddFootnote = (footnote) => {
@ -148,7 +151,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
<button
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: textSizeBubbleOpen()
[styles.bubbleMenuButtonActive]: textSizeBubbleOpen(),
})}
onClick={toggleTextSizePopup}
>
@ -165,7 +168,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH1()
[styles.bubbleMenuButtonActive]: isH1(),
})}
onClick={() => {
props.editor.chain().focus().toggleHeading({ level: 2 }).run()
@ -182,7 +185,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH2()
[styles.bubbleMenuButtonActive]: isH2(),
})}
onClick={() => {
props.editor.chain().focus().toggleHeading({ level: 3 }).run()
@ -199,7 +202,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH3()
[styles.bubbleMenuButtonActive]: isH3(),
})}
onClick={() => {
props.editor.chain().focus().toggleHeading({ level: 4 }).run()
@ -219,7 +222,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isQuote()
[styles.bubbleMenuButtonActive]: isQuote(),
})}
onClick={handleSetPunchline}
>
@ -233,7 +236,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isPunchLine()
[styles.bubbleMenuButtonActive]: isPunchLine(),
})}
onClick={handleSetQuote}
>
@ -250,7 +253,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isIncut()
[styles.bubbleMenuButtonActive]: isIncut(),
})}
onClick={() => {
props.editor.chain().focus().toggleArticle().run()
@ -274,7 +277,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isBold()
[styles.bubbleMenuButtonActive]: isBold(),
})}
onClick={() => props.editor.chain().focus().toggleBold().run()}
>
@ -288,7 +291,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isItalic()
[styles.bubbleMenuButtonActive]: isItalic(),
})}
onClick={() => props.editor.chain().focus().toggleItalic().run()}
>
@ -304,7 +307,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isHighlight()
[styles.bubbleMenuButtonActive]: isHighlight(),
})}
onClick={() => props.editor.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()}
>
@ -321,7 +324,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
type="button"
onClick={() => setLinkEditorOpen(true)}
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isLink()
[styles.bubbleMenuButtonActive]: isLink(),
})}
>
<Icon name="editor-link" />
@ -336,7 +339,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isFootnote()
[styles.bubbleMenuButtonActive]: isFootnote(),
})}
onClick={handleOpenFootnoteEditor}
>
@ -349,7 +352,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
<button
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: listBubbleOpen()
[styles.bubbleMenuButtonActive]: listBubbleOpen(),
})}
onClick={toggleListPopup}
>
@ -366,7 +369,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isBulletList()
[styles.bubbleMenuButtonActive]: isBulletList(),
})}
onClick={() => {
props.editor.chain().focus().toggleBulletList().run()
@ -383,7 +386,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isOrderedList()
[styles.bubbleMenuButtonActive]: isOrderedList(),
})}
onClick={() => {
props.editor.chain().focus().toggleOrderedList().run()

View File

@ -1,13 +1,17 @@
import type { Topic } from '../../../graphql/types.gen'
import { createOptions, Select } from '@thisbeyond/solid-select'
import { useLocalize } from '../../../context/localize'
import '@thisbeyond/solid-select/style.css'
import './TopicSelect.scss'
import styles from './TopicSelect.module.scss'
import { clsx } from 'clsx'
import { createSignal } from 'solid-js'
import { slugify } from '../../../utils/slugify'
import { useLocalize } from '../../../context/localize'
import { clone } from '../../../utils/clone'
import { slugify } from '../../../utils/slugify'
import '@thisbeyond/solid-select/style.css'
import './TopicSelect.scss'
import styles from './TopicSelect.module.scss'
type TopicSelectProps = {
topics: Topic[]
@ -33,7 +37,7 @@ export const TopicSelect = (props: TopicSelectProps) => {
disable: (topic) => {
return props.selectedTopics.some((selectedTopic) => selectedTopic.slug === topic.slug)
},
createable: createValue
createable: createValue,
})
const handleChange = (selectedTopics: Topic[]) => {
@ -57,7 +61,7 @@ export const TopicSelect = (props: TopicSelectProps) => {
return (
<div
class={clsx(styles.selectedItem, {
[styles.mainTopic]: isMainTopic
[styles.mainTopic]: isMainTopic,
})}
onClick={() => handleSelectedItemClick(item)}
>

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 { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Loading } from '../../_shared/Loading'
import { verifyImg } from '../../../utils/verifyImg'
import { UploadedFile } from '../../../pages/types'
import { hideModal } from '../../../stores/ui'
import { handleImageUpload } from '../../../utils/handleImageUpload'
import { verifyImg } from '../../../utils/verifyImg'
import { Button } from '../../_shared/Button'
import { Icon } from '../../_shared/Icon'
import { Loading } from '../../_shared/Loading'
import { InlineForm } from '../InlineForm'
import styles from './UploadModalContent.module.scss'
type Props = {
onClose: (image?: UploadedFile) => void
@ -46,7 +48,7 @@ export const UploadModalContent = (props: Props) => {
source: blob.toString(),
name: file.name,
size: file.size,
file: file
file: file,
}
await runUpload(fileToUpload)
} catch (error) {
@ -70,7 +72,7 @@ export const UploadModalContent = (props: Props) => {
} else {
setDragError(t('Image format not supported'))
}
}
},
})
const handleDrag = (event: MouseEvent) => {
if (event.type === 'dragenter' || event.type === 'dragover') {

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 { createDropzone } from '@solid-primitives/upload'
import { clsx } from 'clsx'
import { createSignal, For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSnackbar } from '../../../context/snackbar'
import { composeMediaItems } from '../../../utils/composeMediaItems'
import { validateUrl } from '../../../utils/validateUrl'
import { VideoPlayer } from '../../_shared/VideoPlayer'
import styles from './VideoUploader.module.scss'
type Props = {
video: MediaItem[]
onVideoAdd: (value: MediaItem[]) => void
@ -22,13 +25,13 @@ export const VideoUploader = (props: Props) => {
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
const {
actions: { showSnackbar }
actions: { showSnackbar },
} = useSnackbar()
const urlInput: {
current: HTMLInputElement
} = {
current: null
current: null,
}
const { setRef: dropzoneRef, files: droppedFiles } = createDropzone({
@ -39,13 +42,13 @@ export const VideoUploader = (props: Props) => {
} else if (droppedFiles()[0].file.type.startsWith('video/')) {
await showSnackbar({
body: t(
'This functionality is currently not available, we would like to work on this issue. Use the download link.'
)
'This functionality is currently not available, we would like to work on this issue. Use the download link.',
),
})
} else {
setError(t('Video format not supported'))
}
}
},
})
const handleDrag = (event) => {
if (event.type === 'dragenter' || event.type === 'dragover') {
@ -84,8 +87,8 @@ export const VideoUploader = (props: Props) => {
onClick={() =>
showSnackbar({
body: t(
'This functionality is currently not available, we would like to work on this issue. Use the download link.'
)
'This functionality is currently not available, we would like to work on this issue. Use the download link.',
),
})
}
ref={dropzoneRef}

View File

@ -14,8 +14,8 @@ export default Node.create({
name: 'article',
defaultOptions: {
HTMLAttributes: {
'data-type': 'incut'
}
'data-type': 'incut',
},
},
group: 'block',
content: 'block+',
@ -23,8 +23,8 @@ export default Node.create({
parseHTML() {
return [
{
tag: 'article'
}
tag: 'article',
},
]
},
@ -35,11 +35,11 @@ export default Node.create({
addAttributes() {
return {
'data-float': {
default: null
default: null,
},
'data-bg': {
default: null
}
default: null,
},
}
},
@ -60,7 +60,7 @@ export default Node.create({
(value) =>
({ commands }) => {
return commands.updateAttributes(this.name, { 'data-bg': value })
},
}
}
}
},
})

View File

@ -14,18 +14,18 @@ declare module '@tiptap/core' {
export const CustomBlockquote = Blockquote.extend({
name: 'blockquote',
defaultOptions: {
HTMLAttributes: {}
HTMLAttributes: {},
},
group: 'block',
content: 'block+',
addAttributes() {
return {
'data-float': {
default: null
default: null,
},
'data-type': {
default: null
}
default: null,
},
}
},
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -41,7 +41,7 @@ export const CustomBlockquote = Blockquote.extend({
(value) =>
({ commands }) => {
return commands.updateAttributes(this.name, { 'data-float': value })
},
}
}
}
},
})

View File

@ -16,20 +16,20 @@ export const CustomImage = Image.extend({
addAttributes() {
return {
src: {
default: null
default: null,
},
alt: {
default: null
default: null,
},
width: {
default: null
default: null,
},
height: {
default: null
default: null,
},
'data-float': {
default: null
}
default: null,
},
}
},
addCommands() {
@ -39,14 +39,14 @@ export const CustomImage = Image.extend({
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: options
attrs: options,
})
},
setImageFloat:
(value) =>
({ commands }) => {
return commands.updateAttributes(this.name, { 'data-float': value })
},
}
}
}
},
})

View File

@ -25,14 +25,14 @@ export const Embed = Node.create<IframeOptions>({
return {
src: { default: null },
width: { default: null },
height: { default: null }
height: { default: null },
}
},
parseHTML() {
return [
{
tag: 'iframe'
}
tag: 'iframe',
},
]
},
renderHTML({ HTMLAttributes }) {
@ -49,7 +49,7 @@ export const Embed = Node.create<IframeOptions>({
iframe.src = node.attrs.src
div.append(iframe)
return {
dom: div
dom: div,
}
}
},
@ -64,7 +64,7 @@ export const Embed = Node.create<IframeOptions>({
tr.replaceRangeWith(selection.from, selection.to, node)
}
return true
},
}
}
}
},
})

View File

@ -12,7 +12,7 @@ export const Figcaption = Node.create({
addOptions() {
return {
HTMLAttributes: {}
HTMLAttributes: {},
}
},
@ -25,8 +25,8 @@ export const Figcaption = Node.create({
parseHTML() {
return [
{
tag: 'figcaption'
}
tag: 'figcaption',
},
]
},
@ -39,7 +39,7 @@ export const Figcaption = Node.create({
(value) =>
({ commands }) => {
return commands.focus(value)
},
}
}
}
},
})

View File

@ -12,7 +12,7 @@ export const Figure = Node.create({
name: 'figure',
addOptions() {
return {
HTMLAttributes: {}
HTMLAttributes: {},
}
},
group: 'block',
@ -22,15 +22,15 @@ export const Figure = Node.create({
addAttributes() {
return {
'data-float': null
'data-float': null,
}
},
parseHTML() {
return [
{
tag: `figure[data-type="${this.name}"]`
}
tag: `figure[data-type="${this.name}"]`,
},
]
},
@ -54,10 +54,10 @@ export const Figure = Node.create({
event.preventDefault()
}
return false
}
}
}
})
},
},
},
}),
]
},
@ -67,7 +67,7 @@ export const Figure = Node.create({
(value) =>
({ commands }) => {
return commands.updateAttributes(this.name, { 'data-float': value })
},
}
}
}
},
})

View File

@ -14,7 +14,7 @@ export const Footnote = Node.create({
name: 'footnote',
addOptions() {
return {
HTMLAttributes: {}
HTMLAttributes: {},
}
},
group: 'inline',
@ -29,18 +29,18 @@ export const Footnote = Node.create({
parseHTML: (element) => element.dataset.value || null,
renderHTML: (attributes) => {
return {
'data-value': attributes.value
}
}
'data-value': attributes.value,
}
},
},
}
},
parseHTML() {
return [
{
tag: 'footnote'
}
tag: 'footnote',
},
]
},
@ -92,7 +92,7 @@ export const Footnote = Node.create({
}
return false
},
}
}
}
},
})

View File

@ -22,7 +22,7 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
addOptions() {
return {
node: 'paragraph',
notAfter: ['paragraph']
notAfter: ['paragraph'],
}
},
@ -61,9 +61,9 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
const lastNode = tr.doc.lastChild
return !nodeEqualsType({ node: lastNode, types: disabledNodes })
}
}
})
},
},
}),
]
}
},
})

View File

@ -413,6 +413,7 @@
}
swiper-slide & {
aspect-ratio: 16/9;
margin-bottom: 0;
@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 { capitalize } from '../../../utils/capitalize'
import { Icon } from '../../_shared/Icon'
import { clsx } from 'clsx'
import { CardTopic } from '../CardTopic'
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { getDescription } from '../../../utils/meta'
import { FeedArticlePopup } from '../FeedArticlePopup'
import { useLocalize } from '../../../context/localize'
import { getPagePath, openPage } from '@nanostores/router'
import { router, useRouter } from '../../../stores/router'
import { Popover } from '../../_shared/Popover'
import { Image } from '../../_shared/Image'
import { clsx } from 'clsx'
import { createMemo, createSignal, For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { router, useRouter } from '../../../stores/router'
import { capitalize } from '../../../utils/capitalize'
import { getDescription } from '../../../utils/meta'
import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image'
import { Popover } from '../../_shared/Popover'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
import { AuthorLink } from '../../Author/AhtorLink'
import stylesHeader from '../../Nav/Header/Header.module.scss'
import { CardTopic } from '../CardTopic'
import { FeedArticlePopup } from '../FeedArticlePopup'
import styles from './ArticleCard.module.scss'
import stylesHeader from '../../Nav/Header/Header.module.scss'
interface ArticleCardProps {
settings?: {
@ -45,7 +48,7 @@ interface ArticleCardProps {
}
const getTitleAndSubtitle = (
article: Shout
article: Shout,
): {
title: string
subtitle: string
@ -90,7 +93,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
event.preventDefault()
openPage(router, 'article', { slug: props.article.slug })
changeSearchParam({
scrollTo: 'comments'
scrollTo: 'comments',
})
}
@ -111,7 +114,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
[styles.shoutCardCompact]: props.settings?.isCompact,
[styles.shoutCardSingle]: props.settings?.isSingle,
[styles.shoutCardBeside]: props.settings?.isBeside,
[styles.shoutCardNoImage]: !props.article.cover
[styles.shoutCardNoImage]: !props.article.cover,
}}
>
<Show when={!props.settings?.noimage && !props.settings?.isFeedMode}>
@ -155,7 +158,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
<div
class={clsx(styles.shoutCardTitlesContainer, {
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode,
})}
>
<a href={getPagePath(router, 'article', { slug: props.article.slug })}>

View File

@ -1,14 +1,18 @@
// TODO: additional entities list column + article
import { For, Show } from 'solid-js'
import { ArticleCard } from './ArticleCard'
import { TopicCard } from '../Topic/Card'
import styles from './Beside.module.scss'
import type { Author, Shout, Topic, User } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon'
import { clsx } from 'clsx'
import { For, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon'
import { AuthorBadge } from '../Author/AuthorBadge'
import { TopicCard } from '../Topic/Card'
import { ArticleCard } from './ArticleCard'
import styles from './Beside.module.scss'
type Props = {
title?: string
@ -36,7 +40,7 @@ export const Beside = (props: Props) => {
'col-lg-8',
styles[
`besideRatingColumn${props.wrapper.charAt(0).toUpperCase() + props.wrapper.slice(1)}`
]
],
)}
>
<Show when={!!props.title}>

View File

@ -1,6 +1,8 @@
import { clsx } from 'clsx'
import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
import { router } from '../../stores/router'
import styles from './CardTopic.module.scss'
type CardTopicProps = {
@ -16,7 +18,7 @@ export const CardTopic = (props: CardTopicProps) => {
<div
class={clsx(styles.shoutTopic, props.class, {
[styles.shoutTopicFloorImportant]: props.isFloorImportant,
[styles.shoutTopicFeedMode]: props.isFeedMode
[styles.shoutTopicFeedMode]: props.isFeedMode,
})}
>
<a href={getPagePath(router, 'topic', { slug: props.slug })}>{props.title}</a>

View File

@ -1,9 +1,12 @@
import styles from './FeedArticlePopup.module.scss'
import type { PopupProps } from '../_shared/Popup'
import { Popup } from '../_shared/Popup'
import { useLocalize } from '../../context/localize'
import { createEffect, createSignal, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { Popup } from '../_shared/Popup'
import styles from './FeedArticlePopup.module.scss'
type FeedArticlePopupProps = {
title: string
shareUrl?: string

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 { JSX } from 'solid-js/jsx-runtime'
import { For, Show } from 'solid-js'
import { ArticleCard } from './ArticleCard'
import './Group.scss'
@ -26,7 +28,7 @@ export default (props: GroupProps) => {
noicon: true,
isFloorImportant: true,
isBigTitle: true,
nodate: true
nodate: true,
}}
/>
</div>
@ -59,7 +61,7 @@ export default (props: GroupProps) => {
isBigTitle: true,
isCompact: true,
isFloorImportant: true,
nodate: true
nodate: true,
}}
/>
)}
@ -76,7 +78,7 @@ export default (props: GroupProps) => {
isBigTitle: true,
isCompact: true,
isFloorImportant: true,
nodate: true
nodate: true,
}}
/>
)}

View File

@ -1,5 +1,7 @@
import { Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen'
import { Show } from 'solid-js'
import { ArticleCard } from './ArticleCard'
export const Row1 = (props: {
@ -19,7 +21,7 @@ export const Row1 = (props: {
isSingle: true,
nodate: props.nodate,
noAuthorLink: props.noAuthorLink,
noauthor: props.noauthor
noauthor: props.noauthor,
}}
/>
</div>

View File

@ -1,11 +1,13 @@
import { createComputed, createSignal, Show, For } from 'solid-js'
import type { Shout } from '../../graphql/types.gen'
import { createComputed, createSignal, Show, For } from 'solid-js'
import { ArticleCard } from './ArticleCard'
const x = [
['12', '12'],
['8', '16'],
['16', '8']
['16', '8'],
]
export const Row2 = (props: {
@ -35,7 +37,7 @@ export const Row2 = (props: {
isWithCover: props.isEqual || x[y()][i()] === '16',
nodate: props.isEqual || props.nodate,
noAuthorLink: props.noAuthorLink,
noauthor: props.noauthor
noauthor: props.noauthor,
}}
/>
</div>

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 { JSX } from 'solid-js/jsx-runtime'
import { For, Show } from 'solid-js'
import { ArticleCard } from './ArticleCard'
export const Row3 = (props: {
@ -24,7 +26,7 @@ export const Row3 = (props: {
settings={{
nodate: props.nodate,
noAuthorLink: props.noAuthorLink,
noauthor: props.noauthor
noauthor: props.noauthor,
}}
/>
</div>

View File

@ -1,4 +1,5 @@
import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './ArticleCard'
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 { For } from 'solid-js'
import { ArticleCard } from './ArticleCard'
export default (props: { articles: Shout[] }) => (
@ -16,7 +18,7 @@ export default (props: { articles: Shout[] }) => (
isWithCover: true,
isBigTitle: true,
isVertical: true,
nodate: true
nodate: true,
}}
/>
</div>

View File

@ -19,11 +19,16 @@
align-items: center;
display: flex;
position: relative;
}
@include media-breakpoint-up(md) {
.sidebarItemNameLabel {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.userpic {
margin-right: 1.2rem;
}
.userpic {
@ -109,7 +114,6 @@
display: inline-block;
line-height: 1;
height: 2.4rem;
margin-bottom: 0.2rem;
margin-right: 0.8rem;
min-width: 2.4rem;
text-align: center;

View File

@ -1,14 +1,16 @@
import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
import { createSignal, For, Show } from 'solid-js'
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { router, useRouter } from '../../../stores/router'
import { useArticlesStore } from '../../../stores/zine/articles'
import { useSeenStore } from '../../../stores/zine/seen'
import { useSession } from '../../../context/session'
import { useLocalize } from '../../../context/localize'
import styles from './Sidebar.module.scss'
import { clsx } from 'clsx'
import { Icon } from '../../_shared/Icon'
import { Userpic } from '../../Author/Userpic'
import { getPagePath } from '@nanostores/router'
import { router, useRouter } from '../../../stores/router'
import styles from './Sidebar.module.scss'
export const Sidebar = () => {
const { t } = useLocalize()
@ -33,7 +35,7 @@ export const Sidebar = () => {
<a
href={getPagePath(router, 'feed')}
class={clsx({
[styles.selected]: page().route === 'feed'
[styles.selected]: page().route === 'feed',
})}
>
<span class={styles.sidebarItemName}>
@ -46,7 +48,7 @@ export const Sidebar = () => {
<a
href={getPagePath(router, 'feedMy')}
class={clsx({
[styles.selected]: page().route === 'feedMy'
[styles.selected]: page().route === 'feedMy',
})}
>
<span class={styles.sidebarItemName}>
@ -59,7 +61,7 @@ export const Sidebar = () => {
<a
href={getPagePath(router, 'feedCollaborations')}
class={clsx({
[styles.selected]: page().route === 'feedCollaborations'
[styles.selected]: page().route === 'feedCollaborations',
})}
>
<span class={styles.sidebarItemName}>
@ -72,7 +74,7 @@ export const Sidebar = () => {
<a
href={getPagePath(router, 'feedDiscussions')}
class={clsx({
[styles.selected]: page().route === 'feedDiscussions'
[styles.selected]: page().route === 'feedDiscussions',
})}
>
<span class={styles.sidebarItemName}>
@ -85,7 +87,7 @@ export const Sidebar = () => {
<a
href={getPagePath(router, 'feedBookmarks')}
class={clsx({
[styles.selected]: page().route === 'feedBookmarks'
[styles.selected]: page().route === 'feedBookmarks',
})}
>
<span class={styles.sidebarItemName}>
@ -98,7 +100,7 @@ export const Sidebar = () => {
<a
href={getPagePath(router, 'feedNotifications')}
class={clsx({
[styles.selected]: page().route === 'feedNotifications'
[styles.selected]: page().route === 'feedNotifications',
})}
>
<span class={styles.sidebarItemName}>
@ -129,7 +131,7 @@ export const Sidebar = () => {
>
<div class={styles.sidebarItemName}>
<Userpic name={author.name} userpic={author.userpic} size="XS" class={styles.userpic} />
{author.name}
<div class={styles.sidebarItemNameLabel}>{author.name}</div>
</div>
</a>
</li>
@ -144,7 +146,7 @@ export const Sidebar = () => {
>
<div class={styles.sidebarItemName}>
<Icon name="hash" class={styles.icon} />
{topic.title}
<div class={styles.sidebarItemNameLabel}>{topic.title}</div>
</div>
</a>
</li>

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 { hideModal } from '../../stores/ui'
import { createSignal, For, createEffect } from 'solid-js'
import { useInbox } from '../../context/inbox'
import { useLocalize } from '../../context/localize'
import { hideModal } from '../../stores/ui'
import InviteUser from './InviteUser'
import styles from './CreateModalContent.module.scss'
type inviteUser = Author & { selected: boolean }
type Props = {
@ -46,7 +49,7 @@ const CreateModalContent = (props: Props) => {
const handleClick = (user) => {
setCollectionToInvite((userCollection) => {
return userCollection.map((clickedUser) =>
user.id === clickedUser.id ? { ...clickedUser, selected: !clickedUser.selected } : clickedUser
user.id === clickedUser.id ? { ...clickedUser, selected: !clickedUser.selected } : clickedUser,
)
})
}

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 { Show, createMemo } from 'solid-js'
import { getImageUrl } from '../../utils/getImageUrl'
import './DialogCard.module.scss'
import styles from './DialogAvatar.module.scss'
type Props = {
name: string
@ -25,7 +27,7 @@ const colors = [
'#668cff',
'#c34cfe',
'#e699ff',
'#6633ff'
'#6633ff',
]
const getById = (letter: string) =>
@ -42,7 +44,7 @@ const DialogAvatar = (props: Props) => {
class={clsx(styles.DialogAvatar, props.class, {
[styles.online]: props.online,
[styles.bordered]: props.bordered,
[styles.small]: props.size === 'small'
[styles.small]: props.size === 'small',
})}
style={{ 'background-color': `${randomBg()}` }}
>

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 GroupDialogAvatar from './GroupDialogAvatar'
import { clsx } from 'clsx'
import styles from './DialogCard.module.scss'
import { Show, Switch, Match, createMemo } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { AuthorBadge } from '../Author/AuthorBadge'
import DialogAvatar from './DialogAvatar'
import GroupDialogAvatar from './GroupDialogAvatar'
import styles from './DialogCard.module.scss'
type DialogProps = {
online?: boolean
message?: string
@ -22,14 +26,14 @@ type DialogProps = {
const DialogCard = (props: DialogProps) => {
const { t, formatTime } = useLocalize()
const companions = createMemo(
() => props.members && props.members.filter((member) => member.id !== props.ownId)
() => props.members && props.members.filter((member) => member.id !== props.ownId),
)
const names = createMemo(
() =>
companions()
?.map((companion) => companion.name)
.join(', ')
.join(', '),
)
return (
@ -37,24 +41,27 @@ const DialogCard = (props: DialogProps) => {
<div
class={clsx(styles.DialogCard, {
[styles.opened]: props.isOpened,
[styles.hovered]: !props.isChatHeader
[styles.hovered]: !props.isChatHeader,
})}
onClick={props.onClick}
>
<Switch>
<Match when={props.members.length === 2}>
<div class={styles.avatar}>
<Switch
fallback={
<Show
when={props.isChatHeader}
fallback={<DialogAvatar name={companions()[0].slug} url={companions()[0].userpic} />}
>
<AuthorBadge nameOnly={true} author={companions()[0]} />
</Show>
fallback={
<div class={styles.avatar}>
<DialogAvatar name={props.members[0].slug} url={props.members[0].userpic} />
</div>
</Match>
}
>
<AuthorBadge nameOnly={true} author={props.members[0]} />
</Show>
}
>
<Match when={props.members.length >= 3}>
<div class={styles.avatar}>
<GroupDialogAvatar users={companions()} />
<GroupDialogAvatar users={props.members} />
</div>
</Match>
</Switch>

View File

@ -1,7 +1,9 @@
import type { Chat } from '../../graphql/types.gen'
import styles from './DialogHeader.module.scss'
import DialogCard from './DialogCard'
import styles from './DialogHeader.module.scss'
type DialogHeader = {
chat: Chat
ownId: number

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 { clsx } from 'clsx'
import { For } from 'solid-js'
import DialogAvatar from './DialogAvatar'
import './DialogCard.module.scss'
import styles from './GroupDialogAvatar.module.scss'
type Props = {
users: ChatMember[]
}

View File

@ -1,8 +1,11 @@
import styles from './InviteUser.module.scss'
import DialogAvatar from './DialogAvatar'
import type { Author } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon'
import DialogAvatar from './DialogAvatar'
import styles from './InviteUser.module.scss'
type DialogProps = {
author: Author
selected: boolean

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 { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon'
import DialogAvatar from './DialogAvatar'
import { MessageActionsPopup } from './MessageActionsPopup'
import QuotedMessage from './QuotedMessage'
import { useLocalize } from '../../context/localize'
import styles from './Message.module.scss'
type Props = {
content: MessageType

View File

@ -1,7 +1,9 @@
import { createEffect, createSignal, For } from 'solid-js'
import type { PopupProps } from '../_shared/Popup'
import { Popup } from '../_shared/Popup'
import { createEffect, createSignal, For } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { Popup } from '../_shared/Popup'
export type MessageActionType = 'reply' | 'copy' | 'pin' | 'forward' | 'select' | 'delete'
@ -18,7 +20,7 @@ export const MessageActionsPopup = (props: MessageActionsPopupProps) => {
{ name: t('Pin'), action: 'pin' },
{ name: t('Forward'), action: 'forward' },
{ name: t('Select'), action: 'select' },
{ name: t('Delete'), action: 'delete' }
{ name: t('Delete'), action: 'delete' },
]
createEffect(() => {
if (props.actionSelect) props.actionSelect(selectedAction())

View File

@ -1,4 +1,5 @@
import { Show } from 'solid-js'
import styles from './MessagesFallback.module.scss'
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 { Show } from 'solid-js'
import { Icon } from '../_shared/Icon'
import styles from './QuotedMessage.module.scss'
type QuotedMessage = {
body: string
@ -17,7 +19,7 @@ const QuotedMessage = (props: QuotedMessage) => {
class={clsx(styles.QuotedMessage, {
[styles.reply]: props.variant === 'reply',
[styles.inline]: props.variant === 'inline',
[styles.own]: props.isOwn
[styles.own]: props.isOwn,
})}
>
<Show when={props.variant === 'reply'}>

View File

@ -1,7 +1,9 @@
import styles from './Search.module.scss'
import { createSignal } from 'solid-js'
import { Icon } from '../_shared/Icon'
import styles from './Search.module.scss'
type Props = {
placeholder: string
onChange: (value: () => string) => void

View File

@ -1,9 +1,11 @@
import styles from './AuthModalHeader.module.scss'
import { Show } from 'solid-js'
import { useLocalize } from '../../../../context/localize'
import { useRouter } from '../../../../stores/router'
import { AuthModalSearchParams } from '../types'
import styles from './AuthModalHeader.module.scss'
type Props = {
modalType: 'login' | 'register'
}
@ -14,7 +16,7 @@ export const AuthModalHeader = (props: Props) => {
const { source } = searchParams()
const generateModalTextsFromSource = (
modalType: 'login' | 'register'
modalType: 'login' | 'register',
): { title: string; description: string } => {
const title = modalType === 'login' ? 'Welcome to Discours' : 'Create account'
@ -22,53 +24,53 @@ export const AuthModalHeader = (props: Props) => {
case 'create': {
return {
title: t(`${title} to publish articles`),
description: ''
description: '',
}
}
case 'bookmark': {
return {
title: t(`${title} to add to your bookmarks`),
description: t(
'In&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': {
return {
title: t(`${title} to participate in discussions`),
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': {
return {
title: t(`${title} to subscribe`),
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': {
return {
title: t(`${title} to subscribe to new publications`),
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': {
return {
title: t(`${title} to vote`),
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: {
return {
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 { ApiError } from '../../../utils/apiClient'
import { useSession } from '../../../context/session'
import { clsx } from 'clsx'
import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui'
import { ApiError } from '../../../utils/apiClient'
import styles from './AuthModal.module.scss'
export const EmailConfirm = () => {
const { t } = useLocalize()
const {
session,
actions: { confirmEmail }
actions: { confirmEmail },
} = useSession()
const [isTokenExpired, setIsTokenExpired] = createSignal(false)

View File

@ -1,14 +1,18 @@
import styles from './AuthModal.module.scss'
import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx'
import { createSignal, JSX, Show } from 'solid-js'
import { useRouter } from '../../../stores/router'
import { email, setEmail } from './sharedLogic'
import type { AuthModalSearchParams } from './types'
import { ApiError } from '../../../utils/apiClient'
import { signSendLink } from '../../../stores/auth'
import { useLocalize } from '../../../context/localize'
import { signSendLink } from '../../../stores/auth'
import { useRouter } from '../../../stores/router'
import { ApiError } from '../../../utils/apiClient'
import { validateEmail } from '../../../utils/validateEmail'
import { email, setEmail } from './sharedLogic'
import styles from './AuthModal.module.scss'
type FormFields = {
email: string
}
@ -83,7 +87,7 @@ export const ForgotPasswordForm = () => {
<div
class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().email
'pretty-form__item--error': validationErrors().email,
})}
>
<input
@ -116,7 +120,7 @@ export const ForgotPasswordForm = () => {
onClick={(event) => {
event.preventDefault()
changeSearchParam({
mode: 'register'
mode: 'register',
})
}}
>
@ -138,7 +142,7 @@ export const ForgotPasswordForm = () => {
class={styles.authLink}
onClick={() =>
changeSearchParam({
mode: 'login'
mode: 'login',
})
}
>

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 { hideModal } from '../../../stores/ui'
import { useSession } from '../../../context/session'
import { signSendLink } from '../../../stores/auth'
import { validateEmail } from '../../../utils/validateEmail'
import { useSnackbar } from '../../../context/snackbar'
import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { useSnackbar } from '../../../context/snackbar'
import { signSendLink } from '../../../stores/auth'
import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui'
import { ApiError } from '../../../utils/apiClient'
import { validateEmail } from '../../../utils/validateEmail'
import { Icon } from '../../_shared/Icon'
import { AuthModalHeader } from './AuthModalHeader'
import { email, setEmail } from './sharedLogic'
import { SocialProviders } from './SocialProviders'
import styles from './AuthModal.module.scss'
type FormFields = {
email: string
@ -36,11 +40,11 @@ export const LoginForm = () => {
const authFormRef: { current: HTMLFormElement } = { current: null }
const {
actions: { showSnackbar }
actions: { showSnackbar },
} = useSnackbar()
const {
actions: { signIn }
actions: { signIn },
} = useSession()
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
@ -144,7 +148,7 @@ export const LoginForm = () => {
</Show>
<div
class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().email
'pretty-form__item--error': validationErrors().email,
})}
>
<input
@ -164,7 +168,7 @@ export const LoginForm = () => {
<div
class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().password
'pretty-form__item--error': validationErrors().password,
})}
>
<input
@ -198,7 +202,7 @@ export const LoginForm = () => {
class="link"
onClick={() =>
changeSearchParam({
mode: 'forgot-password'
mode: 'forgot-password',
})
}
>
@ -215,7 +219,7 @@ export const LoginForm = () => {
class={styles.authLink}
onClick={() =>
changeSearchParam({
mode: 'register'
mode: 'register',
})
}
>

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 { hideModal } from '../../../stores/ui'
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
import { register } from '../../../stores/auth'
import type { JSX } from 'solid-js'
import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { register } from '../../../stores/auth'
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui'
import { ApiError } from '../../../utils/apiClient'
import { validateEmail } from '../../../utils/validateEmail'
import { AuthModalHeader } from './AuthModalHeader'
import { Icon } from '../../_shared/Icon'
import { AuthModalHeader } from './AuthModalHeader'
import { email, setEmail } from './sharedLogic'
import { SocialProviders } from './SocialProviders'
import styles from './AuthModal.module.scss'
type FormFields = {
fullName: string
email: string
@ -126,7 +130,7 @@ export const RegisterForm = () => {
await register({
name: cleanName,
email: cleanEmail,
password: password()
password: password(),
})
setIsSuccess(true)
@ -156,7 +160,7 @@ export const RegisterForm = () => {
</Show>
<div
class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().fullName
'pretty-form__item--error': validationErrors().fullName,
})}
>
<input
@ -174,7 +178,7 @@ export const RegisterForm = () => {
<div
class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().email
'pretty-form__item--error': validationErrors().email,
})}
>
<input
@ -199,7 +203,7 @@ export const RegisterForm = () => {
onClick={(event) => {
event.preventDefault()
changeSearchParam({
mode: 'login'
mode: 'login',
})
}}
>
@ -211,7 +215,7 @@ export const RegisterForm = () => {
<div
class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().password
'pretty-form__item--error': validationErrors().password,
})}
>
<input
@ -252,7 +256,7 @@ export const RegisterForm = () => {
class={styles.authLink}
onClick={() =>
changeSearchParam({
mode: 'login'
mode: 'login',
})
}
>

View File

@ -1,9 +1,9 @@
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize'
import { hideModal } from '../../../stores/ui'
import { apiBaseUrl } from '../../../utils/config'
import { Icon } from '../../_shared/Icon'
import styles from './SocialProviders.module.scss'
import { apiBaseUrl } from '../../../utils/config'
import { useLocalize } from '../../../context/localize'
type Provider = 'facebook' | 'google' | 'vk' | 'github'

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 { clsx } from 'clsx'
import { Show, Component, createEffect, createMemo } from 'solid-js'
import { Dynamic } from 'solid-js/web'
import { useLocalize } from '../../../context/localize'
import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui'
import { isMobile } from '../../../utils/media-query'
import { EmailConfirm } from './EmailConfirm'
import { ForgotPasswordForm } from './ForgotPasswordForm'
import { LoginForm } from './LoginForm'
import { RegisterForm } from './RegisterForm'
import styles from './AuthModal.module.scss'
const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = {
login: LoginForm,
register: RegisterForm,
'forgot-password': ForgotPasswordForm,
'confirm-email': EmailConfirm
'confirm-email': EmailConfirm,
}
export const AuthModal = () => {
@ -40,7 +44,7 @@ export const AuthModal = () => {
<div
ref={rootRef}
class={clsx(styles.view, {
row: !source
row: !source,
})}
classList={{ [styles.signUp]: mode() === 'register' || mode() === 'confirm-email' }}
>
@ -54,7 +58,7 @@ export const AuthModal = () => {
<h4>{t(`Join the global community of authors!`)}</h4>
<p class={styles.authBenefits}>
{t(
'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine'
'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine',
)}
.&nbsp;
{t('New stories every day and even more!')}
@ -77,7 +81,7 @@ export const AuthModal = () => {
</Show>
<div
class={clsx(styles.auth, {
'col-md-12': !source
'col-md-12': !source,
})}
>
<Dynamic component={AUTH_MODAL_MODES[mode()]} />

View File

@ -1,6 +1,7 @@
import { useConfirm } from '../../../context/confirm'
import { useLocalize } from '../../../context/localize'
import { Button } from '../../_shared/Button'
import styles from './ConfirmModal.module.scss'
export const ConfirmModal = () => {
@ -8,7 +9,7 @@ export const ConfirmModal = () => {
const {
confirmMessage,
actions: { resolveConfirm }
actions: { resolveConfirm },
} = useConfirm()
return (

View File

@ -1,5 +1,6 @@
import './Confirmed.scss'
import { onMount } from 'solid-js'
import { useLocalize } from '../../context/localize'
export const Confirmed = (props: { token?: string }) => {

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