This commit is contained in:
Igor Lobanov 2022-10-31 13:51:52 +01:00
parent 4d69749f55
commit 190a3226d5
30 changed files with 330 additions and 313 deletions

View File

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

View File

@ -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' : 'Русский'))

View File

@ -10,10 +10,10 @@ export const Editor = () => {
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 editorCss = (config) => css``
const style = () => (store.error ? `display: none;` : (store.markdown ? `white-space: pre-wrap;` : '')) const style = () => (store.error ? `display: none;` : store.markdown ? `white-space: pre-wrap;` : '')
return ( return (
<ProseMirror <ProseMirror
className='editor col-md-6 shift-content' className="editor col-md-6 shift-content"
style={style()} style={style()}
editorView={store.editorView} editorView={store.editorView}
text={store.text} text={store.text}

View File

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

View File

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

View File

@ -6,14 +6,14 @@ 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; style?: string
className?: 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 +28,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 style={props.style} ref={editorRef} class={props.className} spell-check={false} />
<div
style={props.style}
ref={editorRef}
class={props.className}
spell-check={false}
/>
)
} }
const createEditorState = ( const createEditorState = (
@ -74,8 +68,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 +98,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)
} }

View File

@ -8,16 +8,16 @@ import { isEmpty } from '../prosemirror/helpers'
import type { Styled } from './Layout' import type { Styled } from './Layout'
import '../styles/Sidebar.scss' import '../styles/Sidebar.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={`sidebar-link${props.className ? ' ' + props.className : ''}`}
style={{ "margin-bottom": props.withMargin ? '10px' : '' }} style={{ 'margin-bottom': props.withMargin ? '10px' : '' }}
onClick={props.onClick} onClick={props.onClick}
disabled={props.disabled} disabled={props.disabled}
title={props.title} title={props.title}
@ -34,22 +34,23 @@ 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 = () => (store.collab?.started ? 'Stop' : store.collab?.error ? 'Restart 🚨' : 'Start')
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 +85,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 +99,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 +117,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}>Советы и&nbsp;предложения</span> <span class="sidebar-opener" onClick={toggleSidebar}>
Советы и&nbsp;предложения
</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,27 +130,25 @@ 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')}{' '} {store.path
? 'Close'
: store.drafts.length > 0 && isEmpty(store.text)
? 'Delete ⚠️'
: 'Clear'}{' '}
<Keys keys={[mod, 'w']} /> <Keys keys={[mod, 'w']} />
</Link> </Link>
<Link onClick={onUndo}> <Link onClick={onUndo}>
@ -156,7 +157,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, {}, [

View File

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

View File

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

View File

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

View File

@ -16,7 +16,6 @@ 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 +53,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 +62,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 {
@ -275,26 +274,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 })
@ -380,7 +380,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 })
} }

View File

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

View File

@ -12,7 +12,7 @@
color: var(--background); color: var(--background);
border-color: var(--foreground); border-color: var(--foreground);
} }
.drop-cursor { .drop-cursor {
height: 2px !important; height: 2px !important;
opacity: 0.5; opacity: 0.5;

View File

@ -1,5 +1,5 @@
.sidebar-container { .sidebar-container {
color: rgba(255,255,255,0.5); color: rgba(255, 255, 255, 0.5);
font-family: 'Muller'; font-family: 'Muller';
@include font-size(1.6rem); @include font-size(1.6rem);
overflow: hidden; overflow: hidden;
@ -143,7 +143,7 @@
} }
&.draft { &.draft {
color: rgba(255,255,255,0.5); color: rgba(255, 255, 255, 0.5);
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 +176,22 @@
} }
.theme-switcher { .theme-switcher {
border-bottom: 1px solid rgba(255,255,255,0.3); border-bottom: 1px solid rgba(255, 255, 255, 0.3);
border-top: 1px solid rgba(255,255,255,0.3); border-top: 1px solid rgba(255, 255, 255, 0.3);
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;

View File

@ -7,7 +7,7 @@ export default (props: { article: Shout }) => (
<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>

View File

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

View File

@ -44,10 +44,10 @@ export const Header = (props: Props) => {
const toggleFixed = () => setFixed(!fixed()) const toggleFixed = () => setFixed(!fixed())
// effects // effects
createEffect(() => { createEffect(() => {
const isFixed = fixed() || (modal() && modal() !== 'share'); const isFixed = fixed() || (modal() && modal() !== 'share')
document.body.classList.toggle('fixed', isFixed); document.body.classList.toggle('fixed', isFixed)
document.body.classList.toggle(styles.fixed, isFixed && !modal()); document.body.classList.toggle(styles.fixed, isFixed && !modal())
}) })
// derived // derived

View File

@ -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>
Дискурс&nbsp;&mdash; независимый художественно-аналитический журнал с&nbsp;горизонтальной редакцией, Дискурс&nbsp;&mdash; независимый художественно-аналитический журнал с&nbsp;горизонтальной
основанный на&nbsp;принципах свободы слова, прямой демократии и&nbsp;совместного редактирования. редакцией, основанный на&nbsp;принципах свободы слова, прямой демократии и&nbsp;совместного
Дискурс создаётся открытым медиасообществом ученых, журналистов, музыкантов, писателей, редактирования. Дискурс создаётся открытым медиасообществом ученых, журналистов, музыкантов,
предпринимателей, философов, инженеров, художников и&nbsp;специалистов со&nbsp;всего мира, писателей, предпринимателей, философов, инженеров, художников и&nbsp;специалистов
объединившихся, чтобы вместе делать общий журнал и&nbsp;объяснять с&nbsp;разных точек со&nbsp;всего мира, объединившихся, чтобы вместе делать общий журнал и&nbsp;объяснять
зрения мозаичную картину современности. с&nbsp;разных точек зрения мозаичную картину современности.
</p> </p>
<p> <p>
Мы&nbsp;пишем о&nbsp;культуре, науке и&nbsp;обществе, рассказываем о&nbsp;новых идеях и&nbsp;современном искусстве, Мы&nbsp;пишем о&nbsp;культуре, науке и&nbsp;обществе, рассказываем о&nbsp;новых идеях
публикуем статьи, исследования, репортажи, интервью людей, чью прямую речь стоит услышать, и&nbsp;современном искусстве, публикуем статьи, исследования, репортажи, интервью людей, чью
и&nbsp;работы художников из&nbsp;разных стран&nbsp;&mdash; от&nbsp;фильмов и&nbsp;музыки прямую речь стоит услышать, и&nbsp;работы художников из&nbsp;разных стран&nbsp;&mdash;
до&nbsp;живописи и&nbsp;фотографии. Помогая друг другу делать публикации качественнее от&nbsp;фильмов и&nbsp;музыки до&nbsp;живописи и&nbsp;фотографии. Помогая друг другу делать
и&nbsp;общим голосованием выбирая лучшие материалы для журнала, мы&nbsp;создаём новую публикации качественнее и&nbsp;общим голосованием выбирая лучшие материалы для журнала,
горизонтальную журналистику, чтобы честно рассказывать о&nbsp;важном и&nbsp;интересном. мы&nbsp;создаём новую горизонтальную журналистику, чтобы честно рассказывать о&nbsp;важном
и&nbsp;интересном.
</p> </p>
<p> <p>
Редакция Дискурса открыта для всех: у&nbsp;нас нет цензуры, запретных тем и&nbsp;идеологических рамок. Редакция Дискурса открыта для всех: у&nbsp;нас нет цензуры, запретных тем
Каждый может <a href="/create">прислать материал</a> в&nbsp;журнал и&nbsp;идеологических рамок. Каждый может <a href="/create">прислать материал</a>{' '}
и&nbsp;<a href="/about/guide">присоединиться к&nbsp;редакции</a>. Предоставляя трибуну в&nbsp;журнал и&nbsp;<a href="/about/guide">присоединиться к&nbsp;редакции</a>. Предоставляя
для независимой журналистики и&nbsp;художественных проектов, мы&nbsp;помогаем людям трибуну для независимой журналистики и&nbsp;художественных проектов, мы&nbsp;помогаем людям
рассказывать свои истории так, чтобы они были услышаны. Мы&nbsp;убеждены: чем больше рассказывать свои истории так, чтобы они были услышаны. Мы&nbsp;убеждены: чем больше голосов
голосов будет звучать на&nbsp;Дискурсе, тем громче в&nbsp;полифонии мнений будет слышна истина. будет звучать на&nbsp;Дискурсе, тем громче в&nbsp;полифонии мнений будет слышна истина.
</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> свои статьи и&nbsp;художественные работы&nbsp;&mdash; лучшие из них будут <a href="/create">Создавайте</a> свои статьи и&nbsp;художественные работы&nbsp;&mdash;
опубликованы в&nbsp;журнале. Дискурс&nbsp;&mdash; некоммерческое издание, авторы публикуются лучшие из них будут опубликованы в&nbsp;журнале. Дискурс&nbsp;&mdash; некоммерческое
в&nbsp;журнале на&nbsp;общественных началах, получая при этом <a href="/create?collab=true">поддержку</a> редакции, издание, авторы публикуются в&nbsp;журнале на&nbsp;общественных началах, получая при этом{' '}
право голоса, множество других возможностей и&nbsp;читателей по&nbsp;всему миру. <a href="/create?collab=true">поддержку</a> редакции, право голоса, множество других
возможностей и&nbsp;читателей по&nbsp;всему миру.
</p> </p>
<h3 id="donate">Поддерживать проект</h3> <h3 id="donate">Поддерживать проект</h3>
<p>Дискурс существует на&nbsp;пожертвования читателей. Если вам нравится журнал, пожалуйста,</p>
<p> <p>
<a href="/about/help">поддержите</a> нашу работу. Ваши пожертвования пойдут на&nbsp;выпуск новых Дискурс существует на&nbsp;пожертвования читателей. Если вам нравится журнал, пожалуйста,
материалов, оплату серверов, труда программистов, дизайнеров и&nbsp;редакторов. </p>
<p>
<a href="/about/help">поддержите</a> нашу работу. Ваши пожертвования пойдут на&nbsp;выпуск
новых материалов, оплату серверов, труда программистов, дизайнеров и&nbsp;редакторов.
</p> </p>
<h3 id="cooperation">Сотрудничать с&nbsp;журналом</h3> <h3 id="cooperation">Сотрудничать с&nbsp;журналом</h3>
<p> <p>
Мы всегда открыты для сотрудничества и&nbsp;рады единомышленникам. Если вы хотите помогать Мы всегда открыты для сотрудничества и&nbsp;рады единомышленникам. Если вы хотите помогать
журналу с&nbsp;редактурой, корректурой, иллюстрациями, переводами, версткой, подкастами, журналу с&nbsp;редактурой, корректурой, иллюстрациями, переводами, версткой, подкастами,
мероприятиями, фандрайзингом или как-то ещё&nbsp;&mdash; скорее пишите нам мероприятиями, фандрайзингом или как-то ещё&nbsp;&mdash; скорее пишите нам на&nbsp;
на&nbsp;<a href="mailto:welcome@discours.io">welcome@discours.io</a>. <a href="mailto:welcome@discours.io">welcome@discours.io</a>.
</p> </p>
<p> <p>
Если вы представляете некоммерческую организацию и&nbsp;хотите сделать с&nbsp;нами Если вы представляете некоммерческую организацию и&nbsp;хотите сделать с&nbsp;нами
@ -116,25 +120,26 @@ export const ManifestPage = () => {
</p> </p>
<p> <p>
Если вы разработчик и&nbsp;хотите помогать с&nbsp;развитием сайта Дискурса,{' '} Если вы разработчик и&nbsp;хотите помогать с&nbsp;развитием сайта Дискурса,{' '}
<a href="mailto:services@discours.io">присоединяйтесь к&nbsp;IT-команде самиздата</a>. Открытый <a href="mailto:services@discours.io">присоединяйтесь к&nbsp;IT-команде самиздата</a>.
код платформы для независимой журналистики, а&nbsp;также всех наших спецпроектов Открытый код платформы для независимой журналистики, а&nbsp;также всех наших спецпроектов
и&nbsp;медиаинструментов находится <a href="https://github.com/Discours">в&nbsp;свободном доступе на&nbsp;GitHub</a>. и&nbsp;медиаинструментов находится{' '}
<a href="https://github.com/Discours">в&nbsp;свободном доступе на&nbsp;GitHub</a>.
</p> </p>
<h3 id="follow">Как еще можно помочь</h3> <h3 id="follow">Как еще можно помочь</h3>
<p> <p>
Советуйте Дискурс друзьям и&nbsp;знакомым. Обсуждайте и&nbsp;распространяйте наши Советуйте Дискурс друзьям и&nbsp;знакомым. Обсуждайте и&nbsp;распространяйте наши
публикации&nbsp;&mdash; все материалы открытой редакции можно читать и&nbsp;перепечатывать публикации&nbsp;&mdash; все материалы открытой редакции можно читать и&nbsp;перепечатывать
бесплатно. Подпишитесь на&nbsp;самиздат{' '} бесплатно. Подпишитесь на&nbsp;самиздат <a href="https://vk.com/discoursio">ВКонтакте</a>,
<a href="https://vk.com/discoursio">ВКонтакте</a>,
в&nbsp;<a href="https://facebook.com/discoursio">Фейсбуке</a> в&nbsp;<a href="https://facebook.com/discoursio">Фейсбуке</a>
и&nbsp;в&nbsp;<a href="https://t.me/discoursio">Телеграме</a>, а&nbsp;также и&nbsp;в&nbsp;<a href="https://t.me/discoursio">Телеграме</a>, а&nbsp;также на&nbsp;
на&nbsp;<Opener name="subscribe">рассылку лучших материалов</Opener>, <Opener name="subscribe">рассылку лучших материалов</Opener>, чтобы не&nbsp;пропустить
чтобы не&nbsp;пропустить ничего интересного. ничего интересного.
</p> </p>
<p> <p>
<a href="https://forms.gle/9UnHBAz9Q3tjH5dAA">Рассказывайте о&nbsp;впечатлениях</a> <a href="https://forms.gle/9UnHBAz9Q3tjH5dAA">Рассказывайте о&nbsp;впечатлениях</a>
от&nbsp;материалов открытой редакции, <Opener name="feedback">делитесь идеями</Opener>, от&nbsp;материалов открытой редакции, <Opener name="feedback">делитесь идеями</Opener>,
интересными темами, о&nbsp;которых хотели бы узнать больше, и&nbsp;историями, которые нужно рассказать. интересными темами, о&nbsp;которых хотели бы узнать больше, и&nbsp;историями, которые нужно
рассказать.
</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">
Если вы хотите предложить материал, сотрудничать, рассказать о&nbsp;проблеме, которую нужно Если вы хотите предложить материал, сотрудничать, рассказать о&nbsp;проблеме, которую нужно
осветить, сообщить об&nbsp;ошибке или баге, что-то обсудить, уточнить или посоветовать, осветить, сообщить об&nbsp;ошибке или баге, что-то обсудить, уточнить или посоветовать,
пожалуйста, <Opener name="feedback">напишите нам здесь</Opener> или пожалуйста, <Opener name="feedback">напишите нам здесь</Opener> или на&nbsp;почту{' '}
на&nbsp;почту <a href="mailto:welcome@discours.io">welcome@discours.io</a>. Мы обязательно <a href="mailto:welcome@discours.io">welcome@discours.io</a>. Мы обязательно ответим
ответим и&nbsp;постараемся реализовать все хорошие задумки. и&nbsp;постараемся реализовать все хорошие задумки.
</div> </div>
</div> </div>
</div> </div>

View File

@ -47,7 +47,6 @@ 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'))
type RootSearchParams = { type RootSearchParams = {
modal: string modal: string
lang: string lang: string

View File

@ -1,6 +1,5 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
mutation CreateShoutMutations($shout: ShoutInput!) { mutation CreateShoutMutations($shout: ShoutInput!) {
createShout(input: $shout) { createShout(input: $shout) {

View File

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

View File

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

View File

@ -13,7 +13,7 @@
--danger-color: #fc6847; --danger-color: #fc6847;
--lightgray-color: rgb(84 16 17 / 6%); --lightgray-color: rgb(84 16 17 / 6%);
--font: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, oxygen, ubuntu, cantarell, 'Open Sans', --font: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, oxygen, ubuntu, cantarell, 'Open Sans',
'Helvetica Neue', sans-serif; 'Helvetica Neue', sans-serif;
} }
* { * {