create shout -> edit

This commit is contained in:
bniwredyc 2023-04-11 15:57:48 +02:00
parent 21282aad92
commit ac3f29defc
23 changed files with 126 additions and 279 deletions

View File

@ -1,6 +1,6 @@
overwrite: true
#schema: 'http://localhost:8080'
schema: 'https://v2.discours.io'
schema: 'http://127.0.0.1:8080'
#schema: 'https://v2.discours.io'
generates:
src/graphql/introspec.gen.ts:
plugins:

View File

@ -27,6 +27,7 @@ import { ProjectsPage } from '../pages/about/projects.page'
import { TermsOfUsePage } from '../pages/about/termsOfUse.page'
import { ThanksPage } from '../pages/about/thanks.page'
import { CreatePage } from '../pages/create.page'
import { EditPage } from '../pages/edit.page'
import { ConnectPage } from '../pages/connect.page'
import { InboxPage } from '../pages/inbox.page'
import { LayoutShoutsPage } from '../pages/layoutShouts.page'
@ -45,7 +46,8 @@ const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
expo: LayoutShoutsPage,
connect: ConnectPage,
create: CreatePage,
createSettings: CreatePage,
edit: EditPage,
editSettings: EditPage,
home: HomePage,
topics: AllTopicsPage,
topic: TopicPage,

View File

@ -52,8 +52,6 @@ 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

@ -208,10 +208,12 @@ export const FullArticle = (props: ArticleProps) => {
<Icon name="bookmark" class={styles.icon} />
</div>
</div>
<Show when={canEdit()}>
<div class={styles.shoutStatsItem}>
<a href="/edit" class={styles.shoutStatsItemInner}>
<a
href={getPagePath(router, 'edit', { shoutSlug: props.article.slug })}
class={styles.shoutStatsItemInner}
>
<Icon name="edit" class={clsx(styles.icon, styles.iconEdit)} />
{t('Edit')}
</a>

View File

@ -30,7 +30,7 @@ import { TrailingNode } from './extensions/TrailingNode'
import { EditorBubbleMenu } from './EditorBubbleMenu/EditorBubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu'
import * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'
// import { WebrtcProvider } from 'y-webrtc'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
import { Collaboration } from '@tiptap/extension-collaboration'
import './Prosemirror.scss'
@ -40,7 +40,7 @@ import uniqolor from 'uniqolor'
import { HocuspocusProvider } from '@hocuspocus/provider'
type EditorProps = {
shoutId: number
shoutSlug: string
initialContent?: string
onChange: (text: string) => void
}
@ -53,12 +53,12 @@ export const Editor = (props: EditorProps) => {
const { t } = useLocalize()
const { user } = useSession()
const docName = `shout-${props.shoutId}`
const docName = `shout-${props.shoutSlug}`
if (!providers[docName]) {
providers[docName] = new HocuspocusProvider({
// url: 'wss://hocuspocus.discours.io',
url: 'ws://localhost:4242',
url: 'wss://hocuspocus.discours.io',
// url: 'ws://localhost:4242',
name: docName,
document: yDoc
})
@ -88,6 +88,7 @@ export const Editor = (props: EditorProps) => {
const editor = createTiptapEditor(() => ({
element: editorElRef.current,
content: props.initialContent,
extensions: [
Document,
Text,

View File

@ -1,4 +1,4 @@
import { Switch, Match, createSignal, Show, createEffect } from 'solid-js'
import { Switch, Match, createSignal, Show } from 'solid-js'
import type { Editor } from '@tiptap/core'
import styles from './EditorBubbleMenu.module.scss'
import { Icon } from '../../_shared/Icon'
@ -26,10 +26,6 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
}
)
createEffect(() => {
console.log('!!! editor:', props.editor)
})
const isBold = isActive('bold')
const isItalic = isActive('italic')
const isH1 = isActive('heading', { level: 1 })

View File

@ -44,7 +44,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
<Show when={isSessionLoaded()} keyed={true}>
<div class={clsx(styles.usernav, 'col')}>
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
<Show when={page().route !== 'create'}>
<Show when={page().route !== 'edit'}>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
<a href={getPagePath(router, 'create')}>
<span class={styles.textLabel}>{t('Create post')}</span>

View File

@ -37,7 +37,7 @@ export const FullTopic = (props: Props) => {
{t('Unfollow the topic')}
</button>
</Show>
<a href={`/create/${props.topic.slug}`}>{t('Write about the topic')}</a>
<a href={`/create/?topicId=${props.topic.id}`}>{t('Write about the topic')}</a>
</div>
<Show when={props.topic.pic}>
<img src={props.topic.pic} alt={props.topic.title} />

View File

@ -43,8 +43,8 @@
}
}
.createSettings,
.create {
.editSettings,
.edit {
display: none;
&.visible {

View File

@ -1,15 +1,14 @@
import { createSignal, lazy, onMount, Show, Suspense } from 'solid-js'
import { Loading } from '../_shared/Loading'
import { createSignal, onMount, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { clsx } from 'clsx'
import styles from './Create.module.scss'
import styles from './Edit.module.scss'
import { Title } from '@solidjs/meta'
import { createStore } from 'solid-js/store'
import type { Topic } from '../../graphql/types.gen'
import type { Shout, Topic } from '../../graphql/types.gen'
import { apiClient } from '../../utils/apiClient'
import { TopicSelect } from '../Editor/TopicSelect/TopicSelect'
import { router, useRouter } from '../../stores/router'
import { getPagePath } from '@nanostores/router'
import { getPagePath, openPage } from '@nanostores/router'
import { translit } from '../../utils/ru2en'
import { Editor } from '../Editor/Editor'
@ -18,12 +17,16 @@ type ShoutForm = {
title: string
subtitle: string
selectedTopics: Topic[]
mainTopic: Topic
mainTopic: string
body: string
coverImageUrl: string
}
export const CreateView = () => {
type EditViewProps = {
shout: Shout
}
export const EditView = (props: EditViewProps) => {
const { t } = useLocalize()
const [topics, setTopics] = createSignal<Topic[]>(null)
@ -32,13 +35,13 @@ export const CreateView = () => {
const [isSlugChanged, setIsSlugChanged] = createSignal(false)
const [form, setForm] = createStore<ShoutForm>({
slug: '',
title: '',
subtitle: '',
selectedTopics: [],
mainTopic: null,
body: '',
coverImageUrl: ''
slug: props.shout.slug,
title: props.shout.title,
subtitle: props.shout.subtitle,
selectedTopics: props.shout.topics,
mainTopic: props.shout.mainTopic,
body: props.shout.body,
coverImageUrl: props.shout.cover
})
onMount(async () => {
@ -49,18 +52,18 @@ export const CreateView = () => {
const handleFormSubmit = async (e) => {
e.preventDefault()
const newShout = await apiClient.createArticle({
article: {
slug: form.slug,
title: form.title,
subtitle: form.subtitle,
body: form.body,
topics: form.selectedTopics.map((topic) => topic.slug),
mainTopic: form.selectedTopics[0].slug
}
})
// const newShout = await apiClient.updateArticle({
// article: {
// slug: form.slug,
// title: form.title,
// subtitle: form.subtitle,
// body: form.body,
// topics: form.selectedTopics.map((topic) => topic.slug),
// mainTopic: form.selectedTopics[0].slug
// }
// })
router.open(getPagePath(router, 'article', { slug: newShout.slug }))
// openPage(getPagePath(router, 'article', { slug: newShout.slug }))
}
const handleTitleInputChange = (e) => {
@ -92,8 +95,8 @@ export const CreateView = () => {
<div class="row">
<div class="col-md-20 col-lg-18 col-xl-16">
<div
class={clsx(styles.create, {
[styles.visible]: page().route === 'create'
class={clsx(styles.edit, {
[styles.visible]: page().route === 'edit'
})}
>
<input
@ -116,16 +119,22 @@ export const CreateView = () => {
onChange={(e) => setForm('subtitle', e.currentTarget.value)}
/>
<Editor shoutId={42} onChange={(body) => setForm('body', body)} />
<Editor
shoutSlug={props.shout.slug}
initialContent={form.body}
onChange={(body) => setForm('body', body)}
/>
<div class={styles.saveBlock}>
{/*<button class={clsx('button button--outline', styles.button)}>Сохранить</button>*/}
<a href={getPagePath(router, 'createSettings')}>Настройки</a>
<a href={getPagePath(router, 'editSettings', { shoutSlug: props.shout.slug })}>
Настройки
</a>
</div>
</div>
<div
class={clsx(styles.createSettings, {
[styles.visible]: page().route === 'createSettings'
class={clsx(styles.editSettings, {
[styles.visible]: page().route === 'editSettings'
})}
>
<h1>Настройки публикации</h1>
@ -206,8 +215,8 @@ export const CreateView = () => {
Проверьте ещё раз введённые данные, если всё верно, вы&nbsp;можете сохранить или
опубликовать ваш текст
</p>
{/*<button class={clsx('button button--outline', styles.button)}>Сохранить</button>*/}
<a href={getPagePath(router, 'create')}>Назад</a>
<button class={clsx('button button--outline', styles.button)}>Сохранить</button>
<a href={getPagePath(router, 'edit', { shoutSlug: props.shout.slug })}>Назад</a>
<button type="submit" class={clsx('button button--submit', styles.button)}>
Опубликовать
</button>
@ -222,4 +231,4 @@ export const CreateView = () => {
)
}
export default CreateView
export default EditView

View File

@ -10,18 +10,6 @@ export default gql`
title
subtitle
body
topics {
id
title
slug
}
authors {
id
name
slug
userpic
caption
}
}
}
}

View File

@ -1,28 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation DraftCreateMutation($draft: DraftInput!) {
createDraft(draft: $draft) {
error
draft {
id
slug
title
subtitle
body
topics {
# id
title
slug
}
authors {
id
name
slug
userpic
caption
}
}
}
}
`

View File

@ -1,9 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation DraftDestroyMutation($draft: Int!) {
deleteDraft(draft: $draft) {
error
}
}
`

View File

@ -1,28 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation ShoutFromDraftMutation($draft: Int!) {
draftToShout(draft: $draft) {
error
shout {
_id: slug
slug
title
subtitle
body
topics {
# id
title
slug
}
authors {
id
name
slug
userpic
caption
}
}
}
}
`

View File

@ -1,28 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation DraftUpdateMutation($draft: DraftInput!) {
updateDraft(draft: $draft) {
error
draft {
id
slug
title
subtitle
body
topics {
# id
title
slug
}
authors {
id
name
slug
userpic
caption
}
}
}
}
`

View File

@ -1,17 +0,0 @@
import { gql } from '@urql/core'
export default gql`
query MyDraftsQuery {
loadDrafts {
authors {
id
slug
name
pic
}
createdAt
body
title
}
}
`

View File

@ -105,30 +105,6 @@ export type Community = {
slug: Scalars['String']
}
export type DraftCollab = {
authors: Array<Maybe<Scalars['Int']>>
body?: Maybe<Scalars['String']>
chat?: Maybe<Chat>
cover?: Maybe<Scalars['String']>
createdAt: Scalars['Int']
layout?: Maybe<Scalars['String']>
slug?: Maybe<Scalars['String']>
subtitle?: Maybe<Scalars['String']>
title?: Maybe<Scalars['String']>
topics?: Maybe<Array<Maybe<Scalars['String']>>>
updatedAt?: Maybe<Scalars['Int']>
}
export type DraftInput = {
authors?: InputMaybe<Array<InputMaybe<Scalars['Int']>>>
body?: InputMaybe<Scalars['String']>
cover?: InputMaybe<Scalars['String']>
slug?: InputMaybe<Scalars['String']>
subtitle?: InputMaybe<Scalars['String']>
title?: InputMaybe<Scalars['String']>
topics?: InputMaybe<Array<InputMaybe<Scalars['Int']>>>
}
export enum FollowingEntity {
Author = 'AUTHOR',
Community = 'COMMUNITY',
@ -185,28 +161,23 @@ export type MessagesBy = {
export type Mutation = {
confirmEmail: AuthResult
createChat: Result
createDraft: Result
createMessage: Result
createReaction: Result
createShout: Result
createTopic: Result
deleteChat: Result
deleteDraft: Result
deleteMessage: Result
deleteReaction: Result
deleteShout: Result
destroyTopic: Result
follow: Result
getSession: AuthResult
inviteAccept: Result
inviteAuthor: Result
markAsRead: Result
rateUser: Result
registerUser: AuthResult
sendLink: Result
unfollow: Result
updateChat: Result
updateDraft: Result
updateMessage: Result
updateOnlineStatus: Result
updateProfile: Result
@ -224,10 +195,6 @@ export type MutationCreateChatArgs = {
title?: InputMaybe<Scalars['String']>
}
export type MutationCreateDraftArgs = {
draft: DraftInput
}
export type MutationCreateMessageArgs = {
body: Scalars['String']
chat: Scalars['String']
@ -250,10 +217,6 @@ export type MutationDeleteChatArgs = {
chatId: Scalars['String']
}
export type MutationDeleteDraftArgs = {
draft: Scalars['Int']
}
export type MutationDeleteMessageArgs = {
chatId: Scalars['String']
id: Scalars['Int']
@ -276,15 +239,6 @@ export type MutationFollowArgs = {
what: FollowingEntity
}
export type MutationInviteAcceptArgs = {
draft: Scalars['Int']
}
export type MutationInviteAuthorArgs = {
author: Scalars['Int']
draft: Scalars['Int']
}
export type MutationMarkAsReadArgs = {
chatId: Scalars['String']
ids: Array<InputMaybe<Scalars['Int']>>
@ -316,10 +270,6 @@ export type MutationUpdateChatArgs = {
chat: ChatInput
}
export type MutationUpdateDraftArgs = {
draft: DraftInput
}
export type MutationUpdateMessageArgs = {
body: Scalars['String']
chatId: Scalars['String']
@ -375,13 +325,13 @@ export type Query = {
isEmailUsed: Scalars['Boolean']
loadAuthorsBy: Array<Maybe<Author>>
loadChats: Result
loadDrafts: Array<Maybe<DraftCollab>>
loadMessagesBy: Result
loadReactionsBy: Array<Maybe<Reaction>>
loadRecipients: Result
loadShout?: Maybe<Shout>
loadShouts: Array<Maybe<Shout>>
markdownBody: Scalars['String']
myFeed?: Maybe<Array<Maybe<Shout>>>
searchMessages: Result
searchRecipients: Result
signIn: AuthResult
@ -447,6 +397,10 @@ export type QueryMarkdownBodyArgs = {
body: Scalars['String']
}
export type QueryMyFeedArgs = {
options?: InputMaybe<LoadShoutsOptions>
}
export type QuerySearchMessagesArgs = {
by: MessagesBy
limit?: InputMaybe<Scalars['Int']>
@ -573,7 +527,6 @@ export type Result = {
chats?: Maybe<Array<Maybe<Chat>>>
communities?: Maybe<Array<Maybe<Community>>>
community?: Maybe<Community>
drafts?: Maybe<Array<Maybe<DraftCollab>>>
error?: Maybe<Scalars['String']>
members?: Maybe<Array<Maybe<ChatMember>>>
message?: Maybe<Message>
@ -585,7 +538,6 @@ export type Result = {
slugs?: Maybe<Array<Maybe<Scalars['String']>>>
topic?: Maybe<Topic>
topics?: Maybe<Array<Maybe<Topic>>>
uids?: Maybe<Array<Maybe<Scalars['String']>>>
}
export type Role = {
@ -610,7 +562,6 @@ export type Shout = {
mainTopic?: Maybe<Scalars['String']>
media?: Maybe<Scalars['String']>
publishedAt?: Maybe<Scalars['DateTime']>
publishedBy?: Maybe<User>
slug: Scalars['String']
stat?: Maybe<Stat>
subtitle?: Maybe<Scalars['String']>
@ -624,7 +575,7 @@ export type Shout = {
export type ShoutInput = {
authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
body: Scalars['String']
body?: InputMaybe<Scalars['String']>
community?: InputMaybe<Scalars['Int']>
mainTopic?: InputMaybe<Scalars['String']>
slug?: InputMaybe<Scalars['String']>
@ -676,6 +627,7 @@ export type Token = {
export type Topic = {
body?: Maybe<Scalars['String']>
community: Community
id: Scalars['Int']
oid?: Maybe<Scalars['String']>
pic?: Maybe<Scalars['String']>
slug: Scalars['String']

View File

@ -1,22 +1,19 @@
import { lazy, Show, Suspense } from 'solid-js'
import { PageLayout } from '../components/_shared/PageLayout'
import { Loading } from '../components/_shared/Loading'
import { useSession } from '../context/session'
const CreateView = lazy(() => import('../components/Views/Create'))
import { onMount } from 'solid-js'
import { apiClient } from '../utils/apiClient'
import { router } from '../stores/router'
import { redirectPage } from '@nanostores/router'
export const CreatePage = () => {
const { isAuthenticated, isSessionLoaded } = useSession()
onMount(async () => {
const shout = await apiClient.createArticle({ article: {} })
redirectPage(router, 'edit', { shoutSlug: shout.slug })
})
return (
<PageLayout>
<Show when={isSessionLoaded()}>
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
<Suspense fallback={<Loading />}>
<CreateView />
</Suspense>
</Show>
</Show>
<Loading />
</PageLayout>
)
}

View File

@ -1,4 +1,4 @@
import { ROUTES } from '../stores/router'
import { getServerRoute } from '../utils/getServerRoute'
export default getServerRoute(ROUTES.createSettings)
export default getServerRoute(ROUTES.edit)

40
src/pages/edit.page.tsx Normal file
View File

@ -0,0 +1,40 @@
import { createMemo, createSignal, lazy, onMount, Show, Suspense } from 'solid-js'
import { PageLayout } from '../components/_shared/PageLayout'
import { Loading } from '../components/_shared/Loading'
import { useSession } from '../context/session'
import { Shout } from '../graphql/types.gen'
import { useRouter } from '../stores/router'
import { apiClient } from '../utils/apiClient'
const EditView = lazy(() => import('../components/Views/Edit'))
export const EditPage = () => {
const { isAuthenticated, isSessionLoaded } = useSession()
const { page } = useRouter()
const shoutSlug = createMemo(() => (page().params as Record<'shoutSlug', string>).shoutSlug)
const [shout, setShout] = createSignal<Shout>(null)
onMount(async () => {
const loadedShout = await apiClient.getShout(shoutSlug())
setShout(loadedShout)
})
return (
<PageLayout>
<Show when={isSessionLoaded()}>
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
<Show when={shout()}>
<Suspense fallback={<Loading />}>
<EditView shout={shout()} />
</Suspense>
</Show>
</Show>
</Show>
</PageLayout>
)
}
export const Page = EditPage

View File

@ -0,0 +1,4 @@
import { ROUTES } from '../stores/router'
import { getServerRoute } from '../utils/getServerRoute'
export default getServerRoute(ROUTES.editSettings)

View File

@ -1,33 +0,0 @@
import { createStorageSignal } from '@solid-primitives/storage'
import type { Reaction } from '../graphql/types.gen'
import { createSignal } from 'solid-js'
// TODO: store drafts
// import type { Draft } from '../components/EditorExample/store/context'
interface Collab {
authors: string[] // slugs
invites?: string[]
createdAt: Date
body?: string
title?: string
}
export const drafts = createStorageSignal<{ [key: string]: string }>('drafts', {}) // save drafts on device
export const [collabs, setCollabs] = createSignal<Collab[]>([]) // save collabs in backend or in p2p network
export const [editorReactions, setReactions] = createSignal<Reaction[]>([])
/*
TODO: approvals and proposals derived stores
const approvals = computed(
reactions,
(rdict) => Object.values(rdict)
.filter((r: Reaction) => r.kind === ReactionKind.Accept)
)
const proposals = computed<Reaction[], typeof reactions>(
reactions,
(rdict) => Object.values(rdict)
.filter((r: Reaction) => r.kind === ReactionKind.Propose)
)
*/

View File

@ -9,7 +9,8 @@ export const ROUTES = {
inbox: '/inbox',
connect: '/connect',
create: '/create',
createSettings: '/create/settings',
edit: '/edit/:shoutSlug',
editSettings: '/edit/:shoutSlug/settings',
topics: '/topics',
topic: '/topic/:slug',
authors: '/authors',