Merge remote-tracking branch 'origin/dev' into prepare-inbox
# Conflicts: # src/components/Nav/AuthModal/ForgotPasswordForm.tsx # src/graphql/types.gen.ts # src/stores/auth.ts # src/utils/apiClient.ts # src/utils/config.ts
This commit is contained in:
commit
16dfe9d57b
|
@ -107,7 +107,6 @@
|
||||||
"prosemirror-markdown": "^1.9.4",
|
"prosemirror-markdown": "^1.9.4",
|
||||||
"prosemirror-menu": "^1.2.1",
|
"prosemirror-menu": "^1.2.1",
|
||||||
"prosemirror-model": "^1.16.0",
|
"prosemirror-model": "^1.16.0",
|
||||||
"prosemirror-schema-basic": "^1.2.0",
|
|
||||||
"prosemirror-schema-list": "^1.2.2",
|
"prosemirror-schema-list": "^1.2.2",
|
||||||
"prosemirror-state": "^1.4.1",
|
"prosemirror-state": "^1.4.1",
|
||||||
"prosemirror-view": "^1.28.1",
|
"prosemirror-view": "^1.28.1",
|
||||||
|
|
3
public/icons/content-index-control-expanded.svg
Normal file
3
public/icons/content-index-control-expanded.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 8V5H0V3H10V0L14 4L10 8Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 154 B |
4
public/icons/content-index-control.svg
Normal file
4
public/icons/content-index-control.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 9.24437C6 8.55582 6.57935 8 7.29706 8C8.01477 8 8.59412 8.55582 8.59412 9.24437C8.59412 9.93293 8.01477 10.4887 7.29706 10.4887C6.57935 10.4887 6 9.93293 6 9.24437ZM6 14.222C6 13.5334 6.57935 12.9776 7.29706 12.9776C8.01477 12.9776 8.59412 13.5334 8.59412 14.222C8.59412 14.9105 8.01477 15.4663 7.29706 15.4663C6.57935 15.4663 6 14.9105 6 14.222ZM7.29706 17.9548C6.57935 17.9548 6 18.5189 6 19.1991C6 19.8794 6.588 20.4435 7.29706 20.4435C8.00612 20.4435 8.59412 19.8794 8.59412 19.1991C8.59412 18.5189 8.01477 17.9548 7.29706 17.9548ZM22.0015 20.0284H9.89562V18.3692H22.0015V20.0284ZM9.89562 15.0517H22.0015V13.3925H9.89562V15.0517ZM9.89562 10.0741V8.41491H22.0015V10.0741H9.89562Z" fill="black"/>
|
||||||
|
<rect x="1" y="1" width="26" height="26" stroke="black" stroke-width="2"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 929 B |
2
public/icons/expand.svg
Normal file
2
public/icons/expand.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<svg width="20" height="13" viewBox="0 0 20 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 12.8999L0.100098 3L2.57507 0.525024L10.0002 7.95013L17.4246 0.525716L19.8996 3.00069L12.4747 10.4256L12.4745 10.4254L10 12.8999Z" fill="#000"/></svg>
|
After Width: | Height: | Size: 259 B |
|
@ -7,10 +7,10 @@ import { createMemo, For, onMount, Show } from 'solid-js'
|
||||||
import type { Author, Reaction, Shout } from '../../graphql/types.gen'
|
import type { Author, Reaction, Shout } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { showModal } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
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'
|
import { SharePopup } from './SharePopup'
|
||||||
|
import { useSession } from '../../context/session'
|
||||||
|
|
||||||
const MAX_COMMENT_LEVEL = 6
|
const MAX_COMMENT_LEVEL = 6
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ const formatDate = (date: Date) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FullArticle = (props: ArticleProps) => {
|
export const FullArticle = (props: ArticleProps) => {
|
||||||
const { session } = useAuthStore()
|
const { session } = useSession()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
incrementView({ articleSlug: props.article.slug })
|
incrementView({ articleSlug: props.article.slug })
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import styles from '../Nav/Popup.module.scss'
|
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { Popup, PopupProps } from '../Nav/Popup'
|
|
||||||
|
import styles from '../_shared/Popup.module.scss'
|
||||||
|
import type { PopupProps } from '../_shared/Popup'
|
||||||
|
import { Popup } from '../_shared/Popup'
|
||||||
|
|
||||||
type SharePopupProps = Omit<PopupProps, 'children'>
|
type SharePopupProps = Omit<PopupProps, 'children'>
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,27 @@
|
||||||
.authorDetails {
|
.authorDetails {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-right: 1.2rem;
|
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
|
||||||
|
// padding-right: 1.2rem;
|
||||||
|
|
||||||
|
@include media-breakpoint-down(sm) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorDetailsWrapper {
|
.authorDetailsWrapper {
|
||||||
|
flex: 1 100%;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
padding-right: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.authorName {
|
.authorName {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
font-size: 1.7rem;
|
font-size: 1.7rem;
|
||||||
|
@ -33,15 +46,13 @@
|
||||||
.authorAbout {
|
.authorAbout {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: rgb(0 0 0 / 60%);
|
color: rgb(0 0 0 / 60%);
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorSubscribe {
|
.authorSubscribe {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@include media-breakpoint-down(lg) {
|
|
||||||
padding: 0 0 0 42px;
|
padding: 0 0 0 42px;
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
|
@ -118,6 +129,10 @@
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,3 +236,23 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.authorsListItem {
|
||||||
|
.authorName {
|
||||||
|
@include font-size(2.2rem);
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorSubscribe {
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
@include media-breakpoint-down(sm) {
|
||||||
|
padding: 1rem 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonLabel {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
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 styles from './Card.module.scss'
|
||||||
import { createMemo, For, Show } 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 { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
import { follow, unfollow } from '../../stores/zine/common'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { useSession } from '../../context/session'
|
||||||
|
|
||||||
interface AuthorCardProps {
|
interface AuthorCardProps {
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
|
@ -19,10 +19,11 @@ interface AuthorCardProps {
|
||||||
author: Author
|
author: Author
|
||||||
isAuthorPage?: boolean
|
isAuthorPage?: boolean
|
||||||
noSocialButtons?: boolean
|
noSocialButtons?: boolean
|
||||||
|
isAuthorsList?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthorCard = (props: AuthorCardProps) => {
|
export const AuthorCard = (props: AuthorCardProps) => {
|
||||||
const { session } = useAuthStore()
|
const { session } = useSession()
|
||||||
|
|
||||||
const subscribed = createMemo<boolean>(
|
const subscribed = createMemo<boolean>(
|
||||||
() => session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
() => session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
||||||
|
@ -36,51 +37,81 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
||||||
}
|
}
|
||||||
// TODO: reimplement AuthorCard
|
// TODO: reimplement AuthorCard
|
||||||
return (
|
return (
|
||||||
<div class={style.author} classList={{ [style.authorPage]: props.isAuthorPage }}>
|
<div
|
||||||
<Userpic user={props.author} hasLink={props.hasLink} isBig={props.isAuthorPage} />
|
class={clsx(styles.author)}
|
||||||
|
classList={{
|
||||||
|
[styles.authorPage]: props.isAuthorPage,
|
||||||
|
[styles.authorsListItem]: props.isAuthorsList
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Userpic
|
||||||
|
user={props.author}
|
||||||
|
hasLink={props.hasLink}
|
||||||
|
isBig={props.isAuthorPage}
|
||||||
|
isAuthorsList={props.isAuthorsList}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class={style.authorDetails}>
|
<div class={styles.authorDetails}>
|
||||||
<div class={style.authorDetailsWrapper}>
|
<div class={styles.authorDetailsWrapper}>
|
||||||
<Show when={props.hasLink}>
|
<Show when={props.hasLink}>
|
||||||
<a class={style.authorName} href={`/author/${props.author.slug}`}>
|
<a class={styles.authorName} href={`/author/${props.author.slug}`}>
|
||||||
{name()}
|
{name()}
|
||||||
</a>
|
</a>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!props.hasLink}>
|
<Show when={!props.hasLink}>
|
||||||
<div class={style.authorName}>{name()}</div>
|
<div class={styles.authorName}>{name()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!props.hideDescription}>
|
<Show when={!props.hideDescription}>
|
||||||
<div class={style.authorAbout}>{bio()}</div>
|
<div class={styles.authorAbout} classList={{ 'text-truncate': props.isAuthorsList }}>
|
||||||
|
{bio()}
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={canFollow()}>
|
<Show when={canFollow()}>
|
||||||
<div class={style.authorSubscribe}>
|
<div class={styles.authorSubscribe}>
|
||||||
<Show
|
<Show
|
||||||
when={subscribed()}
|
when={subscribed()}
|
||||||
fallback={
|
fallback={
|
||||||
<button
|
<button
|
||||||
onClick={() => follow}
|
onClick={() => follow}
|
||||||
class={clsx('button button--subscribe', style.button, style.buttonSubscribe)}
|
class={clsx('button', styles.button)}
|
||||||
|
classList={{
|
||||||
|
[styles.buttonSubscribe]: !props.isAuthorsList,
|
||||||
|
'button--subscribe': !props.isAuthorsList,
|
||||||
|
'button--subscribe-topic': props.isAuthorsList,
|
||||||
|
[styles.buttonWrite]: props.isAuthorsList
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="author-subscribe" class={style.icon} />
|
<Show when={!props.isAuthorsList}>
|
||||||
<span class={style.buttonLabel}> {t('Follow')}</span>
|
<Icon name="author-subscribe" class={styles.icon} />
|
||||||
|
|
||||||
|
</Show>
|
||||||
|
<span class={styles.buttonLabel}>{t('Follow')}</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => unfollow}
|
onClick={() => unfollow}
|
||||||
class={clsx('button button--subscribe', style.button, style.buttonSubscribe)}
|
classList={{
|
||||||
|
[styles.buttonSubscribe]: !props.isAuthorsList,
|
||||||
|
'button--subscribe': !props.isAuthorsList,
|
||||||
|
'button--subscribe-topic': props.isAuthorsList,
|
||||||
|
[styles.buttonWrite]: props.isAuthorsList
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="author-unsubscribe" class={style.icon} />
|
<Show when={!props.isAuthorsList}>
|
||||||
<span class={style.buttonLabel}>- {t('Unfollow')}</span>
|
<Icon name="author-unsubscribe" class={styles.icon} />
|
||||||
|
|
||||||
|
</Show>
|
||||||
|
<span class={styles.buttonLabel}>{t('Unfollow')}</span>
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!props.compact}>
|
<Show when={!props.compact && !props.isAuthorsList}>
|
||||||
<button class={clsx(style.buttonWrite, style.button, 'button button--subscribe-topic')}>
|
<button class={clsx(styles.buttonWrite, styles.button, 'button button--subscribe-topic')}>
|
||||||
<Icon name="edit" class={style.icon} />
|
<Icon name="edit" class={styles.icon} />
|
||||||
{t('Write')}
|
{t('Write')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
border: 2px solid black;
|
border: 2px solid black;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 32px;
|
line-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.anonymous {
|
.anonymous {
|
||||||
|
@ -47,3 +47,15 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.authorsList {
|
||||||
|
margin-right: 2.4rem;
|
||||||
|
max-height: 6.8rem;
|
||||||
|
min-width: 6.8rem;
|
||||||
|
height: 6.8rem;
|
||||||
|
width: 6.8rem;
|
||||||
|
|
||||||
|
.userpic {
|
||||||
|
line-height: 6.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Show } from 'solid-js'
|
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 styles from './Userpic.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
interface UserpicProps {
|
interface UserpicProps {
|
||||||
|
@ -8,6 +8,7 @@ interface UserpicProps {
|
||||||
hasLink?: boolean
|
hasLink?: boolean
|
||||||
isBig?: boolean
|
isBig?: boolean
|
||||||
class?: string
|
class?: string
|
||||||
|
isAuthorsList?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props: UserpicProps) => {
|
export default (props: UserpicProps) => {
|
||||||
|
@ -18,7 +19,13 @@ export default (props: UserpicProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(style.circlewrap, props.class)} classList={{ [style.big]: props.isBig }}>
|
<div
|
||||||
|
class={clsx(styles.circlewrap, props.class)}
|
||||||
|
classList={{
|
||||||
|
[styles.big]: props.isBig,
|
||||||
|
[styles.authorsList]: props.isAuthorsList
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Show when={props.hasLink}>
|
<Show when={props.hasLink}>
|
||||||
<a href={`/author/${props.user.slug}`}>
|
<a href={`/author/${props.user.slug}`}>
|
||||||
<Show
|
<Show
|
||||||
|
@ -31,7 +38,7 @@ export default (props: UserpicProps) => {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class={style.userpic}>{letters()}</div>
|
<div class={styles.userpic}>{letters()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
</a>
|
</a>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -47,7 +54,7 @@ export default (props: UserpicProps) => {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class={style.userpic}>{letters()}</div>
|
<div class={styles.userpic}>{letters()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,6 +39,11 @@
|
||||||
padding: 0 $container-padding-x;
|
padding: 0 $container-padding-x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.discoursFooterContent {
|
.discoursFooterContent {
|
||||||
|
@ -56,7 +61,7 @@
|
||||||
padding-top: 1.6rem;
|
padding-top: 1.6rem;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgb(255 255 255 / 70%);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
|
@ -19,6 +19,11 @@
|
||||||
.wide-container {
|
.wide-container {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-discours__actions {
|
.about-discours__actions {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createSignal, onMount } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
import styles from './Subscribe.module.scss'
|
import styles from './Subscribe.module.scss'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { clsx } from 'clsx'
|
||||||
import styles from './Sidebar.module.scss'
|
import styles from './Sidebar.module.scss'
|
||||||
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||||
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
|
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
|
||||||
import { hideModal } from '../../../stores/ui'
|
|
||||||
|
|
||||||
const Off = (props) => <div class={styles.sidebarOff}>{props.children}</div>
|
const Off = (props) => <div class={styles.sidebarOff}>{props.children}</div>
|
||||||
|
|
79
src/components/EditorNew/Editor.tsx
Normal file
79
src/components/EditorNew/Editor.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { onMount } from 'solid-js'
|
||||||
|
import { EditorState, Transaction } from 'prosemirror-state'
|
||||||
|
import { EditorView, MarkViewConstructor, NodeViewConstructor } from 'prosemirror-view'
|
||||||
|
import './prosemirror/styles/ProseMirror.scss'
|
||||||
|
import type { Nodes, Marks } from './prosemirror/schema'
|
||||||
|
import { createImageView } from './prosemirror/views/image'
|
||||||
|
import { schema } from './prosemirror/schema'
|
||||||
|
import { createPlugins } from './prosemirror/plugins'
|
||||||
|
import { DOMSerializer } from 'prosemirror-model'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import { createArticle } from '../../stores/zine/articles'
|
||||||
|
import type { ShoutInput } from '../../graphql/types.gen'
|
||||||
|
import { Sidebar } from './Sidebar'
|
||||||
|
|
||||||
|
const htmlContainer = typeof document === 'undefined' ? null : document.createElement('div')
|
||||||
|
|
||||||
|
const getHtml = (state: EditorState) => {
|
||||||
|
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(state.doc.content)
|
||||||
|
htmlContainer.replaceChildren(fragment)
|
||||||
|
return htmlContainer.innerHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Editor = () => {
|
||||||
|
const editorElRef: {
|
||||||
|
current: HTMLDivElement
|
||||||
|
} = {
|
||||||
|
current: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorViewRef: { current: EditorView } = { current: null }
|
||||||
|
|
||||||
|
const dispatchTransaction = (tr: Transaction) => {
|
||||||
|
const newState = editorViewRef.current.state.apply(tr)
|
||||||
|
editorViewRef.current.updateState(newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const plugins = createPlugins({ schema })
|
||||||
|
|
||||||
|
const nodeViews: Partial<Record<Nodes, NodeViewConstructor>> = {
|
||||||
|
image: createImageView
|
||||||
|
}
|
||||||
|
|
||||||
|
const markViews: Partial<Record<Marks, MarkViewConstructor>> = {}
|
||||||
|
|
||||||
|
editorViewRef.current = new EditorView(editorElRef.current, {
|
||||||
|
state: EditorState.create({
|
||||||
|
schema,
|
||||||
|
plugins
|
||||||
|
}),
|
||||||
|
nodeViews,
|
||||||
|
markViews,
|
||||||
|
dispatchTransaction
|
||||||
|
})
|
||||||
|
|
||||||
|
editorViewRef.current.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSaveButtonClick = () => {
|
||||||
|
const article: ShoutInput = {
|
||||||
|
body: getHtml(editorViewRef.current.state),
|
||||||
|
community: 'discours', // ?
|
||||||
|
slug: 'new-' + Math.floor(Math.random() * 1000000)
|
||||||
|
}
|
||||||
|
createArticle({ article })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="container" style={{ display: 'flex' }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div ref={(el) => (editorElRef.current = el)} />
|
||||||
|
<button class={clsx('button')} onClick={handleSaveButtonClick}>
|
||||||
|
Опубликовать WIP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Sidebar editorViewRef={editorViewRef} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
221
src/components/EditorNew/Sidebar.module.scss
Normal file
221
src/components/EditorNew/Sidebar.module.scss
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
.sidebarContainer {
|
||||||
|
color: rgb(255 255 255 / 50%);
|
||||||
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
@include font-size(120%);
|
||||||
|
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
height: auto;
|
||||||
|
min-height: 50px;
|
||||||
|
padding: 0 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarOff {
|
||||||
|
background: #1f1f1f;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 40px 20px 20px;
|
||||||
|
top: 0;
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
width: 350px;
|
||||||
|
|
||||||
|
.sidebarContainerHidden & {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarOpener {
|
||||||
|
color: #000;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 1em;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::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");
|
||||||
|
content: '';
|
||||||
|
height: 18px;
|
||||||
|
left: 100%;
|
||||||
|
margin-left: 0.3em;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarCloser {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.1517 0.423857L0.42375 13.1518L2.84812 15.5761L15.576 2.84822L13.1517 0.423857Z M15.576 13.1518L2.84812 0.423855L0.423751 2.84822L13.1517 15.5761L15.576 13.1518Z' fill='white'/%3E%3C/svg%3E%0A");
|
||||||
|
cursor: pointer;
|
||||||
|
height: 16px;
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
top: 20px;
|
||||||
|
width: 16px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarLabel {
|
||||||
|
color: var(--foreground);
|
||||||
|
|
||||||
|
> i {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarContainer button,
|
||||||
|
.sidebarContainer a,
|
||||||
|
.sidebarItem {
|
||||||
|
margin: 0;
|
||||||
|
outline: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 24px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarContainer a,
|
||||||
|
.sidebarItem {
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 2px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarLink {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: inherit;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
> span i {
|
||||||
|
position: relative;
|
||||||
|
box-shadow: none;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
color: var(--foreground);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.draft {
|
||||||
|
color: rgb(255 255 255 / 50%);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0 0 1em 1.5em;
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
justify-self: flex-end;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
> i {
|
||||||
|
border: 1px solid;
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
display: inline-block;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0 0.5em 0 0;
|
||||||
|
padding: 1px 4px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.themeSwitcher {
|
||||||
|
border-bottom: 1px solid rgb(255 255 255 / 30%);
|
||||||
|
border-top: 1px solid rgb(255 255 255 / 30%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 1em 0;
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
+ 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,
|
||||||
|
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;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 10em;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
width: 46px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 100%;
|
||||||
|
content: '';
|
||||||
|
height: 16px;
|
||||||
|
left: 6px;
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
transition: left 0.3s, color 0.3s;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + label {
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: #1f1f1f;
|
||||||
|
left: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
113
src/components/EditorNew/Sidebar.tsx
Normal file
113
src/components/EditorNew/Sidebar.tsx
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import { For, createEffect, createSignal, onCleanup, onMount } from 'solid-js'
|
||||||
|
import type { JSX } from 'solid-js'
|
||||||
|
import { undo, redo } from 'prosemirror-history'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import styles from './Sidebar.module.scss'
|
||||||
|
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
|
||||||
|
import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
|
||||||
|
import type { EditorView } from 'prosemirror-view'
|
||||||
|
|
||||||
|
const Off = (props) => <div class={styles.sidebarOff}>{props.children}</div>
|
||||||
|
|
||||||
|
const Link = (props: {
|
||||||
|
withMargin?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
title?: string
|
||||||
|
className?: string
|
||||||
|
children: JSX.Element
|
||||||
|
onClick?: () => void
|
||||||
|
}) => (
|
||||||
|
<button
|
||||||
|
class={clsx(styles.sidebarLink, props.className, {
|
||||||
|
[styles.withMargin]: props.withMargin
|
||||||
|
})}
|
||||||
|
onClick={props.onClick}
|
||||||
|
disabled={props.disabled}
|
||||||
|
title={props.title}
|
||||||
|
data-testid={props['data-testid']}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
const Keys = (props: { keys: string[] }) => (
|
||||||
|
<span>
|
||||||
|
<For each={props.keys}>{(k) => <i>{k}</i>}</For>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
type SidebarProps = {
|
||||||
|
editorViewRef: {
|
||||||
|
current: EditorView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar = (props: SidebarProps) => {
|
||||||
|
const [lastAction, setLastAction] = createSignal<string | undefined>()
|
||||||
|
|
||||||
|
const { editorViewRef } = props
|
||||||
|
|
||||||
|
const onUndo = () => undo(editorViewRef.current.state, editorViewRef.current.dispatch)
|
||||||
|
const onRedo = () => redo(editorViewRef.current.state, editorViewRef.current.dispatch)
|
||||||
|
|
||||||
|
const [isHidden, setIsHidden] = createSignal(true)
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
setIsHidden((oldIsHidden) => !oldIsHidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
setLastAction()
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!lastAction()) return
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
setLastAction()
|
||||||
|
}, 1000)
|
||||||
|
onCleanup(() => clearTimeout(id))
|
||||||
|
})
|
||||||
|
|
||||||
|
const [mod, setMod] = createSignal<'Ctrl' | 'Cmd'>('Ctrl')
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setMod(navigator.platform.includes('Mac') ? 'Cmd' : 'Ctrl')
|
||||||
|
})
|
||||||
|
|
||||||
|
const containerRef: { current: HTMLElement } = {
|
||||||
|
current: null
|
||||||
|
}
|
||||||
|
|
||||||
|
useEscKeyDownHandler(() => setIsHidden(true))
|
||||||
|
useOutsideClickHandler({
|
||||||
|
containerRef,
|
||||||
|
predicate: () => !isHidden(),
|
||||||
|
handler: () => setIsHidden(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={clsx(styles.sidebarContainer, {
|
||||||
|
[styles.sidebarContainerHidden]: isHidden()
|
||||||
|
})}
|
||||||
|
ref={(el) => (containerRef.current = el)}
|
||||||
|
>
|
||||||
|
<span class={styles.sidebarOpener} onClick={toggleSidebar}>
|
||||||
|
Советы и предложения
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Off onClick={() => editorViewRef.current.focus()}>
|
||||||
|
<div class={styles.sidebarCloser} onClick={toggleSidebar} />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Link onClick={onUndo}>
|
||||||
|
Undo <Keys keys={[mod(), 'z']} />
|
||||||
|
</Link>
|
||||||
|
<Link onClick={onRedo}>
|
||||||
|
Redo <Keys keys={[mod(), 'Shift', 'z']} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Off>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
215
src/components/EditorNew/prosemirror/helpers/menu.ts
Normal file
215
src/components/EditorNew/prosemirror/helpers/menu.ts
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
import { toggleMark } from 'prosemirror-commands'
|
||||||
|
import { wrapInList } from 'prosemirror-schema-list'
|
||||||
|
import { blockTypeItem, icons, MenuItem, wrapItem, Dropdown } from 'prosemirror-menu'
|
||||||
|
|
||||||
|
import type { NodeSelection } from 'prosemirror-state'
|
||||||
|
|
||||||
|
import { TextField, openPrompt } from './prompt'
|
||||||
|
|
||||||
|
import type { DiscoursSchema } from '../schema'
|
||||||
|
|
||||||
|
function wrapListItem(nodeType, options) {
|
||||||
|
return cmdItem(wrapInList(nodeType, options.attrs), options)
|
||||||
|
}
|
||||||
|
|
||||||
|
function canInsert(state, nodeType) {
|
||||||
|
const $from = state.selection.$from
|
||||||
|
|
||||||
|
for (let d = $from.depth; d >= 0; d--) {
|
||||||
|
const index = $from.index(d)
|
||||||
|
|
||||||
|
if ($from.node(d).canReplaceWith(index, index, nodeType)) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertImageItem(nodeType) {
|
||||||
|
return new MenuItem({
|
||||||
|
icon: icons.image,
|
||||||
|
label: 'image',
|
||||||
|
enable(state) {
|
||||||
|
return canInsert(state, nodeType)
|
||||||
|
},
|
||||||
|
run(state, _, view) {
|
||||||
|
const {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
node: { attrs }
|
||||||
|
} = state.selection as NodeSelection
|
||||||
|
|
||||||
|
openPrompt({
|
||||||
|
title: 'Insert image',
|
||||||
|
fields: {
|
||||||
|
src: new TextField({
|
||||||
|
label: 'Location',
|
||||||
|
required: true,
|
||||||
|
value: attrs && attrs.src
|
||||||
|
}),
|
||||||
|
title: new TextField({ label: 'Title', value: attrs && attrs.title }),
|
||||||
|
alt: new TextField({
|
||||||
|
label: 'Description',
|
||||||
|
value: attrs ? attrs.alt : state.doc.textBetween(from, to, ' ')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSubmit(newAttrs) {
|
||||||
|
view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(newAttrs)))
|
||||||
|
view.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmdItem(cmd, options) {
|
||||||
|
const passedOptions = {
|
||||||
|
label: options.title,
|
||||||
|
run: cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MenuItem(passedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
function markActive(state, type) {
|
||||||
|
const { from, $from, to, empty } = state.selection
|
||||||
|
|
||||||
|
if (empty) return type.isInSet(state.storedMarks || $from.marks())
|
||||||
|
|
||||||
|
return state.doc.rangeHasMark(from, to, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
function markItem(markType, options) {
|
||||||
|
const passedOptions = {
|
||||||
|
active(state) {
|
||||||
|
return markActive(state, markType)
|
||||||
|
},
|
||||||
|
enable: true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const prop in options) passedOptions[prop] = options[prop]
|
||||||
|
|
||||||
|
return cmdItem(toggleMark(markType), passedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkItem(markType) {
|
||||||
|
return new MenuItem({
|
||||||
|
title: 'Add or remove link',
|
||||||
|
icon: {
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
path: 'M3.27177 14.7277C2.06258 13.5186 2.06258 11.5527 3.27177 10.3435L6.10029 7.51502L4.75675 6.17148L1.92823 9C-0.0234511 10.9517 -0.0234511 14.1196 1.92823 16.0713C3.87991 18.023 7.04785 18.023 8.99952 16.0713L11.828 13.2428L10.4845 11.8992L7.65598 14.7277C6.44679 15.9369 4.48097 15.9369 3.27177 14.7277ZM6.87756 12.536L12.5346 6.87895L11.1203 5.46469L5.4633 11.1217L6.87756 12.536ZM6.17055 4.75768L8.99907 1.92916C10.9507 -0.0225206 14.1187 -0.0225201 16.0704 1.92916C18.022 3.88084 18.022 7.04878 16.0704 9.00046L13.2418 11.829L11.8983 10.4854L14.7268 7.65691C15.936 6.44772 15.936 4.4819 14.7268 3.27271C13.5176 2.06351 11.5518 2.06351 10.3426 3.2727L7.51409 6.10122L6.17055 4.75768Z'
|
||||||
|
},
|
||||||
|
active(state) {
|
||||||
|
return markActive(state, markType)
|
||||||
|
},
|
||||||
|
enable(state) {
|
||||||
|
return !state.selection.empty
|
||||||
|
},
|
||||||
|
run(state, dispatch, view) {
|
||||||
|
if (markActive(state, markType)) {
|
||||||
|
toggleMark(markType)(state, dispatch)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
openPrompt({
|
||||||
|
fields: {
|
||||||
|
href: new TextField({
|
||||||
|
label: 'Link target',
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSubmit(attrs) {
|
||||||
|
toggleMark(markType, attrs)(view.state, view.dispatch)
|
||||||
|
view.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildMenuItems = (schema: DiscoursSchema) => {
|
||||||
|
const toggleStrong = markItem(schema.marks.strong, {
|
||||||
|
title: 'Toggle strong style',
|
||||||
|
icon: {
|
||||||
|
width: 13,
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleEm = markItem(schema.marks.em, {
|
||||||
|
title: 'Toggle emphasis',
|
||||||
|
icon: {
|
||||||
|
width: 14,
|
||||||
|
height: 16,
|
||||||
|
path: 'M4.39216 0V3.42857H6.81882L3.06353 12.5714H0V16H8.78431V12.5714H6.35765L10.1129 3.42857H13.1765V0H4.39216Z'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleLink = linkItem(schema.marks.link)
|
||||||
|
|
||||||
|
const insertImage = insertImageItem(schema.nodes.image)
|
||||||
|
|
||||||
|
const wrapBlockQuote = wrapItem(schema.nodes.blockquote, {
|
||||||
|
title: 'Wrap in block quote',
|
||||||
|
icon: icons.blockquote
|
||||||
|
})
|
||||||
|
|
||||||
|
const headingIcons = [
|
||||||
|
'M0 12H2.57143V7.16571H7.95429V12H10.5257V0H7.95429V4.83429H2.57143V0H0V12Z M12.6801 12H19.3315V9.78857H17.3944V0.342858H15.5087L12.6801 1.42286V3.75429L14.8744 2.93143V9.78857H12.6801V12Z',
|
||||||
|
'M0 12H2.57143V7.16571H7.95429V12H10.5257V0H7.95429V4.83429H2.57143V0H0V12Z M12.4915 12H21.2515V9.78857H15.4229C15.4229 9.05143 16.6229 8.43429 17.9944 7.59429C19.5372 6.68571 21.1658 5.52 21.1658 3.54857C21.1658 1.16571 19.2458 0.102858 16.8972 0.102858C15.4744 0.102858 14.0858 0.48 12.8858 1.33714V3.73714C14.1201 2.79429 15.4915 2.36571 16.6744 2.36571C17.8229 2.36571 18.5772 2.79429 18.5772 3.65143C18.5772 4.76571 17.5487 5.22857 16.3315 5.93143C14.6172 6.94286 12.4915 8.02286 12.4915 10.8514V12Z',
|
||||||
|
'M0 11.7647H2.52101V7.02521H7.79832V11.7647H10.3193V0H7.79832V4.7395H2.52101V0H0V11.7647Z M16.3474 12C18.7004 12 20.9189 11.042 20.9189 8.63866C20.9189 6.95798 19.8936 6.06723 18.7172 5.71429C19.7928 5.34454 20.4483 4.43697 20.4483 3.2605C20.4483 1.17647 18.6836 0.100841 16.3138 0.100841C14.9189 0.100841 13.6079 0.436975 12.5827 0.991597V3.34454C13.7088 2.63865 14.9357 2.31933 15.9609 2.31933C17.339 2.31933 18.0617 2.78992 18.0617 3.61345C18.0617 4.40336 17.3558 4.82353 16.2466 4.80672L14.6668 4.78992L14.6499 6.97479H16.5323C17.6752 6.97479 18.5155 7.31092 18.5155 8.28571C18.5155 9.36134 17.4399 9.7647 16.1457 9.78151C14.8348 9.79832 13.692 9.59664 12.381 8.87395V11.2269C13.692 11.7647 14.8852 12 16.3474 12Z'
|
||||||
|
]
|
||||||
|
|
||||||
|
// 3 is the max heading level mb move to constant
|
||||||
|
const headings: MenuItem[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
headings.push(
|
||||||
|
blockTypeItem(schema.nodes.heading, {
|
||||||
|
label: `H${i + 1}`,
|
||||||
|
attrs: { level: i + 1 },
|
||||||
|
icon: {
|
||||||
|
width: 22,
|
||||||
|
height: 12,
|
||||||
|
path: headingIcons[i]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeMenu = new Dropdown([...headings, wrapBlockQuote], {
|
||||||
|
label: 'Тт',
|
||||||
|
class: 'editor-dropdown'
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapBulletList = wrapListItem(schema.nodes.bullet_list, {
|
||||||
|
title: 'Wrap in bullet list',
|
||||||
|
icon: {
|
||||||
|
width: 20,
|
||||||
|
height: 16,
|
||||||
|
path: 'M0.000114441 1.6C0.000114441 0.714665 0.71478 0 1.60011 0C2.48544 0 3.20011 0.714665 3.20011 1.6C3.20011 2.48533 2.48544 3.19999 1.60011 3.19999C0.71478 3.19999 0.000114441 2.48533 0.000114441 1.6ZM0 8.00013C0 7.1148 0.714665 6.40014 1.6 6.40014C2.48533 6.40014 3.19999 7.1148 3.19999 8.00013C3.19999 8.88547 2.48533 9.60013 1.6 9.60013C0.714665 9.60013 0 8.88547 0 8.00013ZM1.6 12.8C0.714665 12.8 0 13.5254 0 14.4C0 15.2747 0.725332 16 1.6 16C2.47466 16 3.19999 15.2747 3.19999 14.4C3.19999 13.5254 2.48533 12.8 1.6 12.8ZM19.7333 15.4662H4.79999V13.3329H19.7333V15.4662ZM4.79999 9.06677H19.7333V6.93344H4.79999V9.06677ZM4.79999 2.66664V0.533307H19.7333V2.66664H4.79999Z'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapOrderedList = wrapListItem(schema.nodes.ordered_list, {
|
||||||
|
title: 'Wrap in ordered list',
|
||||||
|
icon: {
|
||||||
|
width: 19,
|
||||||
|
height: 16,
|
||||||
|
path: 'M2.00002 4.00003H1.00001V1.00001H0V0H2.00002V4.00003ZM2.00002 13.5V13H0V12H3.00003V16H0V15H2.00002V14.5H1.00001V13.5H2.00002ZM0 6.99998H1.80002L0 9.1V10H3.00003V9H1.20001L3.00003 6.89998V5.99998H0V6.99998ZM4.9987 2.99967V0.999648H18.9988V2.99967H4.9987ZM4.9987 15.0001H18.9988V13.0001H4.9987V15.0001ZM18.9988 8.99987H4.9987V6.99986H18.9988V8.99987Z'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const listMenu = [wrapBulletList, wrapOrderedList]
|
||||||
|
const inlineMenu = [toggleStrong, toggleEm]
|
||||||
|
|
||||||
|
return [[typeMenu, ...inlineMenu, toggleLink, insertImage, ...listMenu]]
|
||||||
|
}
|
197
src/components/EditorNew/prosemirror/helpers/prompt.ts
Normal file
197
src/components/EditorNew/prosemirror/helpers/prompt.ts
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
const prefix = 'ProseMirror-prompt'
|
||||||
|
|
||||||
|
const createButton = ({
|
||||||
|
textContent,
|
||||||
|
type = 'button',
|
||||||
|
className,
|
||||||
|
onClick
|
||||||
|
}: {
|
||||||
|
textContent: string
|
||||||
|
type?: 'button' | 'submit'
|
||||||
|
className: string
|
||||||
|
onClick?: () => void
|
||||||
|
}) => {
|
||||||
|
const button = document.createElement('button')
|
||||||
|
button.type = type
|
||||||
|
button.className = className
|
||||||
|
button.textContent = textContent
|
||||||
|
|
||||||
|
if (onClick) {
|
||||||
|
button.addEventListener('click', onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
export function openPrompt(options: {
|
||||||
|
title?: string
|
||||||
|
fields: Record<string, Field>
|
||||||
|
onSubmit: (values: Record<string, string>) => void
|
||||||
|
}) {
|
||||||
|
const wrapper = document.body.appendChild(document.createElement('div'))
|
||||||
|
wrapper.className = prefix
|
||||||
|
|
||||||
|
const mouseOutside = (ev: MouseEvent & { target: Node }) => {
|
||||||
|
if (!wrapper.contains(ev.target)) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => window.addEventListener('mousedown', mouseOutside), 50)
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
window.removeEventListener('mousedown', mouseOutside)
|
||||||
|
if (wrapper.parentNode) wrapper.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
const domFields: HTMLElement[] = []
|
||||||
|
|
||||||
|
Object.keys(options.fields).forEach((name) => {
|
||||||
|
domFields.push(options.fields[name].render())
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitButton = createButton({ textContent: 'OK', type: 'submit', className: prefix + '-submit' })
|
||||||
|
const cancelButton = createButton({
|
||||||
|
className: prefix + '-cancel',
|
||||||
|
textContent: 'Cancel',
|
||||||
|
onClick: close
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = wrapper.appendChild(document.createElement('form'))
|
||||||
|
|
||||||
|
if (options.title) {
|
||||||
|
form.appendChild(document.createElement('h5')).textContent = options.title
|
||||||
|
}
|
||||||
|
|
||||||
|
domFields.forEach((fieldEl: HTMLElement) => {
|
||||||
|
form.appendChild(document.createElement('div')).appendChild(fieldEl)
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttons = form.appendChild(document.createElement('div'))
|
||||||
|
buttons.className = prefix + '-buttons'
|
||||||
|
buttons.appendChild(submitButton)
|
||||||
|
buttons.appendChild(document.createTextNode(' '))
|
||||||
|
buttons.appendChild(cancelButton)
|
||||||
|
|
||||||
|
const box = wrapper.getBoundingClientRect()
|
||||||
|
wrapper.style.top = (window.innerHeight - box.height) / 2 + 'px'
|
||||||
|
wrapper.style.left = (window.innerWidth - box.width) / 2 + 'px'
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
const values = getValues(options.fields, domFields)
|
||||||
|
if (values) {
|
||||||
|
close()
|
||||||
|
options.onSubmit(values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
submit()
|
||||||
|
})
|
||||||
|
|
||||||
|
form.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
close()
|
||||||
|
} else if (e.key === 'Enter' && !(e.ctrlKey || e.metaKey || e.shiftKey)) {
|
||||||
|
e.preventDefault()
|
||||||
|
submit()
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (!wrapper.contains(document.activeElement)) close()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
form.querySelector('input')?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValues(fields: Record<string, Field>, domFields: HTMLElement[]) {
|
||||||
|
const result = {}
|
||||||
|
|
||||||
|
// TODO: make field read its own value, maybe move to SolidJS
|
||||||
|
const fieldNames = Object.keys(fields)
|
||||||
|
|
||||||
|
for (const [i, fieldName] of fieldNames.entries()) {
|
||||||
|
const field = fields[fieldName]
|
||||||
|
|
||||||
|
const dom = domFields[i]
|
||||||
|
const value = field.read(dom)
|
||||||
|
const bad = field.validate(value)
|
||||||
|
|
||||||
|
if (bad) {
|
||||||
|
reportInvalid(dom, bad)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
result[fieldName] = field.clean(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportInvalid(dom: HTMLElement, message: string) {
|
||||||
|
const msg: HTMLElement = dom.parentNode.appendChild(document.createElement('div'))
|
||||||
|
msg.style.left = dom.offsetLeft + dom.offsetWidth + 2 + 'px'
|
||||||
|
msg.style.top = dom.offsetTop - 5 + 'px'
|
||||||
|
msg.className = 'ProseMirror-invalid'
|
||||||
|
msg.textContent = message
|
||||||
|
setTimeout(msg.remove, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Field {
|
||||||
|
options: any
|
||||||
|
|
||||||
|
constructor(options: any) {
|
||||||
|
this.options = options
|
||||||
|
}
|
||||||
|
|
||||||
|
read(dom: any) {
|
||||||
|
return dom.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// :: (any) → ?string
|
||||||
|
// A field-type-specific validation function.
|
||||||
|
validateType(_value) {
|
||||||
|
return typeof _value === typeof ''
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(value: any) {
|
||||||
|
if (!value && this.options.required) return 'Required field'
|
||||||
|
|
||||||
|
return this.validateType(value) || (this.options.validate && this.options.validate(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
clean(value: any) {
|
||||||
|
return this.options.clean ? this.options.clean(value) : value
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract render(): HTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextField extends Field {
|
||||||
|
render() {
|
||||||
|
const input: HTMLInputElement = document.createElement('input')
|
||||||
|
|
||||||
|
input.type = 'text'
|
||||||
|
input.placeholder = this.options.label
|
||||||
|
input.value = this.options.value || ''
|
||||||
|
input.autocomplete = 'off'
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SelectField extends Field {
|
||||||
|
render() {
|
||||||
|
const select = document.createElement('select')
|
||||||
|
this.options.options.forEach((o: { value: string; label: string }) => {
|
||||||
|
const opt = select.appendChild(document.createElement('option'))
|
||||||
|
opt.value = o.value
|
||||||
|
opt.selected = o.value === this.options.value
|
||||||
|
opt.label = o.label
|
||||||
|
})
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
}
|
18
src/components/EditorNew/prosemirror/plugins/customKeymap.ts
Normal file
18
src/components/EditorNew/prosemirror/plugins/customKeymap.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { baseKeymap } from 'prosemirror-commands'
|
||||||
|
import type { Command } from 'prosemirror-state'
|
||||||
|
import { redo, undo } from 'prosemirror-history'
|
||||||
|
import { keymap } from 'prosemirror-keymap'
|
||||||
|
|
||||||
|
export const customKeymap = () => {
|
||||||
|
const bindings: {
|
||||||
|
[key: string]: Command
|
||||||
|
} = {
|
||||||
|
...baseKeymap,
|
||||||
|
Tab: () => true,
|
||||||
|
// TODO: collab
|
||||||
|
[`Mod-z`]: undo,
|
||||||
|
[`Shift-Mod-z`]: redo
|
||||||
|
}
|
||||||
|
|
||||||
|
return keymap(bindings)
|
||||||
|
}
|
48
src/components/EditorNew/prosemirror/plugins/dragHandle.ts
Normal file
48
src/components/EditorNew/prosemirror/plugins/dragHandle.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { Plugin, NodeSelection } from 'prosemirror-state'
|
||||||
|
import { DecorationSet, Decoration } from 'prosemirror-view'
|
||||||
|
|
||||||
|
const handleIcon = `
|
||||||
|
<svg viewBox="0 0 10 10" height="14" width="14">
|
||||||
|
<path d="M3 2a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2zm4-8a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2z"/>
|
||||||
|
</svg>`
|
||||||
|
|
||||||
|
const createDragHandle = () => {
|
||||||
|
const handle = document.createElement('span')
|
||||||
|
handle.setAttribute('contenteditable', 'false')
|
||||||
|
const icon = document.createElement('span')
|
||||||
|
icon.innerHTML = handleIcon
|
||||||
|
handle.appendChild(icon)
|
||||||
|
handle.classList.add('handle')
|
||||||
|
return handle
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dragHandle = () =>
|
||||||
|
new Plugin({
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
const decos = []
|
||||||
|
state.doc.forEach((node, pos) => {
|
||||||
|
decos.push(
|
||||||
|
Decoration.widget(pos + 1, createDragHandle),
|
||||||
|
Decoration.node(pos, pos + node.nodeSize, { class: 'draggable' })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return DecorationSet.create(state.doc, decos)
|
||||||
|
},
|
||||||
|
handleDOMEvents: {
|
||||||
|
mousedown: (editorView, event: MouseEvent & { target: Element }) => {
|
||||||
|
const target = event.target
|
||||||
|
|
||||||
|
if (target.classList.contains('handle')) {
|
||||||
|
const pos = editorView.posAtCoords({ left: event.x, top: event.y })
|
||||||
|
const resolved = editorView.state.doc.resolve(pos.pos)
|
||||||
|
const tr = editorView.state.tr
|
||||||
|
tr.setSelection(NodeSelection.create(editorView.state.doc, resolved.before()))
|
||||||
|
editorView.dispatch(tr)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
50
src/components/EditorNew/prosemirror/plugins/image.ts
Normal file
50
src/components/EditorNew/prosemirror/plugins/image.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { Plugin } from 'prosemirror-state'
|
||||||
|
import type { DiscoursSchema } from '../schema'
|
||||||
|
|
||||||
|
const REGEX = /^!\[([^[\]]*?)]\((.+?)\)\s+/
|
||||||
|
const MAX_MATCH = 500
|
||||||
|
|
||||||
|
const isUrl = (str: string) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(str)
|
||||||
|
return url.protocol === 'http:' || url.protocol === 'https:'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBlank = (text: string) => text === ' ' || text === '\u00A0'
|
||||||
|
|
||||||
|
export const imageInput = (schema: DiscoursSchema) =>
|
||||||
|
new Plugin({
|
||||||
|
props: {
|
||||||
|
handleTextInput(view, from, to, text) {
|
||||||
|
if (view.composing || !isBlank(text)) return false
|
||||||
|
const $from = view.state.doc.resolve(from)
|
||||||
|
if ($from.parent.type.spec.code) return false
|
||||||
|
const textBefore =
|
||||||
|
$from.parent.textBetween(
|
||||||
|
Math.max(0, $from.parentOffset - MAX_MATCH),
|
||||||
|
$from.parentOffset,
|
||||||
|
null,
|
||||||
|
'\uFFFC'
|
||||||
|
) + text
|
||||||
|
|
||||||
|
const match = REGEX.exec(textBefore)
|
||||||
|
if (match) {
|
||||||
|
const [, title, src] = match
|
||||||
|
if (isUrl(src)) {
|
||||||
|
const node = schema.node('image', { src, title })
|
||||||
|
const start = from - (match[0].length - text.length)
|
||||||
|
const tr = view.state.tr
|
||||||
|
tr.delete(start, to)
|
||||||
|
tr.insert(start, node)
|
||||||
|
view.dispatch(tr)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
24
src/components/EditorNew/prosemirror/plugins/index.ts
Normal file
24
src/components/EditorNew/prosemirror/plugins/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { history } from 'prosemirror-history'
|
||||||
|
import { dropCursor } from 'prosemirror-dropcursor'
|
||||||
|
import { placeholder } from './placeholder'
|
||||||
|
import { t } from '../../../../utils/intl'
|
||||||
|
import styles from '../styles/ProseMirror.module.scss'
|
||||||
|
import type { DiscoursSchema } from '../schema'
|
||||||
|
import { dragHandle } from './dragHandle'
|
||||||
|
import { selectionMenu } from './selectionMenu'
|
||||||
|
import { imageInput } from './image'
|
||||||
|
import { customKeymap } from './customKeymap'
|
||||||
|
|
||||||
|
export const createPlugins = ({ schema }: { schema: DiscoursSchema }) => {
|
||||||
|
return [
|
||||||
|
placeholder(t('Just start typing...')),
|
||||||
|
customKeymap(),
|
||||||
|
history(),
|
||||||
|
dropCursor({ class: styles.dropCursor }),
|
||||||
|
selectionMenu(schema),
|
||||||
|
dragHandle(),
|
||||||
|
imageInput(schema)
|
||||||
|
// TODO
|
||||||
|
// link(),
|
||||||
|
]
|
||||||
|
}
|
23
src/components/EditorNew/prosemirror/plugins/placeholder.ts
Normal file
23
src/components/EditorNew/prosemirror/plugins/placeholder.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { Plugin } from 'prosemirror-state'
|
||||||
|
import { DecorationSet, Decoration } from 'prosemirror-view'
|
||||||
|
import styles from '../styles/ProseMirror.module.scss'
|
||||||
|
|
||||||
|
export const placeholder = (text: string): Plugin =>
|
||||||
|
new Plugin({
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
const { doc } = state
|
||||||
|
|
||||||
|
if (doc.childCount > 1 || !doc.firstChild.isTextblock || doc.firstChild.content.size > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.setAttribute('contenteditable', 'false')
|
||||||
|
div.classList.add(styles.placeholder)
|
||||||
|
div.textContent = text
|
||||||
|
|
||||||
|
return DecorationSet.create(doc, [Decoration.widget(1, div)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { renderGrouped } from 'prosemirror-menu'
|
||||||
|
import { EditorState, Plugin } from 'prosemirror-state'
|
||||||
|
import styles from '../styles/ProseMirror.module.scss'
|
||||||
|
import type { EditorView } from 'prosemirror-view'
|
||||||
|
import type { DiscoursSchema } from '../schema'
|
||||||
|
import { buildMenuItems } from '../helpers/menu'
|
||||||
|
|
||||||
|
export class SelectionMenuView {
|
||||||
|
tooltip: HTMLDivElement
|
||||||
|
|
||||||
|
constructor(view: EditorView, schema: DiscoursSchema) {
|
||||||
|
this.tooltip = document.createElement('div')
|
||||||
|
this.tooltip.className = styles.selectionMenu
|
||||||
|
view.dom.parentNode.appendChild(this.tooltip)
|
||||||
|
const { dom } = renderGrouped(view, buildMenuItems(schema))
|
||||||
|
this.tooltip.appendChild(dom)
|
||||||
|
this.update(view, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(view: EditorView, lastState: EditorState) {
|
||||||
|
const state = view.state
|
||||||
|
|
||||||
|
if (lastState && 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 = ''
|
||||||
|
const { from, to } = state.selection
|
||||||
|
const start = view.coordsAtPos(from)
|
||||||
|
const end = view.coordsAtPos(to)
|
||||||
|
const box = this.tooltip.offsetParent.getBoundingClientRect()
|
||||||
|
const width = this.tooltip.getBoundingClientRect().width
|
||||||
|
const left = (start.left + end.left - width) / 2
|
||||||
|
this.tooltip.style.left = `${left - box.left}px`
|
||||||
|
this.tooltip.style.bottom = `${box.bottom - start.top + 8}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.tooltip.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectionMenu = (schema: DiscoursSchema) =>
|
||||||
|
new Plugin({
|
||||||
|
view(editorView: EditorView) {
|
||||||
|
return new SelectionMenuView(editorView, schema)
|
||||||
|
}
|
||||||
|
})
|
172
src/components/EditorNew/prosemirror/schema.ts
Normal file
172
src/components/EditorNew/prosemirror/schema.ts
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import { Node, Schema, SchemaSpec } from 'prosemirror-model'
|
||||||
|
|
||||||
|
export type Nodes =
|
||||||
|
| 'doc'
|
||||||
|
| 'paragraph'
|
||||||
|
| 'text'
|
||||||
|
| 'heading'
|
||||||
|
| 'ordered_list'
|
||||||
|
| 'bullet_list'
|
||||||
|
| 'list_item'
|
||||||
|
| 'blockquote'
|
||||||
|
| 'image'
|
||||||
|
| 'embed'
|
||||||
|
|
||||||
|
export type Marks = 'strong' | 'em' | 'strikethrough' | 'note' | 'link' | 'highlight'
|
||||||
|
|
||||||
|
export type DiscoursSchema = Schema<Nodes, Marks>
|
||||||
|
|
||||||
|
export const schemaSpec: SchemaSpec<Nodes, Marks> = {
|
||||||
|
nodes: {
|
||||||
|
doc: {
|
||||||
|
content: 'block+'
|
||||||
|
},
|
||||||
|
paragraph: {
|
||||||
|
content: 'inline*',
|
||||||
|
group: 'block',
|
||||||
|
parseDOM: [{ tag: 'p' }],
|
||||||
|
toDOM: () => ['p', 0]
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
group: 'inline'
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
attrs: { level: { default: 1 } },
|
||||||
|
content: 'inline*',
|
||||||
|
group: 'block',
|
||||||
|
defining: true,
|
||||||
|
parseDOM: [
|
||||||
|
{ tag: 'h1', attrs: { level: 1 } },
|
||||||
|
{ tag: 'h2', attrs: { level: 2 } },
|
||||||
|
{ tag: 'h3', attrs: { level: 3 } }
|
||||||
|
],
|
||||||
|
toDOM(node) {
|
||||||
|
return ['h' + node.attrs.level, 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ordered_list: {
|
||||||
|
group: 'block',
|
||||||
|
content: 'list_item+',
|
||||||
|
attrs: { order: { default: 1 } },
|
||||||
|
parseDOM: [
|
||||||
|
{
|
||||||
|
tag: 'ol',
|
||||||
|
getAttrs(dom: HTMLElement) {
|
||||||
|
return { order: dom.hasAttribute('start') ? +dom.getAttribute('start') : 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
toDOM(node) {
|
||||||
|
return node.attrs.order === 1 ? ['ol', 0] : ['ol', { start: node.attrs.order }, 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bullet_list: {
|
||||||
|
group: 'block',
|
||||||
|
content: 'list_item+',
|
||||||
|
parseDOM: [{ tag: 'ul' }],
|
||||||
|
toDOM() {
|
||||||
|
return ['ul', 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
list_item: {
|
||||||
|
content: 'paragraph block*',
|
||||||
|
parseDOM: [{ tag: 'li' }],
|
||||||
|
toDOM() {
|
||||||
|
return ['li', 0]
|
||||||
|
},
|
||||||
|
defining: true
|
||||||
|
},
|
||||||
|
blockquote: {
|
||||||
|
content: 'block+',
|
||||||
|
group: 'block',
|
||||||
|
defining: true,
|
||||||
|
parseDOM: [{ tag: 'blockquote' }],
|
||||||
|
toDOM() {
|
||||||
|
return ['blockquote', 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
embed: {},
|
||||||
|
///
|
||||||
|
image: {
|
||||||
|
inline: true,
|
||||||
|
attrs: {
|
||||||
|
src: {},
|
||||||
|
alt: { default: null },
|
||||||
|
title: { default: null },
|
||||||
|
path: { default: null },
|
||||||
|
width: { default: null }
|
||||||
|
},
|
||||||
|
group: 'inline',
|
||||||
|
draggable: true,
|
||||||
|
parseDOM: [
|
||||||
|
{
|
||||||
|
tag: 'img[src]',
|
||||||
|
getAttrs: (dom: HTMLElement) => ({
|
||||||
|
src: dom.getAttribute('src'),
|
||||||
|
title: dom.getAttribute('title'),
|
||||||
|
alt: dom.getAttribute('alt'),
|
||||||
|
path: dom.dataset.path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
],
|
||||||
|
toDOM: (node: Node) => [
|
||||||
|
'img',
|
||||||
|
{
|
||||||
|
src: node.attrs.src,
|
||||||
|
title: node.attrs.title,
|
||||||
|
alt: node.attrs.alt,
|
||||||
|
'data-path': node.attrs.path
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
marks: {
|
||||||
|
strong: {
|
||||||
|
parseDOM: [
|
||||||
|
{ tag: 'strong' },
|
||||||
|
// This works around a Google Docs misbehavior where
|
||||||
|
// pasted content will be inexplicably wrapped in `<b>`
|
||||||
|
// tags with a font-weight normal.
|
||||||
|
{ tag: 'b', getAttrs: (node: HTMLElement) => node.style.fontWeight !== 'normal' && null },
|
||||||
|
{
|
||||||
|
style: 'font-weight',
|
||||||
|
getAttrs: (value: string) => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
toDOM() {
|
||||||
|
return ['strong', 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
em: {
|
||||||
|
parseDOM: [{ tag: 'i' }, { tag: 'em' }, { style: 'font-style=italic' }],
|
||||||
|
toDOM() {
|
||||||
|
return ['em', 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
attrs: {
|
||||||
|
href: {},
|
||||||
|
title: { default: null }
|
||||||
|
},
|
||||||
|
inclusive: false,
|
||||||
|
parseDOM: [
|
||||||
|
{
|
||||||
|
tag: 'a[href]',
|
||||||
|
getAttrs(dom: HTMLElement) {
|
||||||
|
return { href: dom.getAttribute('href'), title: dom.getAttribute('title') }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
toDOM(node) {
|
||||||
|
const { href, title } = node.attrs
|
||||||
|
return ['a', { href, title }, 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO:
|
||||||
|
highlight: {},
|
||||||
|
strikethrough: {},
|
||||||
|
note: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const schema = new Schema(schemaSpec)
|
|
@ -0,0 +1,21 @@
|
||||||
|
.dropCursor {
|
||||||
|
// TODO check why important
|
||||||
|
height: 2px !important;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectionMenu {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
||||||
|
color: #000;
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
opacity: 0.3;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
329
src/components/EditorNew/prosemirror/styles/ProseMirror.scss
Normal file
329
src/components/EditorNew/prosemirror/styles/ProseMirror.scss
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
.ProseMirror {
|
||||||
|
color: var(--foreground);
|
||||||
|
background-color: var(--background);
|
||||||
|
position: relative;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
outline: none;
|
||||||
|
margin: 1em 1em 1em 0;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: var(--background);
|
||||||
|
background-color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable {
|
||||||
|
position: relative;
|
||||||
|
margin-left: -30px;
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: calc(var(--font-fize) * 1.6px);
|
||||||
|
opacity: 0;
|
||||||
|
cursor: move;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 6px;
|
||||||
|
fill: var(--foreground);
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover > span {
|
||||||
|
background: var(--foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 .handle {
|
||||||
|
height: calc(var(--font-size) * 2.3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable:hover .handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 2px solid;
|
||||||
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
|
margin: 1.5em 0;
|
||||||
|
padding-left: 1.6em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menuitem {
|
||||||
|
display: flex;
|
||||||
|
font-size: small;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
> * {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-disabled {
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
padding: 0.8rem 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-textblock-dropdown {
|
||||||
|
min-width: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu {
|
||||||
|
margin: 0 -4px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-tooltip .ProseMirror-menu {
|
||||||
|
width: fit-content;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menuseparator {
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-dropdown,
|
||||||
|
.ProseMirror-menu-dropdown-menu {
|
||||||
|
padding: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-dropdown {
|
||||||
|
vertical-align: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-dropdown-wrap {
|
||||||
|
padding: 1px 0 1px 4px;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-dropdown::after {
|
||||||
|
content: '';
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
border-top: 4px solid currentcolor;
|
||||||
|
opacity: 0.6;
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: calc(50% - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-dropdown-menu,
|
||||||
|
.ProseMirror-menu-submenu {
|
||||||
|
position: absolute;
|
||||||
|
background: white;
|
||||||
|
color: #666;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-dropdown-menu {
|
||||||
|
z-index: 15;
|
||||||
|
|
||||||
|
/* min-width: 6em; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-dropdown-item {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 8px 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-dropdown-item:hover {
|
||||||
|
background: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-submenu-wrap {
|
||||||
|
position: relative;
|
||||||
|
margin-right: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-submenu-label::after {
|
||||||
|
content: '';
|
||||||
|
border-top: 4px solid transparent;
|
||||||
|
border-bottom: 4px solid transparent;
|
||||||
|
border-left: 4px solid currentcolor;
|
||||||
|
opacity: 0.6;
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: calc(50% - 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-submenu {
|
||||||
|
display: none;
|
||||||
|
left: 100%;
|
||||||
|
top: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-active {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu,
|
||||||
|
.ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menubar {
|
||||||
|
border-top-left-radius: inherit;
|
||||||
|
border-top-right-radius: inherit;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
min-height: 1em;
|
||||||
|
color: #666;
|
||||||
|
padding: 0 1.5em;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
border-bottom: 1px solid silver;
|
||||||
|
background: white;
|
||||||
|
z-index: 10;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menu-disabled.ProseMirror-icon {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-icon svg {
|
||||||
|
fill: currentcolor;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-icon span {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror li {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection *::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection {
|
||||||
|
caret-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-selectednode {
|
||||||
|
outline: 2px solid #8cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure li selections wrap around markers */
|
||||||
|
li.ProseMirror-selectednode {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.ProseMirror-selectednode::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -32px;
|
||||||
|
right: -2px;
|
||||||
|
top: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
border: 2px solid #8cf;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror .empty-node::before {
|
||||||
|
position: absolute;
|
||||||
|
color: #aaa;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror .empty-node:hover::before {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror.editor_empty::before {
|
||||||
|
position: absolute;
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--ui-color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-prompt {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
||||||
|
font-size: 0.7em;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-prompt input[type='text'] {
|
||||||
|
border: none;
|
||||||
|
font-size: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 0.5em 7.5em 0.5em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-prompt-buttons {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-prompt-buttons button {
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 90%;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 10em;
|
||||||
|
margin-bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-prompt-submit {
|
||||||
|
background: url("data:image/svg+xml,%3Csvg width='19' height='15' viewBox='0 0 19 15' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M19 2.73787L16.2621 0L6.78964 9.47248L2.73787 5.42071L0 8.15858L6.78964 14.9482L19 2.73787Z' fill='%23393840'/%3E%3C/svg%3E")
|
||||||
|
center no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-prompt-cancel {
|
||||||
|
background: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.1512 0.423856L0.423263 13.1518L2.84763 15.5761L15.5756 2.84822L13.1512 0.423856Z M15.5755 13.1518L2.84763 0.423855L0.423263 2.84822L13.1512 15.5761L15.5755 13.1518Z' fill='%23393840'/%3E%3C/svg%3E%0A")
|
||||||
|
center no-repeat;
|
||||||
|
}
|
68
src/components/EditorNew/prosemirror/views/image.ts
Normal file
68
src/components/EditorNew/prosemirror/views/image.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import type { EditorView, NodeView, NodeViewConstructor } from 'prosemirror-view'
|
||||||
|
import type { Node } from 'prosemirror-model'
|
||||||
|
|
||||||
|
class ImageView implements NodeView {
|
||||||
|
node: Node
|
||||||
|
view: EditorView
|
||||||
|
getPos: () => number
|
||||||
|
dom: Element
|
||||||
|
container: HTMLElement
|
||||||
|
handle: HTMLElement
|
||||||
|
onResizeFn: any
|
||||||
|
onResizeEndFn: any
|
||||||
|
width: number
|
||||||
|
updating: number
|
||||||
|
|
||||||
|
constructor(node: Node, view: EditorView, getPos: () => number) {
|
||||||
|
this.node = node
|
||||||
|
this.view = view
|
||||||
|
this.getPos = getPos
|
||||||
|
this.onResizeFn = this.onResize.bind(this)
|
||||||
|
this.onResizeEndFn = this.onResizeEnd.bind(this)
|
||||||
|
|
||||||
|
this.container = document.createElement('span')
|
||||||
|
this.container.className = 'image-container'
|
||||||
|
if (node.attrs.width) this.setWidth(node.attrs.width)
|
||||||
|
|
||||||
|
const image = document.createElement('img')
|
||||||
|
image.setAttribute('title', node.attrs.title ?? '')
|
||||||
|
image.setAttribute('src', node.attrs.src)
|
||||||
|
|
||||||
|
this.handle = document.createElement('span')
|
||||||
|
this.handle.className = 'resize-handle'
|
||||||
|
this.handle.addEventListener('mousedown', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
window.addEventListener('mousemove', this.onResizeFn)
|
||||||
|
window.addEventListener('mouseup', this.onResizeEndFn)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.container.appendChild(image)
|
||||||
|
this.container.appendChild(this.handle)
|
||||||
|
this.dom = this.container
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(e: MouseEvent) {
|
||||||
|
this.width = e.pageX - this.container.getBoundingClientRect().left
|
||||||
|
this.setWidth(this.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
onResizeEnd() {
|
||||||
|
window.removeEventListener('mousemove', this.onResizeFn)
|
||||||
|
if (this.updating === this.width) return
|
||||||
|
this.updating = this.width
|
||||||
|
const tr = this.view.state.tr
|
||||||
|
tr.setNodeMarkup(this.getPos(), undefined, {
|
||||||
|
...this.node.attrs,
|
||||||
|
width: this.width
|
||||||
|
})
|
||||||
|
|
||||||
|
this.view.dispatch(tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
setWidth(width: number) {
|
||||||
|
this.container.style.width = width + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createImageView: NodeViewConstructor = (node: Node, view: EditorView, getPos: () => number) =>
|
||||||
|
new ImageView(node, view, getPos)
|
|
@ -12,6 +12,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
&.top {
|
&.top {
|
||||||
border-bottom: 1px solid #e1e1e1;
|
border-bottom: 1px solid #e1e1e1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -8,7 +8,7 @@ const x = [
|
||||||
['8', '4']
|
['8', '4']
|
||||||
]
|
]
|
||||||
|
|
||||||
export const Row2 = (props: { articles: Shout[] }) => {
|
export const Row2 = (props: { articles: Shout[]; isEqual?: boolean }) => {
|
||||||
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)))
|
||||||
|
@ -20,8 +20,11 @@ export const Row2 = (props: { articles: Shout[] }) => {
|
||||||
{(a, i) => {
|
{(a, i) => {
|
||||||
return (
|
return (
|
||||||
<Show when={!!a}>
|
<Show when={!!a}>
|
||||||
<div class={`col-md-${x[y()][i()]}`}>
|
<div class={`col-md-${props.isEqual ? '6' : x[y()][i()]}`}>
|
||||||
<ArticleCard article={a} settings={{ isWithCover: x[y()][i()] === '8' }} />
|
<ArticleCard
|
||||||
|
article={a}
|
||||||
|
settings={{ isWithCover: props.isEqual || x[y()][i()] === '8' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { For } from 'solid-js'
|
import { For } from 'solid-js'
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
import { useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { useArticlesStore } from '../../stores/zine/articles'
|
import { useArticlesStore } from '../../stores/zine/articles'
|
||||||
import { useSeenStore } from '../../stores/zine/seen'
|
import { useSeenStore } from '../../stores/zine/seen'
|
||||||
|
import { useSession } from '../../context/session'
|
||||||
|
|
||||||
type FeedSidebarProps = {
|
type FeedSidebarProps = {
|
||||||
authors: Author[]
|
authors: Author[]
|
||||||
|
@ -14,7 +14,7 @@ type FeedSidebarProps = {
|
||||||
|
|
||||||
export const FeedSidebar = (props: FeedSidebarProps) => {
|
export const FeedSidebar = (props: FeedSidebarProps) => {
|
||||||
const { getSeen: seen } = useSeenStore()
|
const { getSeen: seen } = useSeenStore()
|
||||||
const { session } = useAuthStore()
|
const { session } = useSession()
|
||||||
const { authorEntities } = useAuthorsStore({ authors: props.authors })
|
const { authorEntities } = useAuthorsStore({ authors: props.authors })
|
||||||
const { articlesByTopic } = useArticlesStore()
|
const { articlesByTopic } = useArticlesStore()
|
||||||
const { topicEntities } = useTopicsStore()
|
const { topicEntities } = useTopicsStore()
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
.swiper-slide {
|
.swiper-slide {
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
|
|
||||||
|
.cards-with-cover & {
|
||||||
|
height: 0 !important;
|
||||||
padding-top: 100%;
|
padding-top: 100%;
|
||||||
|
|
||||||
@include media-breakpoint-up(sm) {
|
@include media-breakpoint-up(sm) {
|
||||||
|
@ -12,6 +14,7 @@
|
||||||
padding-top: 35% !important;
|
padding-top: 35% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.slider-arrow-prev,
|
.slider-arrow-prev,
|
||||||
.slider-arrow-next {
|
.slider-arrow-next {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { Icon } from '../Nav/Icon'
|
||||||
interface SliderProps {
|
interface SliderProps {
|
||||||
title?: string
|
title?: string
|
||||||
articles: Shout[]
|
articles: Shout[]
|
||||||
|
slidesPerView?: number
|
||||||
|
isCardsWithCover?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props: SliderProps) => {
|
export default (props: SliderProps) => {
|
||||||
|
@ -19,11 +21,14 @@ export default (props: SliderProps) => {
|
||||||
let pagEl: HTMLDivElement | undefined
|
let pagEl: HTMLDivElement | undefined
|
||||||
let nextEl: HTMLDivElement | undefined
|
let nextEl: HTMLDivElement | undefined
|
||||||
let prevEl: HTMLDivElement | undefined
|
let prevEl: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
const isCardsWithCover = typeof props.isCardsWithCover === 'boolean' ? props.isCardsWithCover : true
|
||||||
|
|
||||||
const opts: SwiperOptions = {
|
const opts: SwiperOptions = {
|
||||||
|
roundLengths: true,
|
||||||
loop: true,
|
loop: true,
|
||||||
centeredSlides: true,
|
centeredSlides: true,
|
||||||
slidesPerView: 1,
|
slidesPerView: 1,
|
||||||
spaceBetween: 8,
|
|
||||||
modules: [Navigation, Pagination],
|
modules: [Navigation, Pagination],
|
||||||
speed: 500,
|
speed: 500,
|
||||||
navigation: { nextEl, prevEl },
|
navigation: { nextEl, prevEl },
|
||||||
|
@ -34,7 +39,12 @@ export default (props: SliderProps) => {
|
||||||
},
|
},
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
768: {
|
768: {
|
||||||
slidesPerView: 1.66666
|
slidesPerView: props.slidesPerView > 0 ? props.slidesPerView : 1.66666,
|
||||||
|
spaceBetween: isCardsWithCover ? 8 : 26
|
||||||
|
},
|
||||||
|
992: {
|
||||||
|
slidesPerView: props.slidesPerView > 0 ? props.slidesPerView : 1.66666,
|
||||||
|
spaceBetween: isCardsWithCover ? 8 : 52
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,12 +58,13 @@ export default (props: SliderProps) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const articles = createMemo(() => props.articles)
|
const articles = createMemo(() => props.articles)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="floor floor--important">
|
<div class="floor floor--important">
|
||||||
<div class="wide-container row">
|
<div class="wide-container row">
|
||||||
<h2 class="col-12">{props.title}</h2>
|
<h2 class="col-12">{props.title}</h2>
|
||||||
<Show when={!!articles()}>
|
<Show when={!!articles()}>
|
||||||
<div class="swiper" ref={el}>
|
<div class="swiper" classList={{ 'cards-with-cover': isCardsWithCover }} ref={el}>
|
||||||
<div class="swiper-wrapper">
|
<div class="swiper-wrapper">
|
||||||
<For each={articles()}>
|
<For each={articles()}>
|
||||||
{(a: Shout) => (
|
{(a: Shout) => (
|
||||||
|
@ -62,7 +73,7 @@ export default (props: SliderProps) => {
|
||||||
settings={{
|
settings={{
|
||||||
additionalClass: 'swiper-slide',
|
additionalClass: 'swiper-slide',
|
||||||
isFloorImportant: true,
|
isFloorImportant: true,
|
||||||
isWithCover: true,
|
isWithCover: isCardsWithCover,
|
||||||
nodate: true
|
nodate: true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -70,10 +81,10 @@ export default (props: SliderProps) => {
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<div class="slider-arrow-next" ref={nextEl} onClick={() => swiper()?.slideNext()}>
|
<div class="slider-arrow-next" ref={nextEl} onClick={() => swiper()?.slideNext()}>
|
||||||
<Icon name="slider-arrow" />
|
<Icon name="slider-arrow" class={'icon'} />
|
||||||
</div>
|
</div>
|
||||||
<div class="slider-arrow-prev" ref={prevEl} onClick={() => swiper()?.slidePrev()}>
|
<div class="slider-arrow-prev" ref={prevEl} onClick={() => swiper()?.slidePrev()}>
|
||||||
<Icon name="slider-arrow" />
|
<Icon name="slider-arrow" class={'icon'} />
|
||||||
</div>
|
</div>
|
||||||
<div class="slider-pagination" ref={pagEl} />
|
<div class="slider-pagination" ref={pagEl} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,16 +2,20 @@ import styles from './AuthModal.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { t } from '../../../utils/intl'
|
import { t } from '../../../utils/intl'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { createMemo, onMount, Show } from 'solid-js'
|
import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { handleClientRouteLinkClick, useRouter } from '../../../stores/router'
|
||||||
import { confirmEmail, useAuthStore } from '../../../stores/auth'
|
import type { ConfirmEmailSearchParams } from './types'
|
||||||
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
type ConfirmEmailSearchParams = {
|
import { useSession } from '../../../context/session'
|
||||||
token: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmailConfirm = () => {
|
export const EmailConfirm = () => {
|
||||||
const { session } = useAuthStore()
|
const {
|
||||||
|
session,
|
||||||
|
actions: { confirmEmail }
|
||||||
|
} = useSession()
|
||||||
|
|
||||||
|
const [isTokenExpired, setIsTokenExpired] = createSignal(false)
|
||||||
|
const [isTokenInvalid, setIsTokenInvalid] = createSignal(false)
|
||||||
|
|
||||||
const confirmedEmail = createMemo(() => session()?.user?.email || '')
|
const confirmedEmail = createMemo(() => session()?.user?.email || '')
|
||||||
|
|
||||||
|
@ -22,23 +26,54 @@ export const EmailConfirm = () => {
|
||||||
try {
|
try {
|
||||||
await confirmEmail(token)
|
await confirmEmail(token)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
if (error.code === 'token_expired') {
|
||||||
|
setIsTokenExpired(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 'token_invalid') {
|
||||||
|
setIsTokenInvalid(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class={styles.title}>{t('Hooray! Welcome!')}</div>
|
{/* TODO: texts */}
|
||||||
|
<Show when={isTokenExpired()}>
|
||||||
|
<div class={styles.title}>Ссылка больше не действительна</div>
|
||||||
|
<div class={styles.text}>
|
||||||
|
<a href="/?modal=auth&mode=login" class={styles.sendLink} onClick={handleClientRouteLinkClick}>
|
||||||
|
{/*TODO: temp solution, should be send link again, but we don't have email here*/}
|
||||||
|
Вход
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={isTokenInvalid()}>
|
||||||
|
<div class={styles.title}>Неправильная ссылка</div>
|
||||||
|
<div class={styles.text}>
|
||||||
|
<a href="/?modal=auth&mode=login" class={styles.sendLink} onClick={handleClientRouteLinkClick}>
|
||||||
|
{/*TODO: temp solution, should be send link again, but we don't have email here*/}
|
||||||
|
Вход
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
<Show when={Boolean(confirmedEmail())}>
|
<Show when={Boolean(confirmedEmail())}>
|
||||||
|
<div class={styles.title}>{t('Hooray! Welcome!')}</div>
|
||||||
<div class={styles.text}>
|
<div class={styles.text}>
|
||||||
{t("You've confirmed email")} {confirmedEmail()}
|
{t("You've confirmed email")} {confirmedEmail()}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
<div>
|
<div>
|
||||||
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
|
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
|
||||||
{t('Go to main page')}
|
{t('Go to main page')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,9 @@ 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'
|
||||||
import { isValidEmail } from './validators'
|
import { isValidEmail } from './validators'
|
||||||
import { signSendLink } from '../../../stores/auth'
|
|
||||||
import { locale } from '../../../stores/ui'
|
import { locale } from '../../../stores/ui'
|
||||||
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
|
import { signSendLink } from '../../../stores/auth'
|
||||||
|
|
||||||
type FormFields = {
|
type FormFields = {
|
||||||
email: string
|
email: string
|
||||||
|
@ -22,15 +23,17 @@ export const ForgotPasswordForm = () => {
|
||||||
setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
|
setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
|
||||||
setEmail(newEmail)
|
setEmail(newEmail)
|
||||||
}
|
}
|
||||||
const [sended, setSended] = createSignal(false)
|
|
||||||
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>({})
|
||||||
|
const [isUserNotFount, setIsUserNotFound] = createSignal(false)
|
||||||
|
|
||||||
const handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
setSubmitError('')
|
setSubmitError('')
|
||||||
|
setIsUserNotFound(false)
|
||||||
|
|
||||||
const newValidationErrors: ValidationErrors = {}
|
const newValidationErrors: ValidationErrors = {}
|
||||||
|
|
||||||
|
@ -51,10 +54,12 @@ export const ForgotPasswordForm = () => {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await signSendLink({ email: email(), lang: locale() })
|
await signSendLink({ email: email(), lang: locale() })
|
||||||
if (result.error) setSubmitError(result.error)
|
|
||||||
else setSended(true)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError && error.code === 'user_not_found') {
|
||||||
|
setIsUserNotFound(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
setSubmitError(error.message)
|
setSubmitError(error.message)
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
|
@ -64,12 +69,7 @@ export const ForgotPasswordForm = () => {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<h4>{t('Forgot password?')}</h4>
|
<h4>{t('Forgot password?')}</h4>
|
||||||
<Show
|
|
||||||
when={!sended()}
|
|
||||||
fallback={<div class={styles.authInfo}>{t('Link sent, check your email')}</div>}
|
|
||||||
>
|
|
||||||
<div class={styles.authSubtitle}>{t('Everything is ok, please give us your email address')}</div>
|
<div class={styles.authSubtitle}>{t('Everything is ok, please give us your email address')}</div>
|
||||||
</Show>
|
|
||||||
<Show when={submitError()}>
|
<Show when={submitError()}>
|
||||||
<div class={styles.authInfo}>
|
<div class={styles.authInfo}>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -77,6 +77,21 @@ export const ForgotPasswordForm = () => {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={isUserNotFount()}>
|
||||||
|
<div class={styles.authSubtitle}>
|
||||||
|
{/*TODO: text*/}
|
||||||
|
{t("We can't find you, check email or")}{' '}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
changeSearchParam('mode', 'register')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('register')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
<Show when={validationErrors().email}>
|
<Show when={validationErrors().email}>
|
||||||
<div class={styles.validationError}>{validationErrors().email}</div>
|
<div class={styles.validationError}>{validationErrors().email}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -2,7 +2,6 @@ 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, signSendLink } from '../../../stores/auth'
|
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
import { createSignal, Show } from 'solid-js'
|
import { createSignal, Show } from 'solid-js'
|
||||||
import { isValidEmail } from './validators'
|
import { isValidEmail } from './validators'
|
||||||
|
@ -10,6 +9,8 @@ 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, locale } from '../../../stores/ui'
|
import { hideModal, locale } from '../../../stores/ui'
|
||||||
|
import { useSession } from '../../../context/session'
|
||||||
|
import { signSendLink } from '../../../stores/auth'
|
||||||
|
|
||||||
type FormFields = {
|
type FormFields = {
|
||||||
email: string
|
email: string
|
||||||
|
@ -26,6 +27,10 @@ export const LoginForm = () => {
|
||||||
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
|
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
|
||||||
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
||||||
|
|
||||||
|
const {
|
||||||
|
actions: { signIn }
|
||||||
|
} = useSession()
|
||||||
|
|
||||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
|
@ -53,6 +58,7 @@ export const LoginForm = () => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
setIsLinkSent(false)
|
setIsLinkSent(false)
|
||||||
|
setIsEmailNotConfirmed(false)
|
||||||
setSubmitError('')
|
setSubmitError('')
|
||||||
|
|
||||||
const newValidationErrors: ValidationErrors = {}
|
const newValidationErrors: ValidationErrors = {}
|
||||||
|
|
|
@ -4,13 +4,14 @@ 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 { 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'
|
import { hideModal } from '../../../stores/ui'
|
||||||
|
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
|
||||||
|
import { register } from '../../../stores/auth'
|
||||||
|
|
||||||
type FormFields = {
|
type FormFields = {
|
||||||
name: string
|
name: string
|
||||||
|
@ -23,7 +24,7 @@ type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
|
||||||
export const RegisterForm = () => {
|
export const RegisterForm = () => {
|
||||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
const { emailChecks } = useAuthStore()
|
const { emailChecks } = useEmailChecks()
|
||||||
|
|
||||||
const [submitError, setSubmitError] = createSignal('')
|
const [submitError, setSubmitError] = createSignal('')
|
||||||
const [name, setName] = createSignal('')
|
const [name, setName] = createSignal('')
|
||||||
|
@ -60,11 +61,14 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
const newValidationErrors: ValidationErrors = {}
|
const newValidationErrors: ValidationErrors = {}
|
||||||
|
|
||||||
if (!name()) {
|
const clearName = name().trim()
|
||||||
|
const clearEmail = email().trim()
|
||||||
|
|
||||||
|
if (!clearName) {
|
||||||
newValidationErrors.name = t('Please enter a name to sign your comments and publication')
|
newValidationErrors.name = t('Please enter a name to sign your comments and publication')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!email()) {
|
if (!clearEmail) {
|
||||||
newValidationErrors.email = t('Please enter email')
|
newValidationErrors.email = t('Please enter email')
|
||||||
} else if (!isValidEmail(email())) {
|
} else if (!isValidEmail(email())) {
|
||||||
newValidationErrors.email = t('Invalid email')
|
newValidationErrors.email = t('Invalid email')
|
||||||
|
@ -76,7 +80,7 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
setValidationErrors(newValidationErrors)
|
setValidationErrors(newValidationErrors)
|
||||||
|
|
||||||
const emailCheckResult = await checkEmail(email())
|
const emailCheckResult = await checkEmail(clearEmail)
|
||||||
|
|
||||||
const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult
|
const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult
|
||||||
|
|
||||||
|
@ -88,8 +92,8 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await register({
|
await register({
|
||||||
name: name(),
|
name: clearName,
|
||||||
email: email(),
|
email: clearEmail,
|
||||||
password: password()
|
password: password()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ export const AuthModal = () => {
|
||||||
class={clsx('row', styles.view)}
|
class={clsx('row', styles.view)}
|
||||||
classList={{ [styles.signUp]: mode() === 'register' || mode() === 'confirm-email' }}
|
classList={{ [styles.signUp]: mode() === 'register' || mode() === 'confirm-email' }}
|
||||||
>
|
>
|
||||||
<div class={clsx('col-sm-6', 'd-md-none', styles.authImage)}>
|
<div class={clsx('col-md-6 d-none d-md-block', styles.authImage)}>
|
||||||
<div
|
<div
|
||||||
class={styles.authImageText}
|
class={styles.authImageText}
|
||||||
classList={{ [styles.hidden]: mode() !== 'register' && mode() !== 'confirm-email' }}
|
classList={{ [styles.hidden]: mode() !== 'register' && mode() !== 'confirm-email' }}
|
||||||
|
@ -68,7 +68,7 @@ export const AuthModal = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={clsx('col-sm-6', styles.auth)}>
|
<div class={clsx('col-md-6', styles.auth)}>
|
||||||
<Dynamic component={AUTH_MODAL_MODES[mode()]} />
|
<Dynamic component={AUTH_MODAL_MODES[mode()]} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,3 +3,7 @@ export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-pas
|
||||||
export type AuthModalSearchParams = {
|
export type AuthModalSearchParams = {
|
||||||
mode: AuthModalMode
|
mode: AuthModalMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConfirmEmailSearchParams = {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
|
@ -71,12 +71,16 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
padding: 0 $container-padding-x 0 0;
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: height 0.2s;
|
transition: height 0.2s;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
padding: 0 6rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
height: 70px;
|
height: 70px;
|
||||||
}
|
}
|
||||||
|
@ -114,6 +118,7 @@
|
||||||
|
|
||||||
.usernav {
|
.usernav {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
font-weight: 500;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
@ -130,6 +135,10 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
@include font-size(1.7rem);
|
@include font-size(1.7rem);
|
||||||
|
|
||||||
|
@include media-breakpoint-down(lg) {
|
||||||
|
margin-right: 7rem;
|
||||||
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(md) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -141,6 +150,7 @@
|
||||||
|
|
||||||
.mainNavigation {
|
.mainNavigation {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
font-weight: 500;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -204,10 +214,14 @@
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
float: right;
|
float: right;
|
||||||
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
padding-left: divide($container-padding-x, 2);
|
|
||||||
width: 2.2rem;
|
width: 2.2rem;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
padding-left: divide($container-padding-x, 2);
|
||||||
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -290,7 +304,7 @@
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
left: 0;
|
left: 0;
|
||||||
margin: 0.2em;
|
margin: 0.2em 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -318,12 +332,24 @@
|
||||||
.articleControls {
|
.articleControls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
right: 5rem;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
right: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.control {
|
.control {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -344,7 +370,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.control + .control {
|
.control + .control {
|
||||||
margin-left: 1.6rem;
|
margin-left: 1.2rem;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@ -385,17 +415,21 @@
|
||||||
|
|
||||||
.userControlItem {
|
.userControlItem {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 2px solid #f6f6f6;
|
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 2.4em;
|
height: 2.4em;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: divide($container-padding-x, 2);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
transition: margin-left 0.3s;
|
||||||
width: 2.4em;
|
width: 2.4em;
|
||||||
|
|
||||||
@include media-breakpoint-up(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
margin-left: 1.2rem;
|
margin-left: 0.4rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerScrolledTop &,
|
||||||
|
.headerScrolledBottom & {
|
||||||
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.circlewrap {
|
.circlewrap {
|
||||||
|
@ -459,10 +493,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.userControlItemWritePost {
|
.userControlItemVerbose {
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
@include media-breakpoint-up(lg) {
|
|
||||||
.icon {
|
.icon {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
import { For, Show, createSignal, createMemo, createEffect, onMount, onCleanup } from 'solid-js'
|
import { For, Show, createSignal, createEffect, onMount, onCleanup } from 'solid-js'
|
||||||
import Notifications from './Notifications'
|
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
import { Modal } from './Modal'
|
import { Modal } from './Modal'
|
||||||
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 } from '../../stores/ui'
|
||||||
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 { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { getLogger } from '../../utils/logger'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { HeaderAuth } from './HeaderAuth'
|
||||||
import { SharePopup } from '../Article/SharePopup'
|
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' },
|
||||||
|
@ -34,40 +27,37 @@ export const Header = (props: Props) => {
|
||||||
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
|
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
|
||||||
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 [isSharePopupVisible, setIsSharePopupVisible] = createSignal(false)
|
const [isSharePopupVisible, setIsSharePopupVisible] = createSignal(false)
|
||||||
const [isProfilePopupVisible, setIsProfilePopupVisible] = createSignal(false)
|
const [isProfilePopupVisible, setIsProfilePopupVisible] = createSignal(false)
|
||||||
|
|
||||||
// stores
|
|
||||||
const { warnings } = useWarningsStore()
|
|
||||||
const { session } = useAuthStore()
|
|
||||||
const { modal } = useModalStore()
|
const { modal } = useModalStore()
|
||||||
|
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
|
||||||
const toggleFixed = () => setFixed((oldFixed) => !oldFixed)
|
const toggleFixed = () => setFixed((oldFixed) => !oldFixed)
|
||||||
// effects
|
// effects
|
||||||
|
|
||||||
|
let windowScrollTop = 0
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
const mainContent = document.querySelector('.main-content') as HTMLDivElement
|
||||||
|
|
||||||
|
if (fixed() || modal() !== null) {
|
||||||
|
windowScrollTop = window.scrollY
|
||||||
|
mainContent.style.marginTop = `-${windowScrollTop}px`
|
||||||
|
}
|
||||||
|
|
||||||
document.body.classList.toggle('fixed', fixed() || modal() !== null)
|
document.body.classList.toggle('fixed', fixed() || modal() !== null)
|
||||||
document.body.classList.toggle(styles.fixed, fixed() && !modal())
|
document.body.classList.toggle(styles.fixed, fixed() && !modal())
|
||||||
|
|
||||||
|
if (!fixed() && !modal()) {
|
||||||
|
mainContent.style.marginTop = ''
|
||||||
|
window.scrollTo(0, windowScrollTop)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// derived
|
|
||||||
const authorized = createMemo(() => session()?.user?.slug)
|
|
||||||
|
|
||||||
const handleBellIconClick = (event: Event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
if (!authorized()) {
|
|
||||||
showModal('auth')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleWarnings()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let scrollTop = window.scrollY
|
let scrollTop = window.scrollY
|
||||||
|
|
||||||
|
@ -131,64 +121,7 @@ export const Header = (props: Props) => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.usernav}>
|
<HeaderAuth setIsProfilePopupVisible={setIsProfilePopupVisible} />
|
||||||
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemWritePost)}>
|
|
||||||
<a href="/create" onClick={handleClientRouteLinkClick}>
|
|
||||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
|
||||||
<Icon name="pencil" class={styles.icon} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={styles.userControlItem}>
|
|
||||||
<a href="#" onClick={handleBellIconClick}>
|
|
||||||
<div>
|
|
||||||
<Icon name="bell-white" counter={authorized() ? warnings().length : 1} />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={visibleWarnings()}>
|
|
||||||
<div class={clsx(styles.userControlItem, 'notifications')}>
|
|
||||||
<Notifications />
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={authorized()}
|
|
||||||
fallback={
|
|
||||||
<div class={clsx(styles.userControlItem, 'loginbtn')}>
|
|
||||||
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
|
||||||
<Icon name="user-anonymous" />
|
|
||||||
</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>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<Show when={props.title}>
|
<Show when={props.title}>
|
||||||
<div class={styles.articleControls}>
|
<div class={styles.articleControls}>
|
||||||
<SharePopup
|
<SharePopup
|
||||||
|
@ -209,7 +142,6 @@ export const Header = (props: Props) => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
|
||||||
<div class={styles.burgerContainer}>
|
<div class={styles.burgerContainer}>
|
||||||
<div class={styles.burger} classList={{ fixed: fixed() }} onClick={toggleFixed}>
|
<div class={styles.burger} classList={{ fixed: fixed() }} onClick={toggleFixed}>
|
||||||
<div />
|
<div />
|
||||||
|
|
107
src/components/Nav/HeaderAuth.tsx
Normal file
107
src/components/Nav/HeaderAuth.tsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import styles from './Header.module.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||||
|
import { t } from '../../utils/intl'
|
||||||
|
import { Icon } from './Icon'
|
||||||
|
import { createSignal, onMount, Show } from 'solid-js'
|
||||||
|
import Notifications from './Notifications'
|
||||||
|
import { ProfilePopup } from './ProfilePopup'
|
||||||
|
import Userpic from '../Author/Userpic'
|
||||||
|
import type { Author } from '../../graphql/types.gen'
|
||||||
|
import { showModal, useWarningsStore } from '../../stores/ui'
|
||||||
|
import { ClientContainer } from '../_shared/ClientContainer'
|
||||||
|
import { useSession } from '../../context/session'
|
||||||
|
|
||||||
|
type HeaderAuthProps = {
|
||||||
|
setIsProfilePopupVisible: (value: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderAuth = (props: HeaderAuthProps) => {
|
||||||
|
const { page } = useRouter()
|
||||||
|
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
||||||
|
const { warnings } = useWarningsStore()
|
||||||
|
|
||||||
|
const { session, isAuthenticated } = useSession()
|
||||||
|
|
||||||
|
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
||||||
|
|
||||||
|
const handleBellIconClick = (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (!isAuthenticated()) {
|
||||||
|
showModal('auth')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleWarnings()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientContainer>
|
||||||
|
<Show when={!session.loading}>
|
||||||
|
<div class={styles.usernav}>
|
||||||
|
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
||||||
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
|
<a href="/create" onClick={handleClientRouteLinkClick}>
|
||||||
|
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||||
|
<Icon name="pencil" class={styles.icon} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={isAuthenticated()}>
|
||||||
|
<div class={styles.userControlItem}>
|
||||||
|
<a href="#" onClick={handleBellIconClick}>
|
||||||
|
<div>
|
||||||
|
<Icon name="bell-white" counter={isAuthenticated() ? warnings().length : 1} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={visibleWarnings()}>
|
||||||
|
<div class={clsx(styles.userControlItem, 'notifications')}>
|
||||||
|
<Notifications />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={isAuthenticated()}
|
||||||
|
fallback={
|
||||||
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
||||||
|
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
||||||
|
<span class={styles.textLabel}>{t('Enter')}</span>
|
||||||
|
<Icon name="user-anonymous" class={styles.icon} />
|
||||||
|
</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>
|
||||||
|
<ProfilePopup
|
||||||
|
onVisibilityChange={(isVisible) => {
|
||||||
|
props.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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</ClientContainer>
|
||||||
|
)
|
||||||
|
}
|
|
@ -8,17 +8,19 @@ img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notifications-counter {
|
.notificationsCounter {
|
||||||
background-color: red;
|
background-color: #d00820;
|
||||||
border-radius: 1rem;
|
border: 2px solid #fff;
|
||||||
|
border-radius: 2em;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
height: 1.5em;
|
height: 1.6em;
|
||||||
line-height: 1.5em;
|
left: 1.1em;
|
||||||
|
line-height: 1.25em;
|
||||||
|
padding: 0 0.25em;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -0.5rem;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
top: -0.5rem;
|
top: -0.5rem;
|
||||||
width: 1.5em;
|
min-width: 1.5em;
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { mergeProps, Show } from 'solid-js'
|
import { mergeProps, Show } from 'solid-js'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import './Icon.css'
|
import styles from './Icon.module.scss'
|
||||||
|
|
||||||
type IconProps = {
|
type IconProps = {
|
||||||
class?: string
|
class?: string
|
||||||
|
@ -16,10 +16,10 @@ export const Icon = (passedProps: IconProps) => {
|
||||||
const props = mergeProps({ title: '', counter: 0 }, passedProps)
|
const props = mergeProps({ title: '', counter: 0 }, passedProps)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx('icon', props.class)} style={props.style}>
|
<div class={clsx('icon', styles.icon, props.class)} style={props.style}>
|
||||||
<img src={`/icons/${props.name}.svg`} alt={props.title ?? props.name} class={props.iconClassName} />
|
<img src={`/icons/${props.name}.svg`} alt={props.title ?? props.name} class={props.iconClassName} />
|
||||||
<Show when={props.counter}>
|
<Show when={props.counter}>
|
||||||
<div class="notifications-counter">{props.counter}</div>
|
<div class={styles.notificationsCounter}>{props.counter}</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
.modalwrap {
|
.modalwrap {
|
||||||
pointer-events: all;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: rgb(20 20 20 / 70%);
|
background: rgb(20 20 20 / 70%);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
overflow: auto;
|
||||||
|
pointer-events: all;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, Show } from 'solid-js'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
import { getLogger } from '../../utils/logger'
|
import { getLogger } from '../../utils/logger'
|
||||||
import './Modal.scss'
|
import './Modal.scss'
|
||||||
|
|
|
@ -2,19 +2,23 @@ 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 { createMemo, For } from 'solid-js'
|
import { createMemo, For } from 'solid-js'
|
||||||
|
import { useSession } from '../../context/session'
|
||||||
|
|
||||||
|
export const ProfileModal = () => {
|
||||||
|
const {
|
||||||
|
session,
|
||||||
|
actions: { signOut }
|
||||||
|
} = useSession()
|
||||||
|
|
||||||
const quit = () => {
|
const quit = () => {
|
||||||
signOut()
|
signOut()
|
||||||
hideModal()
|
hideModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const { session } = useAuthStore()
|
|
||||||
|
|
||||||
const author = createMemo<Author>(() => {
|
const author = createMemo<Author>(() => {
|
||||||
const a: Author = {
|
const a: Author = {
|
||||||
|
id: null,
|
||||||
name: 'anonymous',
|
name: 'anonymous',
|
||||||
userpic: '',
|
userpic: '',
|
||||||
slug: ''
|
slug: ''
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
import { Popup, PopupProps } from './Popup'
|
import { useSession } from '../../context/session'
|
||||||
import { signOut, useAuthStore } from '../../stores/auth'
|
import type { PopupProps } from '../_shared/Popup'
|
||||||
import styles from './Popup.module.scss'
|
import { Popup } from '../_shared/Popup'
|
||||||
|
import styles from '../_shared/Popup.module.scss'
|
||||||
|
|
||||||
type ProfilePopupProps = Omit<PopupProps, 'children'>
|
type ProfilePopupProps = Omit<PopupProps, 'children'>
|
||||||
|
|
||||||
export const ProfilePopup = (props: ProfilePopupProps) => {
|
export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
const { session } = useAuthStore()
|
const {
|
||||||
|
session,
|
||||||
|
actions: { signOut }
|
||||||
|
} = useSession()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup {...props} horizontalAnchor="right">
|
<Popup {...props} horizontalAnchor="right">
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<li>
|
||||||
<a href={`/${session().user?.slug}`}>Профиль</a>
|
<a href={`/author/${session().user?.slug}`}>Профиль</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#">Черновики</a>
|
<a href="#">Черновики</a>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Icon } from './Icon'
|
||||||
import './Topics.scss'
|
import './Topics.scss'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
|
import { handleClientRouteLinkClick } from '../../stores/router'
|
||||||
|
|
||||||
export const NavTopics = (props: { topics: Topic[] }) => {
|
export const NavTopics = (props: { topics: Topic[] }) => {
|
||||||
const tag = (topic: Topic) =>
|
const tag = (topic: Topic) =>
|
||||||
|
@ -17,7 +18,7 @@ export const NavTopics = (props: { topics: Topic[] }) => {
|
||||||
<For each={props.topics}>
|
<For each={props.topics}>
|
||||||
{(topic) => (
|
{(topic) => (
|
||||||
<li class="item">
|
<li class="item">
|
||||||
<a href={`/topic/${topic.slug}`}>
|
<a href={`/topic/${topic.slug}`} onClick={handleClientRouteLinkClick}>
|
||||||
<span>#{tag(topic)}</span>
|
<span>#{tag(topic)}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -5,10 +5,11 @@ export const ConnectPage = () => {
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<article class="container container--static-page">
|
<article class="container container--static-page">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h1 class="col-md-8 offset-md-2">
|
<div class="col-sm-10 col-md-8 col-lg-7 col-xl-6 shift-content">
|
||||||
|
<h1>
|
||||||
<span class="wrapped">Предложить идею</span>
|
<span class="wrapped">Предложить идею</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="col-md-8 col-lg-6 offset-md-3">
|
|
||||||
<p>
|
<p>
|
||||||
Хотите что-то предложить, обсудить или посоветовать? Поделиться темой или идеей? Напишите нам
|
Хотите что-то предложить, обсудить или посоветовать? Поделиться темой или идеей? Напишите нам
|
||||||
скорее! Если укажете свою почту, мы обязательно ответим.
|
скорее! Если укажете свою почту, мы обязательно ответим.
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { loadTopic } from '../../stores/zine/topics'
|
||||||
import { Loading } from '../Loading'
|
import { Loading } from '../Loading'
|
||||||
|
|
||||||
export const TopicPage = (props: PageProps) => {
|
export const TopicPage = (props: PageProps) => {
|
||||||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.authorArticles) && Boolean(props.author))
|
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.topicArticles) && Boolean(props.topic))
|
||||||
|
|
||||||
const slug = createMemo(() => {
|
const slug = createMemo(() => {
|
||||||
const { page: getPage } = useRouter()
|
const { page: getPage } = useRouter()
|
||||||
|
|
|
@ -7,12 +7,11 @@ export const DiscussionRulesPage = () => {
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<article class="container container--static-page">
|
<article class="container container--static-page">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 offset-md-2">
|
<div class="col-md-6 col-xl-7 shift-content order-md-first">
|
||||||
<h1>
|
<h1>
|
||||||
<span class="wrapped" innerHTML={title} />
|
<span class="wrapped" innerHTML={title} />
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="col-lg-10 offset-md-1">
|
|
||||||
<p>
|
<p>
|
||||||
Открытая редакция существует благодаря дружному сообществу авторов
|
Открытая редакция существует благодаря дружному сообществу авторов
|
||||||
и читателей — вдумчивых и сознательных людей, приверженных ценностям
|
и читателей — вдумчивых и сознательных людей, приверженных ценностям
|
||||||
|
@ -30,9 +29,9 @@ export const DiscussionRulesPage = () => {
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
Оскорбления, личные нападки, травля и угрозы. В любом виде. Конкретного
|
Оскорбления, личные нападки, травля и угрозы. В любом виде. Конкретного человека
|
||||||
человека или социальной группы — не суть. Агрессия, переход
|
или социальной группы — не суть. Агрессия, переход на личности
|
||||||
на личности и токсичность едва ли способствуют плодотворному общению.
|
и токсичность едва ли способствуют плодотворному общению.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@ -46,19 +45,19 @@ export const DiscussionRulesPage = () => {
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
Спам, реклама, фейкньюз, ссылки на пропагандистские СМИ, вбросы дезинформации,
|
Спам, реклама, фейкньюз, ссылки на пропагандистские СМИ, вбросы дезинформации,
|
||||||
специально уводящий от темы флуд, провокации, разжигание конфликтов, намеренный
|
специально уводящий от темы флуд, провокации, разжигание конфликтов, намеренный срыв
|
||||||
срыв дискуссий.
|
дискуссий.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
Неаргументированная критика и комментарии вроде «отстой», «зачем
|
Неаргументированная критика и комментарии вроде «отстой», «зачем
|
||||||
я это увидел/а», «не читал, но осуждаю»,
|
я это увидел/а», «не читал, но осуждаю», «либераху
|
||||||
«либераху порвало», «лол», «скатились»,
|
порвало», «лол», «скатились», «первый нах»
|
||||||
«первый нах» и тому подобные. Односложные реплики не подразумевают
|
и тому подобные. Односложные реплики не подразумевают возможность обогащающего
|
||||||
возможность обогащающего диалога, не продуктивны и никак не помогают
|
диалога, не продуктивны и никак не помогают авторам делать материалы лучше,
|
||||||
авторам делать материалы лучше, а читателям — разобраться.
|
а читателям — разобраться.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -114,7 +113,6 @@ export const DiscussionRulesPage = () => {
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,8 +7,8 @@ export const DogmaPage = () => {
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<article class="container container--static-page">
|
<article class="container container--static-page">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h4 class="col-md-8 offset-md-2">Редакционные принципы</h4>
|
<div class="col-md-6 col-xl-7 shift-content order-md-first">
|
||||||
<div class="col-md-8 col-lg-6 offset-md-3">
|
<h4>Редакционные принципы</h4>
|
||||||
<p>
|
<p>
|
||||||
Дискурс - журнал с открытой горизонтальной редакцией. Содержание журнала определяется прямым
|
Дискурс - журнал с открытой горизонтальной редакцией. Содержание журнала определяется прямым
|
||||||
голосованием его авторов. Мы нередко занимаем различные позиции по разным проблемам, но
|
голосованием его авторов. Мы нередко занимаем различные позиции по разным проблемам, но
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
|
import { createSignal, Show } from 'solid-js'
|
||||||
import { MainLayout } from '../../Layouts/MainLayout'
|
import { MainLayout } from '../../Layouts/MainLayout'
|
||||||
import { t } from '../../../utils/intl'
|
import { t } from '../../../utils/intl'
|
||||||
|
import { Icon } from '../../Nav/Icon'
|
||||||
|
|
||||||
export const GuidePage = () => {
|
export const GuidePage = () => {
|
||||||
const title = t('How it works')
|
const title = t('How it works')
|
||||||
|
|
||||||
|
const [indexExpanded, setIndexExpanded] = createSignal(true)
|
||||||
|
|
||||||
|
const toggleIndexExpanded = () => setIndexExpanded((oldExpanded) => !oldExpanded)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
{/*<Meta name="description" content={title} />*/}
|
{/*<Meta name="description" content={title} />*/}
|
||||||
|
@ -16,8 +22,20 @@ export const GuidePage = () => {
|
||||||
|
|
||||||
<article class="container container--static-page">
|
<article class="container container--static-page">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-2">
|
<div class="col-md-4 col-lg-3 order-md-last">
|
||||||
|
<button class="button button--content-index" onClick={toggleIndexExpanded}>
|
||||||
|
<Show when={!indexExpanded()}>
|
||||||
|
<Icon name="content-index-control" />
|
||||||
|
</Show>
|
||||||
|
<Show when={indexExpanded()}>
|
||||||
|
<Icon name="content-index-control-expanded" class={'expanded'} />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={indexExpanded()}>
|
||||||
<nav class="content-index">
|
<nav class="content-index">
|
||||||
|
<h4>Оглавление</h4>
|
||||||
|
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<li>
|
||||||
<a href="#how-it-works">{title}</a>
|
<a href="#how-it-works">{title}</a>
|
||||||
|
@ -39,38 +57,38 @@ export const GuidePage = () => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-6 col-xl-7 shift-content order-md-first">
|
||||||
<h1 id="about">
|
<h1 id="about">
|
||||||
<span class="wrapped">Как устроен Дискурс</span>
|
<span class="wrapped">Как устроен Дискурс</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="col-lg-10 offset-md-1">
|
|
||||||
<p>
|
<p>
|
||||||
Дискурс — независимый журнал о культуре, науке, искусстве и обществе
|
Дискурс — независимый журнал о культуре, науке, искусстве и обществе
|
||||||
с <a href="/about/manifest">открытой редакцией</a>. У нас нет главного редактора,
|
с <a href="/about/manifest">открытой редакцией</a>. У нас нет главного редактора,
|
||||||
инвестора и вообще никого, кто бы принимал единоличные решения. Вместо
|
инвестора и вообще никого, кто бы принимал единоличные решения. Вместо традиционных
|
||||||
традиционных иерархий Дискурс основан на принципах прямой демократии: в нашем
|
иерархий Дискурс основан на принципах прямой демократии: в нашем горизонтальном
|
||||||
горизонтальном сообществе все редакционные вопросы решаются открытым голосованием авторов
|
сообществе все редакционные вопросы решаются открытым голосованием авторов журнала. Вот как
|
||||||
журнала. Вот как это работает.
|
это работает.
|
||||||
</p>
|
</p>
|
||||||
<h3 id="how-it-works">Как устроен сайт Дискурса</h3>
|
<h3 id="how-it-works">Как устроен сайт Дискурса</h3>
|
||||||
<p>Дискурс состоит из четырех основных разделов:</p>
|
<p>Дискурс состоит из четырех основных разделов:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<a href="/topics">Темы</a> — у нас публикуются исследования, обзоры,
|
<a href="/topics">Темы</a> — у нас публикуются исследования, обзоры, эссе,
|
||||||
эссе, интервью, репортажи, аналитика и другие материалы о культуре, науке,
|
интервью, репортажи, аналитика и другие материалы о культуре, науке, искусстве
|
||||||
искусстве и обществе.
|
и обществе.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<a href="/topic/art">Искусство</a> — здесь, например, представлены
|
<a href="/topic/art">Искусство</a> — здесь, например, представлены
|
||||||
художественные произведения: литература, живопись, музыка, фотографии, видео. Этот
|
художественные произведения: литература, живопись, музыка, фотографии, видео. Этот раздел
|
||||||
раздел помогает прозвучать новому искусству, которое создают российские художники,
|
помогает прозвучать новому искусству, которое создают российские художники, писатели,
|
||||||
писатели, режиссёры и музыканты.
|
режиссёры и музыканты.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
{/*
|
{/*
|
||||||
|
@ -105,31 +123,31 @@ export const GuidePage = () => {
|
||||||
<a href="/topic/history">история</a>, <a href="/topic/absurdism">абсурдизм</a>,{' '}
|
<a href="/topic/history">история</a>, <a href="/topic/absurdism">абсурдизм</a>,{' '}
|
||||||
<a href="/topic/sex">секс</a> и т.д.) или в серии (как «
|
<a href="/topic/sex">секс</a> и т.д.) или в серии (как «
|
||||||
<a href="/topic/zakony-mira">Законы мира</a>» или «
|
<a href="/topic/zakony-mira">Законы мира</a>» или «
|
||||||
<a href="/topic/za-liniey-mannergeyma">За линией Маннергейма</a>»). Темы
|
<a href="/topic/za-liniey-mannergeyma">За линией Маннергейма</a>»). Темы объединяют
|
||||||
объединяют сотни публикаций, помогают ориентироваться в журнале и следить
|
сотни публикаций, помогают ориентироваться в журнале и следить за интересными
|
||||||
за интересными материалами.
|
материалами.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 id="become-author">Как стать автором журнала</h3>
|
<h3 id="become-author">Как стать автором журнала</h3>
|
||||||
<p>
|
<p>
|
||||||
Дискурс объединяет журналистов, активистов, музыкантов, художников, фотографов,
|
Дискурс объединяет журналистов, активистов, музыкантов, художников, фотографов, режиссеров,
|
||||||
режиссеров, философов, ученых и других замечательных людей. Каждый может{' '}
|
философов, ученых и других замечательных людей. Каждый может{' '}
|
||||||
<a href="/create">прислать</a>
|
<a href="/create">прислать</a>
|
||||||
свой материал в журнал. Формат и тематика не имеют значения, единственное,
|
свой материал в журнал. Формат и тематика не имеют значения, единственное,
|
||||||
что важно — <a href="/how-to-write-a-good-article">хороший</a> ли материал.
|
что важно — <a href="/how-to-write-a-good-article">хороший</a> ли материал. Если
|
||||||
Если сообщество поддержит вашу публикацию, она выйдет в журнале и станет
|
сообщество поддержит вашу публикацию, она выйдет в журнале и станет доступна
|
||||||
доступна тысячам наших читателей.
|
тысячам наших читателей.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<h3 id="voting">Как проходит голосование</h3>
|
<h3 id="voting">Как проходит голосование</h3>
|
||||||
<p>
|
<p>
|
||||||
Все присылаемые в Дискурс материалы попадают в
|
Все присылаемые в Дискурс материалы попадают в
|
||||||
<strong>«Редакцию»</strong>. Это внутренний раздел сайта, где участники
|
<strong>«Редакцию»</strong>. Это внутренний раздел сайта, где участники сообщества
|
||||||
сообщества решают, что будет опубликовано в Дискурсе. Как только работа получает
|
решают, что будет опубликовано в Дискурсе. Как только работа получает одобрение как
|
||||||
одобрение как минимум пятерых авторов открытой редакции, она немедленно публикуется
|
минимум пятерых авторов открытой редакции, она немедленно публикуется в журнале.
|
||||||
в журнале. Если же материал набирает более 20% голосов «против»,
|
Если же материал набирает более 20% голосов «против»,
|
||||||
он не выходит и может быть отправлен на доработку. Жестких сроков
|
он не выходит и может быть отправлен на доработку. Жестких сроков
|
||||||
рассмотрения материалов у нас нет, иногда это занимает час, иногда месяц,
|
рассмотрения материалов у нас нет, иногда это занимает час, иногда месяц,
|
||||||
обычно — несколько дней.
|
обычно — несколько дней.
|
||||||
|
@ -145,17 +163,17 @@ export const GuidePage = () => {
|
||||||
<p>
|
<p>
|
||||||
Дискурс — журнал с совместным редактированием. Совершенствовать тексты нам
|
Дискурс — журнал с совместным редактированием. Совершенствовать тексты нам
|
||||||
помогает <b>система ремарок</b>. Вы можете выделить часть текста в любой статье
|
помогает <b>система ремарок</b>. Вы можете выделить часть текста в любой статье
|
||||||
и оставить к ней замечание, вопрос или предложение — автор текста
|
и оставить к ней замечание, вопрос или предложение — автор текста получит
|
||||||
получит совет на почту и сможет его учесть. Так мы устраняем опечатки,
|
совет на почту и сможет его учесть. Так мы устраняем опечатки, неточности
|
||||||
неточности и советуем друг другу, как сделать тексты качественнее и интереснее.
|
и советуем друг другу, как сделать тексты качественнее и интереснее.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Среди участников сообщества есть профессиональные редакторы, которые помогают авторам делать
|
Среди участников сообщества есть профессиональные редакторы, которые помогают авторам делать
|
||||||
тексты лучше. Если вашему материалу потребуется доработка, они помогут отредактировать
|
тексты лучше. Если вашему материалу потребуется доработка, они помогут отредактировать текст,
|
||||||
текст, подобрать иллюстрации, придумать заголовок и красиво сверстать публикацию. Если
|
подобрать иллюстрации, придумать заголовок и красиво сверстать публикацию. Если
|
||||||
вы хотите обсудить текст, прежде чем загрузить материал в
|
вы хотите обсудить текст, прежде чем загрузить материал в интернет-редакцию —
|
||||||
интернет-редакцию — разместите его в google-документе, откройте доступ
|
разместите его в google-документе, откройте доступ к редактированию по ссылке
|
||||||
к редактированию по ссылке и напишите нам на
|
и напишите нам на
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
</a>
|
</a>
|
||||||
|
@ -180,10 +198,9 @@ export const GuidePage = () => {
|
||||||
<p>
|
<p>
|
||||||
<strong>Право определять, каким будет журнал</strong>. Дискурс — это
|
<strong>Право определять, каким будет журнал</strong>. Дискурс — это
|
||||||
общественная институция, созданная людьми и ради людей, функционирующая
|
общественная институция, созданная людьми и ради людей, функционирующая
|
||||||
на условиях прямой демократии. Авторы публикуют статьи и художественные
|
на условиях прямой демократии. Авторы публикуют статьи и художественные проекты,
|
||||||
проекты, участвуют в обсуждениях, голосуют за работы коллег и таким
|
участвуют в обсуждениях, голосуют за работы коллег и таким образом вносят
|
||||||
образом вносят свой вклад в развитие проекта, определяя содержание
|
свой вклад в развитие проекта, определяя содержание и направление журнала.
|
||||||
и направление журнала.
|
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -195,9 +212,9 @@ export const GuidePage = () => {
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>Поддержка редакции</strong>. Дискурс предоставляет авторам аккредитацию
|
<strong>Поддержка редакции</strong>. Дискурс предоставляет авторам аккредитацию
|
||||||
на мероприятия, базу контактов, юридическую поддержку, ознакомление
|
на мероприятия, базу контактов, юридическую поддержку, ознакомление с книжными,
|
||||||
с книжными, кино- и музыкальными новинками до их выхода в свет.
|
кино- и музыкальными новинками до их выхода в свет. Если что-то
|
||||||
Если что-то из этого вам понадобится, пишите на почту{' '}
|
из этого вам понадобится, пишите на почту{' '}
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
</a>
|
</a>
|
||||||
|
@ -209,29 +226,29 @@ export const GuidePage = () => {
|
||||||
<strong>Пресс-карты для корреспондентов</strong>. Три опубликованные статьи позволяют
|
<strong>Пресс-карты для корреспондентов</strong>. Три опубликованные статьи позволяют
|
||||||
авторам Дискурса получить официальные удостоверения журналистов (пресс-карты)
|
авторам Дискурса получить официальные удостоверения журналистов (пресс-карты)
|
||||||
на следующий год. Пресс-карты удостоверяют, что вы журналист и можете
|
на следующий год. Пресс-карты удостоверяют, что вы журналист и можете
|
||||||
пользоваться всеми теми правами, которые гарантирует Закон о СМИ. Кроме того,
|
пользоваться всеми теми правами, которые гарантирует Закон о СМИ. Кроме того, многие
|
||||||
многие культурные институции (музеи, галереи и др.) предоставляют журналистам право
|
культурные институции (музеи, галереи и др.) предоставляют журналистам право
|
||||||
свободного входа.
|
свободного входа.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>Помощь сотен специалистов в разных областях</strong>. В основе
|
<strong>Помощь сотен специалистов в разных областях</strong>. В основе Дискурса
|
||||||
Дискурса лежит идея совместного редактирования. Участники редакционного
|
лежит идея совместного редактирования. Участники редакционного сообщества —
|
||||||
сообщества — несколько сотен журналистов, исследователей, художников,
|
несколько сотен журналистов, исследователей, художников, литераторов из разных стран
|
||||||
литераторов из разных стран — изучают материалы друг друга до публикации
|
— изучают материалы друг друга до публикации и помогают сделать
|
||||||
и помогают сделать их качественнее и интереснее. Так, в редакции
|
их качественнее и интереснее. Так, в редакции нередко складываются
|
||||||
нередко складываются творческие союзы: например, авторов текстов и художников,
|
творческие союзы: например, авторов текстов и художников, создающих для них
|
||||||
создающих для них иллюстрации.
|
иллюстрации.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>Пространство общения полное выдающихся людей</strong>. Дискурс —
|
<strong>Пространство общения полное выдающихся людей</strong>. Дискурс —
|
||||||
большое живое сообщество интеллектуалов, разбросанных по всему земному шару.
|
большое живое сообщество интеллектуалов, разбросанных по всему земному шару. Вступив
|
||||||
Вступив в редакцию, вы сможете познакомиться со множеством интересных
|
в редакцию, вы сможете познакомиться со множеством интересных людей,
|
||||||
людей, которые определяют повестку завтрашнего дня, вдохновляют окружающих, создают
|
которые определяют повестку завтрашнего дня, вдохновляют окружающих, создают новое
|
||||||
новое и изучают старое, ищут знания и готовы ими делиться, чтобы менять мир
|
и изучают старое, ищут знания и готовы ими делиться, чтобы менять мир
|
||||||
в соответствии со своими идеалами.
|
в соответствии со своими идеалами.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
@ -265,7 +282,6 @@ export const GuidePage = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
|
import { createSignal, Show } from 'solid-js'
|
||||||
import { MainLayout } from '../../Layouts/MainLayout'
|
import { MainLayout } from '../../Layouts/MainLayout'
|
||||||
import { Donate } from '../../Discours/Donate'
|
import { Donate } from '../../Discours/Donate'
|
||||||
|
import { Icon } from '../../Nav/Icon'
|
||||||
|
|
||||||
// const title = t('Support us')
|
// const title = t('Support us')
|
||||||
|
|
||||||
export const HelpPage = () => {
|
export const HelpPage = () => {
|
||||||
|
const [indexExpanded, setIndexExpanded] = createSignal(true)
|
||||||
|
|
||||||
|
const toggleIndexExpanded = () => setIndexExpanded((oldExpanded) => !oldExpanded)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
{/*<Meta name="description">Здесь можно поддержать Дискурс материально.</Meta>*/}
|
{/*<Meta name="description">Здесь можно поддержать Дискурс материально.</Meta>*/}
|
||||||
|
@ -13,8 +19,20 @@ export const HelpPage = () => {
|
||||||
|
|
||||||
<article class="container container--static-page discours-help">
|
<article class="container container--static-page discours-help">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-2">
|
<div class="col-md-4 col-lg-3 order-md-last">
|
||||||
|
<button class="button button--content-index" onClick={toggleIndexExpanded}>
|
||||||
|
<Show when={!indexExpanded()}>
|
||||||
|
<Icon name="content-index-control" />
|
||||||
|
</Show>
|
||||||
|
<Show when={indexExpanded()}>
|
||||||
|
<Icon name="content-index-control-expanded" class={'expanded'} />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={indexExpanded()}>
|
||||||
<nav class="content-index">
|
<nav class="content-index">
|
||||||
|
<h4>Оглавление</h4>
|
||||||
|
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<li>
|
||||||
<a href="#help-us">Как вы можете поддержать Дискурс?</a>
|
<a href="#help-us">Как вы можете поддержать Дискурс?</a>
|
||||||
|
@ -30,14 +48,14 @@ export const HelpPage = () => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-6 col-xl-7 shift-content order-md-first">
|
||||||
<h1 id="help-us">
|
<h1 id="help-us">
|
||||||
<span class="wrapped">Как вы можете поддержать Дискурс?</span>
|
<span class="wrapped">Как вы можете поддержать Дискурс?</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="col-lg-10 offset-md-1">
|
|
||||||
<p>
|
<p>
|
||||||
Дискурс — уникальное независимое издание с горизонтальной редакцией,
|
Дискурс — уникальное независимое издание с горизонтальной редакцией,
|
||||||
существующее в интересах своих читателей. Ваша поддержка действительно много
|
существующее в интересах своих читателей. Ваша поддержка действительно много
|
||||||
|
@ -65,9 +83,9 @@ export const HelpPage = () => {
|
||||||
<p>
|
<p>
|
||||||
Ваши пожертвования пойдут на оплату серверов, содержание офиса, зарплату редакции
|
Ваши пожертвования пойдут на оплату серверов, содержание офиса, зарплату редакции
|
||||||
и налоги, оплату юридического сопровождения и труда бухгалтера, совершенствование
|
и налоги, оплату юридического сопровождения и труда бухгалтера, совершенствование
|
||||||
сайта, аренду помещения для открытой редакции, на печать альманаха Дискурс
|
сайта, аренду помещения для открытой редакции, на печать альманаха Дискурс с лучшими
|
||||||
с лучшими текстами авторов за полгода, а также на другие редакционные
|
текстами авторов за полгода, а также на другие редакционные и технические
|
||||||
и технические расходы.
|
расходы.
|
||||||
</p>
|
</p>
|
||||||
<h3>Ваша помощь позволит нам</h3>
|
<h3>Ваша помощь позволит нам</h3>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -142,7 +160,6 @@ export const HelpPage = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
|
import { createSignal, Show } from 'solid-js'
|
||||||
import { MainLayout } from '../../Layouts/MainLayout'
|
import { MainLayout } from '../../Layouts/MainLayout'
|
||||||
import { Modal } from '../../Nav/Modal'
|
import { Modal } from '../../Nav/Modal'
|
||||||
import { Feedback } from '../../Discours/Feedback'
|
import { Feedback } from '../../Discours/Feedback'
|
||||||
import Subscribe from '../../Discours/Subscribe'
|
import Subscribe from '../../Discours/Subscribe'
|
||||||
import Opener from '../../Nav/Opener'
|
import Opener from '../../Nav/Opener'
|
||||||
|
import { Icon } from '../../Nav/Icon'
|
||||||
|
|
||||||
// title={t('Manifest')}
|
// title={t('Manifest')}
|
||||||
|
|
||||||
export const ManifestPage = () => {
|
export const ManifestPage = () => {
|
||||||
|
const [indexExpanded, setIndexExpanded] = createSignal(true)
|
||||||
|
|
||||||
|
const toggleIndexExpanded = () => setIndexExpanded((oldExpanded) => !oldExpanded)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<Modal name="feedback">
|
<Modal name="feedback">
|
||||||
|
@ -17,8 +23,20 @@ export const ManifestPage = () => {
|
||||||
</Modal>
|
</Modal>
|
||||||
<article class="container container--static-page">
|
<article class="container container--static-page">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-2">
|
<div class="col-md-4 col-lg-3 order-md-last">
|
||||||
|
<button class="button button--content-index" onClick={toggleIndexExpanded}>
|
||||||
|
<Show when={!indexExpanded()}>
|
||||||
|
<Icon name="content-index-control" />
|
||||||
|
</Show>
|
||||||
|
<Show when={indexExpanded()}>
|
||||||
|
<Icon name="content-index-control-expanded" class={'expanded'} />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={indexExpanded()}>
|
||||||
<nav class="content-index">
|
<nav class="content-index">
|
||||||
|
<h4>Оглавление</h4>
|
||||||
|
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<li>
|
||||||
<a href="#manifest">Манифест</a>
|
<a href="#manifest">Манифест</a>
|
||||||
|
@ -45,14 +63,14 @@ export const ManifestPage = () => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-6 col-xl-7 shift-content order-md-first">
|
||||||
<h1 id="manifest">
|
<h1 id="manifest">
|
||||||
<span class="wrapped">Манифест</span>
|
<span class="wrapped">Манифест</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="col-lg-10 offset-md-1">
|
|
||||||
<p>
|
<p>
|
||||||
Дискурс — независимый художественно-аналитический журнал с горизонтальной
|
Дискурс — независимый художественно-аналитический журнал с горизонтальной
|
||||||
редакцией, основанный на принципах свободы слова, прямой демократии и совместного
|
редакцией, основанный на принципах свободы слова, прямой демократии и совместного
|
||||||
|
@ -78,19 +96,20 @@ export const ManifestPage = () => {
|
||||||
рассказывать свои истории так, чтобы они были услышаны. Мы убеждены: чем больше голосов
|
рассказывать свои истории так, чтобы они были услышаны. Мы убеждены: чем больше голосов
|
||||||
будет звучать на Дискурсе, тем громче в полифонии мнений будет слышна истина.
|
будет звучать на Дискурсе, тем громче в полифонии мнений будет слышна истина.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="h2" id="participation">
|
<h2 class="h2" id="participation">
|
||||||
<span class="wrapped">Как участвовать в самиздате</span>
|
<span class="wrapped">Как участвовать в самиздате</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="col-lg-10 offset-md-1">
|
|
||||||
<p>
|
<p>
|
||||||
Дискурс создается <a href="/about/guide">открытым сообществом</a> энтузиастов новой
|
Дискурс создается <a href="/about/guide">открытым сообществом</a> энтузиастов новой
|
||||||
независимой журналистики. Участвовать в открытой редакции и помогать журналу можно
|
независимой журналистики. Участвовать в открытой редакции и помогать журналу можно
|
||||||
следующими способами:
|
следующими способами:
|
||||||
</p>
|
</p>
|
||||||
|
<details open>
|
||||||
|
<summary>
|
||||||
<h3 id="contribute">Предлагать материалы</h3>
|
<h3 id="contribute">Предлагать материалы</h3>
|
||||||
|
</summary>
|
||||||
<p>
|
<p>
|
||||||
<a href="/create">Создавайте</a> свои статьи и художественные работы —
|
<a href="/create">Создавайте</a> свои статьи и художественные работы —
|
||||||
лучшие из них будут опубликованы в журнале. Дискурс — некоммерческое
|
лучшие из них будут опубликованы в журнале. Дискурс — некоммерческое
|
||||||
|
@ -98,7 +117,12 @@ export const ManifestPage = () => {
|
||||||
<a href="/create?collab=true">поддержку</a> редакции, право голоса, множество других
|
<a href="/create?collab=true">поддержку</a> редакции, право голоса, множество других
|
||||||
возможностей и читателей по всему миру.
|
возможностей и читателей по всему миру.
|
||||||
</p>
|
</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
<h3 id="donate">Поддерживать проект</h3>
|
<h3 id="donate">Поддерживать проект</h3>
|
||||||
|
</summary>
|
||||||
<p>
|
<p>
|
||||||
Дискурс существует на пожертвования читателей. Если вам нравится журнал, пожалуйста,
|
Дискурс существует на пожертвования читателей. Если вам нравится журнал, пожалуйста,
|
||||||
</p>
|
</p>
|
||||||
|
@ -106,7 +130,12 @@ export const ManifestPage = () => {
|
||||||
<a href="/about/help">поддержите</a> нашу работу. Ваши пожертвования пойдут на выпуск
|
<a href="/about/help">поддержите</a> нашу работу. Ваши пожертвования пойдут на выпуск
|
||||||
новых материалов, оплату серверов, труда программистов, дизайнеров и редакторов.
|
новых материалов, оплату серверов, труда программистов, дизайнеров и редакторов.
|
||||||
</p>
|
</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
<h3 id="cooperation">Сотрудничать с журналом</h3>
|
<h3 id="cooperation">Сотрудничать с журналом</h3>
|
||||||
|
</summary>
|
||||||
<p>
|
<p>
|
||||||
Мы всегда открыты для сотрудничества и рады единомышленникам. Если вы хотите помогать
|
Мы всегда открыты для сотрудничества и рады единомышленникам. Если вы хотите помогать
|
||||||
журналу с редактурой, корректурой, иллюстрациями, переводами, версткой, подкастами,
|
журналу с редактурой, корректурой, иллюстрациями, переводами, версткой, подкастами,
|
||||||
|
@ -125,7 +154,12 @@ export const ManifestPage = () => {
|
||||||
и медиаинструментов находится{' '}
|
и медиаинструментов находится{' '}
|
||||||
<a href="https://github.com/Discours">в свободном доступе на GitHub</a>.
|
<a href="https://github.com/Discours">в свободном доступе на GitHub</a>.
|
||||||
</p>
|
</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
<h3 id="follow">Как еще можно помочь</h3>
|
<h3 id="follow">Как еще можно помочь</h3>
|
||||||
|
</summary>
|
||||||
<p>
|
<p>
|
||||||
Советуйте Дискурс друзьям и знакомым. Обсуждайте и распространяйте наши
|
Советуйте Дискурс друзьям и знакомым. Обсуждайте и распространяйте наши
|
||||||
публикации — все материалы открытой редакции можно читать и перепечатывать
|
публикации — все материалы открытой редакции можно читать и перепечатывать
|
||||||
|
@ -141,19 +175,19 @@ export const ManifestPage = () => {
|
||||||
интересными темами, о которых хотели бы узнать больше, и историями, которые нужно
|
интересными темами, о которых хотели бы узнать больше, и историями, которые нужно
|
||||||
рассказать.
|
рассказать.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</details>
|
||||||
|
|
||||||
<h2 class="h2" id="connection">
|
<h2 class="h2" id="connection">
|
||||||
<span class="wrapped">Будем на связи</span>
|
<span class="wrapped">Будем на связи</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="col-lg-10 offset-md-1">
|
<p>
|
||||||
Если вы хотите предложить материал, сотрудничать, рассказать о проблеме, которую нужно
|
Если вы хотите предложить материал, сотрудничать, рассказать о проблеме, которую нужно
|
||||||
осветить, сообщить об ошибке или баге, что-то обсудить, уточнить или посоветовать,
|
осветить, сообщить об ошибке или баге, что-то обсудить, уточнить или посоветовать,
|
||||||
пожалуйста, <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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -8,8 +8,9 @@ export const PartnersPage = () => {
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<article class="container container--static-page">
|
<article class="container container--static-page">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 offset-md-2">{t('Partners')}</div>
|
<div class="col-md-6 col-xl-7 shift-content order-md-first">
|
||||||
<div class="col-md-8 col-lg-6 offset-md-3" />
|
<h1>{t('Partners')}</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|
|
@ -7,20 +7,19 @@ export const PrinciplesPage = () => {
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<article class="container container--static-page">
|
<article class="container container--static-page">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 offset-md-2">
|
<div class="col-md-6 col-xl-7 shift-content order-md-first">
|
||||||
<h1>
|
<h1>
|
||||||
<span class="wrapped">{title}</span>
|
<span class="wrapped">{title}</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="col-lg-10 offset-md-1">
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>Горизонтальность</strong>. Мы все разные, и это классно. Вертикалей
|
<strong>Горизонтальность</strong>. Мы все разные, и это классно. Вертикалей
|
||||||
в мире достаточно, мы — горизонтальное сообщество и ценим наши
|
в мире достаточно, мы — горизонтальное сообщество и ценим наши
|
||||||
различия, потому что знаем — в них наша сила. Благодаря разнообразию
|
различия, потому что знаем — в них наша сила. Благодаря разнообразию сотен
|
||||||
сотен голосов, усиливающих друг друга, в сообществе складывается неповторимая
|
голосов, усиливающих друг друга, в сообществе складывается неповторимая синергия,
|
||||||
синергия, которая помогает вместе достигать большего.
|
которая помогает вместе достигать большего.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -36,34 +35,32 @@ export const PrinciplesPage = () => {
|
||||||
в мире было еще больше хорошего. Обсуждая что-то, мы всегда интересуемся, чем
|
в мире было еще больше хорошего. Обсуждая что-то, мы всегда интересуемся, чем
|
||||||
можем помочь. В самиздате можно найти специалистов практически в любых сферах
|
можем помочь. В самиздате можно найти специалистов практически в любых сферах
|
||||||
и получить поддержку от сотен людей. Благодаря коллективной экспертизе
|
и получить поддержку от сотен людей. Благодаря коллективной экспертизе
|
||||||
глобального сообщества в самиздате выходят крутейшие публикации, которыми можно
|
глобального сообщества в самиздате выходят крутейшие публикации, которыми можно вечно
|
||||||
вечно гордиться.
|
гордиться.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>Взаимоуважение</strong>. Мы ценим, искренне уважаем друг друга
|
<strong>Взаимоуважение</strong>. Мы ценим, искренне уважаем друг друга и вместо
|
||||||
и вместо борщевиков враждебности культивируем цветы добра, мира, знания
|
борщевиков враждебности культивируем цветы добра, мира, знания и юмора. Нам некогда
|
||||||
и юмора. Нам некогда доказывать друг другу, кто круче. Гораздо приятнее
|
доказывать друг другу, кто круче. Гораздо приятнее сотрудничать, помогать и создавать
|
||||||
сотрудничать, помогать и создавать что-то важное, интересное и полезное.
|
что-то важное, интересное и полезное.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>Созидание</strong>. Мы создаем, потому что любим создавать. Мы открыто
|
<strong>Созидание</strong>. Мы создаем, потому что любим создавать. Мы открыто
|
||||||
делимся опытом, дарим идеи, обмениваемся мнениями и благодарим за критику,
|
делимся опытом, дарим идеи, обмениваемся мнениями и благодарим за критику,
|
||||||
используя ее для совершенствования мастерства и саморазвития. Мы знаем,
|
используя ее для совершенствования мастерства и саморазвития. Мы знаем, что
|
||||||
что мир не идеальное место, и делаем всё возможное, чтобы он стал лучше.
|
мир не идеальное место, и делаем всё возможное, чтобы он стал лучше.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="h2" id="participation">
|
<h2 class="h2" id="participation">
|
||||||
<span class="wrapped">Как участвовать в самиздате</span>
|
<span class="wrapped">Как участвовать в самиздате</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="col-lg-10 offset-md-1">
|
|
||||||
<p>
|
<p>
|
||||||
Открытая редакция объединяет сотни потрясающих людей со всего мира, которые делают
|
Открытая редакция объединяет сотни потрясающих людей со всего мира, которые делают
|
||||||
крутейшие вещи. Это пространство, где доверяют, вдохновляют, исследуют и создают новое
|
крутейшие вещи. Это пространство, где доверяют, вдохновляют, исследуют и создают новое
|
||||||
|
@ -75,21 +72,21 @@ export const PrinciplesPage = () => {
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>Действуем, помогаем и делимся</strong>. В редакции мы создаем
|
<strong>Действуем, помогаем и делимся</strong>. В редакции мы создаем свои
|
||||||
свои проекты и помогаем другим создавать свои — советами, делом,
|
проекты и помогаем другим создавать свои — советами, делом, участием,
|
||||||
участием, вовлеченностью. Мы открыто делимся опытом, мнениями и идеями, потому
|
вовлеченностью. Мы открыто делимся опытом, мнениями и идеями, потому что ценим
|
||||||
что ценим силу сотрудничества и знаем, что идеи реализуются скорее, лучше
|
силу сотрудничества и знаем, что идеи реализуются скорее, лучше и веселее, если
|
||||||
и веселее, если над ними трудиться сообща.
|
над ними трудиться сообща.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>Общаемся дружелюбно</strong>. Помните, по ту сторону монитора
|
<strong>Общаемся дружелюбно</strong>. Помните, по ту сторону монитора находятся
|
||||||
находятся реальные люди. Неуважение ранит других так же, как ранило бы вас
|
реальные люди. Неуважение ранит других так же, как ранило бы вас самих. Поэтому
|
||||||
самих. Поэтому не стоит кричать (даже капслоком), заполнять эфир желчью
|
не стоит кричать (даже капслоком), заполнять эфир желчью и бросаться
|
||||||
и бросаться грубостями — так вы рискуете не только растерять
|
грубостями — так вы рискуете не только растерять доверие окружающих,
|
||||||
доверие окружающих, но и остаться непонятым.
|
но и остаться непонятым.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@ -109,22 +106,21 @@ export const PrinciplesPage = () => {
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>Решаем трудности не агрессией, а диалогом</strong>. Обесценивать
|
<strong>Решаем трудности не агрессией, а диалогом</strong>. Обесценивать мнения
|
||||||
мнения и оскорблять других людей только потому, что вы с ними
|
и оскорблять других людей только потому, что вы с ними
|
||||||
не согласны, — не лучший способ донести свою точку зрения. Конечно,
|
не согласны, — не лучший способ донести свою точку зрения. Конечно,
|
||||||
важно высказаться, если вас что-то не устраивает и откровенно бесит.
|
важно высказаться, если вас что-то не устраивает и откровенно бесит.
|
||||||
Но прежде чем сжигать оппонента гневом, попробуйте понять, почему этот
|
Но прежде чем сжигать оппонента гневом, попробуйте понять, почему этот
|
||||||
«нехороший человек» так поступает. Возможно, аргументы собеседника окажутся
|
«нехороший человек» так поступает. Возможно, аргументы собеседника окажутся
|
||||||
убедительными или вам удастся изменить его мнение. В любом случае конфликты
|
убедительными или вам удастся изменить его мнение. В любом случае конфликты решаются
|
||||||
решаются в диалогах и проходят, а налаженное взаимопонимание останется
|
в диалогах и проходят, а налаженное взаимопонимание останется надолго.
|
||||||
надолго.
|
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>Не переходим на личности — это признак токсичности</strong>
|
<strong>Не переходим на личности — это признак токсичности</strong>.
|
||||||
. Всегда мудрее обсуждать точку зрения человека, а не его самого, даже если
|
Всегда мудрее обсуждать точку зрения человека, а не его самого, даже если
|
||||||
он вам не импонирует. Предвзятое отношение ограничивает кругозор, добавляет
|
он вам не импонирует. Предвзятое отношение ограничивает кругозор, добавляет
|
||||||
преждевременные морщины и не помогает окружающим стать лучше. Вежливость
|
преждевременные морщины и не помогает окружающим стать лучше. Вежливость
|
||||||
и взаимоуважение — краеугольная основа вдумчивых и осмысленных
|
и взаимоуважение — краеугольная основа вдумчивых и осмысленных
|
||||||
|
@ -168,14 +164,13 @@ export const PrinciplesPage = () => {
|
||||||
не хватает, мы начинаем действовать — рассказываем об идее,
|
не хватает, мы начинаем действовать — рассказываем об идее,
|
||||||
находим единомышленников, готовим и запускаем проект. Так в сообществе
|
находим единомышленников, готовим и запускаем проект. Так в сообществе
|
||||||
становится на одну крутую активность больше. Так появилось наше сообщество. Так
|
становится на одну крутую активность больше. Так появилось наше сообщество. Так
|
||||||
появился самиздат и все проекты открытой редакции. Чтобы в сообществе
|
появился самиздат и все проекты открытой редакции. Чтобы в сообществе случилось
|
||||||
случилось что-то прекрасное, достаточно просто положить этому начало.
|
что-то прекрасное, достаточно просто положить этому начало.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,8 +8,9 @@ export const ProjectsPage = () => {
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<article class="container container--static-page">
|
<article class="container container--static-page">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 offset-md-2">{t('Projects')}</div>
|
<div class="col-md-6 col-xl-7 shift-content order-md-first">
|
||||||
<div class="col-md-8 col-lg-6 offset-md-3" />
|
<h1>{t('Projects')}</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
|
import { createSignal, Show } from 'solid-js'
|
||||||
import { MainLayout } from '../../Layouts/MainLayout'
|
import { MainLayout } from '../../Layouts/MainLayout'
|
||||||
|
import { Icon } from '../../Nav/Icon'
|
||||||
|
|
||||||
// const title = t('Terms of use')
|
// const title = t('Terms of use')
|
||||||
|
|
||||||
export const TermsOfUsePage = () => {
|
export const TermsOfUsePage = () => {
|
||||||
|
const [indexExpanded, setIndexExpanded] = createSignal(true)
|
||||||
|
|
||||||
|
const toggleIndexExpanded = () => setIndexExpanded((oldExpanded) => !oldExpanded)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
{/*<Meta name="description" content={title} />*/}
|
{/*<Meta name="description" content={title} />*/}
|
||||||
|
@ -11,8 +17,20 @@ export const TermsOfUsePage = () => {
|
||||||
{/*<Meta property="og:description" content={title} />*/}
|
{/*<Meta property="og:description" content={title} />*/}
|
||||||
<article class="container container--static-page">
|
<article class="container container--static-page">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-2">
|
<div class="col-md-4 col-lg-3 order-md-last">
|
||||||
|
<button class="button button--content-index" onClick={toggleIndexExpanded}>
|
||||||
|
<Show when={!indexExpanded()}>
|
||||||
|
<Icon name="content-index-control" />
|
||||||
|
</Show>
|
||||||
|
<Show when={indexExpanded()}>
|
||||||
|
<Icon name="content-index-control-expanded" class={'expanded'} />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={indexExpanded()}>
|
||||||
<nav class="content-index">
|
<nav class="content-index">
|
||||||
|
<h4>Оглавление</h4>
|
||||||
|
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<li>
|
||||||
<a href="#terms-of-use">Пользовательское соглашение</a>
|
<a href="#terms-of-use">Пользовательское соглашение</a>
|
||||||
|
@ -34,14 +52,14 @@ export const TermsOfUsePage = () => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-6 col-xl-7 shift-content order-md-first">
|
||||||
<h1 id="terms-of-use">
|
<h1 id="terms-of-use">
|
||||||
<span class="wrapped">Пользовательское соглашение</span>
|
<span class="wrapped">Пользовательское соглашение</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="col-lg-10 offset-md-1">
|
|
||||||
<p>
|
<p>
|
||||||
Дискурс — это сообщество творческих людей, объединенных идеей делать интересный
|
Дискурс — это сообщество творческих людей, объединенных идеей делать интересный
|
||||||
журнал для всех желающих. Авторы Дискурса сообща посредством прямого голосования определяют
|
журнал для всех желающих. Авторы Дискурса сообща посредством прямого голосования определяют
|
||||||
|
@ -61,10 +79,9 @@ export const TermsOfUsePage = () => {
|
||||||
объектов авторских прав и другой информации для других пользователей.
|
объектов авторских прав и другой информации для других пользователей.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Издательство</strong> — администрация сайта, которая занимается
|
<strong>Издательство</strong> — администрация сайта, которая занимается технической
|
||||||
технической и издательской деятельностью для обеспечения функционирования Сайта
|
и издательской деятельностью для обеспечения функционирования Сайта и Альманаха.
|
||||||
и Альманаха. Издательство не вмешивается в принятие редакционных решений
|
Издательство не вмешивается в принятие редакционных решений авторским сообществом.
|
||||||
авторским сообществом.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Альманах «Дискурс»</strong> (свидетельство о регистрации СМИ: ПИ
|
<strong>Альманах «Дискурс»</strong> (свидетельство о регистрации СМИ: ПИ
|
||||||
|
@ -105,8 +122,7 @@ export const TermsOfUsePage = () => {
|
||||||
Creative Commons BY-NC-ND 4.0
|
Creative Commons BY-NC-ND 4.0
|
||||||
</a>
|
</a>
|
||||||
. Все материалы сайта предназначены исключительно для личного некоммерческого
|
. Все материалы сайта предназначены исключительно для личного некоммерческого
|
||||||
использования. Права на дизайн и программный код сайта принадлежат
|
использования. Права на дизайн и программный код сайта принадлежат Издательству.
|
||||||
Издательству.
|
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -134,9 +150,9 @@ export const TermsOfUsePage = () => {
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
Находясь на Сайте, Пользователь подтверждает свое совершеннолетие,
|
Находясь на Сайте, Пользователь подтверждает свое совершеннолетие, правоспособность,
|
||||||
правоспособность, а также согласие с настоящими Правилами и политикой
|
а также согласие с настоящими Правилами и политикой конфиденциальности
|
||||||
конфиденциальности и готовность нести полную ответственность за их соблюдение.
|
и готовность нести полную ответственность за их соблюдение.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -162,21 +178,21 @@ export const TermsOfUsePage = () => {
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
Пользователь несет всю ответственность за содержание публикуемого контента
|
Пользователь несет всю ответственность за содержание публикуемого контента
|
||||||
и свое взаимодействие с другими пользователями, и обязуется возместить
|
и свое взаимодействие с другими пользователями, и обязуется возместить все
|
||||||
все расходы в случае предъявления каких-либо претензий третьими лицами.
|
расходы в случае предъявления каких-либо претензий третьими лицами. Издательство
|
||||||
Издательство не несет ответственности за содержание публикуемой пользователями
|
не несет ответственности за содержание публикуемой пользователями информации,
|
||||||
информации, в том числе за размещенные на сайте комментарии. Переписка
|
в том числе за размещенные на сайте комментарии. Переписка между
|
||||||
между Пользователем и Издательством считается юридически значимой. Настоящие
|
Пользователем и Издательством считается юридически значимой. Настоящие Правила могут
|
||||||
Правила могут быть изменены Издательством, изменения вступают в силу с момента
|
быть изменены Издательством, изменения вступают в силу с момента публикации
|
||||||
публикации на Сайте.
|
на Сайте.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
Если Пользователь очевидно и целенаправленно нарушает правила, Издательство может
|
Если Пользователь очевидно и целенаправленно нарушает правила, Издательство может
|
||||||
и принять в отношении автора следующие меры: вынести предупреждение
|
и принять в отношении автора следующие меры: вынести предупреждение
|
||||||
и обязать автора устранить допущенное нарушение, удалить контент, нарушающий
|
и обязать автора устранить допущенное нарушение, удалить контент, нарушающий правила,
|
||||||
правила, заблокировать или удалить аккаунт нарушителя.
|
заблокировать или удалить аккаунт нарушителя.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -187,13 +203,13 @@ export const TermsOfUsePage = () => {
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
Данные, которые пользователи сообщают о себе сами при подаче заявки,
|
Данные, которые пользователи сообщают о себе сами при подаче заявки, регистрации,
|
||||||
регистрации, авторизации или заполнения профиля, в том числе ФИО
|
авторизации или заполнения профиля, в том числе ФИО и контактную информацию.
|
||||||
и контактную информацию. Конфиденциальные данные, такие как идентификатор
|
Конфиденциальные данные, такие как идентификатор и электронный адрес,
|
||||||
и электронный адрес, используются для идентификации пользователя. Данные
|
используются для идентификации пользователя. Данные профиля, размещённые публично
|
||||||
профиля, размещённые публично по желанию пользователя, которое выражается
|
по желанию пользователя, которое выражается фактом их предоставления,
|
||||||
фактом их предоставления, используется для демонстрации другим пользователям
|
используется для демонстрации другим пользователям той информации о себе, которую
|
||||||
той информации о себе, которую пользователь готов предоставить.
|
пользователь готов предоставить.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -214,8 +230,7 @@ export const TermsOfUsePage = () => {
|
||||||
<li>
|
<li>
|
||||||
<p class="ng-binding">
|
<p class="ng-binding">
|
||||||
По желанию пользователя Издательство готово удалить любую информацию о нем,
|
По желанию пользователя Издательство готово удалить любую информацию о нем,
|
||||||
собранную автоматическим путем. Для этого следует написать на адрес электронной
|
собранную автоматическим путем. Для этого следует написать на адрес электронной почты{' '}
|
||||||
почты{' '}
|
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
</a>
|
</a>
|
||||||
|
@ -229,8 +244,7 @@ export const TermsOfUsePage = () => {
|
||||||
на их обработку любым способом, не запрещенным законодательством РФ.
|
на их обработку любым способом, не запрещенным законодательством РФ.
|
||||||
</p>
|
</p>
|
||||||
<p class="ng-binding">
|
<p class="ng-binding">
|
||||||
Общедоступные видео на сайте могут транслироваться с YouTube
|
Общедоступные видео на сайте могут транслироваться с YouTube и регулируются{' '}
|
||||||
и регулируются{' '}
|
|
||||||
<a href="https://policies.google.com/privacy" target="_blank">
|
<a href="https://policies.google.com/privacy" target="_blank">
|
||||||
политикой конфиденциальности Google
|
политикой конфиденциальности Google
|
||||||
</a>
|
</a>
|
||||||
|
@ -244,8 +258,7 @@ export const TermsOfUsePage = () => {
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
Данные, которые мы получаем от вас, мы используем только
|
Данные, которые мы получаем от вас, мы используем только
|
||||||
в соответствии с принципами обработки данных, указанными в этом
|
в соответствии с принципами обработки данных, указанными в этом документе.
|
||||||
документе.
|
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -260,7 +273,6 @@ export const TermsOfUsePage = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,12 +12,10 @@ export const ThanksPage = () => {
|
||||||
|
|
||||||
<article class="container container--static-page">
|
<article class="container container--static-page">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 offset-md-2">
|
<div class="col-md-6 col-xl-7 shift-content order-md-first">
|
||||||
<h1>
|
<h1>
|
||||||
<span class="wrapped">{title}</span>
|
<span class="wrapped">{title}</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
|
||||||
<div class="col-md-8 col-lg-6 offset-md-3">
|
|
||||||
{/*
|
{/*
|
||||||
<h3><b>Команда</b></h3>
|
<h3><b>Команда</b></h3>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -2,12 +2,11 @@
|
||||||
// import 'solid-devtools'
|
// import 'solid-devtools'
|
||||||
|
|
||||||
import { MODALS, setLocale, showModal } from '../stores/ui'
|
import { MODALS, setLocale, showModal } from '../stores/ui'
|
||||||
import { Component, createEffect, createMemo, onMount } from 'solid-js'
|
import { Component, createEffect, createMemo } 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, RootSearchParams } from './types'
|
||||||
|
|
||||||
import { HomePage } from './Pages/HomePage'
|
import { HomePage } from './Pages/HomePage'
|
||||||
import { AllTopicsPage } from './Pages/AllTopicsPage'
|
import { AllTopicsPage } from './Pages/AllTopicsPage'
|
||||||
|
@ -30,7 +29,7 @@ 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 { ConnectPage } from './Pages/ConnectPage'
|
import { ConnectPage } from './Pages/ConnectPage'
|
||||||
import { renewSession } from '../stores/auth'
|
import { SessionProvider } from '../context/session'
|
||||||
|
|
||||||
// TODO: lazy load
|
// TODO: lazy load
|
||||||
// const HomePage = lazy(() => import('./Pages/HomePage'))
|
// const HomePage = lazy(() => import('./Pages/HomePage'))
|
||||||
|
@ -52,13 +51,6 @@ import { renewSession } from '../stores/auth'
|
||||||
// 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 = {
|
|
||||||
modal: string
|
|
||||||
lang: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const pagesMap: Record<keyof Routes, Component<PageProps>> = {
|
const pagesMap: Record<keyof Routes, Component<PageProps>> = {
|
||||||
connect: ConnectPage,
|
connect: ConnectPage,
|
||||||
create: CreatePage,
|
create: CreatePage,
|
||||||
|
@ -92,10 +84,6 @@ export const Root = (props: PageProps) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
renewSession()
|
|
||||||
})
|
|
||||||
|
|
||||||
const pageComponent = createMemo(() => {
|
const pageComponent = createMemo(() => {
|
||||||
const result = pagesMap[page().route]
|
const result = pagesMap[page().route]
|
||||||
|
|
||||||
|
@ -114,5 +102,9 @@ export const Root = (props: PageProps) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Dynamic component={pageComponent()} {...props} />
|
return (
|
||||||
|
<SessionProvider>
|
||||||
|
<Dynamic component={pageComponent()} {...props} />
|
||||||
|
</SessionProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,16 +3,8 @@
|
||||||
margin-top: 3.2rem;
|
margin-top: 3.2rem;
|
||||||
|
|
||||||
.stats & {
|
.stats & {
|
||||||
@include media-breakpoint-down(sm) {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topicDetailsItem {
|
.topicDetailsItem {
|
||||||
margin-bottom: 1.2rem;
|
margin-bottom: 1.2rem;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
|
||||||
margin-bottom: 3.2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,9 +25,9 @@
|
||||||
|
|
||||||
.topicTitle {
|
.topicTitle {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@include font-size(1.7rem);
|
@include font-size(2.2rem);
|
||||||
|
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topicAvatar {
|
.topicAvatar {
|
||||||
|
@ -56,10 +48,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.topicDescription {
|
.topicDescription {
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
color: #696969;
|
color: #696969;
|
||||||
margin: 0 0 0.8rem;
|
margin: 0 0 1.6rem;
|
||||||
|
|
||||||
&.compact {
|
&.compact {
|
||||||
font-size: medium;
|
font-size: medium;
|
||||||
|
@ -85,6 +77,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.topicDetailsItem {
|
.topicDetailsItem {
|
||||||
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
margin-right: 1.6rem;
|
margin-right: 1.6rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@ -104,3 +98,9 @@
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controlContainer {
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user