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-icu": "2.3.0",
|
||||
"intl-messageformat": "10.5.3",
|
||||
"just-throttle": "4.2.0",
|
||||
"mailgun.js": "8.2.1",
|
||||
"node-fetch": "3.3.1"
|
||||
},
|
||||
|
@ -13168,6 +13169,11 @@
|
|||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
|
||||
|
@ -28106,6 +28112,11 @@
|
|||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"i18next": "22.4.15",
|
||||
"i18next-icu": "2.3.0",
|
||||
"intl-messageformat": "10.5.3",
|
||||
"just-throttle": "4.2.0",
|
||||
"mailgun.js": "8.2.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 topics": "All topics",
|
||||
"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?",
|
||||
"Art": "Art",
|
||||
"Artist": "Artist",
|
||||
|
@ -100,6 +102,7 @@
|
|||
"Discussion rules": "Discussion rules",
|
||||
"Discussions": "Discussions",
|
||||
"Dogma": "Dogma",
|
||||
"Draft successfully deleted": "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.": "Each image must be no larger than 5 MB.",
|
||||
|
@ -193,6 +196,7 @@
|
|||
"Manifesto": "Manifesto",
|
||||
"Many files, choose only one": "Many files, choose only one",
|
||||
"Material card": "Material card",
|
||||
"Message": "Message",
|
||||
"More": "More",
|
||||
"Most commented": "Commented",
|
||||
"Most read": "Readable",
|
||||
|
@ -206,12 +210,19 @@
|
|||
"New only": "New only",
|
||||
"New password": "New password",
|
||||
"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",
|
||||
"Night mode": "Night mode",
|
||||
"No notifications, yet": "No notifications, yet",
|
||||
"No such account, please try to register": "No such account found, please try to register",
|
||||
"No notifications yet": "No notifications yet",
|
||||
"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 is here": "There is nothing here",
|
||||
"Notifications": "Notifications",
|
||||
|
@ -353,7 +364,6 @@
|
|||
"Where": "From",
|
||||
"Words": "Слов",
|
||||
"Work with us": "Cooperate with Discourse",
|
||||
"Message": "Message",
|
||||
"Write a comment...": "Write a comment...",
|
||||
"Write a short introduction": "Write a short introduction",
|
||||
"Write about the topic": "Write about the topic",
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
"All posts": "Все публикации",
|
||||
"All topics": "Все темы",
|
||||
"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?": "Вы уверены, что хотите продолжить?",
|
||||
"Art": "Искусство",
|
||||
"Artist": "Исполнитель",
|
||||
|
@ -104,6 +106,7 @@
|
|||
"Discussion rules": "Правила сообществ самиздата в соцсетях",
|
||||
"Discussions": "Дискуссии",
|
||||
"Dogma": "Догма",
|
||||
"Draft successfully deleted": "Черновик успешно удален",
|
||||
"Drafts": "Черновики",
|
||||
"Drag the image to this area": "Перетащите изображение в эту область",
|
||||
"Each image must be no larger than 5 MB.": "Каждое изображение должно быть размером не больше 5 мб.",
|
||||
|
@ -217,11 +220,19 @@
|
|||
"New only": "Только новые",
|
||||
"New password": "Новый пароль",
|
||||
"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": "Рассылка",
|
||||
"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": "Такой адрес не найден, попробуйте зарегистрироваться",
|
||||
"Nothing here yet": "Здесь пока ничего нет",
|
||||
"Nothing is here": "Здесь ничего нет",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.comment {
|
||||
margin: 0.5em 0;
|
||||
margin: 0 0 0.5em;
|
||||
padding: 1rem;
|
||||
transition: background-color 0.3s;
|
||||
position: relative;
|
||||
|
|
|
@ -62,7 +62,12 @@ export const Comment = (props: Props) => {
|
|||
const remove = async () => {
|
||||
if (comment()?.id) {
|
||||
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) {
|
||||
await deleteReaction(comment().id)
|
||||
|
@ -136,7 +141,7 @@ export const Comment = (props: Props) => {
|
|||
})}
|
||||
/>
|
||||
<small>
|
||||
<a href={`#comment-${comment()?.id}`}>{comment()?.shout.title || ''}</a>
|
||||
<a href={`#comment_${comment()?.id}`}>{comment()?.shout.title || ''}</a>
|
||||
</small>
|
||||
</div>
|
||||
}
|
||||
|
@ -174,7 +179,7 @@ export const Comment = (props: Props) => {
|
|||
<CommentRatingControl comment={comment()} />
|
||||
</div>
|
||||
</Show>
|
||||
<div class={styles.commentBody} id={'comment-' + (comment().id || '')}>
|
||||
<div class={styles.commentBody}>
|
||||
<Show when={editMode()} fallback={<MD body={body()} />}>
|
||||
<Suspense fallback={<p>{t('Loading')}</p>}>
|
||||
<SimplifiedEditor
|
||||
|
|
|
@ -4,14 +4,15 @@
|
|||
align-self: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
@include font-size(1.2rem);
|
||||
font-size: 1.2rem;
|
||||
gap: 1rem;
|
||||
justify-content: flex-start;
|
||||
margin: 0 1em 0 0;
|
||||
|
||||
.date {
|
||||
font-weight: 500;
|
||||
margin-right: 1rem;
|
||||
|
||||
.icon {
|
||||
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 styles from './Article.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
|
@ -181,7 +181,7 @@ export const CommentsTree = (props: Props) => {
|
|||
<SimplifiedEditor
|
||||
quoteEnabled={true}
|
||||
imageEnabled={true}
|
||||
autoFocus={true}
|
||||
autoFocus={false}
|
||||
submitByCtrlEnter={true}
|
||||
placeholder={t('Write a comment...')}
|
||||
onSubmit={(value) => handleSubmitComment(value)}
|
||||
|
|
|
@ -28,11 +28,26 @@ import styles from './Article.module.scss'
|
|||
import { CardTopic } from '../Feed/CardTopic'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
article: Shout
|
||||
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) => {
|
||||
const { t } = useLocalize()
|
||||
const {
|
||||
|
@ -77,16 +92,15 @@ export const FullArticle = (props: Props) => {
|
|||
return JSON.parse(props.article.media || '[]')
|
||||
})
|
||||
|
||||
const commentsRef: { current: HTMLDivElement } = { current: null }
|
||||
const commentsRef: {
|
||||
current: HTMLDivElement
|
||||
} = { current: null }
|
||||
|
||||
const scrollToComments = () => {
|
||||
window.scrollTo({
|
||||
top: commentsRef.current.offsetTop - 96,
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
scrollTo(commentsRef.current)
|
||||
}
|
||||
|
||||
const { searchParams, changeSearchParam } = useRouter()
|
||||
const { searchParams, changeSearchParam } = useRouter<ArticlePageSearchParams>()
|
||||
|
||||
createEffect(() => {
|
||||
if (props.scrollToComments) {
|
||||
|
@ -105,9 +119,14 @@ export const FullArticle = (props: Props) => {
|
|||
|
||||
createEffect(() => {
|
||||
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) {
|
||||
commentElement.scrollIntoView({ behavior: 'smooth' })
|
||||
scrollTo(commentElement)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info {
|
||||
@include font-size(1.4rem);
|
||||
border: none;
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
flex: 1;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
@ -426,6 +426,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.shareControl {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.buttonSubscribe {
|
||||
align-items: center;
|
||||
aspect-ratio: 1/1;
|
||||
|
@ -447,8 +451,10 @@
|
|||
}
|
||||
|
||||
.buttonWrite {
|
||||
background: #ccc;
|
||||
color: #000;
|
||||
display: inline-flex;
|
||||
font-weight: 500;
|
||||
transition:
|
||||
background-color 0.3s,
|
||||
color 0.3s;
|
||||
|
@ -490,6 +496,7 @@
|
|||
color: #696969;
|
||||
@include font-size(2rem);
|
||||
font-weight: 500;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.authorSubscribe {
|
||||
|
|
|
@ -208,8 +208,7 @@ export const AuthorCard = (props: Props) => {
|
|||
}
|
||||
>
|
||||
<div class={styles.subscribersContainer}>
|
||||
<Switch>
|
||||
<Match when={props.followers && props.followers.length > 0 && !props.isCurrentUser}>
|
||||
<Show when={props.followers && props.followers.length > 0}>
|
||||
<a href="?modal=followers" class={styles.subscribers}>
|
||||
<For each={props.followers.slice(0, 3)}>
|
||||
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
||||
|
@ -218,18 +217,9 @@ export const AuthorCard = (props: Props) => {
|
|||
{t('SubscriberWithCount', { count: props.followers.length })}
|
||||
</div>
|
||||
</a>
|
||||
</Match>
|
||||
<Match when={props.followers && props.followers.length > 0 && props.isCurrentUser}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => redirectPage(router, 'profileSettings')}
|
||||
value={t('Edit profile')}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
|
||||
<Switch>
|
||||
<Match when={!props.isCurrentUser && props.following && props.following.length > 0}>
|
||||
<Show when={props.following && props.following.length > 0}>
|
||||
<a href="?modal=following" class={styles.subscribers}>
|
||||
<For each={props.following.slice(0, 3)}>
|
||||
{(f) => {
|
||||
|
@ -245,23 +235,13 @@ export const AuthorCard = (props: Props) => {
|
|||
{t('SubscriptionWithCount', { count: props?.following.length ?? 0 })}
|
||||
</div>
|
||||
</a>
|
||||
</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>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<ShowOnlyOnClient>
|
||||
<Show when={isSessionLoaded() && props.author.links}>
|
||||
<Show when={isSessionLoaded()}>
|
||||
<Show when={props.author.links && props.author.links.length > 0}>
|
||||
<div class={styles.authorSubscribeSocial}>
|
||||
<For each={props.author.links}>
|
||||
{(link) => (
|
||||
|
@ -278,6 +258,7 @@ export const AuthorCard = (props: Props) => {
|
|||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={canFollow()}>
|
||||
<div class={styles.authorSubscribe}>
|
||||
<Show
|
||||
|
@ -346,9 +327,9 @@ export const AuthorCard = (props: Props) => {
|
|||
|
||||
<Show when={!props.hideWriteButton}>
|
||||
<button
|
||||
class={clsx(styles.button, styles.buttonSubscribe)}
|
||||
class={styles.button}
|
||||
classList={{
|
||||
'button--subscribe': !props.isAuthorsList,
|
||||
'button--light': !props.isAuthorsList,
|
||||
'button--subscribe-topic': props.isAuthorsList,
|
||||
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
||||
}}
|
||||
|
@ -362,6 +343,26 @@ export const AuthorCard = (props: Props) => {
|
|||
</Show>
|
||||
</div>
|
||||
</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>
|
||||
</ShowOnlyOnClient>
|
||||
<Show when={props.followers}>
|
||||
|
|
|
@ -35,11 +35,16 @@ export const Draft = (props: Props) => {
|
|||
const handleDeleteLinkClick = async (e) => {
|
||||
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) {
|
||||
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 {
|
||||
@include font-size(1.4rem);
|
||||
font-weight: 500;
|
||||
margin-right: 1.6rem;
|
||||
|
||||
|
@ -139,9 +135,11 @@
|
|||
.shoutDate {
|
||||
color: #9fa1a7;
|
||||
font-weight: 500;
|
||||
@include font-size(1.2rem);
|
||||
}
|
||||
|
||||
.shoutDetails {
|
||||
align-items: end;
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
@ -149,8 +147,7 @@
|
|||
.shoutDetailsFeedMode {
|
||||
justify-content: space-between;
|
||||
|
||||
.shoutAuthor,
|
||||
.shoutDate {
|
||||
.shoutAuthor {
|
||||
@include font-size(1.2rem);
|
||||
}
|
||||
}
|
||||
|
@ -194,19 +191,27 @@
|
|||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
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 {
|
||||
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 {
|
||||
border-radius: 2em;
|
||||
min-height: 2.6em;
|
||||
padding: 0 1.4em;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--background-color-invert);
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
.sidebar {
|
||||
margin-top: -0.7rem;
|
||||
max-height: calc(100vh - 120px);
|
||||
overflow: auto;
|
||||
top: 120px;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: 0;
|
||||
position: sticky;
|
||||
|
||||
ul > li {
|
||||
|
|
|
@ -7,7 +7,6 @@ import formattedTime from '../../utils/formatDateTime'
|
|||
import { Icon } from '../_shared/Icon'
|
||||
import { MessageActionsPopup } from './MessageActionsPopup'
|
||||
import QuotedMessage from './QuotedMessage'
|
||||
import MD from '../Article/MD'
|
||||
|
||||
type Props = {
|
||||
content: MessageType
|
||||
|
|
|
@ -1,48 +1,22 @@
|
|||
.confirmModal {
|
||||
background: #fff;
|
||||
min-height: 550px;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
min-height: 710px;
|
||||
}
|
||||
}
|
||||
|
||||
.confirmModalTitle {
|
||||
font-size: 26px;
|
||||
line-height: 32px;
|
||||
@include font-size(2rem);
|
||||
|
||||
font-weight: 700;
|
||||
color: #141414;
|
||||
text-align: left;
|
||||
color: var(--default-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.confirmModalActions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 16px;
|
||||
}
|
||||
margin-top: 4rem;
|
||||
gap: 2rem;
|
||||
|
||||
.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%);
|
||||
.confirmAction {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.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 styles from './ConfirmModal.module.scss'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import styles from './ConfirmModal.module.scss'
|
||||
|
||||
export const ConfirmModal = () => {
|
||||
const { t } = useLocalize()
|
||||
|
@ -12,21 +12,26 @@ export const ConfirmModal = () => {
|
|||
} = useConfirm()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class={styles.confirmModal}>
|
||||
<h4 class={styles.confirmModalTitle}>
|
||||
{confirmMessage().confirmBody ?? t('Are you sure you want to to proceed the action?')}
|
||||
</h4>
|
||||
|
||||
<div class={styles.confirmModalActions}>
|
||||
<button class={styles.confirmModalButton} onClick={() => resolveConfirm(false)}>
|
||||
{confirmMessage().declineButtonLabel ?? t('Decline')}
|
||||
</button>
|
||||
<button
|
||||
class={clsx(styles.confirmModalButton, styles.confirmModalButtonPrimary)}
|
||||
<Button
|
||||
onClick={() => resolveConfirm(false)}
|
||||
value={confirmMessage().declineButtonLabel ?? t('Decline')}
|
||||
size="L"
|
||||
variant={confirmMessage().declineButtonVariant ?? 'secondary'}
|
||||
class={styles.confirmAction}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => resolveConfirm(true)}
|
||||
>
|
||||
{confirmMessage().confirmButtonLabel ?? t('Confirm')}
|
||||
</button>
|
||||
value={confirmMessage().confirmButtonLabel ?? t('Confirm')}
|
||||
size="L"
|
||||
variant={confirmMessage().confirmButtonVariant ?? 'primary'}
|
||||
class={styles.confirmAction}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -104,9 +104,9 @@
|
|||
position: relative;
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
flex: 1 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,7 @@
|
|||
overflow: auto;
|
||||
padding: $container-padding-x !important;
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
top: 58px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
|
@ -191,8 +191,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
:global(.view-switcher) {
|
||||
margin-top: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
li {
|
||||
|
@ -217,6 +218,10 @@
|
|||
.fixed & {
|
||||
display: block;
|
||||
}
|
||||
|
||||
a {
|
||||
padding-top: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
.mainNavigationSocial a {
|
||||
|
@ -246,6 +251,30 @@
|
|||
background: #f7f7f8;
|
||||
border: none;
|
||||
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;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
right: 5rem;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
right: 2rem;
|
||||
}
|
||||
|
@ -446,10 +471,6 @@
|
|||
z-index: -1;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
padding: divide($container-padding-x, 2);
|
||||
}
|
||||
|
||||
.userpic {
|
||||
align-items: center;
|
||||
margin-right: 0;
|
||||
|
@ -491,6 +512,7 @@
|
|||
a,
|
||||
a:link {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -534,6 +556,12 @@
|
|||
}
|
||||
|
||||
&:global(.loginbtn) {
|
||||
background: #e9e9ee;
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 2.4rem;
|
||||
width: 2.4rem;
|
||||
|
|
|
@ -67,7 +67,7 @@ export const Header = (props: Props) => {
|
|||
let windowScrollTop = 0
|
||||
|
||||
createEffect(() => {
|
||||
const mainContent = document.querySelector('.main-content') as HTMLDivElement
|
||||
const mainContent = document.querySelector<HTMLDivElement>('.main-content')
|
||||
|
||||
if (fixed() || modal() !== null) {
|
||||
windowScrollTop = window.scrollY
|
||||
|
@ -173,6 +173,11 @@ export const Header = (props: Props) => {
|
|||
|
||||
<div class={clsx(styles.mainHeaderInner, 'wide-container')}>
|
||||
<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)}>
|
||||
<a href={getPagePath(router, 'home')}>
|
||||
<img src="/logo.svg" alt={t('Discours')} />
|
||||
|
@ -214,6 +219,7 @@ export const Header = (props: Props) => {
|
|||
<Link
|
||||
onMouseOver={() => toggleSubnavigation(true, setIsKnowledgeBaseVisible)}
|
||||
onMouseOut={() => hideSubnavigation}
|
||||
routeName="guide"
|
||||
body={t('Knowledge base')}
|
||||
active={isKnowledgeBaseVisible()}
|
||||
/>
|
||||
|
@ -278,6 +284,9 @@ export const Header = (props: Props) => {
|
|||
<div class="pretty-form__item">
|
||||
<input type="email" placeholder="Ваш email" id="subscription-email" />
|
||||
<label for="subscription-email">{t('Your email')}</label>
|
||||
<button class={styles.mobileSubscriptionSubmit}>
|
||||
<Icon name="arrow-right" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
@ -327,11 +336,6 @@ export const Header = (props: Props) => {
|
|||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
<div class={clsx(styles.burgerContainer, 'col-auto')}>
|
||||
<div class={styles.burger} classList={{ fixed: fixed() }} onClick={toggleFixed}>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={clsx(styles.subnavigation, 'col')}
|
||||
|
|
|
@ -108,7 +108,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
return (
|
||||
<ShowOnlyOnClient>
|
||||
<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}>
|
||||
<Show when={isCreatePostButtonVisible()}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||
|
@ -129,7 +129,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
|
||||
<Show when={isNotificationsVisible()}>
|
||||
<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-hover"
|
||||
|
@ -137,6 +137,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
class={clsx(styles.icon, styles.iconHover)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={isSaveButtonVisible()}>
|
||||
|
@ -175,7 +176,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
||||
<a href="?modal=auth&mode=login">
|
||||
<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)} />*/}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -71,8 +71,8 @@
|
|||
}
|
||||
|
||||
.close {
|
||||
right: 3.6rem;
|
||||
top: 12px;
|
||||
right: 1.6rem;
|
||||
top: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,7 @@
|
|||
overflow: auto;
|
||||
padding: 5.4rem 2.4rem 2.4rem;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
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 { clsx } from 'clsx'
|
||||
import { hideModal, useModalStore } from '../../../stores/ui'
|
||||
|
@ -8,7 +8,6 @@ import styles from './Modal.module.scss'
|
|||
import { redirectPage } from '@nanostores/router'
|
||||
import { router } from '../../../stores/router'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { resetSortedArticles } from '../../../stores/zine/articles'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.Topics {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
font-size: 1.6rem;
|
||||
height: 6rem;
|
||||
line-height: 6rem;
|
||||
margin-bottom: 3rem;
|
||||
|
@ -12,32 +11,68 @@
|
|||
padding: 0 divide($container-padding-x, 2);
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
@include font-size(1.4rem);
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-weight: 500;
|
||||
list-style: none;
|
||||
margin-top: 0;
|
||||
padding: 0 7em 0 0;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex-wrap: wrap;
|
||||
overflow: hidden;
|
||||
padding-right: 7em;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-right: 2.4rem;
|
||||
|
||||
&.right {
|
||||
display: none;
|
||||
margin-right: 0;
|
||||
position: absolute;
|
||||
right: 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 {
|
||||
border-bottom: unset;
|
||||
|
||||
&.selected {
|
||||
font-weight: 500;
|
||||
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 {
|
||||
background-color: var(--gray-100);
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
padding-bottom: 0 !important;
|
||||
border-bottom: none !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.userpic {
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './NotificationView.module.scss'
|
||||
import type { Notification } from '../../../graphql/types.gen'
|
||||
import { formatDate } from '../../../utils'
|
||||
import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
||||
import { NotificationType } from '../../../graphql/types.gen'
|
||||
import { openPage } from '@nanostores/router'
|
||||
import { router } from '../../../stores/router'
|
||||
import { getPagePath, openPage } from '@nanostores/router'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { useNotifications } from '../../../context/notifications'
|
||||
import { Userpic } from '../../Author/Userpic'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import notifications from '../../../graphql/query/notifications'
|
||||
import type { ArticlePageSearchParams } from '../../Article/FullArticle'
|
||||
|
||||
type Props = {
|
||||
notification: Notification
|
||||
|
@ -28,13 +27,16 @@ type NotificationData = {
|
|||
slug: string
|
||||
userpic: string
|
||||
}[]
|
||||
reactionIds: number[]
|
||||
}
|
||||
|
||||
export const NotificationView = (props: Props) => {
|
||||
const {
|
||||
actions: { markNotificationAsRead }
|
||||
actions: { markNotificationAsRead, hideNotificationsPanel }
|
||||
} = useNotifications()
|
||||
|
||||
const { changeSearchParam } = useRouter<ArticlePageSearchParams>()
|
||||
|
||||
const { t } = useLocalize()
|
||||
|
||||
const [data, setData] = createSignal<NotificationData>(null)
|
||||
|
@ -51,6 +53,11 @@ export const NotificationView = (props: Props) => {
|
|||
return data().users[data().users.length - 1]
|
||||
})
|
||||
|
||||
const handleLinkClick = (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
hideNotificationsPanel()
|
||||
}
|
||||
|
||||
const content = createMemo(() => {
|
||||
if (!data()) {
|
||||
return null
|
||||
|
@ -66,47 +73,67 @@ export const NotificationView = (props: Props) => {
|
|||
}
|
||||
|
||||
if (shoutTitle.length < data().shout.title.length) {
|
||||
shoutTitle += '...'
|
||||
shoutTitle = `${shoutTitle.trim()}...`
|
||||
|
||||
if (shoutTitle[0] === '«') {
|
||||
shoutTitle += '»'
|
||||
}
|
||||
}
|
||||
|
||||
switch (props.notification.type) {
|
||||
case NotificationType.NewComment: {
|
||||
return t('NewCommentNotificationText', {
|
||||
commentsCount: props.notification.occurrences,
|
||||
shoutTitle,
|
||||
lastCommenterName: lastUser().name,
|
||||
return (
|
||||
<>
|
||||
{t('NotificationNewCommentText1', {
|
||||
commentsCount: props.notification.occurrences
|
||||
})}{' '}
|
||||
<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: {
|
||||
return t('NewReplyNotificationText', {
|
||||
commentsCount: props.notification.occurrences,
|
||||
shoutTitle,
|
||||
lastCommenterName: lastUser().name,
|
||||
return (
|
||||
<>
|
||||
{t('NotificationNewReplyText1', {
|
||||
commentsCount: props.notification.occurrences
|
||||
})}{' '}
|
||||
<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 = () => {
|
||||
props.onClick()
|
||||
|
||||
if (!props.notification.seen) {
|
||||
markNotificationAsRead(props.notification)
|
||||
}
|
||||
|
||||
openPage(router, 'article', { slug: data().shout.slug })
|
||||
props.onClick()
|
||||
|
||||
// switch (props.notification.type) {
|
||||
// case NotificationType.NewComment: {
|
||||
// openPage(router, 'article', { slug: data().shout.slug })
|
||||
// break
|
||||
// }
|
||||
// case NotificationType.NewReply: {
|
||||
// openPage(router, 'article', { slug: data().shout.slug })
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
if (data().reactionIds) {
|
||||
changeSearchParam({ commentId: data().reactionIds[0].toString() })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -4,9 +4,10 @@ import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
|
|||
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
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 { NotificationView } from './NotificationView'
|
||||
import { EmptyMessage } from './EmptyMessage'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
|
@ -30,8 +31,22 @@ export const NotificationsPanel = (props: Props) => {
|
|||
handler: () => handleHide()
|
||||
})
|
||||
|
||||
let windowScrollTop = 0
|
||||
|
||||
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)
|
||||
|
||||
if (!props.isOpen) {
|
||||
mainContent.style.marginTop = ''
|
||||
window.scrollTo(0, windowScrollTop)
|
||||
}
|
||||
})
|
||||
|
||||
useEscKeyDownHandler(handleHide)
|
||||
|
@ -52,10 +67,7 @@ export const NotificationsPanel = (props: Props) => {
|
|||
<Icon name="close" />
|
||||
</div>
|
||||
<div class={styles.title}>{t('Notifications')}</div>
|
||||
<For
|
||||
each={sortedNotifications()}
|
||||
fallback={<div class={styles.emptyMessageContainer}>{t('No notifications, yet')}</div>}
|
||||
>
|
||||
<For each={sortedNotifications()} fallback={<EmptyMessage />}>
|
||||
{(notification) => (
|
||||
<NotificationView
|
||||
notification={notification}
|
||||
|
|
|
@ -158,12 +158,17 @@
|
|||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
line-height: 1.8rem;
|
||||
text-align: left;
|
||||
vertical-align: bottom;
|
||||
|
||||
&:hover {
|
||||
color: rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.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 { DEFAULT_HEADER_OFFSET } from '../../stores/router'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
|
||||
import debounce from 'debounce'
|
||||
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
import styles from './TableOfContents.module.scss'
|
||||
import { isDesktop } from '../../utils/media-query'
|
||||
import throttle from 'just-throttle'
|
||||
|
||||
interface Props {
|
||||
variant: 'article' | 'editor'
|
||||
|
@ -18,6 +14,10 @@ interface Props {
|
|||
body: string
|
||||
}
|
||||
|
||||
const isInViewport = (el: Element): boolean => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
return rect.top <= DEFAULT_HEADER_OFFSET
|
||||
}
|
||||
const scrollToHeader = (element) => {
|
||||
window.scrollTo({
|
||||
behavior: 'smooth',
|
||||
|
@ -31,9 +31,9 @@ const scrollToHeader = (element) => {
|
|||
export const TableOfContents = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
|
||||
const [headings, setHeadings] = createSignal<Element[]>([])
|
||||
const [headings, setHeadings] = createSignal<HTMLElement[]>([])
|
||||
const [areHeadingsLoaded, setAreHeadingsLoaded] = createSignal<boolean>(false)
|
||||
|
||||
const [activeHeaderIndex, setActiveHeaderIndex] = createSignal<number>(-1)
|
||||
const [isVisible, setIsVisible] = createSignal<boolean>(props.variant === 'article')
|
||||
const toggleIsVisible = () => {
|
||||
setIsVisible((visible) => !visible)
|
||||
|
@ -42,15 +42,20 @@ export const TableOfContents = (props: Props) => {
|
|||
setIsVisible(isDesktop())
|
||||
|
||||
const updateHeadings = () => {
|
||||
const { parentSelector } = props
|
||||
|
||||
setHeadings(
|
||||
// eslint-disable-next-line unicorn/prefer-spread
|
||||
setHeadings(Array.from(document.querySelector(parentSelector).querySelectorAll('h2, h3, h4')))
|
||||
Array.from(document.querySelector(props.parentSelector).querySelectorAll<HTMLElement>('h2, h3, h4'))
|
||||
)
|
||||
setAreHeadingsLoaded(true)
|
||||
}
|
||||
|
||||
const debouncedUpdateHeadings = debounce(updateHeadings, 500)
|
||||
|
||||
const updateActiveHeader = throttle(() => {
|
||||
const newActiveIndex = headings().findLastIndex((heading) => isInViewport(heading))
|
||||
setActiveHeaderIndex(newActiveIndex)
|
||||
}, 50)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.body,
|
||||
|
@ -58,6 +63,11 @@ export const TableOfContents = (props: Props) => {
|
|||
)
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('scroll', updateActiveHeader)
|
||||
onCleanup(() => window.removeEventListener('scroll', updateActiveHeader))
|
||||
})
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={
|
||||
|
@ -77,17 +87,17 @@ export const TableOfContents = (props: Props) => {
|
|||
</div>
|
||||
<ul class={styles.TableOfContentsHeadingsList}>
|
||||
<For each={headings()}>
|
||||
{(h) => (
|
||||
{(h, index) => (
|
||||
<li>
|
||||
<button
|
||||
class={clsx(styles.TableOfContentsHeadingsItem, {
|
||||
[styles.TableOfContentsHeadingsItemH3]: h.nodeName === 'H3',
|
||||
[styles.TableOfContentsHeadingsItemH4]: h.nodeName === 'H4'
|
||||
[styles.TableOfContentsHeadingsItemH4]: h.nodeName === 'H4',
|
||||
[styles.active]: index() === activeHeaderIndex()
|
||||
})}
|
||||
innerHTML={h.textContent}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
scrollToHeader(h)
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.picture {
|
||||
display: block;
|
||||
width: 40px;
|
||||
|
|
|
@ -8,8 +8,12 @@
|
|||
}
|
||||
|
||||
.groupControls {
|
||||
margin-bottom: 6rem !important;
|
||||
margin-bottom: 2rem !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 { useLocalize } from '../../../context/localize'
|
||||
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.feedFilter {
|
||||
margin: 0.2em 0 4.8rem;
|
||||
margin-bottom: 4.8rem;
|
||||
margin-top: 0.2em;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
margin-right: 4rem !important;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.button {
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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 {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
|
|
|
@ -2,10 +2,11 @@ import type { JSX } from 'solid-js'
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './Button.module.scss'
|
||||
|
||||
export type ButtonVariant = 'primary' | 'secondary' | 'bordered' | 'inline' | 'light' | 'outline' | 'danger'
|
||||
type Props = {
|
||||
value: string | JSX.Element
|
||||
size?: 'S' | 'M' | 'L'
|
||||
variant?: 'primary' | 'secondary' | 'bordered' | 'inline' | 'light' | 'outline'
|
||||
variant?: ButtonVariant
|
||||
type?: 'submit' | 'button'
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
|
|
|
@ -2,11 +2,14 @@ import { createContext, createSignal, useContext } from 'solid-js'
|
|||
import type { Accessor, JSX } from 'solid-js'
|
||||
|
||||
import { hideModal, showModal } from '../stores/ui'
|
||||
import { ButtonVariant } from '../components/_shared/Button/Button'
|
||||
|
||||
type ConfirmMessage = {
|
||||
confirmBody?: string | JSX.Element
|
||||
confirmButtonLabel?: string
|
||||
confirmButtonVariant?: ButtonVariant
|
||||
declineButtonLabel?: string
|
||||
declineButtonVariant?: ButtonVariant
|
||||
}
|
||||
|
||||
type ConfirmContextType = {
|
||||
|
@ -15,7 +18,9 @@ type ConfirmContextType = {
|
|||
showConfirm: (message?: {
|
||||
confirmBody?: ConfirmMessage['confirmBody']
|
||||
confirmButtonLabel?: ConfirmMessage['confirmButtonLabel']
|
||||
confirmButtonVariant?: ConfirmMessage['confirmButtonVariant']
|
||||
declineButtonLabel?: ConfirmMessage['declineButtonLabel']
|
||||
declineButtonVariant?: ConfirmMessage['declineButtonVariant']
|
||||
}) => Promise<boolean>
|
||||
resolveConfirm: (value: boolean) => void
|
||||
}
|
||||
|
@ -36,13 +41,17 @@ export const ConfirmProvider = (props: { children: JSX.Element }) => {
|
|||
message: {
|
||||
confirmBody?: ConfirmMessage['confirmBody']
|
||||
confirmButtonLabel?: ConfirmMessage['confirmButtonLabel']
|
||||
confirmButtonVariant?: ConfirmMessage['confirmButtonVariant']
|
||||
declineButtonLabel?: ConfirmMessage['declineButtonLabel']
|
||||
declineButtonVariant?: ConfirmMessage['declineButtonVariant']
|
||||
} = {}
|
||||
): Promise<boolean> => {
|
||||
const messageToShow = {
|
||||
confirmBody: message.confirmBody,
|
||||
confirmButtonLabel: message.confirmButtonLabel,
|
||||
declineButtonLabel: message.declineButtonLabel
|
||||
confirmButtonVariant: message.confirmButtonVariant,
|
||||
declineButtonLabel: message.declineButtonLabel,
|
||||
declineButtonVariant: message.declineButtonVariant
|
||||
}
|
||||
|
||||
setConfirmMessage(messageToShow)
|
||||
|
|
|
@ -16,6 +16,7 @@ type NotificationsContextType = {
|
|||
sortedNotifications: Accessor<Notification[]>
|
||||
actions: {
|
||||
showNotificationsPanel: () => void
|
||||
hideNotificationsPanel: () => void
|
||||
markNotificationAsRead: (notification: Notification) => Promise<void>
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +81,11 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
|||
setIsNotificationsPanelOpen(true)
|
||||
}
|
||||
|
||||
const actions = { showNotificationsPanel, markNotificationAsRead }
|
||||
const hideNotificationsPanel = () => {
|
||||
setIsNotificationsPanelOpen(false)
|
||||
}
|
||||
|
||||
const actions = { showNotificationsPanel, hideNotificationsPanel, markNotificationAsRead }
|
||||
|
||||
const value: NotificationsContextType = {
|
||||
notificationEntities,
|
||||
|
|
|
@ -11,18 +11,9 @@ import { setPageLoadManagerPromise } from '../utils/pageLoadManager'
|
|||
|
||||
export const ArticlePage = (props: PageProps) => {
|
||||
const shouts = props.article ? [props.article] : []
|
||||
const { page } = useRouter()
|
||||
|
||||
const slug = createMemo(() => {
|
||||
const { page: getPage } = useRouter()
|
||||
|
||||
const page = getPage()
|
||||
|
||||
if (page.route !== 'article') {
|
||||
throw new Error('ts guard')
|
||||
}
|
||||
|
||||
return page.params.slug
|
||||
})
|
||||
const slug = createMemo(() => page().params['slug'] as string)
|
||||
|
||||
const { articleEntities } = useArticlesStore({
|
||||
shouts
|
||||
|
|
|
@ -289,12 +289,16 @@ button {
|
|||
|
||||
.button--light {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
background-color: #f6f6f6;
|
||||
border-radius: 0.8rem;
|
||||
color: #000;
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
height: auto;
|
||||
padding: 0.6rem 1.2rem 0.6rem 1rem;
|
||||
|
||||
&:hover {
|
||||
background: #e9e9ee;
|
||||
}
|
||||
}
|
||||
|
||||
.button--subscribe-topic {
|
||||
|
@ -589,18 +593,22 @@ figure {
|
|||
|
||||
.view-switcher {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-weight: 500;
|
||||
list-style: none;
|
||||
margin: 3.6rem 0 0;
|
||||
padding: 0;
|
||||
margin: 3.6rem -1rem 0;
|
||||
overflow: auto;
|
||||
padding: 0 1rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-right: 2rem;
|
||||
margin-bottom: 0.6em;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
|
|
|
@ -13,6 +13,6 @@
|
|||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"lib": ["ES2021", "dom"]
|
||||
"lib": ["es2023", "dom"]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user