client-routing, fixes

This commit is contained in:
Igor Lobanov 2022-09-22 11:37:49 +02:00
parent caa8890222
commit 3d45479368
76 changed files with 1252 additions and 852 deletions

View File

@ -27,7 +27,13 @@ module.exports = {
// 'plugin:@typescript-eslint/recommended-requiring-type-checking'
],
rules: {
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^log$'
}
],
// TODO: Remove any usage and enable
'@typescript-eslint/no-explicit-any': 'off',
// TODO: Fix errors and enable this rule
@ -45,9 +51,6 @@ module.exports = {
},
globals: {},
rules: {
// FIXME: turn on
'import/no-default-export': 'off',
// FIXME
'unicorn/prefer-dom-node-append': 'off',

View File

@ -10,11 +10,20 @@ import type { CSSOptions } from 'vite'
// const dev = process.env.NODE_ENV != 'production'
const css: CSSOptions = {
preprocessorOptions: {
scss: {
additionalData: '@import "src/styles/imports";\n'
}
}
}
const astroConfig: AstroUserConfig = {
site: 'https://new.discours.io',
// Enable Solid to support Solid JSX components.
// experimental: { integrations: true },
integrations: [solidJs(), mdx()], // sitemap({
integrations: [solidJs(), mdx()],
// sitemap({
/* customPages: [
'',
'/feed',
@ -39,13 +48,7 @@ const astroConfig: AstroUserConfig = {
'@': './src'
}
},
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "src/styles/imports";\n'
}
}
} as CSSOptions
css
}
}

View File

@ -60,6 +60,8 @@
"@graphql-codegen/urql-introspection": "^2.2.1",
"@graphql-typed-document-node/core": "^3.1.1",
"@popperjs/core": "^2.11.5",
"@solid-devtools/debugger": "^0.9.0",
"@solid-devtools/logger": "^0.4.7",
"@solid-primitives/clipboard": "^1.3.0",
"@solid-primitives/event-listener": "^2.2.0",
"@solid-primitives/intersection-observer": "^2.0.0",
@ -124,6 +126,7 @@
"prosemirror-view": "^1.26.2",
"rollup": "~2.5.0",
"sass": "^1.54.0",
"solid-devtools": "^0.16.2",
"solid-js": "^1.5.3",
"solid-js-form": "^0.1.5",
"solid-jsx": "^0.9.0",

View File

@ -1,5 +1,5 @@
import './Comment.scss'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import { AuthorCard } from '../Author/Card'
import { Show } from 'solid-js/web'
import { clsx } from 'clsx'

View File

@ -1,17 +1,17 @@
import { capitalize } from '../../utils'
import './Full.scss'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import ArticleComment from './Comment'
import { AuthorCard } from '../Author/Card'
import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js'
import type { Author, Reaction, Shout } from '../../graphql/types.gen'
import { t } from '../../utils/intl'
import { showModal } from '../../stores/ui'
import { renderMarkdown } from '@astrojs/markdown-remark'
import { markdownOptions } from '../../../mdx.config'
import { useStore } from '@nanostores/solid'
import { session } from '../../stores/auth'
import { incrementView, loadArticle } from '../../stores/zine/articles'
import { incrementView } from '../../stores/zine/articles'
import { renderMarkdown } from '@astrojs/markdown-remark'
import { markdownOptions } from '../../../mdx.config'
const MAX_COMMENT_LEVEL = 6
@ -39,25 +39,22 @@ const formatDate = (date: Date) => {
}
export const FullArticle = (props: ArticleProps) => {
const [body, setBody] = createSignal('')
const [body, setBody] = createSignal(props.article.body?.startsWith('<') ? props.article.body : '')
const auth = useStore(session)
onMount(() => {
if (!props.article.body) {
loadArticle({ slug: props.article.slug })
createEffect(() => {
if (body() || !props.article.body) {
return
}
if (props.article.body.startsWith('<')) {
setBody(props.article.body)
} else {
renderMarkdown(props.article.body, markdownOptions).then(({ code }) => setBody(code))
}
})
// onMount(() => {
// const b: string = props.article?.body
// if (b?.toString().startsWith('<')) {
// setBody(b)
// } else {
// renderMarkdown(b, markdownOptions).then(({ code }) => setBody(code))
// }
// })
onMount(() => {
incrementView({ articleSlug: props.article.slug })
})

View File

@ -1,7 +1,7 @@
import { For, Show } from 'solid-js/web'
import type { Author } from '../../graphql/types.gen'
import Userpic from './Userpic'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import './Card.scss'
import { createMemo } from 'solid-js'
import { translit } from '../../utils/ru2en'

View File

@ -1,6 +1,6 @@
import { createMemo, For } from 'solid-js'
import './Footer.scss'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import Subscribe from './Subscribe'
import { t } from '../../utils/intl'
import { locale as locstore } from '../../stores/ui'

View File

@ -6,7 +6,7 @@ import { AuthorCard } from '../Author/Card'
import { TopicCard } from '../Topic/Card'
import './Beside.scss'
import type { Author, Shout, Topic, User } from '../../graphql/types.gen'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import { t } from '../../utils/intl'
interface BesideProps {

View File

@ -1,13 +1,17 @@
import { t } from '../../utils/intl'
import { createEffect, createMemo, createSignal, onMount } from 'solid-js'
import { createMemo } from 'solid-js'
import { For, Show } from 'solid-js/web'
import type { Author, Shout, Topic } from '../../graphql/types.gen'
import type { Shout } from '../../graphql/types.gen'
import { capitalize } from '../../utils'
import { translit } from '../../utils/ru2en'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import './Card.scss'
import { locale as localestore } from '../../stores/ui'
import { useStore } from '@nanostores/solid'
import { handleClientRouteLinkClick } from '../../stores/router'
import { getLogger } from '../../utils/logger'
const log = getLogger('card component')
interface ArticleCardProps {
settings?: {
@ -24,45 +28,43 @@ interface ArticleCardProps {
article: Shout
}
export const ArticleCard = (props: ArticleCardProps) => {
const locale = useStore(localestore)
const getTitleAndSubtitle = (article: Shout): { title: string; subtitle: string } => {
let title = article.title
let subtitle = article.subtitle
const [title, setTitle] = createSignal<string>('')
const [subtitle, setSubtitle] = createSignal<string>('')
if (!subtitle) {
let tt = article.title?.split('. ') || []
const article = createMemo<Shout>(() => props.article)
const authors = createMemo<Author[]>(() => article().authors)
const mainTopic = createMemo<Topic>(() =>
props.article.topics.find((articleTopic) => articleTopic.slug === props.article.mainTopic)
)
if (tt?.length === 1) {
tt = article.title?.split(/{!|\?|:|;}\s/) || []
}
const detectSubtitle = () => {
const a = article()
setTitle(a.title || '')
if (!a.subtitle) {
let tt: string[] = a.title?.split('. ') || []
if (tt?.length === 1) tt = a.title?.split(/{!|\?|:|;}\s/) || []
if (tt && tt.length > 1) {
const sep = a.title?.replace(tt[0], '').split(' ', 1)[0]
setTitle(tt[0] + (!(sep === '.' || sep === ':') ? sep : ''))
setSubtitle(capitalize(a.title?.replace(tt[0] + sep, ''), true))
}
} else {
setSubtitle(a.subtitle || '')
if (tt && tt.length > 1) {
const sep = article.title?.replace(tt[0], '').split(' ', 1)[0]
title = tt[0] + (!(sep === '.' || sep === ':') ? sep : '')
subtitle = capitalize(article.title?.replace(tt[0] + sep, ''), true)
}
}
// FIXME: move this to store action
const translateAuthors = () => {
const aaa = new Set(article().authors)
aaa.forEach((a) => {
a.name =
a.name === 'Дискурс' && locale() !== 'ru' ? 'Discours' : translit(a.name || '', locale() || 'ru')
})
return [...aaa]
}
createEffect(translateAuthors, [article(), locale()])
onMount(detectSubtitle)
return { title, subtitle }
}
export const ArticleCard = (props: ArticleCardProps) => {
const locale = useStore(localestore)
const mainTopic = props.article.topics.find(
(articleTopic) => articleTopic.slug === props.article.mainTopic
)
const formattedDate = createMemo<string>(() => {
return new Date(props.article.createdAt)
.toLocaleDateString(locale(), { month: 'long', day: 'numeric', year: 'numeric' })
.replace(' г.', '')
})
const { title, subtitle } = getTitleAndSubtitle(props.article)
const { cover, layout, slug, authors, stat } = props.article
return (
<section
@ -73,49 +75,43 @@ export const ArticleCard = (props: ArticleCardProps) => {
'shout-card--feed': props.settings?.isFeedMode
}}
>
<Show when={mainTopic()}>
<Show when={!props.settings?.noimage && props.article.cover}>
<Show when={mainTopic}>
<Show when={!props.settings?.noimage && cover}>
<div class="shout-card__cover-container">
<div class="shout-card__cover">
<img src={props.article.cover || ''} alt={props.article.title || ''} loading="lazy" />
<img src={cover || ''} alt={title || ''} loading="lazy" />
</div>
</div>
</Show>
<div class="shout-card__content">
<Show
when={
props.article.layout &&
props.article.layout !== 'article' &&
!(props.settings?.noicon || props.settings?.noimage)
}
when={layout && layout !== 'article' && !(props.settings?.noicon || props.settings?.noimage)}
>
<div class="shout-card__type">
<a href={`/topic/${props.article.mainTopic}`}>
<Icon name={props.article.layout} />
<a href={`/topic/${mainTopic.slug}`}>
<Icon name={layout} />
</a>
</div>
</Show>
<Show when={!props.settings?.isGroup}>
<div class="shout__topic">
<a href={`/topic/${mainTopic().slug}`}>
{locale() === 'ru' && mainTopic().title
? mainTopic().title
: mainTopic().slug.replace('-', ' ')}
<a href={`/topic/${mainTopic.slug}`}>
{locale() === 'ru' && mainTopic.title ? mainTopic.title : mainTopic.slug.replace('-', ' ')}
</a>
</div>
</Show>
<div class="shout-card__titles-container">
<a href={`/${props.article.slug || ''}`}>
<a href={`/${slug || ''}`} onClick={handleClientRouteLinkClick}>
<div class="shout-card__title">
<span class="shout-card__link-container">{title()}</span>
<span class="shout-card__link-container">{title}</span>
</div>
<Show when={!props.settings?.nosubtitle && subtitle()}>
<Show when={!props.settings?.nosubtitle && subtitle}>
<div class="shout-card__subtitle">
<span class="shout-card__link-container">{subtitle()}</span>
<span class="shout-card__link-container">{subtitle}</span>
</div>
</Show>
</a>
@ -125,23 +121,26 @@ export const ArticleCard = (props: ArticleCardProps) => {
<div class="shout__details">
<Show when={!props.settings?.noauthor}>
<div class="shout__author">
<For each={authors()}>
{(a: Author) => (
<>
<Show when={authors().indexOf(a) > 0}>, </Show>
<a href={`/author/${a.slug}`}>{a.name}</a>
</>
)}
<For each={authors}>
{(author, index) => {
const name =
author.name === 'Дискурс' && locale() !== 'ru'
? 'Discours'
: translit(author.name || '', locale() || 'ru')
return (
<>
<Show when={index() > 0}>, </Show>
<a href={`/author/${author.slug}`}>{name}</a>
</>
)
}}
</For>
</div>
</Show>
<Show when={!props.settings?.nodate}>
<div class="shout__date">
{new Date(props.article.createdAt)
.toLocaleDateString(locale(), { month: 'long', day: 'numeric', year: 'numeric' })
.replace(' г.', '')}
</div>
<div class="shout__date">{formattedDate()}</div>
</Show>
</div>
</Show>
@ -151,17 +150,17 @@ export const ArticleCard = (props: ArticleCardProps) => {
<div class="shout-card__details-content">
<div class="shout-card__details-item rating">
<button class="rating__control">&minus;</button>
<span class="rating__value">{props.article.stat?.rating || ''}</span>
<span class="rating__value">{stat?.rating || ''}</span>
<button class="rating__control">+</button>
</div>
<div class="shout-card__details-item shout-card__comments">
<Icon name="eye" />
{props.article.stat?.viewed}
{stat?.viewed}
</div>
<div class="shout-card__details-item shout-card__comments">
<a href={`/${props.article.slug + '#comments' || ''}`}>
<a href={`/${slug + '#comments' || ''}`}>
<Icon name="comment" />
{props.article.stat?.commented || ''}
{stat?.commented || ''}
</a>
</div>

View File

@ -1,19 +1,25 @@
import { For } from 'solid-js'
import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './Card'
import { getLogger } from '../../utils/logger'
export default (props: { articles: Shout[] }) => (
<div class="floor floor--1">
<div class="wide-container row">
<div class="col-md-3">
<For each={props.articles.slice(0, 2)}>{(a) => <ArticleCard article={a} />}</For>
</div>
<div class="col-md-6">
<For each={props.articles.slice(2, 3)}>{(a) => <ArticleCard article={a} />}</For>
</div>
<div class="col-md-3">
<For each={props.articles.slice(3, 5)}>{(a) => <ArticleCard article={a} />}</For>
const log = getLogger('Row5')
export const Row5 = (props: { articles: Shout[] }) => {
return (
<div class="floor floor--1">
<div class="wide-container row">
<div class="col-md-3">
<ArticleCard article={props.articles[0]} />
<ArticleCard article={props.articles[1]} />
</div>
<div class="col-md-6">
<ArticleCard article={props.articles[2]} />
</div>
<div class="col-md-3">
<ArticleCard article={props.articles[3]} />
<ArticleCard article={props.articles[4]} />
</div>
</div>
</div>
</div>
)
)
}

View File

@ -4,7 +4,7 @@ import type { Author } from '../../graphql/types.gen'
import { session } from '../../stores/auth'
import { useAuthorsStore } from '../../stores/zine/authors'
import { t } from '../../utils/intl'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import { useTopicsStore } from '../../stores/zine/topics'
import { useArticlesStore } from '../../stores/zine/articles'
import { useSeenStore } from '../../stores/zine/seen'

View File

@ -8,7 +8,7 @@ import 'swiper/scss/pagination'
import './Slider.scss'
import type { Shout } from '../../graphql/types.gen'
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
interface SliderProps {
title?: string

View File

@ -0,0 +1,20 @@
import type { JSX } from 'solid-js'
import { Header } from '../Nav/Header'
import { Footer } from '../Discours/Footer'
import '../../styles/app.scss'
type Props = {
headerTitle?: string
children: JSX.Element
}
export const MainLayout = (props: Props) => {
return (
<>
<Header title={props.headerTitle} />
<main class="main-content">{props.children}</main>
<Footer />
</>
)
}

View File

@ -1,5 +1,5 @@
import { Show } from 'solid-js/web'
import Icon from './Icon'
import { Icon } from './Icon'
import { createEffect, createSignal, onMount } from 'solid-js'
import './AuthModal.scss'
import { Form } from 'solid-js-form'
@ -29,7 +29,7 @@ const titles = {
password: t('Enter your new password')
}
const isProperEmail = (email) => email && email.length > 5 && email.includes('@') && email.includes('.')
// const isProperEmail = (email) => email && email.length > 5 && email.includes('@') && email.includes('.')
// FIXME !!!
// eslint-disable-next-line sonarjs/cognitive-complexity

View File

@ -1,24 +1,35 @@
import { For, Show, createSignal, createMemo, createEffect, onMount, onCleanup } from 'solid-js'
import Private from './Private'
import Notifications from './Notifications'
import Icon from './Icon'
import { Icon } from './Icon'
import { Modal } from './Modal'
import AuthModal from './AuthModal'
import { t } from '../../utils/intl'
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
import { useStore } from '@nanostores/solid'
import { session as ssession } from '../../stores/auth'
import { handleClientRouteLinkClick, router } from '../../stores/router'
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
import './Header.scss'
import { getPagePath } from '@nanostores/router'
import { getLogger } from '../../utils/logger'
const resources = [
{ name: t('zine'), href: '/' },
{ name: t('feed'), href: '/feed' },
{ name: t('topics'), href: '/topics' }
//{ name: t('community'), href: '/community' }
const log = getLogger('header')
const resources: { name: string; route: keyof Routes }[] = [
{ name: t('zine'), route: 'home' },
{ name: t('feed'), route: 'feed' },
{ name: t('topics'), route: 'topics' }
]
export const Header = () => {
const handleEnterClick = () => {
showModal('auth')
}
type Props = {
title?: string
}
export const Header = (props: Props) => {
// signals
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
const [getIsScrolled, setIsScrolled] = createSignal(false)
@ -28,8 +39,9 @@ export const Header = () => {
const { getWarnings } = useWarningsStore()
const session = useStore(ssession)
const { getModal } = useModalStore()
const routing = useStore(router)
const subpath = createMemo(() => routing().path)
const { getPage } = useRouter()
// methods
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
const toggleFixed = () => setFixed(!fixed())
@ -45,19 +57,13 @@ export const Header = () => {
// derived
const authorized = createMemo(() => session()?.user?.slug)
const handleEnterClick = (ev) => {
showModal('auth')
handleClientRouteLinkClick(ev)
}
const handleBellIconClick = (ev) => {
const handleBellIconClick = () => {
if (!authorized()) {
handleEnterClick(ev)
showModal('auth')
return
}
toggleWarnings()
handleClientRouteLinkClick(ev)
}
onMount(() => {
@ -88,22 +94,18 @@ export const Header = () => {
<div class="wide-container">
<nav class="row header__inner" classList={{ fixed: fixed() }}>
<div class="main-logo col-auto">
<a href="/" onClick={handleClientRouteLinkClick}>
<a href={getPagePath(router, 'home')} onClick={handleClientRouteLinkClick}>
<img src="/logo.svg" alt={t('Discours')} />
</a>
</div>
<div class="col main-navigation">
{/*FIXME article header*/}
<div class="article-header">
Дискурс независимый художественно-аналитический журнал с горизонтальной редакцией,
основанный на принципах свободы слова, прямой демократии и совместного редактирования.
</div>
<div class="article-header">{props.title}</div>
<ul class="text-xl inline-flex" classList={{ fixed: fixed() }}>
<ul class="col main-navigation text-xl inline-flex" classList={{ fixed: fixed() }}>
<For each={resources}>
{(r: { href: string; name: string }) => (
<li classList={{ selected: r.href === subpath() }}>
<a href={r.href} onClick={handleClientRouteLinkClick}>
{(r) => (
<li classList={{ selected: r.route === getPage().route }}>
<a href={getPagePath(router, r.route, null)} onClick={handleClientRouteLinkClick}>
{r.name}
</a>
</li>

View File

@ -1,7 +1,7 @@
import { mergeProps, Show } from 'solid-js'
import './Icon.css'
export default (_props: any) => {
export const Icon = (_props: any) => {
const props = mergeProps({ title: '', counter: 0 }, _props)
return (

View File

@ -1,14 +1,15 @@
import type { Author } from '../../graphql/types.gen'
import Userpic from '../Author/Userpic'
import Icon from './Icon'
import { Icon } from './Icon'
import './Private.scss'
import { session as sesstore } from '../../stores/auth'
import { useStore } from '@nanostores/solid'
import { router } from '../../stores/router'
import { useRouter } from '../../stores/router'
export default () => {
const session = useStore(sesstore)
const routing = useStore(router)
const { getPage } = useRouter()
return (
<div class="usercontrol col">
<div class="usercontrol__item usercontrol__item--write-post">
@ -24,14 +25,16 @@ export default () => {
</div>
<div class="usercontrol__item usercontrol__item--inbox">
<a href="/inbox">
<div classList={{ entered: routing().path === '/inbox' }}>
{/*FIXME: replace with route*/}
<div classList={{ entered: getPage().path === '/inbox' }}>
<Icon name="inbox-white" counter={session().info?.unread || 0} />
</div>
</a>
</div>
<div class="usercontrol__item">
<a href={`/${session().user?.slug}`}>
<div classList={{ entered: routing().path === `/${session().user?.slug}` }}>
{/*FIXME: replace with route*/}
<div classList={{ entered: getPage().path === `/${session().user?.slug}` }}>
<Userpic user={session().user as Author} />
</div>
</a>

View File

@ -1,20 +1,21 @@
import { For, Show } from 'solid-js'
import type { Topic } from '../../graphql/types.gen'
import Icon from './Icon'
import { Icon } from './Icon'
import './Topics.scss'
import { t } from '../../utils/intl'
import { locale as langstore } from '../../stores/ui'
import { useStore } from '@nanostores/solid'
export default (props: { topics: Topic[] }) => {
export const NavTopics = (props: { topics: Topic[] }) => {
const locale = useStore(langstore)
const tag = (t: Topic) => (/[ЁА-яё]/.test(t.title || '') && locale() !== 'ru' ? t.slug : t.title)
// TODO: something about subtopics
return (
<nav class="subnavigation wide-container text-2xl">
<ul class="topics">
<Show when={!!props.topics}>
<Show when={props.topics.length > 0}>
<For each={props.topics}>
{(t: Topic) => (
<li class="item">

View File

@ -0,0 +1,14 @@
import { MainLayout } from '../Layouts/MainLayout'
import { AllAuthorsView } from '../Views/AllAuthors'
import type { PageProps } from '../types'
export const AllAuthorsPage = (props: PageProps) => {
return (
<MainLayout>
<AllAuthorsView authors={props.authors} />
</MainLayout>
)
}
// for lazy loading
export default AllAuthorsPage

View File

@ -0,0 +1,14 @@
import { MainLayout } from '../Layouts/MainLayout'
import { AllTopicsView } from '../Views/AllTopics'
import type { PageProps } from '../types'
export const AllTopicsPage = (props: PageProps) => {
return (
<MainLayout>
<AllTopicsView topics={props.topics} />
</MainLayout>
)
}
// for lazy loading
export default AllTopicsPage

View File

@ -0,0 +1,46 @@
import { MainLayout } from '../Layouts/MainLayout'
import { ArticleView } from '../Views/Article'
import type { PageProps } from '../types'
import { loadArticle, useArticlesStore } from '../../stores/zine/articles'
import { createMemo, onMount, Show } from 'solid-js'
import { t } from '../../utils/intl'
import type { Shout } from '../../graphql/types.gen'
import { useRouter } from '../../stores/router'
export const ArticlePage = (props: PageProps) => {
const sortedArticles = props.article ? [props.article] : []
const { getPage } = useRouter()
const page = getPage()
if (page.route !== 'article') {
throw new Error('ts guard')
}
const { getArticleEntities } = useArticlesStore({
sortedArticles
})
const article = createMemo<Shout>(() => getArticleEntities()[page.params.slug])
onMount(() => {
const slug = page.params.slug
const article = getArticleEntities()[slug]
if (!article || !article.body) {
loadArticle({ slug })
}
})
return (
<MainLayout headerTitle={article()?.title || ''}>
<Show when={Boolean(article())} fallback={t('Loading')}>
<ArticleView article={article()} />
</Show>
</MainLayout>
)
}
// for lazy loading
export default ArticlePage

View File

@ -0,0 +1,14 @@
import { MainLayout } from '../Layouts/MainLayout'
import { AuthorView } from '../Views/Author'
import type { PageProps } from '../types'
export const AuthorPage = (props: PageProps) => {
return (
<MainLayout>
<AuthorView author={props.author} authorArticles={props.articles} />
</MainLayout>
)
}
// for lazy loading
export default AuthorPage

View File

@ -0,0 +1,14 @@
import { MainLayout } from '../Layouts/MainLayout'
import { FeedView } from '../Views/Feed'
import type { PageProps } from '../types'
export const FeedPage = (props: PageProps) => {
return (
<MainLayout>
<FeedView articles={props.articles} />
</MainLayout>
)
}
// for lazy loading
export default FeedPage

View File

@ -0,0 +1,13 @@
import { FourOuFourView } from '../Views/FourOuFour'
import { MainLayout } from '../Layouts/MainLayout'
export const FourOuFourPage = () => {
return (
<MainLayout>
<FourOuFourView />
</MainLayout>
)
}
// for lazy loading
export default FourOuFourPage

View File

@ -0,0 +1,14 @@
import { HomeView } from '../Views/Home'
import { MainLayout } from '../Layouts/MainLayout'
import type { PageProps } from '../types'
export const HomePage = (props: PageProps) => {
return (
<MainLayout>
<HomeView randomTopics={props.randomTopics} recentPublishedArticles={props.articles || []} />
</MainLayout>
)
}
// for lazy loading
export default HomePage

View File

@ -0,0 +1,14 @@
import { MainLayout } from '../Layouts/MainLayout'
import { SearchView } from '../Views/Search'
import type { PageProps } from '../types'
export const SearchPage = (props: PageProps) => {
return (
<MainLayout>
<SearchView results={props.searchResults || []} query={props.searchQuery} />
</MainLayout>
)
}
// for lazy loading
export default SearchPage

View File

@ -0,0 +1,14 @@
import { MainLayout } from '../Layouts/MainLayout'
import { TopicView } from '../Views/Topic'
import type { PageProps } from '../types'
export const TopicPage = (props: PageProps) => {
return (
<MainLayout>
<TopicView topic={props.topic} topicArticles={props.articles} />
</MainLayout>
)
}
// for lazy loading
export default TopicPage

64
src/components/Root.tsx Normal file
View File

@ -0,0 +1,64 @@
// FIXME: breaks on vercel, research
// import 'solid-devtools'
import { Component, createMemo, lazy } from 'solid-js'
import { Routes, useRouter } from '../stores/router'
import { Dynamic } from 'solid-js/web'
import { getLogger } from '../utils/logger'
import type { PageProps } from './types'
// do not remove
// for debugging, to disable lazy loading
// import HomePage from './Pages/HomePage'
// import AllTopicsPage from './Pages/AllTopicsPage'
// import TopicPage from './Pages/TopicPage'
// import AllAuthorsPage from './Pages/AllAuthorsPage'
// import AuthorPage from './Pages/AuthorPage'
// import FeedPage from './Pages/FeedPage'
// import ArticlePage from './Pages/ArticlePage'
// import SearchPage from './Pages/SearchPage'
// import FourOuFourPage from './Pages/FourOuFourPage'
const HomePage = lazy(() => import('./Pages/HomePage'))
const AllTopicsPage = lazy(() => import('./Pages/AllTopicsPage'))
const TopicPage = lazy(() => import('./Pages/TopicPage'))
const AllAuthorsPage = lazy(() => import('./Pages/AllAuthorsPage'))
const AuthorPage = lazy(() => import('./Pages/AuthorPage'))
const FeedPage = lazy(() => import('./Pages/FeedPage'))
const ArticlePage = lazy(() => import('./Pages/ArticlePage'))
const SearchPage = lazy(() => import('./Pages/SearchPage'))
const FourOuFourPage = lazy(() => import('./Pages/FourOuFourPage'))
const log = getLogger('root')
const pagesMap: Record<keyof Routes, Component<PageProps>> = {
home: HomePage,
topics: AllTopicsPage,
topic: TopicPage,
authors: AllAuthorsPage,
author: AuthorPage,
feed: FeedPage,
article: ArticlePage,
search: SearchPage
}
export const Root = (props: PageProps) => {
const { getPage } = useRouter()
// log.debug({ route: getPage().route })
const pageComponent = createMemo(() => {
const result = pagesMap[getPage().route]
// log.debug('page', getPage())
if (!result) {
return FourOuFourPage
}
return result
})
// TODO: move MainLayout here
return <Dynamic component={pageComponent()} {...props} />
}

View File

@ -1,5 +1,5 @@
import type { Topic } from '../../graphql/types.gen'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import './FloorHeader.scss'
import { t } from '../../utils/intl'

View File

@ -3,26 +3,34 @@ import type { Author } from '../../graphql/types.gen'
import { AuthorCard } from '../Author/Card'
import { byFirstChar, sortBy } from '../../utils/sortby'
import { groupByName } from '../../utils/groupby'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import { t } from '../../utils/intl'
import { useAuthorsStore } from '../../stores/zine/authors'
import { params as paramsStore, handleClientRouteLinkClick } from '../../stores/router'
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
import { session } from '../../stores/auth'
import { useStore } from '@nanostores/solid'
import '../../styles/AllTopics.scss'
export const AllAuthorsPage = (props: any) => {
const { getSortedAuthors: authorslist } = useAuthorsStore(props.authors)
type AllAuthorsPageSearchParams = {
by: '' | 'name' | 'shouts' | 'rating'
}
type Props = {
authors: Author[]
}
export const AllAuthorsView = (props: Props) => {
const { getSortedAuthors: authorslist } = useAuthorsStore({ authors: props.authors })
const [sortedAuthors, setSortedAuthors] = createSignal<Author[]>([])
const [sortedKeys, setSortedKeys] = createSignal<string[]>([])
const [abc, setAbc] = createSignal([])
const auth = useStore(session)
const subscribed = (s) => Boolean(auth()?.info?.authors && auth()?.info?.authors?.includes(s || ''))
const params = useStore(paramsStore)
const { getSearchParams } = useRouter<AllAuthorsPageSearchParams>()
createEffect(() => {
if ((!params()['by'] || params()['by'] === 'abc') && abc().length === 0) {
if ((!getSearchParams().by || getSearchParams().by === 'name') && abc().length === 0) {
console.log('[authors] default grouping by abc')
const grouped = { ...groupByName(authorslist()) }
grouped['A-Z'] = sortBy(grouped['A-Z'], byFirstChar)
@ -31,10 +39,10 @@ export const AllAuthorsPage = (props: any) => {
keys.sort()
setSortedKeys(keys as string[])
} else {
console.log('[authors] sorting by ' + params()['by'])
setSortedAuthors(sortBy(authorslist(), params()['by']))
console.log('[authors] sorting by ' + getSearchParams().by)
setSortedAuthors(sortBy(authorslist(), getSearchParams().by))
}
}, [authorslist(), params()])
}, [authorslist(), getSearchParams().by])
return (
<div class="all-topics-page">
@ -50,17 +58,17 @@ export const AllAuthorsPage = (props: any) => {
<div class="row">
<div class="col">
<ul class="view-switcher">
<li classList={{ selected: params()['by'] === 'shouts' }}>
<li classList={{ selected: getSearchParams().by === 'shouts' }}>
<a href="/authors?by=shouts" onClick={handleClientRouteLinkClick}>
{t('By shouts')}
</a>
</li>
<li classList={{ selected: params()['by'] === 'rating' }}>
<li classList={{ selected: getSearchParams().by === 'rating' }}>
<a href="/authors?by=rating" onClick={handleClientRouteLinkClick}>
{t('By rating')}
</a>
</li>
<li classList={{ selected: !params()['by'] || params()['by'] === 'abc' }}>
<li classList={{ selected: !getSearchParams().by || getSearchParams().by === 'name' }}>
<a href="/authors" onClick={handleClientRouteLinkClick}>
{t('By alphabet')}
</a>
@ -73,7 +81,7 @@ export const AllAuthorsPage = (props: any) => {
</li>
</ul>
<Show
when={!params()['by'] || params()['by'] === 'abc'}
when={!getSearchParams().by || getSearchParams().by === 'name'}
fallback={() => (
<div class="stats">
<For each={sortedAuthors()}>

View File

@ -1,41 +1,36 @@
import { createEffect, createSignal, For, Show } from 'solid-js'
import { createEffect, For, Show } from 'solid-js'
import type { Topic } from '../../graphql/types.gen'
import { byFirstChar, sortBy } from '../../utils/sortby'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import { t } from '../../utils/intl'
import { useTopicsStore } from '../../stores/zine/topics'
import { params as paramstore, handleClientRouteLinkClick, router } from '../../stores/router'
import { setSortAllTopicsBy, useTopicsStore } from '../../stores/zine/topics'
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
import { TopicCard } from '../Topic/Card'
import { session } from '../../stores/auth'
import { useStore } from '@nanostores/solid'
import '../../styles/AllTopics.scss'
import { groupByTitle } from '../../utils/groupby'
export const AllTopicsPage = (props: { topics?: Topic[] }) => {
const [, setSortedTopics] = createSignal<Partial<Topic>[]>([])
const [, setSortedKeys] = createSignal<string[]>()
const [abc, setAbc] = createSignal([])
const { getSortedTopics } = useTopicsStore({ topics: props.topics || [] })
type AllTopicsPageSearchParams = {
by: 'shouts' | 'authors' | 'title' | ''
}
type Props = {
topics: Topic[]
}
export const AllTopicsView = (props: Props) => {
const { getSearchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
const { getSortedTopics } = useTopicsStore({
topics: props.topics,
sortBy: getSearchParams().by || 'shouts'
})
const auth = useStore(session)
const subscribed = (s) => Boolean(auth()?.info?.topics && auth()?.info?.topics?.includes(s || ''))
const params = useStore(paramstore)
createEffect(() => {
if (abc().length === 0 && (!params()['by'] || params()['by'] === 'abc')) {
console.log('[topics] default grouping by abc')
const grouped = { ...groupByTitle(getSortedTopics()) }
grouped['A-Z'] = sortBy(grouped['A-Z'], byFirstChar)
setAbc(grouped)
const keys = Object.keys(abc)
keys.sort()
setSortedKeys(keys as string[])
} else {
console.log('[topics] sorting by ' + params()['by'])
setSortedTopics(sortBy(getSortedTopics(), params()['by']))
}
}, [getSortedTopics(), params()])
setSortAllTopicsBy(getSearchParams().by || 'shouts')
})
const subscribed = (s) => Boolean(auth()?.info?.topics && auth()?.info?.topics?.includes(s || ''))
return (
<div class="all-topics-page">
@ -52,18 +47,25 @@ export const AllTopicsPage = (props: { topics?: Topic[] }) => {
<div class="row">
<div class="col">
<ul class="view-switcher">
<li classList={{ selected: params()['by'] === 'shouts' }}>
<li classList={{ selected: getSearchParams().by === 'shouts' || !getSearchParams().by }}>
<a href="/topics?by=shouts" onClick={handleClientRouteLinkClick}>
{t('By shouts')}
</a>
</li>
<li classList={{ selected: params()['by'] === 'authors' }}>
<li classList={{ selected: getSearchParams().by === 'authors' }}>
<a href="/topics?by=authors" onClick={handleClientRouteLinkClick}>
{t('By authors')}
</a>
</li>
<li classList={{ selected: params()['by'] === 'abc' }}>
<a href="/topics" onClick={handleClientRouteLinkClick}>
<li classList={{ selected: getSearchParams().by === 'title' }}>
<a
href="/topics?by=title"
onClick={(ev) => {
// just an example
ev.preventDefault()
changeSearchParam('by', 'title')
}}
>
{t('By alphabet')}
</a>
</li>

View File

@ -2,31 +2,27 @@ import { createEffect, createSignal, Show, Suspense } from 'solid-js'
import { FullArticle } from '../Article/FullArticle'
import { t } from '../../utils/intl'
import type { Reaction, Shout } from '../../graphql/types.gen'
import { useCurrentArticleStore } from '../../stores/zine/currentArticle'
import type { Shout } from '../../graphql/types.gen'
import { loadArticleReactions, useReactionsStore } from '../../stores/zine/reactions'
import '../../styles/Article.scss'
interface ArticlePageProps {
article: Shout
slug: string
reactions?: Reaction[]
}
const ARTICLE_COMMENTS_PAGE_SIZE = 50
export const ArticlePage = (props: ArticlePageProps) => {
const { getCurrentArticle } = useCurrentArticleStore({ currentArticle: props.article })
export const ArticleView = (props: ArticlePageProps) => {
const [getCommentsPage] = createSignal(1)
const [getIsCommentsLoading, setIsCommentsLoading] = createSignal(false)
const reactionslist = useReactionsStore(props.reactions)
const reactionslist = useReactionsStore()
createEffect(async () => {
try {
setIsCommentsLoading(true)
await loadArticleReactions({
articleSlug: props.slug,
articleSlug: props.article.slug,
limit: ARTICLE_COMMENTS_PAGE_SIZE,
offset: getCommentsPage() * ARTICLE_COMMENTS_PAGE_SIZE
})
@ -37,11 +33,11 @@ export const ArticlePage = (props: ArticlePageProps) => {
return (
<div class="article-page">
<Show fallback={<div class="center">{t('Loading')}</div>} when={getCurrentArticle()}>
<Show fallback={<div class="center">{t('Loading')}</div>} when={props.article}>
<Suspense>
<FullArticle
article={getCurrentArticle()}
reactions={reactionslist().filter((r) => r.shout.slug === props.slug)}
article={props.article}
reactions={reactionslist().filter((r) => r.shout.slug === props.article.slug)}
isCommentsLoading={getIsCommentsLoading()}
/>
</Suspense>

View File

@ -1,17 +1,16 @@
import { Show, createMemo } from 'solid-js'
import type { Author, Reaction, Shout } from '../../graphql/types.gen'
import type { Author, Shout } from '../../graphql/types.gen'
import Row2 from '../Feed/Row2'
import Row3 from '../Feed/Row3'
import Beside from '../Feed/Beside'
// import Beside from '../Feed/Beside'
import AuthorFull from '../Author/Full'
import { t } from '../../utils/intl'
import { useAuthorsStore } from '../../stores/zine/authors'
import { params } from '../../stores/router'
import { useArticlesStore } from '../../stores/zine/articles'
import '../../styles/Topic.scss'
import { useStore } from '@nanostores/solid'
import { useTopicsStore } from '../../stores/zine/topics'
// import { useTopicsStore } from '../../stores/zine/topics'
import { useRouter } from '../../stores/router'
// TODO: load reactions on client
type AuthorProps = {
@ -21,15 +20,18 @@ type AuthorProps = {
// topics: Topic[]
}
export const AuthorPage = (props: AuthorProps) => {
type AuthorPageSearchParams = {
by: '' | 'viewed' | 'rating' | 'commented' | 'recent'
}
export const AuthorView = (props: AuthorProps) => {
const { getSortedArticles: articles } = useArticlesStore({
sortedArticles: props.authorArticles
})
const { getAuthorEntities: authors } = useAuthorsStore({ authors: [props.author] })
const { getTopicsByAuthor } = useTopicsStore()
const author = createMemo(() => authors()[props.author.slug])
const args = useStore(params)
const { getSearchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
//const slug = createMemo(() => author().slug)
/*
@ -41,7 +43,7 @@ export const AuthorPage = (props: AuthorProps) => {
*/
const title = createMemo(() => {
const m = args()['by']
const m = getSearchParams().by
if (m === 'viewed') return t('Top viewed')
if (m === 'rating') return t('Top rated')
if (m === 'commented') return t('Top discussed')
@ -55,23 +57,23 @@ export const AuthorPage = (props: AuthorProps) => {
<div class="row group__controls">
<div class="col-md-8">
<ul class="view-switcher">
<li classList={{ selected: !args()['by'] || args()['by'] === 'recent' }}>
<button type="button" onClick={() => (args()['by'] = 'recent')}>
<li classList={{ selected: !getSearchParams().by || getSearchParams().by === 'recent' }}>
<button type="button" onClick={() => changeSearchParam('by', 'recent')}>
{t('Recent')}
</button>
</li>
<li classList={{ selected: args()['by'] === 'rating' }}>
<button type="button" onClick={() => (args()['by'] = 'rating')}>
<li classList={{ selected: getSearchParams().by === 'rating' }}>
<button type="button" onClick={() => changeSearchParam('by', 'rating')}>
{t('Popular')}
</button>
</li>
<li classList={{ selected: args()['by'] === 'viewed' }}>
<button type="button" onClick={() => (args()['by'] = 'viewed')}>
<li classList={{ selected: getSearchParams().by === 'viewed' }}>
<button type="button" onClick={() => changeSearchParam('by', 'viewed')}>
{t('Views')}
</button>
</li>
<li classList={{ selected: args()['by'] === 'commented' }}>
<button type="button" onClick={() => (args()['by'] = 'commented')}>
<li classList={{ selected: getSearchParams().by === 'commented' }}>
<button type="button" onClick={() => changeSearchParam('by', 'commented')}>
{t('Discussing')}
</button>
</li>
@ -89,13 +91,14 @@ export const AuthorPage = (props: AuthorProps) => {
<h3 class="col-12">{title()}</h3>
<div class="row">
<Show when={articles()?.length > 0}>
<Beside
title={t('Topics which supported by author')}
values={getTopicsByAuthor()[author().slug].slice(0, 5)}
beside={articles()[0]}
wrapper={'topic'}
topicShortDescription={true}
/>
{/*FIXME*/}
{/*<Beside*/}
{/* title={t('Topics which supported by author')}*/}
{/* values={getTopicsByAuthor()[author().slug].slice(0, 5)}*/}
{/* beside={articles()[0]}*/}
{/* wrapper={'topic'}*/}
{/* topicShortDescription={true}*/}
{/*/>*/}
<Row3 articles={articles().slice(1, 4)} />
<Row2 articles={articles().slice(4, 6)} />
<Row3 articles={articles().slice(10, 13)} />

View File

@ -1,21 +0,0 @@
import { createSignal, Show, Suspense } from 'solid-js'
import type { Reaction, Shout } from '../../graphql/types.gen'
import { t } from '../../utils/intl'
interface ArticlePageProps {
article: Shout
reactions?: Partial<Reaction>[]
}
export const ArticlePage = (props: ArticlePageProps) => {
const [article] = createSignal<Shout>(props.article)
return (
<div class="community-page">
<Show fallback={<div class="center">{t('Loading')}</div>} when={article()}>
<Suspense>{t('Community')}</Suspense>
</Show>
</div>
)
}
export default ArticlePage

View File

@ -1,6 +1,6 @@
import { Title } from '@solidjs/meta'
export default () => (
export const ConnectView = () => (
<>
<Title>Дискурс: Предложить идею</Title>

View File

@ -8,7 +8,7 @@ import { Sidebar } from '../Editor/Sidebar'
import ErrorView from '../Editor/Error'
import { newState } from '../Editor/store'
export default () => {
export const CreateView = () => {
const [store, ctrl] = createCtrl(newState())
const mouseEnterCoords = createMutable({ x: 0, y: 0 })

View File

@ -1,7 +1,7 @@
import { createMemo, For, Show } from 'solid-js'
import type { Shout, Reaction } from '../../graphql/types.gen'
import '../../styles/Feed.scss'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import { byCreated, sortBy } from '../../utils/sortby'
import { TopicCard } from '../Topic/Card'
import { ArticleCard } from '../Feed/Card'
@ -15,12 +15,11 @@ import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles
import { useReactionsStore } from '../../stores/zine/reactions'
import { useAuthorsStore } from '../../stores/zine/authors'
import { useTopicsStore } from '../../stores/zine/topics'
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
interface FeedProps {
articles: Shout[]
reactions: Reaction[]
limit?: number
offset?: number
reactions?: Reaction[]
}
// const AUTHORSHIP_REACTIONS = [
@ -30,12 +29,13 @@ interface FeedProps {
// ReactionKind.Ask
// ]
export const FeedPage = (props: FeedProps) => {
export const FeedView = (props: FeedProps) => {
// state
const { getSortedArticles: articles } = useArticlesStore({ sortedArticles: props.articles })
const reactions = useReactionsStore(props.reactions)
const { getTopAuthors, getSortedAuthors: authors } = useAuthorsStore()
const reactions = useReactionsStore()
const { getSortedAuthors: authors } = useAuthorsStore()
const { getTopTopics } = useTopicsStore()
const { getTopAuthors } = useTopAuthorsStore()
const auth = useStore(session)
@ -56,9 +56,10 @@ export const FeedPage = (props: FeedProps) => {
// eslint-disable-next-line unicorn/consistent-function-scoping
const loadMore = () => {
const limit = props.limit || 50
const offset = props.offset || 0
loadRecentArticles({ limit, offset })
// const limit = props.limit || 50
// const offset = props.offset || 0
// FIXME
loadRecentArticles({ limit: 50, offset: 0 })
}
return (
<>

View File

@ -1,18 +1,19 @@
import '../../styles/FeedSettings.scss'
import { t } from '../../utils/intl'
import { params } from '../../stores/router' // global routing signals
import { useStore } from '@nanostores/solid'
import { handleClientRouteLinkClick } from '../../stores/router'
export const FeedSettings = (props: any) => {
const args = useStore(params)
console.log('[feed-settings] setup articles by', args()['by'])
// type FeedSettingsSearchParams = {
// by: '' | 'topics' | 'authors' | 'reacted'
// }
export const FeedSettingsView = () => {
return (
<div class="container">
<h1>{t('Feed settings')}</h1>
<ul class="view-switcher">
<li class="selected">
<a href="?by=topics" onClick={() => (args()['by'] = 'topics')}>
<a href="?by=topics" onClick={handleClientRouteLinkClick}>
{t('topics')}
</a>
</li>
@ -22,12 +23,12 @@ export const FeedSettings = (props: any) => {
</a>
</li>*/}
<li>
<a href="?by=authors" onClick={() => (args()['by'] = 'authors')}>
<a href="?by=authors" onClick={handleClientRouteLinkClick}>
{t('authors')}
</a>
</li>
<li>
<a href="?by=reacted" onClick={() => (args()['by'] = 'reacted')}>
<a href="?by=reacted" onClick={handleClientRouteLinkClick}>
{t('reactions')}
</a>
</li>

View File

@ -1,8 +1,8 @@
import { t } from '../../utils/intl'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import '../../styles/FourOuFour.scss'
export const FourOuFour = (_props) => {
export const FourOuFourView = (_props) => {
return (
<div class="error-page-wrapper">
<div class="error-page">

View File

@ -1,7 +1,7 @@
import { createMemo, createSignal, Show, Suspense } from 'solid-js'
import { createMemo, For, onMount, Show } from 'solid-js'
import Banner from '../Discours/Banner'
import NavTopics from '../Nav/Topics'
import Row5 from '../Feed/Row5'
import { NavTopics } from '../Nav/Topics'
import { Row5 } from '../Feed/Row5'
import Row3 from '../Feed/Row3'
import Row2 from '../Feed/Row2'
import Row1 from '../Feed/Row1'
@ -10,151 +10,145 @@ import Beside from '../Feed/Beside'
import RowShort from '../Feed/RowShort'
import Slider from '../Feed/Slider'
import Group from '../Feed/Group'
import { getLogger } from '../../utils/logger'
import type { Shout, Topic } from '../../graphql/types.gen'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import { t } from '../../utils/intl'
import { useTopicsStore } from '../../stores/zine/topics'
import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors'
import { router } from '../../stores/router'
import {
loadPublishedArticles,
loadTopArticles,
loadTopMonthArticles,
useArticlesStore
} from '../../stores/zine/articles'
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
const log = getLogger('home view')
type HomeProps = {
randomTopics: Topic[]
recentPublishedArticles: Shout[]
topMonthArticles: Shout[]
topOverallArticles: Shout[]
limit?: number
offset?: number
}
// const LAYOUTS = ['article', 'prose', 'music', 'video', 'image']
export const HomePage = (props: HomeProps) => {
const [someLayout, setSomeLayout] = createSignal([] as Shout[])
const [selectedLayout, setSelectedLayout] = createSignal('article')
const [byLayout, setByLayout] = createSignal({} as { [layout: string]: Shout[] })
const [byTopic, setByTopic] = createSignal({} as { [topic: string]: Shout[] })
const CLIENT_LOAD_ARTICLES_COUNT = 30
const LOAD_MORE_ARTICLES_COUNT = 30
export const HomeView = (props: HomeProps) => {
const {
getSortedArticles,
getTopRatedArticles,
getTopRatedMonthArticles,
getTopArticles,
getTopMonthArticles,
getTopViewedArticles,
getTopCommentedArticles
getTopCommentedArticles,
getArticlesByLayout
} = useArticlesStore({
sortedArticles: props.recentPublishedArticles,
topRatedArticles: props.topOverallArticles,
topRatedMonthArticles: props.topMonthArticles
sortedArticles: props.recentPublishedArticles
})
const articles = createMemo(() => getSortedArticles())
const { getRandomTopics, getSortedTopics, getTopTopics } = useTopicsStore({
const { getRandomTopics, getTopTopics } = useTopicsStore({
randomTopics: props.randomTopics
})
const { getTopAuthors } = useAuthorsStore()
const { getTopAuthors } = useTopAuthorsStore()
// FIXME
// createEffect(() => {
// if (articles() && articles().length > 0 && Object.keys(byTopic()).length === 0) {
// console.debug('[home] ' + getRandomTopics().length.toString() + ' random topics loaded')
// console.debug('[home] ' + articles().length.toString() + ' overall shouts loaded')
// console.log('[home] preparing published articles...')
// // get shouts lists by
// const bl: { [key: string]: Shout[] } = {}
// const bt: { [key: string]: Shout[] } = {}
// articles().forEach((s: Shout) => {
// // by topic
// s.topics?.forEach(({ slug }: any) => {
// if (!bt[slug || '']) bt[slug || ''] = []
// bt[slug as string].push(s)
// })
// // by layout
// const l = s.layout || 'article'
// if (!bl[l]) bl[l] = []
// bl[l].push(s)
// })
// setByLayout(bl)
// setByTopic(bt)
// console.log('[home] some grouped articles are ready')
// }
// }, [articles()])
onMount(() => {
loadTopArticles()
loadTopMonthArticles()
loadPublishedArticles({ limit: CLIENT_LOAD_ARTICLES_COUNT, offset: getSortedArticles().length })
})
// FIXME
// createEffect(() => {
// if (Object.keys(byLayout()).length > 0 && getSortedTopics()) {
// // random special layout pick
// const special = LAYOUTS.filter((la) => la !== 'article')
// const layout = special[Math.floor(Math.random() * special.length)]
// setSomeLayout(byLayout()[layout])
// setSelectedLayout(layout)
// console.log(`[home] <${layout}> layout picked`)
// }
// }, [byLayout()])
const randomLayout = createMemo(() => {
const articlesByLayout = getArticlesByLayout()
const filledLayouts = Object.keys(articlesByLayout).filter(
// FIXME: is 7 ok? or more complex logic needed?
(layout) => articlesByLayout[layout].length > 7
)
const loadMore = () => {
const limit = props.limit || 50
const offset = props.offset || 0
loadPublishedArticles({ limit, offset })
}
return (
<Suspense fallback={t('Loading')}>
<Show when={articles().length > 0}>
<NavTopics topics={getRandomTopics()} />
<Row5 articles={articles().slice(0, 5)} />
<Hero />
<Beside
beside={articles().slice(5, 6)[0]}
title={t('Top viewed')}
values={getTopViewedArticles().slice(0, 5)}
wrapper={'top-article'}
/>
<Row3 articles={articles().slice(6, 9)} />
<Beside
beside={articles().slice(9, 10)[0]}
title={t('Top authors')}
values={getTopAuthors().slice(0, 5)}
wrapper={'author'}
/>
const randomLayout =
filledLayouts.length > 0 ? filledLayouts[Math.floor(Math.random() * filledLayouts.length)] : ''
<Slider title={t('Top month articles')} articles={getTopRatedMonthArticles()} />
<Row2 articles={articles().slice(10, 12)} />
<RowShort articles={articles().slice(12, 16)} />
<Row1 article={articles().slice(16, 17)[0]} />
<Row3 articles={articles().slice(17, 20)} />
<Row3 articles={getTopCommentedArticles()} header={<h2>{t('Top commented')}</h2>} />
return (
<Show when={Boolean(randomLayout)}>
<Group
articles={someLayout()}
articles={articlesByLayout[randomLayout]}
header={
<div class="layout-icon">
<Icon name={selectedLayout()} />
<Icon name={randomLayout} />
</div>
}
/>
<Slider title={t('Favorite')} articles={getTopRatedArticles()} />
<Beside
beside={articles().slice(20, 21)[0]}
title={t('Top topics')}
values={getTopTopics().slice(0, 5)}
wrapper={'topic'}
isTopicCompact={true}
/>
<Row3 articles={articles().slice(21, 24)} />
<Banner />
<Row2 articles={articles().slice(24, 26)} />
<Row3 articles={articles().slice(26, 29)} />
<Row2 articles={articles().slice(29, 31)} />
<Row3 articles={articles().slice(31, 34)} />
<p class="load-more-container">
<button class="button" onClick={loadMore}>
{t('Load more')}
</button>
</p>
</Show>
</Suspense>
)
})
const loadMore = () => {
loadPublishedArticles({ limit: LOAD_MORE_ARTICLES_COUNT, offset: getSortedArticles().length })
}
return (
<>
<NavTopics topics={getRandomTopics()} />
<Row5 articles={getSortedArticles().slice(0, 5)} />
<Hero />
<Beside
beside={getSortedArticles().slice(4, 5)[0]}
title={t('Top viewed')}
values={getTopViewedArticles().slice(0, 5)}
wrapper={'top-article'}
/>
<Row3 articles={getSortedArticles().slice(6, 9)} />
{/*FIXME: ?*/}
<Show when={getTopAuthors().length === 5}>
<Beside
beside={getSortedArticles().slice(8, 9)[0]}
title={t('Top authors')}
values={getTopAuthors()}
wrapper={'author'}
/>
</Show>
<Slider title={t('Top month articles')} articles={getTopMonthArticles()} />
<Row2 articles={getSortedArticles().slice(10, 12)} />
<RowShort articles={getSortedArticles().slice(12, 16)} />
<Row1 article={getSortedArticles().slice(15, 16)[0]} />
<Row3 articles={getSortedArticles().slice(17, 20)} />
<Row3 articles={getTopCommentedArticles()} header={<h2>{t('Top commented')}</h2>} />
{randomLayout()}
<Slider title={t('Favorite')} articles={getTopArticles()} />
<Beside
beside={getSortedArticles().slice(19, 20)[0]}
title={t('Top topics')}
values={getTopTopics().slice(0, 5)}
wrapper={'topic'}
isTopicCompact={true}
/>
<Row3 articles={getSortedArticles().slice(21, 24)} />
<Banner />
<Row2 articles={getSortedArticles().slice(24, 26)} />
<Row3 articles={getSortedArticles().slice(26, 29)} />
<Row2 articles={getSortedArticles().slice(29, 31)} />
<Row3 articles={getSortedArticles().slice(31, 34)} />
<For each={getSortedArticles().slice(35)}>{(article) => <Row1 article={article} />}</For>
<p class="load-more-container">
<button class="button" onClick={loadMore}>
{t('Load more')}
</button>
</p>
</>
)
}

View File

@ -1,14 +1,14 @@
import type { Author, Chat, Message } from '../../graphql/types.gen'
import type { Author } from '../../graphql/types.gen'
import { AuthorCard } from '../Author/Card'
import Icon from '../Nav/Icon'
import { Icon } from '../Nav/Icon'
import '../../styles/Inbox.scss'
interface InboxProps {
chats?: Chat[]
messages?: Message[]
}
// interface InboxProps {
// chats?: Chat[]
// messages?: Message[]
// }
export default (_props: InboxProps) => {
export const InboxView = () => {
// TODO: get user session
return (
<div class="messages container">

View File

@ -1,22 +1,26 @@
import { Show, For, createSignal, createMemo } from 'solid-js'
import { Show, For, createSignal } from 'solid-js'
import '../../styles/Search.scss'
import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from '../Feed/Card'
import { t } from '../../utils/intl'
import { params } from '../../stores/router'
import { useArticlesStore, loadSearchResults } from '../../stores/zine/articles'
import { useStore } from '@nanostores/solid'
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
type Props = {
query?: string
results?: Shout[]
type SearchPageSearchParams = {
by: '' | 'relevance' | 'rating'
}
export const SearchPage = (props: Props) => {
const args = useStore(params)
type Props = {
query: string
results: Shout[]
}
export const SearchView = (props: Props) => {
const { getSortedArticles } = useArticlesStore({ sortedArticles: props.results })
const [getQuery, setQuery] = createSignal(props.query)
const { getSearchParams } = useRouter<SearchPageSearchParams>()
const handleQueryChange = (ev) => {
setQuery(ev.target.value)
}
@ -42,13 +46,21 @@ export const SearchPage = (props: Props) => {
</form>
<ul class="view-switcher">
<li class="selected">
<a href="?by=relevance" onClick={() => (args()['by'] = 'relevance')}>
<li
classList={{
selected: getSearchParams().by === 'relevance'
}}
>
<a href="?by=relevance" onClick={handleClientRouteLinkClick}>
{t('By relevance')}
</a>
</li>
<li>
<a href="?by=rating" onClick={() => (args()['by'] = 'rating')}>
<li
classList={{
selected: getSearchParams().by === 'rating'
}}
>
<a href="?by=rating" onClick={handleClientRouteLinkClick}>
{t('Top rated')}
</a>
</li>

View File

@ -7,19 +7,23 @@ import { ArticleCard } from '../Feed/Card'
import '../../styles/Topic.scss'
import { FullTopic } from '../Topic/Full'
import { t } from '../../utils/intl'
import { params } from '../../stores/router'
import { useRouter } from '../../stores/router'
import { useTopicsStore } from '../../stores/zine/topics'
import { useArticlesStore } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors'
import { useStore } from '@nanostores/solid'
type TopicsPageSearchParams = {
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
}
interface TopicProps {
topic: Topic
topicArticles: Shout[]
}
export const TopicPage = (props: TopicProps) => {
const args = useStore(params)
export const TopicView = (props: TopicProps) => {
const { getSearchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
const { getSortedArticles: sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
const { getTopicEntities } = useTopicsStore({ topics: [props.topic] })
@ -36,10 +40,11 @@ export const TopicPage = (props: TopicProps) => {
*/
const title = createMemo(() => {
const m = args()['by']
if (m === 'viewed') return t('Top viewed')
if (m === 'rating') return t('Top rated')
if (m === 'commented') return t('Top discussed')
// FIXME
// const m = getSearchParams().by
// if (m === 'viewed') return t('Top viewed')
// if (m === 'rating') return t('Top rated')
// if (m === 'commented') return t('Top discussed')
return t('Top recent')
})
@ -50,23 +55,23 @@ export const TopicPage = (props: TopicProps) => {
<div class="row group__controls">
<div class="col-md-8">
<ul class="view-switcher">
<li classList={{ selected: args()['by'] === 'recent' || !args()['by'] }}>
<button type="button" onClick={() => (args()['by'] = 'recent')}>
<li classList={{ selected: getSearchParams().by === 'recent' || !getSearchParams().by }}>
<button type="button" onClick={() => changeSearchParam('by', 'recent')}>
{t('Recent')}
</button>
</li>
<li classList={{ selected: args()['by'] === 'rating' }}>
<button type="button" onClick={() => (args()['by'] = 'rating')}>
<li classList={{ selected: getSearchParams().by === 'rating' }}>
<button type="button" onClick={() => changeSearchParam('by', 'rating')}>
{t('Popular')}
</button>
</li>
<li classList={{ selected: args()['by'] === 'viewed' }}>
<button type="button" onClick={() => (args()['by'] = 'viewed')}>
<li classList={{ selected: getSearchParams().by === 'viewed' }}>
<button type="button" onClick={() => changeSearchParam('by', 'viewed')}>
{t('Views')}
</button>
</li>
<li classList={{ selected: args()['by'] === 'commented' }}>
<button type="button" onClick={() => (args()['by'] = 'commented')}>
<li classList={{ selected: getSearchParams().by === 'commented' }}>
<button type="button" onClick={() => changeSearchParam('by', 'commented')}>
{t('Discussing')}
</button>
</li>

View File

@ -1,15 +0,0 @@
import { isServer } from 'solid-js/web'
import { router } from '../../stores/router'
type Props = {
href: string
children: any
}
export const ServerRouterProvider = (props: Props) => {
if (isServer) {
router.open(props.href)
}
return props.children
}

16
src/components/types.ts Normal file
View File

@ -0,0 +1,16 @@
// in a separate file to avoid circular dependencies
import type { Author, Shout, Topic } from '../graphql/types.gen'
// all the things (she said) that could be passed from the server
export type PageProps = {
randomTopics?: Topic[]
article?: Shout
articles?: Shout[]
author?: Author
authors?: Author[]
topic?: Topic
topics?: Topic[]
searchQuery?: string
// other types?
searchResults?: Shout[]
}

View File

@ -1,105 +0,0 @@
import { errorExchange, makeOperation } from '@urql/core'
import { AuthConfig, authExchange } from '@urql/exchange-auth'
import type { GraphQLError } from 'graphql'
import refreshSession from './mutation/my-session'
const logout = () => {
console.log('[graphql.auth] removing token from localStorage')
localStorage.setItem('token', '')
}
type AuthStore = {
operation?: any
authState?: any
error?: any
mutate?: any
}
const willAuthError = (a: AuthStore) => {
const { operation, authState } = a
// Detect our "refreshSession" mutation and let this operation through:
return (
!authState &&
!(
operation.kind === 'mutation' &&
// Here we find any mutation definition with the "refreshSession" field
operation.query.definitions.some(
(definition: { kind: string; selectionSet: { selections: any[] } }) => {
return (
definition.kind === 'OperationDefinition' &&
definition.selectionSet.selections.some((node) => {
// The field name is just an example, since signup may also be an exception
return node.kind === 'Field' && node.name.value === 'refreshSession'
})
)
}
)
)
)
}
const didAuthError = (r: AuthStore) =>
r?.error?.graphQLErrors?.some(
(e: GraphQLError) => (e as any).response?.status === 401 || e.extensions?.code === 'FORBIDDEN'
)
const addAuthToOperation = (a: AuthStore) => {
const { authState, operation } = a
if (!authState || !authState.token) {
return operation
}
const fetchOptions =
typeof operation.context.fetchOptions === 'function'
? operation.context.fetchOptions()
: operation.context.fetchOptions || {}
return makeOperation(operation.kind, operation, {
...operation.context,
fetchOptions: { ...fetchOptions, headers: { ...fetchOptions.headers, Authorization: authState.token } }
})
}
const getAuth = async (a: AuthStore) => {
// initialize authState if needed
const { authState, mutate } = a
if (!authState) {
const token = localStorage.getItem('token')
if (token) {
console.log('[graphql.auth] got token from localStorage')
return { token }
}
return null
}
// refresh session token
const result = await mutate(refreshSession, { token: authState.refreshToken })
const r = result.data.refreshSession // TODO: backend should send refreshToken too
if (r) {
console.log('[graphql.auth] session was refreshed, save token to localStorage')
localStorage.setItem('token', r.token)
return r
}
// error
console.log('[graphql.auth] remove token from localStorage', result)
localStorage.setItem('token', '')
logout()
return null
}
export const authExchanges = [
errorExchange({
onError: (error: { graphQLErrors: any[] }) => {
const isAuthError = error.graphQLErrors.some((e) => e.extensions?.code === 'FORBIDDEN')
if (isAuthError) logout()
}
}),
authExchange({
addAuthToOperation,
getAuth,
didAuthError,
willAuthError
} as AuthConfig<any>)
]

View File

@ -1,13 +1,11 @@
import { createClient, ClientOptions, dedupExchange, fetchExchange, Exchange } from '@urql/core'
import { devtoolsExchange } from '@urql/devtools'
import { authExchanges } from './auth'
import { baseUrl } from './publicGraphQLClient'
const isDev = true
import { isDev } from '../utils/config'
const TOKEN_LOCAL_STORAGE_KEY = 'token'
const exchanges: Exchange[] = [dedupExchange, ...authExchanges, fetchExchange]
const exchanges: Exchange[] = [dedupExchange, fetchExchange]
if (isDev) {
exchanges.unshift(devtoolsExchange)

View File

@ -1,8 +1,6 @@
import { ClientOptions, dedupExchange, fetchExchange, createClient, Exchange } from '@urql/core'
import { devtoolsExchange } from '@urql/devtools'
// FIXME actual value
const isDev = true
import { isDev } from '../utils/config'
export const baseUrl = 'https://newapi.discours.io'
//export const baseUrl = 'http://localhost:8000'

View File

@ -154,8 +154,6 @@ export type Mutation = {
updateReaction: Result
updateShout: Result
updateTopic: Result
viewReaction: Result
viewShout: Result
}
export type MutationConfirmEmailArgs = {
@ -290,14 +288,6 @@ export type MutationUpdateTopicArgs = {
input: TopicInput
}
export type MutationViewReactionArgs = {
reaction_id: Scalars['Int']
}
export type MutationViewShoutArgs = {
slug: Scalars['String']
}
export type Notification = {
kind: Scalars['String']
template: Scalars['String']
@ -339,6 +329,7 @@ export type Query = {
reactionsByShout: Array<Maybe<Reaction>>
reactionsForShouts: Array<Maybe<Reaction>>
recentAll: Array<Maybe<Shout>>
recentCommented: Array<Maybe<Shout>>
recentPublished: Array<Maybe<Shout>>
recentReacted: Array<Maybe<Shout>>
searchQuery?: Maybe<Array<Maybe<Shout>>>
@ -353,7 +344,7 @@ export type Query = {
topCommented: Array<Maybe<Shout>>
topMonth: Array<Maybe<Shout>>
topOverall: Array<Maybe<Shout>>
topViewed: Array<Maybe<Shout>>
topPublished: Array<Maybe<Shout>>
topicsAll: Array<Maybe<Topic>>
topicsByAuthor: Array<Maybe<Topic>>
topicsByCommunity: Array<Maybe<Topic>>
@ -434,6 +425,11 @@ export type QueryRecentAllArgs = {
offset: Scalars['Int']
}
export type QueryRecentCommentedArgs = {
limit: Scalars['Int']
offset: Scalars['Int']
}
export type QueryRecentPublishedArgs = {
limit: Scalars['Int']
offset: Scalars['Int']
@ -504,7 +500,8 @@ export type QueryTopOverallArgs = {
offset: Scalars['Int']
}
export type QueryTopViewedArgs = {
export type QueryTopPublishedArgs = {
daysago: Scalars['Int']
limit: Scalars['Int']
offset: Scalars['Int']
}

View File

@ -1,31 +1,24 @@
---
import { initRouter } from '../stores/router'
import '../styles/app.scss'
import { Suspense } from 'solid-js'
import { Header } from '../components/Nav/Header'
import { Footer } from '../components/Discours/Footer'
import { ServerRouterProvider } from '../components/providers/ServerRouterProvider'
import { t } from '../utils/intl'
const { pathname, search } = Astro.url
const lang = Astro.url.searchParams['lang']
const { pathname, search, searchParams } = Astro.url
const lang = searchParams.get('lang')
initRouter(pathname, search)
---
<ServerRouterProvider href={pathname + search}>
<html lang={lang || 'en'}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/png" href="/favicon.png" />
<title>{t('Discours')}</title>
</head>
<body>
<Header client:load />
<main class="main-content">
<Suspense>
<slot />
</Suspense>
</main>
<Footer />
</body>
</html>
</ServerRouterProvider>
<!DOCTYPE html>
<html lang={lang || 'ru'}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/png" href="/favicon.png" />
<title>{t('Discours')}</title>
</head>
<body>
<slot />
</body>
</html>

View File

@ -1,10 +1,11 @@
---
// TODO: sync with client router
import About from '../layouts/about.astro'
import { FourOuFour } from '../components/Views/FourOuFour'
import { FourOuFourView } from '../components/Views/FourOuFour'
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
---
<About>
<FourOuFour />
<FourOuFourView />
</About>

View File

@ -1,15 +1,16 @@
---
import { ArticlePage } from '../components/Views/ArticlePage'
import { Root } from '../components/Root'
import Zine from '../layouts/zine.astro'
import { apiClient } from '../utils/apiClient'
const slug = Astro.params.slug as string
const slug = Astro.params.slug?.toString() || ''
if (slug.includes('/') || slug.includes('.map')) {
return Astro.redirect('/404')
}
const article = await apiClient.getArticle({ slug })
if (!article) {
return Astro.redirect('/404')
}
@ -18,5 +19,5 @@ Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate'
---
<Zine>
<ArticlePage slug={slug} article={article} client:idle />
<Root article={article} client:load />
</Zine>

View File

@ -1,17 +1,15 @@
---
import { AuthorPage } from '../../../components/Views/Author'
import { Root } from '../../../components/Root'
import Zine from '../../../layouts/zine.astro'
import { apiClient } from '../../../utils/apiClient'
const limit = 50
const offset = 0
const slug = Astro.params.slug.toString()
const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], offset, limit })
const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: 50 })
const author = articles[0].authors.find((a) => a.slug === slug)
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
---
<Zine>
<AuthorPage authorArticles={articles} author={author} />
<Root articles={articles} author={author} client:load />
</Zine>

View File

@ -1,15 +1,13 @@
---
import { AllAuthorsPage } from '../components/Views/AllAuthors'
import { Root } from '../components/Root'
import Zine from '../layouts/zine.astro'
import { apiClient } from '../utils/apiClient'
import { byCreated, sortBy } from '../utils/sortby'
const { by } = Object.fromEntries(Astro.url.searchParams.entries())
let authors = await apiClient.getAllAuthors()
authors = sortBy(authors, by || byCreated)
const authors = await apiClient.getAllAuthors()
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
---
<Zine>
<AllAuthorsPage authors={authors} client:load />
<Root authors={authors} client:load />
</Zine>

View File

@ -1,8 +1,8 @@
---
import CreatePage from '../components/Views/Create'
import { Root } from '../components/Root'
import Zine from '../layouts/zine.astro'
---
<Zine>
<CreatePage client:load />
<Root client:load />
</Zine>

View File

@ -1,15 +1,11 @@
---
import { FeedPage } from '../../components/Views/Feed'
import { Root } from '../../components/Root'
import Zine from '../../layouts/zine.astro'
import { apiClient } from '../../utils/apiClient'
const limit = 50
const offset = 0
const recentArticles = await apiClient.getRecentArticles({ limit, offset })
const shoutSlugs = recentArticles.map((s) => s.slug)
const reactions = await apiClient.getReactionsForShouts({ shoutSlugs })
const articles = await apiClient.getRecentArticles({ limit: 50 })
---
<Zine>
<FeedPage articles={recentArticles} reactions={reactions} client:load />
<Root articles={articles} client:load />
</Zine>

View File

@ -1,24 +1,15 @@
---
import { HomePage } from '../components/Views/Home'
import Zine from '../layouts/zine.astro'
import { Root } from '../components/Root'
import { apiClient } from '../utils/apiClient'
const limit = 50
const offset = 0
const randomTopics = await apiClient.getRandomTopics()
const recentPublished = await apiClient.getRecentPublishedArticles({ limit, offset })
const topMonth = await apiClient.getTopMonthArticles()
const topOverall = await apiClient.getTopArticles()
const randomTopics = await apiClient.getRandomTopics({ amount: 12 })
const articles = await apiClient.getRecentPublishedArticles({ limit: 5 })
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
---
<Zine>
<HomePage
recentPublishedArticles={recentPublished}
randomTopics={randomTopics}
topMonthArticles={topMonth}
topOverallArticles={topOverall}
client:load
/>
<Root randomTopics={randomTopics} articles={articles} client:load />
</Zine>

View File

@ -1,13 +1,13 @@
---
import { SearchPage } from '../components/Views/Search'
import { Root } from '../components/Root'
import Zine from '../layouts/zine.astro'
import { apiClient } from '../utils/apiClient'
const params: URLSearchParams = Astro.url.searchParams
const q = params.get('q')
const results = await apiClient.getSearchResults({ query: q, limit: 50, offset: 0 })
const searchResults = await apiClient.getSearchResults({ query: q, limit: 50 })
---
<Zine>
<SearchPage results={results} query={q} client:load />
<Root searchResults={searchResults} client:load />
</Zine>

View File

@ -1,17 +1,15 @@
---
import { TopicPage } from '../../components/Views/Topic'
import { Root } from '../../components/Root'
import Zine from '../../layouts/zine.astro'
import { apiClient } from '../../utils/apiClient'
const slug = Astro.params.slug?.toString() || ''
const limit = parseInt(Astro.params?.limit as string, 10) || 50
const offset = parseInt(Astro.params?.offset as string, 10) || 0
const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit, offset })
const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: 50 })
const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug)
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
---
<Zine>
<TopicPage topicArticles={articles} topic={topic} />
<Root articles={articles} topic={topic} client:load />
</Zine>

View File

@ -1,15 +1,14 @@
---
import { AllTopicsPage } from '../components/Views/AllTopics'
import { Root } from '../components/Root'
import Zine from '../layouts/zine.astro'
import { apiClient } from '../utils/apiClient'
import { sortBy } from '../utils/sortby'
const { by } = Object.fromEntries(Astro.url.searchParams.entries())
let topics = await apiClient.getAllTopics()
topics = sortBy(topics, by || 'shouts')
const topics = await apiClient.getAllTopics()
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
---
<Zine>
<AllTopicsPage topics={topics} client:load />
<Root topics={topics} client:load />
</Zine>

View File

@ -1,54 +1,84 @@
import type { Accessor } from 'solid-js'
import { createRouter, createSearchParams } from '@nanostores/router'
import { isServer } from 'solid-js/web'
import { useStore } from '@nanostores/solid'
// Types for :params in route templates
interface Routes {
home: void // TODO: more
// TODO: more
export interface Routes {
home: void
topics: void
authors: void
feed: void
post: 'slug'
article: 'slug'
expo: 'slug'
create: 'collab'
search: 'q'
inbox: 'chat'
author: 'slug'
topic: 'slug'
authors: void
author: 'slug'
feed: void
article: 'slug'
search: 'q'
}
export const params = createSearchParams()
export const router = createRouter<Routes>(
const searchParamsStore = createSearchParams()
const routerStore = createRouter<Routes>(
{
home: '/',
topics: '/topics',
topic: '/topic/:slug',
authors: '/authors',
feed: '/feed',
create: '/create/:collab?',
inbox: '/inbox/:chat?',
search: '/search/:q?',
post: '/:slug',
article: '/articles/:slug',
expo: '/expo/:layout/:topic/:slug',
author: '/author/:slug',
topic: '/topic/:slug'
feed: '/feed',
search: '/search/:q?',
article: '/:slug'
},
{
// enabling search query params passing
search: true,
search: false,
links: false
}
)
export const handleClientRouteLinkClick = (ev) => {
const href = ev.target.href
console.log('[router] faster link', href)
ev.stopPropagation()
ev.preventDefault()
router.open(href)
export const router = routerStore
export const handleClientRouteLinkClick = (event) => {
const link = event.target.closest('a')
if (
link &&
event.button === 0 &&
link.target !== '_blank' &&
link.rel !== 'external' &&
!link.download &&
!event.metaKey &&
!event.ctrlKey &&
!event.shiftKey &&
!event.altKey
) {
const url = new URL(link.href)
if (url.origin === location.origin) {
event.preventDefault()
// TODO: search params
routerStore.open(url.pathname)
}
}
}
export const initRouter = (pathname: string, search: string) => {
routerStore.open(pathname)
const params = Object.fromEntries(new URLSearchParams(search))
searchParamsStore.open(params)
}
if (!isServer) {
const { pathname, search } = window.location
router.open(pathname + search)
initRouter(pathname, search)
}
export const useRouter = <TSearchParams extends Record<string, string> = Record<string, string>>() => {
const getPage = useStore(routerStore)
const getSearchParams = useStore(searchParamsStore) as unknown as Accessor<TSearchParams>
const changeSearchParam = <TKey extends keyof TSearchParams>(key: TKey, value: TSearchParams[TKey]) => {
searchParamsStore.open({ ...searchParamsStore.get(), [key]: value })
}
return {
getPage,
getSearchParams,
changeSearchParam
}
}

View File

@ -1,4 +1,4 @@
import { atom, computed, ReadableAtom } from 'nanostores'
import { atom, computed, map, ReadableAtom } from 'nanostores'
import type { Author, Shout, Topic } from '../../graphql/types.gen'
import type { WritableAtom } from 'nanostores'
import { useStore } from '@nanostores/solid'
@ -7,21 +7,30 @@ import { addAuthorsByTopic } from './authors'
import { addTopicsByAuthor } from './topics'
import { byStat } from '../../utils/sortby'
import { getLogger } from '../../utils/logger'
import { createSignal } from 'solid-js'
const log = getLogger('articles store')
let articleEntitiesStore: WritableAtom<{ [articleSlug: string]: Shout }>
let sortedArticlesStore: WritableAtom<Shout[]>
let topRatedArticlesStore: WritableAtom<Shout[]>
let topRatedMonthArticlesStore: WritableAtom<Shout[]>
let articlesByAuthorsStore: ReadableAtom<{ [authorSlug: string]: Shout[] }>
let articlesByLayoutStore: ReadableAtom<{ [layout: string]: Shout[] }>
let articlesByTopicsStore: ReadableAtom<{ [topicSlug: string]: Shout[] }>
let topViewedArticlesStore: ReadableAtom<Shout[]>
let topCommentedArticlesStore: ReadableAtom<Shout[]>
const [getSortedArticles, setSortedArticles] = createSignal<Shout[]>([])
const topArticlesStore = atom<Shout[]>()
const topMonthArticlesStore = atom<Shout[]>()
const initStore = (initial?: Record<string, Shout>) => {
log.debug('initStore')
if (articleEntitiesStore) {
return
throw new Error('articles store already initialized')
}
articleEntitiesStore = atom<Record<string, Shout>>(initial)
articleEntitiesStore = map(initial)
articlesByAuthorsStore = computed(articleEntitiesStore, (articleEntities) => {
return Object.values(articleEntities).reduce((acc, article) => {
@ -49,6 +58,18 @@ const initStore = (initial?: Record<string, Shout>) => {
}, {} as { [authorSlug: string]: Shout[] })
})
articlesByLayoutStore = computed(articleEntitiesStore, (articleEntities) => {
return Object.values(articleEntities).reduce((acc, article) => {
if (!acc[article.layout]) {
acc[article.layout] = []
}
acc[article.layout].push(article)
return acc
}, {} as { [layout: string]: Shout[] })
})
topViewedArticlesStore = computed(articleEntitiesStore, (articleEntities) => {
const sortedArticles = Object.values(articleEntities)
sortedArticles.sort(byStat('viewed'))
@ -69,7 +90,7 @@ const addArticles = (...args: Shout[][]) => {
const newArticleEntities = allArticles.reduce((acc, article) => {
acc[article.slug] = article
return acc
}, {} as Record<string, Shout>)
}, {} as { [articleSLug: string]: Shout })
if (!articleEntitiesStore) {
initStore(newArticleEntities)
@ -122,14 +143,7 @@ const addArticles = (...args: Shout[][]) => {
}
const addSortedArticles = (articles: Shout[]) => {
if (!sortedArticlesStore) {
sortedArticlesStore = atom(articles)
return
}
if (articles) {
sortedArticlesStore.set([...sortedArticlesStore.get(), ...articles])
}
setSortedArticles((prevSortedArticles) => [...prevSortedArticles, ...articles])
}
export const loadRecentArticles = async ({
@ -156,6 +170,18 @@ export const loadPublishedArticles = async ({
addSortedArticles(newArticles)
}
export const loadTopMonthArticles = async (): Promise<void> => {
const articles = await apiClient.getTopMonthArticles()
addArticles(articles)
topMonthArticlesStore.set(articles)
}
export const loadTopArticles = async (): Promise<void> => {
const articles = await apiClient.getTopArticles()
addArticles(articles)
topArticlesStore.set(articles)
}
export const loadSearchResults = async ({
query,
limit,
@ -174,8 +200,14 @@ export const incrementView = async ({ articleSlug }: { articleSlug: string }): P
await apiClient.incrementView({ articleSlug })
}
export const loadArticle = async ({ slug }: { slug: string }): Promise<Shout> => {
return await apiClient.getArticle({ slug })
export const loadArticle = async ({ slug }: { slug: string }): Promise<void> => {
const article = await apiClient.getArticle({ slug })
if (!article) {
throw new Error(`Can't load article, slug: "${slug}"`)
}
addArticles([article])
}
type InitialState = {
@ -184,32 +216,19 @@ type InitialState = {
topRatedMonthArticles?: Shout[]
}
export const useArticlesStore = ({
sortedArticles,
topRatedArticles,
topRatedMonthArticles
}: InitialState = {}) => {
addArticles(sortedArticles, topRatedArticles, topRatedMonthArticles)
addSortedArticles(sortedArticles)
export const useArticlesStore = ({ sortedArticles }: InitialState = {}) => {
addArticles(sortedArticles)
if (!topRatedArticlesStore) {
topRatedArticlesStore = atom(topRatedArticles)
} else {
topRatedArticlesStore.set(topRatedArticles)
}
if (!topRatedMonthArticlesStore) {
topRatedMonthArticlesStore = atom(topRatedMonthArticles)
} else {
topRatedMonthArticlesStore.set(topRatedMonthArticles)
if (sortedArticles) {
addSortedArticles(sortedArticles)
}
const getArticleEntities = useStore(articleEntitiesStore)
const getSortedArticles = useStore(sortedArticlesStore)
const getTopRatedArticles = useStore(topRatedArticlesStore)
const getTopRatedMonthArticles = useStore(topRatedMonthArticlesStore)
const getTopArticles = useStore(topArticlesStore)
const getTopMonthArticles = useStore(topMonthArticlesStore)
const getArticlesByAuthor = useStore(articlesByAuthorsStore)
const getArticlesByTopic = useStore(articlesByTopicsStore)
const getArticlesByLayout = useStore(articlesByLayoutStore)
// TODO: get from server
const getTopViewedArticles = useStore(topViewedArticlesStore)
// TODO: get from server
@ -220,9 +239,10 @@ export const useArticlesStore = ({
getSortedArticles,
getArticlesByTopic,
getArticlesByAuthor,
getTopRatedArticles,
getTopArticles,
getTopMonthArticles,
getTopViewedArticles,
getTopCommentedArticles,
getTopRatedMonthArticles
getArticlesByLayout
}
}

View File

@ -3,7 +3,11 @@ import type { ReadableAtom, WritableAtom } from 'nanostores'
import { atom, computed } from 'nanostores'
import type { Author } from '../../graphql/types.gen'
import { useStore } from '@nanostores/solid'
import { byCreated, byStat } from '../../utils/sortby'
import { byCreated } from '../../utils/sortby'
import { getLogger } from '../../utils/logger'
const log = getLogger('authors store')
export type AuthorsSortBy = 'created' | 'name'
@ -12,7 +16,6 @@ const sortAllByStore = atom<AuthorsSortBy>('created')
let authorEntitiesStore: WritableAtom<{ [authorSlug: string]: Author }>
let authorsByTopicStore: WritableAtom<{ [topicSlug: string]: Author[] }>
let sortedAuthorsStore: ReadableAtom<Author[]>
let topAuthorsStore: ReadableAtom<Author[]>
const initStore = (initial: { [authorSlug: string]: Author }) => {
if (authorEntitiesStore) {
@ -25,21 +28,18 @@ const initStore = (initial: { [authorSlug: string]: Author }) => {
const authors = Object.values(authorEntities)
switch (sortBy) {
case 'created': {
// log.debug('sorted by created')
authors.sort(byCreated)
break
}
case 'name': {
// log.debug('sorted by name')
authors.sort((a, b) => a.name.localeCompare(b.name))
break
}
}
return authors
})
topAuthorsStore = computed(authorEntitiesStore, (authorEntities) => {
// TODO real top authors
return Object.values(authorEntities)
})
}
export const setSortAllBy = (sortBy: AuthorsSortBy) => {
@ -102,7 +102,6 @@ export const useAuthorsStore = ({ authors }: InitialState = {}) => {
const getAuthorEntities = useStore(authorEntitiesStore)
const getSortedAuthors = useStore(sortedAuthorsStore)
const getAuthorsByTopic = useStore(authorsByTopicStore)
const getTopAuthors = useStore(topAuthorsStore)
return { getAuthorEntities, getSortedAuthors, getAuthorsByTopic, getTopAuthors }
return { getAuthorEntities, getSortedAuthors, getAuthorsByTopic }
}

View File

@ -1,25 +0,0 @@
import { atom, WritableAtom } from 'nanostores'
import type { Shout } from '../../graphql/types.gen'
import { useStore } from '@nanostores/solid'
let currentArticleStore: WritableAtom<Shout | null>
type InitialState = {
currentArticle: Shout
}
export const useCurrentArticleStore = ({ currentArticle }: InitialState) => {
if (!currentArticleStore) {
currentArticleStore = atom(currentArticle)
}
// FIXME
// addTopicsByAuthor
// addAuthorsByTopic
const getCurrentArticle = useStore(currentArticleStore)
return {
getCurrentArticle
}
}

View File

@ -0,0 +1,37 @@
import { createMemo } from 'solid-js'
import { useArticlesStore } from './articles'
import { useAuthorsStore } from './authors'
const TOP_AUTHORS_COUNT = 5
export const useTopAuthorsStore = () => {
const { getArticlesByAuthor } = useArticlesStore()
const { getAuthorEntities } = useAuthorsStore()
const getTopAuthors = createMemo(() => {
const articlesByAuthor = getArticlesByAuthor()
const authorEntities = getAuthorEntities()
return Object.keys(articlesByAuthor)
.sort((authorSlug1, authorSlug2) => {
const author1Rating = articlesByAuthor[authorSlug1].reduce(
(acc, article) => acc + article.stat.rating,
0
)
const author2Rating = articlesByAuthor[authorSlug2].reduce(
(acc, article) => acc + article.stat.rating,
0
)
if (author1Rating === author2Rating) {
return 0
}
return author1Rating > author2Rating ? 1 : -1
})
.slice(0, TOP_AUTHORS_COUNT)
.map((authorSlug) => authorEntities[authorSlug])
.filter(Boolean)
})
return { getTopAuthors }
}

View File

@ -1,20 +1,25 @@
import { apiClient } from '../../utils/apiClient'
import { map, MapStore, ReadableAtom, WritableAtom, atom, computed } from 'nanostores'
import { map, MapStore, ReadableAtom, atom, computed } from 'nanostores'
import type { Topic } from '../../graphql/types.gen'
import { useStore } from '@nanostores/solid'
import { byCreated, byStat } from '../../utils/sortby'
import { byCreated, byTopicStatDesc } from '../../utils/sortby'
import { getLogger } from '../../utils/logger'
import { createSignal } from 'solid-js'
export type TopicsSortBy = 'created' | 'name'
const log = getLogger('topics store')
const sortAllByStore = atom<TopicsSortBy>('created')
export type TopicsSortBy = 'created' | 'title' | 'authors' | 'shouts'
const sortAllByStore = atom<TopicsSortBy>('shouts')
let topicEntitiesStore: MapStore<Record<string, Topic>>
let sortedTopicsStore: ReadableAtom<Topic[]>
let topTopicsStore: ReadableAtom<Topic[]>
let randomTopicsStore: WritableAtom<Topic[]>
const [getRandomTopics, setRandomTopics] = createSignal<Topic[]>()
let topicsByAuthorStore: MapStore<Record<string, Topic[]>>
const initStore = (initial?: Record<string, Topic>) => {
const initStore = (initial?: { [topicSlug: string]: Topic }) => {
if (topicEntitiesStore) {
return
}
@ -25,30 +30,36 @@ const initStore = (initial?: Record<string, Topic>) => {
const topics = Object.values(topicEntities)
switch (sortBy) {
case 'created': {
// log.debug('sorted by created')
topics.sort(byCreated)
break
}
// eslint-disable-next-line unicorn/no-useless-switch-case
case 'name':
default: {
// use default sorting abc stores
console.debug('[topics.store] default sort')
}
case 'shouts':
case 'authors':
// log.debug(`sorted by ${sortBy}`)
topics.sort(byTopicStatDesc(sortBy))
break
case 'title':
// log.debug('sorted by title')
topics.sort((a, b) => a.title.localeCompare(b.title))
break
default:
log.error(`Unknown sort: ${sortBy}`)
}
return topics
})
topTopicsStore = computed(topicEntitiesStore, (topicEntities) => {
const topics = Object.values(topicEntities)
// DISCUSS
// topics.sort(byStat('shouts'))
topics.sort(byStat('rating'))
topics.sort(byTopicStatDesc('shouts'))
return topics
})
}
export const setSortAllBy = (sortBy: TopicsSortBy) => {
sortAllByStore.set(sortBy)
export const setSortAllTopicsBy = (sortBy: TopicsSortBy) => {
if (sortAllByStore.get() !== sortBy) {
sortAllByStore.set(sortBy)
}
}
const addTopics = (...args: Topic[][]) => {
@ -102,24 +113,25 @@ export const loadAllTopics = async (): Promise<void> => {
type InitialState = {
topics?: Topic[]
randomTopics?: Topic[]
sortBy?: TopicsSortBy
}
export const useTopicsStore = ({ topics, randomTopics }: InitialState = {}) => {
if (topics) {
addTopics(topics)
export const useTopicsStore = ({ topics, randomTopics, sortBy }: InitialState = {}) => {
if (sortBy) {
sortAllByStore.set(sortBy)
}
addTopics(topics, randomTopics)
if (randomTopics) {
addTopics(randomTopics)
}
if (!randomTopicsStore) {
randomTopicsStore = atom(randomTopics)
setRandomTopics(randomTopics)
}
const getTopicEntities = useStore(topicEntitiesStore)
const getSortedTopics = useStore(sortedTopicsStore)
const getRandomTopics = useStore(randomTopicsStore)
const getTopicsByAuthor = useStore(topicsByAuthorStore)
const getTopTopics = useStore(topTopicsStore)
return { getTopicEntities, getSortedTopics, getRandomTopics, getTopicsByAuthor, getTopTopics }
return { getTopicEntities, getSortedTopics, getRandomTopics, getTopTopics }
}

View File

@ -35,7 +35,6 @@ const log = getLogger('api-client')
const FEED_SIZE = 50
const REACTIONS_PAGE_SIZE = 100
const DEFAULT_RANDOM_TOPICS_AMOUNT = 12
export const apiClient = {
// auth
@ -87,10 +86,9 @@ export const apiClient = {
return response.data.recentPublished
},
getRandomTopics: async () => {
const response = await publicGraphQLClient
.query(topicsRandomQuery, { amount: DEFAULT_RANDOM_TOPICS_AMOUNT })
.toPromise()
getRandomTopics: async ({ amount }: { amount: number }) => {
log.debug('getRandomTopics')
const response = await publicGraphQLClient.query(topicsRandomQuery, { amount }).toPromise()
return response.data.topicsRandom
},
@ -101,7 +99,7 @@ export const apiClient = {
}: {
query: string
limit: number
offset: number
offset?: number
}): Promise<Shout[]> => {
const response = await publicGraphQLClient
.query(searchResults, {
@ -118,7 +116,7 @@ export const apiClient = {
offset = 0
}: {
limit: number
offset: number
offset?: number
}): Promise<Shout[]> => {
const response = await publicGraphQLClient
.query(articlesRecentAll, {
@ -136,7 +134,7 @@ export const apiClient = {
}: {
topicSlugs: string[]
limit: number
offset: number
offset?: number
}): Promise<Shout[]> => {
const response = await publicGraphQLClient
.query(articlesForTopics, {
@ -155,7 +153,7 @@ export const apiClient = {
}: {
authorSlugs: string[]
limit: number
offset: number
offset?: number
}): Promise<Shout[]> => {
const response = await publicGraphQLClient
.query(articlesForAuthors, {
@ -200,7 +198,6 @@ export const apiClient = {
},
getArticle: async ({ slug }: { slug: string }): Promise<Shout> => {
const response = await publicGraphQLClient.query(articleBySlug, { slug }).toPromise()
return response.data?.getShoutBySlug
},

1
src/utils/config.ts Normal file
View File

@ -0,0 +1 @@
export const isDev = import.meta.env.MODE === 'development'

View File

@ -3,7 +3,7 @@ import ru from '../locales/ru.json'
const dict = { ru }
export const t = (s, lang = 'ru') => {
export const t = (s, lang = 'ru'): string => {
try {
return dict[lang][s]
} catch {

View File

@ -1,12 +1,12 @@
import loglevel from 'loglevel'
import prefix from 'loglevel-plugin-prefix'
import { isDev } from './config'
prefix.reg(loglevel)
prefix.apply(loglevel, {
template: '[%n]'
})
// FIXME isDev
loglevel.enableAll()
loglevel.setLevel(isDev ? loglevel.levels.TRACE : loglevel.levels.ERROR)
export const getLogger = (name: string) => loglevel.getLogger(name)

View File

@ -1,4 +1,4 @@
import type { Stat } from '../graphql/types.gen'
import type { Stat, Topic, TopicStat } from '../graphql/types.gen'
export const byFirstChar = (a, b) => (a.name || a.title || '').localeCompare(b.name || b.title || '')
@ -24,7 +24,7 @@ export const byLength = (a: any[], b: any[]) => {
return 0
}
// FIXME keyof TopicStat
// TODO more typing
export const byStat = (metric: keyof Stat) => {
return (a, b) => {
const x = (a?.stat && a.stat[metric]) || 0
@ -35,6 +35,16 @@ export const byStat = (metric: keyof Stat) => {
}
}
export const byTopicStatDesc = (metric: keyof TopicStat) => {
return (a: Topic, b: Topic) => {
const x = (a?.stat && a.stat[metric]) || 0
const y = (b?.stat && b.stat[metric]) || 0
if (x > y) return -1
if (x < y) return 1
return 0
}
}
export const sortBy = (data, metric) => {
const x = [...data]
x.sort(typeof metric === 'function' ? metric : byStat(metric))

View File

@ -1,46 +0,0 @@
import { createServer } from 'http';
import fs from 'fs';
import mime from 'mime';
import { handler as ssrHandler } from '../dist/server/entry.mjs';
const clientRoot = new URL('../dist/client/', import.meta.url);
async function handle(req, res) {
ssrHandler(req, res, async (err) => {
if (err) {
res.writeHead(500);
res.end(err.stack);
return;
}
let local = new URL('.' + req.url, clientRoot);
try {
const data = await fs.promises.readFile(local);
res.writeHead(200, {
'Content-Type': mime.getType(req.url),
});
res.end(data);
} catch {
res.writeHead(404);
res.end();
}
});
}
const server = createServer((req, res) => {
handle(req, res).catch((error) => {
console.error('[ssr] server error', error);
res.writeHead(500, {
'Content-Type': 'text/plain',
});
res.end(error.toString());
});
});
server.listen(8085);
console.log('[ssr] serving at http://localhost:8085');
// Silence weird <time> warning
console.error = () => {};

252
yarn.lock
View File

@ -1092,7 +1092,7 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.1.tgz#72d647b4ff6a4f82878d184613353af1dd0290f9"
integrity sha512-72a9ghR0gnESIa7jBN53U32FOVCEoztyIlKaNoU05zRhEecduGK9L9c3ww7Mp06JiR+0ls0GBPFJQwwtjn9ksg==
"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.14.0", "@babel/core@^7.18.13", "@babel/core@^7.18.2":
"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.14.0", "@babel/core@^7.18.13", "@babel/core@^7.18.2", "@babel/core@^7.19.1":
version "7.19.1"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.19.1.tgz#c8fa615c5e88e272564ace3d42fbc8b17bfeb22b"
integrity sha512-1H8VgqXme4UXCRv7/Wa1bq7RVymKOzC7znjyFM8KiEzwFqcKUKYNoQef4GhdklgNvoBXyW4gYhuBNCM5o1zImw==
@ -1406,7 +1406,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-typescript@^7.7.2":
"@babel/plugin-syntax-typescript@^7.18.6", "@babel/plugin-syntax-typescript@^7.7.2":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285"
integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==
@ -2377,6 +2377,75 @@
source-map "^0.7.0"
vfile "^5.0.0"
"@motionone/animation@^10.14.0":
version "10.14.0"
resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.14.0.tgz#2f2a3517183bb58d82e389aac777fe0850079de6"
integrity sha512-h+1sdyBP8vbxEBW5gPFDnj+m2DCqdlAuf2g6Iafb1lcMnqjsRXWlPw1AXgvUMXmreyhqmPbJqoNfIKdytampRQ==
dependencies:
"@motionone/easing" "^10.14.0"
"@motionone/types" "^10.14.0"
"@motionone/utils" "^10.14.0"
tslib "^2.3.1"
"@motionone/dom@^10.14.2":
version "10.14.2"
resolved "https://registry.yarnpkg.com/@motionone/dom/-/dom-10.14.2.tgz#85ddd1cfd39dd907dc3bac08d18b08de1afbe519"
integrity sha512-GbGtvTSelXfT4TeQUQ3Y31PllAu0Uvghqr68FSPAJsh1hjbuYPaiPJWpP6+t/t50cHtvUbl4m2SgnGKJ0NCgWA==
dependencies:
"@motionone/animation" "^10.14.0"
"@motionone/generators" "^10.14.0"
"@motionone/types" "^10.14.0"
"@motionone/utils" "^10.14.0"
hey-listen "^1.0.8"
tslib "^2.3.1"
"@motionone/easing@^10.14.0":
version "10.14.0"
resolved "https://registry.yarnpkg.com/@motionone/easing/-/easing-10.14.0.tgz#d8154b7f71491414f3cdee23bd3838d763fffd00"
integrity sha512-2vUBdH9uWTlRbuErhcsMmt1jvMTTqvGmn9fHq8FleFDXBlHFs5jZzHJT9iw+4kR1h6a4SZQuCf72b9ji92qNYA==
dependencies:
"@motionone/utils" "^10.14.0"
tslib "^2.3.1"
"@motionone/generators@^10.14.0":
version "10.14.0"
resolved "https://registry.yarnpkg.com/@motionone/generators/-/generators-10.14.0.tgz#e05d9dd56da78a4b92db99185848a0f3db62242d"
integrity sha512-6kRHezoFfIjFN7pPpaxmkdZXD36tQNcyJe3nwVqwJ+ZfC0e3rFmszR8kp9DEVFs9QL/akWjuGPSLBI1tvz+Vjg==
dependencies:
"@motionone/types" "^10.14.0"
"@motionone/utils" "^10.14.0"
tslib "^2.3.1"
"@motionone/svelte@^10.14.2":
version "10.14.2"
resolved "https://registry.yarnpkg.com/@motionone/svelte/-/svelte-10.14.2.tgz#b9eb39afc0cb8527fd809237bb00046ed99d2530"
integrity sha512-WKgER0eH7b8q0/ODElHIbzMM3uIINdcdCw87jf7xqs4daidsy6e1ckh2XJF2Z8zyWyUEtO4VHvGumRX7EjrxFA==
dependencies:
"@motionone/dom" "^10.14.2"
tslib "^2.3.1"
"@motionone/types@^10.14.0":
version "10.14.0"
resolved "https://registry.yarnpkg.com/@motionone/types/-/types-10.14.0.tgz#148c34f3270b175397e49c3058b33fab405c21e3"
integrity sha512-3bNWyYBHtVd27KncnJLhksMFQ5o2MSdk1cA/IZqsHtA9DnRM1SYgN01CTcJ8Iw8pCXF5Ocp34tyAjY7WRpOJJQ==
"@motionone/utils@^10.14.0":
version "10.14.0"
resolved "https://registry.yarnpkg.com/@motionone/utils/-/utils-10.14.0.tgz#a19a3464ed35b08506747b062d035c7bc9bbe708"
integrity sha512-sLWBLPzRqkxmOTRzSaD3LFQXCPHvDzyHJ1a3VP9PRzBxyVd2pv51/gMOsdAcxQ9n+MIeGJnxzXBYplUHKj4jkw==
dependencies:
"@motionone/types" "^10.14.0"
hey-listen "^1.0.8"
tslib "^2.3.1"
"@motionone/vue@^10.14.2":
version "10.14.2"
resolved "https://registry.yarnpkg.com/@motionone/vue/-/vue-10.14.2.tgz#6a2bee6f672b23cc71855ca5961ae4404ba050b2"
integrity sha512-nxC/j4WhOsXxVDUdWHJDUIvHSb97eu0Kn1HNzGp08Fm9WTFkKy0HtJtTqTdkGVks2jB/XBh/FO3wU2OzyDFZNw==
dependencies:
"@motionone/dom" "^10.14.2"
tslib "^2.3.1"
"@nanostores/i18n@^0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@nanostores/i18n/-/i18n-0.6.0.tgz#1a9f1e976ae8eee399e4e8b76f8f1040bf8a8139"
@ -2511,6 +2580,93 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@solid-devtools/debugger@^0.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@solid-devtools/debugger/-/debugger-0.9.0.tgz#a7b7fe9a7733bab98f7c9f9da6ec2649f029907d"
integrity sha512-9e6YYhJoNXA5TfkkBGDt2XMMZrtwpbD4TeSEnQnpT9Pjwkcngps6in9uh9BN+xkPwAhkV2gB4FqFh0utmNKOwA==
dependencies:
"@solid-devtools/shared" "^0.8.0"
"@solid-primitives/event-bus" "^0.1.2"
"@solid-primitives/immutable" "^0.1.2"
"@solid-primitives/refs" "^0.3.2"
"@solid-primitives/scheduled" "^1.0.1"
"@solid-primitives/utils" "^3.0.2"
object-observer "^5.1.5"
type-fest "^2.19.0"
optionalDependencies:
"@solid-devtools/transform" "^0.7.3"
"@solid-devtools/ext-adapter@^0.16.2":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@solid-devtools/ext-adapter/-/ext-adapter-0.16.2.tgz#b575e0a1b45ae27ec565355e1c4fc61445baace2"
integrity sha512-Ba/dAunhZWYjxibuvEMg8bgaPREhSQrzn9W6df4OpKooqQN2mykUAJfIKp0O8osW5gFgxb4r07hHyiC+9SBaVQ==
dependencies:
"@solid-devtools/debugger" "^0.9.0"
"@solid-devtools/locator" "^0.16.2"
"@solid-devtools/shared" "^0.8.0"
"@solid-primitives/utils" "^3.0.2"
type-fest "^2.19.0"
"@solid-devtools/locator@^0.16.2":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@solid-devtools/locator/-/locator-0.16.2.tgz#62b5af7737c0fe3e976a183f5c52827d3f63ae2e"
integrity sha512-D4j+fDMKpQEEybJjhQ3WMgMaTvOwz9Mfq3D/SLO0s/vGkIjt5lWneorSEb/V2AELfVtYX3Ig19lWc5GuOBB3gA==
dependencies:
"@solid-devtools/debugger" "^0.9.0"
"@solid-devtools/shared" "^0.8.0"
"@solid-primitives/bounds" "^0.0.102"
"@solid-primitives/cursor" "^0.0.100"
"@solid-primitives/event-listener" "^2.2.2"
"@solid-primitives/immutable" "^0.1.2"
"@solid-primitives/keyboard" "^1.0.2"
"@solid-primitives/platform" "^0.0.101"
"@solid-primitives/utils" "^3.0.2"
clsx "^1.2.1"
motion "^10.14.2"
optionalDependencies:
"@solid-devtools/transform" "^0.7.3"
"@solid-devtools/logger@^0.4.7":
version "0.4.7"
resolved "https://registry.yarnpkg.com/@solid-devtools/logger/-/logger-0.4.7.tgz#2151d45037de14ed7556d923123fc20b1d8de167"
integrity sha512-bS3NQQtNox3VhbkxLJGco+CnaRPEhEpm3GWgVjhKdGZun9BxuzoqXT3DlCxZPwM2y42PsJnyi8faX6mC3d5k2g==
dependencies:
"@solid-devtools/debugger" "^0.9.0"
"@solid-devtools/shared" "^0.8.0"
"@solid-primitives/utils" "^3.0.2"
"@solid-devtools/shared@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@solid-devtools/shared/-/shared-0.8.0.tgz#a76c92abec12c8b42ac4478e289aa4b77a269bdb"
integrity sha512-2z3taBZX9ER3DJAi2cLCcHHRT+BczaaclGBQ+BtnyJyEZHX20SDOap+yU+Wth5CGahZ9nowBPcQhhg1GTdVR4w==
dependencies:
"@solid-primitives/event-bus" "^0.1.2"
"@solid-primitives/immutable" "^0.1.2"
"@solid-primitives/utils" "^3.0.2"
solid-js "^1.5.5"
ts-node "^10.9.1"
type-fest "^2.19.0"
"@solid-devtools/transform@^0.7.3":
version "0.7.3"
resolved "https://registry.yarnpkg.com/@solid-devtools/transform/-/transform-0.7.3.tgz#6412026d6247e9a249bfa86c0397f6f0106a52a7"
integrity sha512-EbsxcYdrhgTYIGuBywxI3suA5x5bw66Q3qR6c9CImIeUWbhAEyu5d86NbaNHuhW1USZsPoS1xROmVaxyPSTlvQ==
dependencies:
"@babel/core" "^7.19.1"
"@babel/plugin-syntax-typescript" "^7.18.6"
"@babel/types" "^7.19.0"
"@solid-devtools/shared" "^0.8.0"
solid-js "^1.5.5"
"@solid-primitives/bounds@^0.0.102":
version "0.0.102"
resolved "https://registry.yarnpkg.com/@solid-primitives/bounds/-/bounds-0.0.102.tgz#47a9879fa6f1379ad06a50ab6923c9afaebf0b95"
integrity sha512-bno5qplSGNxDAAdo6qGrT7e4i0HkFc3P4LFVlIU8yGVvfamLGsNva6Fjl/gs71fDunSk7T81jlReaHCc0O4wlQ==
dependencies:
"@solid-primitives/event-listener" "^2.2.2"
"@solid-primitives/resize-observer" "^2.0.4"
"@solid-primitives/utils" "^3.0.2"
"@solid-primitives/clipboard@^1.3.0":
version "1.4.3"
resolved "https://registry.yarnpkg.com/@solid-primitives/clipboard/-/clipboard-1.4.3.tgz#0757fa8a32bceb7e20fcee3507555f870101ff49"
@ -2518,6 +2674,21 @@
dependencies:
"@solid-primitives/utils" "^3.0.2"
"@solid-primitives/cursor@^0.0.100":
version "0.0.100"
resolved "https://registry.yarnpkg.com/@solid-primitives/cursor/-/cursor-0.0.100.tgz#24d08b39f332d30c5887ed6f737854b1df9b95f7"
integrity sha512-XstEQqblHeUfnBoU+wtpx1cfrU+XR2rubyIdO7ARPW8EKHwtO8fRKQsyeyzi9neFl1eCmuy/TZaKs44JH/vSeg==
dependencies:
"@solid-primitives/utils" "^3.0.1"
"@solid-primitives/event-bus@^0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@solid-primitives/event-bus/-/event-bus-0.1.2.tgz#91c5e61013d8d6203c6fb9b57d7eb9b3d69107f4"
integrity sha512-W79mwPqTqflFZppNKyKG6IW/zKw+caqqO4LdPlRkFZAA0n8DIuOB//BriUnnBIf5YasLsMpwkGKzG1ABF1Qpjg==
dependencies:
"@solid-primitives/immutable" "^0.1.2"
"@solid-primitives/utils" "^3.0.2"
"@solid-primitives/event-listener@^2.2.0", "@solid-primitives/event-listener@^2.2.2":
version "2.2.2"
resolved "https://registry.yarnpkg.com/@solid-primitives/event-listener/-/event-listener-2.2.2.tgz#1875cd7bfa6fdd45127d28d3966cdda97ed47e1c"
@ -2525,6 +2696,13 @@
dependencies:
"@solid-primitives/utils" "^3.0.2"
"@solid-primitives/immutable@^0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@solid-primitives/immutable/-/immutable-0.1.2.tgz#4f3146903bcd52de2ddc2f03af2a9c4b5ec131bc"
integrity sha512-h6P3bhQlgo9qsTfJ7eimUYzKkhf0dFchYPyfvEUy/QcG2eSDkKhhgOz0ss6DSP6cdavTDTaQoquLr/iR8LelMA==
dependencies:
"@solid-primitives/utils" "^3.0.2"
"@solid-primitives/intersection-observer@^2.0.0", "@solid-primitives/intersection-observer@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@solid-primitives/intersection-observer/-/intersection-observer-2.0.1.tgz#3152763455caf733a7fab2261d7b21f2c1df622e"
@ -2532,6 +2710,38 @@
dependencies:
"@solid-primitives/utils" "^3.0.2"
"@solid-primitives/keyboard@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@solid-primitives/keyboard/-/keyboard-1.0.2.tgz#7620baf6fab574e4bc7a8de27e566f806beb2768"
integrity sha512-7a0FPro6cx4PqsFjWgfK5OpeUGuCbndTTRg0nKEmwdho/8cyepD7HVi5Ep+3tjG5vx+WI+KwYfzK8AM6GB+aUQ==
dependencies:
"@solid-primitives/event-listener" "^2.2.2"
"@solid-primitives/rootless" "^1.1.3"
"@solid-primitives/utils" "^3.0.2"
"@solid-primitives/platform@^0.0.101":
version "0.0.101"
resolved "https://registry.yarnpkg.com/@solid-primitives/platform/-/platform-0.0.101.tgz#7bfa879152a59169589e2dc999aac8ceb63233c7"
integrity sha512-Dn12QFiihRKIzlGMuPsxpW89uekX3BmreofTCFrZpiwUGSGYTYa2eNbpYFYqkOgSKpGkV+HNU2fVWTuXFJhtWg==
"@solid-primitives/refs@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@solid-primitives/refs/-/refs-0.3.2.tgz#6935105264cfd929df8303d46b4f7a99f535fd47"
integrity sha512-5bwL25wCpnEtlz3cScj3TNHpqeVYAqCbkdmnB/+KLwOJyfNSEm1RsFzOT6SIsd0lRJeY5Of4TeRlUT/tPofAXw==
dependencies:
"@solid-primitives/immutable" "^0.1.2"
"@solid-primitives/rootless" "^1.1.3"
"@solid-primitives/utils" "^3.0.2"
"@solid-primitives/resize-observer@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@solid-primitives/resize-observer/-/resize-observer-2.0.4.tgz#fdfc8d70a6e134d58b63e56dd41e64dcb6cfbf7d"
integrity sha512-YohOMcQMDLpwSYyJN/fPXErys+o5mTMnpQ9AHFirx8gn0+gYCiF2fBrWtgWdHc8TVy2UUehRFU2Xc5FpiltjtQ==
dependencies:
"@solid-primitives/event-listener" "^2.2.2"
"@solid-primitives/rootless" "^1.1.3"
"@solid-primitives/utils" "^3.0.2"
"@solid-primitives/rootless@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@solid-primitives/rootless/-/rootless-1.1.3.tgz#6f1494e5511d38c3b6ed872d5c70dfe1f8e92897"
@ -2563,7 +2773,7 @@
resolved "https://registry.yarnpkg.com/@solid-primitives/storage/-/storage-1.3.2.tgz#ea204d443c39c8098faf6c4787851dbea4e56323"
integrity sha512-l9OkMfdkfNJ8mT+7KeNBqfmnMI9nOU3hwePSKT9FVs3uMKR892UCIYUu6tQ2QyaGjOFeAgPTQSLOhh0RcSUkTw==
"@solid-primitives/utils@^3.0.2":
"@solid-primitives/utils@^3.0.1", "@solid-primitives/utils@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@solid-primitives/utils/-/utils-3.0.2.tgz#b2429dfae6c14029e05ed7174cc953af8370d036"
integrity sha512-LCU3tVrJmyRqJ0ocG5uCEuUNqmGkcAC+cWpDEE49AuvtehkdQfv4CfqvdNJgs3eoQRQhLOrVcgd1bHFJY4lsrQ==
@ -6235,6 +6445,11 @@ header-case@^2.0.4:
capital-case "^1.0.4"
tslib "^2.0.3"
hey-listen@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
hosted-git-info@^2.1.4:
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@ -8481,6 +8696,18 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
motion@^10.14.2:
version "10.14.2"
resolved "https://registry.yarnpkg.com/motion/-/motion-10.14.2.tgz#814bdaaf39655247f40101984ef4245029360029"
integrity sha512-zZp9PL4/O7nSgQBWBDdyvGm25Ef/hQUUVAOnyzxn2IvAhp496M+RB9p1ce4nN7cYLizox2Bq77/dTIjFGkJmAw==
dependencies:
"@motionone/animation" "^10.14.0"
"@motionone/dom" "^10.14.2"
"@motionone/svelte" "^10.14.2"
"@motionone/types" "^10.14.0"
"@motionone/utils" "^10.14.0"
"@motionone/vue" "^10.14.2"
mri@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
@ -8756,6 +8983,11 @@ object-keys@^1.1.1:
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
object-observer@^5.1.5:
version "5.1.5"
resolved "https://registry.yarnpkg.com/object-observer/-/object-observer-5.1.5.tgz#22780df5311cb80c69682784b1738a314389838b"
integrity sha512-HhIPaHoYUQ8BAXUEuie60q4gTDrQR+aV3ewgW/joLdOawWZu2ULR0egR2Rlm6bG4ikIF9V+DwhDTuNvWIUhdLA==
object.assign@^4.1.0, object.assign@^4.1.3, object.assign@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
@ -10191,6 +10423,16 @@ snake-case@^3.0.4:
dot-case "^3.0.4"
tslib "^2.0.3"
solid-devtools@^0.16.2:
version "0.16.2"
resolved "https://registry.yarnpkg.com/solid-devtools/-/solid-devtools-0.16.2.tgz#f153bf56389597703fc6d41e4c1f3ad4618c26dc"
integrity sha512-Q0xWvWldHkahg03/J//a+PzpB2tch1roOA3l3iKDwEhVy3dHwjW1/eagLka0t2HlXZomIQnUPEyIH5cy6ovgWQ==
dependencies:
"@solid-devtools/debugger" "^0.9.0"
"@solid-devtools/ext-adapter" "^0.16.2"
"@solid-devtools/locator" "^0.16.2"
"@solid-devtools/transform" "^0.7.3"
solid-js-form@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/solid-js-form/-/solid-js-form-0.1.5.tgz#052748ccefc1f8cf043661a26eb28e9000f7b1fc"
@ -10199,7 +10441,7 @@ solid-js-form@^0.1.5:
solid-js "^1.1.2"
yup "^0.32.9"
solid-js@^1.1.2, solid-js@^1.5.3:
solid-js@^1.1.2, solid-js@^1.5.3, solid-js@^1.5.5:
version "1.5.5"
resolved "https://registry.yarnpkg.com/solid-js/-/solid-js-1.5.5.tgz#657088e122b4e916ea589f97b8ff6e291d648597"
integrity sha512-5gXszD7ekhe59IyMa3+AvREJnBWVjwaeC7afL8C3UNPj5gQQCrsMs/cXwI3JRpj6D+3TESTyuQ2sY++m4cYiTg==
@ -10977,7 +11219,7 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
type-fest@^2.5.0:
type-fest@^2.19.0, type-fest@^2.5.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==