merged-dev
This commit is contained in:
commit
12f30b29a0
|
@ -27,7 +27,6 @@ module.exports = {
|
||||||
// 'plugin:@typescript-eslint/recommended-requiring-type-checking'
|
// 'plugin:@typescript-eslint/recommended-requiring-type-checking'
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'no-nested-ternary': 'off',
|
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
|
@ -35,8 +34,9 @@ module.exports = {
|
||||||
varsIgnorePattern: '^log$'
|
varsIgnorePattern: '^log$'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'@typescript-eslint/no-explicit-any': 'warn',
|
// TODO: Remove any usage and enable
|
||||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||||
|
|
||||||
// solid-js fix
|
// solid-js fix
|
||||||
'import/no-unresolved': [2, { ignore: ['solid-js/'] }]
|
'import/no-unresolved': [2, { ignore: ['solid-js/'] }]
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
{
|
{
|
||||||
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write",
|
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write",
|
||||||
"package.json": "sort-package-json",
|
"package.json": "sort-package-json"
|
||||||
"*.{scss,css}": "stylelint",
|
|
||||||
"*.{ts,tsx,js}": "eslint --fix"
|
|
||||||
}
|
}
|
||||||
|
|
6
.lintstagedrc.bak
Normal file
6
.lintstagedrc.bak
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write",
|
||||||
|
"package.json": "sort-package-json",
|
||||||
|
"*.{scss,css}": "stylelint",
|
||||||
|
"*.{ts,tsx,js}": "eslint --fix"
|
||||||
|
}
|
18
package.json
18
package.json
|
@ -17,9 +17,9 @@
|
||||||
"lint:code:fix": "eslint . --fix",
|
"lint:code:fix": "eslint . --fix",
|
||||||
"lint:styles": "stylelint **/*.{scss,css}",
|
"lint:styles": "stylelint **/*.{scss,css}",
|
||||||
"lint:styles:fix": "stylelint **/*.{scss,css} --fix",
|
"lint:styles:fix": "stylelint **/*.{scss,css} --fix",
|
||||||
"pre-commit": "",
|
"pre-commit": "lint-staged",
|
||||||
"pre-push": "",
|
|
||||||
"pre-commit-old": "lint-staged",
|
"pre-commit-old": "lint-staged",
|
||||||
|
"pre-push": "",
|
||||||
"pre-push-old": "npm run typecheck",
|
"pre-push-old": "npm run typecheck",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
"vercel-build": "astro build"
|
"vercel-build": "astro build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws-sdk": "^2.0.0",
|
"@aws-sdk/client-s3": "^3.178.0",
|
||||||
"mailgun.js": "^8.0.1"
|
"mailgun.js": "^8.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -60,17 +60,17 @@
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "^2.0.3",
|
||||||
"@urql/exchange-auth": "^1.0.0",
|
"@urql/exchange-auth": "^1.0.0",
|
||||||
"@urql/exchange-graphcache": "^5.0.0",
|
"@urql/exchange-graphcache": "^5.0.0",
|
||||||
"astro": "^1.4.6",
|
"astro": "^1.1.1",
|
||||||
"astro-eslint-parser": "^0.7.3",
|
"astro-eslint-parser": "^0.9.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"bootstrap": "5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"cookie-signature": "^1.2.0",
|
"cookie-signature": "^1.2.0",
|
||||||
"eslint": "^8.22.0",
|
"eslint": "^8.26.0",
|
||||||
"eslint-config-stylelint": "^17.0.0",
|
"eslint-config-stylelint": "^17.0.0",
|
||||||
"eslint-import-resolver-typescript": "^3.5.0",
|
"eslint-import-resolver-typescript": "^3.5.0",
|
||||||
"eslint-plugin-astro": "^0.20.0",
|
"eslint-plugin-astro": "^0.21.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||||
"eslint-plugin-promise": "^6.0.1",
|
"eslint-plugin-promise": "^6.0.1",
|
||||||
|
@ -85,6 +85,8 @@
|
||||||
"idb": "^7.1.0",
|
"idb": "^7.1.0",
|
||||||
"jest": "^29.2.1",
|
"jest": "^29.2.1",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
|
"loglevel": "^1.8.0",
|
||||||
|
"loglevel-plugin-prefix": "^0.8.4",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"markdown-it-container": "^3.0.0",
|
"markdown-it-container": "^3.0.0",
|
||||||
"markdown-it-implicit-figures": "^0.10.0",
|
"markdown-it-implicit-figures": "^0.10.0",
|
||||||
|
@ -123,7 +125,7 @@
|
||||||
"stylelint": "^14.12.1",
|
"stylelint": "^14.12.1",
|
||||||
"stylelint-config-css-modules": "^4.1.0",
|
"stylelint-config-css-modules": "^4.1.0",
|
||||||
"stylelint-config-prettier-scss": "^0.0.1",
|
"stylelint-config-prettier-scss": "^0.0.1",
|
||||||
"stylelint-config-standard-scss": "^5.0.0",
|
"stylelint-config-standard-scss": "^6.0.0",
|
||||||
"stylelint-order": "^5.0.0",
|
"stylelint-order": "^5.0.0",
|
||||||
"stylelint-scss": "^4.3.0",
|
"stylelint-scss": "^4.3.0",
|
||||||
"swiper": "^8.4.2",
|
"swiper": "^8.4.2",
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import './Comment.scss'
|
import './Comment.scss'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { Show } from 'solid-js/web'
|
import { Show, createMemo } from 'solid-js'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import type { Author, Reaction as Point } from '../../graphql/types.gen'
|
import type { Author, Reaction as Point } from '../../graphql/types.gen'
|
||||||
import { createMemo } from 'solid-js'
|
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
// import { createReaction, updateReaction, deleteReaction } from '../../stores/zine/reactions'
|
// import { createReaction, updateReaction, deleteReaction } from '../../stores/zine/reactions'
|
||||||
import MD from './MD'
|
import MD from './MD'
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { showModal } from '../../stores/ui'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { incrementView } from '../../stores/zine/articles'
|
import { incrementView } from '../../stores/zine/articles'
|
||||||
import MD from './MD'
|
import MD from './MD'
|
||||||
|
import { SharePopup } from './SharePopup'
|
||||||
|
|
||||||
const MAX_COMMENT_LEVEL = 6
|
const MAX_COMMENT_LEVEL = 6
|
||||||
|
|
||||||
|
@ -126,9 +127,13 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
{/* </a>*/}
|
{/* </a>*/}
|
||||||
{/*</div>*/}
|
{/*</div>*/}
|
||||||
<div class="shout-stats__item">
|
<div class="shout-stats__item">
|
||||||
<a href="#share" onClick={() => showModal('share')}>
|
<SharePopup
|
||||||
<Icon name="share" />
|
trigger={
|
||||||
</a>
|
<a href="#" onClick={(event) => event.preventDefault()}>
|
||||||
|
<Icon name="share" />
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/*FIXME*/}
|
{/*FIXME*/}
|
||||||
{/*<Show when={canEdit()}>*/}
|
{/*<Show when={canEdit()}>*/}
|
||||||
|
|
45
src/components/Article/SharePopup.tsx
Normal file
45
src/components/Article/SharePopup.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { Icon } from '../Nav/Icon'
|
||||||
|
import styles from '../Nav/Popup.module.scss'
|
||||||
|
import { t } from '../../utils/intl'
|
||||||
|
import { Popup, PopupProps } from '../Nav/Popup'
|
||||||
|
|
||||||
|
type SharePopupProps = Omit<PopupProps, 'children'>
|
||||||
|
|
||||||
|
export const SharePopup = (props: SharePopupProps) => {
|
||||||
|
return (
|
||||||
|
<Popup {...props}>
|
||||||
|
<ul class="nodash">
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<Icon name="vk-white" class={styles.icon} />
|
||||||
|
VK
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<Icon name="facebook-white" class={styles.icon} />
|
||||||
|
Facebook
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<Icon name="twitter-white" class={styles.icon} />
|
||||||
|
Twitter
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<Icon name="telegram-white" class={styles.icon} />
|
||||||
|
Telegram
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<Icon name="link-white" class={styles.icon} />
|
||||||
|
{t('Copy link')}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popup>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,9 +1,8 @@
|
||||||
import { For, Show } from 'solid-js/web'
|
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import Userpic from './Userpic'
|
import Userpic from './Userpic'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import style from './Card.module.scss'
|
import style from './Card.module.scss'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, For, Show } from 'solid-js'
|
||||||
import { translit } from '../../utils/ru2en'
|
import { translit } from '../../utils/ru2en'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { Author } from '../../graphql/types.gen'
|
||||||
import { AuthorCard } from './Card'
|
import { AuthorCard } from './Card'
|
||||||
import './Full.scss'
|
import './Full.scss'
|
||||||
|
|
||||||
export default (props: { author: Author }) => {
|
export const AuthorFull = (props: { author: Author }) => {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { Show } from 'solid-js/web'
|
import { Show } from 'solid-js'
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import style from './Userpic.module.scss'
|
import style from './Userpic.module.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
interface UserpicProps {
|
interface UserpicProps {
|
||||||
user: Author
|
user: Author
|
||||||
hasLink?: boolean
|
hasLink?: boolean
|
||||||
isBig?: boolean
|
isBig?: boolean
|
||||||
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props: UserpicProps) => {
|
export default (props: UserpicProps) => {
|
||||||
|
@ -16,7 +18,7 @@ export default (props: UserpicProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={style.circlewrap} classList={{ [style.big]: props.isBig }}>
|
<div class={clsx(style.circlewrap, props.class)} classList={{ [style.big]: props.isBig }}>
|
||||||
<Show when={props.hasLink}>
|
<Show when={props.hasLink}>
|
||||||
<a href={`/author/${props.user.slug}`}>
|
<a href={`/author/${props.user.slug}`}>
|
||||||
<Show
|
<Show
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import styles from './Banner.module.scss'
|
import styles from './Banner.module.scss'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { showModal } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
import {clsx} from "clsx";
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Icon } from '../Nav/Icon'
|
||||||
import Subscribe from './Subscribe'
|
import Subscribe from './Subscribe'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import {clsx} from "clsx";
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
const locale_title = createMemo(() => (locale() === 'ru' ? 'English' : 'Русский'))
|
const locale_title = createMemo(() => (locale() === 'ru' ? 'English' : 'Русский'))
|
||||||
|
|
7
src/components/Editor/components/Editor.module.scss
Normal file
7
src/components/Editor/components/Editor.module.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.error {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
|
@ -3,18 +3,21 @@ import type { EditorState } from 'prosemirror-state'
|
||||||
import { useState } from '../store/context'
|
import { useState } from '../store/context'
|
||||||
import { ProseMirror } from './ProseMirror'
|
import { ProseMirror } from './ProseMirror'
|
||||||
import '../styles/Editor.scss'
|
import '../styles/Editor.scss'
|
||||||
|
import styles from './Editor.module.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
export const Editor = () => {
|
export const Editor = () => {
|
||||||
const [store, ctrl] = useState()
|
const [store, ctrl] = useState()
|
||||||
const onInit = (text: EditorState, editorView: EditorView) => ctrl.setState({ editorView, text })
|
const onInit = (text: EditorState, editorView: EditorView) => ctrl.setState({ editorView, text })
|
||||||
const onReconfigure = (text: EditorState) => ctrl.setState({ text })
|
const onReconfigure = (text: EditorState) => ctrl.setState({ text })
|
||||||
const onChange = (text: EditorState) => ctrl.setState({ text, lastModified: new Date() })
|
const onChange = (text: EditorState) => ctrl.setState({ text, lastModified: new Date() })
|
||||||
// const editorCss = (config) => css``
|
|
||||||
const style = () => (store.error ? `display: none;` : (store.markdown ? `white-space: pre-wrap;` : ''))
|
|
||||||
return (
|
return (
|
||||||
<ProseMirror
|
<ProseMirror
|
||||||
className='editor col-md-6 shift-content'
|
cssClass={clsx('editor', 'col-md-6', 'shift-content', {
|
||||||
style={style()}
|
[styles.error]: store.error,
|
||||||
|
[styles.markdown]: store.markdown
|
||||||
|
})}
|
||||||
editorView={store.editorView}
|
editorView={store.editorView}
|
||||||
text={store.text}
|
text={store.text}
|
||||||
extensions={store.extensions}
|
extensions={store.extensions}
|
||||||
|
|
|
@ -7,13 +7,13 @@ export default () => {
|
||||||
return (
|
return (
|
||||||
<Switch fallback={<Other />}>
|
<Switch fallback={<Other />}>
|
||||||
<Match when={store.error.id === 'invalid_state'}>
|
<Match when={store.error.id === 'invalid_state'}>
|
||||||
<InvalidState title='Invalid State' />
|
<InvalidState title="Invalid State" />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.error.id === 'invalid_config'}>
|
<Match when={store.error.id === 'invalid_config'}>
|
||||||
<InvalidState title='Invalid Config' />
|
<InvalidState title="Invalid Config" />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.error.id === 'invalid_draft'}>
|
<Match when={store.error.id === 'invalid_draft'}>
|
||||||
<InvalidState title='Invalid Draft' />
|
<InvalidState title="Invalid Draft" />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
|
@ -24,8 +24,8 @@ const InvalidState = (props: { title: string }) => {
|
||||||
const onClick = () => ctrl.clean()
|
const onClick = () => ctrl.clean()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class='error'>
|
<div class="error">
|
||||||
<div class='container'>
|
<div class="container">
|
||||||
<h1>{props.title}</h1>
|
<h1>{props.title}</h1>
|
||||||
<p>
|
<p>
|
||||||
There is an error with the editor state. This is probably due to an old version in which the data
|
There is an error with the editor state. This is probably due to an old version in which the data
|
||||||
|
@ -35,7 +35,7 @@ const InvalidState = (props: { title: string }) => {
|
||||||
<pre>
|
<pre>
|
||||||
<code>{JSON.stringify(store.error.props)}</code>
|
<code>{JSON.stringify(store.error.props)}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button class='primary' onClick={onClick}>
|
<button class="primary" onClick={onClick}>
|
||||||
Clean
|
Clean
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,13 +53,13 @@ const Other = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class='error'>
|
<div class="error">
|
||||||
<div class='container'>
|
<div class="container">
|
||||||
<h1>An error occurred.</h1>
|
<h1>An error occurred.</h1>
|
||||||
<pre>
|
<pre>
|
||||||
<code>{getMessage()}</code>
|
<code>{getMessage()}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button class='primary' onClick={onClick}>
|
<button class="primary" onClick={onClick}>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import type { JSX } from 'solid-js/jsx-runtime';
|
import type { JSX } from 'solid-js/jsx-runtime'
|
||||||
import type { Config } from '../store/context'
|
import type { Config } from '../store/context'
|
||||||
import '../styles/Layout.scss'
|
import '../styles/Layout.scss'
|
||||||
|
|
||||||
export type Styled = {
|
export type Styled = {
|
||||||
children: JSX.Element;
|
children: JSX.Element
|
||||||
config?: Config;
|
config?: Config
|
||||||
'data-testid'?: string;
|
'data-testid'?: string
|
||||||
onClick?: () => void;
|
onClick?: () => void
|
||||||
onMouseEnter?: (e: MouseEvent) => void;
|
onMouseEnter?: (e: MouseEvent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Layout = (props: Styled) => {
|
export const Layout = (props: Styled) => {
|
||||||
return (<div onMouseEnter={props.onMouseEnter} class='layout container' data-testid={props['data-testid']}>
|
return (
|
||||||
{props.children}
|
<div onMouseEnter={props.onMouseEnter} class="layout container" data-testid={props['data-testid']}>
|
||||||
</div>)
|
{props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,13 @@ import { Schema } from 'prosemirror-model'
|
||||||
import type { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
import type { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
||||||
|
|
||||||
interface ProseMirrorProps {
|
interface ProseMirrorProps {
|
||||||
style?: string;
|
cssClass?: string
|
||||||
className?: string;
|
text?: Store<ProseMirrorState>
|
||||||
text?: Store<ProseMirrorState>;
|
editorView?: Store<EditorView>
|
||||||
editorView?: Store<EditorView>;
|
extensions?: Store<ProseMirrorExtension[]>
|
||||||
extensions?: Store<ProseMirrorExtension[]>;
|
onInit: (s: EditorState, v: EditorView) => void
|
||||||
onInit: (s: EditorState, v: EditorView) => void;
|
onReconfigure: (s: EditorState) => void
|
||||||
onReconfigure: (s: EditorState) => void;
|
onChange: (s: EditorState) => void
|
||||||
onChange: (s: EditorState) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProseMirror = (props: ProseMirrorProps) => {
|
export const ProseMirror = (props: ProseMirrorProps) => {
|
||||||
|
@ -28,45 +27,39 @@ export const ProseMirror = (props: ProseMirrorProps) => {
|
||||||
props.onChange(newState)
|
props.onChange(newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect((payload: [EditorState, ProseMirrorExtension[]]) => {
|
createEffect(
|
||||||
const [prevText, prevExtensions] = payload
|
(payload: [EditorState, ProseMirrorExtension[]]) => {
|
||||||
const text = unwrap(props.text)
|
const [prevText, prevExtensions] = payload
|
||||||
const extensions: ProseMirrorExtension[] = unwrap(props.extensions)
|
const text = unwrap(props.text)
|
||||||
if (!text || !extensions?.length) {
|
const extensions: ProseMirrorExtension[] = unwrap(props.extensions)
|
||||||
|
if (!text || !extensions?.length) {
|
||||||
|
return [text, extensions]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.editorView) {
|
||||||
|
const { editorState, nodeViews } = createEditorState(text, extensions)
|
||||||
|
const view = new EditorView(editorRef, { state: editorState, nodeViews, dispatchTransaction })
|
||||||
|
view.focus()
|
||||||
|
props.onInit(editorState, view)
|
||||||
|
return [editorState, extensions]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensions !== prevExtensions || (!(text instanceof EditorState) && text !== prevText)) {
|
||||||
|
const { editorState, nodeViews } = createEditorState(text, extensions, prevText)
|
||||||
|
if (!editorState) return
|
||||||
|
editorView().updateState(editorState)
|
||||||
|
editorView().setProps({ nodeViews, dispatchTransaction })
|
||||||
|
props.onReconfigure(editorState)
|
||||||
|
editorView().focus()
|
||||||
|
return [editorState, extensions]
|
||||||
|
}
|
||||||
|
|
||||||
return [text, extensions]
|
return [text, extensions]
|
||||||
}
|
},
|
||||||
|
[props.text, props.extensions]
|
||||||
if (!props.editorView) {
|
|
||||||
const { editorState, nodeViews } = createEditorState(text, extensions)
|
|
||||||
const view = new EditorView(editorRef, { state: editorState, nodeViews, dispatchTransaction })
|
|
||||||
view.focus()
|
|
||||||
props.onInit(editorState, view)
|
|
||||||
return [editorState, extensions]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extensions !== prevExtensions || (!(text instanceof EditorState) && text !== prevText)) {
|
|
||||||
const { editorState, nodeViews } = createEditorState(text, extensions, prevText)
|
|
||||||
if (!editorState) return
|
|
||||||
editorView().updateState(editorState)
|
|
||||||
editorView().setProps({ nodeViews, dispatchTransaction })
|
|
||||||
props.onReconfigure(editorState)
|
|
||||||
editorView().focus()
|
|
||||||
return [editorState, extensions]
|
|
||||||
}
|
|
||||||
|
|
||||||
return [text, extensions]
|
|
||||||
},
|
|
||||||
[props.text, props.extensions]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return <div ref={editorRef} class={props.cssClass} spell-check={false} />
|
||||||
<div
|
|
||||||
style={props.style}
|
|
||||||
ref={editorRef}
|
|
||||||
class={props.className}
|
|
||||||
spell-check={false}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createEditorState = (
|
const createEditorState = (
|
||||||
|
@ -74,8 +67,8 @@ const createEditorState = (
|
||||||
extensions: ProseMirrorExtension[],
|
extensions: ProseMirrorExtension[],
|
||||||
prevText?: EditorState
|
prevText?: EditorState
|
||||||
): {
|
): {
|
||||||
editorState: EditorState;
|
editorState: EditorState
|
||||||
nodeViews: { [key: string]: NodeViewFn };
|
nodeViews: { [key: string]: NodeViewFn }
|
||||||
} => {
|
} => {
|
||||||
const reconfigure = text instanceof EditorState && prevText?.schema
|
const reconfigure = text instanceof EditorState && prevText?.schema
|
||||||
let schemaSpec = { nodes: {} }
|
let schemaSpec = { nodes: {} }
|
||||||
|
@ -104,7 +97,7 @@ const createEditorState = (
|
||||||
editorState = text.reconfigure({ schema, plugins } as EditorStateConfig)
|
editorState = text.reconfigure({ schema, plugins } as EditorStateConfig)
|
||||||
} else if (text instanceof EditorState) {
|
} else if (text instanceof EditorState) {
|
||||||
editorState = EditorState.fromJSON({ schema, plugins }, text.toJSON())
|
editorState = EditorState.fromJSON({ schema, plugins }, text.toJSON())
|
||||||
} else if (text){
|
} else if (text) {
|
||||||
console.debug(text)
|
console.debug(text)
|
||||||
editorState = EditorState.fromJSON({ schema, plugins }, text)
|
editorState = EditorState.fromJSON({ schema, plugins }, text)
|
||||||
}
|
}
|
||||||
|
|
3
src/components/Editor/components/Sidebar.module.scss
Normal file
3
src/components/Editor/components/Sidebar.module.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.withMargin {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
|
@ -7,17 +7,20 @@ import * as remote from '../remote'
|
||||||
import { isEmpty } from '../prosemirror/helpers'
|
import { isEmpty } from '../prosemirror/helpers'
|
||||||
import type { Styled } from './Layout'
|
import type { Styled } from './Layout'
|
||||||
import '../styles/Sidebar.scss'
|
import '../styles/Sidebar.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import styles from './Sidebar.module.scss'
|
||||||
|
|
||||||
const Off = (props) => <div class='sidebar-off'>{props.children}</div>
|
const Off = (props) => <div class="sidebar-off">{props.children}</div>
|
||||||
|
|
||||||
const Label = (props: Styled) => <h3 class='sidebar-label'>{props.children}</h3>
|
const Label = (props: Styled) => <h3 class="sidebar-label">{props.children}</h3>
|
||||||
|
|
||||||
const Link = (
|
const Link = (
|
||||||
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
|
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
|
||||||
) => (
|
) => (
|
||||||
<button
|
<button
|
||||||
class={`sidebar-link${props.className ? ' ' + props.className : ''}`}
|
class={clsx('sidebar-link', props.className, {
|
||||||
style={{ "margin-bottom": props.withMargin ? '10px' : '' }}
|
[styles.withMargin]: props.withMargin
|
||||||
|
})}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
|
@ -34,22 +37,46 @@ export const Sidebar = () => {
|
||||||
document.body.classList.toggle('dark')
|
document.body.classList.toggle('dark')
|
||||||
ctrl.updateConfig({ theme: document.body.className })
|
ctrl.updateConfig({ theme: document.body.className })
|
||||||
}
|
}
|
||||||
const collabText = () => (store.collab?.started ? 'Stop' : (store.collab?.error ? 'Restart 🚨' : 'Start'))
|
const collabText = () => {
|
||||||
|
if (store.collab?.started) {
|
||||||
|
return 'Stop'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.collab?.error) {
|
||||||
|
return 'Restart 🚨'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Start'
|
||||||
|
}
|
||||||
|
|
||||||
|
const discardText = () => {
|
||||||
|
if (store.path) {
|
||||||
|
return 'Close'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.drafts.length > 0 && isEmpty(store.text)) {
|
||||||
|
return 'Delete ⚠️'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Clear'
|
||||||
|
}
|
||||||
|
|
||||||
const editorView = () => unwrap(store.editorView)
|
const editorView = () => unwrap(store.editorView)
|
||||||
const onToggleMarkdown = () => ctrl.toggleMarkdown()
|
const onToggleMarkdown = () => ctrl.toggleMarkdown()
|
||||||
const onOpenDraft = (draft: Draft) => ctrl.openDraft(unwrap(draft))
|
const onOpenDraft = (draft: Draft) => ctrl.openDraft(unwrap(draft))
|
||||||
const collabUsers = () => store.collab?.y?.provider.awareness.meta.size ?? 0
|
const collabUsers = () => store.collab?.y?.provider.awareness.meta.size ?? 0
|
||||||
const onUndo = () => undo(editorView().state, editorView().dispatch)
|
const onUndo = () => undo(editorView().state, editorView().dispatch)
|
||||||
const onRedo = () => redo(editorView().state, editorView().dispatch)
|
const onRedo = () => redo(editorView().state, editorView().dispatch)
|
||||||
const onCopyAllAsMd = () => remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
|
const onCopyAllAsMd = () =>
|
||||||
|
remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
|
||||||
const onDiscard = () => ctrl.discard()
|
const onDiscard = () => ctrl.discard()
|
||||||
const [isHidden, setIsHidden] = createSignal<boolean | false>()
|
const [isHidden, setIsHidden] = createSignal<boolean | false>()
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
setIsHidden(!isHidden());
|
setIsHidden(!isHidden())
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSidebar();
|
toggleSidebar()
|
||||||
|
|
||||||
const onCollab = () => {
|
const onCollab = () => {
|
||||||
const state = unwrap(store)
|
const state = unwrap(store)
|
||||||
|
@ -84,11 +111,13 @@ export const Sidebar = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = () =>
|
const text = () =>
|
||||||
p.draft.path ? p.draft.path.slice(Math.max(0, p.draft.path.length - length)) : getContent(p.draft.text?.doc)
|
p.draft.path
|
||||||
|
? p.draft.path.slice(Math.max(0, p.draft.path.length - length))
|
||||||
|
: getContent(p.draft.text?.doc)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line solid/no-react-specific-props
|
// eslint-disable-next-line solid/no-react-specific-props
|
||||||
<Link className='draft' onClick={() => onOpenDraft(p.draft)} data-testid='open'>
|
<Link className="draft" onClick={() => onOpenDraft(p.draft)} data-testid="open">
|
||||||
{text()} {p.draft.path && '📎'}
|
{text()} {p.draft.path && '📎'}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
@ -96,9 +125,7 @@ export const Sidebar = () => {
|
||||||
|
|
||||||
const Keys = (props) => (
|
const Keys = (props) => (
|
||||||
<span>
|
<span>
|
||||||
<For each={props.keys}>{(k: Element) => (
|
<For each={props.keys}>{(k: Element) => <i>{k}</i>}</For>
|
||||||
<i>{k}</i>
|
|
||||||
)}</For>
|
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -116,10 +143,12 @@ export const Sidebar = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
|
<div class={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
|
||||||
<span class='sidebar-opener' onClick={toggleSidebar}>Советы и предложения</span>
|
<span class="sidebar-opener" onClick={toggleSidebar}>
|
||||||
|
Советы и предложения
|
||||||
|
</span>
|
||||||
|
|
||||||
<Off onClick={() => editorView().focus()}>
|
<Off onClick={() => editorView().focus()}>
|
||||||
<div class='sidebar-closer' onClick={toggleSidebar}/>
|
<div class="sidebar-closer" onClick={toggleSidebar} />
|
||||||
<Show when={true}>
|
<Show when={true}>
|
||||||
<div>
|
<div>
|
||||||
{store.path && (
|
{store.path && (
|
||||||
|
@ -127,28 +156,21 @@ export const Sidebar = () => {
|
||||||
<i>({store.path.slice(Math.max(0, store.path.length - 24))})</i>
|
<i>({store.path.slice(Math.max(0, store.path.length - 24))})</i>
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
<Link>
|
<Link>Пригласить соавторов</Link>
|
||||||
Пригласить соавторов
|
<Link>Настройки публикации</Link>
|
||||||
</Link>
|
<Link>История правок</Link>
|
||||||
<Link>
|
|
||||||
Настройки публикации
|
|
||||||
</Link>
|
|
||||||
<Link>
|
|
||||||
История правок
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div class='theme-switcher'>
|
<div class="theme-switcher">
|
||||||
Ночная тема
|
Ночная тема
|
||||||
<input type='checkbox' name='theme' id='theme' onClick={toggleTheme} />
|
<input type="checkbox" name="theme" id="theme" onClick={toggleTheme} />
|
||||||
<label for='theme'>Ночная тема</label>
|
<label for="theme">Ночная тема</label>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
onClick={onDiscard}
|
onClick={onDiscard}
|
||||||
disabled={!store.path && store.drafts.length === 0 && isEmpty(store.text)}
|
disabled={!store.path && store.drafts.length === 0 && isEmpty(store.text)}
|
||||||
data-testid='discard'
|
data-testid="discard"
|
||||||
>
|
>
|
||||||
{store.path ? 'Close' : (store.drafts.length > 0 && isEmpty(store.text) ? 'Delete ⚠️' : 'Clear')}{' '}
|
{discardText()} <Keys keys={[mod, 'w']} />
|
||||||
<Keys keys={[mod, 'w']} />
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link onClick={onUndo}>
|
<Link onClick={onUndo}>
|
||||||
Undo <Keys keys={[mod, 'z']} />
|
Undo <Keys keys={[mod, 'z']} />
|
||||||
|
@ -156,7 +178,7 @@ export const Sidebar = () => {
|
||||||
<Link onClick={onRedo}>
|
<Link onClick={onRedo}>
|
||||||
Redo <Keys keys={[mod, 'Shift', 'z']} />
|
Redo <Keys keys={[mod, 'Shift', 'z']} />
|
||||||
</Link>
|
</Link>
|
||||||
<Link onClick={onToggleMarkdown} data-testid='markdown'>
|
<Link onClick={onToggleMarkdown} data-testid="markdown">
|
||||||
Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} />
|
Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} />
|
||||||
</Link>
|
</Link>
|
||||||
<Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link>
|
<Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link>
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import markdownit from 'markdown-it'
|
import markdownit from 'markdown-it'
|
||||||
import { MarkdownSerializer, MarkdownParser, defaultMarkdownSerializer, MarkdownSerializerState } from 'prosemirror-markdown'
|
import {
|
||||||
|
MarkdownSerializer,
|
||||||
|
MarkdownParser,
|
||||||
|
defaultMarkdownSerializer,
|
||||||
|
MarkdownSerializerState
|
||||||
|
} from 'prosemirror-markdown'
|
||||||
import type { Node, Schema } from 'prosemirror-model'
|
import type { Node, Schema } from 'prosemirror-model'
|
||||||
import type { EditorState } from 'prosemirror-state'
|
import type { EditorState } from 'prosemirror-state'
|
||||||
|
|
||||||
|
@ -12,7 +17,6 @@ export const serialize = (state: EditorState) => {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function findAlignment(cell: Node): string | null {
|
function findAlignment(cell: Node): string | null {
|
||||||
const alignment = cell.attrs.style as string
|
const alignment = cell.attrs.style as string
|
||||||
if (!alignment) {
|
if (!alignment) {
|
||||||
|
|
|
@ -36,13 +36,13 @@ export default (plain = false): ProseMirrorExtension => ({
|
||||||
schema: () =>
|
schema: () =>
|
||||||
plain
|
plain
|
||||||
? {
|
? {
|
||||||
nodes: plainSchema.spec.nodes,
|
nodes: plainSchema.spec.nodes,
|
||||||
marks: plainSchema.spec.marks
|
marks: plainSchema.spec.marks
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
nodes: (markdownSchema.spec.nodes as OrderedMap<NodeSpec>).update('blockquote', blockquoteSchema),
|
nodes: (markdownSchema.spec.nodes as OrderedMap<NodeSpec>).update('blockquote', blockquoteSchema),
|
||||||
marks: markdownSchema.spec.marks
|
marks: markdownSchema.spec.marks
|
||||||
},
|
},
|
||||||
plugins: (prev, schema) => [
|
plugins: (prev, schema) => [
|
||||||
...prev,
|
...prev,
|
||||||
keymap({
|
keymap({
|
||||||
|
|
|
@ -10,26 +10,26 @@ const blank = '\u00A0'
|
||||||
|
|
||||||
const onArrow =
|
const onArrow =
|
||||||
(dir: 'left' | 'right') =>
|
(dir: 'left' | 'right') =>
|
||||||
(state: EditorState, dispatch: (tr: Transaction) => void, editorView: EditorView) => {
|
(state: EditorState, dispatch: (tr: Transaction) => void, editorView: EditorView) => {
|
||||||
if (!state.selection.empty) return false
|
if (!state.selection.empty) return false
|
||||||
const $pos = state.selection.$head
|
const $pos = state.selection.$head
|
||||||
const isCode = $pos.marks().find((m: Mark) => m.type.name === 'code')
|
const isCode = $pos.marks().find((m: Mark) => m.type.name === 'code')
|
||||||
const tr = state.tr
|
const tr = state.tr
|
||||||
|
|
||||||
if (dir === 'left') {
|
if (dir === 'left') {
|
||||||
const up = editorView.endOfTextblock('up')
|
const up = editorView.endOfTextblock('up')
|
||||||
if (!$pos.nodeBefore && up && isCode) {
|
if (!$pos.nodeBefore && up && isCode) {
|
||||||
tr.insertText(blank, $pos.pos - 1, $pos.pos)
|
tr.insertText(blank, $pos.pos - 1, $pos.pos)
|
||||||
dispatch(tr)
|
dispatch(tr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const down = editorView.endOfTextblock('down')
|
const down = editorView.endOfTextblock('down')
|
||||||
if (!$pos.nodeAfter && down && isCode) {
|
if (!$pos.nodeAfter && down && isCode) {
|
||||||
tr.insertText(blank, $pos.pos, $pos.pos + 1)
|
tr.insertText(blank, $pos.pos, $pos.pos + 1)
|
||||||
dispatch(tr)
|
dispatch(tr)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const codeKeymap = {
|
const codeKeymap = {
|
||||||
ArrowLeft: onArrow('left'),
|
ArrowLeft: onArrow('left'),
|
||||||
|
|
|
@ -2,7 +2,11 @@ import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
import type { YOptions } from '../../store/context'
|
import type { YOptions } from '../../store/context'
|
||||||
|
|
||||||
interface YUser { background: string, foreground: string, name: string }
|
interface YUser {
|
||||||
|
background: string
|
||||||
|
foreground: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
export const cursorBuilder = (user: YUser): HTMLElement => {
|
export const cursorBuilder = (user: YUser): HTMLElement => {
|
||||||
const cursor = document.createElement('span')
|
const cursor = document.createElement('span')
|
||||||
|
@ -19,10 +23,10 @@ export default (y: YOptions): ProseMirrorExtension => ({
|
||||||
plugins: (prev) =>
|
plugins: (prev) =>
|
||||||
y
|
y
|
||||||
? [
|
? [
|
||||||
...prev,
|
...prev,
|
||||||
ySyncPlugin(y.type),
|
ySyncPlugin(y.type),
|
||||||
yCursorPlugin(y.provider.awareness, { cursorBuilder }),
|
yCursorPlugin(y.provider.awareness, { cursorBuilder }),
|
||||||
yUndoPlugin()
|
yUndoPlugin()
|
||||||
]
|
]
|
||||||
: prev
|
: prev
|
||||||
})
|
})
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
} from 'prosemirror-menu'
|
} from 'prosemirror-menu'
|
||||||
|
|
||||||
import { wrapInList } from 'prosemirror-schema-list'
|
import { wrapInList } from 'prosemirror-schema-list'
|
||||||
import type{ NodeSelection } from 'prosemirror-state'
|
import type { NodeSelection } from 'prosemirror-state'
|
||||||
|
|
||||||
import { TextField, openPrompt } from './prompt'
|
import { TextField, openPrompt } from './prompt'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
|
@ -22,7 +22,6 @@ import type { Schema } from 'prosemirror-model'
|
||||||
|
|
||||||
// Helpers to create specific types of items
|
// Helpers to create specific types of items
|
||||||
|
|
||||||
|
|
||||||
const cut = (something) => something.filter(Boolean)
|
const cut = (something) => something.filter(Boolean)
|
||||||
|
|
||||||
function canInsert(state, nodeType) {
|
function canInsert(state, nodeType) {
|
||||||
|
@ -45,7 +44,11 @@ function insertImageItem(nodeType) {
|
||||||
return canInsert(state, nodeType)
|
return canInsert(state, nodeType)
|
||||||
},
|
},
|
||||||
run(state, _, view) {
|
run(state, _, view) {
|
||||||
const { from, to, node: { attrs } } = state.selection as NodeSelection
|
const {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
node: { attrs }
|
||||||
|
} = state.selection as NodeSelection
|
||||||
|
|
||||||
openPrompt({
|
openPrompt({
|
||||||
title: 'Insert image',
|
title: 'Insert image',
|
||||||
|
@ -78,7 +81,9 @@ function cmdItem(cmd, options) {
|
||||||
|
|
||||||
for (const prop in options) passedOptions[prop] = options[prop]
|
for (const prop in options) passedOptions[prop] = options[prop]
|
||||||
|
|
||||||
if ((!options.enable || options.enable === true) && !options.select) { passedOptions[options.enable ? 'enable' : 'select'] = (state) => cmd(state) }
|
if ((!options.enable || options.enable === true) && !options.select) {
|
||||||
|
passedOptions[options.enable ? 'enable' : 'select'] = (state) => cmd(state)
|
||||||
|
}
|
||||||
|
|
||||||
return new MenuItem(passedOptions)
|
return new MenuItem(passedOptions)
|
||||||
}
|
}
|
||||||
|
@ -130,7 +135,7 @@ function linkItem(markType) {
|
||||||
href: new TextField({
|
href: new TextField({
|
||||||
label: 'Link target',
|
label: 'Link target',
|
||||||
required: true
|
required: true
|
||||||
}),
|
})
|
||||||
},
|
},
|
||||||
callback(attrs) {
|
callback(attrs) {
|
||||||
toggleMark(markType, attrs)(view.state, view.dispatch)
|
toggleMark(markType, attrs)(view.state, view.dispatch)
|
||||||
|
@ -214,7 +219,7 @@ export function buildMenuItems(schema: Schema<any, any>) {
|
||||||
icon: {
|
icon: {
|
||||||
width: 13,
|
width: 13,
|
||||||
height: 16,
|
height: 16,
|
||||||
path: "M9.82857 7.76C10.9371 6.99429 11.7143 5.73714 11.7143 4.57143C11.7143 1.98857 9.71428 0 7.14286 0H0V16H8.04571C10.4343 16 12.2857 14.0571 12.2857 11.6686C12.2857 9.93143 11.3029 8.44571 9.82857 7.76ZM3.42799 2.85708H6.85656C7.80513 2.85708 8.57085 3.6228 8.57085 4.57137C8.57085 5.51994 7.80513 6.28565 6.85656 6.28565H3.42799V2.85708ZM3.42799 13.1429H7.42799C8.37656 13.1429 9.14228 12.3772 9.14228 11.4286C9.14228 10.4801 8.37656 9.71434 7.42799 9.71434H3.42799V13.1429Z"
|
path: 'M9.82857 7.76C10.9371 6.99429 11.7143 5.73714 11.7143 4.57143C11.7143 1.98857 9.71428 0 7.14286 0H0V16H8.04571C10.4343 16 12.2857 14.0571 12.2857 11.6686C12.2857 9.93143 11.3029 8.44571 9.82857 7.76ZM3.42799 2.85708H6.85656C7.80513 2.85708 8.57085 3.6228 8.57085 4.57137C8.57085 5.51994 7.80513 6.28565 6.85656 6.28565H3.42799V2.85708ZM3.42799 13.1429H7.42799C8.37656 13.1429 9.14228 12.3772 9.14228 11.4286C9.14228 10.4801 8.37656 9.71434 7.42799 9.71434H3.42799V13.1429Z'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -225,7 +230,7 @@ export function buildMenuItems(schema: Schema<any, any>) {
|
||||||
icon: {
|
icon: {
|
||||||
width: 14,
|
width: 14,
|
||||||
height: 16,
|
height: 16,
|
||||||
path: "M4.39216 0V3.42857H6.81882L3.06353 12.5714H0V16H8.78431V12.5714H6.35765L10.1129 3.42857H13.1765V0H4.39216Z"
|
path: 'M4.39216 0V3.42857H6.81882L3.06353 12.5714H0V16H8.78431V12.5714H6.35765L10.1129 3.42857H13.1765V0H4.39216Z'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -320,17 +325,16 @@ export function buildMenuItems(schema: Schema<any, any>) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
r.typeMenu = new Dropdown(
|
r.typeMenu = new Dropdown(cut([r.makeHead1, r.makeHead2, r.makeHead3, r.typeMenu, r.wrapBlockQuote]), {
|
||||||
cut([r.makeHead1, r.makeHead2, r.makeHead3, r.typeMenu, r.wrapBlockQuote]),
|
label: 'Тт',
|
||||||
{ label: 'Тт',
|
class: 'editor-dropdown' // TODO: use this class
|
||||||
class: 'editor-dropdown' // TODO: use this class
|
// FIXME: icon svg code shouldn't be here
|
||||||
// FIXME: icon svg code shouldn't be here
|
// icon: {
|
||||||
// icon: {
|
// width: 12,
|
||||||
// width: 12,
|
// height: 12,
|
||||||
// height: 12,
|
// path: "M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z"
|
||||||
// path: "M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z"
|
// }
|
||||||
// }
|
}) as MenuItem
|
||||||
}) as MenuItem
|
|
||||||
r.blockMenu = []
|
r.blockMenu = []
|
||||||
r.listMenu = [cut([r.wrapBulletList, r.wrapOrderedList])]
|
r.listMenu = [cut([r.wrapBulletList, r.wrapOrderedList])]
|
||||||
r.inlineMenu = [cut([r.toggleStrong, r.toggleEm, r.toggleMark])]
|
r.inlineMenu = [cut([r.toggleStrong, r.toggleEm, r.toggleMark])]
|
||||||
|
|
|
@ -1,60 +1,54 @@
|
||||||
import { renderGrouped } from "prosemirror-menu";
|
import { renderGrouped } from 'prosemirror-menu'
|
||||||
import { Plugin } from "prosemirror-state";
|
import { Plugin } from 'prosemirror-state'
|
||||||
import type { ProseMirrorExtension } from "../helpers";
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
import { buildMenuItems } from "./menu";
|
import { buildMenuItems } from './menu'
|
||||||
|
|
||||||
export class SelectionTooltip {
|
export class SelectionTooltip {
|
||||||
tooltip: any;
|
tooltip: any
|
||||||
|
|
||||||
constructor(view: any, schema: any) {
|
constructor(view: any, schema: any) {
|
||||||
this.tooltip = document.createElement("div");
|
this.tooltip = document.createElement('div')
|
||||||
this.tooltip.className = "tooltip";
|
this.tooltip.className = 'tooltip'
|
||||||
view.dom.parentNode.appendChild(this.tooltip);
|
view.dom.parentNode.appendChild(this.tooltip)
|
||||||
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any);
|
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any)
|
||||||
this.tooltip.appendChild(dom);
|
this.tooltip.appendChild(dom)
|
||||||
this.update(view, null);
|
this.update(view, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
update(view: any, lastState: any) {
|
update(view: any, lastState: any) {
|
||||||
const state = view.state;
|
const state = view.state
|
||||||
if (
|
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
||||||
lastState &&
|
return
|
||||||
lastState.doc.eq(state.doc) &&
|
|
||||||
lastState.selection.eq(state.selection)
|
|
||||||
)
|
|
||||||
{return;}
|
|
||||||
|
|
||||||
if (state.selection.empty) {
|
|
||||||
this.tooltip.style.display = "none";
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tooltip.style.display = "";
|
if (state.selection.empty) {
|
||||||
const { from, to } = state.selection;
|
this.tooltip.style.display = 'none'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltip.style.display = ''
|
||||||
|
const { from, to } = state.selection
|
||||||
const start = view.coordsAtPos(from),
|
const start = view.coordsAtPos(from),
|
||||||
end = view.coordsAtPos(to);
|
end = view.coordsAtPos(to)
|
||||||
const box = this.tooltip.offsetParent.getBoundingClientRect();
|
const box = this.tooltip.offsetParent.getBoundingClientRect()
|
||||||
const left = Math.max((start.left + end.left) / 2, start.left + 3);
|
const left = Math.max((start.left + end.left) / 2, start.left + 3)
|
||||||
this.tooltip.style.left = left - box.left + "px";
|
this.tooltip.style.left = left - box.left + 'px'
|
||||||
this.tooltip.style.bottom = box.bottom - (start.top + 15) + "px";
|
this.tooltip.style.bottom = box.bottom - (start.top + 15) + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.tooltip.remove();
|
this.tooltip.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toolTip(schema: any) {
|
export function toolTip(schema: any) {
|
||||||
return new Plugin({
|
return new Plugin({
|
||||||
view(editorView: any) {
|
view(editorView: any) {
|
||||||
return new SelectionTooltip(editorView, schema);
|
return new SelectionTooltip(editorView, schema)
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (): ProseMirrorExtension => ({
|
export default (): ProseMirrorExtension => ({
|
||||||
plugins: (prev, schema) => [
|
plugins: (prev, schema) => [...prev, toolTip(schema)]
|
||||||
...prev,
|
|
||||||
toolTip(schema)
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const tableInputRule = (schema: Schema) =>
|
||||||
new RegExp('^\\|{2,}\\s$'),
|
new RegExp('^\\|{2,}\\s$'),
|
||||||
(state: EditorState, match: string[], start: number, end: number) => {
|
(state: EditorState, match: string[], start: number, end: number) => {
|
||||||
const tr = state.tr
|
const tr = state.tr
|
||||||
const columns = Array.from({length: match[0].trim().length - 1})
|
const columns = Array.from({ length: match[0].trim().length - 1 })
|
||||||
const headers = columns.map(() => schema.node(schema.nodes.table_header, {}))
|
const headers = columns.map(() => schema.node(schema.nodes.table_header, {}))
|
||||||
const cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
|
const cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
|
||||||
const table = schema.node(schema.nodes.table, {}, [
|
const table = schema.node(schema.nodes.table, {}, [
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
import { DOMOutputSpec, DOMSerializer, Node as ProsemirrorNode, NodeSpec, NodeType, Schema } from 'prosemirror-model'
|
import {
|
||||||
|
DOMOutputSpec,
|
||||||
|
DOMSerializer,
|
||||||
|
Node as ProsemirrorNode,
|
||||||
|
NodeSpec,
|
||||||
|
NodeType,
|
||||||
|
Schema
|
||||||
|
} from 'prosemirror-model'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import type { EditorView } from 'prosemirror-view'
|
||||||
import { wrappingInputRule , inputRules } from 'prosemirror-inputrules'
|
import { wrappingInputRule, inputRules } from 'prosemirror-inputrules'
|
||||||
import { splitListItem } from 'prosemirror-schema-list'
|
import { splitListItem } from 'prosemirror-schema-list'
|
||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import type { NodeViewFn, ProseMirrorExtension } from '../helpers'
|
import type { NodeViewFn, ProseMirrorExtension } from '../helpers'
|
||||||
|
@ -59,8 +66,8 @@ class TodoItemView {
|
||||||
this.dom = res.dom
|
this.dom = res.dom
|
||||||
this.contentDOM = res.contentDOM
|
this.contentDOM = res.contentDOM
|
||||||
this.view = view
|
this.view = view
|
||||||
this.getPos = getPos;
|
this.getPos = getPos
|
||||||
(this.dom as HTMLElement).querySelector('input').addEventListener('click', this.handleClick.bind(this))
|
;(this.dom as HTMLElement).querySelector('input').addEventListener('click', this.handleClick.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(e: MouseEvent) {
|
handleClick(e: MouseEvent) {
|
||||||
|
@ -90,5 +97,5 @@ export default (): ProseMirrorExtension => ({
|
||||||
todo_item: (node: ProsemirrorNode, view: EditorView, getPos: () => number) => {
|
todo_item: (node: ProsemirrorNode, view: EditorView, getPos: () => number) => {
|
||||||
return new TodoItemView(node, view, getPos)
|
return new TodoItemView(node, view, getPos)
|
||||||
}
|
}
|
||||||
} as unknown as { [key:string]: NodeViewFn }
|
} as unknown as { [key: string]: NodeViewFn }
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,9 +3,9 @@ import type { Node, Schema, SchemaSpec } from 'prosemirror-model'
|
||||||
import type { Decoration, EditorView, NodeView } from 'prosemirror-view'
|
import type { Decoration, EditorView, NodeView } from 'prosemirror-view'
|
||||||
|
|
||||||
export interface ProseMirrorExtension {
|
export interface ProseMirrorExtension {
|
||||||
schema?: (prev: SchemaSpec) => SchemaSpec;
|
schema?: (prev: SchemaSpec) => SchemaSpec
|
||||||
plugins?: (prev: Plugin[], schema: Schema) => Plugin[];
|
plugins?: (prev: Plugin[], schema: Schema) => Plugin[]
|
||||||
nodeViews?: { [key: string]: NodeViewFn };
|
nodeViews?: { [key: string]: NodeViewFn }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProseMirrorState = EditorState | unknown
|
export type ProseMirrorState = EditorState | unknown
|
||||||
|
|
|
@ -5,7 +5,11 @@ import { Doc, XmlFragment } from 'yjs'
|
||||||
// import type { Reaction } from '../../../graphql/types.gen'
|
// import type { Reaction } from '../../../graphql/types.gen'
|
||||||
// import { setReactions } from '../../../stores/editor'
|
// import { setReactions } from '../../../stores/editor'
|
||||||
|
|
||||||
export const roomConnect = (room: string, username = '', keyname = 'collab'): [XmlFragment, WebrtcProvider] => {
|
export const roomConnect = (
|
||||||
|
room: string,
|
||||||
|
username = '',
|
||||||
|
keyname = 'collab'
|
||||||
|
): [XmlFragment, WebrtcProvider] => {
|
||||||
const ydoc = new Doc()
|
const ydoc = new Doc()
|
||||||
// const yarr = ydoc.getArray(keyname + '-reactions')
|
// const yarr = ydoc.getArray(keyname + '-reactions')
|
||||||
// TODO: use reactions
|
// TODO: use reactions
|
||||||
|
|
|
@ -25,15 +25,16 @@ const customKeymap = (props: ExtensionsProps): ProseMirrorExtension => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createExtensions = (props: ExtensionsProps): ProseMirrorExtension[] => {
|
export const createExtensions = (props: ExtensionsProps): ProseMirrorExtension[] => {
|
||||||
const eee = [
|
const extensions = [
|
||||||
placeholder(t('Just start typing...')),
|
placeholder(t('Just start typing...')),
|
||||||
customKeymap(props),
|
customKeymap(props),
|
||||||
base(props.markdown),
|
base(props.markdown),
|
||||||
selectionMenu(),
|
selectionMenu(),
|
||||||
scrollPlugin(props.config?.typewriterMode)
|
scrollPlugin(props.config?.typewriterMode)
|
||||||
]
|
]
|
||||||
|
|
||||||
if (props.markdown) {
|
if (props.markdown) {
|
||||||
eee.push(
|
extensions.push(
|
||||||
markdown(),
|
markdown(),
|
||||||
todoList(),
|
todoList(),
|
||||||
dragHandle(),
|
dragHandle(),
|
||||||
|
@ -54,8 +55,12 @@ export const createExtensions = (props: ExtensionsProps): ProseMirrorExtension[]
|
||||||
*/
|
*/
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (props.collab?.room) eee.push(collab(props.y))
|
|
||||||
return eee
|
if (props.collab?.room) {
|
||||||
|
extensions.push(collab(props.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensions
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createEmptyText = () => ({
|
export const createEmptyText = () => ({
|
||||||
|
|
|
@ -11,12 +11,12 @@ import { mod } from '../env'
|
||||||
import { serialize, createMarkdownParser } from '../markdown'
|
import { serialize, createMarkdownParser } from '../markdown'
|
||||||
import db from '../db'
|
import db from '../db'
|
||||||
import { isEmpty, isInitialized } from '../prosemirror/helpers'
|
import { isEmpty, isInitialized } from '../prosemirror/helpers'
|
||||||
|
import { useRouter } from '../../../stores/router'
|
||||||
|
|
||||||
const isText = (x) => x && x.doc && x.selection
|
const isText = (x) => x && x.doc && x.selection
|
||||||
const isState = (x) => typeof x.lastModified !== 'string' && Array.isArray(x.drafts || [])
|
const isState = (x) => typeof x.lastModified !== 'string' && Array.isArray(x.drafts || [])
|
||||||
const isDraft = (x): boolean => x && (x.text || x.path)
|
const isDraft = (x): boolean => x && (x.text || x.path)
|
||||||
|
|
||||||
|
|
||||||
export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
|
export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
|
||||||
const [store, setState] = createStore(initial)
|
const [store, setState] = createStore(initial)
|
||||||
|
|
||||||
|
@ -54,8 +54,6 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const toggleMarkdown = () => {
|
const toggleMarkdown = () => {
|
||||||
const state = unwrap(store)
|
const state = unwrap(store)
|
||||||
const editorState = store.text as EditorState
|
const editorState = store.text as EditorState
|
||||||
|
@ -65,7 +63,9 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
|
||||||
|
|
||||||
if (markdown) {
|
if (markdown) {
|
||||||
const lines = serialize(editorState).split('\n')
|
const lines = serialize(editorState).split('\n')
|
||||||
const nodes = lines.map((text) => text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' })
|
const nodes = lines.map((text) =>
|
||||||
|
text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' }
|
||||||
|
)
|
||||||
|
|
||||||
doc = { type: 'doc', content: nodes }
|
doc = { type: 'doc', content: nodes }
|
||||||
} else {
|
} else {
|
||||||
|
@ -178,7 +178,8 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
|
||||||
|
|
||||||
const fetchData = async (): Promise<State> => {
|
const fetchData = async (): Promise<State> => {
|
||||||
const state: State = unwrap(store)
|
const state: State = unwrap(store)
|
||||||
const room = window.location.pathname?.slice(1).trim()
|
const { searchParams } = useRouter<{ room: string }>()
|
||||||
|
const room = searchParams().room
|
||||||
const args = { room: room ?? undefined }
|
const args = { room: room ?? undefined }
|
||||||
const data = await db.get('state')
|
const data = await db.get('state')
|
||||||
let parsed: State
|
let parsed: State
|
||||||
|
@ -275,26 +276,27 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveState = () => debounce(async (state: State) => {
|
const saveState = () =>
|
||||||
const data: State = {
|
debounce(async (state: State) => {
|
||||||
lastModified: state.lastModified,
|
const data: State = {
|
||||||
drafts: state.drafts,
|
lastModified: state.lastModified,
|
||||||
config: state.config,
|
drafts: state.drafts,
|
||||||
path: state.path,
|
config: state.config,
|
||||||
markdown: state.markdown,
|
path: state.path,
|
||||||
collab: {
|
markdown: state.markdown,
|
||||||
room: state.collab?.room
|
collab: {
|
||||||
|
room: state.collab?.room
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (isInitialized(state.text)) {
|
if (isInitialized(state.text)) {
|
||||||
data.text = store.editorView.state.toJSON()
|
data.text = store.editorView.state.toJSON()
|
||||||
} else if (state.text) {
|
} else if (state.text) {
|
||||||
data.text = state.text
|
data.text = state.text
|
||||||
}
|
}
|
||||||
|
|
||||||
db.set('state', JSON.stringify(data))
|
db.set('state', JSON.stringify(data))
|
||||||
}, 200)
|
}, 200)
|
||||||
|
|
||||||
const setFullscreen = (fullscreen: boolean) => {
|
const setFullscreen = (fullscreen: boolean) => {
|
||||||
setState({ fullscreen })
|
setState({ fullscreen })
|
||||||
|
@ -309,7 +311,8 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
|
||||||
const doStartCollab = async (state: State): Promise<State> => {
|
const doStartCollab = async (state: State): Promise<State> => {
|
||||||
const backup = state.args?.room && state.collab?.room !== state.args.room
|
const backup = state.args?.room && state.collab?.room !== state.args.room
|
||||||
const room = state.args?.room ?? uuidv4()
|
const room = state.args?.room ?? uuidv4()
|
||||||
window.history.replaceState(null, '', `/${room}`)
|
const { changeSearchParam } = useRouter<{ room: string }>()
|
||||||
|
changeSearchParam('room', room, true)
|
||||||
|
|
||||||
const { roomConnect } = await import('../prosemirror/p2p')
|
const { roomConnect } = await import('../prosemirror/p2p')
|
||||||
const [type, provider] = roomConnect(room)
|
const [type, provider] = roomConnect(room)
|
||||||
|
@ -380,7 +383,7 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTheme = () => {
|
const updateTheme = () => {
|
||||||
const { theme } = getTheme(unwrap(store))
|
const { theme } = getTheme(unwrap(store))
|
||||||
setState('config', { theme })
|
setState('config', { theme })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,65 +19,65 @@ export interface ExtensionsProps {
|
||||||
typewriterMode?: boolean
|
typewriterMode?: boolean
|
||||||
}
|
}
|
||||||
export interface Args {
|
export interface Args {
|
||||||
cwd?: string;
|
cwd?: string
|
||||||
draft?: string;
|
draft?: string
|
||||||
room?: string;
|
room?: string
|
||||||
text?: any;
|
text?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrettierConfig {
|
export interface PrettierConfig {
|
||||||
printWidth: number;
|
printWidth: number
|
||||||
tabWidth: number;
|
tabWidth: number
|
||||||
useTabs: boolean;
|
useTabs: boolean
|
||||||
semi: boolean;
|
semi: boolean
|
||||||
singleQuote: boolean;
|
singleQuote: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
theme: string;
|
theme: string
|
||||||
// codeTheme: string;
|
// codeTheme: string;
|
||||||
font: string;
|
font: string
|
||||||
fontSize: number;
|
fontSize: number
|
||||||
contentWidth: number;
|
contentWidth: number
|
||||||
typewriterMode: boolean;
|
typewriterMode: boolean
|
||||||
prettier: PrettierConfig;
|
prettier: PrettierConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorObject {
|
export interface ErrorObject {
|
||||||
id: string;
|
id: string
|
||||||
props?: unknown;
|
props?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YOptions {
|
export interface YOptions {
|
||||||
type: XmlFragment;
|
type: XmlFragment
|
||||||
provider: WebrtcProvider;
|
provider: WebrtcProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Collab {
|
export interface Collab {
|
||||||
started?: boolean;
|
started?: boolean
|
||||||
error?: boolean;
|
error?: boolean
|
||||||
room?: string;
|
room?: string
|
||||||
y?: YOptions;
|
y?: YOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LoadingType = 'loading' | 'initialized'
|
export type LoadingType = 'loading' | 'initialized'
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
isMac?: boolean
|
isMac?: boolean
|
||||||
text?: ProseMirrorState;
|
text?: ProseMirrorState
|
||||||
editorView?: EditorView;
|
editorView?: EditorView
|
||||||
extensions?: ProseMirrorExtension[];
|
extensions?: ProseMirrorExtension[]
|
||||||
markdown?: boolean;
|
markdown?: boolean
|
||||||
lastModified?: Date;
|
lastModified?: Date
|
||||||
drafts: Draft[];
|
drafts: Draft[]
|
||||||
config: Config;
|
config: Config
|
||||||
error?: ErrorObject;
|
error?: ErrorObject
|
||||||
loading?: LoadingType;
|
loading?: LoadingType
|
||||||
fullscreen?: boolean;
|
fullscreen?: boolean
|
||||||
collab?: Collab;
|
collab?: Collab
|
||||||
path?: string;
|
path?: string
|
||||||
args?: Args;
|
args?: Args
|
||||||
keymap?: { [key: string]: Command; }
|
keymap?: { [key: string]: Command }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Draft {
|
export interface Draft {
|
||||||
|
@ -91,7 +91,7 @@ export interface Draft {
|
||||||
|
|
||||||
export interface EditorActions {
|
export interface EditorActions {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
[key:string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServiceError extends Error {
|
export class ServiceError extends Error {
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
.article__title {
|
.article__title {
|
||||||
@include font-size(2.4rem);
|
@include font-size(2.4rem);
|
||||||
|
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,10 +32,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.article__controls {
|
.article__controls {
|
||||||
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
align-content: baseline;
|
align-content: baseline;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@include font-size(1.4rem);
|
|
||||||
padding-top: 2em;
|
padding-top: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,14 @@ button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
outline: none;
|
outline: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-family: 'JetBrains Mono';
|
background: none;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--foreground);
|
||||||
|
border: 1px solid var(--foreground);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
background: none;
|
|
||||||
font-family: 'Muller';
|
|
||||||
color: var(--foreground);
|
|
||||||
border: 1px solid var(--foreground);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.primary {
|
button.primary {
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
.main-content {
|
|
||||||
padding-top: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: rgb(0, 100, 200);
|
color: rgb(0 100 200);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +12,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
a:visited {
|
||||||
color: rgb(0, 80, 160);
|
color: rgb(0 80 160);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +28,7 @@ textarea {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
-webkit-padding: 0.4em 0;
|
-webkit-padding: 0.4em 0;
|
||||||
padding: 0.4em;
|
padding: 0.4em;
|
||||||
margin: 0 0 0.5em 0;
|
margin: 0 0 0.5em;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
@ -66,7 +62,6 @@ button:focus {
|
||||||
position: relative;
|
position: relative;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
-webkit-font-variant-ligatures: none;
|
|
||||||
font-variant-ligatures: none;
|
font-variant-ligatures: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
margin: 1em 1em 1em 0;
|
margin: 1em 1em 1em 0;
|
||||||
|
@ -121,12 +116,14 @@ button:focus {
|
||||||
blockquote {
|
blockquote {
|
||||||
border-left: 2px solid;
|
border-left: 2px solid;
|
||||||
@include font-size(1.6rem);
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
margin: 1.5em 0;
|
margin: 1.5em 0;
|
||||||
padding-left: 1.6em;
|
padding-left: 1.6em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-menuitem {
|
.ProseMirror-menuitem {
|
||||||
|
display: flex;
|
||||||
font-size: small;
|
font-size: small;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -157,15 +154,10 @@ button:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-tooltip .ProseMirror-menu {
|
.ProseMirror-tooltip .ProseMirror-menu {
|
||||||
width: -webkit-fit-content;
|
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-menuitem {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menuseparator {
|
.ProseMirror-menuseparator {
|
||||||
border-right: 1px solid #ddd;
|
border-right: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
@ -189,11 +181,11 @@ button:focus {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown:after {
|
.ProseMirror-menu-dropdown::after {
|
||||||
content: '';
|
content: '';
|
||||||
border-left: 4px solid transparent;
|
border-left: 4px solid transparent;
|
||||||
border-right: 4px solid transparent;
|
border-right: 4px solid transparent;
|
||||||
border-top: 4px solid currentColor;
|
border-top: 4px solid currentcolor;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
|
@ -211,6 +203,7 @@ button:focus {
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown-menu {
|
.ProseMirror-menu-dropdown-menu {
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
|
|
||||||
/* min-width: 6em; */
|
/* min-width: 6em; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,11 +221,11 @@ button:focus {
|
||||||
margin-right: -4px;
|
margin-right: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-menu-submenu-label:after {
|
.ProseMirror-menu-submenu-label::after {
|
||||||
content: '';
|
content: '';
|
||||||
border-top: 4px solid transparent;
|
border-top: 4px solid transparent;
|
||||||
border-bottom: 4px solid transparent;
|
border-bottom: 4px solid transparent;
|
||||||
border-left: 4px solid currentColor;
|
border-left: 4px solid currentcolor;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
|
@ -273,7 +266,6 @@ button:focus {
|
||||||
border-bottom: 1px solid silver;
|
border-bottom: 1px solid silver;
|
||||||
background: white;
|
background: white;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
@ -288,7 +280,7 @@ button:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-icon svg {
|
.ProseMirror-icon svg {
|
||||||
fill: currentColor;
|
fill: currentcolor;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,10 +300,6 @@ button:focus {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-hideselection *::-moz-selection {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-hideselection {
|
.ProseMirror-hideselection {
|
||||||
caret-color: transparent;
|
caret-color: transparent;
|
||||||
}
|
}
|
||||||
|
@ -325,7 +313,7 @@ li.ProseMirror-selectednode {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.ProseMirror-selectednode:after {
|
li.ProseMirror-selectednode::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -32px;
|
left: -32px;
|
||||||
|
@ -355,7 +343,7 @@ li.ProseMirror-selectednode:after {
|
||||||
|
|
||||||
.ProseMirror-prompt {
|
.ProseMirror-prompt {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
@ -400,7 +388,7 @@ li.ProseMirror-selectednode:after {
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
||||||
color: #000;
|
color: #000;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
overflow: y-auto;
|
overflow: y-auto;
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-family: 'JetBrains Mono';
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.error pre {
|
.error pre {
|
||||||
background: var(--foreground) 19;
|
background: var(--foreground);
|
||||||
border: 1px solid var(--foreground);
|
border: 1px solid var(--foreground);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
.layout {
|
.layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-family: 'Muller';
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
color: rgba(255,255,255,0.5);
|
color: rgb(255 255 255 / 50%);
|
||||||
font-family: 'Muller';
|
|
||||||
@include font-size(1.6rem);
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -9,6 +9,19 @@
|
||||||
p {
|
p {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
@include font-size(120%);
|
||||||
|
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
height: auto;
|
||||||
|
min-height: 50px;
|
||||||
|
padding: 0 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-off {
|
.sidebar-off {
|
||||||
|
@ -44,7 +57,7 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cmask id='mask0_1090_23825' style='mask-type:alpha' maskUnits='userSpaceOnUse' x='0' y='14' width='4' height='4'%3E%3Crect y='14.8237' width='3.17647' height='3.17647' fill='%23fff'/%3E%3C/mask%3E%3Cg mask='url(%23mask0_1090_23825)'%3E%3Cpath d='M16.0941 1.05908H0.847027C0.379194 1.05908 0 1.43828 0 1.90611V18.0003L3.38824 14.612H16.0942C16.562 14.612 16.9412 14.2328 16.9412 13.765V1.90614C16.9412 1.43831 16.562 1.05912 16.0942 1.05912L16.0941 1.05908ZM15.2471 12.9179H1.69412V2.7532H15.2471V12.9179Z' fill='black'/%3E%3C/g%3E%3Crect x='1' y='1' width='16' height='12.8235' stroke='black' stroke-width='2'/%3E%3Crect x='4.23535' y='3.17627' width='9.52941' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='9.5293' width='7.41176' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='6.35303' width='5.29412' height='2.11765' fill='black'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cmask id='mask0_1090_23825' style='mask-type:alpha' maskUnits='userSpaceOnUse' x='0' y='14' width='4' height='4'%3E%3Crect y='14.8237' width='3.17647' height='3.17647' fill='%23fff'/%3E%3C/mask%3E%3Cg mask='url(%23mask0_1090_23825)'%3E%3Cpath d='M16.0941 1.05908H0.847027C0.379194 1.05908 0 1.43828 0 1.90611V18.0003L3.38824 14.612H16.0942C16.562 14.612 16.9412 14.2328 16.9412 13.765V1.90614C16.9412 1.43831 16.562 1.05912 16.0942 1.05912L16.0941 1.05908ZM15.2471 12.9179H1.69412V2.7532H15.2471V12.9179Z' fill='black'/%3E%3C/g%3E%3Crect x='1' y='1' width='16' height='12.8235' stroke='black' stroke-width='2'/%3E%3Crect x='4.23535' y='3.17627' width='9.52941' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='9.5293' width='7.41176' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='6.35303' width='5.29412' height='2.11765' fill='black'/%3E%3C/svg%3E");
|
||||||
content: '';
|
content: '';
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
@ -73,7 +86,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-label {
|
.sidebar-label {
|
||||||
color: var(--foreground) #7f7f7f;
|
color: var(--foreground);
|
||||||
|
|
||||||
> i {
|
> i {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
@ -83,16 +97,15 @@
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container button,
|
.sidebar-container button,
|
||||||
.sidebar-container a,
|
.sidebar-container a,
|
||||||
.sidebar-item {
|
.sidebar-item {
|
||||||
text-align: left;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
outline: none;
|
outline: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-family: 'Muller';
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,20 +116,6 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container {
|
|
||||||
h4 {
|
|
||||||
@include font-size(120%);
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
height: auto;
|
|
||||||
min-height: 50px;
|
|
||||||
padding: 0 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-link {
|
.sidebar-link {
|
||||||
background: none;
|
background: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -138,12 +137,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
color: var(--foreground) 99;
|
color: var(--foreground);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.draft {
|
&.draft {
|
||||||
color: rgba(255,255,255,0.5);
|
color: rgb(255 255 255 / 50%);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin: 0 0 1em 1.5em;
|
margin: 0 0 1em 1.5em;
|
||||||
width: calc(100% - 2rem);
|
width: calc(100% - 2rem);
|
||||||
|
@ -176,20 +175,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-switcher {
|
.theme-switcher {
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.3);
|
border-bottom: 1px solid rgb(255 255 255 / 30%);
|
||||||
border-top: 1px solid rgba(255,255,255,0.3);
|
border-top: 1px solid rgb(255 255 255 / 30%);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
padding: 1em 0;
|
padding: 1em 0;
|
||||||
|
|
||||||
input[type=checkbox] {
|
input[type='checkbox'] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
+ label {
|
+ label {
|
||||||
background: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.20869 7.73227C5.22953 7.36499 4.38795 6.70402 3.79906 5.83976C3.2103 4.97565 2.90318 3.95064 2.91979 2.90512C2.93639 1.8597 3.27597 0.844915 3.8919 0C2.82862 0.254038 1.87585 0.844877 1.17594 1.68438C0.475894 2.52388 0.0660276 3.5671 0.00731938 4.6585C-0.0513888 5.74989 0.244296 6.83095 0.850296 7.74073C1.45631 8.65037 2.34006 9.33992 3.36994 9.70637C4.39987 10.073 5.52063 10.0969 6.56523 9.77466C7.60985 9.45247 8.52223 8.80134 9.16667 7.91837C8.1842 8.15404 7.15363 8.08912 6.20869 7.73205V7.73227Z' fill='white'/%3E%3C/svg%3E%0A") no-repeat 30px 9px,
|
background: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.20869 7.73227C5.22953 7.36499 4.38795 6.70402 3.79906 5.83976C3.2103 4.97565 2.90318 3.95064 2.91979 2.90512C2.93639 1.8597 3.27597 0.844915 3.8919 0C2.82862 0.254038 1.87585 0.844877 1.17594 1.68438C0.475894 2.52388 0.0660276 3.5671 0.00731938 4.6585C-0.0513888 5.74989 0.244296 6.83095 0.850296 7.74073C1.45631 8.65037 2.34006 9.33992 3.36994 9.70637C4.39987 10.073 5.52063 10.0969 6.56523 9.77466C7.60985 9.45247 8.52223 8.80134 9.16667 7.91837C8.1842 8.15404 7.15363 8.08912 6.20869 7.73205V7.73227Z' fill='white'/%3E%3C/svg%3E%0A")
|
||||||
url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.41196 0H5.58811V2.43024H6.41196V0ZM5.99988 8.96576C4.36601 8.96576 3.03419 7.63397 3.03419 6.00007C3.04792 4.3662 4.36598 3.04818 5.99988 3.03439C7.63375 3.03439 8.96557 4.3662 8.96557 6.00007C8.96557 7.63395 7.63375 8.96576 5.99988 8.96576ZM5.58811 9.56977H6.41196V12H5.58811V9.56977ZM12.0002 5.58811H9.56996V6.41196H12.0002V5.58811ZM0 5.58811H2.43024V6.41196H0V5.58811ZM8.81339 3.76727L10.5318 2.04891L9.94925 1.46641L8.23089 3.18477L8.81339 3.76727ZM3.7745 8.8129L2.05614 10.5313L1.47364 9.94877L3.192 8.2304L3.7745 8.8129ZM9.95043 10.5269L10.5329 9.94437L8.81456 8.22601L8.23207 8.80851L9.95043 10.5269ZM3.76864 3.18731L3.18614 3.76981L1.46778 2.05145L2.05028 1.46895L3.76864 3.18731Z' fill='%231F1F1F'/%3E%3C/svg%3E%0A") #000 no-repeat 8px 8px;
|
no-repeat 30px 9px,
|
||||||
|
url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.41196 0H5.58811V2.43024H6.41196V0ZM5.99988 8.96576C4.36601 8.96576 3.03419 7.63397 3.03419 6.00007C3.04792 4.3662 4.36598 3.04818 5.99988 3.03439C7.63375 3.03439 8.96557 4.3662 8.96557 6.00007C8.96557 7.63395 7.63375 8.96576 5.99988 8.96576ZM5.58811 9.56977H6.41196V12H5.58811V9.56977ZM12.0002 5.58811H9.56996V6.41196H12.0002V5.58811ZM0 5.58811H2.43024V6.41196H0V5.58811ZM8.81339 3.76727L10.5318 2.04891L9.94925 1.46641L8.23089 3.18477L8.81339 3.76727ZM3.7745 8.8129L2.05614 10.5313L1.47364 9.94877L3.192 8.2304L3.7745 8.8129ZM9.95043 10.5269L10.5329 9.94437L8.81456 8.22601L8.23207 8.80851L9.95043 10.5269ZM3.76864 3.18731L3.18614 3.76981L1.46778 2.05145L2.05028 1.46895L3.76864 3.18731Z' fill='%231F1F1F'/%3E%3C/svg%3E%0A")
|
||||||
|
#000 no-repeat 8px 8px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -200,7 +201,7 @@
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -216,7 +217,7 @@
|
||||||
&:checked + label {
|
&:checked + label {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
background-color: #1f1f1f;
|
background-color: #1f1f1f;
|
||||||
left: 24px;
|
left: 24px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// TODO: additional entities list column + article
|
// TODO: additional entities list column + article
|
||||||
|
|
||||||
import { For, Show } from 'solid-js/web'
|
import { For, Show } from 'solid-js'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { TopicCard } from '../Topic/Card'
|
import { TopicCard } from '../Topic/Card'
|
||||||
|
@ -21,7 +21,7 @@ interface BesideProps {
|
||||||
iconButton?: boolean
|
iconButton?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props: BesideProps) => {
|
export const Beside = (props: BesideProps) => {
|
||||||
return (
|
return (
|
||||||
<Show when={!!props.beside?.slug && props.values?.length > 0}>
|
<Show when={!!props.beside?.slug && props.values?.length > 0}>
|
||||||
<div class="floor floor--9">
|
<div class="floor floor--9">
|
||||||
|
|
|
@ -418,7 +418,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shoutCardDetailsTtem {
|
.shoutCardDetailsItem {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-right: 1.7em;
|
margin-right: 1.7em;
|
||||||
|
@ -454,6 +454,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shoutCardDetailsViewed {
|
||||||
|
.icon {
|
||||||
|
margin-top: -0.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.rating {
|
.rating {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, For, Show } from 'solid-js'
|
||||||
import { For, Show } from 'solid-js/web'
|
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { capitalize } from '../../utils'
|
import { capitalize } from '../../utils'
|
||||||
import { translit } from '../../utils/ru2en'
|
import { translit } from '../../utils/ru2en'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import style from './Card.module.scss'
|
import styles from './Card.module.scss'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { handleClientRouteLinkClick } from '../../stores/router'
|
import { handleClientRouteLinkClick } from '../../stores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
@ -72,33 +71,33 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
class={clsx(style.shoutCard, `${props.settings?.additionalClass || ''}`)}
|
class={clsx(styles.shoutCard, `${props.settings?.additionalClass || ''}`)}
|
||||||
classList={{
|
classList={{
|
||||||
[style.shoutCardShort]: props.settings?.isShort,
|
[styles.shoutCardShort]: props.settings?.isShort,
|
||||||
[style.shoutCardPhotoBottom]: props.settings?.noimage && props.settings?.photoBottom,
|
[styles.shoutCardPhotoBottom]: props.settings?.noimage && props.settings?.photoBottom,
|
||||||
[style.shoutCardFeed]: props.settings?.isFeedMode,
|
[styles.shoutCardFeed]: props.settings?.isFeedMode,
|
||||||
[style.shoutCardFloorImportant]: props.settings?.isFloorImportant,
|
[styles.shoutCardFloorImportant]: props.settings?.isFloorImportant,
|
||||||
[style.shoutCardWithCover]: props.settings?.isWithCover,
|
[styles.shoutCardWithCover]: props.settings?.isWithCover,
|
||||||
[style.shoutCardBigTitle]: props.settings?.isBigTitle,
|
[styles.shoutCardBigTitle]: props.settings?.isBigTitle,
|
||||||
[style.shoutCardVertical]: props.settings?.isVertical,
|
[styles.shoutCardVertical]: props.settings?.isVertical,
|
||||||
[style.shoutCardWithBorder]: props.settings?.withBorder,
|
[styles.shoutCardWithBorder]: props.settings?.withBorder,
|
||||||
[style.shoutCardCompact]: props.settings?.isCompact,
|
[styles.shoutCardCompact]: props.settings?.isCompact,
|
||||||
[style.shoutCardSingle]: props.settings?.isSingle
|
[styles.shoutCardSingle]: props.settings?.isSingle
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={!props.settings?.noimage && cover}>
|
<Show when={!props.settings?.noimage && cover}>
|
||||||
<div class={style.shoutCardCoverContainer}>
|
<div class={styles.shoutCardCoverContainer}>
|
||||||
<div class={style.shoutCardCover}>
|
<div class={styles.shoutCardCover}>
|
||||||
<img src={cover || ''} alt={title || ''} loading="lazy" />
|
<img src={cover || ''} alt={title || ''} loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class={style.shoutCardContent}>
|
<div class={styles.shoutCardContent}>
|
||||||
<Show when={layout && layout !== 'article' && !(props.settings?.noicon || props.settings?.noimage)}>
|
<Show when={layout && layout !== 'article' && !(props.settings?.noicon || props.settings?.noimage)}>
|
||||||
<div class={style.shoutCardType}>
|
<div class={styles.shoutCardType}>
|
||||||
<a href={`/topic/${mainTopic.slug}`}>
|
<a href={`/topic/${mainTopic.slug}`}>
|
||||||
<Icon name={layout} class={style.icon} />
|
<Icon name={layout} class={styles.icon} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -113,24 +112,24 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class={style.shoutCardTitlesContainer}>
|
<div class={styles.shoutCardTitlesContainer}>
|
||||||
<a href={`/${slug || ''}`} onClick={handleClientRouteLinkClick}>
|
<a href={`/${slug || ''}`} onClick={handleClientRouteLinkClick}>
|
||||||
<div class={style.shoutCardTitle}>
|
<div class={styles.shoutCardTitle}>
|
||||||
<span class={style.shoutCardLinkContainer}>{title}</span>
|
<span class={styles.shoutCardLinkContainer}>{title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!props.settings?.nosubtitle && subtitle}>
|
<Show when={!props.settings?.nosubtitle && subtitle}>
|
||||||
<div class={style.shoutCardSubtitle}>
|
<div class={styles.shoutCardSubtitle}>
|
||||||
<span class={style.shoutCardLinkContainer}>{subtitle}</span>
|
<span class={styles.shoutCardLinkContainer}>{subtitle}</span>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!props.settings?.noauthor || !props.settings?.nodate}>
|
<Show when={!props.settings?.noauthor || !props.settings?.nodate}>
|
||||||
<div class={style.shoutDetails}>
|
<div class={styles.shoutDetails}>
|
||||||
<Show when={!props.settings?.noauthor}>
|
<Show when={!props.settings?.noauthor}>
|
||||||
<div class={style.shoutAuthor}>
|
<div class={styles.shoutAuthor}>
|
||||||
<For each={authors}>
|
<For each={authors}>
|
||||||
{(author, index) => {
|
{(author, index) => {
|
||||||
const name =
|
const name =
|
||||||
|
@ -150,44 +149,50 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!props.settings?.nodate}>
|
<Show when={!props.settings?.nodate}>
|
||||||
<div class={style.shoutDate}>{formattedDate()}</div>
|
<div class={styles.shoutDate}>{formattedDate()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.settings?.isFeedMode}>
|
<Show when={props.settings?.isFeedMode}>
|
||||||
<section class={style.shoutCardDetails}>
|
<section class={styles.shoutCardDetails}>
|
||||||
<div class={style.shoutCardDetailsContent}>
|
<div class={styles.shoutCardDetailsContent}>
|
||||||
<div class={clsx(style.shoutCardDetailsItem, 'rating')}>
|
<div class={clsx(styles.shoutCardDetailsItem, styles.rating)}>
|
||||||
<button class="rating__control">−</button>
|
<button class={styles.ratingControl}>−</button>
|
||||||
<span class="rating__value">{stat?.rating || ''}</span>
|
<span class={styles.ratingValue}>{stat?.rating || ''}</span>
|
||||||
<button class="rating__control">+</button>
|
<button class={styles.ratingControl}>+</button>
|
||||||
</div>
|
</div>
|
||||||
<div class={clsx(style.shoutCardDetailsItem, style.shoutCardComments)}>
|
<div
|
||||||
<Icon name="eye" class={style.icon} />
|
class={clsx(
|
||||||
|
styles.shoutCardDetailsItem,
|
||||||
|
styles.shoutCardDetailsViewed,
|
||||||
|
styles.shoutCardComments
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon name="eye" class={styles.icon} />
|
||||||
{stat?.viewed}
|
{stat?.viewed}
|
||||||
</div>
|
</div>
|
||||||
<div class={clsx(style.shoutCardDetailsTtem, style.shoutCardComments)}>
|
<div class={clsx(styles.shoutCardDetailsItem, styles.shoutCardComments)}>
|
||||||
<a href={`/${slug + '#comments' || ''}`}>
|
<a href={`/${slug + '#comments' || ''}`}>
|
||||||
<Icon name="comment" class={style.icon} />
|
<Icon name="comment" class={styles.icon} />
|
||||||
{stat?.commented || ''}
|
{stat?.commented || ''}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={style.shoutCardDetailsItem}>
|
<div class={styles.shoutCardDetailsItem}>
|
||||||
<button>
|
<button>
|
||||||
<Icon name="bookmark" class={style.icon} />
|
<Icon name="bookmark" class={styles.icon} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={style.shoutCardDetailsItem}>
|
<div class={styles.shoutCardDetailsItem}>
|
||||||
<button>
|
<button>
|
||||||
<Icon name="ellipsis" class={style.icon} />
|
<Icon name="ellipsis" class={styles.icon} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button--light shout-card__edit-control">{t('Collaborate')}</button>
|
<button class={clsx('button--light', styles.shoutCardEditControl)}>{t('Collaborate')}</button>
|
||||||
</section>
|
</section>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { JSX } from 'solid-js/jsx-runtime'
|
import type { JSX } from 'solid-js/jsx-runtime'
|
||||||
import { For, Show } from 'solid-js/web'
|
import { For, Show } from 'solid-js'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
import './Group.scss'
|
import './Group.scss'
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { For, Suspense } from 'solid-js/web'
|
import { Row1 } from './Row1'
|
||||||
import OneWide from './Row1'
|
import { Row2 } from './Row2'
|
||||||
import Row2 from './Row2'
|
import { Row3 } from './Row3'
|
||||||
import Row3 from './Row3'
|
|
||||||
import { shuffle } from '../../utils'
|
import { shuffle } from '../../utils'
|
||||||
import { createMemo, createSignal } from 'solid-js'
|
import { createMemo, createSignal, For, Suspense } from 'solid-js'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import './List.scss'
|
import './List.scss'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
|
|
||||||
export const Block6 = (props: { articles: Shout[] }) => {
|
export const Block6 = (props: { articles: Shout[] }) => {
|
||||||
const dice = createMemo(() => shuffle([OneWide, Row2, Row3]))
|
const dice = createMemo(() => shuffle([Row1, Row2, Row3]))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -2,12 +2,12 @@ import { Show } from 'solid-js'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
|
|
||||||
export default (props: { article: Shout }) => (
|
export const Row1 = (props: { article: Shout }) => (
|
||||||
<Show when={!!props.article}>
|
<Show when={!!props.article}>
|
||||||
<div class="floor floor--one-article">
|
<div class="floor floor--one-article">
|
||||||
<div class="wide-container row">
|
<div class="wide-container row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<ArticleCard article={props.article} settings={{isSingle: true}} />
|
<ArticleCard article={props.article} settings={{ isSingle: true }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { createComputed, createSignal, Show } from 'solid-js'
|
import { createComputed, createSignal, Show, For } from 'solid-js'
|
||||||
import { For } from 'solid-js/web'
|
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
|
|
||||||
const x = [
|
const x = [
|
||||||
['6', '6'],
|
['6', '6'],
|
||||||
['4', '8'],
|
['4', '8'],
|
||||||
['8', '4']
|
['8', '4']
|
||||||
]
|
]
|
||||||
|
|
||||||
export default (props: { articles: Shout[] }) => {
|
export const Row2 = (props: { articles: Shout[] }) => {
|
||||||
const [y, setY] = createSignal(0)
|
const [y, setY] = createSignal(0)
|
||||||
|
|
||||||
createComputed(() => setY(Math.floor(Math.random() * x.length)))
|
createComputed(() => setY(Math.floor(Math.random() * x.length)))
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { JSX } from 'solid-js/jsx-runtime'
|
import type { JSX } from 'solid-js/jsx-runtime'
|
||||||
import { For } from 'solid-js/web'
|
import { For } from 'solid-js'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
|
|
||||||
export default (props: { articles: Shout[]; header?: JSX.Element }) => {
|
export const Row3 = (props: { articles: Shout[]; header?: JSX.Element }) => {
|
||||||
return (
|
return (
|
||||||
<div class="floor">
|
<div class="floor">
|
||||||
<div class="wide-container row">
|
<div class="wide-container row">
|
||||||
|
|
|
@ -85,12 +85,12 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
|
||||||
</For>
|
</For>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p class="settings">
|
<div class="settings">
|
||||||
<a href="/feed/settings">
|
<a href="/feed/settings">
|
||||||
<strong>{t('Feed settings')}</strong>
|
<strong>{t('Feed settings')}</strong>
|
||||||
|
<Icon name="settings" />
|
||||||
</a>
|
</a>
|
||||||
<Icon name="settings" />
|
</div>
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { For } from 'solid-js/web'
|
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
import { Swiper, Navigation, Pagination } from 'swiper'
|
import { Swiper, Navigation, Pagination } from 'swiper'
|
||||||
import type { SwiperOptions } from 'swiper'
|
import type { SwiperOptions } from 'swiper'
|
||||||
|
@ -7,7 +6,7 @@ import 'swiper/scss/navigation'
|
||||||
import 'swiper/scss/pagination'
|
import 'swiper/scss/pagination'
|
||||||
import './Slider.scss'
|
import './Slider.scss'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, Show, For } from 'solid-js'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
|
|
||||||
interface SliderProps {
|
interface SliderProps {
|
||||||
|
|
|
@ -148,11 +148,11 @@
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
/* Red/500 */
|
/* Red/500 */
|
||||||
color: #D00820;
|
color: #d00820;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #D00820;
|
color: #d00820;
|
||||||
border-color: #D00820;
|
border-color: #d00820;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -160,3 +160,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #141414;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 24px;
|
||||||
|
margin-bottom: 52px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
import styles from './ConfirmEmail.module.scss'
|
|
||||||
import authModalStyles from './AuthModal.module.scss'
|
|
||||||
import { clsx } from 'clsx'
|
|
||||||
import { t } from '../../../utils/intl'
|
|
||||||
import { hideModal } from '../../../stores/ui'
|
|
||||||
import { onMount } from 'solid-js'
|
|
||||||
import { useRouter } from '../../../stores/router'
|
|
||||||
import { confirmEmail } from '../../../stores/auth'
|
|
||||||
|
|
||||||
type ConfirmEmailSearchParams = {
|
|
||||||
token: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ConfirmEmail = () => {
|
|
||||||
const confirmedEmail = 'test@test.com'
|
|
||||||
|
|
||||||
const { searchParams } = useRouter<ConfirmEmailSearchParams>()
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
const token = searchParams().token
|
|
||||||
try {
|
|
||||||
await confirmEmail(token)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div class={styles.title}>{t('Hooray! Welcome!')}</div>
|
|
||||||
<div class={styles.text}>
|
|
||||||
{t("You've confirmed email")} {confirmedEmail}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button class={clsx('button', authModalStyles.submitButton)} onClick={() => hideModal()}>
|
|
||||||
Перейти на главную
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
import styles from './ConfirmEmail.module.scss'
|
|
||||||
import authModalStyles from './AuthModal.module.scss'
|
|
||||||
import { clsx } from 'clsx'
|
|
||||||
import { t } from '../../../utils/intl'
|
|
||||||
import { hideModal } from '../../../stores/ui'
|
|
||||||
import { onMount } from 'solid-js'
|
|
||||||
import { useRouter } from '../../../stores/router'
|
|
||||||
|
|
||||||
type ConfirmOAuthSearchParams = {
|
|
||||||
token: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ConfirmOAuth = () => {
|
|
||||||
const { searchParams } = useRouter<ConfirmOAuthSearchParams>()
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
console.debug('[confirm-oauth] params', searchParams())
|
|
||||||
const token = searchParams().token
|
|
||||||
localStorage.setItem('token', token)
|
|
||||||
window.addEventListener('mousemove', () => window.close())
|
|
||||||
window.addEventListener('keydown', () => window.close())
|
|
||||||
window.addEventListener('click', () => window.close())
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div class={styles.title}>{t('Hooray! Welcome!')}</div>
|
|
||||||
<div class={styles.text}>
|
|
||||||
{t("You've confirmed your account")} { /* TODO: get '%username%' */ }
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button class={clsx('button', authModalStyles.submitButton)} onClick={() => hideModal()}>
|
|
||||||
{t('Back to mainpage')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
44
src/components/Nav/AuthModal/EmailConfirm.tsx
Normal file
44
src/components/Nav/AuthModal/EmailConfirm.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import styles from './AuthModal.module.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import { t } from '../../../utils/intl'
|
||||||
|
import { hideModal } from '../../../stores/ui'
|
||||||
|
import { createMemo, onMount, Show } from 'solid-js'
|
||||||
|
import { useRouter } from '../../../stores/router'
|
||||||
|
import { confirmEmail, useAuthStore } from '../../../stores/auth'
|
||||||
|
|
||||||
|
type ConfirmEmailSearchParams = {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailConfirm = () => {
|
||||||
|
const { session } = useAuthStore()
|
||||||
|
|
||||||
|
const confirmedEmail = createMemo(() => session()?.user?.email || '')
|
||||||
|
|
||||||
|
const { searchParams } = useRouter<ConfirmEmailSearchParams>()
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const token = searchParams().token
|
||||||
|
try {
|
||||||
|
await confirmEmail(token)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div class={styles.title}>{t('Hooray! Welcome!')}</div>
|
||||||
|
<Show when={Boolean(confirmedEmail())}>
|
||||||
|
<div class={styles.text}>
|
||||||
|
{t("You've confirmed email")} {confirmedEmail()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div>
|
||||||
|
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
|
||||||
|
{t('Go to main page')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
import { Show } from 'solid-js/web'
|
|
||||||
import { t } from '../../../utils/intl'
|
import { t } from '../../../utils/intl'
|
||||||
import styles from './AuthModal.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createSignal, JSX } from 'solid-js'
|
import { createSignal, JSX, Show } from 'solid-js'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
@ -64,7 +63,7 @@ export const ForgotPasswordForm = () => {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<h4>{t('Forgot password?')}</h4>
|
<h4>{t('Forgot password?')}</h4>
|
||||||
{t('Everything is ok, please give us your email address')}
|
<div class={styles.authSubtitle}>{t('Everything is ok, please give us your email address')}</div>
|
||||||
<Show when={submitError()}>
|
<Show when={submitError()}>
|
||||||
<div class={styles.authInfo}>
|
<div class={styles.authInfo}>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { Show } from 'solid-js/web'
|
|
||||||
import { t } from '../../../utils/intl'
|
import { t } from '../../../utils/intl'
|
||||||
import styles from './AuthModal.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { SocialProviders } from './SocialProviders'
|
import { SocialProviders } from './SocialProviders'
|
||||||
import { signIn } from '../../../stores/auth'
|
import { signIn, signSendLink } from '../../../stores/auth'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal, Show } from 'solid-js'
|
||||||
import { isValidEmail } from './validators'
|
import { isValidEmail } from './validators'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
|
@ -23,6 +22,9 @@ export const LoginForm = () => {
|
||||||
const [submitError, setSubmitError] = createSignal('')
|
const [submitError, setSubmitError] = createSignal('')
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
// TODO: better solution for interactive error messages
|
||||||
|
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
|
||||||
|
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
||||||
|
|
||||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
|
@ -49,6 +51,7 @@ export const LoginForm = () => {
|
||||||
const handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
setIsLinkSent(false)
|
||||||
setSubmitError('')
|
setSubmitError('')
|
||||||
|
|
||||||
const newValidationErrors: ValidationErrors = {}
|
const newValidationErrors: ValidationErrors = {}
|
||||||
|
@ -72,10 +75,12 @@ export const LoginForm = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await signIn({ email: email(), password: password() })
|
await signIn({ email: email(), password: password() })
|
||||||
|
hideModal()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ApiError) {
|
if (error instanceof ApiError) {
|
||||||
if (error.code === 'email_not_confirmed') {
|
if (error.code === 'email_not_confirmed') {
|
||||||
setSubmitError(t('Please, confirm email'))
|
setSubmitError(t('Please, confirm email'))
|
||||||
|
setIsEmailNotConfirmed(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,11 +101,17 @@ export const LoginForm = () => {
|
||||||
<h4>{t('Enter the Discours')}</h4>
|
<h4>{t('Enter the Discours')}</h4>
|
||||||
<Show when={submitError()}>
|
<Show when={submitError()}>
|
||||||
<div class={styles.authInfo}>
|
<div class={styles.authInfo}>
|
||||||
<ul>
|
<div class={styles.warn}>{submitError()}</div>
|
||||||
<li class={styles.warn}>{submitError()}</li>
|
<Show when={isEmailNotConfirmed()}>
|
||||||
</ul>
|
<a href="#" class={styles.sendLink} onClick={handleSendLinkAgainClick}>
|
||||||
|
{t('Send link again')}
|
||||||
|
</a>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={isLinkSent()}>
|
||||||
|
<div class={styles.authInfo}>{t('Link sent, check your email')}</div>
|
||||||
|
</Show>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { Show } from 'solid-js/web'
|
import { Show, createSignal } from 'solid-js'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
import { t } from '../../../utils/intl'
|
import { t } from '../../../utils/intl'
|
||||||
import styles from './AuthModal.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { SocialProviders } from './SocialProviders'
|
import { SocialProviders } from './SocialProviders'
|
||||||
import { checkEmail, register, useAuthStore } from '../../../stores/auth'
|
import { checkEmail, register, useAuthStore } from '../../../stores/auth'
|
||||||
import { createSignal } from 'solid-js'
|
|
||||||
import { isValidEmail } from './validators'
|
import { isValidEmail } from './validators'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
import { hideModal } from '../../../stores/ui'
|
||||||
|
|
||||||
type FormFields = {
|
type FormFields = {
|
||||||
name: string
|
name: string
|
||||||
|
@ -29,6 +29,7 @@ export const RegisterForm = () => {
|
||||||
const [name, setName] = createSignal('')
|
const [name, setName] = createSignal('')
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
|
const [isSuccess, setIsSuccess] = createSignal(false)
|
||||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
|
||||||
const handleEmailInput = (newEmail: string) => {
|
const handleEmailInput = (newEmail: string) => {
|
||||||
|
@ -91,6 +92,8 @@ export const RegisterForm = () => {
|
||||||
email: email(),
|
email: email(),
|
||||||
password: password()
|
password: password()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setIsSuccess(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ApiError && error.code === 'user_already_exists') {
|
if (error instanceof ApiError && error.code === 'user_already_exists') {
|
||||||
return
|
return
|
||||||
|
@ -103,87 +106,100 @@ export const RegisterForm = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<>
|
||||||
<h4>{t('Create account')}</h4>
|
<Show when={!isSuccess()}>
|
||||||
<Show when={submitError()}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div class={styles.authInfo}>
|
<h4>{t('Create account')}</h4>
|
||||||
<ul>
|
<Show when={submitError()}>
|
||||||
<li class={styles.warn}>{submitError()}</li>
|
<div class={styles.authInfo}>
|
||||||
</ul>
|
<ul>
|
||||||
|
<li class={styles.warn}>{submitError()}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="pretty-form__item">
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
placeholder={t('Full name')}
|
||||||
|
autocomplete=""
|
||||||
|
onInput={(event) => handleNameInput(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<label for="name">{t('Full name')}</label>
|
||||||
|
</div>
|
||||||
|
<Show when={validationErrors().name}>
|
||||||
|
<div class={styles.validationError}>{validationErrors().name}</div>
|
||||||
|
</Show>
|
||||||
|
<div class="pretty-form__item">
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
autocomplete="email"
|
||||||
|
type="text"
|
||||||
|
value={email()}
|
||||||
|
placeholder={t('Email')}
|
||||||
|
onInput={(event) => handleEmailInput(event.currentTarget.value)}
|
||||||
|
onBlur={handleEmailBlur}
|
||||||
|
/>
|
||||||
|
<label for="email">{t('Email')}</label>
|
||||||
|
</div>
|
||||||
|
<Show when={validationErrors().email}>
|
||||||
|
<div class={styles.validationError}>{validationErrors().email}</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={emailChecks()[email()]}>
|
||||||
|
<div class={styles.validationError}>
|
||||||
|
{t("This email is already taken. If it's you")},{' '}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
changeSearchParam('mode', 'login')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('enter')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="pretty-form__item">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
type="password"
|
||||||
|
placeholder={t('Password')}
|
||||||
|
onInput={(event) => handlePasswordInput(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<label for="password">{t('Password')}</label>
|
||||||
|
</div>
|
||||||
|
<Show when={validationErrors().password}>
|
||||||
|
<div class={styles.validationError}>{validationErrors().password}</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
||||||
|
{isSubmitting() ? '...' : t('Join')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SocialProviders />
|
||||||
|
|
||||||
|
<div class={styles.authControl}>
|
||||||
|
<span class={styles.authLink} onClick={() => changeSearchParam('mode', 'login')}>
|
||||||
|
{t('I have an account')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Show>
|
||||||
|
<Show when={isSuccess()}>
|
||||||
|
<div class={styles.title}>{t('Almost done! Check your email.')}</div>
|
||||||
|
<div class={styles.text}>{t("We've sent you a message with a link to enter our website.")}</div>
|
||||||
|
<div>
|
||||||
|
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
|
||||||
|
{t('Back to main page')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="pretty-form__item">
|
</>
|
||||||
<input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
placeholder={t('Full name')}
|
|
||||||
autocomplete=""
|
|
||||||
onInput={(event) => handleNameInput(event.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<label for="name">{t('Full name')}</label>
|
|
||||||
</div>
|
|
||||||
<Show when={validationErrors().name}>
|
|
||||||
<div class={styles.validationError}>{validationErrors().name}</div>
|
|
||||||
</Show>
|
|
||||||
<div class="pretty-form__item">
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
autocomplete="email"
|
|
||||||
type="text"
|
|
||||||
value={email()}
|
|
||||||
placeholder={t('Email')}
|
|
||||||
onInput={(event) => handleEmailInput(event.currentTarget.value)}
|
|
||||||
onBlur={handleEmailBlur}
|
|
||||||
/>
|
|
||||||
<label for="email">{t('Email')}</label>
|
|
||||||
</div>
|
|
||||||
<Show when={validationErrors().email}>
|
|
||||||
<div class={styles.validationError}>{validationErrors().email}</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={emailChecks()[email()]}>
|
|
||||||
<div class={styles.validationError}>
|
|
||||||
{t("This email is already taken. If it's you")},{' '}
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
changeSearchParam('mode', 'login')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('enter')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="pretty-form__item">
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
type="password"
|
|
||||||
placeholder={t('Password')}
|
|
||||||
onInput={(event) => handlePasswordInput(event.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<label for="password">{t('Password')}</label>
|
|
||||||
</div>
|
|
||||||
<Show when={validationErrors().password}>
|
|
||||||
<div class={styles.validationError}>{validationErrors().password}</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
|
||||||
{isSubmitting() ? '...' : t('Join')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SocialProviders />
|
|
||||||
|
|
||||||
<div class={styles.authControl}>
|
|
||||||
<span class={styles.authLink} onClick={() => changeSearchParam('mode', 'login')}>
|
|
||||||
{t('I have an account')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Show } from 'solid-js/web'
|
import { Dynamic } from 'solid-js/web'
|
||||||
import { createEffect, createMemo } from 'solid-js'
|
import { Component, createEffect, createMemo } from 'solid-js'
|
||||||
import { t } from '../../../utils/intl'
|
import { t } from '../../../utils/intl'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { handleClientRouteLinkClick, useRouter } from '../../../stores/router'
|
import { handleClientRouteLinkClick, useRouter } from '../../../stores/router'
|
||||||
|
@ -8,15 +8,24 @@ import styles from './AuthModal.module.scss'
|
||||||
import { LoginForm } from './LoginForm'
|
import { LoginForm } from './LoginForm'
|
||||||
import { RegisterForm } from './RegisterForm'
|
import { RegisterForm } from './RegisterForm'
|
||||||
import { ForgotPasswordForm } from './ForgotPasswordForm'
|
import { ForgotPasswordForm } from './ForgotPasswordForm'
|
||||||
import { ConfirmEmail } from './ConfirmEmail'
|
import { EmailConfirm } from './EmailConfirm'
|
||||||
import type { AuthModalMode, AuthModalSearchParams } from './types'
|
import type { AuthModalMode, AuthModalSearchParams } from './types'
|
||||||
import { ConfirmOAuth } from './ConfirmOAuth'
|
|
||||||
|
|
||||||
|
const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = {
|
||||||
|
login: LoginForm,
|
||||||
|
register: RegisterForm,
|
||||||
|
'forgot-password': ForgotPasswordForm,
|
||||||
|
'confirm-email': EmailConfirm
|
||||||
|
}
|
||||||
|
|
||||||
export const AuthModal = () => {
|
export const AuthModal = () => {
|
||||||
let rootRef: HTMLDivElement
|
let rootRef: HTMLDivElement
|
||||||
|
|
||||||
const { searchParams } = useRouter<AuthModalSearchParams>()
|
const { searchParams } = useRouter<AuthModalSearchParams>()
|
||||||
const mode = createMemo<AuthModalMode>(() => searchParams().mode || 'login')
|
|
||||||
|
const mode = createMemo<AuthModalMode>(() => {
|
||||||
|
return AUTH_MODAL_MODES[searchParams().mode] ? searchParams().mode : 'login'
|
||||||
|
})
|
||||||
|
|
||||||
createEffect((oldMode) => {
|
createEffect((oldMode) => {
|
||||||
if (oldMode !== mode()) {
|
if (oldMode !== mode()) {
|
||||||
|
@ -28,12 +37,12 @@ export const AuthModal = () => {
|
||||||
<div
|
<div
|
||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
class={clsx('row', styles.view)}
|
class={clsx('row', styles.view)}
|
||||||
classList={{ [styles.signUp]: mode() === 'register' || mode().startsWith('confirm-') }}
|
classList={{ [styles.signUp]: mode() === 'register' || mode() === 'confirm-email' }}
|
||||||
>
|
>
|
||||||
<div class={clsx('col-sm-6', 'd-md-none', styles.authImage)}>
|
<div class={clsx('col-sm-6', 'd-md-none', styles.authImage)}>
|
||||||
<div
|
<div
|
||||||
class={styles.authImageText}
|
class={styles.authImageText}
|
||||||
classList={{ [styles.hidden]: mode() !== 'register' && !mode().startsWith('confirm-') }}
|
classList={{ [styles.hidden]: mode() !== 'register' && mode() !== 'confirm-email' }}
|
||||||
>
|
>
|
||||||
<h2>{t('Discours')}</h2>
|
<h2>{t('Discours')}</h2>
|
||||||
<h4>{t(`Join the global community of authors!`)}</h4>
|
<h4>{t(`Join the global community of authors!`)}</h4>
|
||||||
|
@ -60,21 +69,7 @@ export const AuthModal = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={clsx('col-sm-6', styles.auth)}>
|
<div class={clsx('col-sm-6', styles.auth)}>
|
||||||
<Show when={mode() === 'login'}>
|
<Dynamic component={AUTH_MODAL_MODES[mode()]} />
|
||||||
<LoginForm />
|
|
||||||
</Show>
|
|
||||||
<Show when={mode() === 'register'}>
|
|
||||||
<RegisterForm />
|
|
||||||
</Show>
|
|
||||||
<Show when={mode() === 'forgot-password'}>
|
|
||||||
<ForgotPasswordForm />
|
|
||||||
</Show>
|
|
||||||
<Show when={mode() === 'confirm-email'}>
|
|
||||||
<ConfirmEmail />
|
|
||||||
</Show>
|
|
||||||
<Show when={mode() === 'confirm-oauth'}>
|
|
||||||
<ConfirmOAuth />
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'confirm-oauth' | 'forgot-password'
|
export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-password'
|
||||||
|
|
||||||
export type AuthModalSearchParams = {
|
export type AuthModalSearchParams = {
|
||||||
mode: AuthModalMode
|
mode: AuthModalMode
|
||||||
|
|
6
src/components/Nav/Confirmed.scss
Normal file
6
src/components/Nav/Confirmed.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 420px;
|
||||||
|
}
|
18
src/components/Nav/Confirmed.tsx
Normal file
18
src/components/Nav/Confirmed.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import './Confirmed.scss'
|
||||||
|
import { onMount } from 'solid-js'
|
||||||
|
import { t } from '../../utils/intl'
|
||||||
|
|
||||||
|
export const Confirmed = (props: { token?: string }) => {
|
||||||
|
onMount(() => {
|
||||||
|
const token = props.token ?? document.cookie.split(';').at(0).replace('token=', '')
|
||||||
|
window.addEventListener('mousemove', () => window.close())
|
||||||
|
window.addEventListener('keydown', () => window.close())
|
||||||
|
window.addEventListener('click', () => window.close())
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="center">{t('You was successfully authorized')}</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -35,18 +35,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupShare {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.headerScrolledTop & {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s, z-index 0s 0.3s;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerFixed {
|
.headerFixed {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -327,18 +315,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.userControl {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.headerWithTitle.headerScrolledBottom & {
|
|
||||||
transition: opacity 0.3s, z-index 0s 0.3s;
|
|
||||||
opacity: 0;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.articleControls {
|
.articleControls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
@ -348,18 +324,14 @@
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.icon {
|
.control {
|
||||||
margin-left: 1.6rem;
|
cursor: pointer;
|
||||||
opacity: 0.6;
|
border: 0;
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
.icon {
|
||||||
vertical-align: middle;
|
opacity: 0.6;
|
||||||
}
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
a {
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: none;
|
background: none;
|
||||||
|
@ -370,4 +342,141 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control + .control {
|
||||||
|
margin-left: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userControl {
|
||||||
|
align-items: baseline;
|
||||||
|
display: flex;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.headerWithTitle.headerScrolledBottom & {
|
||||||
|
transition: opacity 0.3s, z-index 0s 0.3s;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include font-size(1.7rem);
|
||||||
|
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
padding: divide($container-padding-x, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userpic {
|
||||||
|
margin-right: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userControlItem {
|
||||||
|
align-items: center;
|
||||||
|
border: 2px solid #f6f6f6;
|
||||||
|
border-radius: 100%;
|
||||||
|
display: flex;
|
||||||
|
height: 2.4em;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: divide($container-padding-x, 2);
|
||||||
|
position: relative;
|
||||||
|
width: 2.4em;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
margin-left: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circlewrap {
|
||||||
|
height: 23px;
|
||||||
|
min-width: 23px;
|
||||||
|
width: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
a {
|
||||||
|
border: none;
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
filter: invert(0);
|
||||||
|
transition: filter 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 100%;
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userControlItemInbox,
|
||||||
|
.userControlItemSearch {
|
||||||
|
@include media-breakpoint-down(sm) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userControlItemWritePost {
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
.icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLabel {
|
||||||
|
display: inline;
|
||||||
|
padding: 0 1.2rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&,
|
||||||
|
a::before {
|
||||||
|
border-radius: 1.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
import { For, Show, createSignal, createMemo, createEffect, onMount, onCleanup } from 'solid-js'
|
import { For, Show, createSignal, createMemo, createEffect, onMount, onCleanup } from 'solid-js'
|
||||||
import Private from './Private'
|
|
||||||
import Notifications from './Notifications'
|
import Notifications from './Notifications'
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
import { Modal } from './Modal'
|
import { Modal } from './Modal'
|
||||||
import { Popup } from './Popup'
|
|
||||||
import { AuthModal } from './AuthModal'
|
import { AuthModal } from './AuthModal'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
|
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
||||||
import styles from './Header.module.scss'
|
import styles from './Header.module.scss'
|
||||||
import stylesPopup from './Popup.module.scss'
|
|
||||||
import privateStyles from './Private.module.scss'
|
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
|
import { getLogger } from '../../utils/logger'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { SharePopup } from '../Article/SharePopup'
|
||||||
|
import { ProfilePopup } from './ProfilePopup'
|
||||||
|
import Userpic from '../Author/Userpic'
|
||||||
|
import type { Author } from '../../graphql/types.gen'
|
||||||
|
|
||||||
|
const log = getLogger('header')
|
||||||
|
|
||||||
const resources: { name: string; route: keyof Routes }[] = [
|
const resources: { name: string; route: keyof Routes }[] = [
|
||||||
{ name: t('zine'), route: 'home' },
|
{ name: t('zine'), route: 'home' },
|
||||||
|
@ -32,6 +35,9 @@ export const Header = (props: Props) => {
|
||||||
const [getIsScrolled, setIsScrolled] = createSignal(false)
|
const [getIsScrolled, setIsScrolled] = createSignal(false)
|
||||||
const [fixed, setFixed] = createSignal(false)
|
const [fixed, setFixed] = createSignal(false)
|
||||||
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
||||||
|
const [isSharePopupVisible, setIsSharePopupVisible] = createSignal(false)
|
||||||
|
const [isProfilePopupVisible, setIsProfilePopupVisible] = createSignal(false)
|
||||||
|
|
||||||
// stores
|
// stores
|
||||||
const { warnings } = useWarningsStore()
|
const { warnings } = useWarningsStore()
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuthStore()
|
||||||
|
@ -41,13 +47,11 @@ export const Header = (props: Props) => {
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
||||||
const toggleFixed = () => setFixed(!fixed())
|
const toggleFixed = () => setFixed((oldFixed) => !oldFixed)
|
||||||
// effects
|
// effects
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const isFixed = fixed() || (modal() && modal() !== 'share');
|
document.body.classList.toggle('fixed', fixed() || modal() !== null)
|
||||||
|
document.body.classList.toggle(styles.fixed, fixed() && !modal())
|
||||||
document.body.classList.toggle('fixed', isFixed);
|
|
||||||
document.body.classList.toggle(styles.fixed, isFixed && !modal());
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// derived
|
// derived
|
||||||
|
@ -85,7 +89,8 @@ export const Header = (props: Props) => {
|
||||||
classList={{
|
classList={{
|
||||||
[styles.headerFixed]: props.isHeaderFixed,
|
[styles.headerFixed]: props.isHeaderFixed,
|
||||||
[styles.headerScrolledTop]: !getIsScrollingBottom() && getIsScrolled(),
|
[styles.headerScrolledTop]: !getIsScrollingBottom() && getIsScrolled(),
|
||||||
[styles.headerScrolledBottom]: getIsScrollingBottom() && getIsScrolled(),
|
[styles.headerScrolledBottom]:
|
||||||
|
(getIsScrollingBottom() && getIsScrolled() && !isProfilePopupVisible()) || isSharePopupVisible(),
|
||||||
[styles.headerWithTitle]: Boolean(props.title)
|
[styles.headerWithTitle]: Boolean(props.title)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -94,41 +99,6 @@ export const Header = (props: Props) => {
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div class={clsx(styles.mainHeaderInner, 'wide-container')}>
|
<div class={clsx(styles.mainHeaderInner, 'wide-container')}>
|
||||||
<Popup name="share" class={clsx(styles.popupShare, stylesPopup.popupShare)}>
|
|
||||||
<ul class="nodash">
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<Icon name="vk-white" class={stylesPopup.icon} />
|
|
||||||
VK
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<Icon name="facebook-white" class={stylesPopup.icon} />
|
|
||||||
Facebook
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<Icon name="twitter-white" class={stylesPopup.icon} />
|
|
||||||
Twitter
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<Icon name="telegram-white" class={stylesPopup.icon} />
|
|
||||||
Telegram
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<Icon name="link-white" class={stylesPopup.icon} />
|
|
||||||
{t('Copy link')}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Popup>
|
|
||||||
|
|
||||||
<nav class={clsx(styles.headerInner, 'row')} classList={{ fixed: fixed() }}>
|
<nav class={clsx(styles.headerInner, 'row')} classList={{ fixed: fixed() }}>
|
||||||
<div class={clsx(styles.mainLogo, 'col-auto')}>
|
<div class={clsx(styles.mainLogo, 'col-auto')}>
|
||||||
<a href={getPagePath(router, 'home')} onClick={handleClientRouteLinkClick}>
|
<a href={getPagePath(router, 'home')} onClick={handleClientRouteLinkClick}>
|
||||||
|
@ -162,8 +132,15 @@ export const Header = (props: Props) => {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.usernav}>
|
<div class={styles.usernav}>
|
||||||
<div class={clsx(privateStyles.userControl, styles.userControl, 'col')}>
|
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
||||||
<div class={privateStyles.userControlItem}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemWritePost)}>
|
||||||
|
<a href="/create">
|
||||||
|
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||||
|
<Icon name="pencil" class={styles.icon} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class={styles.userControlItem}>
|
||||||
<a href="#" onClick={handleBellIconClick}>
|
<a href="#" onClick={handleBellIconClick}>
|
||||||
<div>
|
<div>
|
||||||
<Icon name="bell-white" counter={authorized() ? warnings().length : 1} />
|
<Icon name="bell-white" counter={authorized() ? warnings().length : 1} />
|
||||||
|
@ -172,7 +149,7 @@ export const Header = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={visibleWarnings()}>
|
<Show when={visibleWarnings()}>
|
||||||
<div class={clsx(privateStyles.userControlItem, 'notifications')}>
|
<div class={clsx(styles.userControlItem, 'notifications')}>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -180,31 +157,56 @@ export const Header = (props: Props) => {
|
||||||
<Show
|
<Show
|
||||||
when={authorized()}
|
when={authorized()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={clsx(privateStyles.userControlItem, 'loginbtn')}>
|
<div class={clsx(styles.userControlItem, 'loginbtn')}>
|
||||||
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
||||||
<Icon name="user-anonymous" />
|
<Icon name="user-anonymous" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Private />
|
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
||||||
|
<a href="/inbox">
|
||||||
|
{/*FIXME: replace with route*/}
|
||||||
|
<div classList={{ entered: page().path === '/inbox' }}>
|
||||||
|
<Icon name="inbox-white" counter={session()?.news?.unread || 0} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ProfilePopup
|
||||||
|
onVisibilityChange={(isVisible) => {
|
||||||
|
setIsProfilePopupVisible(isVisible)
|
||||||
|
}}
|
||||||
|
containerCssClass={styles.control}
|
||||||
|
trigger={
|
||||||
|
<div class={styles.userControlItem}>
|
||||||
|
<button class={styles.button}>
|
||||||
|
<div classList={{ entered: page().path === `/${session().user?.slug}` }}>
|
||||||
|
<Userpic user={session().user as Author} class={styles.userpic} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.title}>
|
<Show when={props.title}>
|
||||||
<div class={styles.articleControls}>
|
<div class={styles.articleControls}>
|
||||||
<button
|
<SharePopup
|
||||||
onClick={() => {
|
onVisibilityChange={(isVisible) => {
|
||||||
// FIXME: Popup
|
setIsSharePopupVisible(isVisible)
|
||||||
showModal('share')
|
|
||||||
}}
|
}}
|
||||||
>
|
containerCssClass={styles.control}
|
||||||
<Icon name="share-outline" class={styles.icon} />
|
trigger={<Icon name="share-outline" class={styles.icon} />}
|
||||||
</button>
|
/>
|
||||||
<a href="#comments">
|
<a href="#comments" class={styles.control}>
|
||||||
<Icon name="comments-outline" class={styles.icon} />
|
<Icon name="comments-outline" class={styles.icon} />
|
||||||
</a>
|
</a>
|
||||||
<Icon name="pencil-outline" class={styles.icon} />
|
<a href="#" class={styles.control} onClick={(event) => event.preventDefault()}>
|
||||||
<Icon name="bookmark" class={styles.icon} />
|
<Icon name="pencil-outline" class={styles.icon} />
|
||||||
|
</a>
|
||||||
|
<a href="#" class={styles.control} onClick={(event) => event.preventDefault()}>
|
||||||
|
<Icon name="bookmark" class={styles.icon} />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
.icon {
|
.icon {
|
||||||
|
line-height: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
|
import { getLogger } from '../../utils/logger'
|
||||||
import './Modal.scss'
|
import './Modal.scss'
|
||||||
import { hideModal, useModalStore } from '../../stores/ui'
|
import { hideModal, useModalStore } from '../../stores/ui'
|
||||||
|
|
||||||
|
const log = getLogger('modal')
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
name: string
|
name: string
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
|
@ -31,7 +34,7 @@ export const Modal = (props: ModalProps) => {
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setVisible(modal() === props.name)
|
setVisible(modal() === props.name)
|
||||||
console.debug(`[auth.modal] ${props.name} is ${modal() === props.name ? 'visible' : 'hidden'}`)
|
log.debug(`${props.name} is ${modal() === props.name ? 'visible' : 'hidden'}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { For, Portal, Show } from 'solid-js/web'
|
import { Portal } from 'solid-js/web'
|
||||||
import { useWarningsStore } from '../../stores/ui'
|
import { useWarningsStore } from '../../stores/ui'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, For, Show } from 'solid-js'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { warnings } = useWarningsStore()
|
const { warnings } = useWarningsStore()
|
||||||
|
|
|
@ -1,10 +1,25 @@
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.popup {
|
.popup {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 2px solid #000;
|
border: 2px solid #000;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
&.horizontalAnchorCenter {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.horizontalAnchorRight {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@include font-size(1.6rem);
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
padding: 2.4rem 2.4rem 2.4rem 1.6rem;
|
padding: 2.4rem;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
|
@ -14,7 +29,6 @@
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin-bottom: 1.6rem;
|
margin-bottom: 1.6rem;
|
||||||
padding-left: 3.6rem;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
|
@ -24,23 +38,36 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
border: none;
|
border: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
img {
|
||||||
|
filter: invert(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
max-height: 2rem;
|
max-height: 2rem;
|
||||||
max-width: 2rem;
|
max-width: 2rem;
|
||||||
|
transition: filter 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
left: 1.5rem;
|
display: inline-block;
|
||||||
position: absolute;
|
width: 3.6rem;
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupShare {
|
// TODO: animation
|
||||||
right: 1em;
|
// .popup {
|
||||||
top: 4.5rem;
|
// opacity: 1;
|
||||||
}
|
// transition: opacity 0.3s;
|
||||||
|
// z-index: 1;
|
||||||
|
// &.visible {
|
||||||
|
// opacity: 0;
|
||||||
|
// transition: opacity 0.3s, z-index 0s 0.3s;
|
||||||
|
// z-index: -1;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
@ -1,31 +1,61 @@
|
||||||
import { createEffect, createSignal, JSX, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, JSX, onCleanup, onMount, Show } from 'solid-js'
|
||||||
import style from './Popup.module.scss'
|
import styles from './Popup.module.scss'
|
||||||
import { hideModal, useModalStore } from '../../stores/ui'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
interface PopupProps {
|
type HorizontalAnchor = 'center' | 'right'
|
||||||
name: string
|
|
||||||
|
export type PopupProps = {
|
||||||
|
containerCssClass?: string
|
||||||
|
trigger: JSX.Element
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
class?: string
|
onVisibilityChange?: (isVisible) => void
|
||||||
|
horizontalAnchor?: HorizontalAnchor
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Popup = (props: PopupProps) => {
|
export const Popup = (props: PopupProps) => {
|
||||||
const { modal } = useModalStore()
|
const [isVisible, setIsVisible] = createSignal(false)
|
||||||
|
const horizontalAnchor: HorizontalAnchor = props.horizontalAnchor || 'center'
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.onVisibilityChange) {
|
||||||
|
props.onVisibilityChange(isVisible())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let container: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent & { target: Element }) => {
|
||||||
|
if (!isVisible()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target === container || container?.contains(event.target)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('keydown', (e: KeyboardEvent) => {
|
document.addEventListener('click', handleClickOutside, { capture: true })
|
||||||
if (e.key === 'Escape') hideModal()
|
onCleanup(() => document.removeEventListener('click', handleClickOutside, { capture: true }))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const [visible, setVisible] = createSignal(false)
|
const toggle = () => setIsVisible((oldVisible) => !oldVisible)
|
||||||
createEffect(() => {
|
|
||||||
setVisible(modal() === props.name)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={visible()}>
|
<span class={clsx(styles.container, props.containerCssClass)} ref={container}>
|
||||||
<div class={clsx(style.popup, props.class)}>{props.children}</div>
|
<span onClick={toggle}>{props.trigger}</span>
|
||||||
</Show>
|
<Show when={isVisible()}>
|
||||||
|
<div
|
||||||
|
class={clsx(styles.popup, {
|
||||||
|
[styles.horizontalAnchorCenter]: horizontalAnchor === 'center',
|
||||||
|
[styles.horizontalAnchorRight]: horizontalAnchor === 'right'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
.userControl {
|
|
||||||
align-items: baseline;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
@include font-size(1.7rem);
|
|
||||||
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
@include media-breakpoint-down(md) {
|
|
||||||
padding: divide($container-padding-x, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.circlewrap {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.userControlItem {
|
|
||||||
align-items: center;
|
|
||||||
border: 2px solid #f6f6f6;
|
|
||||||
border-radius: 100%;
|
|
||||||
display: flex;
|
|
||||||
height: 2.4em;
|
|
||||||
justify-content: center;
|
|
||||||
margin-left: divide($container-padding-x, 2);
|
|
||||||
position: relative;
|
|
||||||
width: 2.4em;
|
|
||||||
|
|
||||||
@include media-breakpoint-up(sm) {
|
|
||||||
margin-left: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circlewrap {
|
|
||||||
height: 23px;
|
|
||||||
min-width: 23px;
|
|
||||||
width: 23px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: none;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
filter: invert(0);
|
|
||||||
transition: filter 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 100%;
|
|
||||||
content: '';
|
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 20px;
|
|
||||||
vertical-align: middle;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.userControlItemWritePost {
|
|
||||||
@include media-breakpoint-up(lg) {
|
|
||||||
.icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textLabel {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.userControlItemInbox,
|
|
||||||
.userControlItemSearch {
|
|
||||||
@include media-breakpoint-down(sm) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
import type { Author } from '../../graphql/types.gen'
|
|
||||||
import Userpic from '../Author/Userpic'
|
|
||||||
import { Icon } from './Icon'
|
|
||||||
import styles from './Private.module.scss'
|
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import { useRouter } from '../../stores/router'
|
|
||||||
import { clsx } from 'clsx'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const { session } = useAuthStore()
|
|
||||||
const { page } = useRouter()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={clsx(styles.userControl, 'col')}>
|
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemWritePost)}>
|
|
||||||
<a href="/create">
|
|
||||||
<span class={styles.textLabel}>опубликовать материал</span>
|
|
||||||
<Icon name="pencil" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
|
||||||
<a href="/inbox">
|
|
||||||
{/*FIXME: replace with route*/}
|
|
||||||
<div classList={{ entered: page().path === '/inbox' }}>
|
|
||||||
<Icon name="inbox-white" counter={session()?.news?.unread || 0} />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class={styles.userControlItem}>
|
|
||||||
<a href={`/${session().user?.slug}`}>
|
|
||||||
{/*FIXME: replace with route*/}
|
|
||||||
<div classList={{ entered: page().path === `/${session().user?.slug}` }}>
|
|
||||||
<Userpic user={session().user as Author} />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { For } from 'solid-js/web'
|
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { hideModal } from '../../stores/ui'
|
import { hideModal } from '../../stores/ui'
|
||||||
import { useAuthStore, signOut } from '../../stores/auth'
|
import { useAuthStore, signOut } from '../../stores/auth'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, For } from 'solid-js'
|
||||||
|
|
||||||
const quit = () => {
|
const quit = () => {
|
||||||
signOut()
|
signOut()
|
||||||
|
|
44
src/components/Nav/ProfilePopup.tsx
Normal file
44
src/components/Nav/ProfilePopup.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Popup, PopupProps } from './Popup'
|
||||||
|
import { signOut, useAuthStore } from '../../stores/auth'
|
||||||
|
|
||||||
|
type ProfilePopupProps = Omit<PopupProps, 'children'>
|
||||||
|
|
||||||
|
export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
|
const { session } = useAuthStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popup {...props} horizontalAnchor="right">
|
||||||
|
<ul class="nodash">
|
||||||
|
<li>
|
||||||
|
<a href={`/${session().user?.slug}`}>Профиль</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">Черновики</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">Подписки</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">Комментарии</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">Закладки</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">Настройки</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
signOut()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выйти из аккаунта
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popup>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { MainLayout } from '../Layouts/MainLayout'
|
import { MainLayout } from '../Layouts/MainLayout'
|
||||||
import { AuthorView } from '../Views/Author'
|
import { AuthorView, PRERENDERED_ARTICLES_COUNT } from '../Views/Author'
|
||||||
import type { PageProps } from '../types'
|
import type { PageProps } from '../types'
|
||||||
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
import { loadArticlesForAuthors, resetSortedArticles } from '../../stores/zine/articles'
|
import { loadAuthorArticles, resetSortedArticles } from '../../stores/zine/articles'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { loadAuthor } from '../../stores/zine/authors'
|
import { loadAuthor } from '../../stores/zine/authors'
|
||||||
import { Loading } from '../Loading'
|
import { Loading } from '../Loading'
|
||||||
|
@ -27,7 +27,7 @@ export const AuthorPage = (props: PageProps) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadArticlesForAuthors({ authorSlugs: [slug()] })
|
await loadAuthorArticles({ authorSlug: slug(), limit: PRERENDERED_ARTICLES_COUNT })
|
||||||
await loadAuthor({ slug: slug() })
|
await loadAuthor({ slug: slug() })
|
||||||
|
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
|
|
|
@ -1,30 +1,14 @@
|
||||||
import { MainLayout } from '../Layouts/MainLayout'
|
import { MainLayout } from '../Layouts/MainLayout'
|
||||||
import { FeedView } from '../Views/Feed'
|
import { FeedView } from '../Views/Feed'
|
||||||
import type { PageProps } from '../types'
|
import { onCleanup } from 'solid-js'
|
||||||
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { resetSortedArticles } from '../../stores/zine/articles'
|
||||||
import { loadRecentArticles, resetSortedArticles } from '../../stores/zine/articles'
|
|
||||||
import { Loading } from '../Loading'
|
|
||||||
|
|
||||||
export const FeedPage = (props: PageProps) => {
|
|
||||||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.feedArticles))
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (isLoaded()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadRecentArticles({ limit: 50, offset: 0 })
|
|
||||||
|
|
||||||
setIsLoaded(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
export const FeedPage = () => {
|
||||||
onCleanup(() => resetSortedArticles())
|
onCleanup(() => resetSortedArticles())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<Show when={isLoaded()} fallback={<Loading />}>
|
<FeedView />
|
||||||
<FeedView articles={props.feedArticles} />
|
|
||||||
</Show>
|
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { HomeView } from '../Views/Home'
|
import { HomeView, PRERENDERED_ARTICLES_COUNT } from '../Views/Home'
|
||||||
import { MainLayout } from '../Layouts/MainLayout'
|
import { MainLayout } from '../Layouts/MainLayout'
|
||||||
import type { PageProps } from '../types'
|
import type { PageProps } from '../types'
|
||||||
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
|
@ -14,7 +14,7 @@ export const HomePage = (props: PageProps) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadPublishedArticles({ limit: 5, offset: 0 })
|
await loadPublishedArticles({ limit: PRERENDERED_ARTICLES_COUNT, offset: 0 })
|
||||||
await loadRandomTopics()
|
await loadRandomTopics()
|
||||||
|
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { MainLayout } from '../Layouts/MainLayout'
|
import { MainLayout } from '../Layouts/MainLayout'
|
||||||
import { TopicView } from '../Views/Topic'
|
import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../Views/Topic'
|
||||||
import type { PageProps } from '../types'
|
import type { PageProps } from '../types'
|
||||||
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
import { loadArticlesForTopics, resetSortedArticles } from '../../stores/zine/articles'
|
import { loadTopicArticles, resetSortedArticles } from '../../stores/zine/articles'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { loadTopic } from '../../stores/zine/topics'
|
import { loadTopic } from '../../stores/zine/topics'
|
||||||
import { Loading } from '../Loading'
|
import { Loading } from '../Loading'
|
||||||
|
@ -27,7 +27,7 @@ export const TopicPage = (props: PageProps) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadArticlesForTopics({ topicSlugs: [slug()] })
|
await loadTopicArticles({ topicSlug: slug(), limit: PRERENDERED_ARTICLES_COUNT, offset: 0 })
|
||||||
await loadTopic({ slug: slug() })
|
await loadTopic({ slug: slug() })
|
||||||
|
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
|
|
|
@ -54,28 +54,29 @@ export const ManifestPage = () => {
|
||||||
|
|
||||||
<div class="col-lg-10 offset-md-1">
|
<div class="col-lg-10 offset-md-1">
|
||||||
<p>
|
<p>
|
||||||
Дискурс — независимый художественно-аналитический журнал с горизонтальной редакцией,
|
Дискурс — независимый художественно-аналитический журнал с горизонтальной
|
||||||
основанный на принципах свободы слова, прямой демократии и совместного редактирования.
|
редакцией, основанный на принципах свободы слова, прямой демократии и совместного
|
||||||
Дискурс создаётся открытым медиасообществом ученых, журналистов, музыкантов, писателей,
|
редактирования. Дискурс создаётся открытым медиасообществом ученых, журналистов, музыкантов,
|
||||||
предпринимателей, философов, инженеров, художников и специалистов со всего мира,
|
писателей, предпринимателей, философов, инженеров, художников и специалистов
|
||||||
объединившихся, чтобы вместе делать общий журнал и объяснять с разных точек
|
со всего мира, объединившихся, чтобы вместе делать общий журнал и объяснять
|
||||||
зрения мозаичную картину современности.
|
с разных точек зрения мозаичную картину современности.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Мы пишем о культуре, науке и обществе, рассказываем о новых идеях и современном искусстве,
|
Мы пишем о культуре, науке и обществе, рассказываем о новых идеях
|
||||||
публикуем статьи, исследования, репортажи, интервью людей, чью прямую речь стоит услышать,
|
и современном искусстве, публикуем статьи, исследования, репортажи, интервью людей, чью
|
||||||
и работы художников из разных стран — от фильмов и музыки
|
прямую речь стоит услышать, и работы художников из разных стран —
|
||||||
до живописи и фотографии. Помогая друг другу делать публикации качественнее
|
от фильмов и музыки до живописи и фотографии. Помогая друг другу делать
|
||||||
и общим голосованием выбирая лучшие материалы для журнала, мы создаём новую
|
публикации качественнее и общим голосованием выбирая лучшие материалы для журнала,
|
||||||
горизонтальную журналистику, чтобы честно рассказывать о важном и интересном.
|
мы создаём новую горизонтальную журналистику, чтобы честно рассказывать о важном
|
||||||
|
и интересном.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Редакция Дискурса открыта для всех: у нас нет цензуры, запретных тем и идеологических рамок.
|
Редакция Дискурса открыта для всех: у нас нет цензуры, запретных тем
|
||||||
Каждый может <a href="/create">прислать материал</a> в журнал
|
и идеологических рамок. Каждый может <a href="/create">прислать материал</a>{' '}
|
||||||
и <a href="/about/guide">присоединиться к редакции</a>. Предоставляя трибуну
|
в журнал и <a href="/about/guide">присоединиться к редакции</a>. Предоставляя
|
||||||
для независимой журналистики и художественных проектов, мы помогаем людям
|
трибуну для независимой журналистики и художественных проектов, мы помогаем людям
|
||||||
рассказывать свои истории так, чтобы они были услышаны. Мы убеждены: чем больше
|
рассказывать свои истории так, чтобы они были услышаны. Мы убеждены: чем больше голосов
|
||||||
голосов будет звучать на Дискурсе, тем громче в полифонии мнений будет слышна истина.
|
будет звучать на Дискурсе, тем громче в полифонии мнений будет слышна истина.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -91,23 +92,26 @@ export const ManifestPage = () => {
|
||||||
</p>
|
</p>
|
||||||
<h3 id="contribute">Предлагать материалы</h3>
|
<h3 id="contribute">Предлагать материалы</h3>
|
||||||
<p>
|
<p>
|
||||||
<a href="/create">Создавайте</a> свои статьи и художественные работы — лучшие из них будут
|
<a href="/create">Создавайте</a> свои статьи и художественные работы —
|
||||||
опубликованы в журнале. Дискурс — некоммерческое издание, авторы публикуются
|
лучшие из них будут опубликованы в журнале. Дискурс — некоммерческое
|
||||||
в журнале на общественных началах, получая при этом <a href="/create?collab=true">поддержку</a> редакции,
|
издание, авторы публикуются в журнале на общественных началах, получая при этом{' '}
|
||||||
право голоса, множество других возможностей и читателей по всему миру.
|
<a href="/create?collab=true">поддержку</a> редакции, право голоса, множество других
|
||||||
|
возможностей и читателей по всему миру.
|
||||||
</p>
|
</p>
|
||||||
<h3 id="donate">Поддерживать проект</h3>
|
<h3 id="donate">Поддерживать проект</h3>
|
||||||
<p>Дискурс существует на пожертвования читателей. Если вам нравится журнал, пожалуйста,</p>
|
|
||||||
<p>
|
<p>
|
||||||
<a href="/about/help">поддержите</a> нашу работу. Ваши пожертвования пойдут на выпуск новых
|
Дискурс существует на пожертвования читателей. Если вам нравится журнал, пожалуйста,
|
||||||
материалов, оплату серверов, труда программистов, дизайнеров и редакторов.
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="/about/help">поддержите</a> нашу работу. Ваши пожертвования пойдут на выпуск
|
||||||
|
новых материалов, оплату серверов, труда программистов, дизайнеров и редакторов.
|
||||||
</p>
|
</p>
|
||||||
<h3 id="cooperation">Сотрудничать с журналом</h3>
|
<h3 id="cooperation">Сотрудничать с журналом</h3>
|
||||||
<p>
|
<p>
|
||||||
Мы всегда открыты для сотрудничества и рады единомышленникам. Если вы хотите помогать
|
Мы всегда открыты для сотрудничества и рады единомышленникам. Если вы хотите помогать
|
||||||
журналу с редактурой, корректурой, иллюстрациями, переводами, версткой, подкастами,
|
журналу с редактурой, корректурой, иллюстрациями, переводами, версткой, подкастами,
|
||||||
мероприятиями, фандрайзингом или как-то ещё — скорее пишите нам
|
мероприятиями, фандрайзингом или как-то ещё — скорее пишите нам на
|
||||||
на <a href="mailto:welcome@discours.io">welcome@discours.io</a>.
|
<a href="mailto:welcome@discours.io">welcome@discours.io</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Если вы представляете некоммерческую организацию и хотите сделать с нами
|
Если вы представляете некоммерческую организацию и хотите сделать с нами
|
||||||
|
@ -116,25 +120,26 @@ export const ManifestPage = () => {
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Если вы разработчик и хотите помогать с развитием сайта Дискурса,{' '}
|
Если вы разработчик и хотите помогать с развитием сайта Дискурса,{' '}
|
||||||
<a href="mailto:services@discours.io">присоединяйтесь к IT-команде самиздата</a>. Открытый
|
<a href="mailto:services@discours.io">присоединяйтесь к IT-команде самиздата</a>.
|
||||||
код платформы для независимой журналистики, а также всех наших спецпроектов
|
Открытый код платформы для независимой журналистики, а также всех наших спецпроектов
|
||||||
и медиаинструментов находится <a href="https://github.com/Discours">в свободном доступе на GitHub</a>.
|
и медиаинструментов находится{' '}
|
||||||
|
<a href="https://github.com/Discours">в свободном доступе на GitHub</a>.
|
||||||
</p>
|
</p>
|
||||||
<h3 id="follow">Как еще можно помочь</h3>
|
<h3 id="follow">Как еще можно помочь</h3>
|
||||||
<p>
|
<p>
|
||||||
Советуйте Дискурс друзьям и знакомым. Обсуждайте и распространяйте наши
|
Советуйте Дискурс друзьям и знакомым. Обсуждайте и распространяйте наши
|
||||||
публикации — все материалы открытой редакции можно читать и перепечатывать
|
публикации — все материалы открытой редакции можно читать и перепечатывать
|
||||||
бесплатно. Подпишитесь на самиздат{' '}
|
бесплатно. Подпишитесь на самиздат <a href="https://vk.com/discoursio">ВКонтакте</a>,
|
||||||
<a href="https://vk.com/discoursio">ВКонтакте</a>,
|
|
||||||
в <a href="https://facebook.com/discoursio">Фейсбуке</a>
|
в <a href="https://facebook.com/discoursio">Фейсбуке</a>
|
||||||
и в <a href="https://t.me/discoursio">Телеграме</a>, а также
|
и в <a href="https://t.me/discoursio">Телеграме</a>, а также на
|
||||||
на <Opener name="subscribe">рассылку лучших материалов</Opener>,
|
<Opener name="subscribe">рассылку лучших материалов</Opener>, чтобы не пропустить
|
||||||
чтобы не пропустить ничего интересного.
|
ничего интересного.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://forms.gle/9UnHBAz9Q3tjH5dAA">Рассказывайте о впечатлениях</a>
|
<a href="https://forms.gle/9UnHBAz9Q3tjH5dAA">Рассказывайте о впечатлениях</a>
|
||||||
от материалов открытой редакции, <Opener name="feedback">делитесь идеями</Opener>,
|
от материалов открытой редакции, <Opener name="feedback">делитесь идеями</Opener>,
|
||||||
интересными темами, о которых хотели бы узнать больше, и историями, которые нужно рассказать.
|
интересными темами, о которых хотели бы узнать больше, и историями, которые нужно
|
||||||
|
рассказать.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -145,9 +150,9 @@ export const ManifestPage = () => {
|
||||||
<div class="col-lg-10 offset-md-1">
|
<div class="col-lg-10 offset-md-1">
|
||||||
Если вы хотите предложить материал, сотрудничать, рассказать о проблеме, которую нужно
|
Если вы хотите предложить материал, сотрудничать, рассказать о проблеме, которую нужно
|
||||||
осветить, сообщить об ошибке или баге, что-то обсудить, уточнить или посоветовать,
|
осветить, сообщить об ошибке или баге, что-то обсудить, уточнить или посоветовать,
|
||||||
пожалуйста, <Opener name="feedback">напишите нам здесь</Opener> или
|
пожалуйста, <Opener name="feedback">напишите нам здесь</Opener> или на почту{' '}
|
||||||
на почту <a href="mailto:welcome@discours.io">welcome@discours.io</a>. Мы обязательно
|
<a href="mailto:welcome@discours.io">welcome@discours.io</a>. Мы обязательно ответим
|
||||||
ответим и постараемся реализовать все хорошие задумки.
|
и постараемся реализовать все хорошие задумки.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
// FIXME: breaks on vercel, research
|
// FIXME: breaks on vercel, research
|
||||||
// import 'solid-devtools'
|
// import 'solid-devtools'
|
||||||
|
|
||||||
import { hideModal, MODALS, setLocale, showModal } from '../stores/ui'
|
import { MODALS, setLocale, showModal } from '../stores/ui'
|
||||||
import { Component, createEffect, createMemo } from 'solid-js'
|
import { Component, createEffect, createMemo, onMount } from 'solid-js'
|
||||||
import { Routes, useRouter } from '../stores/router'
|
import { Routes, useRouter } from '../stores/router'
|
||||||
import { Dynamic, isServer } from 'solid-js/web'
|
import { Dynamic, isServer } from 'solid-js/web'
|
||||||
|
import { getLogger } from '../utils/logger'
|
||||||
|
|
||||||
import type { PageProps } from './types'
|
import type { PageProps } from './types'
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ import { ProjectsPage } from './Pages/about/ProjectsPage'
|
||||||
import { TermsOfUsePage } from './Pages/about/TermsOfUsePage'
|
import { TermsOfUsePage } from './Pages/about/TermsOfUsePage'
|
||||||
import { ThanksPage } from './Pages/about/ThanksPage'
|
import { ThanksPage } from './Pages/about/ThanksPage'
|
||||||
import { CreatePage } from './Pages/CreatePage'
|
import { CreatePage } from './Pages/CreatePage'
|
||||||
|
import { renewSession } from '../stores/auth'
|
||||||
|
|
||||||
// TODO: lazy load
|
// TODO: lazy load
|
||||||
// const HomePage = lazy(() => import('./Pages/HomePage'))
|
// const HomePage = lazy(() => import('./Pages/HomePage'))
|
||||||
|
@ -47,6 +49,7 @@ import { CreatePage } from './Pages/CreatePage'
|
||||||
// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
|
// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
|
||||||
// const CreatePage = lazy(() => import('./Pages/about/CreatePage'))
|
// const CreatePage = lazy(() => import('./Pages/about/CreatePage'))
|
||||||
|
|
||||||
|
const log = getLogger('root')
|
||||||
|
|
||||||
type RootSearchParams = {
|
type RootSearchParams = {
|
||||||
modal: string
|
modal: string
|
||||||
|
@ -83,6 +86,10 @@ export const Root = (props: PageProps) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
renewSession()
|
||||||
|
})
|
||||||
|
|
||||||
const pageComponent = createMemo(() => {
|
const pageComponent = createMemo(() => {
|
||||||
const result = pagesMap[page().route]
|
const result = pagesMap[page().route]
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import { capitalize, plural } from '../../utils'
|
import { capitalize, plural } from '../../utils'
|
||||||
import { Show } from 'solid-js/web'
|
|
||||||
import style from './Card.module.scss'
|
import style from './Card.module.scss'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, Show } from 'solid-js'
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/types.gen'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
import { FollowingEntity } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
import { follow, unfollow } from '../../stores/zine/common'
|
||||||
|
import { getLogger } from '../../utils/logger'
|
||||||
|
|
||||||
|
const log = getLogger('TopicCard')
|
||||||
|
|
||||||
interface TopicProps {
|
interface TopicProps {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, Show } from 'solid-js'
|
||||||
import { Show } from 'solid-js/web'
|
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/types.gen'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
import { FollowingEntity } from '../../graphql/types.gen'
|
||||||
import './Full.scss'
|
import './Full.scss'
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import { Show, createMemo } from 'solid-js'
|
import { Show, createMemo, createSignal, For, onMount } from 'solid-js'
|
||||||
import type { Author, Shout } from '../../graphql/types.gen'
|
import type { Author, Shout } from '../../graphql/types.gen'
|
||||||
import Row2 from '../Feed/Row2'
|
import { Row2 } from '../Feed/Row2'
|
||||||
import Row3 from '../Feed/Row3'
|
import { Row3 } from '../Feed/Row3'
|
||||||
// import Beside from '../Feed/Beside'
|
import { AuthorFull } from '../Author/Full'
|
||||||
import AuthorFull from '../Author/Full'
|
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { useArticlesStore } from '../../stores/zine/articles'
|
import { loadAuthorArticles, useArticlesStore } from '../../stores/zine/articles'
|
||||||
|
|
||||||
import '../../styles/Topic.scss'
|
import '../../styles/Topic.scss'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
import { useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import Beside from '../Feed/Beside'
|
import { Beside } from '../Feed/Beside'
|
||||||
|
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||||
|
import { splitToPages } from '../../utils/splitToPages'
|
||||||
|
|
||||||
// TODO: load reactions on client
|
// TODO: load reactions on client
|
||||||
type AuthorProps = {
|
type AuthorProps = {
|
||||||
|
@ -26,16 +27,37 @@ type AuthorPageSearchParams = {
|
||||||
by: '' | 'viewed' | 'rating' | 'commented' | 'recent'
|
by: '' | 'viewed' | 'rating' | 'commented' | 'recent'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PRERENDERED_ARTICLES_COUNT = 12
|
||||||
|
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
|
||||||
|
|
||||||
export const AuthorView = (props: AuthorProps) => {
|
export const AuthorView = (props: AuthorProps) => {
|
||||||
const { sortedArticles } = useArticlesStore({
|
const { sortedArticles } = useArticlesStore({
|
||||||
sortedArticles: props.authorArticles
|
sortedArticles: props.authorArticles
|
||||||
})
|
})
|
||||||
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
||||||
const { topicsByAuthor } = useTopicsStore()
|
const { topicsByAuthor } = useTopicsStore()
|
||||||
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
const author = createMemo(() => authorEntities()[props.authorSlug])
|
const author = createMemo(() => authorEntities()[props.authorSlug])
|
||||||
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
||||||
|
|
||||||
|
const loadMore = async () => {
|
||||||
|
saveScrollPosition()
|
||||||
|
const { hasMore } = await loadAuthorArticles({
|
||||||
|
authorSlug: author().slug,
|
||||||
|
limit: LOAD_MORE_PAGE_SIZE,
|
||||||
|
offset: sortedArticles().length
|
||||||
|
})
|
||||||
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
|
restoreScrollPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
const m = searchParams().by
|
const m = searchParams().by
|
||||||
if (m === 'viewed') return t('Top viewed')
|
if (m === 'viewed') return t('Top viewed')
|
||||||
|
@ -44,6 +66,10 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
return t('Top recent')
|
return t('Top recent')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
|
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="container author-page">
|
<div class="container author-page">
|
||||||
<Show when={author()} fallback={<div class="center">{t('Loading')}</div>}>
|
<Show when={author()} fallback={<div class="center">{t('Loading')}</div>}>
|
||||||
|
@ -83,31 +109,39 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="col-12">{title()}</h3>
|
<h3 class="col-12">{title()}</h3>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<Show when={sortedArticles().length > 0}>
|
<Beside
|
||||||
<Beside
|
title={t('Topics which supported by author')}
|
||||||
title={t('Topics which supported by author')}
|
values={topicsByAuthor()[author().slug].slice(0, 5)}
|
||||||
values={topicsByAuthor()[author().slug].slice(0, 5)}
|
beside={sortedArticles()[0]}
|
||||||
beside={sortedArticles()[0]}
|
wrapper={'topic'}
|
||||||
wrapper={'topic'}
|
topicShortDescription={true}
|
||||||
topicShortDescription={true}
|
isTopicCompact={true}
|
||||||
isTopicCompact={true}
|
isTopicInRow={true}
|
||||||
isTopicInRow={true}
|
iconButton={true}
|
||||||
iconButton={true}
|
/>
|
||||||
/>
|
<Row3 articles={sortedArticles().slice(1, 4)} />
|
||||||
<Row3 articles={sortedArticles().slice(1, 4)} />
|
<Row2 articles={sortedArticles().slice(4, 6)} />
|
||||||
|
<Row3 articles={sortedArticles().slice(6, 9)} />
|
||||||
|
<Row3 articles={sortedArticles().slice(9, 12)} />
|
||||||
|
|
||||||
<Show when={sortedArticles().length > 4}>
|
<For each={pages()}>
|
||||||
<Row2 articles={sortedArticles().slice(4, 6)} />
|
{(page) => (
|
||||||
</Show>
|
<>
|
||||||
|
<Row3 articles={page.slice(0, 3)} />
|
||||||
|
<Row3 articles={page.slice(3, 6)} />
|
||||||
|
<Row3 articles={page.slice(6, 9)} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
<Show when={sortedArticles().length > 6}>
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
<Row3 articles={sortedArticles().slice(6, 9)} />
|
<p class="load-more-container">
|
||||||
</Show>
|
<button class="button" onClick={loadMore}>
|
||||||
|
{t('Load more')}
|
||||||
<Show when={sortedArticles().length > 9}>
|
</button>
|
||||||
<Row3 articles={sortedArticles().slice(9, 12)} />
|
</p>
|
||||||
</Show>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createMemo, For, Show } from 'solid-js'
|
import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
|
||||||
import type { Shout, Reaction } from '../../graphql/types.gen'
|
|
||||||
import '../../styles/Feed.scss'
|
import '../../styles/Feed.scss'
|
||||||
|
import stylesBeside from '../../components/Feed/Beside.module.scss'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { byCreated, sortBy } from '../../utils/sortby'
|
import { byCreated, sortBy } from '../../utils/sortby'
|
||||||
import { TopicCard } from '../Topic/Card'
|
import { TopicCard } from '../Topic/Card'
|
||||||
|
@ -16,11 +16,6 @@ import { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
import { useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
||||||
|
|
||||||
interface FeedProps {
|
|
||||||
articles: Shout[]
|
|
||||||
reactions?: Reaction[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// const AUTHORSHIP_REACTIONS = [
|
// const AUTHORSHIP_REACTIONS = [
|
||||||
// ReactionKind.Accept,
|
// ReactionKind.Accept,
|
||||||
// ReactionKind.Reject,
|
// ReactionKind.Reject,
|
||||||
|
@ -28,9 +23,11 @@ interface FeedProps {
|
||||||
// ReactionKind.Ask
|
// ReactionKind.Ask
|
||||||
// ]
|
// ]
|
||||||
|
|
||||||
export const FeedView = (props: FeedProps) => {
|
export const FEED_PAGE_SIZE = 20
|
||||||
|
|
||||||
|
export const FeedView = () => {
|
||||||
// state
|
// state
|
||||||
const { sortedArticles } = useArticlesStore({ sortedArticles: props.articles })
|
const { sortedArticles } = useArticlesStore()
|
||||||
const reactions = useReactionsStore()
|
const reactions = useReactionsStore()
|
||||||
const { sortedAuthors } = useAuthorsStore()
|
const { sortedAuthors } = useAuthorsStore()
|
||||||
const { topTopics } = useTopicsStore()
|
const { topTopics } = useTopicsStore()
|
||||||
|
@ -39,6 +36,8 @@ export const FeedView = (props: FeedProps) => {
|
||||||
|
|
||||||
const topReactions = createMemo(() => sortBy(reactions(), byCreated))
|
const topReactions = createMemo(() => sortBy(reactions(), byCreated))
|
||||||
|
|
||||||
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
// const expectingFocus = createMemo<Shout[]>(() => {
|
// const expectingFocus = createMemo<Shout[]>(() => {
|
||||||
// // 1 co-author notifications needs
|
// // 1 co-author notifications needs
|
||||||
// // TODO: list of articles where you are co-author
|
// // TODO: list of articles where you are co-author
|
||||||
|
@ -52,13 +51,15 @@ export const FeedView = (props: FeedProps) => {
|
||||||
// return []
|
// return []
|
||||||
// })
|
// })
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
const loadMore = async () => {
|
||||||
const loadMore = () => {
|
const { hasMore } = await loadRecentArticles({ limit: FEED_PAGE_SIZE, offset: sortedArticles().length })
|
||||||
// const limit = props.limit || 50
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
// const offset = props.offset || 0
|
|
||||||
// FIXME
|
|
||||||
loadRecentArticles({ limit: 50, offset: 0 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadMore()
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="container feed">
|
<div class="container feed">
|
||||||
|
@ -90,7 +91,7 @@ export const FeedView = (props: FeedProps) => {
|
||||||
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<div class="beside-column-title">
|
<div class={stylesBeside.besideColumnTitle}>
|
||||||
<h4>{t('Popular authors')}</h4>
|
<h4>{t('Popular authors')}</h4>
|
||||||
<a href="/user/list">
|
<a href="/user/list">
|
||||||
{t('All authors')}
|
{t('All authors')}
|
||||||
|
@ -98,7 +99,7 @@ export const FeedView = (props: FeedProps) => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="beside-column">
|
<ul class={stylesBeside.besideColumn}>
|
||||||
<For each={topAuthors().slice(0, 5)}>
|
<For each={topAuthors().slice(0, 5)}>
|
||||||
{(author) => (
|
{(author) => (
|
||||||
<li>
|
<li>
|
||||||
|
@ -112,10 +113,6 @@ export const FeedView = (props: FeedProps) => {
|
||||||
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<p class="load-more-container">
|
|
||||||
<button class="button">{t('Load more')}</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="col-md-3">
|
<aside class="col-md-3">
|
||||||
|
@ -135,12 +132,13 @@ export const FeedView = (props: FeedProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
<p class="load-more-container">
|
<p class="load-more-container">
|
||||||
<button class="button" onClick={loadMore}>
|
<button class="button" onClick={loadMore}>
|
||||||
{t('Load more')}
|
{t('Load more')}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { createMemo, For, onMount, Show } from 'solid-js'
|
import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
|
||||||
import Banner from '../Discours/Banner'
|
import Banner from '../Discours/Banner'
|
||||||
import { NavTopics } from '../Nav/Topics'
|
import { NavTopics } from '../Nav/Topics'
|
||||||
import { Row5 } from '../Feed/Row5'
|
import { Row5 } from '../Feed/Row5'
|
||||||
import Row3 from '../Feed/Row3'
|
import { Row3 } from '../Feed/Row3'
|
||||||
import Row2 from '../Feed/Row2'
|
import { Row2 } from '../Feed/Row2'
|
||||||
import Row1 from '../Feed/Row1'
|
import { Row1 } from '../Feed/Row1'
|
||||||
import Hero from '../Discours/Hero'
|
import Hero from '../Discours/Hero'
|
||||||
import Beside from '../Feed/Beside'
|
import { Beside } from '../Feed/Beside'
|
||||||
import RowShort from '../Feed/RowShort'
|
import RowShort from '../Feed/RowShort'
|
||||||
import Slider from '../Feed/Slider'
|
import Slider from '../Feed/Slider'
|
||||||
import Group from '../Feed/Group'
|
import Group from '../Feed/Group'
|
||||||
|
@ -23,12 +23,14 @@ import {
|
||||||
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||||
|
import { splitToPages } from '../../utils/splitToPages'
|
||||||
|
|
||||||
type HomeProps = {
|
type HomeProps = {
|
||||||
randomTopics: Topic[]
|
randomTopics: Topic[]
|
||||||
recentPublishedArticles: Shout[]
|
recentPublishedArticles: Shout[]
|
||||||
}
|
}
|
||||||
const PRERENDERED_ARTICLES_COUNT = 5
|
|
||||||
|
export const PRERENDERED_ARTICLES_COUNT = 5
|
||||||
const CLIENT_LOAD_ARTICLES_COUNT = 29
|
const CLIENT_LOAD_ARTICLES_COUNT = 29
|
||||||
const LOAD_MORE_PAGE_SIZE = 16 // Row1 + Row3 + Row2 + Beside (3 + 1) + Row1 + Row 2 + Row3
|
const LOAD_MORE_PAGE_SIZE = 16 // Row1 + Row3 + Row2 + Beside (3 + 1) + Row1 + Row 2 + Row3
|
||||||
|
|
||||||
|
@ -46,14 +48,20 @@ export const HomeView = (props: HomeProps) => {
|
||||||
const { randomTopics, topTopics } = useTopicsStore({
|
const { randomTopics, topTopics } = useTopicsStore({
|
||||||
randomTopics: props.randomTopics
|
randomTopics: props.randomTopics
|
||||||
})
|
})
|
||||||
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
const { topAuthors } = useTopAuthorsStore()
|
const { topAuthors } = useTopAuthorsStore()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
loadTopArticles()
|
loadTopArticles()
|
||||||
loadTopMonthArticles()
|
loadTopMonthArticles()
|
||||||
if (sortedArticles().length < PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) {
|
if (sortedArticles().length < PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) {
|
||||||
loadPublishedArticles({ limit: CLIENT_LOAD_ARTICLES_COUNT, offset: sortedArticles().length })
|
const { hasMore } = await loadPublishedArticles({
|
||||||
|
limit: CLIENT_LOAD_ARTICLES_COUNT,
|
||||||
|
offset: sortedArticles().length
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -82,22 +90,23 @@ export const HomeView = (props: HomeProps) => {
|
||||||
|
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
saveScrollPosition()
|
saveScrollPosition()
|
||||||
await loadPublishedArticles({ limit: LOAD_MORE_PAGE_SIZE, offset: sortedArticles().length })
|
|
||||||
|
const { hasMore } = await loadPublishedArticles({
|
||||||
|
limit: LOAD_MORE_PAGE_SIZE,
|
||||||
|
offset: sortedArticles().length
|
||||||
|
})
|
||||||
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
|
|
||||||
restoreScrollPosition()
|
restoreScrollPosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
const pages = createMemo<Shout[][]>(() => {
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
return sortedArticles()
|
splitToPages(
|
||||||
.slice(PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT)
|
sortedArticles(),
|
||||||
.reduce((acc, article, index) => {
|
PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT,
|
||||||
if (index % LOAD_MORE_PAGE_SIZE === 0) {
|
LOAD_MORE_PAGE_SIZE
|
||||||
acc.push([])
|
)
|
||||||
}
|
)
|
||||||
|
|
||||||
acc[acc.length - 1].push(article)
|
|
||||||
return acc
|
|
||||||
}, [] as Shout[][])
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={locale() && sortedArticles().length > 0}>
|
<Show when={locale() && sortedArticles().length > 0}>
|
||||||
|
@ -170,11 +179,13 @@ export const HomeView = (props: HomeProps) => {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<p class="load-more-container">
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
<button class="button" onClick={loadMore}>
|
<p class="load-more-container">
|
||||||
{t('Load more')}
|
<button class="button" onClick={loadMore}>
|
||||||
</button>
|
{t('Load more')}
|
||||||
</p>
|
</button>
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { For, Show, createMemo } from 'solid-js'
|
import { For, Show, createMemo, onMount, createSignal } from 'solid-js'
|
||||||
import type { Shout, Topic } from '../../graphql/types.gen'
|
import type { Shout, Topic } from '../../graphql/types.gen'
|
||||||
import Row3 from '../Feed/Row3'
|
import { Row3 } from '../Feed/Row3'
|
||||||
import Row2 from '../Feed/Row2'
|
import { Row2 } from '../Feed/Row2'
|
||||||
import Beside from '../Feed/Beside'
|
import { Beside } from '../Feed/Beside'
|
||||||
import { ArticleCard } from '../Feed/Card'
|
import { ArticleCard } from '../Feed/Card'
|
||||||
import '../../styles/Topic.scss'
|
import '../../styles/Topic.scss'
|
||||||
import { FullTopic } from '../Topic/Full'
|
import { FullTopic } from '../Topic/Full'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
import { useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { useArticlesStore } from '../../stores/zine/articles'
|
import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/articles'
|
||||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
|
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||||
|
import { splitToPages } from '../../utils/splitToPages'
|
||||||
|
|
||||||
type TopicsPageSearchParams = {
|
type TopicsPageSearchParams = {
|
||||||
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
|
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
|
||||||
|
@ -22,9 +24,14 @@ interface TopicProps {
|
||||||
topicSlug: string
|
topicSlug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PRERENDERED_ARTICLES_COUNT = 21
|
||||||
|
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
|
||||||
|
|
||||||
export const TopicView = (props: TopicProps) => {
|
export const TopicView = (props: TopicProps) => {
|
||||||
const { searchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
|
const { searchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
|
||||||
|
|
||||||
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
|
const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
|
||||||
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
||||||
|
|
||||||
|
@ -32,6 +39,24 @@ export const TopicView = (props: TopicProps) => {
|
||||||
|
|
||||||
const topic = createMemo(() => topicEntities()[props.topicSlug])
|
const topic = createMemo(() => topicEntities()[props.topicSlug])
|
||||||
|
|
||||||
|
const loadMore = async () => {
|
||||||
|
saveScrollPosition()
|
||||||
|
|
||||||
|
const { hasMore } = await loadPublishedArticles({
|
||||||
|
limit: LOAD_MORE_PAGE_SIZE,
|
||||||
|
offset: sortedArticles().length
|
||||||
|
})
|
||||||
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
|
|
||||||
|
restoreScrollPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
const m = searchParams().by
|
const m = searchParams().by
|
||||||
if (m === 'viewed') return t('Top viewed')
|
if (m === 'viewed') return t('Top viewed')
|
||||||
|
@ -40,6 +65,10 @@ export const TopicView = (props: TopicProps) => {
|
||||||
return t('Top recent')
|
return t('Top recent')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
|
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="topic-page container">
|
<div class="topic-page container">
|
||||||
<Show when={topic()}>
|
<Show when={topic()}>
|
||||||
|
@ -110,6 +139,24 @@ export const TopicView = (props: TopicProps) => {
|
||||||
<Row3 articles={sortedArticles().slice(15, 18)} />
|
<Row3 articles={sortedArticles().slice(15, 18)} />
|
||||||
<Row3 articles={sortedArticles().slice(18, 21)} />
|
<Row3 articles={sortedArticles().slice(18, 21)} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<For each={pages()}>
|
||||||
|
{(page) => (
|
||||||
|
<>
|
||||||
|
<Row3 articles={page.slice(0, 3)} />
|
||||||
|
<Row3 articles={page.slice(3, 6)} />
|
||||||
|
<Row3 articles={page.slice(6, 9)} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
|
<p class="load-more-container">
|
||||||
|
<button class="button" onClick={loadMore}>
|
||||||
|
{t('Load more')}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,6 @@ export type PageProps = {
|
||||||
authorArticles?: Shout[]
|
authorArticles?: Shout[]
|
||||||
topicArticles?: Shout[]
|
topicArticles?: Shout[]
|
||||||
homeArticles?: Shout[]
|
homeArticles?: Shout[]
|
||||||
feedArticles?: Shout[]
|
|
||||||
author?: Author
|
author?: Author
|
||||||
allAuthors?: Author[]
|
allAuthors?: Author[]
|
||||||
topic?: Topic
|
topic?: Topic
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation CreateShoutMutations($shout: ShoutInput!) {
|
mutation CreateShoutMutation($shout: ShoutInput!) {
|
||||||
createShout(input: $shout) {
|
createShout(input: $shout) {
|
||||||
error
|
error
|
||||||
shout {
|
shout {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation DeleteShoutMutation($shout: String!) {
|
mutation DeleteShoutMutation($shout: String!) {
|
||||||
deleteShout(slug: $shout) {
|
deleteShout(slug: $shout) {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation ConfirmEmailMutation($code: String!) {
|
mutation ConfirmEmailMutation($token: String!) {
|
||||||
confirmEmail(code: $code) {
|
confirmEmail(token: $token) {
|
||||||
error
|
error
|
||||||
token
|
token
|
||||||
user {
|
user {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation DeleteReactionMutation($id: Int!) {
|
mutation DeleteReactionMutation($id: Int!) {
|
||||||
deleteReaction(id: $id) {
|
deleteReaction(id: $id) {
|
||||||
|
|
|
@ -171,7 +171,7 @@ export type Mutation = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationConfirmEmailArgs = {
|
export type MutationConfirmEmailArgs = {
|
||||||
token: Scalars['String']
|
code: Scalars['String']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationCreateChatArgs = {
|
export type MutationCreateChatArgs = {
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
"Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики",
|
"Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики",
|
||||||
"How it works": "Как это работает",
|
"How it works": "Как это работает",
|
||||||
"How to write an article": "Как написать статью",
|
"How to write an article": "Как написать статью",
|
||||||
"I have an account": "У меня есть аккаунт",
|
"I have an account": "У меня есть аккаунт!",
|
||||||
"I have no account yet": "У меня еще нет аккаунта",
|
"I have no account yet": "У меня еще нет аккаунта",
|
||||||
"I know the password": "Я знаю пароль",
|
"I know the password": "Я знаю пароль",
|
||||||
"Join our maillist": "Чтобы получать рассылку лучших публикаций, просто укажите свою почту",
|
"Join our maillist": "Чтобы получать рассылку лучших публикаций, просто укажите свою почту",
|
||||||
|
@ -157,7 +157,13 @@
|
||||||
"Restore password": "Восстановить пароль",
|
"Restore password": "Восстановить пароль",
|
||||||
"Hooray! Welcome!": "Ура! Добро пожаловать!",
|
"Hooray! Welcome!": "Ура! Добро пожаловать!",
|
||||||
"You've confirmed email": "Вы подтвердили почту",
|
"You've confirmed email": "Вы подтвердили почту",
|
||||||
"You've confirmed your account": "Вы подтвердили свою учётную запись",
|
|
||||||
"This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы",
|
"This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы",
|
||||||
"enter": "войдите"
|
"enter": "войдите",
|
||||||
|
"Go to main page": "Перейти на главную",
|
||||||
|
"Back to main page": "Вернуться на главную",
|
||||||
|
"Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.",
|
||||||
|
"We've sent you a message with a link to enter our website.": "Мы выслали вам письмо с ссылкой на почту. Перейдите по ссылке в письме, чтобы войти на сайт.",
|
||||||
|
"Send link again": "Прислать ссылку ещё раз",
|
||||||
|
"Link sent, check your email": "Ссылка отправлена, проверьте почту",
|
||||||
|
"Create post": "Создать публикацию"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@ import { Root } from '../../../components/Root'
|
||||||
import Zine from '../../../layouts/zine.astro'
|
import Zine from '../../../layouts/zine.astro'
|
||||||
import { apiClient } from '../../../utils/apiClient'
|
import { apiClient } from '../../../utils/apiClient'
|
||||||
import { initRouter } from '../../../stores/router'
|
import { initRouter } from '../../../stores/router'
|
||||||
|
import { PRERENDERED_ARTICLES_COUNT } from '../../../components/Views/Author'
|
||||||
|
|
||||||
const slug = Astro.params.slug.toString()
|
const slug = Astro.params.slug.toString()
|
||||||
const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: 50 })
|
const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT })
|
||||||
const author = articles[0].authors.find((a) => a.slug === slug)
|
const author = articles[0].authors.find((a) => a.slug === slug)
|
||||||
|
|
||||||
const { pathname, search } = Astro.url
|
const { pathname, search } = Astro.url
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
---
|
---
|
||||||
|
import { Confirmed } from '../../components/Nav/Confirmed'
|
||||||
|
import { t } from '../../utils/intl'
|
||||||
|
|
||||||
const token = Astro.params.token?.toString() || ''
|
const token = Astro.params.token?.toString() || ''
|
||||||
return Astro.redirect('/?modal=confirm-oauth&token=' + token)
|
|
||||||
---
|
---
|
||||||
|
<html>
|
||||||
|
<head><title>{t('Discours')}</title></head>
|
||||||
|
<body>
|
||||||
|
<Confirmed token={token} />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
10
src/pages/confirm/index.astro
Normal file
10
src/pages/confirm/index.astro
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
import { t } from '../../utils/intl'
|
||||||
|
import { Confirmed } from '../../components/Nav/Confirmed'
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head><title>{t('Discours')}</title></head>
|
||||||
|
<body>
|
||||||
|
<Confirmed />
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,16 +1,12 @@
|
||||||
---
|
---
|
||||||
import { Root } from '../../components/Root'
|
import { Root } from '../../components/Root'
|
||||||
import Zine from '../../layouts/zine.astro'
|
import Zine from '../../layouts/zine.astro'
|
||||||
import { apiClient } from '../../utils/apiClient'
|
|
||||||
|
|
||||||
import { initRouter } from '../../stores/router'
|
import { initRouter } from '../../stores/router'
|
||||||
|
|
||||||
const { pathname, search } = Astro.url
|
const { pathname, search } = Astro.url
|
||||||
initRouter(pathname, search)
|
initRouter(pathname, search)
|
||||||
|
|
||||||
const articles = await apiClient.getRecentArticles({ limit: 50 })
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Zine>
|
<Zine>
|
||||||
<Root feedArticles={articles} client:load />
|
<Root client:load />
|
||||||
</Zine>
|
</Zine>
|
||||||
|
|
|
@ -3,14 +3,14 @@ import Zine from '../layouts/zine.astro'
|
||||||
import { Root } from '../components/Root'
|
import { Root } from '../components/Root'
|
||||||
import { apiClient } from '../utils/apiClient'
|
import { apiClient } from '../utils/apiClient'
|
||||||
import { initRouter } from '../stores/router'
|
import { initRouter } from '../stores/router'
|
||||||
|
import { PRERENDERED_ARTICLES_COUNT } from '../components/Views/Home'
|
||||||
|
|
||||||
const randomTopics = await apiClient.getRandomTopics({ amount: 12 })
|
const randomTopics = await apiClient.getRandomTopics({ amount: 12 })
|
||||||
const articles = await apiClient.getRecentPublishedArticles({ limit: 5 })
|
const articles = await apiClient.getRecentPublishedArticles({ limit: PRERENDERED_ARTICLES_COUNT })
|
||||||
|
|
||||||
const { pathname, search } = Astro.url
|
const { pathname, search } = Astro.url
|
||||||
initRouter(pathname, search)
|
initRouter(pathname, search)
|
||||||
|
|
||||||
|
|
||||||
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
|
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
import { Root } from '../../components/Root'
|
import { Root } from '../../components/Root'
|
||||||
import Zine from '../../layouts/zine.astro'
|
import Zine from '../../layouts/zine.astro'
|
||||||
import { apiClient } from '../../utils/apiClient'
|
import { apiClient } from '../../utils/apiClient'
|
||||||
|
import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Topic'
|
||||||
|
|
||||||
const slug = Astro.params.slug?.toString() || ''
|
const slug = Astro.params.slug?.toString() || ''
|
||||||
const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: 50 })
|
const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT })
|
||||||
const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug)
|
const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug)
|
||||||
|
|
||||||
import { initRouter } from '../../stores/router'
|
import { initRouter } from '../../stores/router'
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
---
|
---
|
||||||
return Astro.redirect('/?modal=auth&mode=welcome')
|
return Astro.redirect('/?modal=auth&mode=register')
|
||||||
---
|
---
|
||||||
|
|
|
@ -11,7 +11,6 @@ export const signIn = async (params) => {
|
||||||
setToken(authResult.token)
|
setToken(authResult.token)
|
||||||
console.debug('signed in')
|
console.debug('signed in')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signOut = () => {
|
export const signOut = () => {
|
||||||
// TODO: call backend to revoke token
|
// TODO: call backend to revoke token
|
||||||
setSession(null)
|
setSession(null)
|
||||||
|
@ -54,7 +53,7 @@ export const register = async ({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signSendLink = async ({ email, lang }: { email: string, lang: string }) => {
|
export const signSendLink = async ({ email, lang }: { email: string; lang: string }) => {
|
||||||
await apiClient.authSendLink({ email, lang })
|
await apiClient.authSendLink({ email, lang })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useRouter } from './router'
|
||||||
|
|
||||||
//export const locale = persistentAtom<string>('locale', 'ru')
|
//export const locale = persistentAtom<string>('locale', 'ru')
|
||||||
export const [locale, setLocale] = createSignal('ru')
|
export const [locale, setLocale] = createSignal('ru')
|
||||||
export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'share' | 'thank' | 'donate'
|
export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'thank' | 'donate'
|
||||||
type WarnKind = 'error' | 'warn' | 'info'
|
type WarnKind = 'error' | 'warn' | 'info'
|
||||||
|
|
||||||
export interface Warning {
|
export interface Warning {
|
||||||
|
@ -17,7 +17,6 @@ export const MODALS: Record<ModalType, ModalType> = {
|
||||||
auth: 'auth',
|
auth: 'auth',
|
||||||
subscribe: 'subscribe',
|
subscribe: 'subscribe',
|
||||||
feedback: 'feedback',
|
feedback: 'feedback',
|
||||||
share: 'share',
|
|
||||||
thank: 'thank',
|
thank: 'thank',
|
||||||
donate: 'donate'
|
donate: 'donate'
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,40 +123,109 @@ const addSortedArticles = (articles: Shout[]) => {
|
||||||
setSortedArticles((prevSortedArticles) => [...prevSortedArticles, ...articles])
|
setSortedArticles((prevSortedArticles) => [...prevSortedArticles, ...articles])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadFeed = async ({
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
}: {
|
||||||
|
limit: number
|
||||||
|
offset?: number
|
||||||
|
}): Promise<{ hasMore: boolean }> => {
|
||||||
|
// TODO: load actual feed
|
||||||
|
return await loadRecentArticles({ limit, offset })
|
||||||
|
}
|
||||||
|
|
||||||
export const loadRecentArticles = async ({
|
export const loadRecentArticles = async ({
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset
|
||||||
}: {
|
}: {
|
||||||
limit?: number
|
limit: number
|
||||||
offset?: number
|
offset?: number
|
||||||
}): Promise<void> => {
|
}): Promise<{ hasMore: boolean }> => {
|
||||||
const newArticles = await apiClient.getRecentArticles({ limit, offset })
|
const newArticles = await apiClient.getRecentArticles({ limit: limit + 1, offset })
|
||||||
|
const hasMore = newArticles.length === limit + 1
|
||||||
|
|
||||||
|
if (hasMore) {
|
||||||
|
newArticles.splice(-1)
|
||||||
|
}
|
||||||
|
|
||||||
addArticles(newArticles)
|
addArticles(newArticles)
|
||||||
addSortedArticles(newArticles)
|
addSortedArticles(newArticles)
|
||||||
|
|
||||||
|
return { hasMore }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadPublishedArticles = async ({
|
export const loadPublishedArticles = async ({
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset = 0
|
||||||
}: {
|
}: {
|
||||||
limit?: number
|
limit: number
|
||||||
offset?: number
|
offset?: number
|
||||||
}): Promise<void> => {
|
}): Promise<{ hasMore: boolean }> => {
|
||||||
const newArticles = await apiClient.getPublishedArticles({ limit, offset })
|
const newArticles = await apiClient.getPublishedArticles({ limit: limit + 1, offset })
|
||||||
|
const hasMore = newArticles.length === limit + 1
|
||||||
|
|
||||||
|
if (hasMore) {
|
||||||
|
newArticles.splice(-1)
|
||||||
|
}
|
||||||
|
|
||||||
addArticles(newArticles)
|
addArticles(newArticles)
|
||||||
addSortedArticles(newArticles)
|
addSortedArticles(newArticles)
|
||||||
|
|
||||||
|
return { hasMore }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadArticlesForAuthors = async ({ authorSlugs }: { authorSlugs: string[] }): Promise<void> => {
|
export const loadAuthorArticles = async ({
|
||||||
const articles = await apiClient.getArticlesForAuthors({ authorSlugs, limit: 50 })
|
authorSlug,
|
||||||
addArticles(articles)
|
limit,
|
||||||
setSortedArticles(articles)
|
offset = 0
|
||||||
|
}: {
|
||||||
|
authorSlug: string
|
||||||
|
limit: number
|
||||||
|
offset?: number
|
||||||
|
}): Promise<{ hasMore: boolean }> => {
|
||||||
|
const newArticles = await apiClient.getArticlesForAuthors({
|
||||||
|
authorSlugs: [authorSlug],
|
||||||
|
limit: limit + 1,
|
||||||
|
offset
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasMore = newArticles.length === limit + 1
|
||||||
|
|
||||||
|
if (hasMore) {
|
||||||
|
newArticles.splice(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
addArticles(newArticles)
|
||||||
|
addSortedArticles(newArticles)
|
||||||
|
|
||||||
|
return { hasMore }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadArticlesForTopics = async ({ topicSlugs }: { topicSlugs: string[] }): Promise<void> => {
|
export const loadTopicArticles = async ({
|
||||||
const articles = await apiClient.getArticlesForTopics({ topicSlugs, limit: 50 })
|
topicSlug,
|
||||||
addArticles(articles)
|
limit,
|
||||||
setSortedArticles(articles)
|
offset
|
||||||
|
}: {
|
||||||
|
topicSlug: string
|
||||||
|
limit: number
|
||||||
|
offset: number
|
||||||
|
}): Promise<{ hasMore: boolean }> => {
|
||||||
|
const newArticles = await apiClient.getArticlesForTopics({
|
||||||
|
topicSlugs: [topicSlug],
|
||||||
|
limit: limit + 1,
|
||||||
|
offset
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasMore = newArticles.length === limit + 1
|
||||||
|
|
||||||
|
if (hasMore) {
|
||||||
|
newArticles.splice(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
addArticles(newArticles)
|
||||||
|
addSortedArticles(newArticles)
|
||||||
|
|
||||||
|
return { hasMore }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resetSortedArticles = () => {
|
export const resetSortedArticles = () => {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user