Feature/video upload (#107)

* Add Video Player and Video Uploader
* Remove old video component
This commit is contained in:
Ilya Y 2023-06-10 17:10:05 +03:00 committed by GitHub
parent 5b824d8e2f
commit be53a5dce8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 450 additions and 90 deletions

View File

@ -132,6 +132,7 @@
"Just start typing...": "Just start typing...",
"Knowledge base": "Knowledge base",
"Last rev.": "Посл. изм.",
"Let's log in": "Let's log in",
"Link sent, check your email": "Link sent, check your email",
"Lists": "Lists",
"Literature": "Literature",
@ -231,6 +232,7 @@
"Thank you": "Thank you",
"This comment has not yet been rated": "This comment has not yet been rated",
"This email is already taken. If it's you": "This email is already taken. If it's you",
"This functionality is currently not available, we would like to work on this issue. Use the download link.": "This functionality is currently not available, we would like to work on this issue. Use the download link.",
"This post has not been rated yet": "This post has not been rated yet",
"To leave a comment please": "To leave a comment please",
"To write a comment, you must": "To write a comment, you must",
@ -250,9 +252,11 @@
"Unfollow the topic": "Unfollow the topic",
"Unnamed draft": "Unnamed draft",
"Upload": "Upload",
"Upload video": "Upload video",
"Username": "Username",
"Userpic": "Userpic",
"Video": "Video",
"Video format not supported": "Video format not supported",
"Views": "Views",
"We are convinced that one voice is good, but many is better": "We are convinced that one voice is good, but many is better",
"We can't find you, check email or": "We can't find you, check email or",
@ -321,5 +325,7 @@
"user already exist": "user already exists",
"video": "video",
"view": "view",
"zine": "zine"
"zine": "zine",
"Insert video link": "Insert video link",
"Looks like you forgot to upload the video": "Looks like you forgot to upload the video"
}

View File

@ -139,6 +139,7 @@
"Karma": "Карма",
"Knowledge base": "База знаний",
"Last rev.": "Посл. изм.",
"Let's log in": "Давайте авторизуемся",
"Link sent, check your email": "Ссылка отправлена, проверьте почту",
"Lists": "Списки",
"Literature": "Литература",
@ -244,6 +245,7 @@
"Thank you": "Благодарности",
"This comment has not yet been rated": "Этот комментарий еще пока никто не оценил",
"This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы",
"This functionality is currently not available, we would like to work on this issue. Use the download link.": "В данный момент этот функционал не доступен, бы работаем над этой проблемой. Воспользуйтесь загрузкой по ссылке.",
"This post has not been rated yet": "Эту публикацию еще пока никто не оценил",
"To leave a comment please": "Чтобы оставить комментарий, необходимо",
"To write a comment, you must": "Чтобы написать комментарий, необходимо",
@ -263,9 +265,11 @@
"Unfollow the topic": "Отписаться от темы",
"Unnamed draft": "Unnamed draft",
"Upload": "Загрузить",
"Upload video": "Загрузить видео",
"Username": "Имя пользователя",
"Userpic": "Аватар",
"Video": "Видео",
"Video format not supported": "Тип видео не поддерживается",
"Views": "Просмотры",
"We are convinced that one voice is good, but many is better": "Мы убеждены, один голос хорошо, а много — лучше",
"We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или",
@ -343,5 +347,7 @@
"user already exist": "пользователь уже существует",
"video": "видео",
"view": "просмотр",
"zine": "журнал"
"zine": "журнал",
"Insert video link": "Вставить ссылку на видео",
"Looks like you forgot to upload the video": "Похоже, что вы забыли загрузить видео"
}

View File

@ -10,7 +10,7 @@ import { ShoutRatingControl } from './ShoutRatingControl'
import { clsx } from 'clsx'
import { CommentsTree } from './CommentsTree'
import { useSession } from '../../context/session'
import VideoPlayer from './VideoPlayer'
import { VideoPlayer } from '../_shared/VideoPlayer'
import Slider from '../_shared/Slider'
import { getPagePath } from '@nanostores/router'
import { router, useRouter } from '../../stores/router'
@ -29,7 +29,6 @@ interface ArticleProps {
interface MediaItem {
url?: string
pic?: string
title?: string
body?: string
}
@ -40,17 +39,12 @@ const MediaView = (props: { media: MediaItem; kind: Shout['layout'] }) => {
return (
<>
<Switch fallback={<a href={props.media.url}>{t('Cannot show this media type')}</a>}>
<Match when={props.kind === 'audio'}>
<div>
<h5>{props.media.title}</h5>
<audio controls>
<source src={props.media.url} />
</audio>
<hr />
</div>
</Match>
<Match when={props.kind === 'video'}>
<VideoPlayer url={props.media.url} />
<VideoPlayer
videoUrl={props.media.url}
title={props.media.title}
description={props.media.body}
/>
</Match>
</Switch>
</>
@ -169,15 +163,12 @@ export const FullArticle = (props: ArticleProps) => {
</Show>
</div>
<Show when={media() && props.article.layout !== 'image'}>
<Show when={media()}>
<div class="media-items">
<For each={media() || []}>
{(m: MediaItem) => (
<div class={styles.shoutMediaBody}>
<MediaView media={m} kind={props.article.layout} />
<Show when={m?.body}>
<div innerHTML={m.body} />
</Show>
</div>
)}
</For>

View File

@ -1,12 +0,0 @@
.videoContainer {
aspect-ratio: 16/9;
@include media-breakpoint-up(md) {
margin: 0 0 1em -16.6666%;
}
iframe {
height: 100%;
width: 100%;
}
}

View File

@ -1,25 +0,0 @@
import { Show } from 'solid-js'
import styles from './VideoPlayer.module.scss'
export default (props: { url: string }) => (
<div class={styles.videoContainer}>
<Show when={props.url.includes('youtube.com')}>
<iframe
id="ytplayer"
width="640"
height="360"
src={`https://www.youtube.com/embed/${props.url.split('watch=').pop()}`}
allowfullscreen
/>
</Show>
<Show when={props.url.includes('vimeo.com')}>
<iframe
src={'https://player.vimeo.com/video/' + props.url.split('video/').pop()}
width="420"
height="345"
allow="autoplay; fullscreen"
allowfullscreen
/>
</Show>
</div>
)

View File

@ -19,7 +19,7 @@ export const BlockquoteBubbleMenu = (props: Props) => {
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => {
props.editor.chain().focus().setBlockQuoteFloat('left').run()
}}
@ -33,7 +33,7 @@ export const BlockquoteBubbleMenu = (props: Props) => {
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setBlockQuoteFloat(null).run()}
>
<Icon name="editor-image-align-center" />
@ -45,7 +45,7 @@ export const BlockquoteBubbleMenu = (props: Props) => {
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setBlockQuoteFloat('right').run()}
>
<Icon name="editor-image-align-right" />

View File

@ -19,7 +19,7 @@ export const FigureBubbleMenu = (props: Props) => {
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setImageFloat('left').run()}
>
<Icon name="editor-image-align-left" />
@ -31,7 +31,7 @@ export const FigureBubbleMenu = (props: Props) => {
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setImageFloat(null).run()}
>
<Icon name="editor-image-align-center" />
@ -43,7 +43,7 @@ export const FigureBubbleMenu = (props: Props) => {
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setImageFloat('right').run()}
>
<Icon name="editor-image-align-right" />
@ -53,7 +53,7 @@ export const FigureBubbleMenu = (props: Props) => {
<div class={styles.delimiter} />
<button
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => {
props.editor.chain().focus().imageToFigure().run()
}}
@ -63,7 +63,7 @@ export const FigureBubbleMenu = (props: Props) => {
<div class={styles.delimiter} />
<Popover content={t('Add image')}>
{(triggerRef: (el) => void) => (
<button type="button" ref={triggerRef} class={clsx(styles.bubbleMenuButton)}>
<button type="button" ref={triggerRef} class={styles.bubbleMenuButton}>
<Icon name="editor-image-add" />
</button>
)}

View File

@ -19,21 +19,21 @@ export const IncutBubbleMenu = (props: Props) => {
<div ref={props.ref} class={styles.BubbleMenu}>
<button
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setArticleFloat('left').run()}
>
<Icon name="editor-image-align-left" />
</button>
<button
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setArticleFloat('half-left').run()}
>
<Icon name="editor-image-half-align-left" />
</button>
<button
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setArticleFloat(null).run()}
>
<Icon name="editor-image-align-center" />
@ -41,7 +41,7 @@ export const IncutBubbleMenu = (props: Props) => {
<button
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setArticleFloat('half-right').run()}
>
<Icon name="editor-image-half-align-right" />
@ -49,7 +49,7 @@ export const IncutBubbleMenu = (props: Props) => {
<button
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setArticleFloat('right').run()}
>
<Icon name="editor-image-align-right" />
@ -59,7 +59,7 @@ export const IncutBubbleMenu = (props: Props) => {
<div class={styles.dropDownHolder}>
<button
type="button"
class={clsx(styles.bubbleMenuButton)}
class={styles.bubbleMenuButton}
onClick={() => setSubstratBubbleOpen(!substratBubbleOpen())}
>
<span style={{ color: 'white' }}>{t('Substrate')}</span>

View File

@ -58,7 +58,6 @@ export const Editor = (props: EditorProps) => {
const { t } = useLocalize()
const { user } = useSession()
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
const [floatMenuRef, setFloatMenuRef] = createSignal<'blockquote' | 'image' | 'incut'>()
const docName = `shout-${props.shoutId}`

View File

@ -0,0 +1,89 @@
.VideoUploader {
margin: 2rem 0;
.dropArea {
border: 2px dashed rgba(38, 56, 217, 0.3);
border-radius: 16px;
color: #2638d9;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
padding: 24px;
transition: background-color 0.3s ease-in-out;
&.active {
background-color: rgba(#2638d9, 0.3);
&::after {
content: '';
top: 0;
transform: translateX(100%);
width: 100%;
height: 100%;
position: absolute;
z-index: 0;
animation: slide 1.8s infinite;
background: linear-gradient(
to right,
rgba(#fff, 0) 0%,
rgba(#fff, 0.8) 50%,
rgb(128 186 232 / 0%) 99%,
rgb(125 185 232 / 0%) 100%
);
}
}
}
.error {
color: var(--danger-color);
text-align: center;
padding: 1rem;
}
.inputHolder {
display: flex;
align-items: center;
justify-content: center;
margin: 1rem 0;
.urlInput {
display: block;
width: unset;
margin: auto;
padding: 1rem 0;
border: none;
background: unset;
font-size: 18px;
min-width: 20em;
text-align: center;
border-bottom: 1px solid transparent;
transition: all 0.35s ease-in-out;
border-radius: 0;
&::placeholder {
color: #2638d9;
font-weight: 500;
}
&:focus,
&:active,
&.hasError {
outline: none;
width: 100%;
text-align: left;
border-bottom-color: var(--default-color);
}
}
}
}
@keyframes slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}

View File

@ -0,0 +1,133 @@
import { clsx } from 'clsx'
import styles from './VideoUploader.module.scss'
import { useLocalize } from '../../../context/localize'
import { createDropzone } from '@solid-primitives/upload'
import { createEffect, createSignal, Show } from 'solid-js'
import { useSnackbar } from '../../../context/snackbar'
import { validateUrl } from '../../../utils/validateUrl'
import { VideoPlayer } from '../../_shared/VideoPlayer'
// import { handleFileUpload } from '../../../utils/handleFileUpload'
type VideoItem = {
url: string
title: string
body: string
}
type Props = {
class?: string
data: (value: VideoItem) => void
}
export const VideoUploader = (props: Props) => {
const { t } = useLocalize()
const [dragActive, setDragActive] = createSignal(false)
const [dragError, setDragError] = createSignal<string | undefined>()
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
const [data, setData] = createSignal<VideoItem>()
const updateData = (key, value) => {
setData((prev) => ({ ...prev, [key]: value }))
}
createEffect(() => {
props.data(data())
})
const {
actions: { showSnackbar }
} = useSnackbar()
const urlInput: {
current: HTMLInputElement
} = {
current: null
}
// const [videoUrl, setVideoUrl] = createSignal<string | undefined>()
// const runUpload = async (file) => {
// try {
// const fileUrl = await handleFileUpload(file)
// setVideoUrl(fileUrl)
// } catch (error) {
// console.error('[runUpload]', error)
// }
// }
const { setRef: dropzoneRef, files: droppedFiles } = createDropzone({
onDrop: async () => {
setDragActive(false)
if (droppedFiles().length > 1) {
setDragError(t('Many files, choose only one'))
} else if (droppedFiles()[0].file.type.startsWith('video/')) {
await showSnackbar({
body: t(
'This functionality is currently not available, we would like to work on this issue. Use the download link.'
)
})
// await runUpload(droppedFiles()[0])
} else {
setDragError(t('Video format not supported'))
}
}
})
const handleDrag = (event) => {
if (event.type === 'dragenter' || event.type === 'dragover') {
setDragActive(true)
setDragError()
} else if (event.type === 'dragleave') {
setDragActive(false)
}
}
const handleUrlInput = async (value: string) => {
if (validateUrl(value)) {
updateData('url', value)
} else {
setIncorrectUrl(true)
}
}
return (
<div class={clsx(styles.VideoUploader, props.class)}>
<Show
when={data() && data().url}
fallback={
<>
<div
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
ref={dropzoneRef}
class={clsx(styles.dropArea, { [styles.active]: dragActive() })}
>
{t('Upload video')}
</div>
<Show when={dragError()}>
<div class={styles.error}>{dragError()}</div>
</Show>
<div class={styles.inputHolder}>
<input
class={clsx(styles.urlInput, { [styles.hasError]: incorrectUrl() })}
ref={(el) => (urlInput.current = el)}
type="text"
placeholder={t('Insert video link')}
onChange={(event) => handleUrlInput(event.currentTarget.value)}
/>
</div>
<Show when={incorrectUrl()}>
<div class={styles.error}>{t('It does not look like url')}</div>
</Show>
</>
}
>
<VideoPlayer
deleteAction={() => setData()}
videoUrl={data().url}
title={data().title}
description={data().body}
/>
</Show>
</div>
)
}

View File

@ -0,0 +1 @@
export { VideoUploader } from './VideoUploader'

View File

@ -218,6 +218,7 @@
.validationError {
position: absolute;
z-index: 1;
top: 100%;
font-size: small;
color: #f00;

View File

@ -1,4 +1,4 @@
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
import { createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { clsx } from 'clsx'
import { Title } from '@solidjs/meta'
@ -15,8 +15,11 @@ import { Modal } from '../Nav/Modal'
import { hideModal, showModal } from '../../stores/ui'
import { imageProxy } from '../../utils/imageProxy'
import { GrowingTextarea } from '../_shared/GrowingTextarea'
import { VideoUploader } from '../Editor/VideoUploader'
import { VideoPlayer } from '../_shared/VideoPlayer'
import { slugify } from '../../utils/slugify'
type EditViewProps = {
type Props = {
shout: Shout
}
@ -33,12 +36,13 @@ const EMPTY_TOPIC: Topic = {
slug: ''
}
export const EditView = (props: EditViewProps) => {
export const EditView = (props: Props) => {
const { t } = useLocalize()
const { user } = useSession()
const [isScrolled, setIsScrolled] = createSignal(false)
const [topics, setTopics] = createSignal<Topic[]>(null)
const [coverImage, setCoverImage] = createSignal<string>(null)
const [media, setMedia] = createSignal<string>(props.shout.media)
const { page } = useRouter()
const {
form,
@ -56,7 +60,9 @@ export const EditView = (props: EditViewProps) => {
selectedTopics: shoutTopics,
mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC,
body: props.shout.body,
coverImageUrl: props.shout.cover
coverImageUrl: props.shout.cover,
media: media(),
layout: props.shout.layout
})
onMount(async () => {
@ -77,7 +83,7 @@ export const EditView = (props: EditViewProps) => {
const handleTitleInputChange = (value) => {
setForm('title', value)
setForm('slug', slugify(value))
if (value) {
setFormErrors('title', '')
}
@ -111,10 +117,13 @@ export const EditView = (props: EditViewProps) => {
if (newSelectedTopics.length > 0) {
setFormErrors('selectedTopics', '')
}
setForm('selectedTopics', newSelectedTopics)
}
const handleAddMedia = (data) => {
setForm('media', JSON.stringify([data]))
}
return (
<>
<div class={styles.container}>
@ -157,6 +166,33 @@ export const EditView = (props: EditViewProps) => {
initialValue={form.subtitle}
maxLength={100}
/>
<Show when={props.shout.layout === 'video'}>
<Show
when={media()}
fallback={
<VideoUploader
data={(data) => {
handleAddMedia(data)
}}
/>
}
>
<For each={JSON.parse(media())}>
{(mediaItem) => (
<>
<VideoPlayer
videoUrl={mediaItem?.url}
title={mediaItem?.title}
description={mediaItem?.body}
deleteAction={() => setMedia(null)}
/>
</>
)}
</For>
</Show>
</Show>
<Editor
shoutId={props.shout.id}
initialContent={props.shout.body}

View File

@ -0,0 +1,39 @@
.VideoPlayer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 1rem 0;
position: relative;
.controls {
display: flex;
flex-direction: row;
margin-top: auto;
}
.deleteAction {
position: absolute;
top: -15px;
right: -15px;
padding: 0;
min-width: 30px;
}
.deleteIcon {
filter: invert(1);
}
.videoContainer {
width: 100%;
aspect-ratio: 16/9;
@include media-breakpoint-up(md) {
margin: 0 0 1em -16.6666%;
}
iframe {
height: 100%;
width: 100%;
}
}
}

View File

@ -0,0 +1,79 @@
import { createEffect, createSignal, Match, Switch, Show } from 'solid-js'
import { Button } from '../Button'
import { Icon } from '../Icon'
import { Popover } from '../Popover'
import { clsx } from 'clsx'
import styles from './VideoPlayer.module.scss'
import { useLocalize } from '../../../context/localize'
type Props = {
videoUrl: string
title?: string
description?: string
class?: string
deleteAction?: () => void
}
export const VideoPlayer = (props: Props) => {
const { t } = useLocalize()
const [videoId, setVideoId] = createSignal<string | undefined>()
const [isVimeo, setIsVimeo] = createSignal(false)
createEffect(() => {
const isYoutube = props.videoUrl.includes('youtube.com') || props.videoUrl.includes('youtu.be')
setIsVimeo(!isYoutube)
if (isYoutube) {
if (props.videoUrl.includes('youtube.com')) {
const videoIdMatch = props.videoUrl.match(/v=(\w+)/)
setVideoId(videoIdMatch && videoIdMatch[1])
} else {
const videoIdMatch = props.videoUrl.match(/youtu.be\/(\w+)/)
setVideoId(videoIdMatch && videoIdMatch[1])
}
} else {
const videoIdMatch = props.videoUrl.match(/vimeo.com\/(\d+)/)
setVideoId(videoIdMatch && videoIdMatch[1])
}
})
return (
<div class={clsx(styles.VideoPlayer, props.class)}>
<Show when={props.deleteAction}>
<Popover content={t('Delete')}>
{(triggerRef: (el) => void) => (
<Button
ref={triggerRef}
size="S"
class={styles.deleteAction}
onClick={props.deleteAction}
value={<Icon class={styles.deleteIcon} name="delete" />}
/>
)}
</Popover>
</Show>
<Switch>
<Match when={isVimeo()}>
<div class={styles.videoContainer}>
<iframe
src={`https://player.vimeo.com/video/${videoId()}`}
width="640"
height="360"
allow="autoplay; fullscreen; picture-in-picture"
allowfullscreen
/>
</div>
</Match>
<Match when={!isVimeo()}>
<div class={styles.videoContainer}>
<iframe
width="560"
height="315"
src={`https://www.youtube.com/embed/${videoId()}`}
allowfullscreen
/>
</div>
</Match>
</Switch>
</div>
)
}

View File

@ -0,0 +1 @@
export { VideoPlayer } from './VideoPlayer'

View File

@ -16,6 +16,7 @@ type WordCounter = {
}
type ShoutForm = {
layout?: string
shoutId: number
slug: string
title: string
@ -24,6 +25,7 @@ type ShoutForm = {
mainTopic?: Topic
body: string
coverImageUrl: string
media?: string
}
type EditorContextType = {
@ -89,6 +91,12 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
return false
}
const parsedMedia = JSON.parse(form.media)
if (form.layout === 'video' && !parsedMedia[0]) {
showSnackbar({ type: 'error', body: t('Looks like you forgot to upload the video') })
return false
}
return true
}
@ -113,7 +121,8 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
slug: form.slug,
subtitle: form.subtitle,
title: form.title,
cover: form.coverImageUrl
cover: form.coverImageUrl,
media: form.media
},
publish
})

View File

@ -11,6 +11,7 @@ export default gql`
cover
# community
mainTopic
media
topics {
id
title

View File

@ -582,7 +582,9 @@ export type ShoutInput = {
body?: InputMaybe<Scalars['String']>
community?: InputMaybe<Scalars['Int']>
cover?: InputMaybe<Scalars['String']>
layout?: InputMaybe<Scalars['String']>
mainTopic?: InputMaybe<TopicInput>
media?: InputMaybe<Scalars['String']>
slug?: InputMaybe<Scalars['String']>
subtitle?: InputMaybe<Scalars['String']>
title?: InputMaybe<Scalars['String']>

View File

@ -8,9 +8,11 @@ import { apiClient } from '../utils/apiClient'
import { redirectPage } from '@nanostores/router'
import { router } from '../stores/router'
const handleCreateArticle = async () => {
const shout = await apiClient.createArticle({ article: {} })
redirectPage(router, 'edit', { shoutId: shout.id.toString() })
const handleCreate = async (layout: 'article' | 'video') => {
const shout = await apiClient.createArticle({ article: { layout: layout } })
redirectPage(router, 'edit', {
shoutId: shout.id.toString()
})
}
export const CreatePage = () => {
@ -21,7 +23,7 @@ export const CreatePage = () => {
<h1>{t('Choose a post type')}</h1>
<ul class={clsx('nodash', styles.list)}>
<li>
<div class={styles.link} onClick={handleCreateArticle}>
<div class={styles.link} onClick={() => handleCreate('article')}>
<Icon name="create-article" class={styles.icon} />
<div>{t('article')}</div>
</div>
@ -45,10 +47,10 @@ export const CreatePage = () => {
</a>
</li>
<li>
<a href="#">
<div class={styles.link} onClick={() => handleCreate('video')}>
<Icon name="create-video" class={styles.icon} />
<div>{t('music')}</div>
</a>
<div>{t('video')}</div>
</div>
</li>
</ul>
<Button value={t('Back')} onClick={() => window.history.back()} />

View File

@ -1,14 +1,16 @@
import { createMemo, createSignal, lazy, onMount, Show, Suspense } from 'solid-js'
import { createEffect, 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'
import { useLocalize } from '../context/localize'
const EditView = lazy(() => import('../components/Views/Edit'))
const Edit = lazy(() => import('../components/Views/Edit'))
export const EditPage = () => {
const { t } = useLocalize()
const { isAuthenticated, isSessionLoaded } = useSession()
const { page } = useRouter()
@ -30,14 +32,14 @@ export const EditPage = () => {
fallback={
<div class="wide-container">
<div class="row">
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">Давайте авторизуемся</div>
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">{t("Let's log in")}</div>
</div>
</div>
}
>
<Show when={shout()}>
<Suspense fallback={<Loading />}>
<EditView shout={shout()} />
<Edit shout={shout()} />
</Suspense>
</Show>
</Show>

View File

@ -36,7 +36,7 @@
}
.icon {
margin: auto 21px auto;
margin: auto auto 21px;
img {
height: 54px;

View File

@ -245,7 +245,7 @@ export const apiClient = {
},
createArticle: async ({ article }: { article: ShoutInput }): Promise<Shout> => {
const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise()
console.debug('[createArticle]:', response.data)
console.log('!!! [createArticle]:', response.data)
return response.data.createShout.shout
},
updateArticle: async ({