From 9f7d5d04b6c36c5a81751d1c31f7a73efa6d1d3e Mon Sep 17 00:00:00 2001
From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com>
Date: Sun, 2 Jul 2023 08:08:42 +0300
Subject: [PATCH] Feature/gallery upload (#117)
* upgrade Swiper
---
package-lock.json | 35 +-
package.json | 2 +-
public/icons/delete-white.svg | 3 +
public/icons/swiper-l-arr.svg | 3 +
public/icons/swiper-plus.svg | 4 +
public/icons/swiper-r-arr.svg | 3 +
public/locales/en/translation.json | 13 +-
public/locales/ru/translation.json | 16 +-
.../Article/AudioPlayer/AudioPlayer.tsx | 2 +-
src/components/Article/CommentDate.tsx | 5 +-
src/components/Article/FullArticle.tsx | 132 +++----
src/components/Discours/Subscribe.tsx | 4 +-
.../Editor/VideoUploader/VideoUploader.tsx | 15 +-
src/components/Editor/extensions/Article.ts | 1 +
src/components/Feed/ArticleCard.tsx | 26 +-
.../Nav/AuthModal/ForgotPasswordForm.tsx | 4 +-
src/components/Nav/AuthModal/LoginForm.tsx | 4 +-
src/components/Nav/AuthModal/RegisterForm.tsx | 6 +-
src/components/Nav/HeaderAuth.tsx | 6 +-
src/components/Views/Edit.tsx | 49 ++-
src/components/Views/Feed.tsx | 1 -
src/components/Views/Home.tsx | 2 +-
src/components/Views/Topic.tsx | 2 +-
.../_shared/DropArea/DropArea.module.scss | 71 ++++
src/components/_shared/DropArea/DropArea.tsx | 103 +++++
src/components/_shared/DropArea/index.ts | 1 +
.../GrowingTextarea/GrowingTextarea.tsx | 4 +-
src/components/_shared/Loading.module.scss | 5 +
src/components/_shared/Loading.tsx | 12 +-
.../_shared/{ => Slider}/Slider.scss | 40 ++
.../_shared/{ => Slider}/Slider.tsx | 60 +--
src/components/_shared/Slider/index.ts | 1 +
.../_shared/SolidSwiper/SolidSwiper.tsx | 352 ++++++++++++++++++
.../_shared/SolidSwiper/Swiper.module.scss | 323 ++++++++++++++++
src/components/_shared/SolidSwiper/index.ts | 1 +
.../_shared/SolidSwiper/swiper.d.ts | 45 +++
src/pages/create.page.tsx | 7 +-
src/pages/layoutShouts.page.tsx | 2 +-
src/pages/types.ts | 11 +-
src/stores/zine/layouts.ts | 48 +++
src/styles/app.scss | 6 +
src/utils/handleFileUpload.ts | 1 +
src/utils/{validators.ts => validateEmail.ts} | 2 +-
src/utils/validateFile.ts | 37 ++
44 files changed, 1271 insertions(+), 199 deletions(-)
create mode 100644 public/icons/delete-white.svg
create mode 100644 public/icons/swiper-l-arr.svg
create mode 100644 public/icons/swiper-plus.svg
create mode 100644 public/icons/swiper-r-arr.svg
create mode 100644 src/components/_shared/DropArea/DropArea.module.scss
create mode 100644 src/components/_shared/DropArea/DropArea.tsx
create mode 100644 src/components/_shared/DropArea/index.ts
rename src/components/_shared/{ => Slider}/Slider.scss (85%)
rename src/components/_shared/{ => Slider}/Slider.tsx (65%)
create mode 100644 src/components/_shared/Slider/index.ts
create mode 100644 src/components/_shared/SolidSwiper/SolidSwiper.tsx
create mode 100644 src/components/_shared/SolidSwiper/Swiper.module.scss
create mode 100644 src/components/_shared/SolidSwiper/index.ts
create mode 100644 src/components/_shared/SolidSwiper/swiper.d.ts
create mode 100644 src/stores/zine/layouts.ts
rename src/utils/{validators.ts => validateEmail.ts} (66%)
create mode 100644 src/utils/validateFile.ts
diff --git a/package-lock.json b/package-lock.json
index e98981fa..8140903c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -142,7 +142,7 @@
"stylelint-config-standard-scss": "9.0.0",
"stylelint-order": "6.0.3",
"stylelint-scss": "5.0.0",
- "swiper": "8.4.7",
+ "swiper": "9.4.1",
"ts-node": "10.9.1",
"typescript": "5.0.4",
"undici": "5.21.0",
@@ -8792,15 +8792,6 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/dom7": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.6.tgz",
- "integrity": "sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA==",
- "dev": true,
- "dependencies": {
- "ssr-window": "^4.0.0"
- }
- },
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
@@ -19178,9 +19169,9 @@
}
},
"node_modules/swiper": {
- "version": "8.4.7",
- "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.7.tgz",
- "integrity": "sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g==",
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/swiper/-/swiper-9.4.1.tgz",
+ "integrity": "sha512-1nT2T8EzUpZ0FagEqaN/YAhRj33F2x/lN6cyB0/xoYJDMf8KwTFT3hMOeoB8Tg4o3+P/CKqskP+WX0Df046fqA==",
"dev": true,
"funding": [
{
@@ -19192,9 +19183,7 @@
"url": "http://opencollective.com/swiper"
}
],
- "hasInstallScript": true,
"dependencies": {
- "dom7": "^4.0.4",
"ssr-window": "^4.0.2"
},
"engines": {
@@ -27161,15 +27150,6 @@
}
}
},
- "dom7": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.6.tgz",
- "integrity": "sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA==",
- "dev": true,
- "requires": {
- "ssr-window": "^4.0.0"
- }
- },
"domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
@@ -34870,12 +34850,11 @@
}
},
"swiper": {
- "version": "8.4.7",
- "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.7.tgz",
- "integrity": "sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g==",
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/swiper/-/swiper-9.4.1.tgz",
+ "integrity": "sha512-1nT2T8EzUpZ0FagEqaN/YAhRj33F2x/lN6cyB0/xoYJDMf8KwTFT3hMOeoB8Tg4o3+P/CKqskP+WX0Df046fqA==",
"dev": true,
"requires": {
- "dom7": "^4.0.4",
"ssr-window": "^4.0.2"
}
},
diff --git a/package.json b/package.json
index 78f2dd31..867fa707 100644
--- a/package.json
+++ b/package.json
@@ -162,7 +162,7 @@
"stylelint-config-standard-scss": "9.0.0",
"stylelint-order": "6.0.3",
"stylelint-scss": "5.0.0",
- "swiper": "8.4.7",
+ "swiper": "9.4.1",
"ts-node": "10.9.1",
"typescript": "5.0.4",
"undici": "5.21.0",
diff --git a/public/icons/delete-white.svg b/public/icons/delete-white.svg
new file mode 100644
index 00000000..0d73c7b8
--- /dev/null
+++ b/public/icons/delete-white.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/icons/swiper-l-arr.svg b/public/icons/swiper-l-arr.svg
new file mode 100644
index 00000000..037f4032
--- /dev/null
+++ b/public/icons/swiper-l-arr.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/icons/swiper-plus.svg b/public/icons/swiper-plus.svg
new file mode 100644
index 00000000..2861f158
--- /dev/null
+++ b/public/icons/swiper-plus.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/icons/swiper-r-arr.svg b/public/icons/swiper-r-arr.svg
new file mode 100644
index 00000000..1f615fe1
--- /dev/null
+++ b/public/icons/swiper-r-arr.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 8ec92dcd..99e2c25f 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -5,6 +5,7 @@
"Add another image": "Add another image",
"Add comment": "Comment",
"Add image": "Add image",
+ "Add images": "Add images",
"Add link": "Add link",
"Add signature": "Add signature",
"Add url": "Add url",
@@ -52,7 +53,6 @@
"Cooperate": "Cooperate",
"Copy": "Copy",
"Copy link": "Copy link",
- "Link copied": "Link copied",
"Corrections history": "Corrections history",
"Create Chat": "Create Chat",
"Create Group": "Create a group",
@@ -74,18 +74,16 @@
"Dogma": "Dogma",
"Drafts": "Drafts",
"Drag the image to this area": "Drag the image to this area",
+ "Each image must be no larger than 5 MB.": "Each image must be no larger than 5 MB.",
"Edit": "Edit",
"Editing": "Editing",
"Email": "Mail",
"Enter": "Enter",
"Enter URL address": "Enter URL address",
+ "Enter image description": "Enter image description",
+ "Enter image title": "Enter image title",
"Enter text": "Enter text",
"Enter the Discours": "Enter the Discours",
- "Enter the Discours from bookmark": "Sign in to add to bookmarks",
- "Enter the Discours from discussions": "Sign in to participate in the discussions",
- "Enter the Discours from follow": "Sign in to follow",
- "Enter the Discours from subscribe": "Sign in to subscribe to new publications",
- "Enter the Discours from vote": "Sign in to vote",
"Enter the code or click the link from email to confirm": "Enter the code from the email or follow the link in the email to confirm registration",
"Enter your new password": "Enter your new password",
"Error": "Error",
@@ -147,6 +145,7 @@
"Knowledge base": "Knowledge base",
"Last rev.": "Посл. изм.",
"Let's log in": "Let's log in",
+ "Link copied": "Link copied",
"Link sent, check your email": "Link sent, check your email",
"Lists": "Lists",
"Literature": "Literature",
@@ -232,6 +231,7 @@
"Something went wrong, please try again": "Something went wrong, please try again",
"Sorry, this address is already taken, please choose another one.": "Sorry, this address is already taken, please choose another one",
"Special projects": "Special projects",
+ "Specify the source and the name of the author": "Specify the source and the name of the author",
"Start conversation": "Start a conversation",
"Subsccriptions": "Subscriptions",
"Subscribe": "Subscribe",
@@ -246,7 +246,6 @@
"Terms of use": "Site rules",
"Text checking": "Text checking",
"Thank you": "Thank you",
- "Thank you for subscribing": "Thank you for subscribing",
"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.",
diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json
index eead45a2..d7326f93 100644
--- a/public/locales/ru/translation.json
+++ b/public/locales/ru/translation.json
@@ -4,9 +4,11 @@
"About myself": "О себе",
"About the project": "О проекте",
"Accomplices": "Соучастники",
+ "Accomplices": "Соучастники",
"Add another image": "Добавить другое изображение",
"Add comment": "Комментировать",
"Add image": "Добавить изображение",
+ "Add images": "Добавить изображения",
"Add link": "Добавить ссылку",
"Add signature": "Добавить подпись",
"Add to bookmarks": "Добавить в закладки",
@@ -55,7 +57,6 @@
"Cooperate": "Соучаствовать",
"Copy": "Скопировать",
"Copy link": "Скопировать ссылку",
- "Link copied": "Ссылка скопирована",
"Corrections history": "История правок",
"Create Chat": "Создать чат",
"Create Group": "Создать группу",
@@ -77,19 +78,17 @@
"Dogma": "Догма",
"Drafts": "Черновики",
"Drag the image to this area": "Перетащите изображение в эту область",
+ "Each image must be no larger than 5 MB.": "Каждое изображение должно быть размером не больше 5 мб.",
"Edit": "Редактировать",
"Edited": "Отредактирован",
"Editing": "Редактирование",
"Email": "Почта",
"Enter": "Войти",
"Enter URL address": "Введите адрес ссылки",
+ "Enter image description": "Введите описание изображения",
+ "Enter image title": "Введите название изображения",
"Enter text": "Введите текст",
"Enter the Discours": "Войти в Дискурс",
- "Enter the Discours from bookmark": "Войдите, чтобы добавить в закладки",
- "Enter the Discours from discussions": "Войдите для участия в дискуссиях",
- "Enter the Discours from follow": "Войдите, чтобы подписаться",
- "Enter the Discours from subscribe": "Войдите для подписки на новые публикации",
- "Enter the Discours from vote": "Войдите, чтобы голосовать",
"Enter the code or click the link from email to confirm": "Введите код из письма или пройдите по ссылке в письме для подтверждения регистрации",
"Enter your new password": "Введите новый пароль",
"Error": "Ошибка",
@@ -155,6 +154,7 @@
"Knowledge base": "База знаний",
"Last rev.": "Посл. изм.",
"Let's log in": "Давайте авторизуемся",
+ "Link copied": "Ссылка скопирована",
"Link sent, check your email": "Ссылка отправлена, проверьте почту",
"Lists": "Списки",
"Literature": "Литература",
@@ -245,6 +245,7 @@
"Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз",
"Sorry, this address is already taken, please choose another one.": "Увы, этот адрес уже занят, выберите другой",
"Special projects": "Спецпроекты",
+ "Specify the source and the name of the author": "Укажите источник и имя автора",
"Start conversation": "Начать беседу",
"Subheader": "Подзаголовок",
"Subscribe": "Подписаться",
@@ -260,10 +261,9 @@
"Terms of use": "Правила сайта",
"Text checking": "Проверка текста",
"Thank you": "Благодарности",
- "Thank you for subscribing": "Спасибо, что подписались на рассылку",
"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 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": "Чтобы написать комментарий, необходимо",
diff --git a/src/components/Article/AudioPlayer/AudioPlayer.tsx b/src/components/Article/AudioPlayer/AudioPlayer.tsx
index 4007593c..f0b36bd6 100644
--- a/src/components/Article/AudioPlayer/AudioPlayer.tsx
+++ b/src/components/Article/AudioPlayer/AudioPlayer.tsx
@@ -80,7 +80,7 @@ export default (props: { media: MediaItem[]; articleSlug: string; body: string }
setTracks(
tracks().map((track) => ({
...track,
- isCurrent: track.id === m.id ? true : false,
+ isCurrent: track.id === m.id,
isPlaying: track.id === m.id ? !track.isPlaying : false
}))
)
diff --git a/src/components/Article/CommentDate.tsx b/src/components/Article/CommentDate.tsx
index 148c742b..28500a9d 100644
--- a/src/components/Article/CommentDate.tsx
+++ b/src/components/Article/CommentDate.tsx
@@ -1,10 +1,10 @@
-import styles from './CommentDate.module.scss'
-import { Icon } from '../_shared/Icon'
import { Show } from 'solid-js'
+import { Icon } from '../_shared/Icon'
import type { Reaction } from '../../graphql/types.gen'
import { formatDate } from '../../utils'
import { useLocalize } from '../../context/localize'
import { clsx } from 'clsx'
+import styles from './CommentDate.module.scss'
type Props = {
comment: Reaction
@@ -15,7 +15,6 @@ type Props = {
export const CommentDate = (props: Props) => {
const { t } = useLocalize()
-
const formattedDate = (date) => {
const formatDateOptions: Intl.DateTimeFormatOptions = props.isShort
? { month: 'long', day: 'numeric', year: 'numeric' }
diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx
index f879d1e5..5f15083a 100644
--- a/src/components/Article/FullArticle.tsx
+++ b/src/components/Article/FullArticle.tsx
@@ -1,5 +1,3 @@
-import { createEffect, createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js'
-
import { capitalize, formatDate } from '../../utils'
import { Icon } from '../_shared/Icon'
import { AuthorCard } from '../Author/AuthorCard'
@@ -13,7 +11,6 @@ import { clsx } from 'clsx'
import { CommentsTree } from './CommentsTree'
import { useSession } from '../../context/session'
import { VideoPlayer } from '../_shared/VideoPlayer'
-import Slider from '../_shared/Slider'
import { getPagePath } from '@nanostores/router'
import { router, useRouter } from '../../stores/router'
import { useReactions } from '../../context/reactions'
@@ -23,6 +20,9 @@ import stylesHeader from '../Nav/Header.module.scss'
import styles from './Article.module.scss'
import { imageProxy } from '../../utils/imageProxy'
import { Popover } from '../_shared/Popover'
+import article from '../Editor/extensions/Article'
+import { SolidSwiper } from '../_shared/SolidSwiper'
+import { createEffect, For, createMemo, Match, onMount, Show, Switch, createSignal } from 'solid-js'
interface ArticleProps {
article: Shout
@@ -35,33 +35,6 @@ interface MediaItem {
body?: string
}
-const MediaView = (props: { media: MediaItem; kind: Shout['layout'] }) => {
- const { t } = useLocalize()
-
- return (
- <>
- {t('Cannot show this media type')}}>
-
-
-
-
-
-
{props.media.title}
-
-
-
-
-
- >
- )
-}
-
export const FullArticle = (props: ArticleProps) => {
const { t } = useLocalize()
const {
@@ -95,8 +68,10 @@ export const FullArticle = (props: ArticleProps) => {
}, 'bookmark')
}
- const media = createMemo(() => JSON.parse(props.article.media || '[]'))
- const body = createMemo(() => props.article.body || '')
+ const body = createMemo(() => props.article.body)
+ const media = createMemo(() => {
+ return JSON.parse(props.article.media || '[]')
+ })
const commentsRef: { current: HTMLDivElement } = { current: null }
const scrollToComments = () => {
@@ -141,7 +116,8 @@ export const FullArticle = (props: ArticleProps) => {
-
+ {/*TODO: Check styles.shoutTopic*/}
+
-
-
-
- {(m) => (
-
-
-

-
-
-
-
- )}
-
-
-
-
diff --git a/src/components/Discours/Subscribe.tsx b/src/components/Discours/Subscribe.tsx
index 0d794589..6357cedb 100644
--- a/src/components/Discours/Subscribe.tsx
+++ b/src/components/Discours/Subscribe.tsx
@@ -1,7 +1,7 @@
import { createSignal, JSX, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
-import { isValidEmail } from '../../utils/validators'
+import { validateEmail } from '../../utils/validateEmail'
import { Button } from '../_shared/Button'
import styles from './Subscribe.module.scss'
@@ -23,7 +23,7 @@ export default () => {
return false
}
- if (!isValidEmail(email())) {
+ if (!validateEmail(email())) {
setEmailError(t('Please check your email address'))
return false
}
diff --git a/src/components/Editor/VideoUploader/VideoUploader.tsx b/src/components/Editor/VideoUploader/VideoUploader.tsx
index 85725c6a..86bdab77 100644
--- a/src/components/Editor/VideoUploader/VideoUploader.tsx
+++ b/src/components/Editor/VideoUploader/VideoUploader.tsx
@@ -6,32 +6,27 @@ import { createEffect, createSignal, Show } from 'solid-js'
import { useSnackbar } from '../../../context/snackbar'
import { validateUrl } from '../../../utils/validateUrl'
import { VideoPlayer } from '../../_shared/VideoPlayer'
+import type { MediaItem } from '../../../pages/types'
// import { handleFileUpload } from '../../../utils/handleFileUpload'
-type VideoItem = {
- url: string
- title: string
- body: string
-}
-
type Props = {
class?: string
- data: (value: VideoItem) => void
+ data: (value: MediaItem[]) => void
}
export const VideoUploader = (props: Props) => {
const { t } = useLocalize()
const [dragActive, setDragActive] = createSignal(false)
- const [dragError, setDragError] = createSignal
()
+ const [dragError, setDragError] = createSignal()
const [incorrectUrl, setIncorrectUrl] = createSignal(false)
- const [data, setData] = createSignal()
+ const [data, setData] = createSignal()
const updateData = (key, value) => {
setData((prev) => ({ ...prev, [key]: value }))
}
createEffect(() => {
- props.data(data())
+ props.data([data()])
})
const {
diff --git a/src/components/Editor/extensions/Article.ts b/src/components/Editor/extensions/Article.ts
index 15a91511..fc16dac0 100644
--- a/src/components/Editor/extensions/Article.ts
+++ b/src/components/Editor/extensions/Article.ts
@@ -48,6 +48,7 @@ export default Node.create({
return {
toggleArticle:
() =>
+ // eslint-disable-next-line unicorn/consistent-function-scoping
({ commands }) => {
return commands.toggleWrap('article')
},
diff --git a/src/components/Feed/ArticleCard.tsx b/src/components/Feed/ArticleCard.tsx
index 7ba125f3..b4b4e697 100644
--- a/src/components/Feed/ArticleCard.tsx
+++ b/src/components/Feed/ArticleCard.tsx
@@ -1,7 +1,6 @@
import { createMemo, createSignal, For, Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen'
import { capitalize, formatDate } from '../../utils'
-import { translit } from '../../utils/ru2en'
import { Icon } from '../_shared/Icon'
import styles from './ArticleCard.module.scss'
import { clsx } from 'clsx'
@@ -221,7 +220,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
@@ -233,7 +235,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
)}
@@ -244,7 +249,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
)}
@@ -263,7 +271,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
trigger={
}
/>
@@ -282,7 +293,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
trigger={
}
/>
diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx
index 04e76b3d..fb9268d0 100644
--- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx
+++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx
@@ -7,7 +7,7 @@ import type { AuthModalSearchParams } from './types'
import { ApiError } from '../../../utils/apiClient'
import { signSendLink } from '../../../stores/auth'
import { useLocalize } from '../../../context/localize'
-import { isValidEmail } from '../../../utils/validators'
+import { validateEmail } from '../../../utils/validateEmail'
type FormFields = {
email: string
@@ -38,7 +38,7 @@ export const ForgotPasswordForm = () => {
if (!email()) {
newValidationErrors.email = t('Please enter email')
- } else if (!isValidEmail(email())) {
+ } else if (!validateEmail(email())) {
newValidationErrors.email = t('Invalid email')
}
diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx
index f90c4dba..f62bb35f 100644
--- a/src/components/Nav/AuthModal/LoginForm.tsx
+++ b/src/components/Nav/AuthModal/LoginForm.tsx
@@ -9,7 +9,7 @@ import type { AuthModalSearchParams } from './types'
import { hideModal } from '../../../stores/ui'
import { useSession } from '../../../context/session'
import { signSendLink } from '../../../stores/auth'
-import { isValidEmail } from '../../../utils/validators'
+import { validateEmail } from '../../../utils/validateEmail'
import { generateModalTitleFromSource } from '../../../utils/custom-i18n'
import { useSnackbar } from '../../../context/snackbar'
@@ -76,7 +76,7 @@ export const LoginForm = () => {
if (!email()) {
newValidationErrors.email = t('Please enter email')
- } else if (!isValidEmail(email())) {
+ } else if (!validateEmail(email())) {
newValidationErrors.email = t('Invalid email')
}
diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx
index e82ae40f..91775e3f 100644
--- a/src/components/Nav/AuthModal/RegisterForm.tsx
+++ b/src/components/Nav/AuthModal/RegisterForm.tsx
@@ -11,7 +11,7 @@ import { hideModal } from '../../../stores/ui'
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
import { register } from '../../../stores/auth'
import { useLocalize } from '../../../context/localize'
-import { isValidEmail } from '../../../utils/validators'
+import { validateEmail } from '../../../utils/validateEmail'
import { generateModalTitleFromSource } from '../../../utils/custom-i18n'
type FormFields = {
@@ -40,7 +40,7 @@ export const RegisterForm = () => {
}
const handleEmailBlur = () => {
- if (isValidEmail(email())) {
+ if (validateEmail(email())) {
checkEmail(email())
}
}
@@ -93,7 +93,7 @@ export const RegisterForm = () => {
if (!cleanEmail) {
newValidationErrors.email = t('Please enter email')
- } else if (!isValidEmail(email())) {
+ } else if (!validateEmail(email())) {
newValidationErrors.email = t('Invalid email')
}
diff --git a/src/components/Nav/HeaderAuth.tsx b/src/components/Nav/HeaderAuth.tsx
index e1adb93c..d0db14da 100644
--- a/src/components/Nav/HeaderAuth.tsx
+++ b/src/components/Nav/HeaderAuth.tsx
@@ -195,7 +195,11 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
{/*FIXME: replace with route*/}
-
+
diff --git a/src/components/Views/Edit.tsx b/src/components/Views/Edit.tsx
index 7b0c7637..6a5bcb78 100644
--- a/src/components/Views/Edit.tsx
+++ b/src/components/Views/Edit.tsx
@@ -1,4 +1,4 @@
-import { createSignal, For, 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'
@@ -18,6 +18,7 @@ import { GrowingTextarea } from '../_shared/GrowingTextarea'
import { VideoUploader } from '../Editor/VideoUploader'
import { VideoPlayer } from '../_shared/VideoPlayer'
import { slugify } from '../../utils/slugify'
+import { SolidSwiper } from '../_shared/SolidSwiper'
type Props = {
shout: Shout
@@ -42,7 +43,7 @@ export const EditView = (props: Props) => {
const [isScrolled, setIsScrolled] = createSignal(false)
const [topics, setTopics] = createSignal
(null)
const [coverImage, setCoverImage] = createSignal(null)
- const [media, setMedia] = createSignal(props.shout.media)
+
const { page } = useRouter()
const {
form,
@@ -61,10 +62,14 @@ export const EditView = (props: Props) => {
mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC,
body: props.shout.body,
coverImageUrl: props.shout.cover,
- media: media(),
+ media: props.shout.media,
layout: props.shout.layout
})
+ const mediaItems = createMemo(() => {
+ return JSON.parse(form.media || '[]')
+ })
+
onMount(async () => {
const allTopics = await apiClient.getAllTopics()
setTopics(allTopics)
@@ -120,8 +125,23 @@ export const EditView = (props: Props) => {
setForm('selectedTopics', newSelectedTopics)
}
- const handleAddMedia = (data) => {
- setForm('media', JSON.stringify([data]))
+ const handleAddImages = (data) => {
+ const newImages = [...mediaItems(), ...data]
+ setForm('media', JSON.stringify(newImages))
+ }
+ const handleSortedImages = (data) => {
+ setForm('media', JSON.stringify(data))
+ }
+
+ const handleImageDelete = (index) => {
+ const copy = [...mediaItems()]
+ copy.splice(index, 1)
+ setForm('media', JSON.stringify(copy))
+ }
+
+ const handleImageChange = (index, value) => {
+ const updated = mediaItems().map((item, idx) => (idx === index ? value : item))
+ setForm('media', JSON.stringify(updated))
}
return (
@@ -167,25 +187,36 @@ export const EditView = (props: Props) => {
maxLength={100}
/>
+
+ handleImageDelete(index)}
+ onImagesAdd={(value) => handleAddImages(value)}
+ onImagesSorted={(value) => handleSortedImages(value)}
+ />
+
+
{
- handleAddMedia(data)
+ handleAddImages(data)
}}
/>
}
>
-
+
{(mediaItem) => (
<>
setMedia(null)}
+ deleteAction={() => setForm('media', null)}
/>
>
)}
diff --git a/src/components/Views/Feed.tsx b/src/components/Views/Feed.tsx
index e0ba8337..9bdde1a6 100644
--- a/src/components/Views/Feed.tsx
+++ b/src/components/Views/Feed.tsx
@@ -18,7 +18,6 @@ import styles from './Feed.module.scss'
import stylesTopic from '../Feed/CardTopic.module.scss'
import stylesBeside from '../../components/Feed/Beside.module.scss'
import { CommentDate } from '../Article/CommentDate'
-import {Beside} from "../Feed/Beside";
export const FEED_PAGE_SIZE = 20
diff --git a/src/components/Views/Home.tsx b/src/components/Views/Home.tsx
index 6d401d49..ac77037f 100644
--- a/src/components/Views/Home.tsx
+++ b/src/components/Views/Home.tsx
@@ -8,7 +8,7 @@ import { Row1 } from '../Feed/Row1'
import Hero from '../Discours/Hero'
import { Beside } from '../Feed/Beside'
import RowShort from '../Feed/RowShort'
-import Slider from '../_shared/Slider'
+import { Slider } from '../_shared/Slider'
import Group from '../Feed/Group'
import type { Shout, Topic } from '../../graphql/types.gen'
diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx
index aca0d116..ec1772e8 100644
--- a/src/components/Views/Topic.tsx
+++ b/src/components/Views/Topic.tsx
@@ -13,7 +13,7 @@ import { useAuthorsStore } from '../../stores/zine/authors'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages'
import { clsx } from 'clsx'
-import Slider from '../_shared/Slider'
+import { Slider } from '../_shared/Slider'
import { Row1 } from '../Feed/Row1'
import { ArticleCard } from '../Feed/ArticleCard'
import { useLocalize } from '../../context/localize'
diff --git a/src/components/_shared/DropArea/DropArea.module.scss b/src/components/_shared/DropArea/DropArea.module.scss
new file mode 100644
index 00000000..049537c3
--- /dev/null
+++ b/src/components/_shared/DropArea/DropArea.module.scss
@@ -0,0 +1,71 @@
+.DropArea {
+ .field {
+ 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;
+ cursor: pointer;
+ overflow: hidden;
+ position: relative;
+
+ .text {
+ position: relative;
+ z-index: 1;
+ }
+
+ &.active,
+ &:hover {
+ 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%
+ );
+ }
+ }
+ }
+
+ .description {
+ @include font-size(1.2rem);
+
+ margin-top: 1.6rem;
+ text-align: center;
+ color: var(--secondary-color);
+ }
+
+ .error {
+ @include font-size(1.4rem);
+
+ color: var(--danger-color);
+ margin-top: 1.6rem;
+ text-align: center;
+ padding: 1rem;
+ }
+}
+
+@keyframes slide {
+ 0% {
+ transform: translateX(-100%);
+ }
+
+ 100% {
+ transform: translateX(100%);
+ }
+}
diff --git a/src/components/_shared/DropArea/DropArea.tsx b/src/components/_shared/DropArea/DropArea.tsx
new file mode 100644
index 00000000..b827368e
--- /dev/null
+++ b/src/components/_shared/DropArea/DropArea.tsx
@@ -0,0 +1,103 @@
+import { clsx } from 'clsx'
+import styles from './DropArea.module.scss'
+import { createSignal, JSX, Show } from 'solid-js'
+import { createDropzone, createFileUploader } from '@solid-primitives/upload'
+import { useLocalize } from '../../../context/localize'
+import { validateFiles } from '../../../utils/validateFile'
+import type { FileTypeToUpload } from '../../../pages/types'
+import { handleFileUpload } from '../../../utils/handleFileUpload'
+
+type Props = {
+ class?: string
+ placeholder: string
+ description?: string | JSX.Element
+ fileType: FileTypeToUpload
+ isMultiply: boolean
+ onUpload: (value: string[]) => void
+}
+
+export const DropArea = (props: Props) => {
+ const { t } = useLocalize()
+ const [dragActive, setDragActive] = createSignal(false)
+ const [dropAreaError, setDropAreaError] = createSignal()
+ const [loading, setLoading] = createSignal(false)
+
+ const runUpload = async (files) => {
+ try {
+ setLoading(true)
+
+ const results: string[] = []
+ for (const file of files) {
+ const result = await handleFileUpload(file)
+ results.push(result)
+ }
+ props.onUpload(results)
+ setLoading(false)
+ } catch (error) {
+ setDropAreaError('Error')
+ console.error('[runUpload]', error)
+ }
+ }
+
+ const initUpload = async (selectedFiles) => {
+ if (!props.isMultiply && files.length > 1) {
+ setDropAreaError(t('Many files, choose only one'))
+ return
+ }
+ const isValid = validateFiles(props.fileType, selectedFiles)
+ if (isValid) {
+ await runUpload(selectedFiles)
+ } else {
+ setDropAreaError(t('Invalid file type'))
+ return false
+ }
+ }
+
+ const { files, selectFiles } = createFileUploader({
+ multiple: true,
+ accept: `${props.fileType}/*`
+ })
+
+ const { setRef: dropzoneRef, files: droppedFiles } = createDropzone({
+ onDrop: async () => {
+ setDragActive(false)
+ await initUpload(droppedFiles())
+ }
+ })
+ const handleDrag = (event) => {
+ if (event.type === 'dragenter' || event.type === 'dragover') {
+ setDragActive(true)
+ } else if (event.type === 'dragleave') {
+ setDragActive(false)
+ }
+ }
+ const handleDropFieldClick = async () => {
+ selectFiles((selectedFiles) => {
+ const filesArray = selectedFiles.map((file) => {
+ return file
+ })
+ initUpload(filesArray)
+ })
+ }
+
+ return (
+
+
+
{loading() ? 'Loading...' : props.placeholder}
+
+
+ {dropAreaError()}
+
+
+ {props.description}
+
+
+ )
+}
diff --git a/src/components/_shared/DropArea/index.ts b/src/components/_shared/DropArea/index.ts
new file mode 100644
index 00000000..e88fc9d3
--- /dev/null
+++ b/src/components/_shared/DropArea/index.ts
@@ -0,0 +1 @@
+export { DropArea } from './DropArea'
diff --git a/src/components/_shared/GrowingTextarea/GrowingTextarea.tsx b/src/components/_shared/GrowingTextarea/GrowingTextarea.tsx
index 434753a9..7644e042 100644
--- a/src/components/_shared/GrowingTextarea/GrowingTextarea.tsx
+++ b/src/components/_shared/GrowingTextarea/GrowingTextarea.tsx
@@ -11,11 +11,10 @@ type Props = {
}
export const GrowingTextarea = (props: Props) => {
- const [value, setValue] = createSignal('')
+ const [value, setValue] = createSignal(props.initialValue ?? '')
const [isFocused, setIsFocused] = createSignal(false)
const handleChangeValue = (event) => {
setValue(event.target.value)
- props.value(event.target.value)
}
const handleKeyDown = async (event) => {
@@ -39,6 +38,7 @@ export const GrowingTextarea = (props: Props) => {
value={props.initialValue}
onKeyDown={handleKeyDown}
onInput={(event) => handleChangeValue(event)}
+ onChange={(event) => props.value(event.target.value)}
placeholder={props.placeholder}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
diff --git a/src/components/_shared/Loading.module.scss b/src/components/_shared/Loading.module.scss
index de09f09b..7f28bbb2 100644
--- a/src/components/_shared/Loading.module.scss
+++ b/src/components/_shared/Loading.module.scss
@@ -24,4 +24,9 @@
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: linear;
+
+ .small & {
+ width: 32px;
+ height: 32px;
+ }
}
diff --git a/src/components/_shared/Loading.tsx b/src/components/_shared/Loading.tsx
index 252c0f52..44c764c5 100644
--- a/src/components/_shared/Loading.tsx
+++ b/src/components/_shared/Loading.tsx
@@ -1,8 +1,16 @@
import styles from './Loading.module.scss'
+import { clsx } from 'clsx'
-export const Loading = () => {
+type Props = {
+ size?: 'small'
+}
+export const Loading = (props: Props) => {
return (
-
+
)
diff --git a/src/components/_shared/Slider.scss b/src/components/_shared/Slider/Slider.scss
similarity index 85%
rename from src/components/_shared/Slider.scss
rename to src/components/_shared/Slider/Slider.scss
index 7438b45e..1aa7d959 100644
--- a/src/components/_shared/Slider.scss
+++ b/src/components/_shared/Slider/Slider.scss
@@ -211,3 +211,43 @@
padding: 1rem;
width: auto;
}
+
+.uploadPreview {
+ background: unset;
+ position: relative;
+ padding: 0 40px;
+
+ .sliders-container {
+ position: relative;
+ }
+
+ .swiper {
+ background: unset;
+
+ .swiper-wrapper {
+ min-height: 400px;
+ }
+ }
+
+ .slider-arrow-next,
+ .slider-arrow-prev {
+ background: none;
+ filter: invert(1);
+ width: 40px;
+ max-height: 540px;
+
+ .icon {
+ margin: auto;
+ width: 12px;
+ height: 20px;
+ }
+ }
+
+ //
+ //.slider-arrow-prev {
+ // margin-left: -40px;
+ //}
+ //.slider-arrow-next {
+ // margin-right: -40px;
+ //}
+}
diff --git a/src/components/_shared/Slider.tsx b/src/components/_shared/Slider/Slider.tsx
similarity index 65%
rename from src/components/_shared/Slider.tsx
rename to src/components/_shared/Slider/Slider.tsx
index a4db0a32..d5744504 100644
--- a/src/components/_shared/Slider.tsx
+++ b/src/components/_shared/Slider/Slider.tsx
@@ -1,26 +1,28 @@
-import { Swiper, Navigation, Pagination, Lazy, Thumbs } from 'swiper'
+//TODO: Replace with SolidSwiper.tsx
+
+import { Swiper, Navigation, Pagination, Thumbs } from 'swiper'
import type { SwiperOptions } from 'swiper'
import 'swiper/scss'
import 'swiper/scss/navigation'
import 'swiper/scss/pagination'
-import 'swiper/scss/lazy'
import 'swiper/scss/thumbs'
import './Slider.scss'
import { createEffect, createSignal, JSX, Show } from 'solid-js'
-import { Icon } from './Icon'
+import { Icon } from '../Icon'
import { clsx } from 'clsx'
-interface SliderProps {
+interface Props {
title?: string
slidesPerView?: number
isCardsWithCover?: boolean
children?: JSX.Element
- class?: string
isPageGallery?: boolean
hasThumbs?: boolean
+ variant?: 'uploadPreview'
+ slideIndex?: (value: number) => void
}
-export default (props: SliderProps) => {
+export const Slider = (props: Props) => {
let el: HTMLDivElement | undefined
let thumbsEl: HTMLDivElement | undefined
let pagEl: HTMLDivElement | undefined
@@ -31,15 +33,20 @@ export default (props: SliderProps) => {
const [swiper, setSwiper] = createSignal
()
const [swiperThumbs, setSwiperThumbs] = createSignal()
-
const opts: SwiperOptions = {
- lazy: true,
roundLengths: true,
loop: true,
centeredSlides: true,
slidesPerView: 1,
- modules: [Navigation, Pagination, Lazy, Thumbs],
+ modules: [Navigation, Pagination, Thumbs],
speed: 500,
+ on: {
+ slideChange: () => {
+ if (swiper()) {
+ props.slideIndex(swiper().realIndex || 0)
+ }
+ }
+ },
navigation: { nextEl, prevEl },
breakpoints: {
768: {
@@ -62,8 +69,7 @@ export default (props: SliderProps) => {
setSwiperThumbs(
new Swiper(thumbsEl, {
slidesPerView: 'auto',
- modules: [Lazy, Thumbs],
- lazy: true,
+ modules: [Thumbs],
roundLengths: true,
spaceBetween: 20,
freeMode: true,
@@ -98,14 +104,16 @@ export default (props: SliderProps) => {
})
return (
-
+
-
{props.title}
+
+ {props.title}
+
{
ref={el}
>
{props.children}
-
swiper()?.slideNext()}>
-
-
-
swiper()?.slidePrev()}>
-
-
-
+
+ swiper()?.slideNext()}>
+
+
+ swiper()?.slidePrev()}>
+
+
+
+ {/**/}
@@ -132,6 +142,14 @@ export default (props: SliderProps) => {
+
+ swiper()?.slideNext()}>
+
+
+ swiper()?.slidePrev()}>
+
+
+
)
}
diff --git a/src/components/_shared/Slider/index.ts b/src/components/_shared/Slider/index.ts
new file mode 100644
index 00000000..6d43da3c
--- /dev/null
+++ b/src/components/_shared/Slider/index.ts
@@ -0,0 +1 @@
+export { Slider } from './Slider'
diff --git a/src/components/_shared/SolidSwiper/SolidSwiper.tsx b/src/components/_shared/SolidSwiper/SolidSwiper.tsx
new file mode 100644
index 00000000..e6802a3a
--- /dev/null
+++ b/src/components/_shared/SolidSwiper/SolidSwiper.tsx
@@ -0,0 +1,352 @@
+import { createEffect, createSignal, For, Match, Show, Switch, on } from 'solid-js'
+import { MediaItem } from '../../../pages/types'
+import { Icon } from '../Icon'
+import { Popover } from '../Popover'
+import { useLocalize } from '../../../context/localize'
+import { register } from 'swiper/element/bundle'
+import { DropArea } from '../DropArea'
+import { GrowingTextarea } from '../GrowingTextarea'
+import MD from '../../Article/MD'
+import { createFileUploader } from '@solid-primitives/upload'
+import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
+import { SwiperRef } from './swiper'
+import { validateFiles } from '../../../utils/validateFile'
+import { handleFileUpload } from '../../../utils/handleFileUpload'
+import { useSnackbar } from '../../../context/snackbar'
+import { Loading } from '../Loading'
+import { imageProxy } from '../../../utils/imageProxy'
+import { clsx } from 'clsx'
+import styles from './Swiper.module.scss'
+
+type Props = {
+ images: MediaItem[]
+ editorMode?: boolean
+ onImagesAdd?: (value: MediaItem[]) => void
+ onImagesSorted?: (value: MediaItem[]) => void
+ onImageDelete?: (mediaItemIndex: number) => void
+ onImageChange?: (index: number, value: MediaItem) => void
+}
+
+const composeMediaItem = (value) => {
+ return value.map((url) => {
+ return {
+ url: url,
+ source: '',
+ title: '',
+ body: ''
+ }
+ })
+}
+
+register()
+
+SwiperCore.use([Pagination, Navigation, Manipulation])
+
+export const SolidSwiper = (props: Props) => {
+ const { t } = useLocalize()
+ const [loading, setLoading] = createSignal(false)
+ const [slideIndex, setSlideIndex] = createSignal(0)
+
+ const dropAreaRef: { current: HTMLElement } = { current: null }
+ const mainSwipeRef: { current: SwiperRef } = { current: null }
+ const thumbSwipeRef: { current: SwiperRef } = { current: null }
+
+ const {
+ actions: { showSnackbar }
+ } = useSnackbar()
+
+ const handleSlideDescriptionChange = (index: number, field: string, value) => {
+ props.onImageChange(index, { ...props.images[index], [field]: value })
+ }
+ const swipeToUploaded = () => {
+ setTimeout(() => {
+ mainSwipeRef.current.swiper.slideTo(props.images.length - 1)
+ }, 0)
+ }
+ const handleSlideChange = () => {
+ thumbSwipeRef.current.swiper.slideTo(mainSwipeRef.current.swiper.activeIndex)
+ setSlideIndex(mainSwipeRef.current.swiper.activeIndex)
+ }
+
+ createEffect(
+ on(
+ () => props.images.length,
+ () => {
+ mainSwipeRef.current?.swiper.update()
+ thumbSwipeRef.current?.swiper.update()
+ }
+ )
+ )
+
+ const handleDropAreaUpload = (value: string[]) => {
+ props.onImagesAdd(composeMediaItem(value))
+ swipeToUploaded()
+ }
+
+ const handleDelete = (index: number) => {
+ props.onImageDelete(index)
+
+ if (index === 0) {
+ mainSwipeRef.current.swiper.update()
+ } else {
+ mainSwipeRef.current.swiper.slideTo(index - 1)
+ }
+ }
+
+ const { selectFiles } = createFileUploader({
+ multiple: true,
+ accept: `image/*`
+ })
+
+ const initUpload = async (selectedFiles) => {
+ const isValid = validateFiles('image', selectedFiles)
+ if (isValid) {
+ try {
+ setLoading(true)
+ const results: string[] = []
+ for (const file of selectedFiles) {
+ const result = await handleFileUpload(file)
+ results.push(result)
+ }
+ props.onImagesAdd(composeMediaItem(results))
+ setLoading(false)
+ swipeToUploaded()
+ } catch (error) {
+ await showSnackbar({ type: 'error', body: t('Error') })
+ console.error('[runUpload]', error)
+ }
+ } else {
+ await showSnackbar({ type: 'error', body: t('Invalid file type') })
+ return false
+ }
+ }
+ const handleUploadThumb = async () => {
+ selectFiles((selectedFiles) => {
+ initUpload(selectedFiles)
+ })
+ }
+
+ const handleChangeIndex = (direction: 'left' | 'right', index: number) => {
+ const images = [...props.images]
+ if (direction === 'left' && index > 0) {
+ const copy = images.splice(index, 1)[0]
+ images.splice(index - 1, 0, copy)
+ } else if (direction === 'right' && index < images.length - 1) {
+ const copy = images.splice(index, 1)[0]
+ images.splice(index + 1, 0, copy)
+ }
+ props.onImagesSorted(images)
+ setTimeout(() => {
+ mainSwipeRef.current.swiper.slideTo(direction === 'left' ? index - 1 : index + 1)
+ }, 0)
+ }
+
+ return (
+
+
+
+ (dropAreaRef.current = el)}
+ fileType="image"
+ isMultiply={true}
+ placeholder={t('Add images')}
+ onUpload={handleDropAreaUpload}
+ description={
+
+ {t('You can upload up to 100 images in .jpg, .png format.')}
+
+ {t('Each image must be no larger than 5 MB.')}
+
+ }
+ />
+
+
0}>
+
+
(mainSwipeRef.current = el)}
+ slides-per-view={1}
+ thumbs-swiper={'.thumbSwiper'}
+ observer={true}
+ onSlideChange={handleSlideChange}
+ >
+
+ {(slide, index) => (
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+
+
+
})
+
+
+ {(triggerRef: (el) => void) => (
+ handleDelete(index())}
+ class={styles.action}
+ >
+
+
+ )}
+
+
+
+
+
+
+
+ handleSlideDescriptionChange(index(), 'title', event.target.value)
+ }
+ />
+
+ handleSlideDescriptionChange(index(), 'source', event.target.value)
+ }
+ />
+ handleSlideDescriptionChange(index(), 'body', value)}
+ />
+
+
+
+
+
+ {slide.title}
+
+
+ {slide.source}
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
mainSwipeRef.current.swiper.slidePrev()}
+ >
+
+
+
mainSwipeRef.current.swiper.slideNext()}
+ >
+
+
+
+ {slideIndex() + 1} / {props.images.length}
+
+
+
+
+
+
(thumbSwipeRef.current = el)}
+ slides-per-view={'auto'}
+ free-mode={true}
+ observer={true}
+ space-between={20}
+ auto-scroll-offset={1}
+ watch-overflow={true}
+ slide-to-clicked-slide={true}
+ watch-slides-visibility={true}
+ watch-slides-progress={true}
+ direction={props.editorMode ? 'horizontal' : 'vertical'}
+ slides-offset-after={props.editorMode && 140}
+ >
+
+ {(slide, index) => (
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+
+
+
+
+
handleDelete(index())}>
+
+
+
handleChangeIndex('left', index())}
+ >
+
+
+
handleChangeIndex('right', index())}
+ >
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
thumbSwipeRef.current.swiper.slidePrev()}
+ >
+
+
+
thumbSwipeRef.current.swiper.slideNext()}
+ >
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/_shared/SolidSwiper/Swiper.module.scss b/src/components/_shared/SolidSwiper/Swiper.module.scss
new file mode 100644
index 00000000..6b4d1534
--- /dev/null
+++ b/src/components/_shared/SolidSwiper/Swiper.module.scss
@@ -0,0 +1,323 @@
+$navigation-reserve: 32px;
+$slide-height: 500px;
+
+.Swiper {
+ display: block;
+ margin: 2rem 0;
+
+ &.articleMode {
+ background: var(--background-color-invert);
+ color: var(--default-color-invert);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .container {
+ margin: auto;
+ max-width: 800px;
+ position: relative;
+ padding: 24px 0;
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+
+ .holder {
+ width: 600px;
+ }
+
+ .thumbsHolder {
+ width: unset;
+ }
+
+ .thumbs {
+ padding: 52px 0;
+ width: 110px;
+ overflow: hidden;
+ height: $slide-height + 40px;
+ box-sizing: border-box;
+ margin: 0;
+ position: relative;
+
+ & > swiper-container {
+ position: absolute;
+ top: 52px;
+ bottom: 52px;
+ left: 0;
+ }
+
+ .thumbsNav {
+ height: 52px;
+ padding: 14px 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 110px;
+ left: 0;
+ right: 0;
+
+ .icon {
+ transform: rotate(45deg);
+ }
+
+ &.prev {
+ top: 0;
+ }
+
+ &.next {
+ top: unset;
+ bottom: 0;
+ }
+ }
+ }
+ }
+ }
+
+ &.editorMode {
+ color: #0d0d0d;
+ }
+
+ .action {
+ border-radius: 50%;
+ width: 32px;
+ height: 32px;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ background: rgba(#000, 0.3);
+ cursor: pointer;
+ display: none;
+
+ .icon {
+ width: 14px;
+ height: 14px;
+ }
+ }
+
+ .holder {
+ position: relative;
+ box-sizing: border-box;
+ padding: 0 $navigation-reserve;
+ overflow: hidden;
+
+ .counter {
+ @include font-size(1.2rem);
+
+ position: absolute;
+ z-index: 2;
+ top: 476px;
+ right: $navigation-reserve;
+ font-weight: 600;
+ padding: 0.2rem 0.8rem;
+ color: var(--background-color);
+ background-color: var(--default-color);
+ }
+
+ .image {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: $slide-height;
+ background: var(--placeholder-color-semi);
+ position: relative;
+
+ &:hover .action {
+ display: flex;
+ }
+
+ img {
+ max-height: 100%;
+ }
+ }
+ }
+
+ .navigation {
+ display: flex;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ justify-content: center;
+ align-items: center;
+ width: $navigation-reserve;
+ cursor: pointer;
+ height: $slide-height;
+
+ &.disabled {
+ opacity: 0.5;
+ cursor: inherit;
+ }
+
+ &.prev {
+ left: 0;
+ }
+
+ &.next {
+ right: 0;
+ }
+
+ .icon {
+ height: $navigation-reserve;
+ width: $navigation-reserve;
+ transition: 0.3s ease-in-out;
+ }
+
+ &:not(.disabled):hover .icon {
+ scale: 1.1;
+ }
+ }
+
+ &.articleMode .navigation {
+ filter: invert(1);
+ }
+
+ .slideDescription {
+ margin-top: 8px;
+ .articleTitle {
+ @include font-size(1.4rem);
+ }
+
+ .source {
+ @include font-size(1.2rem);
+
+ color: var(--secondary-color);
+ }
+ .body {
+ @include font-size(1.7rem);
+
+ margin-top: 24px;
+ }
+ }
+
+ .thumbs {
+ margin: 3rem 0;
+ max-height: 488px;
+ position: relative;
+
+ .navigation {
+ height: unset;
+
+ &.prev {
+ left: -$navigation-reserve;
+ }
+
+ &.next {
+ right: -$navigation-reserve;
+ }
+ }
+
+ .upload {
+ border: 1px solid #ccced3;
+ box-sizing: border-box;
+ cursor: pointer;
+
+ .inner {
+ position: relative;
+ z-index: 1000;
+ width: 110px;
+ height: 75px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+
+ .imageThumb {
+ width: 110px;
+ height: 75px;
+ background-size: cover;
+ background-position: 50% 50%;
+ background-color: var(--placeholder-color-semi);
+ opacity: 0.5;
+ filter: grayscale(1);
+ transition: filter 0.3s ease-in-out, opacity 0.5s ease-in-out;
+
+ .thumbAction {
+ display: none;
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ flex-direction: column;
+ gap: 5px;
+
+ .action {
+ position: static;
+ display: flex;
+ width: 18px;
+ height: 18px;
+
+ &.hidden {
+ display: none;
+ }
+
+ .icon {
+ width: 8px;
+ height: 8px;
+ }
+ }
+ }
+
+ &:hover {
+ opacity: 1;
+ cursor: pointer;
+ filter: unset;
+
+ .thumbAction {
+ display: flex;
+ }
+ }
+ }
+ }
+
+ .addSlides {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 2rem auto;
+ }
+
+ .description {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5em;
+ margin: 1em 0;
+
+ .descriptionText {
+ @include font-size(1.4rem);
+
+ line-height: 1.1;
+ }
+
+ .input {
+ @include font-size(1.4rem);
+
+ padding: 0;
+ margin: 0;
+ border: none;
+ height: 1.2em;
+
+ &:focus {
+ outline: none;
+ }
+
+ &::placeholder {
+ color: var(--placeholder-color);
+ }
+
+ &.title {
+ font-weight: 500;
+ }
+ }
+ }
+}
+
+:global(.swiper-slide-thumb-active) {
+ .imageThumb {
+ opacity: 1 !important;
+ filter: unset !important;
+
+ .thumbAction {
+ display: flex !important;
+ }
+ }
+}
diff --git a/src/components/_shared/SolidSwiper/index.ts b/src/components/_shared/SolidSwiper/index.ts
new file mode 100644
index 00000000..fbd847e0
--- /dev/null
+++ b/src/components/_shared/SolidSwiper/index.ts
@@ -0,0 +1 @@
+export { SolidSwiper } from './SolidSwiper'
diff --git a/src/components/_shared/SolidSwiper/swiper.d.ts b/src/components/_shared/SolidSwiper/swiper.d.ts
new file mode 100644
index 00000000..e7b06700
--- /dev/null
+++ b/src/components/_shared/SolidSwiper/swiper.d.ts
@@ -0,0 +1,45 @@
+import 'solid-js'
+import { SwiperOptions } from 'swiper'
+import { SwiperSlideProps } from 'swiper/react'
+
+type Kebab
= T extends `${infer F}${infer R}`
+ ? Kebab ? '' : '-'}${Lowercase}`>
+ : A
+
+/**
+ * Helper for converting object keys to kebab case because Swiper web components parameters are available as kebab-case attributes.
+ * @link https://swiperjs.com/element#parameters-as-attributes
+ */
+type KebabObjectKeys = {
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ [key in keyof T as Kebab]: T[key] extends Object ? KebabObjectKeys : T[key]
+}
+
+/**
+ * Swiper 9 doesn't support Typescript yet, we are watching the following issue:
+ * @link https://github.com/nolimits4web/swiper/issues/6466
+ *
+ * All parameters can be found on the following page:
+ * @link https://swiperjs.com/swiper-api#parameters
+ */
+type SwiperRef = HTMLElement & { swiper: Swiper; initialize: () => void }
+
+declare module 'solid-js' {
+ namespace JSX {
+ interface IntrinsicElements {
+ 'swiper-container': SwiperContainerAttributes
+ 'swiper-slide': SwiperSlideAttributes
+ }
+
+ interface SwiperContainerAttributes extends KebabObjectKeys {
+ ref?: RefObject
+ children?: JSX.Element
+ onSlideChange?: () => void
+ class?: string
+ }
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
+ interface SwiperSlideAttributes extends KebabObjectKeys {
+ style?: unknown
+ }
+ }
+}
diff --git a/src/pages/create.page.tsx b/src/pages/create.page.tsx
index f48d0109..c6af1819 100644
--- a/src/pages/create.page.tsx
+++ b/src/pages/create.page.tsx
@@ -7,8 +7,9 @@ import styles from '../styles/Create.module.scss'
import { apiClient } from '../utils/apiClient'
import { redirectPage } from '@nanostores/router'
import { router } from '../stores/router'
+import { LayoutType } from './types'
-const handleCreate = async (layout: 'article' | 'video') => {
+const handleCreate = async (layout: LayoutType) => {
const shout = await apiClient.createArticle({ article: { layout: layout } })
redirectPage(router, 'edit', {
shoutId: shout.id.toString()
@@ -35,10 +36,10 @@ export const CreatePage = () => {
-
+ handleCreate('image')}>
{t('images')}
-
+
diff --git a/src/pages/layoutShouts.page.tsx b/src/pages/layoutShouts.page.tsx
index 581d1921..d7671f5e 100644
--- a/src/pages/layoutShouts.page.tsx
+++ b/src/pages/layoutShouts.page.tsx
@@ -12,7 +12,7 @@ import { clsx } from 'clsx'
import { Row3 } from '../components/Feed/Row3'
import { Row2 } from '../components/Feed/Row2'
import { Beside } from '../components/Feed/Beside'
-import Slider from '../components/_shared/Slider'
+import { Slider } from '../components/_shared/Slider'
import { Row1 } from '../components/Feed/Row1'
import styles from '../styles/Topic.module.scss'
import { ArticleCard } from '../components/Feed/ArticleCard'
diff --git a/src/pages/types.ts b/src/pages/types.ts
index 3297c9c0..e76de3c1 100644
--- a/src/pages/types.ts
+++ b/src/pages/types.ts
@@ -14,7 +14,7 @@ export type PageProps = {
topic?: Topic
allTopics?: Topic[]
searchQuery?: string
- layout?: string // LayoutType
+ layout?: LayoutType
// other types?
searchResults?: Shout[]
chats?: Chat[]
@@ -33,3 +33,12 @@ export type UploadFile = {
}
export type LayoutType = 'article' | 'audio' | 'video' | 'image' | 'literature'
+
+export type FileTypeToUpload = 'image' | 'video' | 'doc'
+
+export type MediaItem = {
+ url: string
+ title: string
+ body: string
+ source?: string
+}
diff --git a/src/stores/zine/layouts.ts b/src/stores/zine/layouts.ts
new file mode 100644
index 00000000..b96ea96c
--- /dev/null
+++ b/src/stores/zine/layouts.ts
@@ -0,0 +1,48 @@
+import type { Shout, LoadShoutsOptions } from '../../graphql/types.gen'
+import { apiClient } from '../../utils/apiClient'
+import { createSignal } from 'solid-js'
+
+export type LayoutType = 'article' | 'audio' | 'video' | 'image' | 'literature'
+
+const [sortedLayoutShouts, setSortedLayoutShouts] = createSignal