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": { "rules": {
"recommended": true, "recommended": true,
"complexity": { "complexity": {
"all": true,
"noForEach": "off", "noForEach": "off",
"useOptionalChain": "warn", "useOptionalChain": "warn",
"useLiteralKeys": "off" "useLiteralKeys": "off"

View File

@ -5,8 +5,7 @@ import type { Accessor, JSX } from 'solid-js'
import { createContext, createSignal, useContext } from 'solid-js' import { createContext, createSignal, useContext } from 'solid-js'
type <%= h.changeCase.pascal(name) %>ContextType = { type <%= h.changeCase.pascal(name) %>ContextType = {
actions: {
}
} }
const <%= h.changeCase.pascal(name) %>Context = createContext<<%= h.changeCase.pascal(name) %>ContextType>() 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 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> 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 [editMode, setEditMode] = createSignal(false)
const [clearEditor, setClearEditor] = createSignal(false) const [clearEditor, setClearEditor] = createSignal(false)
const { author } = useSession() const { author } = useSession()
const { createReaction, deleteReaction, updateReaction } = useReactions()
const { const { showConfirm } = useConfirm()
actions: { createReaction, deleteReaction, updateReaction }, const { showSnackbar } = useSnackbar()
} = useReactions()
const {
actions: { showConfirm },
} = useConfirm()
const {
actions: { showSnackbar },
} = useSnackbar()
const isCommentAuthor = createMemo(() => props.comment.created_by?.slug === author()?.slug) const isCommentAuthor = createMemo(() => props.comment.created_by?.slug === author()?.slug)
const comment = createMemo(() => props.comment) const comment = createMemo(() => props.comment)
const body = createMemo(() => (comment().body || '').trim()) const body = createMemo(() => (comment().body || '').trim())
const remove = async () => { const remove = async () => {
if (comment()?.id) { if (comment()?.id) {
try { try {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -83,28 +83,47 @@ const getTitleAndSubtitle = (
} }
} }
// TODO: simple fast auto translated title/substitle
return { title, subtitle } 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) => { export const ArticleCard = (props: ArticleCardProps) => {
const { t, lang, formatDate } = useLocalize() const { t, lang, formatDate } = useLocalize()
const { author } = useSession() const { author } = useSession()
const mainTopicSlug = props.article.main_topic || '' const { changeSearchParams } = useRouter()
const mainTopic = props.article.topics?.find((tpc: Topic) => tpc.slug === mainTopicSlug) const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const mainTopicTitle = const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
mainTopicSlug && lang() === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || '' 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>(() => const formattedDate = createMemo<string>(() =>
props.article.published_at ? formatDate(new Date(props.article.published_at * 1000)) : '', props.article.published_at ? formatDate(new Date(props.article.published_at * 1000)) : '',
) )
const { title, subtitle } = getTitleAndSubtitle(props.article)
const canEdit = () => const canEdit = () =>
props.article.authors?.some((a) => a && a?.slug === author()?.slug) || props.article.authors?.some((a) => a && a?.slug === author()?.slug) ||
props.article.created_by?.id === author()?.id props.article.created_by?.id === author()?.id
const { changeSearchParams } = useRouter()
const scrollToComments = (event) => { const scrollToComments = (event) => {
event.preventDefault() event.preventDefault()
openPage(router, 'article', { slug: props.article.slug }) openPage(router, 'article', { slug: props.article.slug })
@ -112,28 +131,6 @@ export const ArticleCard = (props: ArticleCardProps) => {
scrollTo: 'comments', 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 ( return (
<section <section
class={clsx(styles.shoutCard, props.settings?.additionalClass, { class={clsx(styles.shoutCard, props.settings?.additionalClass, {
@ -152,7 +149,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
[aspectRatio()]: props.withAspectRatio, [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={styles.shoutCardCoverContainer}>
<div <div
class={clsx(styles.shoutCardCover, { class={clsx(styles.shoutCardCover, {
@ -223,7 +220,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
</Show> </Show>
</a> </a>
</div> </div>
<Show when={!props.settings?.noauthor || !props.settings?.nodate}> <Show when={!(props.settings?.noauthor && props.settings?.nodate)}>
<div <div
class={clsx(styles.shoutDetails, { [styles.shoutDetailsFeedMode]: props.settings?.isFeedMode })} 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 = () => { export const ChangePasswordForm = () => {
const { searchParams, changeSearchParams } = useRouter<AuthModalSearchParams>() const { searchParams, changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize() const { t } = useLocalize()
const { const { changePassword } = useSession()
actions: { changePassword },
} = useSession()
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
const [newPassword, setNewPassword] = createSignal<string>() const [newPassword, setNewPassword] = createSignal<string>()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,9 +22,7 @@ export const TopicBadge = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { mediaMatches } = useMediaQuery() const { mediaMatches } = useMediaQuery()
const [isMobileView, setIsMobileView] = createSignal(false) const [isMobileView, setIsMobileView] = createSignal(false)
const { const { requireAuthentication } = useSession()
actions: { requireAuthentication },
} = useSession()
const { setFollowing, loading: subLoading } = useFollowing() const { setFollowing, loading: subLoading } = useFollowing()
const [followed, setFollowed] = createSignal() 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 { useLocalize } from '../../context/localize'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { loadAuthors, setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors' import { loadAuthors, setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors'
import { capitalize } from '../../utils/capitalize'
import { isCyrillic } from '../../utils/cyrillic'
import { dummyFilter } from '../../utils/dummyFilter' import { dummyFilter } from '../../utils/dummyFilter'
import { getImageUrl } from '../../utils/getImageUrl' import { getImageUrl } from '../../utils/getImageUrl'
import { translit } from '../../utils/ru2en'
import { scrollHandler } from '../../utils/scroll' import { scrollHandler } from '../../utils/scroll'
import { authorLetterReduce, translateAuthor } from '../../utils/translate'
import { AuthorBadge } from '../Author/AuthorBadge' import { AuthorBadge } from '../Author/AuthorBadge'
import { Loading } from '../_shared/Loading' import { Loading } from '../_shared/Loading'
import { SearchField } from '../_shared/SearchField' import { SearchField } from '../_shared/SearchField'
@ -77,36 +75,10 @@ export const AllAuthorsView = (props: Props) => {
shouts: loadMoreByShouts, shouts: loadMoreByShouts,
followers: loadMoreByFollowers, followers: loadMoreByFollowers,
}[searchParams().by]() }[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[] }>(() => { const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
return sortedAuthors().reduce( return sortedAuthors().reduce(
(acc, author) => { (acc, author) => authorLetterReduce(acc, author, lang()),
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
},
{} as { [letter: string]: Author[] }, {} as { [letter: string]: Author[] },
) )
}) })
@ -207,7 +179,7 @@ export const AllAuthorsView = (props: Props) => {
{(author) => ( {(author) => (
<div class={clsx(styles.topic, 'topic col-sm-12 col-md-8')}> <div class={clsx(styles.topic, 'topic col-sm-12 col-md-8')}>
<div class="topic-title"> <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}> <Show when={author.stat}>
<span class={styles.articlesCounter}>{author.stat.shouts}</span> <span class={styles.articlesCounter}>{author.stat.shouts}</span>
</Show> </Show>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,11 +9,9 @@ import { useSession } from './session'
type ProfileFormContextType = { type ProfileFormContextType = {
form: ProfileInput form: ProfileInput
actions: { setForm: (profile: ProfileInput) => void
setForm: (profile: ProfileInput) => void submit: (profile: ProfileInput) => Promise<void>
submit: (profile: ProfileInput) => Promise<void> updateFormField: (fieldName: string, value: string, remove?: boolean) => void
updateFormField: (fieldName: string, value: string, remove?: boolean) => void
}
} }
const ProfileFormContext = createContext<ProfileFormContextType>() const ProfileFormContext = createContext<ProfileFormContextType>()
@ -73,11 +71,9 @@ export const ProfileFormProvider = (props: { children: JSX.Element }) => {
const value: ProfileFormContextType = { const value: ProfileFormContextType = {
form, form,
actions: { submit,
submit, updateFormField,
updateFormField, setForm,
setForm,
},
} }
return <ProfileFormContext.Provider value={value}>{props.children}</ProfileFormContext.Provider> 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 = { type ReactionsContextType = {
reactionEntities: Record<number, Reaction> reactionEntities: Record<number, Reaction>
actions: { loadReactionsBy: ({
loadReactionsBy: ({ by,
by, limit,
limit, offset,
offset, }: {
}: { by: ReactionBy
by: ReactionBy limit?: number
limit?: number offset?: number
offset?: number }) => Promise<Reaction[]>
}) => Promise<Reaction[]> createReaction: (reaction: ReactionInput) => Promise<void>
createReaction: (reaction: ReactionInput) => Promise<void> updateReaction: (id: number, reaction: ReactionInput) => Promise<void>
updateReaction: (id: number, reaction: ReactionInput) => Promise<void> deleteReaction: (id: number) => Promise<void>
deleteReaction: (id: number) => Promise<void>
}
} }
const ReactionsContext = createContext<ReactionsContextType>() const ReactionsContext = createContext<ReactionsContextType>()
@ -104,7 +102,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
deleteReaction, deleteReaction,
} }
const value: ReactionsContextType = { reactionEntities, actions } const value: ReactionsContextType = { reactionEntities, ...actions }
return <ReactionsContext.Provider value={value}>{props.children}</ReactionsContext.Provider> return <ReactionsContext.Provider value={value}>{props.children}</ReactionsContext.Provider>
} }

View File

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

View File

@ -14,13 +14,11 @@ type SnackbarMessage = {
type SnackbarContextType = { type SnackbarContextType = {
snackbarMessage: Accessor<SnackbarMessage> snackbarMessage: Accessor<SnackbarMessage>
actions: { showSnackbar: (message: {
showSnackbar: (message: { type?: SnackbarMessage['type']
type?: SnackbarMessage['type'] body: SnackbarMessage['body']
body: SnackbarMessage['body'] duration?: SnackbarMessage['duration']
duration?: SnackbarMessage['duration'] }) => Promise<void>
}) => Promise<void>
}
} }
const SnackbarContext = createContext<SnackbarContextType>() const SnackbarContext = createContext<SnackbarContextType>()
@ -71,11 +69,7 @@ export const SnackbarProvider = (props: { children: JSX.Element }) => {
return currentCheckMessagesPromise return currentCheckMessagesPromise
} }
const actions = { const value: SnackbarContextType = { snackbarMessage, showSnackbar }
showSnackbar,
}
const value: SnackbarContextType = { snackbarMessage, actions }
return <SnackbarContext.Provider value={value}>{props.children}</SnackbarContext.Provider> 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()]) const article = createMemo<Shout>(() => articleEntities()[slug()])
onMount(async () => { onMount(async () => {
if (!article() || !article().body) { if (!article()?.body) {
const loadShoutPromise = loadShout(slug()) const loadShoutPromise = loadShout(slug())
setPageLoadManagerPromise(loadShoutPromise) setPageLoadManagerPromise(loadShoutPromise)
await 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 widthString = options.width ? options.width.toString() : ''
const heightString = options.height ? options.height.toString() : '' const heightString = options.height ? options.height.toString() : ''
if ((!widthString && !heightString) || options.noSizeUrlPart) { if (!(widthString || heightString) || options.noSizeUrlPart) {
return '' 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
}