diff --git a/src/components/Article/AudioPlayer/PlayerHeader.tsx b/src/components/Article/AudioPlayer/PlayerHeader.tsx index 11d430e1..3894af1f 100644 --- a/src/components/Article/AudioPlayer/PlayerHeader.tsx +++ b/src/components/Article/AudioPlayer/PlayerHeader.tsx @@ -1,7 +1,7 @@ import { clsx } from 'clsx' import { Show, createSignal } from 'solid-js' import { Icon } from '~/components/_shared/Icon' -import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler' +import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler' import { MediaItem } from '~/types/mediaitem' import styles from './AudioPlayer.module.scss' diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index 7aeb71bf..8546e4ba 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -5,7 +5,7 @@ import { useLocalize } from '~/context/localize' import { useReactions } from '~/context/reactions' import { useSession } from '~/context/session' import { Author, Reaction, ReactionKind, ReactionSort } from '~/graphql/schema/core.gen' -import { byCreated, byStat } from '~/lib/sortby' +import { byCreated, byStat } from '~/lib/sortBy' import { Button } from '../_shared/Button' import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated' diff --git a/src/components/Author/AuthorBadge/AuthorBadge.tsx b/src/components/Author/AuthorBadge/AuthorBadge.tsx index 4388e992..51ffcf82 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.tsx +++ b/src/components/Author/AuthorBadge/AuthorBadge.tsx @@ -13,7 +13,7 @@ import { useSession } from '~/context/session' import { Author, FollowingEntity } from '~/graphql/schema/core.gen' import { isCyrillic } from '~/intl/translate' import { translit } from '~/intl/translit' -import { mediaMatches } from '~/utils/media-query' +import { mediaMatches } from '~/lib/mediaQuery' import { Userpic } from '../Userpic' import styles from './AuthorBadge.module.scss' diff --git a/src/components/Editor/AudioUploader/AudioUploader.tsx b/src/components/Editor/AudioUploader/AudioUploader.tsx index c5e0e4f1..0e9c46e2 100644 --- a/src/components/Editor/AudioUploader/AudioUploader.tsx +++ b/src/components/Editor/AudioUploader/AudioUploader.tsx @@ -3,8 +3,8 @@ import { Show } from 'solid-js' import { isServer } from 'solid-js/web' import { DropArea } from '~/components/_shared/DropArea' import { useLocalize } from '~/context/localize' +import { composeMediaItems } from '~/lib/composeMediaItems' import { MediaItem } from '~/types/mediaitem' -import { composeMediaItems } from '~/utils/composeMediaItems' import { AudioPlayer } from '../../Article/AudioPlayer' import styles from './AudioUploader.module.scss' diff --git a/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx index 72dd43e5..a86bd1ef 100644 --- a/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx +++ b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx @@ -5,8 +5,8 @@ import { renderUploadedImage } from '~/components/Editor/renderUploadedImage' import { Icon } from '~/components/_shared/Icon' import { useLocalize } from '~/context/localize' import { useUI } from '~/context/ui' +import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler' import { UploadedFile } from '~/types/upload' -import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler' import { Modal } from '../../Nav/Modal' import { InlineForm } from '../InlineForm' import { UploadModalContent } from '../UploadModalContent' diff --git a/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx b/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx index b409b6dd..96b6472c 100644 --- a/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx +++ b/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx @@ -2,7 +2,7 @@ import { Editor } from '@tiptap/core' import { createEditorTransaction } from 'solid-tiptap' import { useLocalize } from '~/context/localize' -import { validateUrl } from '~/utils/validateUrl' +import { validateUrl } from '~/utils/validate' import { InlineForm } from '../InlineForm' type Props = { diff --git a/src/components/Editor/Panel/Panel.tsx b/src/components/Editor/Panel/Panel.tsx index ca6d60ec..6427487a 100644 --- a/src/components/Editor/Panel/Panel.tsx +++ b/src/components/Editor/Panel/Panel.tsx @@ -9,8 +9,8 @@ import { Icon } from '~/components/_shared/Icon' import { useEditorContext } from '~/context/editor' import { useLocalize } from '~/context/localize' import { useUI } from '~/context/ui' -import { useEscKeyDownHandler } from '~/utils/useEscKeyDownHandler' -import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler' +import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler' +import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler' import styles from './Panel.module.scss' const typograf = new Typograf({ locale: ['ru', 'en-US'] }) diff --git a/src/components/Editor/UploadModalContent/UploadModalContent.tsx b/src/components/Editor/UploadModalContent/UploadModalContent.tsx index e6ff8f01..31ddc7fe 100644 --- a/src/components/Editor/UploadModalContent/UploadModalContent.tsx +++ b/src/components/Editor/UploadModalContent/UploadModalContent.tsx @@ -10,7 +10,6 @@ import { useSession } from '~/context/session' import { useUI } from '~/context/ui' import { handleImageUpload } from '~/lib/handleImageUpload' import { UploadedFile } from '~/types/upload' -import { verifyImg } from '~/utils/verifyImg' import { InlineForm } from '../InlineForm' import styles from './UploadModalContent.module.scss' @@ -19,6 +18,9 @@ type Props = { onClose: (image?: UploadedFile) => void } +const verify = (url: string) => + fetch(url, { method: 'HEAD' }).then((res) => res.headers.get('Content-Type')?.startsWith('image')) + export const UploadModalContent = (props: Props) => { const { t } = useLocalize() const { hideModal } = useUI() @@ -87,7 +89,7 @@ export const UploadModalContent = (props: Props) => { } const handleValidate = async (value: string) => { - const validationResult = await verifyImg(value) + const validationResult = await verify(value) if (!validationResult) { return t('Invalid image URL') } diff --git a/src/components/Editor/VideoUploader/VideoUploader.tsx b/src/components/Editor/VideoUploader/VideoUploader.tsx index 1413c0c4..6f4c9a69 100644 --- a/src/components/Editor/VideoUploader/VideoUploader.tsx +++ b/src/components/Editor/VideoUploader/VideoUploader.tsx @@ -5,8 +5,8 @@ import { For, Show, createSignal } from 'solid-js' import { VideoPlayer } from '~/components/_shared/VideoPlayer' import { useLocalize } from '~/context/localize' import { useSnackbar } from '~/context/ui' -import { composeMediaItems } from '~/utils/composeMediaItems' -import { validateUrl } from '~/utils/validateUrl' +import { composeMediaItems } from '~/lib/composeMediaItems' +import { validateUrl } from '~/utils/validate' import { MediaItem } from '~/types/mediaitem' import styles from './VideoUploader.module.scss' diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index b6cff004..7c6efc10 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -4,7 +4,7 @@ import { JSX, Show, createSignal } from 'solid-js' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { useSnackbar, useUI } from '~/context/ui' -import { validateEmail } from '~/utils/validateEmail' +import { validateEmail } from '~/utils/validate' import { AuthModalHeader } from './AuthModalHeader' import { PasswordField } from './PasswordField' diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index a326d335..00ae273b 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -6,7 +6,7 @@ import { useSearchParams } from '@solidjs/router' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { useUI } from '~/context/ui' -import { validateEmail } from '~/utils/validateEmail' +import { validateEmail } from '~/utils/validate' import { AuthModalHeader } from './AuthModalHeader' import { PasswordField } from './PasswordField' import { SocialProviders } from './SocialProviders' diff --git a/src/components/Nav/AuthModal/SendResetLinkForm.tsx b/src/components/Nav/AuthModal/SendResetLinkForm.tsx index 076bcb1d..f7ef038b 100644 --- a/src/components/Nav/AuthModal/SendResetLinkForm.tsx +++ b/src/components/Nav/AuthModal/SendResetLinkForm.tsx @@ -3,7 +3,7 @@ import { JSX, Show, createSignal, onMount } from 'solid-js' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' -import { validateEmail } from '~/utils/validateEmail' +import { validateEmail } from '~/utils/validate' import { email, setEmail } from './sharedLogic' import { useSearchParams } from '@solidjs/router' diff --git a/src/components/Nav/AuthModal/index.tsx b/src/components/Nav/AuthModal/index.tsx index 13cee4ea..b0b118c1 100644 --- a/src/components/Nav/AuthModal/index.tsx +++ b/src/components/Nav/AuthModal/index.tsx @@ -4,7 +4,7 @@ import { Dynamic } from 'solid-js/web' import { useLocalize } from '~/context/localize' import { AuthModalSource, useUI } from '~/context/ui' -import { isMobile } from '~/utils/media-query' +import { isMobile } from '~/lib/mediaQuery' import { ChangePasswordForm } from './ChangePasswordForm' import { EmailConfirm } from './EmailConfirm' import { LoginForm } from './LoginForm' diff --git a/src/components/Nav/Modal/Modal.tsx b/src/components/Nav/Modal/Modal.tsx index 73319dba..666d5cfd 100644 --- a/src/components/Nav/Modal/Modal.tsx +++ b/src/components/Nav/Modal/Modal.tsx @@ -4,8 +4,8 @@ import type { JSX } from 'solid-js' import { Show } from 'solid-js' import { Icon } from '~/components/_shared/Icon' import { useUI } from '~/context/ui' -import { isPortrait } from '~/utils/media-query' -import { useEscKeyDownHandler } from '~/utils/useEscKeyDownHandler' +import { isPortrait } from '~/lib/mediaQuery' +import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler' import styles from './Modal.module.scss' interface Props { diff --git a/src/components/Nav/SearchModal/SearchModal.tsx b/src/components/Nav/SearchModal/SearchModal.tsx index 40a30726..2c815076 100644 --- a/src/components/Nav/SearchModal/SearchModal.tsx +++ b/src/components/Nav/SearchModal/SearchModal.tsx @@ -7,7 +7,7 @@ import { Button } from '~/components/_shared/Button' import { Icon } from '~/components/_shared/Icon' import { useFeed } from '~/context/feed' import { useLocalize } from '~/context/localize' -import { byScore } from '~/lib/sortby' +import { byScore } from '~/lib/sortBy' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' import { FEED_PAGE_SIZE } from '../../Views/Feed/Feed' diff --git a/src/components/NotificationsPanel/NotificationsPanel.tsx b/src/components/NotificationsPanel/NotificationsPanel.tsx index 7409dd2d..fcdd1dff 100644 --- a/src/components/NotificationsPanel/NotificationsPanel.tsx +++ b/src/components/NotificationsPanel/NotificationsPanel.tsx @@ -5,8 +5,8 @@ import { throttle } from 'throttle-debounce' import { useLocalize } from '~/context/localize' import { PAGE_SIZE, useNotifications } from '~/context/notifications' import { useSession } from '~/context/session' -import { useEscKeyDownHandler } from '~/utils/useEscKeyDownHandler' -import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler' +import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler' +import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler' import { Button } from '../_shared/Button' import { Icon } from '../_shared/Icon' diff --git a/src/components/TableOfContents/TableOfContents.tsx b/src/components/TableOfContents/TableOfContents.tsx index 0373e715..8fb7617a 100644 --- a/src/components/TableOfContents/TableOfContents.tsx +++ b/src/components/TableOfContents/TableOfContents.tsx @@ -4,7 +4,7 @@ import { debounce, throttle } from 'throttle-debounce' import { useLocalize } from '~/context/localize' import { DEFAULT_HEADER_OFFSET } from '~/context/ui' -import { isDesktop } from '~/utils/media-query' +import { isDesktop } from '~/lib/mediaQuery' import { Icon } from '../_shared/Icon' import styles from './TableOfContents.module.scss' diff --git a/src/components/Topic/TopicBadge/TopicBadge.tsx b/src/components/Topic/TopicBadge/TopicBadge.tsx index 3811d284..5a70e406 100644 --- a/src/components/Topic/TopicBadge/TopicBadge.tsx +++ b/src/components/Topic/TopicBadge/TopicBadge.tsx @@ -7,8 +7,8 @@ import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { FollowingEntity, Topic } from '~/graphql/schema/core.gen' import { getImageUrl } from '~/lib/getImageUrl' +import { mediaMatches } from '~/lib/mediaQuery' import { capitalize } from '~/utils/capitalize' -import { mediaMatches } from '~/utils/media-query' import styles from './TopicBadge.module.scss' type Props = { diff --git a/src/components/Views/AllAuthors/AllAuthors.tsx b/src/components/Views/AllAuthors/AllAuthors.tsx index bb2d736c..6ac67afe 100644 --- a/src/components/Views/AllAuthors/AllAuthors.tsx +++ b/src/components/Views/AllAuthors/AllAuthors.tsx @@ -11,7 +11,7 @@ import { useLocalize } from '~/context/localize' import type { Author } from '~/graphql/schema/core.gen' import { authorLetterReduce, translateAuthor } from '~/intl/translate' import { dummyFilter } from '~/lib/dummyFilter' -import { byFirstChar, byStat } from '~/lib/sortby' +import { byFirstChar, byStat } from '~/lib/sortBy' import { scrollHandler } from '~/utils/scroll' import styles from './AllAuthors.module.scss' import stylesAuthorList from './AuthorsList.module.scss' diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index f270dc39..e9c6aecf 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -13,9 +13,9 @@ import loadShoutsQuery from '~/graphql/query/core/articles-load-by' import getAuthorFollowersQuery from '~/graphql/query/core/author-followers' import getAuthorFollowsQuery from '~/graphql/query/core/author-follows' import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen' -import { byCreated } from '~/lib/sortby' +import { byCreated } from '~/lib/sortBy' +import { paginate } from '~/utils/paginate' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' -import { splitToPages } from '~/utils/splitToPages' import stylesArticle from '../../Article/Article.module.scss' import { Comment } from '../../Article/Comment' import { AuthorCard } from '../../Author/AuthorCard' @@ -59,7 +59,7 @@ export const AuthorView = (props: AuthorViewProps) => { // derivatives const me = createMemo(() => session()?.user?.app_data?.profile as Author) const pages = createMemo(() => - splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) + paginate(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) ) // fx diff --git a/src/components/Views/EditView/EditSettingsView.tsx b/src/components/Views/EditView/EditSettingsView.tsx index 7a557bed..52ea6494 100644 --- a/src/components/Views/EditView/EditSettingsView.tsx +++ b/src/components/Views/EditView/EditSettingsView.tsx @@ -10,8 +10,8 @@ import { useGraphQL } from '~/context/graphql' import { useLocalize } from '~/context/localize' import getMyShoutQuery from '~/graphql/query/core/article-my' import type { Shout, Topic } from '~/graphql/schema/core.gen' +import { isDesktop } from '~/lib/mediaQuery' import { clone } from '~/utils/clone' -import { isDesktop } from '~/utils/media-query' import { Panel } from '../../Editor' import { AutoSaveNotice } from '../../Editor/AutoSaveNotice' import { Modal } from '../../Nav/Modal' diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index 07bdc909..7858d51c 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -26,10 +26,10 @@ import getMyShoutQuery from '~/graphql/query/core/article-my' import type { Shout, Topic } from '~/graphql/schema/core.gen' import { slugify } from '~/intl/translit' import { getImageUrl } from '~/lib/getImageUrl' +import { isDesktop } from '~/lib/mediaQuery' import { LayoutType } from '~/types/common' import { MediaItem } from '~/types/mediaitem' import { clone } from '~/utils/clone' -import { isDesktop } from '~/utils/media-query' import { Editor, Panel } from '../../Editor' import { AudioUploader } from '../../Editor/AudioUploader' import { AutoSaveNotice } from '../../Editor/AutoSaveNotice' diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index 3924cbd7..745b8a63 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -12,7 +12,7 @@ import getShoutsQuery from '~/graphql/query/core/articles-load-by' import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top' import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' import { LayoutType } from '~/types/common' -import { getUnixtime } from '~/utils/getServerDate' +import { getUnixtime } from '~/utils/date' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' import { ArticleCard } from '../../Feed/ArticleCard' import styles from './Expo.module.scss' diff --git a/src/components/Views/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx index 9b0046dd..1cc0ca38 100644 --- a/src/components/Views/Feed/Feed.tsx +++ b/src/components/Views/Feed/Feed.tsx @@ -16,7 +16,7 @@ import { useTopics } from '~/context/topics' import { useUI } from '~/context/ui' import { loadUnratedShouts } from '~/graphql/api/private' import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen' -import { byCreated } from '~/lib/sortby' +import { byCreated } from '~/lib/sortBy' import { FeedSearchParams } from '~/routes/feed/(feed)' import { CommentDate } from '../../Article/CommentDate' import { getShareUrl } from '../../Article/SharePopup' diff --git a/src/components/Views/Home.tsx b/src/components/Views/Home.tsx index 251a3e99..d3674dca 100644 --- a/src/components/Views/Home.tsx +++ b/src/components/Views/Home.tsx @@ -6,7 +6,7 @@ import { loadShouts } from '~/graphql/api/public' import { Author, Shout, Topic } from '~/graphql/schema/core.gen' import { SHOUTS_PER_PAGE } from '~/routes/(main)' import { capitalize } from '~/utils/capitalize' -import { splitToPages } from '~/utils/splitToPages' +import { paginate } from '~/utils/paginate' import Banner from '../Discours/Banner' import Hero from '../Discours/Hero' import { Beside } from '../Feed/Beside' @@ -62,11 +62,7 @@ export const HomeView = (props: HomeViewProps) => { ) const pages = createMemo(() => - splitToPages( - props.featuredShouts || [], - SHOUTS_PER_PAGE + CLIENT_LOAD_ARTICLES_COUNT, - LOAD_MORE_PAGE_SIZE - ) + paginate(props.featuredShouts || [], SHOUTS_PER_PAGE + CLIENT_LOAD_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) ) return ( diff --git a/src/components/Views/Profile/ProfileSecurity.tsx b/src/components/Views/Profile/ProfileSecurity.tsx index e07407c4..7a6345a2 100644 --- a/src/components/Views/Profile/ProfileSecurity.tsx +++ b/src/components/Views/Profile/ProfileSecurity.tsx @@ -10,7 +10,7 @@ import { Loading } from '~/components/_shared/Loading' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { DEFAULT_HEADER_OFFSET, useSnackbar, useUI } from '~/context/ui' -import { validateEmail } from '~/utils/validateEmail' +import { validateEmail } from '~/utils/validate' import styles from './Settings.module.scss' type FormField = 'oldPassword' | 'newPassword' | 'newPasswordConfirm' | 'email' diff --git a/src/components/Views/Profile/ProfileSettings.tsx b/src/components/Views/Profile/ProfileSettings.tsx index 6179ac2a..d986e420 100644 --- a/src/components/Views/Profile/ProfileSettings.tsx +++ b/src/components/Views/Profile/ProfileSettings.tsx @@ -23,7 +23,7 @@ import { getImageUrl } from '~/lib/getImageUrl' import { handleImageUpload } from '~/lib/handleImageUpload' import { profileSocialLinks } from '~/lib/profileSocialLinks' import { clone } from '~/utils/clone' -import { validateUrl } from '~/utils/validateUrl' +import { validateUrl } from '~/utils/validate' import { Modal } from '../../Nav/Modal' import { ProfileSettingsNavigation } from '../../Nav/ProfileSettingsNavigation' import { Button } from '../../_shared/Button' @@ -188,6 +188,14 @@ export const ProfileSettings = () => { updateFormField('links', link, true) } + const slugUpdate = (ev: InputEvent) => { + const input = (ev.target || ev.currentTarget) as HTMLInputElement + const value = input.value + const newValue = value.startsWith('@') || value.startsWith('!') ? value.substring(1) : value + input.value = newValue + updateFormField('slug', newValue) + } + return ( 0 && isFormInitialized()} fallback={}> <> @@ -293,7 +301,7 @@ export const ProfileSettings = () => {

{t('Address on Discours')}

- +
{ id="user-address" data-lpignore="true" autocomplete="one-time-code2" - onInput={(event) => updateFormField('slug', event.currentTarget.value)} + onInput={slugUpdate} value={form.slug || ''} ref={(el) => (slugInputRef = el)} class="nolabel" diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index 141e130d..023f5d79 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -144,7 +144,12 @@ export const PublishSettings = (props: Props) => { const handleSaveDraft = () => { saveShout({ ...props.form, ...settingsForm }) } - + const removeSpecial = (ev: InputEvent) => { + const input = ev.target as HTMLInputElement + const value = input.value + const newValue = value.startsWith('@') || value.startsWith('!') ? value.substring(1) : value + input.value = newValue + } return (
@@ -235,7 +240,7 @@ export const PublishSettings = (props: Props) => {

{t('Slug')}

- +
diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index f46a4654..102b1bf7 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -8,9 +8,9 @@ import { useTopics } from '~/context/topics' import { loadAuthors, loadFollowersByTopic, loadShouts } from '~/graphql/api/public' import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' import { SHOUTS_PER_PAGE } from '~/routes/(main)' -import { getUnixtime } from '~/utils/getServerDate' +import { getUnixtime } from '~/utils/date' +import { paginate } from '~/utils/paginate' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' -import { splitToPages } from '~/utils/splitToPages' import styles from '../../styles/Topic.module.scss' import { Beside } from '../Feed/Beside' import { Row1 } from '../Feed/Row1' @@ -140,7 +140,7 @@ export const TopicView = (props: Props) => { }) */ const pages = createMemo(() => - splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) + paginate(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) ) return (
diff --git a/src/components/_shared/DropArea/DropArea.tsx b/src/components/_shared/DropArea/DropArea.tsx index 33f4114a..e334d3de 100644 --- a/src/components/_shared/DropArea/DropArea.tsx +++ b/src/components/_shared/DropArea/DropArea.tsx @@ -6,7 +6,7 @@ import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { handleFileUpload } from '~/lib/handleFileUpload' import { handleImageUpload } from '~/lib/handleImageUpload' -import { validateFiles } from '~/utils/validateFile' +import { validateUploads } from '~/lib/validateUploads' import styles from './DropArea.module.scss' @@ -62,7 +62,7 @@ export const DropArea = (props: Props) => { setDropAreaError(t('Many files, choose only one')) return } - const isValid = validateFiles(props.fileType, selectedFiles) + const isValid = validateUploads(props.fileType, selectedFiles) if (isValid) { await runUpload(selectedFiles) } else { diff --git a/src/components/_shared/DropdownSelect/DropdownSelect.tsx b/src/components/_shared/DropdownSelect/DropdownSelect.tsx index fe972703..af39bd93 100644 --- a/src/components/_shared/DropdownSelect/DropdownSelect.tsx +++ b/src/components/_shared/DropdownSelect/DropdownSelect.tsx @@ -1,7 +1,7 @@ import { clsx } from 'clsx' import { For, Show, createSignal } from 'solid-js' -import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler' +import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler' import styles from './DropdownSelect.module.scss' diff --git a/src/components/_shared/Lightbox/Lightbox.tsx b/src/components/_shared/Lightbox/Lightbox.tsx index f1c0532e..b543e257 100644 --- a/src/components/_shared/Lightbox/Lightbox.tsx +++ b/src/components/_shared/Lightbox/Lightbox.tsx @@ -2,7 +2,7 @@ import { clsx } from 'clsx' import { Show, createEffect, createMemo, createSignal, on, onCleanup } from 'solid-js' import { getImageUrl } from '~/lib/getImageUrl' -import { useEscKeyDownHandler } from '~/utils/useEscKeyDownHandler' +import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler' import { Icon } from '../Icon' import styles from './Lightbox.module.scss' diff --git a/src/components/_shared/Newsletter/Newsletter.tsx b/src/components/_shared/Newsletter/Newsletter.tsx index 91176da6..9f52b951 100644 --- a/src/components/_shared/Newsletter/Newsletter.tsx +++ b/src/components/_shared/Newsletter/Newsletter.tsx @@ -2,7 +2,7 @@ import { JSX, Show, createSignal } from 'solid-js' import { useLocalize } from '~/context/localize' import { useSnackbar } from '~/context/ui' -import { validateEmail } from '~/utils/validateEmail' +import { validateEmail } from '~/utils/validate' import { Button } from '../Button' import { Icon } from '../Icon' diff --git a/src/components/_shared/Popup/Popup.tsx b/src/components/_shared/Popup/Popup.tsx index 81537575..0418b4ce 100644 --- a/src/components/_shared/Popup/Popup.tsx +++ b/src/components/_shared/Popup/Popup.tsx @@ -1,7 +1,7 @@ import { clsx } from 'clsx' import { JSX, Show, createEffect, createSignal } from 'solid-js' -import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler' +import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler' import styles from './Popup.module.scss' diff --git a/src/components/_shared/SolidSwiper/EditorSwiper.tsx b/src/components/_shared/SolidSwiper/EditorSwiper.tsx index fec1edbc..26d2062d 100644 --- a/src/components/_shared/SolidSwiper/EditorSwiper.tsx +++ b/src/components/_shared/SolidSwiper/EditorSwiper.tsx @@ -6,10 +6,10 @@ import { Manipulation, Navigation, Pagination } from 'swiper/modules' import { useLocalize } from '~/context/localize' import { useSnackbar } from '~/context/ui' +import { composeMediaItems } from '~/lib/composeMediaItems' import { getImageUrl } from '~/lib/getImageUrl' import { handleImageUpload } from '~/lib/handleImageUpload' -import { composeMediaItems } from '~/utils/composeMediaItems' -import { validateFiles } from '~/utils/validateFile' +import { validateUploads } from '~/lib/validateUploads' import { DropArea } from '../DropArea' import { Icon } from '../Icon' import { Image } from '../Image' @@ -90,7 +90,7 @@ export const EditorSwiper = (props: Props) => { }) const initUpload = async (selectedFiles: UploadFile[]) => { - const isValid = validateFiles('image', selectedFiles) + const isValid = validateUploads('image', selectedFiles) if (!isValid) { await showSnackbar({ type: 'error', body: t('Invalid file type') }) diff --git a/src/context/authors.tsx b/src/context/authors.tsx index 34da77ff..8b0f1546 100644 --- a/src/context/authors.tsx +++ b/src/context/authors.tsx @@ -17,7 +17,7 @@ import { Shout, Topic } from '~/graphql/schema/core.gen' -import { byStat } from '~/lib/sortby' +import { byStat } from '~/lib/sortBy' import { useFeed } from './feed' const TOP_AUTHORS_COUNT = 5 diff --git a/src/context/feed.tsx b/src/context/feed.tsx index e412de80..0fdaa6b6 100644 --- a/src/context/feed.tsx +++ b/src/context/feed.tsx @@ -10,7 +10,7 @@ import { Shout, Topic } from '~/graphql/schema/core.gen' -import { byStat } from '../lib/sortby' +import { byStat } from '../lib/sortBy' import { useGraphQL } from './graphql' export const PRERENDERED_ARTICLES_COUNT = 5 diff --git a/src/context/profile.tsx b/src/context/profile.tsx index 821cf8d5..4c5b68b5 100644 --- a/src/context/profile.tsx +++ b/src/context/profile.tsx @@ -76,16 +76,21 @@ export const ProfileProvider = (props: { children: JSX.Element }) => { } }) + // TODO: validation error for `!` and `@` + const updateFormField = (fieldName: string, value: string, remove?: boolean) => { + let val = value + if (fieldName === 'slug' && value.startsWith('@')) val = value.substring(1) + if (fieldName === 'slug' && value.startsWith('!')) val = value.substring(1) if (fieldName === 'links') { setForm((prev) => { const updatedLinks = remove - ? (prev.links || []).filter((item) => item !== value) - : [...(prev.links || []), value] + ? (prev.links || []).filter((item) => item !== val) + : [...(prev.links || []), val] return { ...prev, links: updatedLinks } }) } else { - setForm((prev) => ({ ...prev, [fieldName]: value })) + setForm((prev) => ({ ...prev, [fieldName]: val })) } } diff --git a/src/context/topics.tsx b/src/context/topics.tsx index 4f60da6e..b97e0cd8 100644 --- a/src/context/topics.tsx +++ b/src/context/topics.tsx @@ -13,7 +13,7 @@ import { import { loadTopics } from '~/graphql/api/public' import { Topic } from '~/graphql/schema/core.gen' import { getRandomTopicsFromArray } from '~/lib/getRandomTopicsFromArray' -import { byTopicStatDesc } from '../lib/sortby' +import { byTopicStatDesc } from '../lib/sortBy' type TopicsContextType = { topicEntities: Accessor<{ [topicSlug: string]: Topic }> diff --git a/src/utils/composeMediaItems.ts b/src/lib/composeMediaItems.ts similarity index 100% rename from src/utils/composeMediaItems.ts rename to src/lib/composeMediaItems.ts diff --git a/src/utils/media-query.ts b/src/lib/mediaQuery.ts similarity index 100% rename from src/utils/media-query.ts rename to src/lib/mediaQuery.ts diff --git a/src/utils/useEscKeyDownHandler.ts b/src/lib/useEscKeyDownHandler.ts similarity index 100% rename from src/utils/useEscKeyDownHandler.ts rename to src/lib/useEscKeyDownHandler.ts diff --git a/src/utils/useOutsideClickHandler.ts b/src/lib/useOutsideClickHandler.ts similarity index 100% rename from src/utils/useOutsideClickHandler.ts rename to src/lib/useOutsideClickHandler.ts diff --git a/src/utils/validateFile.ts b/src/lib/validateUploads.ts similarity index 78% rename from src/utils/validateFile.ts rename to src/lib/validateUploads.ts index 8ba40887..709d2bd3 100644 --- a/src/utils/validateFile.ts +++ b/src/lib/validateUploads.ts @@ -1,9 +1,9 @@ import { UploadFile } from '@solid-primitives/upload' -export const validateFiles = (fileType: string, files: UploadFile[]): boolean => { - const imageExtensions = new Set(['jpg', 'jpeg', 'png', 'gif', 'bmp']) - const docExtensions = new Set(['doc', 'docx', 'pdf', 'txt']) +export const imageExtensions = new Set(['jpg', 'jpeg', 'png', 'gif', 'bmp']) +export const docExtensions = new Set(['doc', 'docx', 'pdf', 'txt']) +export const validateUploads = (fileType: string, files: UploadFile[]): boolean => { for (const file of files) { let isValid: boolean diff --git a/src/routes/(main).tsx b/src/routes/(main).tsx index 5e003914..e3f589a3 100644 --- a/src/routes/(main).tsx +++ b/src/routes/(main).tsx @@ -3,7 +3,7 @@ import { Show, Suspense, createEffect, createSignal, onMount } from 'solid-js' import { useTopics } from '~/context/topics' import { loadShouts, loadTopics } from '~/graphql/api/public' import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' -import { byStat } from '~/lib/sortby' +import { byStat } from '~/lib/sortBy' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' import { HomeView, HomeViewProps } from '../components/Views/Home' import { Loading } from '../components/_shared/Loading' diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 11f61ad9..00000000 --- a/src/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface CommentDefinition { - user: string - time_ago: string - content: string - comments: CommentDefinition[] -} - -export interface StoryDefinition { - id: string - points: string - url: string - title: string - domain: string - type: string - time_ago: string - user: string - comments_count: number - comments: CommentDefinition[] -} - -export interface UserDefinition { - error: string - id: string - created: string - karma: number - about: string -} - -export type StoryTypes = 'top' | 'new' | 'show' | 'ask' | 'job' diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 00000000..95aafb26 --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,2 @@ +export const getShortDate = (date: Date) => date.toISOString().slice(0, 10) // 2023-12-31 +export const getUnixtime = (date: Date) => Math.floor(date.getTime() / 1000) as number diff --git a/src/utils/getNumeralsDeclension.ts b/src/utils/getNumeralsDeclension.ts deleted file mode 100644 index dafc15c0..00000000 --- a/src/utils/getNumeralsDeclension.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Usage in tsx: {getNumeralsDeclension(NUMBER, ['яблоко', 'яблока', 'яблок'])} -export const getNumeralsDeclension = (number: number, words: string[], cases = [2, 0, 1, 1, 1, 2]) => - words[number % 100 > 4 && number % 100 < 20 ? 2 : cases[number % 10 < 5 ? number % 10 : 5]] diff --git a/src/utils/getServerDate.ts b/src/utils/getServerDate.ts deleted file mode 100644 index 39fa4223..00000000 --- a/src/utils/getServerDate.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const getServerDate = (date: Date): string => { - // 2023-12-31 - return date.toISOString().slice(0, 10) -} - -export const getUnixtime = (date: Date): number => { - return Math.floor(date.getTime() / 1000) -} diff --git a/src/utils/paginate.ts b/src/utils/paginate.ts new file mode 100644 index 00000000..e514a4f8 --- /dev/null +++ b/src/utils/paginate.ts @@ -0,0 +1,10 @@ +export function paginate(arr: T[], startIndex: number, pageSize: number): T[][] { + return arr.slice(startIndex).reduce((acc, item, index) => { + if (index % pageSize === 0) { + acc.push([]) + } + + acc?.at(-1)?.push(item) + return acc + }, [] as T[][]) +} diff --git a/src/utils/splitToPages.ts b/src/utils/splitToPages.ts deleted file mode 100644 index 3946cb35..00000000 --- a/src/utils/splitToPages.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function splitToPages(arr: T[], startIndex: number, pageSize: number): T[][] { - return arr.slice(startIndex).reduce((acc, article, index) => { - if (index % pageSize === 0) { - acc.push([]) - } - - acc?.at(-1)?.push(article) - return acc - }, [] as T[][]) -} diff --git a/src/utils/validate.ts b/src/utils/validate.ts new file mode 100644 index 00000000..8c4d14f5 --- /dev/null +++ b/src/utils/validate.ts @@ -0,0 +1,10 @@ +export const validateEmail = (email: string) => { + if (!email) return false + + return /^[\w%+.-]+@[\d.a-z-]+\.[a-z]{2,}$/i.test(email) +} + +export const validateUrl = (value: string) => { + // TODO: make it better + return value.includes('.') && !value.includes(' ') +} diff --git a/src/utils/validateEmail.ts b/src/utils/validateEmail.ts deleted file mode 100644 index e3cd2913..00000000 --- a/src/utils/validateEmail.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const validateEmail = (email: string) => { - if (!email) { - return false - } - - return /^[\w%+.-]+@[\d.a-z-]+\.[a-z]{2,}$/i.test(email) -} diff --git a/src/utils/validateUrl.ts b/src/utils/validateUrl.ts deleted file mode 100644 index 9f416965..00000000 --- a/src/utils/validateUrl.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const validateUrl = (value: string) => { - return value.includes('.') && !value.includes(' ') -} diff --git a/src/utils/verifyImg.ts b/src/utils/verifyImg.ts deleted file mode 100644 index 0c8b3642..00000000 --- a/src/utils/verifyImg.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const verifyImg = (url: string) => { - return fetch(url, { method: 'HEAD' }).then((res) => { - return res.headers.get('Content-Type')?.startsWith('image') - }) -} - -const supportedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'tiff', 'bpg'] -export const isImageExtension = (value: string) => { - return supportedExtensions.some((extension) => value.includes(extension)) -}