Feature/video upload (#107)
* Add Video Player and Video Uploader * Remove old video component
This commit is contained in:
parent
5b824d8e2f
commit
be53a5dce8
|
@ -132,6 +132,7 @@
|
||||||
"Just start typing...": "Just start typing...",
|
"Just start typing...": "Just start typing...",
|
||||||
"Knowledge base": "Knowledge base",
|
"Knowledge base": "Knowledge base",
|
||||||
"Last rev.": "Посл. изм.",
|
"Last rev.": "Посл. изм.",
|
||||||
|
"Let's log in": "Let's log in",
|
||||||
"Link sent, check your email": "Link sent, check your email",
|
"Link sent, check your email": "Link sent, check your email",
|
||||||
"Lists": "Lists",
|
"Lists": "Lists",
|
||||||
"Literature": "Literature",
|
"Literature": "Literature",
|
||||||
|
@ -231,6 +232,7 @@
|
||||||
"Thank you": "Thank you",
|
"Thank you": "Thank you",
|
||||||
"This comment has not yet been rated": "This comment has not yet been rated",
|
"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 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",
|
"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 leave a comment please": "To leave a comment please",
|
||||||
"To write a comment, you must": "To write a comment, you must",
|
"To write a comment, you must": "To write a comment, you must",
|
||||||
|
@ -250,9 +252,11 @@
|
||||||
"Unfollow the topic": "Unfollow the topic",
|
"Unfollow the topic": "Unfollow the topic",
|
||||||
"Unnamed draft": "Unnamed draft",
|
"Unnamed draft": "Unnamed draft",
|
||||||
"Upload": "Upload",
|
"Upload": "Upload",
|
||||||
|
"Upload video": "Upload video",
|
||||||
"Username": "Username",
|
"Username": "Username",
|
||||||
"Userpic": "Userpic",
|
"Userpic": "Userpic",
|
||||||
"Video": "Video",
|
"Video": "Video",
|
||||||
|
"Video format not supported": "Video format not supported",
|
||||||
"Views": "Views",
|
"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 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",
|
"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",
|
"user already exist": "user already exists",
|
||||||
"video": "video",
|
"video": "video",
|
||||||
"view": "view",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,6 +139,7 @@
|
||||||
"Karma": "Карма",
|
"Karma": "Карма",
|
||||||
"Knowledge base": "База знаний",
|
"Knowledge base": "База знаний",
|
||||||
"Last rev.": "Посл. изм.",
|
"Last rev.": "Посл. изм.",
|
||||||
|
"Let's log in": "Давайте авторизуемся",
|
||||||
"Link sent, check your email": "Ссылка отправлена, проверьте почту",
|
"Link sent, check your email": "Ссылка отправлена, проверьте почту",
|
||||||
"Lists": "Списки",
|
"Lists": "Списки",
|
||||||
"Literature": "Литература",
|
"Literature": "Литература",
|
||||||
|
@ -244,6 +245,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": "Такой email уже зарегистрирован. Если это вы",
|
"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": "Эту публикацию еще пока никто не оценил",
|
"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": "Чтобы написать комментарий, необходимо",
|
||||||
|
@ -263,9 +265,11 @@
|
||||||
"Unfollow the topic": "Отписаться от темы",
|
"Unfollow the topic": "Отписаться от темы",
|
||||||
"Unnamed draft": "Unnamed draft",
|
"Unnamed draft": "Unnamed draft",
|
||||||
"Upload": "Загрузить",
|
"Upload": "Загрузить",
|
||||||
|
"Upload video": "Загрузить видео",
|
||||||
"Username": "Имя пользователя",
|
"Username": "Имя пользователя",
|
||||||
"Userpic": "Аватар",
|
"Userpic": "Аватар",
|
||||||
"Video": "Видео",
|
"Video": "Видео",
|
||||||
|
"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": "Не можем вас найти, проверьте адрес электронной почты или",
|
||||||
|
@ -343,5 +347,7 @@
|
||||||
"user already exist": "пользователь уже существует",
|
"user already exist": "пользователь уже существует",
|
||||||
"video": "видео",
|
"video": "видео",
|
||||||
"view": "просмотр",
|
"view": "просмотр",
|
||||||
"zine": "журнал"
|
"zine": "журнал",
|
||||||
|
"Insert video link": "Вставить ссылку на видео",
|
||||||
|
"Looks like you forgot to upload the video": "Похоже, что вы забыли загрузить видео"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { ShoutRatingControl } from './ShoutRatingControl'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { CommentsTree } from './CommentsTree'
|
import { CommentsTree } from './CommentsTree'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import VideoPlayer from './VideoPlayer'
|
import { VideoPlayer } from '../_shared/VideoPlayer'
|
||||||
import Slider from '../_shared/Slider'
|
import Slider from '../_shared/Slider'
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { router, useRouter } from '../../stores/router'
|
import { router, useRouter } from '../../stores/router'
|
||||||
|
@ -29,7 +29,6 @@ interface ArticleProps {
|
||||||
|
|
||||||
interface MediaItem {
|
interface MediaItem {
|
||||||
url?: string
|
url?: string
|
||||||
pic?: string
|
|
||||||
title?: string
|
title?: string
|
||||||
body?: string
|
body?: string
|
||||||
}
|
}
|
||||||
|
@ -40,17 +39,12 @@ const MediaView = (props: { media: MediaItem; kind: Shout['layout'] }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Switch fallback={<a href={props.media.url}>{t('Cannot show this media type')}</a>}>
|
<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'}>
|
<Match when={props.kind === 'video'}>
|
||||||
<VideoPlayer url={props.media.url} />
|
<VideoPlayer
|
||||||
|
videoUrl={props.media.url}
|
||||||
|
title={props.media.title}
|
||||||
|
description={props.media.body}
|
||||||
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</>
|
</>
|
||||||
|
@ -169,15 +163,12 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={media() && props.article.layout !== 'image'}>
|
<Show when={media()}>
|
||||||
<div class="media-items">
|
<div class="media-items">
|
||||||
<For each={media() || []}>
|
<For each={media() || []}>
|
||||||
{(m: MediaItem) => (
|
{(m: MediaItem) => (
|
||||||
<div class={styles.shoutMediaBody}>
|
<div class={styles.shoutMediaBody}>
|
||||||
<MediaView media={m} kind={props.article.layout} />
|
<MediaView media={m} kind={props.article.layout} />
|
||||||
<Show when={m?.body}>
|
|
||||||
<div innerHTML={m.body} />
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
@ -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%;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
|
@ -19,7 +19,7 @@ export const BlockquoteBubbleMenu = (props: Props) => {
|
||||||
<button
|
<button
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.editor.chain().focus().setBlockQuoteFloat('left').run()
|
props.editor.chain().focus().setBlockQuoteFloat('left').run()
|
||||||
}}
|
}}
|
||||||
|
@ -33,7 +33,7 @@ export const BlockquoteBubbleMenu = (props: Props) => {
|
||||||
<button
|
<button
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setBlockQuoteFloat(null).run()}
|
onClick={() => props.editor.chain().focus().setBlockQuoteFloat(null).run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-center" />
|
<Icon name="editor-image-align-center" />
|
||||||
|
@ -45,7 +45,7 @@ export const BlockquoteBubbleMenu = (props: Props) => {
|
||||||
<button
|
<button
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setBlockQuoteFloat('right').run()}
|
onClick={() => props.editor.chain().focus().setBlockQuoteFloat('right').run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-right" />
|
<Icon name="editor-image-align-right" />
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const FigureBubbleMenu = (props: Props) => {
|
||||||
<button
|
<button
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setImageFloat('left').run()}
|
onClick={() => props.editor.chain().focus().setImageFloat('left').run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-left" />
|
<Icon name="editor-image-align-left" />
|
||||||
|
@ -31,7 +31,7 @@ export const FigureBubbleMenu = (props: Props) => {
|
||||||
<button
|
<button
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setImageFloat(null).run()}
|
onClick={() => props.editor.chain().focus().setImageFloat(null).run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-center" />
|
<Icon name="editor-image-align-center" />
|
||||||
|
@ -43,7 +43,7 @@ export const FigureBubbleMenu = (props: Props) => {
|
||||||
<button
|
<button
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setImageFloat('right').run()}
|
onClick={() => props.editor.chain().focus().setImageFloat('right').run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-right" />
|
<Icon name="editor-image-align-right" />
|
||||||
|
@ -53,7 +53,7 @@ export const FigureBubbleMenu = (props: Props) => {
|
||||||
<div class={styles.delimiter} />
|
<div class={styles.delimiter} />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.editor.chain().focus().imageToFigure().run()
|
props.editor.chain().focus().imageToFigure().run()
|
||||||
}}
|
}}
|
||||||
|
@ -63,7 +63,7 @@ export const FigureBubbleMenu = (props: Props) => {
|
||||||
<div class={styles.delimiter} />
|
<div class={styles.delimiter} />
|
||||||
<Popover content={t('Add image')}>
|
<Popover content={t('Add image')}>
|
||||||
{(triggerRef: (el) => void) => (
|
{(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" />
|
<Icon name="editor-image-add" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -19,21 +19,21 @@ export const IncutBubbleMenu = (props: Props) => {
|
||||||
<div ref={props.ref} class={styles.BubbleMenu}>
|
<div ref={props.ref} class={styles.BubbleMenu}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setArticleFloat('left').run()}
|
onClick={() => props.editor.chain().focus().setArticleFloat('left').run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-left" />
|
<Icon name="editor-image-align-left" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setArticleFloat('half-left').run()}
|
onClick={() => props.editor.chain().focus().setArticleFloat('half-left').run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-half-align-left" />
|
<Icon name="editor-image-half-align-left" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setArticleFloat(null).run()}
|
onClick={() => props.editor.chain().focus().setArticleFloat(null).run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-center" />
|
<Icon name="editor-image-align-center" />
|
||||||
|
@ -41,7 +41,7 @@ export const IncutBubbleMenu = (props: Props) => {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setArticleFloat('half-right').run()}
|
onClick={() => props.editor.chain().focus().setArticleFloat('half-right').run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-half-align-right" />
|
<Icon name="editor-image-half-align-right" />
|
||||||
|
@ -49,7 +49,7 @@ export const IncutBubbleMenu = (props: Props) => {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setArticleFloat('right').run()}
|
onClick={() => props.editor.chain().focus().setArticleFloat('right').run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-right" />
|
<Icon name="editor-image-align-right" />
|
||||||
|
@ -59,7 +59,7 @@ export const IncutBubbleMenu = (props: Props) => {
|
||||||
<div class={styles.dropDownHolder}>
|
<div class={styles.dropDownHolder}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => setSubstratBubbleOpen(!substratBubbleOpen())}
|
onClick={() => setSubstratBubbleOpen(!substratBubbleOpen())}
|
||||||
>
|
>
|
||||||
<span style={{ color: 'white' }}>{t('Substrate')}</span>
|
<span style={{ color: 'white' }}>{t('Substrate')}</span>
|
||||||
|
|
|
@ -58,7 +58,6 @@ export const Editor = (props: EditorProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { user } = useSession()
|
const { user } = useSession()
|
||||||
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
||||||
const [floatMenuRef, setFloatMenuRef] = createSignal<'blockquote' | 'image' | 'incut'>()
|
|
||||||
|
|
||||||
const docName = `shout-${props.shoutId}`
|
const docName = `shout-${props.shoutId}`
|
||||||
|
|
||||||
|
|
|
@ -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%);
|
||||||
|
}
|
||||||
|
}
|
133
src/components/Editor/VideoUploader/VideoUploader.tsx
Normal file
133
src/components/Editor/VideoUploader/VideoUploader.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/Editor/VideoUploader/index.ts
Normal file
1
src/components/Editor/VideoUploader/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { VideoUploader } from './VideoUploader'
|
|
@ -218,6 +218,7 @@
|
||||||
|
|
||||||
.validationError {
|
.validationError {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
font-size: small;
|
font-size: small;
|
||||||
color: #f00;
|
color: #f00;
|
||||||
|
|
|
@ -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 { useLocalize } from '../../context/localize'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Title } from '@solidjs/meta'
|
import { Title } from '@solidjs/meta'
|
||||||
|
@ -15,8 +15,11 @@ import { Modal } from '../Nav/Modal'
|
||||||
import { hideModal, showModal } from '../../stores/ui'
|
import { hideModal, showModal } from '../../stores/ui'
|
||||||
import { imageProxy } from '../../utils/imageProxy'
|
import { imageProxy } from '../../utils/imageProxy'
|
||||||
import { GrowingTextarea } from '../_shared/GrowingTextarea'
|
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
|
shout: Shout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,12 +36,13 @@ const EMPTY_TOPIC: Topic = {
|
||||||
slug: ''
|
slug: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditView = (props: EditViewProps) => {
|
export const EditView = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { user } = useSession()
|
const { user } = useSession()
|
||||||
const [isScrolled, setIsScrolled] = createSignal(false)
|
const [isScrolled, setIsScrolled] = createSignal(false)
|
||||||
const [topics, setTopics] = createSignal<Topic[]>(null)
|
const [topics, setTopics] = createSignal<Topic[]>(null)
|
||||||
const [coverImage, setCoverImage] = createSignal<string>(null)
|
const [coverImage, setCoverImage] = createSignal<string>(null)
|
||||||
|
const [media, setMedia] = createSignal<string>(props.shout.media)
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const {
|
const {
|
||||||
form,
|
form,
|
||||||
|
@ -56,7 +60,9 @@ export const EditView = (props: EditViewProps) => {
|
||||||
selectedTopics: shoutTopics,
|
selectedTopics: shoutTopics,
|
||||||
mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC,
|
mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC,
|
||||||
body: props.shout.body,
|
body: props.shout.body,
|
||||||
coverImageUrl: props.shout.cover
|
coverImageUrl: props.shout.cover,
|
||||||
|
media: media(),
|
||||||
|
layout: props.shout.layout
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
@ -77,7 +83,7 @@ export const EditView = (props: EditViewProps) => {
|
||||||
|
|
||||||
const handleTitleInputChange = (value) => {
|
const handleTitleInputChange = (value) => {
|
||||||
setForm('title', value)
|
setForm('title', value)
|
||||||
|
setForm('slug', slugify(value))
|
||||||
if (value) {
|
if (value) {
|
||||||
setFormErrors('title', '')
|
setFormErrors('title', '')
|
||||||
}
|
}
|
||||||
|
@ -111,10 +117,13 @@ export const EditView = (props: EditViewProps) => {
|
||||||
if (newSelectedTopics.length > 0) {
|
if (newSelectedTopics.length > 0) {
|
||||||
setFormErrors('selectedTopics', '')
|
setFormErrors('selectedTopics', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
setForm('selectedTopics', newSelectedTopics)
|
setForm('selectedTopics', newSelectedTopics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddMedia = (data) => {
|
||||||
|
setForm('media', JSON.stringify([data]))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class={styles.container}>
|
<div class={styles.container}>
|
||||||
|
@ -157,6 +166,33 @@ export const EditView = (props: EditViewProps) => {
|
||||||
initialValue={form.subtitle}
|
initialValue={form.subtitle}
|
||||||
maxLength={100}
|
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
|
<Editor
|
||||||
shoutId={props.shout.id}
|
shoutId={props.shout.id}
|
||||||
initialContent={props.shout.body}
|
initialContent={props.shout.body}
|
||||||
|
|
39
src/components/_shared/VideoPlayer/VideoPlayer.module.scss
Normal file
39
src/components/_shared/VideoPlayer/VideoPlayer.module.scss
Normal 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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
src/components/_shared/VideoPlayer/VideoPlayer.tsx
Normal file
79
src/components/_shared/VideoPlayer/VideoPlayer.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/_shared/VideoPlayer/index.ts
Normal file
1
src/components/_shared/VideoPlayer/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { VideoPlayer } from './VideoPlayer'
|
|
@ -16,6 +16,7 @@ type WordCounter = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShoutForm = {
|
type ShoutForm = {
|
||||||
|
layout?: string
|
||||||
shoutId: number
|
shoutId: number
|
||||||
slug: string
|
slug: string
|
||||||
title: string
|
title: string
|
||||||
|
@ -24,6 +25,7 @@ type ShoutForm = {
|
||||||
mainTopic?: Topic
|
mainTopic?: Topic
|
||||||
body: string
|
body: string
|
||||||
coverImageUrl: string
|
coverImageUrl: string
|
||||||
|
media?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditorContextType = {
|
type EditorContextType = {
|
||||||
|
@ -89,6 +91,12 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +121,8 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
slug: form.slug,
|
slug: form.slug,
|
||||||
subtitle: form.subtitle,
|
subtitle: form.subtitle,
|
||||||
title: form.title,
|
title: form.title,
|
||||||
cover: form.coverImageUrl
|
cover: form.coverImageUrl,
|
||||||
|
media: form.media
|
||||||
},
|
},
|
||||||
publish
|
publish
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,6 +11,7 @@ export default gql`
|
||||||
cover
|
cover
|
||||||
# community
|
# community
|
||||||
mainTopic
|
mainTopic
|
||||||
|
media
|
||||||
topics {
|
topics {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
|
|
|
@ -582,7 +582,9 @@ export type ShoutInput = {
|
||||||
body?: InputMaybe<Scalars['String']>
|
body?: InputMaybe<Scalars['String']>
|
||||||
community?: InputMaybe<Scalars['Int']>
|
community?: InputMaybe<Scalars['Int']>
|
||||||
cover?: InputMaybe<Scalars['String']>
|
cover?: InputMaybe<Scalars['String']>
|
||||||
|
layout?: InputMaybe<Scalars['String']>
|
||||||
mainTopic?: InputMaybe<TopicInput>
|
mainTopic?: InputMaybe<TopicInput>
|
||||||
|
media?: InputMaybe<Scalars['String']>
|
||||||
slug?: InputMaybe<Scalars['String']>
|
slug?: InputMaybe<Scalars['String']>
|
||||||
subtitle?: InputMaybe<Scalars['String']>
|
subtitle?: InputMaybe<Scalars['String']>
|
||||||
title?: InputMaybe<Scalars['String']>
|
title?: InputMaybe<Scalars['String']>
|
||||||
|
|
|
@ -8,9 +8,11 @@ import { apiClient } from '../utils/apiClient'
|
||||||
import { redirectPage } from '@nanostores/router'
|
import { redirectPage } from '@nanostores/router'
|
||||||
import { router } from '../stores/router'
|
import { router } from '../stores/router'
|
||||||
|
|
||||||
const handleCreateArticle = async () => {
|
const handleCreate = async (layout: 'article' | 'video') => {
|
||||||
const shout = await apiClient.createArticle({ article: {} })
|
const shout = await apiClient.createArticle({ article: { layout: layout } })
|
||||||
redirectPage(router, 'edit', { shoutId: shout.id.toString() })
|
redirectPage(router, 'edit', {
|
||||||
|
shoutId: shout.id.toString()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreatePage = () => {
|
export const CreatePage = () => {
|
||||||
|
@ -21,7 +23,7 @@ export const CreatePage = () => {
|
||||||
<h1>{t('Choose a post type')}</h1>
|
<h1>{t('Choose a post type')}</h1>
|
||||||
<ul class={clsx('nodash', styles.list)}>
|
<ul class={clsx('nodash', styles.list)}>
|
||||||
<li>
|
<li>
|
||||||
<div class={styles.link} onClick={handleCreateArticle}>
|
<div class={styles.link} onClick={() => handleCreate('article')}>
|
||||||
<Icon name="create-article" class={styles.icon} />
|
<Icon name="create-article" class={styles.icon} />
|
||||||
<div>{t('article')}</div>
|
<div>{t('article')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,10 +47,10 @@ export const CreatePage = () => {
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#">
|
<div class={styles.link} onClick={() => handleCreate('video')}>
|
||||||
<Icon name="create-video" class={styles.icon} />
|
<Icon name="create-video" class={styles.icon} />
|
||||||
<div>{t('music')}</div>
|
<div>{t('video')}</div>
|
||||||
</a>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Button value={t('Back')} onClick={() => window.history.back()} />
|
<Button value={t('Back')} onClick={() => window.history.back()} />
|
||||||
|
|
|
@ -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 { PageLayout } from '../components/_shared/PageLayout'
|
||||||
import { Loading } from '../components/_shared/Loading'
|
import { Loading } from '../components/_shared/Loading'
|
||||||
import { useSession } from '../context/session'
|
import { useSession } from '../context/session'
|
||||||
import { Shout } from '../graphql/types.gen'
|
import { Shout } from '../graphql/types.gen'
|
||||||
import { useRouter } from '../stores/router'
|
import { useRouter } from '../stores/router'
|
||||||
import { apiClient } from '../utils/apiClient'
|
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 = () => {
|
export const EditPage = () => {
|
||||||
|
const { t } = useLocalize()
|
||||||
const { isAuthenticated, isSessionLoaded } = useSession()
|
const { isAuthenticated, isSessionLoaded } = useSession()
|
||||||
|
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
|
@ -30,14 +32,14 @@ export const EditPage = () => {
|
||||||
fallback={
|
fallback={
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class="row">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show when={shout()}>
|
<Show when={shout()}>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<EditView shout={shout()} />
|
<Edit shout={shout()} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin: auto 21px auto;
|
margin: auto auto 21px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 54px;
|
height: 54px;
|
||||||
|
|
|
@ -245,7 +245,7 @@ export const apiClient = {
|
||||||
},
|
},
|
||||||
createArticle: async ({ article }: { article: ShoutInput }): Promise<Shout> => {
|
createArticle: async ({ article }: { article: ShoutInput }): Promise<Shout> => {
|
||||||
const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise()
|
const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise()
|
||||||
console.debug('[createArticle]:', response.data)
|
console.log('!!! [createArticle]:', response.data)
|
||||||
return response.data.createShout.shout
|
return response.data.createShout.shout
|
||||||
},
|
},
|
||||||
updateArticle: async ({
|
updateArticle: async ({
|
||||||
|
|
Loading…
Reference in New Issue
Block a user