no-actions-context-value
This commit is contained in:
parent
6283558b00
commit
333d7c5961
|
@ -34,6 +34,7 @@
|
|||
"rules": {
|
||||
"recommended": true,
|
||||
"complexity": {
|
||||
"all": true,
|
||||
"noForEach": "off",
|
||||
"useOptionalChain": "warn",
|
||||
"useLiteralKeys": "off"
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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>()
|
||||
|
||||
|
|
|
@ -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 === 'Дискурс') {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -94,9 +94,7 @@ const SimplifiedEditor = (props: Props) => {
|
|||
current: null,
|
||||
}
|
||||
|
||||
const {
|
||||
actions: { setEditor },
|
||||
} = useEditorContext()
|
||||
const { setEditor } = useEditorContext()
|
||||
|
||||
const ImageFigure = Figure.extend({
|
||||
name: 'capturedImage',
|
||||
|
|
|
@ -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}>
|
||||
<>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 })}
|
||||
>
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>(() => {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
export const isCyrillic = (s: string): boolean => {
|
||||
const cyrillicRegex = /[\u0400-\u04FF]/ // Range for Cyrillic characters
|
||||
|
||||
return cyrillicRegex.test(s)
|
||||
}
|
|
@ -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
39
src/utils/translate.ts
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user