merged
This commit is contained in:
commit
328acd9ce8
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -13,6 +13,7 @@
|
||||||
"i18next": "22.4.15",
|
"i18next": "22.4.15",
|
||||||
"i18next-icu": "2.3.0",
|
"i18next-icu": "2.3.0",
|
||||||
"intl-messageformat": "10.5.3",
|
"intl-messageformat": "10.5.3",
|
||||||
|
"just-throttle": "4.2.0",
|
||||||
"mailgun.js": "8.2.1",
|
"mailgun.js": "8.2.1",
|
||||||
"node-fetch": "3.3.1"
|
"node-fetch": "3.3.1"
|
||||||
},
|
},
|
||||||
|
@ -13168,6 +13169,11 @@
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/just-throttle": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/just-throttle/-/just-throttle-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-/iAZv1953JcExpvsywaPKjSzfTiCLqeguUTE6+VmK15mOcwxBx7/FHrVvS4WEErMR03TRazH8kcBSHqMagYIYg=="
|
||||||
|
},
|
||||||
"node_modules/kebab-case": {
|
"node_modules/kebab-case": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
|
||||||
|
@ -28106,6 +28112,11 @@
|
||||||
"object.values": "^1.1.6"
|
"object.values": "^1.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"just-throttle": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/just-throttle/-/just-throttle-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-/iAZv1953JcExpvsywaPKjSzfTiCLqeguUTE6+VmK15mOcwxBx7/FHrVvS4WEErMR03TRazH8kcBSHqMagYIYg=="
|
||||||
|
},
|
||||||
"kebab-case": {
|
"kebab-case": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"i18next": "22.4.15",
|
"i18next": "22.4.15",
|
||||||
"i18next-icu": "2.3.0",
|
"i18next-icu": "2.3.0",
|
||||||
"intl-messageformat": "10.5.3",
|
"intl-messageformat": "10.5.3",
|
||||||
|
"just-throttle": "4.2.0",
|
||||||
"mailgun.js": "8.2.1",
|
"mailgun.js": "8.2.1",
|
||||||
"node-fetch": "3.3.1"
|
"node-fetch": "3.3.1"
|
||||||
},
|
},
|
||||||
|
|
4
public/icons/key.svg
Normal file
4
public/icons/key.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="12" height="20" viewBox="0 0 12 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M6 0.25C2.87109 0.25 0.375 2.76758 0.375 5.875C0.375 8.0332 1.5918 9.9043 3.375 10.8477V17.3809C3.375 17.791 3.5625 18.1777 3.88477 18.4336L5.24219 19.5059C5.44727 19.6699 5.69531 19.75 5.94141 19.75C6.2168 19.75 6.49219 19.6484 6.70898 19.4492L9.15625 17.1797C9.33789 17.0117 9.39062 16.7461 9.28906 16.5215L8.625 15.0586V15.0234L9.52734 13.9043C9.79688 13.5684 9.8125 13.0918 9.56055 12.7402L8.625 11.4316V10.8477C10.4082 9.9043 11.625 8.0332 11.625 5.875C11.625 2.76758 9.12891 0.25 6 0.25ZM6 1.75C8.27344 1.75 10.125 3.60156 10.125 5.875C10.125 7.40625 9.28125 8.80469 7.92383 9.52148L7.125 9.94336V11.9121L8.10352 13.2812L7.125 14.4941V15.3828L7.64648 16.5332L5.92383 18.1328L4.97852 17.3848C4.91211 17.334 4.875 17.2559 4.875 17.1719V9.94336L4.07617 9.52148C2.71875 8.80469 1.875 7.40625 1.875 5.875C1.875 3.60156 3.72656 1.75 6 1.75ZM6 3.25C5.17188 3.25 4.5 3.92188 4.5 4.75C4.5 5.57812 5.17188 6.25 6 6.25C6.82812 6.25 7.5 5.57812 7.5 4.75C7.5 3.92188 6.82812 3.25 6 3.25Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -28,6 +28,8 @@
|
||||||
"All posts": "All posts",
|
"All posts": "All posts",
|
||||||
"All topics": "All topics",
|
"All topics": "All topics",
|
||||||
"Almost done! Check your email.": "Almost done! Just checking your email.",
|
"Almost done! Check your email.": "Almost done! Just checking your email.",
|
||||||
|
"Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?",
|
||||||
|
"Are you sure you want to delete this draft?": "Are you sure you want to delete this draft?",
|
||||||
"Are you sure you want to to proceed the action?": "Are you sure you want to to proceed the action?",
|
"Are you sure you want to to proceed the action?": "Are you sure you want to to proceed the action?",
|
||||||
"Art": "Art",
|
"Art": "Art",
|
||||||
"Artist": "Artist",
|
"Artist": "Artist",
|
||||||
|
@ -100,6 +102,7 @@
|
||||||
"Discussion rules": "Discussion rules",
|
"Discussion rules": "Discussion rules",
|
||||||
"Discussions": "Discussions",
|
"Discussions": "Discussions",
|
||||||
"Dogma": "Dogma",
|
"Dogma": "Dogma",
|
||||||
|
"Draft successfully deleted": "Draft successfully deleted",
|
||||||
"Drafts": "Drafts",
|
"Drafts": "Drafts",
|
||||||
"Drag the image to this area": "Drag the image to this area",
|
"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.",
|
"Each image must be no larger than 5 MB.": "Each image must be no larger than 5 MB.",
|
||||||
|
@ -193,6 +196,7 @@
|
||||||
"Manifesto": "Manifesto",
|
"Manifesto": "Manifesto",
|
||||||
"Many files, choose only one": "Many files, choose only one",
|
"Many files, choose only one": "Many files, choose only one",
|
||||||
"Material card": "Material card",
|
"Material card": "Material card",
|
||||||
|
"Message": "Message",
|
||||||
"More": "More",
|
"More": "More",
|
||||||
"Most commented": "Commented",
|
"Most commented": "Commented",
|
||||||
"Most read": "Readable",
|
"Most read": "Readable",
|
||||||
|
@ -206,12 +210,19 @@
|
||||||
"New only": "New only",
|
"New only": "New only",
|
||||||
"New password": "New password",
|
"New password": "New password",
|
||||||
"New stories every day and even more!": "New stories and more are waiting for you every day!",
|
"New stories every day and even more!": "New stories and more are waiting for you every day!",
|
||||||
"NewCommentNotificationText": "{commentsCount, plural, one {New comment} other {{commentsCount} comments}} to your publication {shoutTitle} from {lastCommenterName}{restUsersCount, plural, =0 {} one { one more user} other { and more {restUsersCount} users}}",
|
|
||||||
"NewReplyNotificationText": "{commentsCount, plural, one {New reply} other {{commentsCount} replays} other {{commentsCount} новых ответов}} to your publication {shoutTitle} от {lastCommenterName}{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}",
|
"NotificationNewCommentText1": "{commentsCount, plural, one {New comment} other {{commentsCount} comments}} to your publication",
|
||||||
|
"NotificationNewCommentText2": "from",
|
||||||
|
"NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { one more user} other { and more {restUsersCount} users}}",
|
||||||
|
|
||||||
|
"NotificationNewReplyText1": "{commentsCount, plural, one {New reply} other {{commentsCount} replays}} to your publication",
|
||||||
|
"NotificationNewReplyText2": "from",
|
||||||
|
"NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}",
|
||||||
|
|
||||||
"Newsletter": "Newsletter",
|
"Newsletter": "Newsletter",
|
||||||
"Night mode": "Night mode",
|
"Night mode": "Night mode",
|
||||||
"No notifications, yet": "No notifications, yet",
|
"No notifications yet": "No notifications yet",
|
||||||
"No such account, please try to register": "No such account found, please try to register",
|
"Write good articles, comment\nand it won't be so empty here": "Write good articles, comment\nand it won't be so empty here",
|
||||||
"Nothing here yet": "There's nothing here yet",
|
"Nothing here yet": "There's nothing here yet",
|
||||||
"Nothing is here": "There is nothing here",
|
"Nothing is here": "There is nothing here",
|
||||||
"Notifications": "Notifications",
|
"Notifications": "Notifications",
|
||||||
|
@ -353,7 +364,6 @@
|
||||||
"Where": "From",
|
"Where": "From",
|
||||||
"Words": "Слов",
|
"Words": "Слов",
|
||||||
"Work with us": "Cooperate with Discourse",
|
"Work with us": "Cooperate with Discourse",
|
||||||
"Message": "Message",
|
|
||||||
"Write a comment...": "Write a comment...",
|
"Write a comment...": "Write a comment...",
|
||||||
"Write a short introduction": "Write a short introduction",
|
"Write a short introduction": "Write a short introduction",
|
||||||
"Write about the topic": "Write about the topic",
|
"Write about the topic": "Write about the topic",
|
||||||
|
|
|
@ -31,6 +31,8 @@
|
||||||
"All posts": "Все публикации",
|
"All posts": "Все публикации",
|
||||||
"All topics": "Все темы",
|
"All topics": "Все темы",
|
||||||
"Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.",
|
"Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.",
|
||||||
|
"Are you sure you want to delete this comment?": "Уверены, что хотите удалить этот комментарий?",
|
||||||
|
"Are you sure you want to delete this draft?": "Уверены, что хотите удалить этот черновик?",
|
||||||
"Are you sure you want to to proceed the action?": "Вы уверены, что хотите продолжить?",
|
"Are you sure you want to to proceed the action?": "Вы уверены, что хотите продолжить?",
|
||||||
"Art": "Искусство",
|
"Art": "Искусство",
|
||||||
"Artist": "Исполнитель",
|
"Artist": "Исполнитель",
|
||||||
|
@ -104,6 +106,7 @@
|
||||||
"Discussion rules": "Правила сообществ самиздата в соцсетях",
|
"Discussion rules": "Правила сообществ самиздата в соцсетях",
|
||||||
"Discussions": "Дискуссии",
|
"Discussions": "Дискуссии",
|
||||||
"Dogma": "Догма",
|
"Dogma": "Догма",
|
||||||
|
"Draft successfully deleted": "Черновик успешно удален",
|
||||||
"Drafts": "Черновики",
|
"Drafts": "Черновики",
|
||||||
"Drag the image to this area": "Перетащите изображение в эту область",
|
"Drag the image to this area": "Перетащите изображение в эту область",
|
||||||
"Each image must be no larger than 5 MB.": "Каждое изображение должно быть размером не больше 5 мб.",
|
"Each image must be no larger than 5 MB.": "Каждое изображение должно быть размером не больше 5 мб.",
|
||||||
|
@ -217,11 +220,19 @@
|
||||||
"New only": "Только новые",
|
"New only": "Только новые",
|
||||||
"New password": "Новый пароль",
|
"New password": "Новый пароль",
|
||||||
"New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!",
|
"New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!",
|
||||||
"NewCommentNotificationText": "{commentsCount, plural, one {Новый комментарий} few {{commentsCount} новых комментария} other {{commentsCount} новых комментариев}} к вашей публикации {shoutTitle} от {lastCommenterName}{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
|
|
||||||
"NewReplyNotificationText": "{commentsCount, plural, one {Новый ответ} few {{commentsCount} новых ответа} other {{commentsCount} новых ответов}} к вашему комментарию к публикации {shoutTitle} от {lastCommenterName}{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
|
"NotificationNewCommentText1": "{commentsCount, plural, one {Новый комментарий} few {{commentsCount} новых комментария} other {{commentsCount} новых комментариев}} к вашей публикации",
|
||||||
|
"NotificationNewCommentText2": "от",
|
||||||
|
"NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
|
||||||
|
|
||||||
|
"NotificationNewReplyText1": "{commentsCount, plural, one {Новый ответ} few {{commentsCount} новых ответа} other {{commentsCount} новых ответов}} на ваш комментарий к публикации",
|
||||||
|
"NotificationNewReplyText2": "от",
|
||||||
|
"NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
|
||||||
|
|
||||||
"Newsletter": "Рассылка",
|
"Newsletter": "Рассылка",
|
||||||
"Night mode": "Ночная тема",
|
"Night mode": "Ночная тема",
|
||||||
"No notifications, yet": "Тут пока пусто",
|
"No notifications yet": "Уведомлений пока нет",
|
||||||
|
"Write good articles, comment\nand it won't be so empty here": "Пишите хорошие статьи, комментируйте,\nи здесь станет не так пусто",
|
||||||
"No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться",
|
"No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться",
|
||||||
"Nothing here yet": "Здесь пока ничего нет",
|
"Nothing here yet": "Здесь пока ничего нет",
|
||||||
"Nothing is here": "Здесь ничего нет",
|
"Nothing is here": "Здесь ничего нет",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.comment {
|
.comment {
|
||||||
margin: 0.5em 0;
|
margin: 0 0 0.5em;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -62,7 +62,12 @@ export const Comment = (props: Props) => {
|
||||||
const remove = async () => {
|
const remove = async () => {
|
||||||
if (comment()?.id) {
|
if (comment()?.id) {
|
||||||
try {
|
try {
|
||||||
const isConfirmed = await showConfirm()
|
const isConfirmed = await showConfirm({
|
||||||
|
confirmBody: t('Are you sure you want to delete this comment?'),
|
||||||
|
confirmButtonLabel: t('Delete'),
|
||||||
|
confirmButtonVariant: 'danger',
|
||||||
|
declineButtonVariant: 'primary'
|
||||||
|
})
|
||||||
|
|
||||||
if (isConfirmed) {
|
if (isConfirmed) {
|
||||||
await deleteReaction(comment().id)
|
await deleteReaction(comment().id)
|
||||||
|
@ -136,7 +141,7 @@ export const Comment = (props: Props) => {
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<small>
|
<small>
|
||||||
<a href={`#comment-${comment()?.id}`}>{comment()?.shout.title || ''}</a>
|
<a href={`#comment_${comment()?.id}`}>{comment()?.shout.title || ''}</a>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -174,7 +179,7 @@ export const Comment = (props: Props) => {
|
||||||
<CommentRatingControl comment={comment()} />
|
<CommentRatingControl comment={comment()} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={styles.commentBody} id={'comment-' + (comment().id || '')}>
|
<div class={styles.commentBody}>
|
||||||
<Show when={editMode()} fallback={<MD body={body()} />}>
|
<Show when={editMode()} fallback={<MD body={body()} />}>
|
||||||
<Suspense fallback={<p>{t('Loading')}</p>}>
|
<Suspense fallback={<p>{t('Loading')}</p>}>
|
||||||
<SimplifiedEditor
|
<SimplifiedEditor
|
||||||
|
|
|
@ -4,14 +4,15 @@
|
||||||
align-self: center;
|
align-self: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
gap: 1rem;
|
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
margin: 0 1em 0 0;
|
margin: 0 1em 0 0;
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
margin-right: 1rem;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Show, createMemo, createSignal, onMount, For, createEffect } from 'solid-js'
|
import { Show, createMemo, createSignal, onMount, For } from 'solid-js'
|
||||||
import { Comment } from './Comment'
|
import { Comment } from './Comment'
|
||||||
import styles from './Article.module.scss'
|
import styles from './Article.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
@ -181,7 +181,7 @@ export const CommentsTree = (props: Props) => {
|
||||||
<SimplifiedEditor
|
<SimplifiedEditor
|
||||||
quoteEnabled={true}
|
quoteEnabled={true}
|
||||||
imageEnabled={true}
|
imageEnabled={true}
|
||||||
autoFocus={true}
|
autoFocus={false}
|
||||||
submitByCtrlEnter={true}
|
submitByCtrlEnter={true}
|
||||||
placeholder={t('Write a comment...')}
|
placeholder={t('Write a comment...')}
|
||||||
onSubmit={(value) => handleSubmitComment(value)}
|
onSubmit={(value) => handleSubmitComment(value)}
|
||||||
|
|
|
@ -28,11 +28,26 @@ import styles from './Article.module.scss'
|
||||||
import { CardTopic } from '../Feed/CardTopic'
|
import { CardTopic } from '../Feed/CardTopic'
|
||||||
import { createPopper } from '@popperjs/core'
|
import { createPopper } from '@popperjs/core'
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
article: Shout
|
article: Shout
|
||||||
scrollToComments?: boolean
|
scrollToComments?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ArticlePageSearchParams = {
|
||||||
|
scrollTo: 'comments'
|
||||||
|
commentId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollTo = (el: HTMLElement) => {
|
||||||
|
const { top } = el.getBoundingClientRect()
|
||||||
|
|
||||||
|
window.scrollTo({
|
||||||
|
top: top + window.scrollY - 96,
|
||||||
|
left: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const FullArticle = (props: Props) => {
|
export const FullArticle = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const {
|
const {
|
||||||
|
@ -77,16 +92,15 @@ export const FullArticle = (props: Props) => {
|
||||||
return JSON.parse(props.article.media || '[]')
|
return JSON.parse(props.article.media || '[]')
|
||||||
})
|
})
|
||||||
|
|
||||||
const commentsRef: { current: HTMLDivElement } = { current: null }
|
const commentsRef: {
|
||||||
|
current: HTMLDivElement
|
||||||
|
} = { current: null }
|
||||||
|
|
||||||
const scrollToComments = () => {
|
const scrollToComments = () => {
|
||||||
window.scrollTo({
|
scrollTo(commentsRef.current)
|
||||||
top: commentsRef.current.offsetTop - 96,
|
|
||||||
left: 0,
|
|
||||||
behavior: 'smooth'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { searchParams, changeSearchParam } = useRouter()
|
const { searchParams, changeSearchParam } = useRouter<ArticlePageSearchParams>()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.scrollToComments) {
|
if (props.scrollToComments) {
|
||||||
|
@ -105,9 +119,14 @@ export const FullArticle = (props: Props) => {
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (searchParams().commentId && isReactionsLoaded()) {
|
if (searchParams().commentId && isReactionsLoaded()) {
|
||||||
const commentElement = document.querySelector(`[id='comment_${searchParams().commentId}']`)
|
const commentElement = document.querySelector<HTMLElement>(
|
||||||
|
`[id='comment_${searchParams().commentId}']`
|
||||||
|
)
|
||||||
|
|
||||||
|
changeSearchParam({ commentId: null })
|
||||||
|
|
||||||
if (commentElement) {
|
if (commentElement) {
|
||||||
commentElement.scrollIntoView({ behavior: 'smooth' })
|
scrollTo(commentElement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
@include media-breakpoint-up(sm) {
|
@include media-breakpoint-up(sm) {
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,6 +426,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shareControl {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.buttonSubscribe {
|
.buttonSubscribe {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
|
@ -447,8 +451,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonWrite {
|
.buttonWrite {
|
||||||
|
background: #ccc;
|
||||||
color: #000;
|
color: #000;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
font-weight: 500;
|
||||||
transition:
|
transition:
|
||||||
background-color 0.3s,
|
background-color 0.3s,
|
||||||
color 0.3s;
|
color 0.3s;
|
||||||
|
@ -490,6 +496,7 @@
|
||||||
color: #696969;
|
color: #696969;
|
||||||
@include font-size(2rem);
|
@include font-size(2rem);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorSubscribe {
|
.authorSubscribe {
|
||||||
|
|
|
@ -208,76 +208,57 @@ export const AuthorCard = (props: Props) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class={styles.subscribersContainer}>
|
<div class={styles.subscribersContainer}>
|
||||||
<Switch>
|
<Show when={props.followers && props.followers.length > 0}>
|
||||||
<Match when={props.followers && props.followers.length > 0 && !props.isCurrentUser}>
|
<a href="?modal=followers" class={styles.subscribers}>
|
||||||
<a href="?modal=followers" class={styles.subscribers}>
|
<For each={props.followers.slice(0, 3)}>
|
||||||
<For each={props.followers.slice(0, 3)}>
|
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
||||||
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
</For>
|
||||||
</For>
|
<div class={styles.subscribersCounter}>
|
||||||
<div class={styles.subscribersCounter}>
|
{t('SubscriberWithCount', { count: props.followers.length })}
|
||||||
{t('SubscriberWithCount', { count: props.followers.length })}
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</a>
|
</Show>
|
||||||
</Match>
|
|
||||||
<Match when={props.followers && props.followers.length > 0 && props.isCurrentUser}>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => redirectPage(router, 'profileSettings')}
|
|
||||||
value={t('Edit profile')}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
|
|
||||||
<Switch>
|
<Show when={props.following && props.following.length > 0}>
|
||||||
<Match when={!props.isCurrentUser && props.following && props.following.length > 0}>
|
<a href="?modal=following" class={styles.subscribers}>
|
||||||
<a href="?modal=following" class={styles.subscribers}>
|
<For each={props.following.slice(0, 3)}>
|
||||||
<For each={props.following.slice(0, 3)}>
|
{(f) => {
|
||||||
{(f) => {
|
if ('name' in f) {
|
||||||
if ('name' in f) {
|
return <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />
|
||||||
return <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />
|
} else if ('title' in f) {
|
||||||
} else if ('title' in f) {
|
return <Userpic name={f.title} userpic={f.pic} class={styles.userpic} />
|
||||||
return <Userpic name={f.title} userpic={f.pic} class={styles.userpic} />
|
}
|
||||||
}
|
return null
|
||||||
return null
|
}}
|
||||||
}}
|
</For>
|
||||||
</For>
|
<div class={styles.subscribersCounter}>
|
||||||
<div class={styles.subscribersCounter}>
|
{t('SubscriptionWithCount', { count: props?.following.length ?? 0 })}
|
||||||
{t('SubscriptionWithCount', { count: props?.following.length ?? 0 })}
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</a>
|
</Show>
|
||||||
</Match>
|
|
||||||
<Match when={props.isCurrentUser && props.following && props.following.length > 0}>
|
|
||||||
<SharePopup
|
|
||||||
containerCssClass={stylesHeader.control}
|
|
||||||
title={props.author.name}
|
|
||||||
description={props.author.bio}
|
|
||||||
imageUrl={props.author.userpic}
|
|
||||||
shareUrl={getShareUrl({ pathname: `/author/${props.author.slug}` })}
|
|
||||||
trigger={<Button variant="secondary" value={t('Share')} />}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<ShowOnlyOnClient>
|
<ShowOnlyOnClient>
|
||||||
<Show when={isSessionLoaded() && props.author.links}>
|
<Show when={isSessionLoaded()}>
|
||||||
<div class={styles.authorSubscribeSocial}>
|
<Show when={props.author.links && props.author.links.length > 0}>
|
||||||
<For each={props.author.links}>
|
<div class={styles.authorSubscribeSocial}>
|
||||||
{(link) => (
|
<For each={props.author.links}>
|
||||||
<a
|
{(link) => (
|
||||||
class={styles.socialLink}
|
<a
|
||||||
href={link.startsWith('http') ? link : `https://${link}`}
|
class={styles.socialLink}
|
||||||
target="_blank"
|
href={link.startsWith('http') ? link : `https://${link}`}
|
||||||
rel="nofollow noopener"
|
target="_blank"
|
||||||
>
|
rel="nofollow noopener"
|
||||||
<span class={styles.authorSubscribeSocialLabel}>
|
>
|
||||||
{link.startsWith('http') ? link : `https://${link}`}
|
<span class={styles.authorSubscribeSocialLabel}>
|
||||||
</span>
|
{link.startsWith('http') ? link : `https://${link}`}
|
||||||
</a>
|
</span>
|
||||||
)}
|
</a>
|
||||||
</For>
|
)}
|
||||||
</div>
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
<Show when={canFollow()}>
|
<Show when={canFollow()}>
|
||||||
<div class={styles.authorSubscribe}>
|
<div class={styles.authorSubscribe}>
|
||||||
<Show
|
<Show
|
||||||
|
@ -346,9 +327,9 @@ export const AuthorCard = (props: Props) => {
|
||||||
|
|
||||||
<Show when={!props.hideWriteButton}>
|
<Show when={!props.hideWriteButton}>
|
||||||
<button
|
<button
|
||||||
class={clsx(styles.button, styles.buttonSubscribe)}
|
class={styles.button}
|
||||||
classList={{
|
classList={{
|
||||||
'button--subscribe': !props.isAuthorsList,
|
'button--light': !props.isAuthorsList,
|
||||||
'button--subscribe-topic': props.isAuthorsList,
|
'button--subscribe-topic': props.isAuthorsList,
|
||||||
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
||||||
}}
|
}}
|
||||||
|
@ -362,6 +343,26 @@ export const AuthorCard = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={props.isCurrentUser}>
|
||||||
|
<div class={styles.authorSubscribe}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => redirectPage(router, 'profileSettings')}
|
||||||
|
value={t('Edit profile')}
|
||||||
|
class={styles.button}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SharePopup
|
||||||
|
containerCssClass={styles.shareControl}
|
||||||
|
title={props.author.name}
|
||||||
|
description={props.author.bio}
|
||||||
|
imageUrl={props.author.userpic}
|
||||||
|
shareUrl={getShareUrl({ pathname: `/author/${props.author.slug}` })}
|
||||||
|
trigger={<Button variant="secondary" value={t('Share')} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</ShowOnlyOnClient>
|
</ShowOnlyOnClient>
|
||||||
<Show when={props.followers}>
|
<Show when={props.followers}>
|
||||||
|
|
|
@ -35,11 +35,16 @@ export const Draft = (props: Props) => {
|
||||||
const handleDeleteLinkClick = async (e) => {
|
const handleDeleteLinkClick = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const isConfirmed = await showConfirm()
|
const isConfirmed = await showConfirm({
|
||||||
|
confirmBody: t('Are you sure you want to delete this draft?'),
|
||||||
|
confirmButtonLabel: t('Delete'),
|
||||||
|
confirmButtonVariant: 'danger',
|
||||||
|
declineButtonVariant: 'primary'
|
||||||
|
})
|
||||||
if (isConfirmed) {
|
if (isConfirmed) {
|
||||||
props.onDelete(props.shout)
|
props.onDelete(props.shout)
|
||||||
|
|
||||||
await showSnackbar({ type: 'success', body: t('Success') })
|
await showSnackbar({ body: t('Draft successfully deleted') })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -115,12 +115,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.shoutAuthor,
|
|
||||||
.shoutDate {
|
|
||||||
@include font-size(1.4rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shoutAuthor {
|
.shoutAuthor {
|
||||||
|
@include font-size(1.4rem);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-right: 1.6rem;
|
margin-right: 1.6rem;
|
||||||
|
|
||||||
|
@ -139,9 +135,11 @@
|
||||||
.shoutDate {
|
.shoutDate {
|
||||||
color: #9fa1a7;
|
color: #9fa1a7;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@include font-size(1.2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shoutDetails {
|
.shoutDetails {
|
||||||
|
align-items: end;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -149,8 +147,7 @@
|
||||||
.shoutDetailsFeedMode {
|
.shoutDetailsFeedMode {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.shoutAuthor,
|
.shoutAuthor {
|
||||||
.shoutDate {
|
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,19 +191,27 @@
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
margin-bottom: 1.4rem;
|
margin-bottom: 1.4rem;
|
||||||
transition: color 0.2s, background-color 0.2s, box-shadow 0.2s;
|
transition:
|
||||||
|
color 0.2s,
|
||||||
|
background-color 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shoutCardLinkContainer {
|
.shoutCardLinkContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: color 0.2s, background-color 0.2s, box-shadow 0.2s;
|
transition:
|
||||||
|
color 0.2s,
|
||||||
|
background-color 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shoutCardEditControl {
|
.shoutCardEditControl {
|
||||||
border-radius: 2em;
|
border-radius: 2em;
|
||||||
min-height: 2.6em;
|
min-height: 2.6em;
|
||||||
padding: 0 1.4em;
|
padding: 0 1.4em;
|
||||||
transition: background-color 0.2s, color 0.2s;
|
transition:
|
||||||
|
background-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--background-color-invert);
|
background: var(--background-color-invert);
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
margin-top: -0.7rem;
|
||||||
max-height: calc(100vh - 120px);
|
max-height: calc(100vh - 120px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
top: 120px;
|
top: 120px;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
|
margin-top: 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|
||||||
ul > li {
|
ul > li {
|
||||||
|
|
|
@ -7,7 +7,6 @@ import formattedTime from '../../utils/formatDateTime'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { MessageActionsPopup } from './MessageActionsPopup'
|
import { MessageActionsPopup } from './MessageActionsPopup'
|
||||||
import QuotedMessage from './QuotedMessage'
|
import QuotedMessage from './QuotedMessage'
|
||||||
import MD from '../Article/MD'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: MessageType
|
content: MessageType
|
||||||
|
|
|
@ -1,48 +1,22 @@
|
||||||
.confirmModal {
|
.confirmModal {
|
||||||
background: #fff;
|
|
||||||
min-height: 550px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
.confirmModalTitle {
|
||||||
min-height: 710px;
|
@include font-size(2rem);
|
||||||
}
|
|
||||||
}
|
font-weight: 700;
|
||||||
|
color: var(--default-color);
|
||||||
.confirmModalTitle {
|
text-align: center;
|
||||||
font-size: 26px;
|
}
|
||||||
line-height: 32px;
|
|
||||||
font-weight: 700;
|
.confirmModalActions {
|
||||||
color: #141414;
|
display: flex;
|
||||||
text-align: left;
|
justify-content: space-between;
|
||||||
}
|
margin-top: 4rem;
|
||||||
|
gap: 2rem;
|
||||||
.confirmModalActions {
|
|
||||||
display: flex;
|
.confirmAction {
|
||||||
justify-content: space-between;
|
flex: 1;
|
||||||
margin-top: 16px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.confirmModalButton {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
margin-right: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-top: 32px;
|
|
||||||
padding: 1.6rem !important;
|
|
||||||
border: 1px solid black;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgb(0 0 0 / 8%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmModalButtonPrimary {
|
|
||||||
margin-right: 0;
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgb(0 0 0 / 60%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { clsx } from 'clsx'
|
|
||||||
import { useConfirm } from '../../../context/confirm'
|
import { useConfirm } from '../../../context/confirm'
|
||||||
import styles from './ConfirmModal.module.scss'
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { Button } from '../../_shared/Button'
|
||||||
|
import styles from './ConfirmModal.module.scss'
|
||||||
|
|
||||||
export const ConfirmModal = () => {
|
export const ConfirmModal = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
@ -12,21 +12,26 @@ export const ConfirmModal = () => {
|
||||||
} = useConfirm()
|
} = useConfirm()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class={styles.confirmModal}>
|
||||||
<h4 class={styles.confirmModalTitle}>
|
<h4 class={styles.confirmModalTitle}>
|
||||||
{confirmMessage().confirmBody ?? t('Are you sure you want to to proceed the action?')}
|
{confirmMessage().confirmBody ?? t('Are you sure you want to to proceed the action?')}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class={styles.confirmModalActions}>
|
<div class={styles.confirmModalActions}>
|
||||||
<button class={styles.confirmModalButton} onClick={() => resolveConfirm(false)}>
|
<Button
|
||||||
{confirmMessage().declineButtonLabel ?? t('Decline')}
|
onClick={() => resolveConfirm(false)}
|
||||||
</button>
|
value={confirmMessage().declineButtonLabel ?? t('Decline')}
|
||||||
<button
|
size="L"
|
||||||
class={clsx(styles.confirmModalButton, styles.confirmModalButtonPrimary)}
|
variant={confirmMessage().declineButtonVariant ?? 'secondary'}
|
||||||
|
class={styles.confirmAction}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
onClick={() => resolveConfirm(true)}
|
onClick={() => resolveConfirm(true)}
|
||||||
>
|
value={confirmMessage().confirmButtonLabel ?? t('Confirm')}
|
||||||
{confirmMessage().confirmButtonLabel ?? t('Confirm')}
|
size="L"
|
||||||
</button>
|
variant={confirmMessage().confirmButtonVariant ?? 'primary'}
|
||||||
|
class={styles.confirmAction}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -104,9 +104,9 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@include media-breakpoint-down(lg) {
|
@include media-breakpoint-down(lg) {
|
||||||
flex: 1 !important;
|
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
padding: 0 !important;
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: $container-padding-x !important;
|
padding: $container-padding-x !important;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 64px;
|
top: 58px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
@ -191,8 +191,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
:global(.view-switcher) {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
@ -217,6 +218,10 @@
|
||||||
.fixed & {
|
.fixed & {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
padding-top: 0.1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainNavigationSocial a {
|
.mainNavigationSocial a {
|
||||||
|
@ -246,6 +251,30 @@
|
||||||
background: #f7f7f8;
|
background: #f7f7f8;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 1.6rem;
|
border-radius: 1.6rem;
|
||||||
|
padding-right: 5.6rem;
|
||||||
|
|
||||||
|
&:not(:placeholder-shown) {
|
||||||
|
& ~ .mobileSubscriptionSubmit {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileSubscriptionSubmit {
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
display: none;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
left: 50%;
|
||||||
|
position: relative;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 16px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -387,15 +416,11 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 5rem;
|
right: 0;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@include media-breakpoint-up(lg) {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-breakpoint-up(xl) {
|
@include media-breakpoint-up(xl) {
|
||||||
right: 2rem;
|
right: 2rem;
|
||||||
}
|
}
|
||||||
|
@ -446,10 +471,6 @@
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(md) {
|
|
||||||
padding: divide($container-padding-x, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.userpic {
|
.userpic {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
@ -491,6 +512,7 @@
|
||||||
a,
|
a,
|
||||||
a:link {
|
a:link {
|
||||||
border: none;
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
height: auto;
|
height: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -534,6 +556,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&:global(.loginbtn) {
|
&:global(.loginbtn) {
|
||||||
|
background: #e9e9ee;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(xl) {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
height: 2.4rem;
|
height: 2.4rem;
|
||||||
width: 2.4rem;
|
width: 2.4rem;
|
||||||
|
|
|
@ -67,7 +67,7 @@ export const Header = (props: Props) => {
|
||||||
let windowScrollTop = 0
|
let windowScrollTop = 0
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const mainContent = document.querySelector('.main-content') as HTMLDivElement
|
const mainContent = document.querySelector<HTMLDivElement>('.main-content')
|
||||||
|
|
||||||
if (fixed() || modal() !== null) {
|
if (fixed() || modal() !== null) {
|
||||||
windowScrollTop = window.scrollY
|
windowScrollTop = window.scrollY
|
||||||
|
@ -173,6 +173,11 @@ export const Header = (props: Props) => {
|
||||||
|
|
||||||
<div class={clsx(styles.mainHeaderInner, 'wide-container')}>
|
<div class={clsx(styles.mainHeaderInner, 'wide-container')}>
|
||||||
<nav class={clsx('row', styles.headerInner, { ['fixed']: fixed() })}>
|
<nav class={clsx('row', styles.headerInner, { ['fixed']: fixed() })}>
|
||||||
|
<div class={clsx(styles.burgerContainer, 'col-auto')}>
|
||||||
|
<div class={styles.burger} classList={{ fixed: fixed() }} onClick={toggleFixed}>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class={clsx('col-md-5 col-xl-4 col-auto', styles.mainLogo)}>
|
<div class={clsx('col-md-5 col-xl-4 col-auto', styles.mainLogo)}>
|
||||||
<a href={getPagePath(router, 'home')}>
|
<a href={getPagePath(router, 'home')}>
|
||||||
<img src="/logo.svg" alt={t('Discours')} />
|
<img src="/logo.svg" alt={t('Discours')} />
|
||||||
|
@ -214,6 +219,7 @@ export const Header = (props: Props) => {
|
||||||
<Link
|
<Link
|
||||||
onMouseOver={() => toggleSubnavigation(true, setIsKnowledgeBaseVisible)}
|
onMouseOver={() => toggleSubnavigation(true, setIsKnowledgeBaseVisible)}
|
||||||
onMouseOut={() => hideSubnavigation}
|
onMouseOut={() => hideSubnavigation}
|
||||||
|
routeName="guide"
|
||||||
body={t('Knowledge base')}
|
body={t('Knowledge base')}
|
||||||
active={isKnowledgeBaseVisible()}
|
active={isKnowledgeBaseVisible()}
|
||||||
/>
|
/>
|
||||||
|
@ -278,6 +284,9 @@ export const Header = (props: Props) => {
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<input type="email" placeholder="Ваш email" id="subscription-email" />
|
<input type="email" placeholder="Ваш email" id="subscription-email" />
|
||||||
<label for="subscription-email">{t('Your email')}</label>
|
<label for="subscription-email">{t('Your email')}</label>
|
||||||
|
<button class={styles.mobileSubscriptionSubmit}>
|
||||||
|
<Icon name="arrow-right" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@ -327,11 +336,6 @@ export const Header = (props: Props) => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={clsx(styles.burgerContainer, 'col-auto')}>
|
|
||||||
<div class={styles.burger} classList={{ fixed: fixed() }} onClick={toggleFixed}>
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.subnavigation, 'col')}
|
class={clsx(styles.subnavigation, 'col')}
|
||||||
|
|
|
@ -108,7 +108,7 @@ export const HeaderAuth = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<ShowOnlyOnClient>
|
<ShowOnlyOnClient>
|
||||||
<Show when={isSessionLoaded()} keyed={true}>
|
<Show when={isSessionLoaded()} keyed={true}>
|
||||||
<div class={clsx('col-sm-6 col-lg-7', styles.usernav)}>
|
<div class={clsx('col-auto col-lg-7', styles.usernav)}>
|
||||||
<div class={styles.userControl}>
|
<div class={styles.userControl}>
|
||||||
<Show when={isCreatePostButtonVisible()}>
|
<Show when={isCreatePostButtonVisible()}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
|
@ -129,13 +129,14 @@ export const HeaderAuth = (props: Props) => {
|
||||||
|
|
||||||
<Show when={isNotificationsVisible()}>
|
<Show when={isNotificationsVisible()}>
|
||||||
<div class={styles.userControlItem} onClick={handleBellIconClick}>
|
<div class={styles.userControlItem} onClick={handleBellIconClick}>
|
||||||
{/*TODO: check markup (cursor: pointer, hover)*/}
|
<div class={styles.button}>
|
||||||
<Icon name="bell-white" counter={unreadNotificationsCount()} class={styles.icon} />
|
<Icon name="bell-white" counter={unreadNotificationsCount()} class={styles.icon} />
|
||||||
<Icon
|
<Icon
|
||||||
name="bell-white-hover"
|
name="bell-white-hover"
|
||||||
counter={unreadNotificationsCount()}
|
counter={unreadNotificationsCount()}
|
||||||
class={clsx(styles.icon, styles.iconHover)}
|
class={clsx(styles.icon, styles.iconHover)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
@ -175,7 +176,7 @@ export const HeaderAuth = (props: Props) => {
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
||||||
<a href="?modal=auth&mode=login">
|
<a href="?modal=auth&mode=login">
|
||||||
<span class={styles.textLabel}>{t('Enter')}</span>
|
<span class={styles.textLabel}>{t('Enter')}</span>
|
||||||
<Icon name="user-default" class={styles.icon} />
|
<Icon name="key" class={styles.icon} />
|
||||||
{/*<Icon name="user-default" class={clsx(styles.icon, styles.iconHover)} />*/}
|
{/*<Icon name="user-default" class={clsx(styles.icon, styles.iconHover)} />*/}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -71,8 +71,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
right: 3.6rem;
|
right: 1.6rem;
|
||||||
top: 12px;
|
top: 1.6rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,7 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 5.4rem 2.4rem 2.4rem;
|
padding: 5.4rem 2.4rem 2.4rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
@include media-breakpoint-up(sm) {
|
@include media-breakpoint-up(sm) {
|
||||||
padding: 5rem;
|
padding: 5rem;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createEffect, createMemo, createSignal, on, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { hideModal, useModalStore } from '../../../stores/ui'
|
import { hideModal, useModalStore } from '../../../stores/ui'
|
||||||
|
@ -8,7 +8,6 @@ import styles from './Modal.module.scss'
|
||||||
import { redirectPage } from '@nanostores/router'
|
import { redirectPage } from '@nanostores/router'
|
||||||
import { router } from '../../../stores/router'
|
import { router } from '../../../stores/router'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { resetSortedArticles } from '../../../stores/zine/articles'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string
|
name: string
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
.Topics {
|
.Topics {
|
||||||
@include font-size(1.4rem);
|
font-size: 1.6rem;
|
||||||
|
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
line-height: 6rem;
|
line-height: 6rem;
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
|
@ -12,32 +11,68 @@
|
||||||
padding: 0 divide($container-padding-x, 2);
|
padding: 0 divide($container-padding-x, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
@include font-size(1.4rem);
|
||||||
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding: 0 7em 0 0;
|
overflow: auto;
|
||||||
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-right: 7em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
margin-right: 2.4rem;
|
margin-right: 2.4rem;
|
||||||
|
|
||||||
&.right {
|
&.right {
|
||||||
|
display: none;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.icon) {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.3em;
|
||||||
|
top: 0.15em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
border-bottom: unset;
|
border-bottom: unset;
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-bottom: 2px solid var(--default-color);
|
border-bottom: 2px solid var(--default-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.icon {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.3em;
|
||||||
|
position: relative;
|
||||||
|
top: 0.15em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
.EmptyMessage {
|
||||||
|
// TODO: check markup
|
||||||
|
color: var(--black-500);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 24px;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import styles from './EmptyMessage.module.scss'
|
||||||
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
|
||||||
|
export const EmptyMessage = () => {
|
||||||
|
const { t } = useLocalize()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={clsx(styles.EmptyMessage)}>
|
||||||
|
<div class={styles.title}>{t('No notifications yet')}</div>
|
||||||
|
<div>{t("Write good articles, comment\nand it won't be so empty here")}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/NotificationsPanel/EmptyMessage/index.ts
Normal file
1
src/components/NotificationsPanel/EmptyMessage/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { EmptyMessage } from './EmptyMessage'
|
|
@ -20,6 +20,13 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--gray-100);
|
background-color: var(--gray-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:visited {
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.userpic {
|
.userpic {
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import styles from './NotificationView.module.scss'
|
import styles from './NotificationView.module.scss'
|
||||||
import type { Notification } from '../../../graphql/types.gen'
|
import type { Notification } from '../../../graphql/types.gen'
|
||||||
import { formatDate } from '../../../utils'
|
|
||||||
import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
||||||
import { NotificationType } from '../../../graphql/types.gen'
|
import { NotificationType } from '../../../graphql/types.gen'
|
||||||
import { openPage } from '@nanostores/router'
|
import { getPagePath, openPage } from '@nanostores/router'
|
||||||
import { router } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { useNotifications } from '../../../context/notifications'
|
import { useNotifications } from '../../../context/notifications'
|
||||||
import { Userpic } from '../../Author/Userpic'
|
import { Userpic } from '../../Author/Userpic'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import notifications from '../../../graphql/query/notifications'
|
import type { ArticlePageSearchParams } from '../../Article/FullArticle'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
notification: Notification
|
notification: Notification
|
||||||
|
@ -28,13 +27,16 @@ type NotificationData = {
|
||||||
slug: string
|
slug: string
|
||||||
userpic: string
|
userpic: string
|
||||||
}[]
|
}[]
|
||||||
|
reactionIds: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotificationView = (props: Props) => {
|
export const NotificationView = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
actions: { markNotificationAsRead }
|
actions: { markNotificationAsRead, hideNotificationsPanel }
|
||||||
} = useNotifications()
|
} = useNotifications()
|
||||||
|
|
||||||
|
const { changeSearchParam } = useRouter<ArticlePageSearchParams>()
|
||||||
|
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const [data, setData] = createSignal<NotificationData>(null)
|
const [data, setData] = createSignal<NotificationData>(null)
|
||||||
|
@ -51,6 +53,11 @@ export const NotificationView = (props: Props) => {
|
||||||
return data().users[data().users.length - 1]
|
return data().users[data().users.length - 1]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleLinkClick = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
hideNotificationsPanel()
|
||||||
|
}
|
||||||
|
|
||||||
const content = createMemo(() => {
|
const content = createMemo(() => {
|
||||||
if (!data()) {
|
if (!data()) {
|
||||||
return null
|
return null
|
||||||
|
@ -66,47 +73,67 @@ export const NotificationView = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shoutTitle.length < data().shout.title.length) {
|
if (shoutTitle.length < data().shout.title.length) {
|
||||||
shoutTitle += '...'
|
shoutTitle = `${shoutTitle.trim()}...`
|
||||||
|
|
||||||
|
if (shoutTitle[0] === '«') {
|
||||||
|
shoutTitle += '»'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (props.notification.type) {
|
switch (props.notification.type) {
|
||||||
case NotificationType.NewComment: {
|
case NotificationType.NewComment: {
|
||||||
return t('NewCommentNotificationText', {
|
return (
|
||||||
commentsCount: props.notification.occurrences,
|
<>
|
||||||
shoutTitle,
|
{t('NotificationNewCommentText1', {
|
||||||
lastCommenterName: lastUser().name,
|
commentsCount: props.notification.occurrences
|
||||||
restUsersCount: data().users.length - 1
|
})}{' '}
|
||||||
})
|
<a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}>
|
||||||
|
{shoutTitle}
|
||||||
|
</a>{' '}
|
||||||
|
{t('NotificationNewCommentText2')}{' '}
|
||||||
|
<a href={getPagePath(router, 'author', { slug: lastUser().slug })} onClick={handleLinkClick}>
|
||||||
|
{lastUser().name}
|
||||||
|
</a>{' '}
|
||||||
|
{t('NotificationNewCommentText3', {
|
||||||
|
restUsersCount: data().users.length - 1
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case NotificationType.NewReply: {
|
case NotificationType.NewReply: {
|
||||||
return t('NewReplyNotificationText', {
|
return (
|
||||||
commentsCount: props.notification.occurrences,
|
<>
|
||||||
shoutTitle,
|
{t('NotificationNewReplyText1', {
|
||||||
lastCommenterName: lastUser().name,
|
commentsCount: props.notification.occurrences
|
||||||
restUsersCount: data().users.length - 1
|
})}{' '}
|
||||||
})
|
<a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}>
|
||||||
|
{shoutTitle}
|
||||||
|
</a>{' '}
|
||||||
|
{t('NotificationNewReplyText2')}{' '}
|
||||||
|
<a href={getPagePath(router, 'author', { slug: lastUser().slug })} onClick={handleLinkClick}>
|
||||||
|
{lastUser().name}
|
||||||
|
</a>{' '}
|
||||||
|
{t('NotificationNewReplyText3', {
|
||||||
|
restUsersCount: data().users.length - 1
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
|
props.onClick()
|
||||||
|
|
||||||
if (!props.notification.seen) {
|
if (!props.notification.seen) {
|
||||||
markNotificationAsRead(props.notification)
|
markNotificationAsRead(props.notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
openPage(router, 'article', { slug: data().shout.slug })
|
openPage(router, 'article', { slug: data().shout.slug })
|
||||||
props.onClick()
|
|
||||||
|
|
||||||
// switch (props.notification.type) {
|
if (data().reactionIds) {
|
||||||
// case NotificationType.NewComment: {
|
changeSearchParam({ commentId: data().reactionIds[0].toString() })
|
||||||
// openPage(router, 'article', { slug: data().shout.slug })
|
}
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// case NotificationType.NewReply: {
|
|
||||||
// openPage(router, 'article', { slug: data().shout.slug })
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -4,9 +4,10 @@ import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
|
||||||
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
|
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { createEffect, For, onCleanup, onMount } from 'solid-js'
|
import { createEffect, For } from 'solid-js'
|
||||||
import { useNotifications } from '../../context/notifications'
|
import { useNotifications } from '../../context/notifications'
|
||||||
import { NotificationView } from './NotificationView'
|
import { NotificationView } from './NotificationView'
|
||||||
|
import { EmptyMessage } from './EmptyMessage'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
@ -30,8 +31,22 @@ export const NotificationsPanel = (props: Props) => {
|
||||||
handler: () => handleHide()
|
handler: () => handleHide()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let windowScrollTop = 0
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
const mainContent = document.querySelector<HTMLDivElement>('.main-content')
|
||||||
|
|
||||||
|
if (props.isOpen) {
|
||||||
|
windowScrollTop = window.scrollY
|
||||||
|
mainContent.style.marginTop = `-${windowScrollTop}px`
|
||||||
|
}
|
||||||
|
|
||||||
document.body.classList.toggle('fixed', props.isOpen)
|
document.body.classList.toggle('fixed', props.isOpen)
|
||||||
|
|
||||||
|
if (!props.isOpen) {
|
||||||
|
mainContent.style.marginTop = ''
|
||||||
|
window.scrollTo(0, windowScrollTop)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
useEscKeyDownHandler(handleHide)
|
useEscKeyDownHandler(handleHide)
|
||||||
|
@ -52,10 +67,7 @@ export const NotificationsPanel = (props: Props) => {
|
||||||
<Icon name="close" />
|
<Icon name="close" />
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.title}>{t('Notifications')}</div>
|
<div class={styles.title}>{t('Notifications')}</div>
|
||||||
<For
|
<For each={sortedNotifications()} fallback={<EmptyMessage />}>
|
||||||
each={sortedNotifications()}
|
|
||||||
fallback={<div class={styles.emptyMessageContainer}>{t('No notifications, yet')}</div>}
|
|
||||||
>
|
|
||||||
{(notification) => (
|
{(notification) => (
|
||||||
<NotificationView
|
<NotificationView
|
||||||
notification={notification}
|
notification={notification}
|
||||||
|
|
|
@ -158,12 +158,17 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 20px;
|
line-height: 1.8rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
vertical-align: bottom;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: rgb(0 0 0 / 50%);
|
color: rgb(0 0 0 / 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.TableOfContentsHeadingsItemH3,
|
.TableOfContentsHeadingsItemH3,
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import { For, Show, createSignal, createEffect, on } from 'solid-js'
|
import { For, Show, createSignal, createEffect, on, onMount, onCleanup } from 'solid-js'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
import { DEFAULT_HEADER_OFFSET } from '../../stores/router'
|
import { DEFAULT_HEADER_OFFSET } from '../../stores/router'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
||||||
import debounce from 'debounce'
|
import debounce from 'debounce'
|
||||||
|
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
|
||||||
import styles from './TableOfContents.module.scss'
|
import styles from './TableOfContents.module.scss'
|
||||||
import { isDesktop } from '../../utils/media-query'
|
import { isDesktop } from '../../utils/media-query'
|
||||||
|
import throttle from 'just-throttle'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
variant: 'article' | 'editor'
|
variant: 'article' | 'editor'
|
||||||
|
@ -18,6 +14,10 @@ interface Props {
|
||||||
body: string
|
body: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isInViewport = (el: Element): boolean => {
|
||||||
|
const rect = el.getBoundingClientRect()
|
||||||
|
return rect.top <= DEFAULT_HEADER_OFFSET
|
||||||
|
}
|
||||||
const scrollToHeader = (element) => {
|
const scrollToHeader = (element) => {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
|
@ -31,9 +31,9 @@ const scrollToHeader = (element) => {
|
||||||
export const TableOfContents = (props: Props) => {
|
export const TableOfContents = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const [headings, setHeadings] = createSignal<Element[]>([])
|
const [headings, setHeadings] = createSignal<HTMLElement[]>([])
|
||||||
const [areHeadingsLoaded, setAreHeadingsLoaded] = createSignal<boolean>(false)
|
const [areHeadingsLoaded, setAreHeadingsLoaded] = createSignal<boolean>(false)
|
||||||
|
const [activeHeaderIndex, setActiveHeaderIndex] = createSignal<number>(-1)
|
||||||
const [isVisible, setIsVisible] = createSignal<boolean>(props.variant === 'article')
|
const [isVisible, setIsVisible] = createSignal<boolean>(props.variant === 'article')
|
||||||
const toggleIsVisible = () => {
|
const toggleIsVisible = () => {
|
||||||
setIsVisible((visible) => !visible)
|
setIsVisible((visible) => !visible)
|
||||||
|
@ -42,15 +42,20 @@ export const TableOfContents = (props: Props) => {
|
||||||
setIsVisible(isDesktop())
|
setIsVisible(isDesktop())
|
||||||
|
|
||||||
const updateHeadings = () => {
|
const updateHeadings = () => {
|
||||||
const { parentSelector } = props
|
setHeadings(
|
||||||
|
// eslint-disable-next-line unicorn/prefer-spread
|
||||||
// eslint-disable-next-line unicorn/prefer-spread
|
Array.from(document.querySelector(props.parentSelector).querySelectorAll<HTMLElement>('h2, h3, h4'))
|
||||||
setHeadings(Array.from(document.querySelector(parentSelector).querySelectorAll('h2, h3, h4')))
|
)
|
||||||
setAreHeadingsLoaded(true)
|
setAreHeadingsLoaded(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedUpdateHeadings = debounce(updateHeadings, 500)
|
const debouncedUpdateHeadings = debounce(updateHeadings, 500)
|
||||||
|
|
||||||
|
const updateActiveHeader = throttle(() => {
|
||||||
|
const newActiveIndex = headings().findLastIndex((heading) => isInViewport(heading))
|
||||||
|
setActiveHeaderIndex(newActiveIndex)
|
||||||
|
}, 50)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.body,
|
() => props.body,
|
||||||
|
@ -58,6 +63,11 @@ export const TableOfContents = (props: Props) => {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener('scroll', updateActiveHeader)
|
||||||
|
onCleanup(() => window.removeEventListener('scroll', updateActiveHeader))
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={
|
when={
|
||||||
|
@ -77,17 +87,17 @@ export const TableOfContents = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<ul class={styles.TableOfContentsHeadingsList}>
|
<ul class={styles.TableOfContentsHeadingsList}>
|
||||||
<For each={headings()}>
|
<For each={headings()}>
|
||||||
{(h) => (
|
{(h, index) => (
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
class={clsx(styles.TableOfContentsHeadingsItem, {
|
class={clsx(styles.TableOfContentsHeadingsItem, {
|
||||||
[styles.TableOfContentsHeadingsItemH3]: h.nodeName === 'H3',
|
[styles.TableOfContentsHeadingsItemH3]: h.nodeName === 'H3',
|
||||||
[styles.TableOfContentsHeadingsItemH4]: h.nodeName === 'H4'
|
[styles.TableOfContentsHeadingsItemH4]: h.nodeName === 'H4',
|
||||||
|
[styles.active]: index() === activeHeaderIndex()
|
||||||
})}
|
})}
|
||||||
innerHTML={h.textContent}
|
innerHTML={h.textContent}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
scrollToHeader(h)
|
scrollToHeader(h)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.picture {
|
.picture {
|
||||||
display: block;
|
display: block;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
|
|
|
@ -8,8 +8,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.groupControls {
|
.groupControls {
|
||||||
margin-bottom: 6rem !important;
|
margin-bottom: 2rem !important;
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
margin-bottom: 6rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { apiClient } from '../../../utils/apiClient'
|
||||||
import { Comment } from '../../Article/Comment'
|
import { Comment } from '../../Article/Comment'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
|
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
|
||||||
import { hideModal } from '../../../stores/ui'
|
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { Loading } from '../../_shared/Loading'
|
import { Loading } from '../../_shared/Loading'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
.feedFilter {
|
.feedFilter {
|
||||||
margin: 0.2em 0 4.8rem;
|
margin-bottom: 4.8rem;
|
||||||
|
margin-top: 0.2em;
|
||||||
|
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(md) {
|
||||||
margin-right: 4rem !important;
|
margin-right: 4rem !important;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.button {
|
.button {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@ -31,6 +31,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
border: 3px solid var(--danger-color);
|
||||||
|
background: var(--background-color);
|
||||||
|
color: var(--danger-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--danger-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.inline {
|
&.inline {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
|
@ -2,10 +2,11 @@ import type { JSX } from 'solid-js'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import styles from './Button.module.scss'
|
import styles from './Button.module.scss'
|
||||||
|
|
||||||
|
export type ButtonVariant = 'primary' | 'secondary' | 'bordered' | 'inline' | 'light' | 'outline' | 'danger'
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string | JSX.Element
|
value: string | JSX.Element
|
||||||
size?: 'S' | 'M' | 'L'
|
size?: 'S' | 'M' | 'L'
|
||||||
variant?: 'primary' | 'secondary' | 'bordered' | 'inline' | 'light' | 'outline'
|
variant?: ButtonVariant
|
||||||
type?: 'submit' | 'button'
|
type?: 'submit' | 'button'
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
|
|
@ -2,11 +2,14 @@ import { createContext, createSignal, useContext } from 'solid-js'
|
||||||
import type { Accessor, JSX } from 'solid-js'
|
import type { Accessor, JSX } from 'solid-js'
|
||||||
|
|
||||||
import { hideModal, showModal } from '../stores/ui'
|
import { hideModal, showModal } from '../stores/ui'
|
||||||
|
import { ButtonVariant } from '../components/_shared/Button/Button'
|
||||||
|
|
||||||
type ConfirmMessage = {
|
type ConfirmMessage = {
|
||||||
confirmBody?: string | JSX.Element
|
confirmBody?: string | JSX.Element
|
||||||
confirmButtonLabel?: string
|
confirmButtonLabel?: string
|
||||||
|
confirmButtonVariant?: ButtonVariant
|
||||||
declineButtonLabel?: string
|
declineButtonLabel?: string
|
||||||
|
declineButtonVariant?: ButtonVariant
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfirmContextType = {
|
type ConfirmContextType = {
|
||||||
|
@ -15,7 +18,9 @@ type ConfirmContextType = {
|
||||||
showConfirm: (message?: {
|
showConfirm: (message?: {
|
||||||
confirmBody?: ConfirmMessage['confirmBody']
|
confirmBody?: ConfirmMessage['confirmBody']
|
||||||
confirmButtonLabel?: ConfirmMessage['confirmButtonLabel']
|
confirmButtonLabel?: ConfirmMessage['confirmButtonLabel']
|
||||||
|
confirmButtonVariant?: ConfirmMessage['confirmButtonVariant']
|
||||||
declineButtonLabel?: ConfirmMessage['declineButtonLabel']
|
declineButtonLabel?: ConfirmMessage['declineButtonLabel']
|
||||||
|
declineButtonVariant?: ConfirmMessage['declineButtonVariant']
|
||||||
}) => Promise<boolean>
|
}) => Promise<boolean>
|
||||||
resolveConfirm: (value: boolean) => void
|
resolveConfirm: (value: boolean) => void
|
||||||
}
|
}
|
||||||
|
@ -36,13 +41,17 @@ export const ConfirmProvider = (props: { children: JSX.Element }) => {
|
||||||
message: {
|
message: {
|
||||||
confirmBody?: ConfirmMessage['confirmBody']
|
confirmBody?: ConfirmMessage['confirmBody']
|
||||||
confirmButtonLabel?: ConfirmMessage['confirmButtonLabel']
|
confirmButtonLabel?: ConfirmMessage['confirmButtonLabel']
|
||||||
|
confirmButtonVariant?: ConfirmMessage['confirmButtonVariant']
|
||||||
declineButtonLabel?: ConfirmMessage['declineButtonLabel']
|
declineButtonLabel?: ConfirmMessage['declineButtonLabel']
|
||||||
|
declineButtonVariant?: ConfirmMessage['declineButtonVariant']
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const messageToShow = {
|
const messageToShow = {
|
||||||
confirmBody: message.confirmBody,
|
confirmBody: message.confirmBody,
|
||||||
confirmButtonLabel: message.confirmButtonLabel,
|
confirmButtonLabel: message.confirmButtonLabel,
|
||||||
declineButtonLabel: message.declineButtonLabel
|
confirmButtonVariant: message.confirmButtonVariant,
|
||||||
|
declineButtonLabel: message.declineButtonLabel,
|
||||||
|
declineButtonVariant: message.declineButtonVariant
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfirmMessage(messageToShow)
|
setConfirmMessage(messageToShow)
|
||||||
|
|
|
@ -16,6 +16,7 @@ type NotificationsContextType = {
|
||||||
sortedNotifications: Accessor<Notification[]>
|
sortedNotifications: Accessor<Notification[]>
|
||||||
actions: {
|
actions: {
|
||||||
showNotificationsPanel: () => void
|
showNotificationsPanel: () => void
|
||||||
|
hideNotificationsPanel: () => void
|
||||||
markNotificationAsRead: (notification: Notification) => Promise<void>
|
markNotificationAsRead: (notification: Notification) => Promise<void>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,7 +81,11 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
setIsNotificationsPanelOpen(true)
|
setIsNotificationsPanelOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = { showNotificationsPanel, markNotificationAsRead }
|
const hideNotificationsPanel = () => {
|
||||||
|
setIsNotificationsPanelOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = { showNotificationsPanel, hideNotificationsPanel, markNotificationAsRead }
|
||||||
|
|
||||||
const value: NotificationsContextType = {
|
const value: NotificationsContextType = {
|
||||||
notificationEntities,
|
notificationEntities,
|
||||||
|
|
|
@ -11,18 +11,9 @@ import { setPageLoadManagerPromise } from '../utils/pageLoadManager'
|
||||||
|
|
||||||
export const ArticlePage = (props: PageProps) => {
|
export const ArticlePage = (props: PageProps) => {
|
||||||
const shouts = props.article ? [props.article] : []
|
const shouts = props.article ? [props.article] : []
|
||||||
|
const { page } = useRouter()
|
||||||
|
|
||||||
const slug = createMemo(() => {
|
const slug = createMemo(() => page().params['slug'] as string)
|
||||||
const { page: getPage } = useRouter()
|
|
||||||
|
|
||||||
const page = getPage()
|
|
||||||
|
|
||||||
if (page.route !== 'article') {
|
|
||||||
throw new Error('ts guard')
|
|
||||||
}
|
|
||||||
|
|
||||||
return page.params.slug
|
|
||||||
})
|
|
||||||
|
|
||||||
const { articleEntities } = useArticlesStore({
|
const { articleEntities } = useArticlesStore({
|
||||||
shouts
|
shouts
|
||||||
|
|
|
@ -289,12 +289,16 @@ button {
|
||||||
|
|
||||||
.button--light {
|
.button--light {
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
|
border-radius: 0.8rem;
|
||||||
color: #000;
|
color: #000;
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 0.6rem 1.2rem 0.6rem 1rem;
|
padding: 0.6rem 1.2rem 0.6rem 1rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e9e9ee;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--subscribe-topic {
|
.button--subscribe-topic {
|
||||||
|
@ -589,18 +593,22 @@ figure {
|
||||||
|
|
||||||
.view-switcher {
|
.view-switcher {
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 3.6rem 0 0;
|
margin: 3.6rem -1rem 0;
|
||||||
padding: 0;
|
overflow: auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 2rem;
|
margin-right: 2rem;
|
||||||
margin-bottom: 0.6em;
|
margin-bottom: 0.6em;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|
|
@ -13,6 +13,6 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"lib": ["ES2021", "dom"]
|
"lib": ["es2023", "dom"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user