main topic select, save topics WiP

This commit is contained in:
bniwredyc 2023-05-10 22:20:53 +02:00
parent a73918b8f6
commit 01f33413eb
11 changed files with 165 additions and 57 deletions

14
package-lock.json generated
View File

@ -35,7 +35,7 @@
"@solid-primitives/storage": "1.3.9",
"@solid-primitives/upload": "0.0.110",
"@solidjs/meta": "0.28.2",
"@thisbeyond/solid-select": "0.13.0",
"@thisbeyond/solid-select": "0.14.0",
"@tiptap/core": "2.0.3",
"@tiptap/extension-blockquote": "2.0.3",
"@tiptap/extension-bold": "2.0.3",
@ -5745,9 +5745,9 @@
}
},
"node_modules/@thisbeyond/solid-select": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.13.0.tgz",
"integrity": "sha512-eION+Xf8TGLs1NZrvRo1NRKOl4plYMbY7UswHhh5bEUY8oMltjrBhUWF0hzaFViEc1zZpkCQyafaD89iofG6Tg==",
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz",
"integrity": "sha512-ecq4U3Vnc/nJbU84ARuPg2scNuYt994ljF5AmBlzuZW87x43mWiGJ5hEWufIJJMpDT6CcnCIx/xbrdDkaDEHQw==",
"dev": true,
"peerDependencies": {
"solid-js": "^1.5"
@ -24828,9 +24828,9 @@
"requires": {}
},
"@thisbeyond/solid-select": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.13.0.tgz",
"integrity": "sha512-eION+Xf8TGLs1NZrvRo1NRKOl4plYMbY7UswHhh5bEUY8oMltjrBhUWF0hzaFViEc1zZpkCQyafaD89iofG6Tg==",
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz",
"integrity": "sha512-ecq4U3Vnc/nJbU84ARuPg2scNuYt994ljF5AmBlzuZW87x43mWiGJ5hEWufIJJMpDT6CcnCIx/xbrdDkaDEHQw==",
"dev": true,
"requires": {}
},

View File

@ -55,7 +55,7 @@
"@solid-primitives/storage": "1.3.9",
"@solid-primitives/upload": "0.0.110",
"@solidjs/meta": "0.28.2",
"@thisbeyond/solid-select": "0.13.0",
"@thisbeyond/solid-select": "0.14.0",
"@tiptap/core": "2.0.3",
"@tiptap/extension-blockquote": "2.0.3",
"@tiptap/extension-bold": "2.0.3",

View File

@ -0,0 +1,13 @@
.selectedItem {
cursor: pointer;
&.mainTopic {
cursor: default;
background: #000;
color: #ccc;
+ :global(.solid-select-multi-value-remove) {
background: #000;
}
}
}

View File

@ -3,32 +3,73 @@ import { createOptions, Select } from '@thisbeyond/solid-select'
import { useLocalize } from '../../../context/localize'
import '@thisbeyond/solid-select/style.css'
import './TopicSelect.scss'
import styles from './TopicSelect.module.scss'
import { clsx } from 'clsx'
import { createSignal } from 'solid-js'
import { slugify } from '../../../utils/slugify'
type TopicSelectProps = {
topics: Topic[]
selectedTopics: Topic[]
onChange: (selectedTopics: Topic[]) => void
mainTopic?: Topic
onMainTopicChange: (mainTopic: Topic) => void
}
export const TopicSelect = (props: TopicSelectProps) => {
const { t } = useLocalize()
const [isDisabled, setIsDisabled] = createSignal(false)
const createValue = (title): Topic => {
const minId = Math.min(...props.selectedTopics.map((topic) => topic.id))
const id = minId < 0 ? minId - 1 : -2
return { id, title, slug: slugify(title) }
}
const selectProps = createOptions(props.topics, {
key: 'title',
disable: (topic) => {
// console.log({ selectedTopics: clone(props.selectedTopics) })
return props.selectedTopics.some((selectedTopic) => selectedTopic.slug === topic.slug)
}
},
createable: createValue
})
const handleChange = (selectedTopics: Topic[]) => {
props.onChange(selectedTopics)
}
const handleSelectedItemClick = (topic: Topic) => {
setIsDisabled(true)
props.onMainTopicChange(topic)
setIsDisabled(false)
}
const format = (item, type) => {
if (type === 'option') {
return item.label
}
const isMainTopic = item.id === props.mainTopic.id
return (
<div
class={clsx(styles.selectedItem, {
[styles.mainTopic]: isMainTopic
})}
onClick={() => handleSelectedItemClick(item)}
>
{item.title}
</div>
)
}
return (
<Select
multiple={true}
disabled={isDisabled()}
{...selectProps}
format={format}
placeholder={t('Topics')}
class="TopicSelect"
onChange={handleChange}

View File

@ -217,3 +217,7 @@
color: #f00;
}
}
.topicSelectContainer {
height: 64px;
}

View File

@ -26,6 +26,11 @@ const scrollTop = () => {
})
}
const EMPTY_TOPIC: Topic = {
id: -1,
slug: ''
}
export const EditView = (props: EditViewProps) => {
const { t } = useLocalize()
const { user } = useSession()
@ -39,13 +44,15 @@ export const EditView = (props: EditViewProps) => {
actions: { setForm, setFormErrors }
} = useEditorContext()
const shoutTopics = props.shout.topics || []
setForm({
shoutId: props.shout.id,
slug: props.shout.slug,
title: props.shout.title,
subtitle: props.shout.subtitle,
selectedTopics: props.shout.topics || [],
mainTopic: props.shout.mainTopic,
selectedTopics: shoutTopics,
mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC,
body: props.shout.body,
coverImageUrl: props.shout.cover
})
@ -86,6 +93,24 @@ export const EditView = (props: EditViewProps) => {
setForm('coverImageUrl', imgUrl)
}
const handleTopicSelectChange = (newSelectedTopics) => {
console.log({ newSelectedTopics })
if (newSelectedTopics.length === 0) {
setForm('mainTopic', EMPTY_TOPIC)
} else if (
form.selectedTopics.length === 0 ||
newSelectedTopics.every((topic) => topic.id !== form.mainTopic.id)
) {
setForm('mainTopic', newSelectedTopics[0])
}
if (newSelectedTopics.length > 0) {
setFormErrors('selectedTopics', '')
}
setForm('selectedTopics', newSelectedTopics)
}
return (
<>
<button
@ -184,16 +209,20 @@ export const EditView = (props: EditViewProps) => {
{/* его на&nbsp;страницах интересных ему тем. Темы можно менять местами, первая тема*/}
{/* становится заглавной*/}
{/*</p>*/}
<div class="pretty-form__item">
<div class={clsx('pretty-form__item', styles.topicSelectContainer)}>
<Show when={topics()}>
<TopicSelect
topics={topics()}
onChange={(newSelectedTopics) => setForm('selectedTopics', newSelectedTopics)}
onChange={handleTopicSelectChange}
selectedTopics={form.selectedTopics}
onMainTopicChange={(mainTopic) => setForm('mainTopic', mainTopic)}
mainTopic={form.mainTopic}
/>
</Show>
{/*<input type="text" name="topics" id="topics" placeholder="Темы" class="nolabel" />*/}
</div>
<Show when={formErrors.selectedTopics}>
<div class={styles.validationError}>{formErrors.selectedTopics}</div>
</Show>
{/*<h4>Соавторы</h4>*/}
{/*<p class="description">У каждого соавтора можно добавить роль</p>*/}

View File

@ -5,9 +5,9 @@ import { Topic } from '../graphql/types.gen'
import { apiClient } from '../utils/apiClient'
import { useLocalize } from './localize'
import { useSnackbar } from './snackbar'
import { translit } from '../utils/ru2en'
import { openPage } from '@nanostores/router'
import { router, useRouter } from '../stores/router'
import { slugify } from '../utils/slugify'
type WordCounter = {
characters: number
@ -20,7 +20,7 @@ type ShoutForm = {
title: string
subtitle: string
selectedTopics: Topic[]
mainTopic: string
mainTopic?: Topic
body: string
coverImageUrl: string
}
@ -29,7 +29,7 @@ type EditorContextType = {
isEditorPanelVisible: Accessor<boolean>
wordCounter: Accessor<WordCounter>
form: ShoutForm
formErrors: Partial<ShoutForm>
formErrors: Record<keyof ShoutForm, string>
actions: {
saveShout: () => Promise<void>
publishShout: () => Promise<void>
@ -38,7 +38,7 @@ type EditorContextType = {
toggleEditorPanel: () => void
countWords: (value: WordCounter) => void
setForm: SetStoreFunction<ShoutForm>
setFormErrors: SetStoreFunction<Partial<ShoutForm>>
setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>>
}
}
@ -60,7 +60,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
const [form, setForm] = createStore<ShoutForm>(null)
const [formErrors, setFormErrors] = createStore<Partial<ShoutForm>>(null)
const [formErrors, setFormErrors] = createStore<Record<keyof ShoutForm, string>>(null)
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
characters: 0,
@ -79,31 +79,48 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
return true
}
const saveShout = async () => {
if (isEditorPanelVisible()) {
toggleEditorPanel()
const validateSettings = () => {
if (form.selectedTopics.length === 0) {
setFormErrors('selectedTopics', t('Required'))
return false
}
if (!validate()) {
return
return true
}
try {
const shout = await apiClient.updateArticle({
const updateShout = async ({ publish }: { publish: boolean }) => {
return apiClient.updateArticle({
shoutId: form.shoutId,
shoutInput: {
body: form.body,
topics: form.selectedTopics.map((topic) => topic.slug),
topics: form.selectedTopics,
// authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
// community?: InputMaybe<Scalars['Int']>
mainTopic: form.selectedTopics[0]?.slug || 'society',
mainTopic: form.mainTopic,
slug: form.slug,
subtitle: form.subtitle,
title: form.title,
cover: form.coverImageUrl
},
publish: false
publish
})
}
const saveShout = async () => {
if (isEditorPanelVisible()) {
toggleEditorPanel()
}
if (page().route === 'edit' && !validate()) {
return
}
if (page().route === 'editSettings' && !validateSettings()) {
return
}
try {
const shout = await updateShout({ publish: false })
if (shout.visibility === 'owner') {
openPage(router, 'drafts')
@ -120,32 +137,20 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
if (isEditorPanelVisible()) {
toggleEditorPanel()
}
if (!validate()) {
return
}
if (page().route === 'edit') {
const slug = translit(form.title.toLowerCase()).replaceAll(' ', '-')
const slug = slugify(form.title)
setForm('slug', slug)
openPage(router, 'editSettings', { shoutId: form.shoutId.toString() })
return
}
try {
await apiClient.updateArticle({
shoutId: form.shoutId,
shoutInput: {
body: form.body,
topics: form.selectedTopics.map((topic) => topic.slug),
// authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
// community?: InputMaybe<Scalars['Int']>
mainTopic: form.selectedTopics[0]?.slug || 'society',
slug: form.slug,
subtitle: form.subtitle,
title: form.title,
cover: form.coverImageUrl
},
publish: true
})
await updateShout({ publish: true })
openPage(router, 'feed')
} catch (error) {
console.error('[publishShout]', error)

View File

@ -3,6 +3,7 @@ import { gql } from '@urql/core'
export default gql`
query TopicsAllQuery {
topicsAll {
id
title
body
slug

View File

@ -582,11 +582,11 @@ export type ShoutInput = {
body?: InputMaybe<Scalars['String']>
community?: InputMaybe<Scalars['Int']>
cover?: InputMaybe<Scalars['String']>
mainTopic?: InputMaybe<Scalars['String']>
mainTopic?: InputMaybe<TopicInput>
slug?: InputMaybe<Scalars['String']>
subtitle?: InputMaybe<Scalars['String']>
title?: InputMaybe<Scalars['String']>
topics?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
topics?: InputMaybe<Array<InputMaybe<TopicInput>>>
}
export type ShoutsFilterBy = {
@ -628,7 +628,6 @@ export type Token = {
export type Topic = {
body?: Maybe<Scalars['String']>
community: Community
id: Scalars['Int']
oid?: Maybe<Scalars['String']>
pic?: Maybe<Scalars['String']>
@ -639,7 +638,7 @@ export type Topic = {
export type TopicInput = {
body?: InputMaybe<Scalars['String']>
community: Scalars['String']
id?: InputMaybe<Scalars['Int']>
pic?: InputMaybe<Scalars['String']>
slug: Scalars['String']
title?: InputMaybe<Scalars['String']>

View File

@ -25,7 +25,16 @@ export const EditPage = () => {
return (
<PageLayout>
<Show when={isSessionLoaded()}>
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
<Show
when={isAuthenticated()}
fallback={
<div class="wide-container">
<div class="row">
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">Давайте авторизуемся</div>
</div>
</div>
}
>
<Show when={shout()}>
<Suspense fallback={<Loading />}>
<EditView shout={shout()} />

7
src/utils/slugify.ts Normal file
View File

@ -0,0 +1,7 @@
import { translit } from './ru2en'
export const slugify = (text) => {
return translit(text.toLowerCase())
.replaceAll(/[^\da-z]/g, '')
.replaceAll(' ', '-')
}