main topic select, save topics WiP
This commit is contained in:
parent
a73918b8f6
commit
01f33413eb
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -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": {}
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
13
src/components/Editor/TopicSelect/TopicSelect.module.scss
Normal file
13
src/components/Editor/TopicSelect/TopicSelect.module.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
.selectedItem {
|
||||
cursor: pointer;
|
||||
|
||||
&.mainTopic {
|
||||
cursor: default;
|
||||
background: #000;
|
||||
color: #ccc;
|
||||
|
||||
+ :global(.solid-select-multi-value-remove) {
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -217,3 +217,7 @@
|
|||
color: #f00;
|
||||
}
|
||||
}
|
||||
|
||||
.topicSelectContainer {
|
||||
height: 64px;
|
||||
}
|
||||
|
|
|
@ -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) => {
|
|||
{/* его на страницах интересных ему тем. Темы можно менять местами, первая тема*/}
|
||||
{/* становится заглавной*/}
|
||||
{/*</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>*/}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -3,6 +3,7 @@ import { gql } from '@urql/core'
|
|||
export default gql`
|
||||
query TopicsAllQuery {
|
||||
topicsAll {
|
||||
id
|
||||
title
|
||||
body
|
||||
slug
|
||||
|
|
|
@ -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']>
|
||||
|
|
|
@ -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
7
src/utils/slugify.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { translit } from './ru2en'
|
||||
|
||||
export const slugify = (text) => {
|
||||
return translit(text.toLowerCase())
|
||||
.replaceAll(/[^\da-z]/g, '')
|
||||
.replaceAll(' ', '-')
|
||||
}
|
Loading…
Reference in New Issue
Block a user