client-routing, fixes
This commit is contained in:
parent
caa8890222
commit
3d45479368
11
.eslintrc.js
11
.eslintrc.js
|
@ -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',
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 })
|
||||
})
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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">−</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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
20
src/components/Layouts/MainLayout.tsx
Normal file
20
src/components/Layouts/MainLayout.tsx
Normal 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 />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
14
src/components/Pages/AllAuthorsPage.tsx
Normal file
14
src/components/Pages/AllAuthorsPage.tsx
Normal 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
|
14
src/components/Pages/AllTopicsPage.tsx
Normal file
14
src/components/Pages/AllTopicsPage.tsx
Normal 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
|
46
src/components/Pages/ArticlePage.tsx
Normal file
46
src/components/Pages/ArticlePage.tsx
Normal 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
|
14
src/components/Pages/AuthorPage.tsx
Normal file
14
src/components/Pages/AuthorPage.tsx
Normal 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
|
14
src/components/Pages/FeedPage.tsx
Normal file
14
src/components/Pages/FeedPage.tsx
Normal 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
|
13
src/components/Pages/FourOuFourPage.tsx
Normal file
13
src/components/Pages/FourOuFourPage.tsx
Normal 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
|
14
src/components/Pages/HomePage.tsx
Normal file
14
src/components/Pages/HomePage.tsx
Normal 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
|
14
src/components/Pages/SearchPage.tsx
Normal file
14
src/components/Pages/SearchPage.tsx
Normal 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
|
14
src/components/Pages/TopicPage.tsx
Normal file
14
src/components/Pages/TopicPage.tsx
Normal 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
64
src/components/Root.tsx
Normal 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} />
|
||||
}
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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()}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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)} />
|
||||
|
|
|
@ -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
|
|
@ -1,6 +1,6 @@
|
|||
import { Title } from '@solidjs/meta'
|
||||
|
||||
export default () => (
|
||||
export const ConnectView = () => (
|
||||
<>
|
||||
<Title>Дискурс: Предложить идею</Title>
|
||||
|
||||
|
|
|
@ -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 })
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
16
src/components/types.ts
Normal 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[]
|
||||
}
|
|
@ -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>)
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
37
src/stores/zine/topAuthors.ts
Normal file
37
src/stores/zine/topAuthors.ts
Normal 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 }
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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
1
src/utils/config.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const isDev = import.meta.env.MODE === 'development'
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
252
yarn.lock
|
@ -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==
|
||||
|
|
Loading…
Reference in New Issue
Block a user