no-actions-context-value
Some checks failed
deploy / test (push) Failing after 59s
deploy / Update templates on Mailgun (push) Has been skipped

This commit is contained in:
Untone 2024-02-04 20:40:15 +03:00
parent 6283558b00
commit 333d7c5961
57 changed files with 250 additions and 425 deletions

View File

@ -34,6 +34,7 @@
"rules": {
"recommended": true,
"complexity": {
"all": true,
"noForEach": "off",
"useOptionalChain": "warn",
"useLiteralKeys": "off"

View File

@ -5,8 +5,7 @@ import type { Accessor, JSX } from 'solid-js'
import { createContext, createSignal, useContext } from 'solid-js'
type <%= h.changeCase.pascal(name) %>ContextType = {
actions: {
}
}
const <%= h.changeCase.pascal(name) %>Context = createContext<<%= h.changeCase.pascal(name) %>ContextType>()
@ -19,7 +18,7 @@ export const <%= h.changeCase.pascal(name) %>Provider = (props: { children: JSX.
const actions = {
}
const value: <%= h.changeCase.pascal(name) %>ContextType = { actions }
const value: <%= h.changeCase.pascal(name) %>ContextType = { ...actions }
return <<%= h.changeCase.pascal(name) %>Context.Provider value={value}>{props.children}</<%= h.changeCase.pascal(name) %>Context.Provider>
}

View File

@ -39,23 +39,14 @@ export const Comment = (props: Props) => {
const [editMode, setEditMode] = createSignal(false)
const [clearEditor, setClearEditor] = createSignal(false)
const { author } = useSession()
const {
actions: { createReaction, deleteReaction, updateReaction },
} = useReactions()
const {
actions: { showConfirm },
} = useConfirm()
const {
actions: { showSnackbar },
} = useSnackbar()
const { createReaction, deleteReaction, updateReaction } = useReactions()
const { showConfirm } = useConfirm()
const { showSnackbar } = useSnackbar()
const isCommentAuthor = createMemo(() => props.comment.created_by?.slug === author()?.slug)
const comment = createMemo(() => props.comment)
const body = createMemo(() => (comment().body || '').trim())
const remove = async () => {
if (comment()?.id) {
try {

View File

@ -19,13 +19,8 @@ type Props = {
export const CommentRatingControl = (props: Props) => {
const { t } = useLocalize()
const { author } = useSession()
const {
actions: { showSnackbar },
} = useSnackbar()
const {
reactionEntities,
actions: { createReaction, deleteReaction, loadReactionsBy },
} = useReactions()
const { showSnackbar } = useSnackbar()
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions()
const checkReaction = (reactionKind: ReactionKind) =>
Object.values(reactionEntities).some(
@ -86,7 +81,7 @@ export const CommentRatingControl = (props: Props) => {
<div class={styles.commentRating}>
<button
role="button"
disabled={!canVote() || !author()}
disabled={!(canVote() && author())}
onClick={() => handleRatingChange(true)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, {
[styles.voted]: isUpvoted(),
@ -112,7 +107,7 @@ export const CommentRatingControl = (props: Props) => {
</Popup>
<button
role="button"
disabled={!canVote() || !author()}
disabled={!(canVote() && author())}
onClick={() => handleRatingChange(false)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, {
[styles.voted]: isDownvoted(),

View File

@ -49,11 +49,7 @@ export const CommentsTree = (props: Props) => {
const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
const [clearEditor, setClearEditor] = createSignal(false)
const [clickedReplyId, setClickedReplyId] = createSignal<number>()
const {
reactionEntities,
actions: { createReaction },
} = useReactions()
const { reactionEntities, createReaction } = useReactions()
const comments = createMemo(() =>
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT'),

View File

@ -15,9 +15,9 @@ import { MediaItem } from '../../pages/types'
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
import { showModal } from '../../stores/ui'
import { capitalize } from '../../utils/capitalize'
import { isCyrillic } from '../../utils/cyrillic'
import { getImageUrl, getOpenGraphImageUrl } from '../../utils/getImageUrl'
import { getDescription, getKeywords } from '../../utils/meta'
import { isCyrillic } from '../../utils/translate'
import { AuthorBadge } from '../Author/AuthorBadge'
import { CardTopic } from '../Feed/CardTopic'
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
@ -69,18 +69,16 @@ const scrollTo = (el: HTMLElement) => {
const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
export const FullArticle = (props: Props) => {
const { searchParams, changeSearchParams } = useRouter<ArticlePageSearchParams>()
const { loadReactionsBy } = useReactions()
const [selectedImage, setSelectedImage] = createSignal('')
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const { t, formatDate, lang } = useLocalize()
const {
author,
isAuthenticated,
actions: { requireAuthentication },
} = useSession()
const { author, isAuthenticated, requireAuthentication } = useSession()
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000)))
const canEdit = () => props.article.authors?.some((a) => Boolean(a) && a?.slug === author()?.slug)
const mainTopic = createMemo(() => {
const main_topic_slug = props.article.topics.length > 0 ? props.article.main_topic : null
@ -92,8 +90,6 @@ export const FullArticle = (props: Props) => {
return props.article.topics[0]
})
const canEdit = () => props.article.authors?.some((a) => Boolean(a) && a?.slug === author()?.slug)
const handleBookmarkButtonClick = (ev) => {
requireAuthentication(() => {
// TODO: implement bookmark clicked
@ -153,8 +149,6 @@ export const FullArticle = (props: Props) => {
scrollTo(commentsRef.current)
}
const { searchParams, changeSearchParams } = useRouter<ArticlePageSearchParams>()
createEffect(() => {
if (props.scrollToComments) {
scrollToComments()
@ -184,10 +178,6 @@ export const FullArticle = (props: Props) => {
}
})
const {
actions: { loadReactionsBy },
} = useReactions()
const clickHandlers = []
const documentClickHandlers = []
@ -292,7 +282,7 @@ export const FullArticle = (props: Props) => {
// Check iframes size
const articleContainer: { current: HTMLElement } = { current: null }
const updateIframeSizes = () => {
if (!articleContainer?.current || !props.article.body) return
if (!(articleContainer?.current && props.article.body)) return
const iframes = articleContainer?.current?.querySelectorAll('iframe')
if (!iframes) return
const containerWidth = articleContainer.current?.offsetWidth

View File

@ -19,16 +19,8 @@ interface ShoutRatingControlProps {
export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
const { t } = useLocalize()
const {
author,
actions: { requireAuthentication },
} = useSession()
const {
reactionEntities,
actions: { createReaction, deleteReaction, loadReactionsBy },
} = useReactions()
const { author, requireAuthentication } = useSession()
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions()
const [isLoading, setIsLoading] = createSignal(false)
const checkReaction = (reactionKind: ReactionKind) =>

View File

@ -8,8 +8,8 @@ import { useMediaQuery } from '../../../context/mediaQuery'
import { useSession } from '../../../context/session'
import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
import { router, useRouter } from '../../../stores/router'
import { isCyrillic } from '../../../utils/cyrillic'
import { translit } from '../../../utils/ru2en'
import { isCyrillic } from '../../../utils/translate'
import { Button } from '../../_shared/Button'
import { CheckButton } from '../../_shared/CheckButton'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
@ -36,11 +36,7 @@ type Props = {
}
export const AuthorBadge = (props: Props) => {
const { mediaMatches } = useMediaQuery()
const {
author,
actions: { requireAuthentication },
} = useSession()
const { author, requireAuthentication } = useSession()
const [isMobileView, setIsMobileView] = createSignal(false)
const [isFollowed, setIsFollowed] = createSignal<boolean>()

View File

@ -10,9 +10,9 @@ import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
import { SubscriptionFilter } from '../../../pages/types'
import { router, useRouter } from '../../../stores/router'
import { isCyrillic } from '../../../utils/cyrillic'
import { isAuthor } from '../../../utils/isAuthor'
import { translit } from '../../../utils/ru2en'
import { isCyrillic } from '../../../utils/translate'
import { SharePopup, getShareUrl } from '../../Article/SharePopup'
import { Modal } from '../../Nav/Modal'
import { TopicBadge } from '../../Topic/TopicBadge'
@ -31,17 +31,14 @@ type Props = {
}
export const AuthorCard = (props: Props) => {
const { t, lang } = useLocalize()
const {
author,
isSessionLoaded,
actions: { requireAuthentication },
} = useSession()
const { author, isSessionLoaded, requireAuthentication } = useSession()
const { setFollowing } = useFollowing()
const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([])
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
const [isFollowed, setIsFollowed] = createSignal<boolean>()
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
const isSubscribed = () => props.followers?.some((entity) => entity.id === author()?.id)
createEffect(
on(
() => props.followers,
@ -52,8 +49,6 @@ export const AuthorCard = (props: Props) => {
),
)
const { setFollowing } = useFollowing()
const name = createMemo(() => {
if (lang() !== 'ru' && isCyrillic(props.author.name)) {
if (props.author.name === 'Дискурс') {

View File

@ -4,8 +4,8 @@ import { createMemo } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Author } from '../../../graphql/schema/core.gen'
import { capitalize } from '../../../utils/capitalize'
import { isCyrillic } from '../../../utils/cyrillic'
import { translit } from '../../../utils/ru2en'
import { isCyrillic } from '../../../utils/translate'
import { Userpic } from '../Userpic'
import styles from './AhtorLink.module.scss'

View File

@ -27,9 +27,7 @@ export const Donate = () => {
const [showingPayment, setShowingPayment] = createSignal<boolean>()
const [period, setPeriod] = createSignal(monthly)
const [amount, setAmount] = createSignal(0)
const {
actions: { showSnackbar },
} = useSnackbar()
const { showSnackbar } = useSnackbar()
const initiated = () => {
try {

View File

@ -20,13 +20,8 @@ type Props = {
export const Draft = (props: Props) => {
const { t, formatDate } = useLocalize()
const {
actions: { showConfirm },
} = useConfirm()
const {
actions: { showSnackbar },
} = useSnackbar()
const { showConfirm } = useConfirm()
const { showSnackbar } = useSnackbar()
const handlePublishLinkClick = (e) => {
e.preventDefault()

View File

@ -80,9 +80,7 @@ export const Editor = (props: Props) => {
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
const {
actions: { showSnackbar },
} = useSnackbar()
const { showSnackbar } = useSnackbar()
const docName = `shout-${props.shoutId}`
@ -337,10 +335,7 @@ export const Editor = (props: Props) => {
content: initialContent ?? null,
}))
const {
actions: { countWords, setEditor },
} = useEditorContext()
const { countWords, setEditor } = useEditorContext()
setEditor(editor)
const html = useEditorHTML(() => editor())

View File

@ -24,18 +24,10 @@ type Props = {
export const Panel = (props: Props) => {
const { t } = useLocalize()
const {
isEditorPanelVisible,
wordCounter,
editorRef,
form,
actions: { toggleEditorPanel, saveShout, publishShout },
} = useEditorContext()
const containerRef: { current: HTMLElement } = {
current: null,
}
const { isEditorPanelVisible, wordCounter, editorRef, form, toggleEditorPanel, saveShout, publishShout } =
useEditorContext()
const containerRef: { current: HTMLElement } = { current: null }
const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false)
const [isTypographyFixed, setIsTypographyFixed] = createSignal(false)

View File

@ -94,9 +94,7 @@ const SimplifiedEditor = (props: Props) => {
current: null,
}
const {
actions: { setEditor },
} = useEditorContext()
const { setEditor } = useEditorContext()
const ImageFigure = Figure.extend({
name: 'capturedImage',

View File

@ -160,7 +160,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
submitButtonText={t('Send')}
/>
</Match>
<Match when={!linkEditorOpen() || !footnoteEditorOpen()}>
<Match when={!(linkEditorOpen() && footnoteEditorOpen())}>
<>
<Show when={!props.isCommonMarkup}>
<>

View File

@ -24,9 +24,7 @@ export const VideoUploader = (props: Props) => {
const [error, setError] = createSignal<string>()
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
const {
actions: { showSnackbar },
} = useSnackbar()
const { showSnackbar } = useSnackbar()
const urlInput: {
current: HTMLInputElement

View File

@ -83,28 +83,47 @@ const getTitleAndSubtitle = (
}
}
// TODO: simple fast auto translated title/substitle
return { title, subtitle }
}
const getMainTopicTitle = (article: Shout, lng: string) => {
const mainTopicSlug = article.main_topic || ''
const mainTopic = article.topics?.find((tpc: Topic) => tpc.slug === mainTopicSlug)
const mainTopicTitle =
mainTopicSlug && lng === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || ''
return [mainTopicTitle, mainTopicSlug]
}
const LAYOUT_ASPECT = {
music: styles.aspectRatio1x1,
literature: styles.aspectRatio16x9,
video: styles.aspectRatio16x9,
image: styles.aspectRatio4x3,
}
export const ArticleCard = (props: ArticleCardProps) => {
const { t, lang, formatDate } = useLocalize()
const { author } = useSession()
const mainTopicSlug = props.article.main_topic || ''
const mainTopic = props.article.topics?.find((tpc: Topic) => tpc.slug === mainTopicSlug)
const mainTopicTitle =
mainTopicSlug && lang() === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || ''
const { changeSearchParams } = useRouter()
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true)
const description = getDescription(props.article.body)
const aspectRatio = () => LAYOUT_ASPECT[props.article.layout]
const [mainTopicTitle, mainTopicSlug] = getMainTopicTitle(props.article, lang())
const { title, subtitle } = getTitleAndSubtitle(props.article)
const formattedDate = createMemo<string>(() =>
props.article.published_at ? formatDate(new Date(props.article.published_at * 1000)) : '',
)
const { title, subtitle } = getTitleAndSubtitle(props.article)
const canEdit = () =>
props.article.authors?.some((a) => a && a?.slug === author()?.slug) ||
props.article.created_by?.id === author()?.id
const { changeSearchParams } = useRouter()
const scrollToComments = (event) => {
event.preventDefault()
openPage(router, 'article', { slug: props.article.slug })
@ -112,28 +131,6 @@ export const ArticleCard = (props: ArticleCardProps) => {
scrollTo: 'comments',
})
}
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true)
const description = getDescription(props.article.body)
const aspectRatio = () => {
switch (props.article.layout) {
case 'music': {
return styles.aspectRatio1x1
}
case 'image': {
return styles.aspectRatio4x3
}
case 'video':
case 'literature': {
return styles.aspectRatio16x9
}
}
}
return (
<section
class={clsx(styles.shoutCard, props.settings?.additionalClass, {
@ -152,7 +149,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
[aspectRatio()]: props.withAspectRatio,
})}
>
<Show when={!props.settings?.noimage && !props.settings?.isFeedMode}>
<Show when={!(props.settings?.noimage || props.settings?.isFeedMode)}>
<div class={styles.shoutCardCoverContainer}>
<div
class={clsx(styles.shoutCardCover, {
@ -223,7 +220,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
</Show>
</a>
</div>
<Show when={!props.settings?.noauthor || !props.settings?.nodate}>
<Show when={!(props.settings?.noauthor && props.settings?.nodate)}>
<div
class={clsx(styles.shoutDetails, { [styles.shoutDetailsFeedMode]: props.settings?.isFeedMode })}
>

View File

@ -21,9 +21,7 @@ type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
export const ChangePasswordForm = () => {
const { searchParams, changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize()
const {
actions: { changePassword },
} = useSession()
const { changePassword } = useSession()
const [isSubmitting, setIsSubmitting] = createSignal(false)
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
const [newPassword, setNewPassword] = createSignal<string>()

View File

@ -25,9 +25,7 @@ export const ForgotPasswordForm = () => {
setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
setEmail(newEmail.toLowerCase())
}
const {
actions: { forgotPassword },
} = useSession()
const { forgotPassword } = useSession()
const [isSubmitting, setIsSubmitting] = createSignal(false)
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
const [isUserNotFound, setIsUserNotFound] = createSignal(false)

View File

@ -25,28 +25,18 @@ type FormFields = {
type ValidationErrors = Partial<Record<keyof FormFields, string>>
export const LoginForm = () => {
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize()
const [submitError, setSubmitError] = createSignal('')
const [isSubmitting, setIsSubmitting] = createSignal(false)
const [password, setPassword] = createSignal('')
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
// TODO: better solution for interactive error messages
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
const [isLinkSent, setIsLinkSent] = createSignal(false)
const authFormRef: { current: HTMLFormElement } = { current: null }
const {
actions: { showSnackbar },
} = useSnackbar()
const {
actions: { signIn },
} = useSession()
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const [password, setPassword] = createSignal('')
const { showSnackbar } = useSnackbar()
const { signIn } = useSession()
const handleEmailInput = (newEmail: string) => {
setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
@ -67,7 +57,7 @@ export const LoginForm = () => {
changeSearchParams({ mode: 'forgot-password' })
// NOTE: temporary solution, needs logic in authorizer
/* FIXME:
const { actions: { authorizer } } = useSession()
const { authorizer } = useSession()
const result = await authorizer().verifyEmail({ token })
if (!result) setSubmitError('cant sign send link')
*/
@ -82,15 +72,15 @@ export const LoginForm = () => {
const newValidationErrors: ValidationErrors = {}
if (!email()) {
newValidationErrors.email = t('Please enter email')
} else if (!validateEmail(email())) {
newValidationErrors.email = t('Invalid email')
const validateAndSetError = (field, message) => {
if (!field()) {
newValidationErrors[field.name] = t(message)
}
}
if (!password()) {
newValidationErrors.password = t('Please enter password')
}
validateAndSetError(email, 'Please enter email')
validateAndSetError(() => validateEmail(email()), 'Invalid email')
validateAndSetError(password, 'Please enter password')
if (Object.keys(newValidationErrors).length > 0) {
setValidationErrors(newValidationErrors)

View File

@ -34,9 +34,7 @@ export const RegisterForm = () => {
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize()
const { emailChecks } = useEmailChecks()
const {
actions: { signUp },
} = useSession()
const { signUp } = useSession()
const [submitError, setSubmitError] = createSignal('')
const [fullName, setFullName] = createSignal('')
const [password, setPassword] = createSignal('')
@ -67,11 +65,9 @@ export const RegisterForm = () => {
}
setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
setValidationErrors(({ fullName: _notNeeded, ...rest }) => rest)
setSubmitError('')
const newValidationErrors: ValidationErrors = {}
const cleanName = fullName().trim()
const cleanEmail = email().trim()
@ -90,9 +86,7 @@ export const RegisterForm = () => {
}
setValidationErrors(newValidationErrors)
const emailCheckResult = await checkEmail(cleanEmail)
const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult
if (!isValid) {

View File

@ -10,9 +10,7 @@ export const PROVIDERS = ['facebook', 'google', 'github'] // 'vk' | 'telegram'
export const SocialProviders = () => {
const { t } = useLocalize()
const {
actions: { oauth },
} = useSession()
const { oauth } = useSession()
return (
<div class={styles.container}>

View File

@ -29,7 +29,6 @@ export const AuthModal = () => {
const rootRef: { current: HTMLDivElement } = { current: null }
const { t } = useLocalize()
const { searchParams } = useRouter<AuthModalSearchParams>()
const { source } = searchParams()
const mode = createMemo<AuthModalMode>(() => {

View File

@ -6,11 +6,7 @@ import styles from './ConfirmModal.module.scss'
export const ConfirmModal = () => {
const { t } = useLocalize()
const {
confirmMessage,
actions: { resolveConfirm },
} = useConfirm()
const { confirmMessage, resolveConfirm } = useConfirm()
return (
<div class={styles.confirmModal}>

View File

@ -46,12 +46,8 @@ export const Header = (props: Props) => {
const { t, lang } = useLocalize()
const { modal } = useModalStore()
const { page } = useRouter()
const {
actions: { requireAuthentication },
} = useSession()
const { requireAuthentication } = useSession()
const { searchParams } = useRouter<HeaderSearchParams>()
const [randomTopics, setRandomTopics] = createSignal([])
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
const [getIsScrolled, setIsScrolled] = createSignal(false)
@ -62,9 +58,7 @@ export const Header = (props: Props) => {
const [isTopicsVisible, setIsTopicsVisible] = createSignal(false)
const [isZineVisible, setIsZineVisible] = createSignal(false)
const [isFeedVisible, setIsFeedVisible] = createSignal(false)
const toggleFixed = () => {
setFixed(!fixed())
}
const toggleFixed = () => setFixed(!fixed())
const tag = (topic: Topic) =>
/[ЁА-яё]/.test(topic.title || '') && lang() !== 'ru' ? topic.slug : topic.title
@ -82,7 +76,7 @@ export const Header = (props: Props) => {
document.body.classList.toggle('fixed', fixed() || modal() !== null)
document.body.classList.toggle(styles.fixed, fixed() && !modal())
if (!fixed() && !modal()) {
if (!(fixed() || modal())) {
mainContent.style.marginTop = ''
window.scrollTo(0, windowScrollTop)
}

View File

@ -33,15 +33,8 @@ export const HeaderAuth = (props: Props) => {
const { t } = useLocalize()
const { page } = useRouter()
const { session, author, isAuthenticated, isSessionLoaded } = useSession()
const {
unreadNotificationsCount,
actions: { showNotificationsPanel },
} = useNotifications()
const {
form,
actions: { toggleEditorPanel, saveShout, publishShout },
} = useEditorContext()
const { unreadNotificationsCount, showNotificationsPanel } = useNotifications()
const { form, toggleEditorPanel, saveShout, publishShout } = useEditorContext()
const handleBellIconClick = (event: Event) => {
event.preventDefault()

View File

@ -12,11 +12,7 @@ import styles from '../_shared/Popup/Popup.module.scss'
type ProfilePopupProps = Omit<PopupProps, 'children'>
export const ProfilePopup = (props: ProfilePopupProps) => {
const {
author,
actions: { signOut },
} = useSession()
const { author, signOut } = useSession()
const { t } = useLocalize()
return (

View File

@ -45,9 +45,7 @@ const reactionsCaption = (threadId: string) =>
export const NotificationGroup = (props: NotificationGroupProps) => {
const { t, formatTime, formatDate } = useLocalize()
const { changeSearchParams } = useRouter<ArticlePageSearchParams>()
const {
actions: { hideNotificationsPanel, markSeenThread },
} = useNotifications()
const { hideNotificationsPanel, markSeenThread } = useNotifications()
const handleClick = (threadId: string) => {
props.onClick()

View File

@ -46,7 +46,6 @@ const isEarlier = (date: Date) => {
export const NotificationsPanel = (props: Props) => {
const [isLoading, setIsLoading] = createSignal(false)
const { isAuthenticated } = useSession()
const { t } = useLocalize()
const {
@ -55,7 +54,8 @@ export const NotificationsPanel = (props: Props) => {
unreadNotificationsCount,
loadedNotificationsCount,
totalNotificationsCount,
actions: { loadNotificationsGrouped, markSeenAll },
loadNotificationsGrouped,
markSeenAll,
} = useNotifications()
const handleHide = () => {
props.onClose()

View File

@ -31,7 +31,6 @@ const GrowingTextarea = lazy(() => import('../../components/_shared/GrowingTexta
export const ProfileSettings = () => {
const { t } = useLocalize()
const [prevForm, setPrevForm] = createStore({})
const [isFormInitialized, setIsFormInitialized] = createSignal(false)
const [social, setSocial] = createSignal([])
@ -44,21 +43,10 @@ export const ProfileSettings = () => {
const [hostname, setHostname] = createSignal<string | null>(null)
const [slugError, setSlugError] = createSignal<string>()
const [nameError, setNameError] = createSignal<string>()
const {
form,
actions: { submit, updateFormField, setForm },
} = useProfileForm()
const {
actions: { showSnackbar },
} = useSnackbar()
const {
actions: { loadAuthor },
} = useSession()
const {
actions: { showConfirm },
} = useConfirm()
const { form, submit, updateFormField, setForm } = useProfileForm()
const { showSnackbar } = useSnackbar()
const { loadAuthor } = useSession()
const { showConfirm } = useConfirm()
createEffect(() => {
if (Object.keys(form).length > 0 && !isFormInitialized()) {

View File

@ -37,10 +37,7 @@ export const TopicCard = (props: TopicProps) => {
const title = createMemo(() =>
capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''),
)
const {
author,
actions: { requireAuthentication },
} = useSession()
const { author, requireAuthentication } = useSession()
const { setFollowing, loading: subLoading } = useFollowing()
const [followed, setFollowed] = createSignal()
@ -84,8 +81,11 @@ export const TopicCard = (props: TopicProps) => {
classList={{
[clsx('col-sm-18 col-md-24 col-lg-14 col-xl-15', styles.topicDetails)]: props.isNarrow,
[clsx('col-24 col-sm-17 col-md-18', styles.topicDetails)]: props.compact,
[clsx('col-sm-17 col-md-18', styles.topicDetails)]:
!props.subscribeButtonBottom && !props.isNarrow && !props.compact,
[clsx('col-sm-17 col-md-18', styles.topicDetails)]: !(
props.subscribeButtonBottom ||
props.isNarrow ||
props.compact
),
}}
>
<Show when={title() && !props.isCardMode}>
@ -120,7 +120,7 @@ export const TopicCard = (props: TopicProps) => {
classList={{
'col-sm-6 col-md-24 col-lg-10 col-xl-9': props.isNarrow,
'col-24 col-sm-7 col-md-6': props.compact,
'col-sm-7 col-md-6': !props.subscribeButtonBottom && !props.isNarrow && !props.compact,
'col-sm-7 col-md-6': !(props.subscribeButtonBottom || props.isNarrow || props.compact),
}}
>
<ShowOnlyOnClient>

View File

@ -18,9 +18,7 @@ type Props = {
export const FullTopic = (props: Props) => {
const { t } = useLocalize()
const { subscriptions, setFollowing } = useFollowing()
const {
actions: { requireAuthentication },
} = useSession()
const { requireAuthentication } = useSession()
const [followed, setFollowed] = createSignal()
createEffect(() => {

View File

@ -22,9 +22,7 @@ export const TopicBadge = (props: Props) => {
const { t, lang } = useLocalize()
const { mediaMatches } = useMediaQuery()
const [isMobileView, setIsMobileView] = createSignal(false)
const {
actions: { requireAuthentication },
} = useSession()
const { requireAuthentication } = useSession()
const { setFollowing, loading: subLoading } = useFollowing()
const [followed, setFollowed] = createSignal()

View File

@ -7,12 +7,10 @@ import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useRouter } from '../../stores/router'
import { loadAuthors, setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors'
import { capitalize } from '../../utils/capitalize'
import { isCyrillic } from '../../utils/cyrillic'
import { dummyFilter } from '../../utils/dummyFilter'
import { getImageUrl } from '../../utils/getImageUrl'
import { translit } from '../../utils/ru2en'
import { scrollHandler } from '../../utils/scroll'
import { authorLetterReduce, translateAuthor } from '../../utils/translate'
import { AuthorBadge } from '../Author/AuthorBadge'
import { Loading } from '../_shared/Loading'
import { SearchField } from '../_shared/SearchField'
@ -77,36 +75,10 @@ export const AllAuthorsView = (props: Props) => {
shouts: loadMoreByShouts,
followers: loadMoreByFollowers,
}[searchParams().by]()
const translate = (author: Author) =>
lang() === 'en' && isCyrillic(author.name)
? capitalize(translit(author.name.replace(/ё/, 'e').replace(/ь/, '')).replace(/-/, ' '), true)
: author.name
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
return sortedAuthors().reduce(
(acc, author) => {
let letter = ''
if (!letter && author && author.name) {
const name = translate(author)
.replace(/[^\dA-zА-я]/, ' ')
.trim()
const nameParts = name.trim().split(' ')
const found = nameParts.filter(Boolean).pop()
if (found && found.length > 0) {
letter = found[0].toUpperCase()
}
}
if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '@'
if (/[^A-z]/.test(letter) && lang() === 'en') letter = '@'
if (!acc[letter]) acc[letter] = []
author.name = translate(author)
acc[letter].push(author)
// Sort authors within each letter group alphabetically by name
acc[letter].sort((a, b) => a.name.localeCompare(b.name))
return acc
},
(acc, author) => authorLetterReduce(acc, author, lang()),
{} as { [letter: string]: Author[] },
)
})
@ -207,7 +179,7 @@ export const AllAuthorsView = (props: Props) => {
{(author) => (
<div class={clsx(styles.topic, 'topic col-sm-12 col-md-8')}>
<div class="topic-title">
<a href={`/author/${author.slug}`}>{translate(author)}</a>
<a href={`/author/${author.slug}`}>{translateAuthor(author, lang())}</a>
<Show when={author.stat}>
<span class={styles.articlesCounter}>{author.stat.shouts}</span>
</Show>

View File

@ -26,9 +26,7 @@ export const DraftsView = () => {
if (isSessionLoaded()) loadDrafts()
})
const {
actions: { publishShoutById, deleteShout },
} = useEditorContext()
const { publishShoutById, deleteShout } = useEditorContext()
const handleDraftDelete = async (shout: Shout) => {
const result = deleteShout(shout.id)

View File

@ -53,18 +53,19 @@ const handleScrollTopButtonClick = (e) => {
export const EditView = (props: Props) => {
const { t } = useLocalize()
const [isScrolled, setIsScrolled] = createSignal(false)
const { page } = useRouter()
const {
form,
formErrors,
actions: { setForm, setFormErrors, saveDraft, saveDraftToLocalStorage, getDraftFromLocalStorage },
setForm,
setFormErrors,
saveDraft,
saveDraftToLocalStorage,
getDraftFromLocalStorage,
} = useEditorContext()
const shoutTopics = props.shout.topics || []
const draft = getDraftFromLocalStorage(props.shout.id)
if (draft) {
setForm(draft)
} else {

View File

@ -117,7 +117,8 @@ export const FeedView = (props: Props) => {
const { page, searchParams, changeSearchParams } = useRouter<FeedSearchParams>()
const [isLoading, setIsLoading] = createSignal(false)
const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false)
const { session } = useSession()
const { loadReactionsBy } = useReactions()
const { sortedArticles } = useArticlesStore()
const { topTopics } = useTopicsStore()
const { topAuthors } = useTopAuthorsStore()
@ -141,10 +142,6 @@ export const FeedView = (props: Props) => {
return visibility
})
const { session } = useSession()
const {
actions: { loadReactionsBy },
} = useReactions()
const loadUnratedArticles = async () => {
if (session()) {
const result = await apiClient.getUnratedShouts(UNRATED_ARTICLES_COUNT)

View File

@ -44,11 +44,7 @@ type Props = {
export const InboxView = (props: Props) => {
const { t } = useLocalize()
const {
chats,
messages,
actions: { loadChats, getMessages, sendMessage, createChat },
} = useInbox()
const { chats, messages, loadChats, getMessages, sendMessage, createChat } = useInbox()
const [recipients, setRecipients] = createSignal<Author[]>(props.authors)
const [sortByGroup, setSortByGroup] = createSignal(false)
const [sortByPerToPer, setSortByPerToPer] = createSignal(false)
@ -197,7 +193,7 @@ export const InboxView = (props: Props) => {
<Show when={chatsToShow()}>
<ul class="view-switcher">
<li class={clsx({ 'view-switcher__item--selected': !sortByPerToPer() && !sortByGroup() })}>
<li class={clsx({ 'view-switcher__item--selected': !(sortByPerToPer() || sortByGroup()) })}>
<button
onClick={() => {
setSortByPerToPer(false)

View File

@ -85,10 +85,7 @@ export const PublishSettings = (props: Props) => {
createEffect(() => setTopics(sortedTopics()))
const {
formErrors,
actions: { setForm, setFormErrors, saveShout, publishShout },
} = useEditorContext()
const { formErrors, setForm, setFormErrors, saveShout, publishShout } = useEditorContext()
const handleUploadModalContentCloseSetCover = (image: UploadedFile) => {
hideModal()

View File

@ -26,7 +26,7 @@ export const DarkModeToggle = (props: Props) => {
document.documentElement.dataset.editorDarkMode = 'false'
}
if (!editorDarkModeAttr && !editorDarkModeSelected) {
if (!(editorDarkModeAttr || editorDarkModeSelected)) {
localStorage.setItem('editorDarkMode', 'false')
document.documentElement.dataset.editorDarkMode = 'false'
}

View File

@ -40,11 +40,8 @@ export const InviteMembers = (props: Props) => {
]
const { sortedAuthors } = useAuthorsStore({ sortBy: 'name' })
const {
actions: { loadChats, createChat },
} = useInbox()
const { loadChats, createChat } = useInbox()
const [authorsToInvite, setAuthorsToInvite] = createSignal<InviteAuthor[]>()
const [searchResultAuthors, setSearchResultAuthors] = createSignal<Author[]>()
const [collectionToInvite, setCollectionToInvite] = createSignal<number[]>([])
const fetcher = async (page: number) => {

View File

@ -22,10 +22,7 @@ type Props = {
export const ShareLinks = (props: Props) => {
const { t } = useLocalize()
const [isLinkCopied, setIsLinkCopied] = createSignal(false)
const {
actions: { showSnackbar },
} = useSnackbar()
const { showSnackbar } = useSnackbar()
const [share] = createSocialShare(() => ({
title: props.title,

View File

@ -40,9 +40,7 @@ export const EditorSwiper = (props: Props) => {
const mainSwipeRef: { current: SwiperRef } = { current: null }
const thumbSwipeRef: { current: SwiperRef } = { current: null }
const {
actions: { showSnackbar },
} = useSnackbar()
const { showSnackbar } = useSnackbar()
const handleSlideDescriptionChange = (index: number, field: string, value) => {
if (props.onImageChange) {

View File

@ -17,9 +17,7 @@ export const Subscribe = (props: Props) => {
const [title, setTitle] = createSignal('')
const [email, setEmail] = createSignal('')
const [emailError, setEmailError] = createSignal<string>(null)
const {
actions: { showSnackbar },
} = useSnackbar()
const { showSnackbar } = useSnackbar()
const validate = (): boolean => {
if (!email()) {

View File

@ -15,16 +15,14 @@ type ConfirmMessage = {
type ConfirmContextType = {
confirmMessage: Accessor<ConfirmMessage>
actions: {
showConfirm: (message?: {
confirmBody?: ConfirmMessage['confirmBody']
confirmButtonLabel?: ConfirmMessage['confirmButtonLabel']
confirmButtonVariant?: ConfirmMessage['confirmButtonVariant']
declineButtonLabel?: ConfirmMessage['declineButtonLabel']
declineButtonVariant?: ConfirmMessage['declineButtonVariant']
}) => Promise<boolean>
resolveConfirm: (value: boolean) => void
}
showConfirm: (message?: {
confirmBody?: ConfirmMessage['confirmBody']
confirmButtonLabel?: ConfirmMessage['confirmButtonLabel']
confirmButtonVariant?: ConfirmMessage['confirmButtonVariant']
declineButtonLabel?: ConfirmMessage['declineButtonLabel']
declineButtonVariant?: ConfirmMessage['declineButtonVariant']
}) => Promise<boolean>
resolveConfirm: (value: boolean) => void
}
const ConfirmContext = createContext<ConfirmContextType>()
@ -73,7 +71,7 @@ export const ConfirmProvider = (props: { children: JSX.Element }) => {
resolveConfirm,
}
const value: ConfirmContextType = { confirmMessage, actions }
const value: ConfirmContextType = { confirmMessage, ...actions }
return <ConfirmContext.Provider value={value}>{props.children}</ConfirmContext.Provider>
}

View File

@ -40,20 +40,18 @@ type EditorContextType = {
form: ShoutForm
formErrors: Record<keyof ShoutForm, string>
editorRef: { current: () => Editor }
actions: {
saveShout: (form: ShoutForm) => Promise<void>
saveDraft: (form: ShoutForm) => Promise<void>
saveDraftToLocalStorage: (form: ShoutForm) => void
getDraftFromLocalStorage: (shoutId: number) => ShoutForm
publishShout: (form: ShoutForm) => Promise<void>
publishShoutById: (shoutId: number) => Promise<void>
deleteShout: (shoutId: number) => Promise<boolean>
toggleEditorPanel: () => void
countWords: (value: WordCounter) => void
setForm: SetStoreFunction<ShoutForm>
setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>>
setEditor: (editor: () => Editor) => void
}
saveShout: (form: ShoutForm) => Promise<void>
saveDraft: (form: ShoutForm) => Promise<void>
saveDraftToLocalStorage: (form: ShoutForm) => void
getDraftFromLocalStorage: (shoutId: number) => ShoutForm
publishShout: (form: ShoutForm) => Promise<void>
publishShoutById: (shoutId: number) => Promise<void>
deleteShout: (shoutId: number) => Promise<boolean>
toggleEditorPanel: () => void
countWords: (value: WordCounter) => void
setForm: SetStoreFunction<ShoutForm>
setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>>
setEditor: (editor: () => Editor) => void
}
const EditorContext = createContext<EditorContextType>()
@ -84,9 +82,7 @@ const removeDraftFromLocalStorage = (shoutId: number) => {
export const EditorProvider = (props: { children: JSX.Element }) => {
const { t } = useLocalize()
const { page } = useRouter()
const {
actions: { showSnackbar },
} = useSnackbar()
const { showSnackbar } = useSnackbar()
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
const editorRef: { current: () => Editor } = { current: null }
const [form, setForm] = createStore<ShoutForm>(null)
@ -251,7 +247,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
}
const value: EditorContextType = {
actions,
...actions,
form,
formErrors,
editorRef,

View File

@ -12,14 +12,12 @@ import { SSEMessage, useConnect } from './connect'
type InboxContextType = {
chats: Accessor<Chat[]>
messages?: Accessor<Message[]>
actions: {
createChat: (members: number[], title: string) => Promise<{ chat: Chat }>
loadChats: () => Promise<Array<Chat>>
loadRecipients: () => Array<Author>
loadMessages: (by: MessagesBy, limit: number, offset: number) => Promise<Array<Message>>
getMessages?: (chatId: string) => Promise<Array<Message>>
sendMessage?: (args: MutationCreate_MessageArgs) => void
}
createChat: (members: number[], title: string) => Promise<{ chat: Chat }>
loadChats: () => Promise<Array<Chat>>
loadRecipients: () => Array<Author>
loadMessages: (by: MessagesBy, limit: number, offset: number) => Promise<Array<Message>>
getMessages?: (chatId: string) => Promise<Array<Message>>
sendMessage?: (args: MutationCreate_MessageArgs) => void
}
const InboxContext = createContext<InboxContextType>()
@ -119,7 +117,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
sendMessage,
}
const value: InboxContextType = { chats, messages, actions }
const value: InboxContextType = { chats, messages, ...actions }
return <InboxContext.Provider value={value}>{props.children}</InboxContext.Provider>
}

View File

@ -20,14 +20,12 @@ type NotificationsContextType = {
sortedNotifications: Accessor<NotificationGroup[]>
loadedNotificationsCount: Accessor<number>
totalNotificationsCount: Accessor<number>
actions: {
showNotificationsPanel: () => void
hideNotificationsPanel: () => void
markSeen: (notification_id: number) => Promise<void>
markSeenThread: (threadId: string) => Promise<void>
markSeenAll: () => Promise<void>
loadNotificationsGrouped: (options: QueryLoad_NotificationsArgs) => Promise<NotificationGroup[]>
}
showNotificationsPanel: () => void
hideNotificationsPanel: () => void
markSeen: (notification_id: number) => Promise<void>
markSeenThread: (threadId: string) => Promise<void>
markSeenAll: () => Promise<void>
loadNotificationsGrouped: (options: QueryLoad_NotificationsArgs) => Promise<NotificationGroup[]>
}
export const PAGE_SIZE = 20
@ -130,7 +128,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
unreadNotificationsCount,
loadedNotificationsCount,
totalNotificationsCount,
actions,
...actions,
}
const handleNotificationPanelClose = () => {

View File

@ -9,11 +9,9 @@ import { useSession } from './session'
type ProfileFormContextType = {
form: ProfileInput
actions: {
setForm: (profile: ProfileInput) => void
submit: (profile: ProfileInput) => Promise<void>
updateFormField: (fieldName: string, value: string, remove?: boolean) => void
}
setForm: (profile: ProfileInput) => void
submit: (profile: ProfileInput) => Promise<void>
updateFormField: (fieldName: string, value: string, remove?: boolean) => void
}
const ProfileFormContext = createContext<ProfileFormContextType>()
@ -73,11 +71,9 @@ export const ProfileFormProvider = (props: { children: JSX.Element }) => {
const value: ProfileFormContextType = {
form,
actions: {
submit,
updateFormField,
setForm,
},
submit,
updateFormField,
setForm,
}
return <ProfileFormContext.Provider value={value}>{props.children}</ProfileFormContext.Provider>

View File

@ -8,20 +8,18 @@ import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/sc
type ReactionsContextType = {
reactionEntities: Record<number, Reaction>
actions: {
loadReactionsBy: ({
by,
limit,
offset,
}: {
by: ReactionBy
limit?: number
offset?: number
}) => Promise<Reaction[]>
createReaction: (reaction: ReactionInput) => Promise<void>
updateReaction: (id: number, reaction: ReactionInput) => Promise<void>
deleteReaction: (id: number) => Promise<void>
}
loadReactionsBy: ({
by,
limit,
offset,
}: {
by: ReactionBy
limit?: number
offset?: number
}) => Promise<Reaction[]>
createReaction: (reaction: ReactionInput) => Promise<void>
updateReaction: (id: number, reaction: ReactionInput) => Promise<void>
deleteReaction: (id: number) => Promise<void>
}
const ReactionsContext = createContext<ReactionsContextType>()
@ -104,7 +102,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
deleteReaction,
}
const value: ReactionsContextType = { reactionEntities, actions }
const value: ReactionsContextType = { reactionEntities, ...actions }
return <ReactionsContext.Provider value={value}>{props.children}</ReactionsContext.Provider>
}

View File

@ -50,27 +50,25 @@ export type SessionContextType = {
authError: Accessor<string>
isSessionLoaded: Accessor<boolean>
isAuthenticated: Accessor<boolean>
actions: {
loadSession: () => AuthToken | Promise<AuthToken>
setSession: (token: AuthToken | null) => void // setSession
loadAuthor: (info?: unknown) => Author | Promise<Author>
setAuthor: (a: Author) => void
requireAuthentication: (
callback: (() => Promise<void>) | (() => void),
modalSource: AuthModalSource,
) => void
signUp: (params: SignupInput) => Promise<{ data: AuthToken; errors: Error[] }>
signIn: (params: LoginInput) => Promise<{ data: AuthToken; errors: Error[] }>
signOut: () => Promise<void>
oauth: (provider: string) => Promise<void>
forgotPassword: (
params: ForgotPasswordInput,
) => Promise<{ data: ForgotPasswordResponse; errors: Error[] }>
changePassword: (password: string, token: string) => void
confirmEmail: (input: VerifyEmailInput) => Promise<AuthToken> // email confirm callback is in auth.discours.io
setIsSessionLoaded: (loaded: boolean) => void
authorizer: () => Authorizer
}
loadSession: () => AuthToken | Promise<AuthToken>
setSession: (token: AuthToken | null) => void // setSession
loadAuthor: (info?: unknown) => Author | Promise<Author>
setAuthor: (a: Author) => void
requireAuthentication: (
callback: (() => Promise<void>) | (() => void),
modalSource: AuthModalSource,
) => void
signUp: (params: SignupInput) => Promise<{ data: AuthToken; errors: Error[] }>
signIn: (params: LoginInput) => Promise<{ data: AuthToken; errors: Error[] }>
signOut: () => Promise<void>
oauth: (provider: string) => Promise<void>
forgotPassword: (
params: ForgotPasswordInput,
) => Promise<{ data: ForgotPasswordResponse; errors: Error[] }>
changePassword: (password: string, token: string) => void
confirmEmail: (input: VerifyEmailInput) => Promise<AuthToken> // email confirm callback is in auth.discours.io
setIsSessionLoaded: (loaded: boolean) => void
authorizer: () => Authorizer
}
const noop = () => {}
@ -86,9 +84,7 @@ export const SessionProvider = (props: {
children: JSX.Element
}) => {
const { t } = useLocalize()
const {
actions: { showSnackbar },
} = useSnackbar()
const { showSnackbar } = useSnackbar()
const { searchParams, changeSearchParams } = useRouter()
const [config, setConfig] = createSignal<ConfigType>(defaultConfig)
const authorizer = createMemo(() => new Authorizer(config()))
@ -365,7 +361,7 @@ export const SessionProvider = (props: {
session,
isSessionLoaded,
author,
actions,
...actions,
isAuthenticated,
}

View File

@ -14,13 +14,11 @@ type SnackbarMessage = {
type SnackbarContextType = {
snackbarMessage: Accessor<SnackbarMessage>
actions: {
showSnackbar: (message: {
type?: SnackbarMessage['type']
body: SnackbarMessage['body']
duration?: SnackbarMessage['duration']
}) => Promise<void>
}
showSnackbar: (message: {
type?: SnackbarMessage['type']
body: SnackbarMessage['body']
duration?: SnackbarMessage['duration']
}) => Promise<void>
}
const SnackbarContext = createContext<SnackbarContextType>()
@ -71,11 +69,7 @@ export const SnackbarProvider = (props: { children: JSX.Element }) => {
return currentCheckMessagesPromise
}
const actions = {
showSnackbar,
}
const value: SnackbarContextType = { snackbarMessage, actions }
const value: SnackbarContextType = { snackbarMessage, showSnackbar }
return <SnackbarContext.Provider value={value}>{props.children}</SnackbarContext.Provider>
}

View File

@ -25,7 +25,7 @@ export const ArticlePage = (props: PageProps) => {
const article = createMemo<Shout>(() => articleEntities()[slug()])
onMount(async () => {
if (!article() || !article().body) {
if (!article()?.body) {
const loadShoutPromise = loadShout(slug())
setPageLoadManagerPromise(loadShoutPromise)
await loadShoutPromise

View File

@ -1,5 +0,0 @@
export const isCyrillic = (s: string): boolean => {
const cyrillicRegex = /[\u0400-\u04FF]/ // Range for Cyrillic characters
return cyrillicRegex.test(s)
}

View File

@ -4,7 +4,7 @@ const getSizeUrlPart = (options: { width?: number; height?: number; noSizeUrlPar
const widthString = options.width ? options.width.toString() : ''
const heightString = options.height ? options.height.toString() : ''
if ((!widthString && !heightString) || options.noSizeUrlPart) {
if (!(widthString || heightString) || options.noSizeUrlPart) {
return ''
}

39
src/utils/translate.ts Normal file
View File

@ -0,0 +1,39 @@
import { Author } from '../graphql/schema/core.gen'
import { capitalize } from './capitalize'
import { translit } from './ru2en'
export const isCyrillic = (s: string): boolean => {
const cyrillicRegex = /[\u0400-\u04FF]/ // Range for Cyrillic characters
return cyrillicRegex.test(s)
}
export const translateAuthor = (author: Author, lng: string) =>
lng === 'en' && isCyrillic(author.name)
? capitalize(translit(author.name.replace(/ё/, 'e').replace(/ь/, '')).replace(/-/, ' '), true)
: author.name
export const authorLetterReduce = (acc, author: Author, lng: string) => {
let letter = ''
if (!letter && author && author.name) {
const name = translateAuthor(author, lng)
.replace(/[^\dA-zА-я]/, ' ')
.trim()
const nameParts = name.trim().split(' ')
const found = nameParts.filter(Boolean).pop()
if (found && found.length > 0) {
letter = found[0].toUpperCase()
}
}
if (/[^ËА-яё]/.test(letter) && lng === 'ru') letter = '@'
if (/[^A-z]/.test(letter) && lng === 'en') letter = '@'
if (!acc[letter]) acc[letter] = []
author.name = translateAuthor(author, lng)
acc[letter].push(author)
// Sort authors within each letter group alphabetically by name
acc[letter].sort((a, b) => a.name.localeCompare(b.name))
return acc
}