utils-refactored

This commit is contained in:
Untone 2024-07-13 14:42:53 +03:00
parent 95612eb7b8
commit 2d7fbc42a8
55 changed files with 103 additions and 135 deletions

View File

@ -1,7 +1,7 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { Icon } from '~/components/_shared/Icon' import { Icon } from '~/components/_shared/Icon'
import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler' import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
import { MediaItem } from '~/types/mediaitem' import { MediaItem } from '~/types/mediaitem'
import styles from './AudioPlayer.module.scss' import styles from './AudioPlayer.module.scss'

View File

@ -5,7 +5,7 @@ import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions' import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { Author, Reaction, ReactionKind, ReactionSort } from '~/graphql/schema/core.gen' 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 { Button } from '../_shared/Button'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated' import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'

View File

@ -13,7 +13,7 @@ import { useSession } from '~/context/session'
import { Author, FollowingEntity } from '~/graphql/schema/core.gen' import { Author, FollowingEntity } from '~/graphql/schema/core.gen'
import { isCyrillic } from '~/intl/translate' import { isCyrillic } from '~/intl/translate'
import { translit } from '~/intl/translit' import { translit } from '~/intl/translit'
import { mediaMatches } from '~/utils/media-query' import { mediaMatches } from '~/lib/mediaQuery'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import styles from './AuthorBadge.module.scss' import styles from './AuthorBadge.module.scss'

View File

@ -3,8 +3,8 @@ import { Show } from 'solid-js'
import { isServer } from 'solid-js/web' import { isServer } from 'solid-js/web'
import { DropArea } from '~/components/_shared/DropArea' import { DropArea } from '~/components/_shared/DropArea'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { composeMediaItems } from '~/lib/composeMediaItems'
import { MediaItem } from '~/types/mediaitem' import { MediaItem } from '~/types/mediaitem'
import { composeMediaItems } from '~/utils/composeMediaItems'
import { AudioPlayer } from '../../Article/AudioPlayer' import { AudioPlayer } from '../../Article/AudioPlayer'
import styles from './AudioUploader.module.scss' import styles from './AudioUploader.module.scss'

View File

@ -5,8 +5,8 @@ import { renderUploadedImage } from '~/components/Editor/renderUploadedImage'
import { Icon } from '~/components/_shared/Icon' import { Icon } from '~/components/_shared/Icon'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useUI } from '~/context/ui' import { useUI } from '~/context/ui'
import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
import { UploadedFile } from '~/types/upload' import { UploadedFile } from '~/types/upload'
import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler'
import { Modal } from '../../Nav/Modal' import { Modal } from '../../Nav/Modal'
import { InlineForm } from '../InlineForm' import { InlineForm } from '../InlineForm'
import { UploadModalContent } from '../UploadModalContent' import { UploadModalContent } from '../UploadModalContent'

View File

@ -2,7 +2,7 @@ import { Editor } from '@tiptap/core'
import { createEditorTransaction } from 'solid-tiptap' import { createEditorTransaction } from 'solid-tiptap'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { validateUrl } from '~/utils/validateUrl' import { validateUrl } from '~/utils/validate'
import { InlineForm } from '../InlineForm' import { InlineForm } from '../InlineForm'
type Props = { type Props = {

View File

@ -9,8 +9,8 @@ import { Icon } from '~/components/_shared/Icon'
import { useEditorContext } from '~/context/editor' import { useEditorContext } from '~/context/editor'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useUI } from '~/context/ui' import { useUI } from '~/context/ui'
import { useEscKeyDownHandler } from '~/utils/useEscKeyDownHandler' import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler'
import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler' import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
import styles from './Panel.module.scss' import styles from './Panel.module.scss'
const typograf = new Typograf({ locale: ['ru', 'en-US'] }) const typograf = new Typograf({ locale: ['ru', 'en-US'] })

View File

@ -10,7 +10,6 @@ import { useSession } from '~/context/session'
import { useUI } from '~/context/ui' import { useUI } from '~/context/ui'
import { handleImageUpload } from '~/lib/handleImageUpload' import { handleImageUpload } from '~/lib/handleImageUpload'
import { UploadedFile } from '~/types/upload' import { UploadedFile } from '~/types/upload'
import { verifyImg } from '~/utils/verifyImg'
import { InlineForm } from '../InlineForm' import { InlineForm } from '../InlineForm'
import styles from './UploadModalContent.module.scss' import styles from './UploadModalContent.module.scss'
@ -19,6 +18,9 @@ type Props = {
onClose: (image?: UploadedFile) => void 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) => { export const UploadModalContent = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { hideModal } = useUI() const { hideModal } = useUI()
@ -87,7 +89,7 @@ export const UploadModalContent = (props: Props) => {
} }
const handleValidate = async (value: string) => { const handleValidate = async (value: string) => {
const validationResult = await verifyImg(value) const validationResult = await verify(value)
if (!validationResult) { if (!validationResult) {
return t('Invalid image URL') return t('Invalid image URL')
} }

View File

@ -5,8 +5,8 @@ import { For, Show, createSignal } from 'solid-js'
import { VideoPlayer } from '~/components/_shared/VideoPlayer' import { VideoPlayer } from '~/components/_shared/VideoPlayer'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSnackbar } from '~/context/ui' import { useSnackbar } from '~/context/ui'
import { composeMediaItems } from '~/utils/composeMediaItems' import { composeMediaItems } from '~/lib/composeMediaItems'
import { validateUrl } from '~/utils/validateUrl' import { validateUrl } from '~/utils/validate'
import { MediaItem } from '~/types/mediaitem' import { MediaItem } from '~/types/mediaitem'
import styles from './VideoUploader.module.scss' import styles from './VideoUploader.module.scss'

View File

@ -4,7 +4,7 @@ import { JSX, Show, createSignal } from 'solid-js'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { useSnackbar, useUI } from '~/context/ui' import { useSnackbar, useUI } from '~/context/ui'
import { validateEmail } from '~/utils/validateEmail' import { validateEmail } from '~/utils/validate'
import { AuthModalHeader } from './AuthModalHeader' import { AuthModalHeader } from './AuthModalHeader'
import { PasswordField } from './PasswordField' import { PasswordField } from './PasswordField'

View File

@ -6,7 +6,7 @@ import { useSearchParams } from '@solidjs/router'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { useUI } from '~/context/ui' import { useUI } from '~/context/ui'
import { validateEmail } from '~/utils/validateEmail' import { validateEmail } from '~/utils/validate'
import { AuthModalHeader } from './AuthModalHeader' import { AuthModalHeader } from './AuthModalHeader'
import { PasswordField } from './PasswordField' import { PasswordField } from './PasswordField'
import { SocialProviders } from './SocialProviders' import { SocialProviders } from './SocialProviders'

View File

@ -3,7 +3,7 @@ import { JSX, Show, createSignal, onMount } from 'solid-js'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { validateEmail } from '~/utils/validateEmail' import { validateEmail } from '~/utils/validate'
import { email, setEmail } from './sharedLogic' import { email, setEmail } from './sharedLogic'
import { useSearchParams } from '@solidjs/router' import { useSearchParams } from '@solidjs/router'

View File

@ -4,7 +4,7 @@ import { Dynamic } from 'solid-js/web'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { AuthModalSource, useUI } from '~/context/ui' import { AuthModalSource, useUI } from '~/context/ui'
import { isMobile } from '~/utils/media-query' import { isMobile } from '~/lib/mediaQuery'
import { ChangePasswordForm } from './ChangePasswordForm' import { ChangePasswordForm } from './ChangePasswordForm'
import { EmailConfirm } from './EmailConfirm' import { EmailConfirm } from './EmailConfirm'
import { LoginForm } from './LoginForm' import { LoginForm } from './LoginForm'

View File

@ -4,8 +4,8 @@ import type { JSX } from 'solid-js'
import { Show } from 'solid-js' import { Show } from 'solid-js'
import { Icon } from '~/components/_shared/Icon' import { Icon } from '~/components/_shared/Icon'
import { useUI } from '~/context/ui' import { useUI } from '~/context/ui'
import { isPortrait } from '~/utils/media-query' import { isPortrait } from '~/lib/mediaQuery'
import { useEscKeyDownHandler } from '~/utils/useEscKeyDownHandler' import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler'
import styles from './Modal.module.scss' import styles from './Modal.module.scss'
interface Props { interface Props {

View File

@ -7,7 +7,7 @@ import { Button } from '~/components/_shared/Button'
import { Icon } from '~/components/_shared/Icon' import { Icon } from '~/components/_shared/Icon'
import { useFeed } from '~/context/feed' import { useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { byScore } from '~/lib/sortby' import { byScore } from '~/lib/sortBy'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { FEED_PAGE_SIZE } from '../../Views/Feed/Feed' import { FEED_PAGE_SIZE } from '../../Views/Feed/Feed'

View File

@ -5,8 +5,8 @@ import { throttle } from 'throttle-debounce'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { PAGE_SIZE, useNotifications } from '~/context/notifications' import { PAGE_SIZE, useNotifications } from '~/context/notifications'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { useEscKeyDownHandler } from '~/utils/useEscKeyDownHandler' import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler'
import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler' import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'

View File

@ -4,7 +4,7 @@ import { debounce, throttle } from 'throttle-debounce'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { DEFAULT_HEADER_OFFSET } from '~/context/ui' import { DEFAULT_HEADER_OFFSET } from '~/context/ui'
import { isDesktop } from '~/utils/media-query' import { isDesktop } from '~/lib/mediaQuery'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import styles from './TableOfContents.module.scss' import styles from './TableOfContents.module.scss'

View File

@ -7,8 +7,8 @@ import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { FollowingEntity, Topic } from '~/graphql/schema/core.gen' import { FollowingEntity, Topic } from '~/graphql/schema/core.gen'
import { getImageUrl } from '~/lib/getImageUrl' import { getImageUrl } from '~/lib/getImageUrl'
import { mediaMatches } from '~/lib/mediaQuery'
import { capitalize } from '~/utils/capitalize' import { capitalize } from '~/utils/capitalize'
import { mediaMatches } from '~/utils/media-query'
import styles from './TopicBadge.module.scss' import styles from './TopicBadge.module.scss'
type Props = { type Props = {

View File

@ -11,7 +11,7 @@ import { useLocalize } from '~/context/localize'
import type { Author } from '~/graphql/schema/core.gen' import type { Author } from '~/graphql/schema/core.gen'
import { authorLetterReduce, translateAuthor } from '~/intl/translate' import { authorLetterReduce, translateAuthor } from '~/intl/translate'
import { dummyFilter } from '~/lib/dummyFilter' import { dummyFilter } from '~/lib/dummyFilter'
import { byFirstChar, byStat } from '~/lib/sortby' import { byFirstChar, byStat } from '~/lib/sortBy'
import { scrollHandler } from '~/utils/scroll' import { scrollHandler } from '~/utils/scroll'
import styles from './AllAuthors.module.scss' import styles from './AllAuthors.module.scss'
import stylesAuthorList from './AuthorsList.module.scss' import stylesAuthorList from './AuthorsList.module.scss'

View File

@ -13,9 +13,9 @@ import loadShoutsQuery from '~/graphql/query/core/articles-load-by'
import getAuthorFollowersQuery from '~/graphql/query/core/author-followers' import getAuthorFollowersQuery from '~/graphql/query/core/author-followers'
import getAuthorFollowsQuery from '~/graphql/query/core/author-follows' import getAuthorFollowsQuery from '~/graphql/query/core/author-follows'
import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen' 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 { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { splitToPages } from '~/utils/splitToPages'
import stylesArticle from '../../Article/Article.module.scss' import stylesArticle from '../../Article/Article.module.scss'
import { Comment } from '../../Article/Comment' import { Comment } from '../../Article/Comment'
import { AuthorCard } from '../../Author/AuthorCard' import { AuthorCard } from '../../Author/AuthorCard'
@ -59,7 +59,7 @@ export const AuthorView = (props: AuthorViewProps) => {
// derivatives // derivatives
const me = createMemo<Author>(() => session()?.user?.app_data?.profile as Author) const me = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const pages = createMemo<Shout[][]>(() => const pages = createMemo<Shout[][]>(() =>
splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) paginate(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
) )
// fx // fx

View File

@ -10,8 +10,8 @@ import { useGraphQL } from '~/context/graphql'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import getMyShoutQuery from '~/graphql/query/core/article-my' import getMyShoutQuery from '~/graphql/query/core/article-my'
import type { Shout, Topic } from '~/graphql/schema/core.gen' import type { Shout, Topic } from '~/graphql/schema/core.gen'
import { isDesktop } from '~/lib/mediaQuery'
import { clone } from '~/utils/clone' import { clone } from '~/utils/clone'
import { isDesktop } from '~/utils/media-query'
import { Panel } from '../../Editor' import { Panel } from '../../Editor'
import { AutoSaveNotice } from '../../Editor/AutoSaveNotice' import { AutoSaveNotice } from '../../Editor/AutoSaveNotice'
import { Modal } from '../../Nav/Modal' import { Modal } from '../../Nav/Modal'

View File

@ -26,10 +26,10 @@ import getMyShoutQuery from '~/graphql/query/core/article-my'
import type { Shout, Topic } from '~/graphql/schema/core.gen' import type { Shout, Topic } from '~/graphql/schema/core.gen'
import { slugify } from '~/intl/translit' import { slugify } from '~/intl/translit'
import { getImageUrl } from '~/lib/getImageUrl' import { getImageUrl } from '~/lib/getImageUrl'
import { isDesktop } from '~/lib/mediaQuery'
import { LayoutType } from '~/types/common' import { LayoutType } from '~/types/common'
import { MediaItem } from '~/types/mediaitem' import { MediaItem } from '~/types/mediaitem'
import { clone } from '~/utils/clone' import { clone } from '~/utils/clone'
import { isDesktop } from '~/utils/media-query'
import { Editor, Panel } from '../../Editor' import { Editor, Panel } from '../../Editor'
import { AudioUploader } from '../../Editor/AudioUploader' import { AudioUploader } from '../../Editor/AudioUploader'
import { AutoSaveNotice } from '../../Editor/AutoSaveNotice' import { AutoSaveNotice } from '../../Editor/AutoSaveNotice'

View File

@ -12,7 +12,7 @@ import getShoutsQuery from '~/graphql/query/core/articles-load-by'
import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top' import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top'
import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen'
import { LayoutType } from '~/types/common' import { LayoutType } from '~/types/common'
import { getUnixtime } from '~/utils/getServerDate' import { getUnixtime } from '~/utils/date'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { ArticleCard } from '../../Feed/ArticleCard' import { ArticleCard } from '../../Feed/ArticleCard'
import styles from './Expo.module.scss' import styles from './Expo.module.scss'

View File

@ -16,7 +16,7 @@ import { useTopics } from '~/context/topics'
import { useUI } from '~/context/ui' import { useUI } from '~/context/ui'
import { loadUnratedShouts } from '~/graphql/api/private' import { loadUnratedShouts } from '~/graphql/api/private'
import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen' 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 { FeedSearchParams } from '~/routes/feed/(feed)'
import { CommentDate } from '../../Article/CommentDate' import { CommentDate } from '../../Article/CommentDate'
import { getShareUrl } from '../../Article/SharePopup' import { getShareUrl } from '../../Article/SharePopup'

View File

@ -6,7 +6,7 @@ import { loadShouts } from '~/graphql/api/public'
import { Author, Shout, Topic } from '~/graphql/schema/core.gen' import { Author, Shout, Topic } from '~/graphql/schema/core.gen'
import { SHOUTS_PER_PAGE } from '~/routes/(main)' import { SHOUTS_PER_PAGE } from '~/routes/(main)'
import { capitalize } from '~/utils/capitalize' import { capitalize } from '~/utils/capitalize'
import { splitToPages } from '~/utils/splitToPages' import { paginate } from '~/utils/paginate'
import Banner from '../Discours/Banner' import Banner from '../Discours/Banner'
import Hero from '../Discours/Hero' import Hero from '../Discours/Hero'
import { Beside } from '../Feed/Beside' import { Beside } from '../Feed/Beside'
@ -62,11 +62,7 @@ export const HomeView = (props: HomeViewProps) => {
) )
const pages = createMemo<Shout[][]>(() => const pages = createMemo<Shout[][]>(() =>
splitToPages( paginate(props.featuredShouts || [], SHOUTS_PER_PAGE + CLIENT_LOAD_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
props.featuredShouts || [],
SHOUTS_PER_PAGE + CLIENT_LOAD_ARTICLES_COUNT,
LOAD_MORE_PAGE_SIZE
)
) )
return ( return (

View File

@ -10,7 +10,7 @@ import { Loading } from '~/components/_shared/Loading'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { DEFAULT_HEADER_OFFSET, useSnackbar, useUI } from '~/context/ui' 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' import styles from './Settings.module.scss'
type FormField = 'oldPassword' | 'newPassword' | 'newPasswordConfirm' | 'email' type FormField = 'oldPassword' | 'newPassword' | 'newPasswordConfirm' | 'email'

View File

@ -23,7 +23,7 @@ import { getImageUrl } from '~/lib/getImageUrl'
import { handleImageUpload } from '~/lib/handleImageUpload' import { handleImageUpload } from '~/lib/handleImageUpload'
import { profileSocialLinks } from '~/lib/profileSocialLinks' import { profileSocialLinks } from '~/lib/profileSocialLinks'
import { clone } from '~/utils/clone' import { clone } from '~/utils/clone'
import { validateUrl } from '~/utils/validateUrl' import { validateUrl } from '~/utils/validate'
import { Modal } from '../../Nav/Modal' import { Modal } from '../../Nav/Modal'
import { ProfileSettingsNavigation } from '../../Nav/ProfileSettingsNavigation' import { ProfileSettingsNavigation } from '../../Nav/ProfileSettingsNavigation'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
@ -188,6 +188,14 @@ export const ProfileSettings = () => {
updateFormField('links', link, true) 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 ( return (
<Show when={Object.keys(form).length > 0 && isFormInitialized()} fallback={<Loading />}> <Show when={Object.keys(form).length > 0 && isFormInitialized()} fallback={<Loading />}>
<> <>
@ -293,7 +301,7 @@ export const ProfileSettings = () => {
<h4>{t('Address on Discours')}</h4> <h4>{t('Address on Discours')}</h4>
<div class="pretty-form__item"> <div class="pretty-form__item">
<div class={styles.discoursName}> <div class={styles.discoursName}>
<label for="user-address">https://{hostname()}/author/</label> <label for="user-address">{hostname()}/@</label>
<div class={styles.discoursNameField}> <div class={styles.discoursNameField}>
<input <input
type="text" type="text"
@ -301,7 +309,7 @@ export const ProfileSettings = () => {
id="user-address" id="user-address"
data-lpignore="true" data-lpignore="true"
autocomplete="one-time-code2" autocomplete="one-time-code2"
onInput={(event) => updateFormField('slug', event.currentTarget.value)} onInput={slugUpdate}
value={form.slug || ''} value={form.slug || ''}
ref={(el) => (slugInputRef = el)} ref={(el) => (slugInputRef = el)}
class="nolabel" class="nolabel"

View File

@ -144,7 +144,12 @@ export const PublishSettings = (props: Props) => {
const handleSaveDraft = () => { const handleSaveDraft = () => {
saveShout({ ...props.form, ...settingsForm }) 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 ( return (
<form class={clsx(styles.PublishSettings, 'inputs-wrapper')}> <form class={clsx(styles.PublishSettings, 'inputs-wrapper')}>
<div class="wide-container"> <div class="wide-container">
@ -235,7 +240,7 @@ export const PublishSettings = (props: Props) => {
<h4>{t('Slug')}</h4> <h4>{t('Slug')}</h4>
<div class="pretty-form__item"> <div class="pretty-form__item">
<input type="text" name="slug" id="slug" value={settingsForm.slug} /> <input type="text" name="slug" id="slug" value={settingsForm.slug} onInput={removeSpecial} />
<label for="slug">{t('Slug')}</label> <label for="slug">{t('Slug')}</label>
</div> </div>

View File

@ -8,9 +8,9 @@ import { useTopics } from '~/context/topics'
import { loadAuthors, loadFollowersByTopic, loadShouts } from '~/graphql/api/public' import { loadAuthors, loadFollowersByTopic, loadShouts } from '~/graphql/api/public'
import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
import { SHOUTS_PER_PAGE } from '~/routes/(main)' 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 { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { splitToPages } from '~/utils/splitToPages'
import styles from '../../styles/Topic.module.scss' import styles from '../../styles/Topic.module.scss'
import { Beside } from '../Feed/Beside' import { Beside } from '../Feed/Beside'
import { Row1 } from '../Feed/Row1' import { Row1 } from '../Feed/Row1'
@ -140,7 +140,7 @@ export const TopicView = (props: Props) => {
}) })
*/ */
const pages = createMemo<Shout[][]>(() => const pages = createMemo<Shout[][]>(() =>
splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) paginate(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
) )
return ( return (
<div class={styles.topicPage}> <div class={styles.topicPage}>

View File

@ -6,7 +6,7 @@ import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { handleFileUpload } from '~/lib/handleFileUpload' import { handleFileUpload } from '~/lib/handleFileUpload'
import { handleImageUpload } from '~/lib/handleImageUpload' import { handleImageUpload } from '~/lib/handleImageUpload'
import { validateFiles } from '~/utils/validateFile' import { validateUploads } from '~/lib/validateUploads'
import styles from './DropArea.module.scss' import styles from './DropArea.module.scss'
@ -62,7 +62,7 @@ export const DropArea = (props: Props) => {
setDropAreaError(t('Many files, choose only one')) setDropAreaError(t('Many files, choose only one'))
return return
} }
const isValid = validateFiles(props.fileType, selectedFiles) const isValid = validateUploads(props.fileType, selectedFiles)
if (isValid) { if (isValid) {
await runUpload(selectedFiles) await runUpload(selectedFiles)
} else { } else {

View File

@ -1,7 +1,7 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createSignal } from 'solid-js' import { For, Show, createSignal } from 'solid-js'
import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler' import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
import styles from './DropdownSelect.module.scss' import styles from './DropdownSelect.module.scss'

View File

@ -2,7 +2,7 @@ import { clsx } from 'clsx'
import { Show, createEffect, createMemo, createSignal, on, onCleanup } from 'solid-js' import { Show, createEffect, createMemo, createSignal, on, onCleanup } from 'solid-js'
import { getImageUrl } from '~/lib/getImageUrl' import { getImageUrl } from '~/lib/getImageUrl'
import { useEscKeyDownHandler } from '~/utils/useEscKeyDownHandler' import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler'
import { Icon } from '../Icon' import { Icon } from '../Icon'
import styles from './Lightbox.module.scss' import styles from './Lightbox.module.scss'

View File

@ -2,7 +2,7 @@ import { JSX, Show, createSignal } from 'solid-js'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSnackbar } from '~/context/ui' import { useSnackbar } from '~/context/ui'
import { validateEmail } from '~/utils/validateEmail' import { validateEmail } from '~/utils/validate'
import { Button } from '../Button' import { Button } from '../Button'
import { Icon } from '../Icon' import { Icon } from '../Icon'

View File

@ -1,7 +1,7 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { JSX, Show, createEffect, createSignal } from 'solid-js' import { JSX, Show, createEffect, createSignal } from 'solid-js'
import { useOutsideClickHandler } from '~/utils/useOutsideClickHandler' import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
import styles from './Popup.module.scss' import styles from './Popup.module.scss'

View File

@ -6,10 +6,10 @@ import { Manipulation, Navigation, Pagination } from 'swiper/modules'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSnackbar } from '~/context/ui' import { useSnackbar } from '~/context/ui'
import { composeMediaItems } from '~/lib/composeMediaItems'
import { getImageUrl } from '~/lib/getImageUrl' import { getImageUrl } from '~/lib/getImageUrl'
import { handleImageUpload } from '~/lib/handleImageUpload' import { handleImageUpload } from '~/lib/handleImageUpload'
import { composeMediaItems } from '~/utils/composeMediaItems' import { validateUploads } from '~/lib/validateUploads'
import { validateFiles } from '~/utils/validateFile'
import { DropArea } from '../DropArea' import { DropArea } from '../DropArea'
import { Icon } from '../Icon' import { Icon } from '../Icon'
import { Image } from '../Image' import { Image } from '../Image'
@ -90,7 +90,7 @@ export const EditorSwiper = (props: Props) => {
}) })
const initUpload = async (selectedFiles: UploadFile[]) => { const initUpload = async (selectedFiles: UploadFile[]) => {
const isValid = validateFiles('image', selectedFiles) const isValid = validateUploads('image', selectedFiles)
if (!isValid) { if (!isValid) {
await showSnackbar({ type: 'error', body: t('Invalid file type') }) await showSnackbar({ type: 'error', body: t('Invalid file type') })

View File

@ -17,7 +17,7 @@ import {
Shout, Shout,
Topic Topic
} from '~/graphql/schema/core.gen' } from '~/graphql/schema/core.gen'
import { byStat } from '~/lib/sortby' import { byStat } from '~/lib/sortBy'
import { useFeed } from './feed' import { useFeed } from './feed'
const TOP_AUTHORS_COUNT = 5 const TOP_AUTHORS_COUNT = 5

View File

@ -10,7 +10,7 @@ import {
Shout, Shout,
Topic Topic
} from '~/graphql/schema/core.gen' } from '~/graphql/schema/core.gen'
import { byStat } from '../lib/sortby' import { byStat } from '../lib/sortBy'
import { useGraphQL } from './graphql' import { useGraphQL } from './graphql'
export const PRERENDERED_ARTICLES_COUNT = 5 export const PRERENDERED_ARTICLES_COUNT = 5

View File

@ -76,16 +76,21 @@ export const ProfileProvider = (props: { children: JSX.Element }) => {
} }
}) })
// TODO: validation error for `!` and `@`
const updateFormField = (fieldName: string, value: string, remove?: boolean) => { 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') { if (fieldName === 'links') {
setForm((prev) => { setForm((prev) => {
const updatedLinks = remove const updatedLinks = remove
? (prev.links || []).filter((item) => item !== value) ? (prev.links || []).filter((item) => item !== val)
: [...(prev.links || []), value] : [...(prev.links || []), val]
return { ...prev, links: updatedLinks } return { ...prev, links: updatedLinks }
}) })
} else { } else {
setForm((prev) => ({ ...prev, [fieldName]: value })) setForm((prev) => ({ ...prev, [fieldName]: val }))
} }
} }

View File

@ -13,7 +13,7 @@ import {
import { loadTopics } from '~/graphql/api/public' import { loadTopics } from '~/graphql/api/public'
import { Topic } from '~/graphql/schema/core.gen' import { Topic } from '~/graphql/schema/core.gen'
import { getRandomTopicsFromArray } from '~/lib/getRandomTopicsFromArray' import { getRandomTopicsFromArray } from '~/lib/getRandomTopicsFromArray'
import { byTopicStatDesc } from '../lib/sortby' import { byTopicStatDesc } from '../lib/sortBy'
type TopicsContextType = { type TopicsContextType = {
topicEntities: Accessor<{ [topicSlug: string]: Topic }> topicEntities: Accessor<{ [topicSlug: string]: Topic }>

View File

@ -1,9 +1,9 @@
import { UploadFile } from '@solid-primitives/upload' import { UploadFile } from '@solid-primitives/upload'
export const validateFiles = (fileType: string, files: UploadFile[]): boolean => { export const imageExtensions = new Set(['jpg', 'jpeg', 'png', 'gif', 'bmp'])
const imageExtensions = new Set(['jpg', 'jpeg', 'png', 'gif', 'bmp']) export const docExtensions = new Set(['doc', 'docx', 'pdf', 'txt'])
const docExtensions = new Set(['doc', 'docx', 'pdf', 'txt'])
export const validateUploads = (fileType: string, files: UploadFile[]): boolean => {
for (const file of files) { for (const file of files) {
let isValid: boolean let isValid: boolean

View File

@ -3,7 +3,7 @@ import { Show, Suspense, createEffect, createSignal, onMount } from 'solid-js'
import { useTopics } from '~/context/topics' import { useTopics } from '~/context/topics'
import { loadShouts, loadTopics } from '~/graphql/api/public' import { loadShouts, loadTopics } from '~/graphql/api/public'
import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' 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 { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { HomeView, HomeViewProps } from '../components/Views/Home' import { HomeView, HomeViewProps } from '../components/Views/Home'
import { Loading } from '../components/_shared/Loading' import { Loading } from '../components/_shared/Loading'

View File

@ -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'

2
src/utils/date.ts Normal file
View File

@ -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

View File

@ -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]]

View File

@ -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)
}

10
src/utils/paginate.ts Normal file
View File

@ -0,0 +1,10 @@
export function paginate<T>(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[][])
}

View File

@ -1,10 +0,0 @@
export function splitToPages<T>(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[][])
}

10
src/utils/validate.ts Normal file
View File

@ -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(' ')
}

View File

@ -1,7 +0,0 @@
export const validateEmail = (email: string) => {
if (!email) {
return false
}
return /^[\w%+.-]+@[\d.a-z-]+\.[a-z]{2,}$/i.test(email)
}

View File

@ -1,3 +0,0 @@
export const validateUrl = (value: string) => {
return value.includes('.') && !value.includes(' ')
}

View File

@ -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))
}