Merge branch 'dev' into editor

This commit is contained in:
bniwredyc 2023-04-03 17:23:02 +02:00
commit bc2abbc2bd
10 changed files with 112 additions and 80 deletions

View File

@ -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}>&nbsp;+{newReactions().length}</span> <span class={styles.newReactions}>&nbsp;+{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')}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@ export default gql`
viewed viewed
reacted reacted
rating rating
commented
} }
} }
} }

View File

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

View File

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

View 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
}