editor very WIP

This commit is contained in:
bniwredyc 2023-03-08 17:35:13 +01:00
parent a7b0df4b24
commit 195781741c
21 changed files with 7469 additions and 1107 deletions

8047
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -33,19 +33,19 @@
"@aws-sdk/client-s3": "^3.282.0",
"@aws-sdk/lib-storage": "^3.282.0",
"formidable": "^2.1.1",
"i18next": "^22.4.10",
"mailgun.js": "^8.2.0"
"i18next": "^22.4.11",
"mailgun.js": "^8.2.1"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@graphql-codegen/cli": "^3.2.1",
"@graphql-codegen/typescript": "^3.0.1",
"@graphql-codegen/typescript-operations": "^3.0.1",
"@graphql-codegen/cli": "^3.2.2",
"@graphql-codegen/typescript": "^3.0.2",
"@graphql-codegen/typescript-operations": "^3.0.2",
"@graphql-codegen/typescript-urql": "^3.7.3",
"@graphql-codegen/urql-introspection": "^2.2.1",
"@graphql-tools/url-loader": "^7.17.13",
"@graphql-tools/url-loader": "^7.17.14",
"@graphql-typed-document-node/core": "^3.1.2",
"@nanostores/router": "^0.8.1",
"@nanostores/router": "^0.8.2",
"@nanostores/solid": "^0.3.2",
"@popperjs/core": "^2.11.6",
"@solid-primitives/memo": "^1.2.0",
@ -53,15 +53,44 @@
"@solid-primitives/storage": "^1.3.7",
"@solid-primitives/upload": "^0.0.109",
"@solidjs/meta": "^0.28.2",
"@tiptap/core": "^2.0.0-beta.220",
"@tiptap/extension-blockquote": "^2.0.0-beta.220",
"@tiptap/extension-bold": "^2.0.0-beta.220",
"@tiptap/extension-bubble-menu": "^2.0.0-beta.220",
"@tiptap/extension-bullet-list": "^2.0.0-beta.220",
"@tiptap/extension-character-count": "^2.0.0-beta.220",
"@tiptap/extension-collaboration": "^2.0.0-beta.220",
"@tiptap/extension-collaboration-cursor": "^2.0.0-beta.220",
"@tiptap/extension-document": "^2.0.0-beta.220",
"@tiptap/extension-dropcursor": "^2.0.0-beta.220",
"@tiptap/extension-floating-menu": "^2.0.0-beta.220",
"@tiptap/extension-focus": "^2.0.0-beta.220",
"@tiptap/extension-gapcursor": "^2.0.0-beta.220",
"@tiptap/extension-hard-break": "^2.0.0-beta.220",
"@tiptap/extension-heading": "^2.0.0-beta.220",
"@tiptap/extension-highlight": "^2.0.0-beta.220",
"@tiptap/extension-history": "^2.0.0-beta.220",
"@tiptap/extension-horizontal-rule": "^2.0.0-beta.220",
"@tiptap/extension-image": "^2.0.0-beta.220",
"@tiptap/extension-italic": "^2.0.0-beta.220",
"@tiptap/extension-link": "^2.0.0-beta.220",
"@tiptap/extension-list-item": "^2.0.0-beta.220",
"@tiptap/extension-ordered-list": "^2.0.0-beta.220",
"@tiptap/extension-paragraph": "^2.0.0-beta.220",
"@tiptap/extension-placeholder": "^2.0.0-beta.220",
"@tiptap/extension-strike": "^2.0.0-beta.220",
"@tiptap/extension-text": "^2.0.0-beta.220",
"@tiptap/extension-underline": "^2.0.0-beta.220",
"@tiptap/extension-youtube": "^2.0.0-beta.220",
"@types/express": "^4.17.15",
"@types/node": "^18.14.6",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"@typescript-eslint/eslint-plugin": "^5.54.1",
"@typescript-eslint/parser": "^5.54.1",
"@urql/core": "^3.1.1",
"@urql/devtools": "^2.0.3",
"@urql/exchange-graphcache": "^5.0.9",
"babel-preset-solid": "^1.5.6",
"babel-preset-solid": "^1.6.12",
"bcryptjs": "^2.4.3",
"bootstrap": "^5.2.3",
"clsx": "^1.2.1",
@ -75,18 +104,19 @@
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-solid": "^0.10.0",
"eslint-plugin-solid": "^0.11.0",
"eslint-plugin-sonarjs": "^0.18.0",
"eslint-plugin-unicorn": "^46.0.0",
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6",
"graphql-ws": "^5.11.2",
"graphql-ws": "^5.12.0",
"hast-util-select": "^5.0.4",
"husky": "^8.0.3",
"hygen": "^6.2.11",
"i18next-http-backend": "^2.1.1",
"idb": "^7.1.1",
"jest": "^29.4.3",
"install": "^0.13.0",
"jest": "^29.5.0",
"js-cookie": "^3.0.1",
"lint-staged": "^13.1.2",
"loglevel": "^1.8.1",
@ -97,6 +127,7 @@
"markdown-it-mark": "^3.0.1",
"markdown-it-replace-link": "^1.1.0",
"nanostores": "^0.7.4",
"npm": "^9.6.0",
"orderedmap": "^2.1.0",
"prettier": "^2.7.1",
"prettier-eslint": "^15.0.1",
@ -116,14 +147,15 @@
"rollup": "^3.18.0",
"rollup-plugin-visualizer": "^5.9.0",
"sass": "^1.58.3",
"solid-js": "^1.6.11",
"solid-js": "^1.6.12",
"solid-tiptap": "^0.5.1",
"solid-transition-group": "^0.0.13",
"sort-package-json": "^2.3.0",
"stylelint": "^15.2.0",
"stylelint-config-css-modules": "^4.1.0",
"stylelint-config-prettier-scss": "^0.0.1",
"stylelint-config-standard-scss": "^7.0.1",
"stylelint-order": "^6.0.1",
"stylelint-order": "^6.0.3",
"stylelint-scss": "^4.4.0",
"swiper": "^8.4.7",
"ts-node": "^10.9.1",
@ -134,10 +166,11 @@
"vite": "^4.1.4",
"vite-plugin-sass-dts": "^1.2.16",
"vite-plugin-solid": "^2.6.1",
"vite-plugin-ssr": "^0.4.90",
"vite-plugin-ssr": "^0.4.91",
"wonka": "^6.2.3",
"ws": "^8.12.1",
"y-prosemirror": "^1.2.0",
"y-indexeddb": "^9.0.9",
"y-prosemirror": "^1.0.20",
"y-protocols": "^1.0.5",
"y-webrtc": "^10.2.4",
"yjs": "^13.5.48"

View File

@ -231,5 +231,6 @@
"Your name will appear on your profile page and as your signature in publications, comments and responses.": "Your name will appear on your profile page and as your signature in publications, comments and responses",
"zine": "zine",
"By time": "By time",
"New only": "New only"
"New only": "New only",
"Short opening": "Short opening"
}

View File

@ -249,5 +249,6 @@
"view": "просмотр",
"zine": "журнал",
"By time": "По порядку",
"New only": "Только новые"
"New only": "Только новые",
"Short opening": "Небольшое вступление, чтобы заинтересовать читателя"
}

View File

@ -1,6 +1,6 @@
import { Show, createMemo, createSignal, onMount, For } from 'solid-js'
import { Comment } from './Comment'
import styles from '../../styles/Article.module.scss'
import styles from './Article.module.scss'
import { clsx } from 'clsx'
import { Author, Reaction, ReactionKind } from '../../graphql/types.gen'
import { useSession } from '../../context/session'
@ -55,6 +55,8 @@ export const CommentsTree = (props: Props) => {
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
)
console.log(JSON.parse(JSON.stringify(reactionEntities)))
const sortedComments = createMemo(() => {
let newSortedComments = [...comments()]
newSortedComments = newSortedComments.sort(byCreated)

View File

@ -1,163 +0,0 @@
h1 {
@include font-size(4rem);
line-height: 1.1;
margin-top: 0.5em;
}
h2 {
line-height: 1.1;
}
img {
max-width: 100%;
}
.article {
padding-top: 2em;
}
.article__header {
margin-bottom: 2em;
@include media-breakpoint-up(md) {
margin: 0 -16.6666% 2em;
}
}
.article__cover {
background-size: cover;
height: 0;
padding-bottom: 56.2%;
}
.article__body {
font-size: 1.7rem;
line-height: 1.6;
img {
display: block;
margin-bottom: 0.5em;
}
blockquote {
border-left: 4px solid;
font-size: 2rem;
font-weight: 500;
font-style: italic;
line-height: 1.4;
margin: 1.5em 0 1.5em -16.6666%;
padding: 0 0 0 1em;
}
mark {
background: none;
font-size: 2rem;
font-weight: bold;
line-height: 1.4;
}
}
.article__author {
margin-bottom: 2em;
}
.article__authors-list {
margin-top: 2em;
h4 {
color: #696969;
font-size: 1.5rem;
font-weight: normal;
}
}
.write-comment {
border: 2px solid #f6f6f6;
@include font-size(1.7rem);
outline: none;
padding: 0.2em 0.4em;
width: 100%;
&::placeholder {
color: #858585;
}
}
.comment-warning {
background: #f6f6f6;
@include font-size(2.2rem);
margin-bottom: 1em;
padding: 2.4rem 1.8rem;
}
.article-stats {
border-bottom: 1px solid #e8e8e8;
border-top: 4px solid #000;
padding: 3.2rem 0;
}
.article-stats__item {
@include font-size(1.7rem);
font-weight: 500;
display: inline-block;
margin-right: $grid-gutter-width;
vertical-align: baseline;
.icon {
display: inline-block;
margin-right: 0.2em;
transition: filter 0.2s;
vertical-align: middle;
}
img {
display: block;
}
a {
border: none;
&:hover {
.icon {
filter: invert(1);
}
}
}
}
.article-stats__item--likes {
.icon {
vertical-align: baseline;
}
.icon:last-of-type {
// transform: rotate(180deg);
transform-origin: center;
margin-left: 0.3em;
vertical-align: middle;
}
}
.topics-list {
margin: 2.4rem 0;
.article__topic {
display: inline-block;
margin: 0 0.8rem 0.8rem 0;
a {
background: #f6f6f6;
color: #000;
padding: 0.4rem 0.8rem;
transition: background-color 0.2s;
&:hover {
background-color: rgb(0 0 0 / 20%);
}
}
}
}

View File

@ -1,5 +1,4 @@
import { capitalize, formatDate } from '../../utils'
import './Full.scss'
import { Icon } from '../_shared/Icon'
import { AuthorCard } from '../Author/Card'
import { createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js'
@ -7,8 +6,6 @@ import type { Author, Shout } from '../../graphql/types.gen'
import MD from './MD'
import { SharePopup } from './SharePopup'
import { getDescription } from '../../utils/meta'
import stylesHeader from '../Nav/Header.module.scss'
import styles from '../../styles/Article.module.scss'
import { ShoutRatingControl } from './ShoutRatingControl'
import { clsx } from 'clsx'
import { CommentsTree } from './CommentsTree'
@ -20,6 +17,8 @@ import { router } from '../../stores/router'
import { useReactions } from '../../context/reactions'
import { Title } from '@solidjs/meta'
import { useLocalize } from '../../context/localize'
import stylesHeader from '../Nav/Header.module.scss'
import styles from './Article.module.scss'
interface ArticleProps {
article: Shout
@ -108,7 +107,7 @@ export const FullArticle = (props: ArticleProps) => {
return (
<>
<Title>{props.article.title}</Title>
<div class="shout wide-container">
<div class="wide-container">
<article class="col-md-6 shift-content">
<div class={styles.shoutHeader}>
<div class={styles.shoutTopic}>
@ -168,7 +167,7 @@ export const FullArticle = (props: ArticleProps) => {
<Show when={media() && props.article.layout === 'image'}>
<Slider slidesPerView={1} isPageGallery={true} isCardsWithCover={true} hasThumbs={true}>
<For each={media() || []}>
{(m: MediaItem) => (
{(m) => (
<div class="swiper-slide">
<div class="swiper-slide__inner">
<img src={m.url || m.pic} alt={m.title} loading="lazy" />
@ -181,7 +180,7 @@ export const FullArticle = (props: ArticleProps) => {
</Slider>
</Show>
<div class="shout wide-container">
<div class=" wide-container">
<div class="col-md-8 shift-content">
<div class={styles.shoutStats}>
<div class={styles.shoutStatsItem}>

View File

@ -35,7 +35,10 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
const shoutRatingReactions = createMemo(() =>
Object.values(reactionEntities).filter(
(r) => [ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) && r.shout.id === props.shout.id
(r) =>
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
r.shout.id === props.shout.id &&
!r.replyTo
)
)

View File

@ -0,0 +1,7 @@
.container {
width: 680px;
}
.editor {
width: 100%;
}

View File

@ -0,0 +1,132 @@
import { createTiptapEditor } from 'solid-tiptap'
import { clsx } from 'clsx'
import { useLocalize } from '../../context/localize'
import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold'
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
import * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'
import { Dropcursor } from '@tiptap/extension-dropcursor'
import { Italic } from '@tiptap/extension-italic'
import { Strike } from '@tiptap/extension-strike'
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
import { Underline } from '@tiptap/extension-underline'
import { FloatingMenu } from '@tiptap/extension-floating-menu'
import { BulletList } from '@tiptap/extension-bullet-list'
import { OrderedList } from '@tiptap/extension-ordered-list'
import { ListItem } from '@tiptap/extension-list-item'
import { CharacterCount } from '@tiptap/extension-character-count'
import { Collaboration } from '@tiptap/extension-collaboration'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
import { Placeholder } from '@tiptap/extension-placeholder'
import { Gapcursor } from '@tiptap/extension-gapcursor'
import { HardBreak } from '@tiptap/extension-hard-break'
import { Heading } from '@tiptap/extension-heading'
import { Highlight } from '@tiptap/extension-highlight'
import { Link } from '@tiptap/extension-link'
import { Youtube } from '@tiptap/extension-youtube'
import { Document } from '@tiptap/extension-document'
import { Text } from '@tiptap/extension-text'
import { Image } from '@tiptap/extension-image'
import { History } from '@tiptap/extension-history'
import { Paragraph } from '@tiptap/extension-paragraph'
import Focus from '@tiptap/extension-focus'
import { TrailingNode } from './extensions/TrailingNode'
import './Prosemirror.scss'
import styles from './Editor.module.scss'
import { Show } from 'solid-js'
import { EditorBubbleMenu } from './EditorBubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu'
type EditorProps = {
initialContent?: string
}
// const ydoc = new Y.Doc()
// // TODO
// const provider = new WebrtcProvider('slug!!!!!!', ydoc)
export const Editor = (props: EditorProps) => {
const { t } = useLocalize()
const editorElRef: {
current: HTMLDivElement
} = {
current: null
}
const bubbleMenuRef: {
current: HTMLDivElement
} = {
current: null
}
const floatingMenuRef: {
current: HTMLDivElement
} = {
current: null
}
const editor = createTiptapEditor(() => ({
element: editorElRef.current,
extensions: [
Document,
Text,
Paragraph,
Dropcursor,
Blockquote,
Bold,
Italic,
Strike,
HorizontalRule,
Underline,
BubbleMenu.configure({
element: bubbleMenuRef.current
}),
FloatingMenu.configure({
tippyOptions: {
placement: 'left'
},
element: floatingMenuRef.current
}),
BulletList,
OrderedList,
ListItem,
CharacterCount,
// Collaboration.configure({
// document: ydoc
// }),
// CollaborationCursor.configure({
// provider,
// user: {
// name: 'Cyndi Lauper',
// color: '#f783ac'
// }
// }),
// TODO conditional indexedDB
// History,
Placeholder.configure({
placeholder: t('Short opening')
}),
Focus,
Gapcursor,
HardBreak,
Heading,
Highlight,
Image,
Link,
Youtube,
TrailingNode
]
}))
return (
<div class={clsx('container', styles.container)}>
<div class={styles.editor} ref={(el) => (editorElRef.current = el)} />
<EditorBubbleMenu editor={editor()} ref={(el) => (bubbleMenuRef.current = el)} />
<EditorFloatingMenu editor={editor()} ref={(el) => (floatingMenuRef.current = el)} />
</div>
)
}
export default Editor

View File

@ -0,0 +1,14 @@
import type { Editor } from '@tiptap/core'
type BubbleMenuProps = {
editor: Editor
ref: (el: HTMLDivElement) => void
}
export const EditorBubbleMenu = (props: BubbleMenuProps) => {
return (
<div ref={props.ref}>
<button>bold</button>
</div>
)
}

View File

@ -0,0 +1,4 @@
.editorFloatingMenu {
position: relative;
left: -100%;
}

View File

@ -0,0 +1,15 @@
import type { Editor } from '@tiptap/core'
import styles from './EditorFloatingMenu.module.scss'
type FloatingMenuProps = {
editor: Editor
ref: (el: HTMLDivElement) => void
}
export const EditorFloatingMenu = (props: FloatingMenuProps) => {
return (
<div ref={props.ref} class={styles.editorFloatingMenu}>
<button>+</button>
</div>
)
}

View File

@ -0,0 +1,14 @@
.ProseMirror {
outline: none;
}
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
font-weight: 500;
font-size: 20px;
line-height: 30px;
opacity: 0.3;
}

View File

@ -0,0 +1,69 @@
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
function nodeEqualsType({ types, node }) {
return (Array.isArray(types) && types.includes(node.type)) || node.type === types
}
/**
* Extension based on:
* - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js
* - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts
*/
export interface TrailingNodeOptions {
node: string
notAfter: string[]
}
export const TrailingNode = Extension.create<TrailingNodeOptions>({
name: 'trailingNode',
addOptions() {
return {
node: 'paragraph',
notAfter: ['paragraph']
}
},
addProseMirrorPlugins() {
const plugin = new PluginKey(this.name)
const disabledNodes = Object.entries(this.editor.schema.nodes)
.map(([, value]) => value)
.filter((node) => this.options.notAfter.includes(node.name))
return [
new Plugin({
key: plugin,
appendTransaction: (_, __, state) => {
const { doc, tr, schema } = state
const shouldInsertNodeAtEnd = plugin.getState(state)
const endPosition = doc.content.size
const type = schema.nodes[this.options.node]
if (!shouldInsertNodeAtEnd) {
return
}
return tr.insert(endPosition, type.create())
},
state: {
init: (_, state) => {
const lastNode = state.tr.doc.lastChild
return !nodeEqualsType({ node: lastNode, types: disabledNodes })
},
apply: (tr, value) => {
if (!tr.docChanged) {
return value
}
const lastNode = tr.doc.lastChild
return !nodeEqualsType({ node: lastNode, types: disabledNodes })
}
}
})
]
}
})

View File

@ -10,7 +10,7 @@ import { useRouter } from '../../stores/router'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages'
import styles from './Author.module.scss'
import stylesArticle from '../../styles/Article.module.scss'
import stylesArticle from '../Article/Article.module.scss'
import { clsx } from 'clsx'
import Userpic from '../Author/Userpic'
import { Popup } from '../_shared/Popup'

View File

@ -2,18 +2,14 @@ import { lazy, Suspense } from 'solid-js'
import { Loading } from '../_shared/Loading'
import { useLocalize } from '../../context/localize'
const Editor = lazy(() => import('../EditorNew/Editor'))
const Editor = lazy(() => import('../Editor/Editor'))
export const CreateView = () => {
const { t } = useLocalize()
const newArticleIpsum = `<h1>${t('Header')}</h1>
<h2>${t('Subheader')}</h2>
<p>${t('A short introduction to keep the reader interested')}</p>`
return (
<Suspense fallback={<Loading />}>
<Editor initialContent={newArticleIpsum} />
<Editor />
</Suspense>
)
}

View File

@ -29,7 +29,7 @@ export const FeedView = () => {
const { sortedAuthors } = useAuthorsStore()
const { topTopics } = useTopicsStore()
const { topAuthors } = useTopAuthorsStore()
const { session } = useSession()
const { session, userSlug } = useSession()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [topComments, setTopComments] = createSignal<Reaction[]>([])
@ -48,7 +48,6 @@ export const FeedView = () => {
// }
// })
const userSlug = createMemo(() => session()?.user?.slug)
createEffect(async () => {
if (userSlug()) {
// load recent editing shouts ( visibility = authors )

View File

@ -69,10 +69,9 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
const deleteReaction = async (id: number): Promise<void> => {
const reaction = await apiClient.destroyReaction(id)
setReactionEntities((oldState) => ({
...oldState,
setReactionEntities({
[reaction.id]: undefined
}))
})
}
const updateReaction = async (id: number, input: ReactionInput): Promise<void> => {