Merge branch 'dev' into editor
This commit is contained in:
commit
bc2abbc2bd
|
@ -10,7 +10,6 @@ import { useReactions } from '../../context/reactions'
|
||||||
import { byCreated } from '../../utils/sortby'
|
import { byCreated } from '../../utils/sortby'
|
||||||
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import Cookie from 'js-cookie'
|
|
||||||
|
|
||||||
type CommentsOrder = 'createdAt' | 'rating' | 'newOnly'
|
type CommentsOrder = 'createdAt' | 'rating' | 'newOnly'
|
||||||
|
|
||||||
|
@ -110,7 +109,7 @@ export const CommentsTree = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class={styles.commentsHeaderWrapper}>
|
<div class={styles.commentsHeaderWrapper}>
|
||||||
<h2 id="comments" class={styles.commentsHeader}>
|
<h2 class={styles.commentsHeader}>
|
||||||
{t('Comments')} {comments().length.toString() || ''}
|
{t('Comments')} {comments().length.toString() || ''}
|
||||||
<Show when={newReactions().length > 0}>
|
<Show when={newReactions().length > 0}>
|
||||||
<span class={styles.newReactions}> +{newReactions().length}</span>
|
<span class={styles.newReactions}> +{newReactions().length}</span>
|
||||||
|
@ -166,7 +165,7 @@ export const CommentsTree = (props: Props) => {
|
||||||
</ul>
|
</ul>
|
||||||
<ShowIfAuthenticated
|
<ShowIfAuthenticated
|
||||||
fallback={
|
fallback={
|
||||||
<div class={styles.signInMessage} id="comments">
|
<div class={styles.signInMessage}>
|
||||||
{t('To write a comment, you must')}{' '}
|
{t('To write a comment, you must')}{' '}
|
||||||
<a href="?modal=auth&mode=register" class={styles.link}>
|
<a href="?modal=auth&mode=register" class={styles.link}>
|
||||||
{t('sign up')}
|
{t('sign up')}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { capitalize, formatDate } from '../../utils'
|
import { capitalize, formatDate } from '../../utils'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js'
|
import { createEffect, createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js'
|
||||||
import type { Author, Shout } from '../../graphql/types.gen'
|
import type { Author, Shout } from '../../graphql/types.gen'
|
||||||
import MD from './MD'
|
import MD from './MD'
|
||||||
import { SharePopup } from './SharePopup'
|
import { SharePopup } from './SharePopup'
|
||||||
|
@ -13,7 +13,7 @@ import { useSession } from '../../context/session'
|
||||||
import VideoPlayer from './VideoPlayer'
|
import VideoPlayer from './VideoPlayer'
|
||||||
import Slider from '../_shared/Slider'
|
import Slider from '../_shared/Slider'
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { router } from '../../stores/router'
|
import { router, useRouter } from '../../stores/router'
|
||||||
import { useReactions } from '../../context/reactions'
|
import { useReactions } from '../../context/reactions'
|
||||||
import { Title } from '@solidjs/meta'
|
import { Title } from '@solidjs/meta'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
@ -65,19 +65,6 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
props.article.topics[0]
|
props.article.topics[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const windowHash = window.location.hash
|
|
||||||
if (windowHash?.length > 0) {
|
|
||||||
const comments = document.querySelector(windowHash)
|
|
||||||
if (comments) {
|
|
||||||
window.scrollTo({
|
|
||||||
top: comments.getBoundingClientRect().top,
|
|
||||||
behavior: 'smooth'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadReactionsBy({
|
await loadReactionsBy({
|
||||||
by: { shout: props.article.slug }
|
by: { shout: props.article.slug }
|
||||||
|
@ -104,14 +91,6 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
actions: { loadReactionsBy }
|
actions: { loadReactionsBy }
|
||||||
} = useReactions()
|
} = useReactions()
|
||||||
|
|
||||||
let commentsRef: HTMLDivElement | undefined
|
|
||||||
const scrollToComments = () => {
|
|
||||||
if (!isReactionsLoaded()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
commentsRef.scrollIntoView({ behavior: 'smooth' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>{props.article.title}</Title>
|
<Title>{props.article.title}</Title>
|
||||||
|
@ -205,12 +184,10 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class={styles.shoutStatsItem} onClick={() => scrollToComments()}>
|
<a href="#comments" class={styles.shoutStatsItem}>
|
||||||
<div class={styles.shoutStatsItemInner}>
|
<Icon name="comment" class={styles.icon} />
|
||||||
<Icon name="comment" class={styles.icon} />
|
{props.article.stat?.commented ?? ''}
|
||||||
{/*{props.article.stat?.commented || ''}*/}
|
</a>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={styles.shoutStatsItem}>
|
<div class={styles.shoutStatsItem}>
|
||||||
<SharePopup
|
<SharePopup
|
||||||
|
@ -277,7 +254,7 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<div ref={commentsRef}>
|
<div id="comments">
|
||||||
<Show when={isReactionsLoaded()}>
|
<Show when={isReactionsLoaded()}>
|
||||||
<CommentsTree
|
<CommentsTree
|
||||||
shoutId={props.article.id}
|
shoutId={props.article.id}
|
||||||
|
|
|
@ -155,7 +155,6 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!props.settings?.nodate}>
|
<Show when={!props.settings?.nodate}>
|
||||||
<div class={styles.shoutDate}>{formattedDate()}</div>
|
<div class={styles.shoutDate}>{formattedDate()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -173,7 +172,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={clsx(styles.shoutCardDetailsItem, styles.shoutCardComments)}>
|
<div class={clsx(styles.shoutCardDetailsItem, styles.shoutCardComments)}>
|
||||||
<a href={`/${slug + '#comments' || ''}`}>
|
<a href={`/${slug + '#comments'}`}>
|
||||||
<Icon name="comment" class={clsx(styles.icon, styles.feedControlIcon)} />
|
<Icon name="comment" class={clsx(styles.icon, styles.feedControlIcon)} />
|
||||||
{stat?.commented || t('Add comment')}
|
{stat?.commented || t('Add comment')}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -29,7 +29,9 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
<a href="#">{t('Subscriptions')}</a>
|
<a href="#">{t('Subscriptions')}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#">{t('Comments')}</a>
|
<a href={`${getPagePath(router, 'author', { slug: userSlug() })}/?by=commented`}>
|
||||||
|
{t('Comments')}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#">{t('Bookmarks')}</a>
|
<a href="#">{t('Bookmarks')}</a>
|
||||||
|
|
|
@ -26,7 +26,7 @@ type AuthorProps = {
|
||||||
authorSlug: string
|
authorSlug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorPageSearchParams = {
|
export type AuthorPageSearchParams = {
|
||||||
by: '' | 'viewed' | 'rating' | 'commented' | 'recent' | 'followed' | 'about' | 'popular'
|
by: '' | 'viewed' | 'rating' | 'commented' | 'recent' | 'followed' | 'about' | 'popular'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,11 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
|
|
||||||
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
||||||
|
|
||||||
changeSearchParam('by', 'rating')
|
onMount(() => {
|
||||||
|
if (!searchParams().by) {
|
||||||
|
changeSearchParam('by', 'rating')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
saveScrollPosition()
|
saveScrollPosition()
|
||||||
|
@ -131,7 +135,7 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
*/}
|
*/}
|
||||||
<li classList={{ selected: searchParams().by === 'about' }}>
|
<li classList={{ selected: searchParams().by === 'about' }}>
|
||||||
<button type="button" onClick={() => changeSearchParam('by', 'about')}>
|
<button type="button" onClick={() => changeSearchParam('by', 'about')}>
|
||||||
О себе
|
{t('About myself')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const SearchView = (props: Props) => {
|
||||||
const [query, setQuery] = createSignal(props.query)
|
const [query, setQuery] = createSignal(props.query)
|
||||||
const [offset, setOffset] = createSignal(0)
|
const [offset, setOffset] = createSignal(0)
|
||||||
|
|
||||||
const { searchParams, handleClientRouteLinkClick } = useRouter<SearchPageSearchParams>()
|
const { searchParams } = useRouter<SearchPageSearchParams>()
|
||||||
let searchEl: HTMLInputElement
|
let searchEl: HTMLInputElement
|
||||||
const handleQueryChange = (_ev) => {
|
const handleQueryChange = (_ev) => {
|
||||||
setQuery(searchEl.value)
|
setQuery(searchEl.value)
|
||||||
|
@ -72,18 +72,14 @@ export const SearchView = (props: Props) => {
|
||||||
selected: searchParams().by === 'relevance'
|
selected: searchParams().by === 'relevance'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<a href="?by=relevance" onClick={handleClientRouteLinkClick}>
|
<a href="?by=relevance">{t('By relevance')}</a>
|
||||||
{t('By relevance')}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
classList={{
|
classList={{
|
||||||
selected: searchParams().by === 'rating'
|
selected: searchParams().by === 'rating'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<a href="?by=rating" onClick={handleClientRouteLinkClick}>
|
<a href="?by=rating">{t('Top rated')}</a>
|
||||||
{t('Top rated')}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ export default gql`
|
||||||
viewed
|
viewed
|
||||||
reacted
|
reacted
|
||||||
rating
|
rating
|
||||||
|
commented
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useRouter } from '../stores/router'
|
||||||
import { Loading } from '../components/_shared/Loading'
|
import { Loading } from '../components/_shared/Loading'
|
||||||
import { ReactionsProvider } from '../context/reactions'
|
import { ReactionsProvider } from '../context/reactions'
|
||||||
import { FullArticle } from '../components/Article/FullArticle'
|
import { FullArticle } from '../components/Article/FullArticle'
|
||||||
|
import { setPageLoadManagerPromise } from '../utils/pageLoadManager'
|
||||||
|
|
||||||
export const ArticlePage = (props: PageProps) => {
|
export const ArticlePage = (props: PageProps) => {
|
||||||
const shouts = props.article ? [props.article] : []
|
const shouts = props.article ? [props.article] : []
|
||||||
|
@ -33,7 +34,9 @@ export const ArticlePage = (props: PageProps) => {
|
||||||
const articleValue = articleEntities()[slug()]
|
const articleValue = articleEntities()[slug()]
|
||||||
|
|
||||||
if (!articleValue || !articleValue.body) {
|
if (!articleValue || !articleValue.body) {
|
||||||
await loadShout(slug())
|
const loadShoutPromise = loadShout(slug())
|
||||||
|
setPageLoadManagerPromise(loadShoutPromise)
|
||||||
|
await loadShoutPromise
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { Accessor } from 'solid-js'
|
||||||
import { createRouter, createSearchParams } from '@nanostores/router'
|
import { createRouter, createSearchParams } from '@nanostores/router'
|
||||||
import { isServer } from 'solid-js/web'
|
import { isServer } from 'solid-js/web'
|
||||||
import { useStore } from '@nanostores/solid'
|
import { useStore } from '@nanostores/solid'
|
||||||
|
import { getPageLoadManagerPromise } from '../utils/pageLoadManager'
|
||||||
|
|
||||||
export const ROUTES = {
|
export const ROUTES = {
|
||||||
home: '/',
|
home: '/',
|
||||||
|
@ -40,9 +41,8 @@ const routerStore = createRouter(ROUTES, {
|
||||||
|
|
||||||
export const router = routerStore
|
export const router = routerStore
|
||||||
|
|
||||||
const handleClientRouteLinkClick = (event) => {
|
const checkOpenOnClient = (link: HTMLAnchorElement, event) => {
|
||||||
const link = event.target.closest('a')
|
return (
|
||||||
if (
|
|
||||||
link &&
|
link &&
|
||||||
event.button === 0 &&
|
event.button === 0 &&
|
||||||
link.target !== '_blank' &&
|
link.target !== '_blank' &&
|
||||||
|
@ -52,43 +52,84 @@ const handleClientRouteLinkClick = (event) => {
|
||||||
!event.ctrlKey &&
|
!event.ctrlKey &&
|
||||||
!event.shiftKey &&
|
!event.shiftKey &&
|
||||||
!event.altKey
|
!event.altKey
|
||||||
) {
|
)
|
||||||
const url = new URL(link.href)
|
}
|
||||||
if (url.origin === location.origin) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
if (url.hash) {
|
const scrollToHash = (hash: string) => {
|
||||||
let selector = url.hash
|
let selector = hash
|
||||||
|
|
||||||
if (/^#\d+/.test(selector)) {
|
if (/^#\d+/.test(selector)) {
|
||||||
// id="1" fix
|
// id="1" fix
|
||||||
// https://stackoverflow.com/questions/20306204/using-queryselector-with-ids-that-are-numbers
|
// https://stackoverflow.com/questions/20306204/using-queryselector-with-ids-that-are-numbers
|
||||||
selector = `[id="${selector.replace('#', '')}"]`
|
selector = `[id="${selector.replace('#', '')}"]`
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchor = document.querySelector(selector)
|
const anchor = document.querySelector(selector)
|
||||||
const headerOffset = 80 // 80px for header
|
const headerOffset = 80 // 80px for header
|
||||||
const elementPosition = anchor ? anchor.getBoundingClientRect().top : 0
|
const elementPosition = anchor ? anchor.getBoundingClientRect().top : 0
|
||||||
const newScrollTop = elementPosition + window.scrollY - headerOffset
|
const newScrollTop = elementPosition + window.scrollY - headerOffset
|
||||||
|
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: newScrollTop,
|
top: newScrollTop,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return
|
const handleClientRouteLinkClick = async (event) => {
|
||||||
}
|
const link = event.target.closest('a')
|
||||||
|
|
||||||
routerStore.open(url.pathname)
|
if (!checkOpenOnClient(link, event)) {
|
||||||
const params = Object.fromEntries(new URLSearchParams(url.search))
|
return
|
||||||
searchParamsStore.open(params)
|
}
|
||||||
|
|
||||||
window.scrollTo({
|
const url = new URL(link.href)
|
||||||
top: 0,
|
if (url.origin !== location.origin) {
|
||||||
left: 0
|
return
|
||||||
})
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (url.pathname) {
|
||||||
|
routerStore.open(url.pathname)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.search) {
|
||||||
|
const params = Object.fromEntries(new URLSearchParams(url.search))
|
||||||
|
searchParamsStore.open(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url.hash) {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
left: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await getPageLoadManagerPromise()
|
||||||
|
|
||||||
|
const images = document.querySelectorAll('img')
|
||||||
|
|
||||||
|
let imagesLoaded = 0
|
||||||
|
|
||||||
|
const imageLoadEventHandler = () => {
|
||||||
|
imagesLoaded++
|
||||||
|
if (imagesLoaded === images.length) {
|
||||||
|
scrollToHash(url.hash)
|
||||||
|
images.forEach((image) => image.removeEventListener('load', imageLoadEventHandler))
|
||||||
|
images.forEach((image) => image.removeEventListener('error', imageLoadEventHandler))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
images.forEach((image) => {
|
||||||
|
if (image.complete) {
|
||||||
|
imagesLoaded++
|
||||||
|
}
|
||||||
|
|
||||||
|
image.addEventListener('load', imageLoadEventHandler)
|
||||||
|
image.addEventListener('error', imageLoadEventHandler)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initRouter = (pathname: string, search: Record<string, string>) => {
|
export const initRouter = (pathname: string, search: Record<string, string>) => {
|
||||||
|
@ -129,7 +170,6 @@ export const useRouter = <TSearchParams extends Record<string, string> = Record<
|
||||||
return {
|
return {
|
||||||
page,
|
page,
|
||||||
searchParams,
|
searchParams,
|
||||||
changeSearchParam,
|
changeSearchParam
|
||||||
handleClientRouteLinkClick
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
11
src/utils/pageLoadManager.ts
Normal file
11
src/utils/pageLoadManager.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
const pageLoadManager: {
|
||||||
|
promise: Promise<any>
|
||||||
|
} = { promise: Promise.resolve() }
|
||||||
|
|
||||||
|
export const getPageLoadManagerPromise = () => {
|
||||||
|
return pageLoadManager.promise
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setPageLoadManagerPromise = (promise: Promise<any>) => {
|
||||||
|
pageLoadManager.promise = promise
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user