editor very WIP
This commit is contained in:
parent
a7b0df4b24
commit
195781741c
8047
package-lock.json
generated
8047
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
67
package.json
67
package.json
|
@ -33,19 +33,19 @@
|
||||||
"@aws-sdk/client-s3": "^3.282.0",
|
"@aws-sdk/client-s3": "^3.282.0",
|
||||||
"@aws-sdk/lib-storage": "^3.282.0",
|
"@aws-sdk/lib-storage": "^3.282.0",
|
||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
"i18next": "^22.4.10",
|
"i18next": "^22.4.11",
|
||||||
"mailgun.js": "^8.2.0"
|
"mailgun.js": "^8.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.21.0",
|
"@babel/core": "^7.21.0",
|
||||||
"@graphql-codegen/cli": "^3.2.1",
|
"@graphql-codegen/cli": "^3.2.2",
|
||||||
"@graphql-codegen/typescript": "^3.0.1",
|
"@graphql-codegen/typescript": "^3.0.2",
|
||||||
"@graphql-codegen/typescript-operations": "^3.0.1",
|
"@graphql-codegen/typescript-operations": "^3.0.2",
|
||||||
"@graphql-codegen/typescript-urql": "^3.7.3",
|
"@graphql-codegen/typescript-urql": "^3.7.3",
|
||||||
"@graphql-codegen/urql-introspection": "^2.2.1",
|
"@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",
|
"@graphql-typed-document-node/core": "^3.1.2",
|
||||||
"@nanostores/router": "^0.8.1",
|
"@nanostores/router": "^0.8.2",
|
||||||
"@nanostores/solid": "^0.3.2",
|
"@nanostores/solid": "^0.3.2",
|
||||||
"@popperjs/core": "^2.11.6",
|
"@popperjs/core": "^2.11.6",
|
||||||
"@solid-primitives/memo": "^1.2.0",
|
"@solid-primitives/memo": "^1.2.0",
|
||||||
|
@ -53,15 +53,44 @@
|
||||||
"@solid-primitives/storage": "^1.3.7",
|
"@solid-primitives/storage": "^1.3.7",
|
||||||
"@solid-primitives/upload": "^0.0.109",
|
"@solid-primitives/upload": "^0.0.109",
|
||||||
"@solidjs/meta": "^0.28.2",
|
"@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/express": "^4.17.15",
|
||||||
"@types/node": "^18.14.6",
|
"@types/node": "^18.14.6",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
"@typescript-eslint/eslint-plugin": "^5.54.1",
|
||||||
"@typescript-eslint/parser": "^5.54.0",
|
"@typescript-eslint/parser": "^5.54.1",
|
||||||
"@urql/core": "^3.1.1",
|
"@urql/core": "^3.1.1",
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "^2.0.3",
|
||||||
"@urql/exchange-graphcache": "^5.0.9",
|
"@urql/exchange-graphcache": "^5.0.9",
|
||||||
"babel-preset-solid": "^1.5.6",
|
"babel-preset-solid": "^1.6.12",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"bootstrap": "^5.2.3",
|
"bootstrap": "^5.2.3",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
@ -75,18 +104,19 @@
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
"eslint-plugin-promise": "^6.1.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-sonarjs": "^0.18.0",
|
||||||
"eslint-plugin-unicorn": "^46.0.0",
|
"eslint-plugin-unicorn": "^46.0.0",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"graphql-ws": "^5.11.2",
|
"graphql-ws": "^5.12.0",
|
||||||
"hast-util-select": "^5.0.4",
|
"hast-util-select": "^5.0.4",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"hygen": "^6.2.11",
|
"hygen": "^6.2.11",
|
||||||
"i18next-http-backend": "^2.1.1",
|
"i18next-http-backend": "^2.1.1",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"jest": "^29.4.3",
|
"install": "^0.13.0",
|
||||||
|
"jest": "^29.5.0",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"lint-staged": "^13.1.2",
|
"lint-staged": "^13.1.2",
|
||||||
"loglevel": "^1.8.1",
|
"loglevel": "^1.8.1",
|
||||||
|
@ -97,6 +127,7 @@
|
||||||
"markdown-it-mark": "^3.0.1",
|
"markdown-it-mark": "^3.0.1",
|
||||||
"markdown-it-replace-link": "^1.1.0",
|
"markdown-it-replace-link": "^1.1.0",
|
||||||
"nanostores": "^0.7.4",
|
"nanostores": "^0.7.4",
|
||||||
|
"npm": "^9.6.0",
|
||||||
"orderedmap": "^2.1.0",
|
"orderedmap": "^2.1.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"prettier-eslint": "^15.0.1",
|
"prettier-eslint": "^15.0.1",
|
||||||
|
@ -116,14 +147,15 @@
|
||||||
"rollup": "^3.18.0",
|
"rollup": "^3.18.0",
|
||||||
"rollup-plugin-visualizer": "^5.9.0",
|
"rollup-plugin-visualizer": "^5.9.0",
|
||||||
"sass": "^1.58.3",
|
"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",
|
"solid-transition-group": "^0.0.13",
|
||||||
"sort-package-json": "^2.3.0",
|
"sort-package-json": "^2.3.0",
|
||||||
"stylelint": "^15.2.0",
|
"stylelint": "^15.2.0",
|
||||||
"stylelint-config-css-modules": "^4.1.0",
|
"stylelint-config-css-modules": "^4.1.0",
|
||||||
"stylelint-config-prettier-scss": "^0.0.1",
|
"stylelint-config-prettier-scss": "^0.0.1",
|
||||||
"stylelint-config-standard-scss": "^7.0.1",
|
"stylelint-config-standard-scss": "^7.0.1",
|
||||||
"stylelint-order": "^6.0.1",
|
"stylelint-order": "^6.0.3",
|
||||||
"stylelint-scss": "^4.4.0",
|
"stylelint-scss": "^4.4.0",
|
||||||
"swiper": "^8.4.7",
|
"swiper": "^8.4.7",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
|
@ -134,10 +166,11 @@
|
||||||
"vite": "^4.1.4",
|
"vite": "^4.1.4",
|
||||||
"vite-plugin-sass-dts": "^1.2.16",
|
"vite-plugin-sass-dts": "^1.2.16",
|
||||||
"vite-plugin-solid": "^2.6.1",
|
"vite-plugin-solid": "^2.6.1",
|
||||||
"vite-plugin-ssr": "^0.4.90",
|
"vite-plugin-ssr": "^0.4.91",
|
||||||
"wonka": "^6.2.3",
|
"wonka": "^6.2.3",
|
||||||
"ws": "^8.12.1",
|
"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-protocols": "^1.0.5",
|
||||||
"y-webrtc": "^10.2.4",
|
"y-webrtc": "^10.2.4",
|
||||||
"yjs": "^13.5.48"
|
"yjs": "^13.5.48"
|
||||||
|
|
|
@ -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",
|
"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",
|
"zine": "zine",
|
||||||
"By time": "By time",
|
"By time": "By time",
|
||||||
"New only": "New only"
|
"New only": "New only",
|
||||||
|
"Short opening": "Short opening"
|
||||||
}
|
}
|
||||||
|
|
|
@ -249,5 +249,6 @@
|
||||||
"view": "просмотр",
|
"view": "просмотр",
|
||||||
"zine": "журнал",
|
"zine": "журнал",
|
||||||
"By time": "По порядку",
|
"By time": "По порядку",
|
||||||
"New only": "Только новые"
|
"New only": "Только новые",
|
||||||
|
"Short opening": "Небольшое вступление, чтобы заинтересовать читателя"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Show, createMemo, createSignal, onMount, For } from 'solid-js'
|
import { Show, createMemo, createSignal, onMount, For } from 'solid-js'
|
||||||
import { Comment } from './Comment'
|
import { Comment } from './Comment'
|
||||||
import styles from '../../styles/Article.module.scss'
|
import styles from './Article.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Author, Reaction, ReactionKind } from '../../graphql/types.gen'
|
import { Author, Reaction, ReactionKind } from '../../graphql/types.gen'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
|
@ -55,6 +55,8 @@ export const CommentsTree = (props: Props) => {
|
||||||
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
|
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console.log(JSON.parse(JSON.stringify(reactionEntities)))
|
||||||
|
|
||||||
const sortedComments = createMemo(() => {
|
const sortedComments = createMemo(() => {
|
||||||
let newSortedComments = [...comments()]
|
let newSortedComments = [...comments()]
|
||||||
newSortedComments = newSortedComments.sort(byCreated)
|
newSortedComments = newSortedComments.sort(byCreated)
|
||||||
|
|
|
@ -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%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { capitalize, formatDate } from '../../utils'
|
import { capitalize, formatDate } from '../../utils'
|
||||||
import './Full.scss'
|
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js'
|
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 MD from './MD'
|
||||||
import { SharePopup } from './SharePopup'
|
import { SharePopup } from './SharePopup'
|
||||||
import { getDescription } from '../../utils/meta'
|
import { getDescription } from '../../utils/meta'
|
||||||
import stylesHeader from '../Nav/Header.module.scss'
|
|
||||||
import styles from '../../styles/Article.module.scss'
|
|
||||||
import { ShoutRatingControl } from './ShoutRatingControl'
|
import { ShoutRatingControl } from './ShoutRatingControl'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { CommentsTree } from './CommentsTree'
|
import { CommentsTree } from './CommentsTree'
|
||||||
|
@ -20,6 +17,8 @@ import { router } from '../../stores/router'
|
||||||
import { useReactions } from '../../context/reactions'
|
import { useReactions } from '../../context/reactions'
|
||||||
import { Title } from '@solidjs/meta'
|
import { Title } from '@solidjs/meta'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
import stylesHeader from '../Nav/Header.module.scss'
|
||||||
|
import styles from './Article.module.scss'
|
||||||
|
|
||||||
interface ArticleProps {
|
interface ArticleProps {
|
||||||
article: Shout
|
article: Shout
|
||||||
|
@ -108,7 +107,7 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>{props.article.title}</Title>
|
<Title>{props.article.title}</Title>
|
||||||
<div class="shout wide-container">
|
<div class="wide-container">
|
||||||
<article class="col-md-6 shift-content">
|
<article class="col-md-6 shift-content">
|
||||||
<div class={styles.shoutHeader}>
|
<div class={styles.shoutHeader}>
|
||||||
<div class={styles.shoutTopic}>
|
<div class={styles.shoutTopic}>
|
||||||
|
@ -168,7 +167,7 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
<Show when={media() && props.article.layout === 'image'}>
|
<Show when={media() && props.article.layout === 'image'}>
|
||||||
<Slider slidesPerView={1} isPageGallery={true} isCardsWithCover={true} hasThumbs={true}>
|
<Slider slidesPerView={1} isPageGallery={true} isCardsWithCover={true} hasThumbs={true}>
|
||||||
<For each={media() || []}>
|
<For each={media() || []}>
|
||||||
{(m: MediaItem) => (
|
{(m) => (
|
||||||
<div class="swiper-slide">
|
<div class="swiper-slide">
|
||||||
<div class="swiper-slide__inner">
|
<div class="swiper-slide__inner">
|
||||||
<img src={m.url || m.pic} alt={m.title} loading="lazy" />
|
<img src={m.url || m.pic} alt={m.title} loading="lazy" />
|
||||||
|
@ -181,7 +180,7 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
</Slider>
|
</Slider>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="shout wide-container">
|
<div class=" wide-container">
|
||||||
<div class="col-md-8 shift-content">
|
<div class="col-md-8 shift-content">
|
||||||
<div class={styles.shoutStats}>
|
<div class={styles.shoutStats}>
|
||||||
<div class={styles.shoutStatsItem}>
|
<div class={styles.shoutStatsItem}>
|
||||||
|
|
|
@ -35,7 +35,10 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
||||||
|
|
||||||
const shoutRatingReactions = createMemo(() =>
|
const shoutRatingReactions = createMemo(() =>
|
||||||
Object.values(reactionEntities).filter(
|
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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
7
src/components/Editor/Editor.module.scss
Normal file
7
src/components/Editor/Editor.module.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.container {
|
||||||
|
width: 680px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
132
src/components/Editor/Editor.tsx
Normal file
132
src/components/Editor/Editor.tsx
Normal 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
|
0
src/components/Editor/EditorBubbleMenu.module.scss
Normal file
0
src/components/Editor/EditorBubbleMenu.module.scss
Normal file
14
src/components/Editor/EditorBubbleMenu.tsx
Normal file
14
src/components/Editor/EditorBubbleMenu.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
4
src/components/Editor/EditorFloatingMenu.module.scss
Normal file
4
src/components/Editor/EditorFloatingMenu.module.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.editorFloatingMenu {
|
||||||
|
position: relative;
|
||||||
|
left: -100%;
|
||||||
|
}
|
15
src/components/Editor/EditorFloatingMenu.tsx
Normal file
15
src/components/Editor/EditorFloatingMenu.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
14
src/components/Editor/Prosemirror.scss
Normal file
14
src/components/Editor/Prosemirror.scss
Normal 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;
|
||||||
|
}
|
69
src/components/Editor/extensions/TrailingNode.tsx
Normal file
69
src/components/Editor/extensions/TrailingNode.tsx
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
|
@ -10,7 +10,7 @@ import { useRouter } from '../../stores/router'
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||||
import { splitToPages } from '../../utils/splitToPages'
|
import { splitToPages } from '../../utils/splitToPages'
|
||||||
import styles from './Author.module.scss'
|
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 { clsx } from 'clsx'
|
||||||
import Userpic from '../Author/Userpic'
|
import Userpic from '../Author/Userpic'
|
||||||
import { Popup } from '../_shared/Popup'
|
import { Popup } from '../_shared/Popup'
|
||||||
|
|
|
@ -2,18 +2,14 @@ import { lazy, Suspense } from 'solid-js'
|
||||||
import { Loading } from '../_shared/Loading'
|
import { Loading } from '../_shared/Loading'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
||||||
const Editor = lazy(() => import('../EditorNew/Editor'))
|
const Editor = lazy(() => import('../Editor/Editor'))
|
||||||
|
|
||||||
export const CreateView = () => {
|
export const CreateView = () => {
|
||||||
const { t } = useLocalize()
|
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 (
|
return (
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Editor initialContent={newArticleIpsum} />
|
<Editor />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export const FeedView = () => {
|
||||||
const { sortedAuthors } = useAuthorsStore()
|
const { sortedAuthors } = useAuthorsStore()
|
||||||
const { topTopics } = useTopicsStore()
|
const { topTopics } = useTopicsStore()
|
||||||
const { topAuthors } = useTopAuthorsStore()
|
const { topAuthors } = useTopAuthorsStore()
|
||||||
const { session } = useSession()
|
const { session, userSlug } = useSession()
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
const [topComments, setTopComments] = createSignal<Reaction[]>([])
|
const [topComments, setTopComments] = createSignal<Reaction[]>([])
|
||||||
|
|
||||||
|
@ -48,7 +48,6 @@ export const FeedView = () => {
|
||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
|
|
||||||
const userSlug = createMemo(() => session()?.user?.slug)
|
|
||||||
createEffect(async () => {
|
createEffect(async () => {
|
||||||
if (userSlug()) {
|
if (userSlug()) {
|
||||||
// load recent editing shouts ( visibility = authors )
|
// load recent editing shouts ( visibility = authors )
|
||||||
|
|
|
@ -69,10 +69,9 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
const deleteReaction = async (id: number): Promise<void> => {
|
const deleteReaction = async (id: number): Promise<void> => {
|
||||||
const reaction = await apiClient.destroyReaction(id)
|
const reaction = await apiClient.destroyReaction(id)
|
||||||
setReactionEntities((oldState) => ({
|
setReactionEntities({
|
||||||
...oldState,
|
|
||||||
[reaction.id]: undefined
|
[reaction.id]: undefined
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateReaction = async (id: number, input: ReactionInput): Promise<void> => {
|
const updateReaction = async (id: number, input: ReactionInput): Promise<void> => {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user