Merge remote-tracking branch 'origin/dev' into prepare-inbox

# Conflicts:
#	src/components/Inbox/Message.module.scss
#	src/components/Views/Inbox.tsx
#	src/styles/Inbox.scss
#	src/utils/config.ts
This commit is contained in:
ilya-bkv 2022-11-21 08:38:34 +03:00
commit 6e21d0640b
69 changed files with 578 additions and 11357 deletions

View File

@ -1,11 +1,4 @@
# Astro + Solid.js
Created with
``` ```
npm init astro -- --template framework-solid yarn install
npm start
``` ```
Astro working with [Solid](https://www.solidjs.com/).
Write your Solid components as `.jsx` or `.tsx` files in your project.

View File

@ -1,7 +1,7 @@
import { defineConfig, AstroUserConfig } from 'astro/config' import { defineConfig, AstroUserConfig } from 'astro/config'
import vercel from '@astrojs/vercel/serverless' import vercel from '@astrojs/vercel/serverless'
import solidJs from '@astrojs/solid-js' import solidJs from '@astrojs/solid-js'
import type { CSSOptions, PluginOption } from 'vite' import type { CSSOptions } from 'vite'
import defaultGenerateScopedName from 'postcss-modules/build/generateScopedName' import defaultGenerateScopedName from 'postcss-modules/build/generateScopedName'
import { isDev } from './src/utils/config' import { isDev } from './src/utils/config'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'

View File

@ -1,5 +1,5 @@
overwrite: true overwrite: true
schema: 'https://testapi.discours.io/graphql' schema: 'http://v2.discours.io/graphql'
generates: generates:
src/graphql/introspec.gen.ts: src/graphql/introspec.gen.ts:
plugins: plugins:

View File

@ -62,7 +62,7 @@
"astro": "^1.6.8", "astro": "^1.6.8",
"astro-eslint-parser": "^0.9.0", "astro-eslint-parser": "^0.9.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bootstrap": "5.1.3", "bootstrap": "5.2.2",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"cookie-signature": "^1.2.0", "cookie-signature": "^1.2.0",

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@ interface AuthorCardProps {
isAuthorPage?: boolean isAuthorPage?: boolean
noSocialButtons?: boolean noSocialButtons?: boolean
isAuthorsList?: boolean isAuthorsList?: boolean
truncateBio?: boolean
} }
export const AuthorCard = (props: AuthorCardProps) => { export const AuthorCard = (props: AuthorCardProps) => {
@ -63,7 +64,8 @@ export const AuthorCard = (props: AuthorCardProps) => {
</Show> </Show>
<Show when={!props.hideDescription}> <Show when={!props.hideDescription}>
<div class={styles.authorAbout} classList={{ 'text-truncate': props.isAuthorsList }}> {props.isAuthorsList}
<div class={styles.authorAbout} classList={{ 'text-truncate': props.truncateBio }}>
{bio()} {bio()}
</div> </div>
</Show> </Show>
@ -86,7 +88,6 @@ export const AuthorCard = (props: AuthorCardProps) => {
> >
<Show when={!props.isAuthorsList}> <Show when={!props.isAuthorsList}>
<Icon name="author-subscribe" class={styles.icon} /> <Icon name="author-subscribe" class={styles.icon} />
&nbsp;
</Show> </Show>
<span class={styles.buttonLabel}>{t('Follow')}</span> <span class={styles.buttonLabel}>{t('Follow')}</span>
</button> </button>
@ -103,7 +104,6 @@ export const AuthorCard = (props: AuthorCardProps) => {
> >
<Show when={!props.isAuthorsList}> <Show when={!props.isAuthorsList}>
<Icon name="author-unsubscribe" class={styles.icon} /> <Icon name="author-unsubscribe" class={styles.icon} />
&nbsp;
</Show> </Show>
<span class={styles.buttonLabel}>{t('Unfollow')}</span> <span class={styles.buttonLabel}>{t('Unfollow')}</span>
</button> </button>

View File

@ -70,6 +70,7 @@
@include font-size(1.5rem); @include font-size(1.5rem);
font-weight: 500; font-weight: 500;
padding-right: 0.3em;
white-space: nowrap; white-space: nowrap;
img { img {
@ -108,3 +109,7 @@ button.follow {
max-width: 2em; max-width: 2em;
max-height: 2em; max-height: 2em;
} }
.shoutCardContainer {
display: flex;
}

View File

@ -4,10 +4,11 @@ import { For, Show } from 'solid-js'
import { ArticleCard } from './Card' import { ArticleCard } from './Card'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/Card'
import { TopicCard } from '../Topic/Card' import { TopicCard } from '../Topic/Card'
import style from './Beside.module.scss' import styles from './Beside.module.scss'
import type { Author, Shout, Topic, User } from '../../graphql/types.gen' import type { Author, Shout, Topic, User } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { clsx } from 'clsx'
interface BesideProps { interface BesideProps {
title?: string title?: string
@ -29,21 +30,28 @@ export const Beside = (props: BesideProps) => {
<Show when={!!props.values}> <Show when={!!props.values}>
<div class="col-md-4"> <div class="col-md-4">
<Show when={!!props.title}> <Show when={!!props.title}>
<div class={style.besideColumnTitle}> <div class={styles.besideColumnTitle}>
<h4>{props.title}</h4> <h4>{props.title}</h4>
<Show when={props.wrapper === 'author'}> <Show when={props.wrapper === 'author'}>
<a href="/user/list"> <a href="/authors">
{t('All authors')} {t('All authors')}
<Icon name="arrow-right" /> <Icon name="arrow-right" class={styles.icon} />
</a>
</Show>
<Show when={props.wrapper === 'topic'}>
<a href="/topics">
{t('All topics')}
<Icon name="arrow-right" class={styles.icon} />
</a> </a>
</Show> </Show>
</div> </div>
</Show> </Show>
<ul class={style.besideColumn}> <ul class={styles.besideColumn}>
<For each={[...props.values]}> <For each={[...props.values]}>
{(value: Partial<Shout | User | Topic>) => ( {(value: Partial<Shout | User | Topic>) => (
<li classList={{ [style.top]: props.wrapper.startsWith('top-') }}> <li classList={{ [styles.top]: props.wrapper.startsWith('top-') }}>
<Show when={props.wrapper === 'topic'}> <Show when={props.wrapper === 'topic'}>
<TopicCard <TopicCard
topic={value as Topic} topic={value as Topic}
@ -51,10 +59,16 @@ export const Beside = (props: BesideProps) => {
shortDescription={props.topicShortDescription} shortDescription={props.topicShortDescription}
isTopicInRow={props.isTopicInRow} isTopicInRow={props.isTopicInRow}
iconButton={props.iconButton} iconButton={props.iconButton}
showPublications={true}
/> />
</Show> </Show>
<Show when={props.wrapper === 'author'}> <Show when={props.wrapper === 'author'}>
<AuthorCard author={value as Author} compact={true} hasLink={true} /> <AuthorCard
author={value as Author}
compact={true}
hasLink={true}
truncateBio={true}
/>
</Show> </Show>
<Show when={props.wrapper === 'article' && value?.slug}> <Show when={props.wrapper === 'article' && value?.slug}>
<ArticleCard article={value as Shout} settings={{ noimage: true }} /> <ArticleCard article={value as Shout} settings={{ noimage: true }} />
@ -71,8 +85,8 @@ export const Beside = (props: BesideProps) => {
</ul> </ul>
</div> </div>
</Show> </Show>
<div class="col-md-8"> <div class={clsx('col-md-8', styles.shoutCardContainer)}>
<ArticleCard article={props.beside} settings={{ isBigTitle: true }} /> <ArticleCard article={props.beside} settings={{ isBigTitle: true, isBeside: true }} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -675,3 +675,19 @@
@include font-size(2.4rem); @include font-size(2.4rem);
} }
} }
.shoutCardBeside {
&,
.shoutCardCoverContainer {
flex: 1;
}
.shoutCardCover {
height: 100%;
padding: 0;
}
.shoutCardContent {
padding-top: 1.6rem;
}
}

View File

@ -6,7 +6,6 @@ import { translit } from '../../utils/ru2en'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import styles from './Card.module.scss' import styles from './Card.module.scss'
import { locale } from '../../stores/ui' import { locale } from '../../stores/ui'
import { handleClientRouteLinkClick } from '../../stores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import CardTopic from './CardTopic' import CardTopic from './CardTopic'
@ -29,6 +28,7 @@ interface ArticleCardProps {
withBorder?: boolean withBorder?: boolean
isCompact?: boolean isCompact?: boolean
isSingle?: boolean isSingle?: boolean
isBeside?: boolean
} }
article: Shout article: Shout
} }
@ -82,7 +82,8 @@ export const ArticleCard = (props: ArticleCardProps) => {
[styles.shoutCardVertical]: props.settings?.isVertical, [styles.shoutCardVertical]: props.settings?.isVertical,
[styles.shoutCardWithBorder]: props.settings?.withBorder, [styles.shoutCardWithBorder]: props.settings?.withBorder,
[styles.shoutCardCompact]: props.settings?.isCompact, [styles.shoutCardCompact]: props.settings?.isCompact,
[styles.shoutCardSingle]: props.settings?.isSingle [styles.shoutCardSingle]: props.settings?.isSingle,
[styles.shoutCardBeside]: props.settings?.isBeside
}} }}
> >
<Show when={!props.settings?.noimage && cover}> <Show when={!props.settings?.noimage && cover}>
@ -113,7 +114,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
</Show> </Show>
<div class={styles.shoutCardTitlesContainer}> <div class={styles.shoutCardTitlesContainer}>
<a href={`/${slug || ''}`} onClick={handleClientRouteLinkClick}> <a href={`/${slug || ''}`}>
<div class={styles.shoutCardTitle}> <div class={styles.shoutCardTitle}>
<span class={styles.shoutCardLinkContainer}>{title}</span> <span class={styles.shoutCardLinkContainer}>{title}</span>
</div> </div>

View File

@ -23,7 +23,7 @@ export const Row2 = (props: { articles: Shout[]; isEqual?: boolean }) => {
<div class={`col-md-${props.isEqual ? '6' : x[y()][i()]}`}> <div class={`col-md-${props.isEqual ? '6' : x[y()][i()]}`}>
<ArticleCard <ArticleCard
article={a} article={a}
settings={{ isWithCover: props.isEqual || x[y()][i()] === '8' }} settings={{ isWithCover: props.isEqual || x[y()][i()] === '8', nodate: props.isEqual }}
/> />
</div> </div>
</Show> </Show>

View File

@ -3,7 +3,7 @@ import { clsx } from 'clsx'
import { t } from '../../../utils/intl' import { t } from '../../../utils/intl'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
import { createMemo, createSignal, onMount, Show } from 'solid-js' import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { handleClientRouteLinkClick, useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import type { ConfirmEmailSearchParams } from './types' import type { ConfirmEmailSearchParams } from './types'
import { ApiError } from '../../../utils/apiClient' import { ApiError } from '../../../utils/apiClient'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
@ -48,7 +48,7 @@ export const EmailConfirm = () => {
<Show when={isTokenExpired()}> <Show when={isTokenExpired()}>
<div class={styles.title}>Ссылка больше не действительна</div> <div class={styles.title}>Ссылка больше не действительна</div>
<div class={styles.text}> <div class={styles.text}>
<a href="/?modal=auth&mode=login" class={styles.sendLink} onClick={handleClientRouteLinkClick}> <a href="/?modal=auth&mode=login" class={styles.sendLink}>
{/*TODO: temp solution, should be send link again, but we don't have email here*/} {/*TODO: temp solution, should be send link again, but we don't have email here*/}
Вход Вход
</a> </a>
@ -57,7 +57,7 @@ export const EmailConfirm = () => {
<Show when={isTokenInvalid()}> <Show when={isTokenInvalid()}>
<div class={styles.title}>Неправильная ссылка</div> <div class={styles.title}>Неправильная ссылка</div>
<div class={styles.text}> <div class={styles.text}>
<a href="/?modal=auth&mode=login" class={styles.sendLink} onClick={handleClientRouteLinkClick}> <a href="/?modal=auth&mode=login" class={styles.sendLink}>
{/*TODO: temp solution, should be send link again, but we don't have email here*/} {/*TODO: temp solution, should be send link again, but we don't have email here*/}
Вход Вход
</a> </a>

View File

@ -2,7 +2,7 @@ import { Dynamic } from 'solid-js/web'
import { Component, createEffect, createMemo } from 'solid-js' import { Component, createEffect, createMemo } from 'solid-js'
import { t } from '../../../utils/intl' import { t } from '../../../utils/intl'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
import { handleClientRouteLinkClick, useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
import { LoginForm } from './LoginForm' import { LoginForm } from './LoginForm'
@ -57,9 +57,8 @@ export const AuthModal = () => {
{t('By signing up you agree with our')}{' '} {t('By signing up you agree with our')}{' '}
<a <a
href="/about/terms-of-use" href="/about/terms-of-use"
onClick={(event) => { onClick={() => {
hideModal() hideModal()
handleClientRouteLinkClick(event)
}} }}
> >
{t('terms of use')} {t('terms of use')}

View File

@ -4,7 +4,7 @@ import { Modal } from './Modal'
import { AuthModal } from './AuthModal' import { AuthModal } from './AuthModal'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useModalStore } from '../../stores/ui' import { useModalStore } from '../../stores/ui'
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router' import { router, Routes, useRouter } from '../../stores/router'
import styles from './Header.module.scss' import styles from './Header.module.scss'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
@ -91,7 +91,7 @@ export const Header = (props: Props) => {
<div class={clsx(styles.mainHeaderInner, 'wide-container')}> <div class={clsx(styles.mainHeaderInner, 'wide-container')}>
<nav class={clsx(styles.headerInner, 'row')} classList={{ fixed: fixed() }}> <nav class={clsx(styles.headerInner, 'row')} classList={{ fixed: fixed() }}>
<div class={clsx(styles.mainLogo, 'col-auto')}> <div class={clsx(styles.mainLogo, 'col-auto')}>
<a href={getPagePath(router, 'home')} onClick={handleClientRouteLinkClick}> <a href={getPagePath(router, 'home')}>
<img src="/logo.svg" alt={t('Discours')} /> <img src="/logo.svg" alt={t('Discours')} />
</a> </a>
</div> </div>
@ -107,9 +107,7 @@ export const Header = (props: Props) => {
<For each={resources}> <For each={resources}>
{(r) => ( {(r) => (
<li classList={{ [styles.selected]: r.route === page().route }}> <li classList={{ [styles.selected]: r.route === page().route }}>
<a href={getPagePath(router, r.route, null)} onClick={handleClientRouteLinkClick}> <a href={getPagePath(router, r.route, null)}>{r.name}</a>
{r.name}
</a>
</li> </li>
)} )}
</For> </For>

View File

@ -1,6 +1,6 @@
import styles from './Header.module.scss' import styles from './Header.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { createSignal, Show } from 'solid-js' import { createSignal, Show } from 'solid-js'
@ -9,7 +9,7 @@ import { ProfilePopup } from './ProfilePopup'
import Userpic from '../Author/Userpic' import Userpic from '../Author/Userpic'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { showModal, useWarningsStore } from '../../stores/ui' import { showModal, useWarningsStore } from '../../stores/ui'
import { ClientContainer } from '../_shared/ClientContainer' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
type HeaderAuthProps = { type HeaderAuthProps = {
@ -37,12 +37,12 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
} }
return ( return (
<ClientContainer> <ShowOnlyOnClient>
<Show when={!session.loading}> <Show when={!session.loading}>
<div class={styles.usernav}> <div class={styles.usernav}>
<div class={clsx(styles.userControl, styles.userControl, 'col')}> <div class={clsx(styles.userControl, styles.userControl, 'col')}>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}> <div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
<a href="/create" onClick={handleClientRouteLinkClick}> <a href="/create">
<span class={styles.textLabel}>{t('Create post')}</span> <span class={styles.textLabel}>{t('Create post')}</span>
<Icon name="pencil" class={styles.icon} /> <Icon name="pencil" class={styles.icon} />
</a> </a>
@ -68,7 +68,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
when={isAuthenticated()} when={isAuthenticated()}
fallback={ fallback={
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}> <div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}> <a href="?modal=auth&mode=login">
<span class={styles.textLabel}>{t('Enter')}</span> <span class={styles.textLabel}>{t('Enter')}</span>
<Icon name="user-anonymous" class={styles.icon} /> <Icon name="user-anonymous" class={styles.icon} />
</a> </a>
@ -102,6 +102,6 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
</div> </div>
</div> </div>
</Show> </Show>
</ClientContainer> </ShowOnlyOnClient>
) )
} }

View File

@ -4,7 +4,6 @@ import { Icon } from '../_shared/Icon'
import './Topics.scss' import './Topics.scss'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { locale } from '../../stores/ui' import { locale } from '../../stores/ui'
import { handleClientRouteLinkClick } from '../../stores/router'
export const NavTopics = (props: { topics: Topic[] }) => { export const NavTopics = (props: { topics: Topic[] }) => {
const tag = (topic: Topic) => const tag = (topic: Topic) =>
@ -18,7 +17,7 @@ export const NavTopics = (props: { topics: Topic[] }) => {
<For each={props.topics}> <For each={props.topics}>
{(topic) => ( {(topic) => (
<li class="item"> <li class="item">
<a href={`/topic/${topic.slug}`} onClick={handleClientRouteLinkClick}> <a href={`/topic/${topic.slug}`}>
<span>#{tag(topic)}</span> <span>#{tag(topic)}</span>
</a> </a>
</li> </li>

View File

@ -1,7 +1,7 @@
import { PageWrap } from '../_shared/PageWrap' import { PageWrap } from '../_shared/PageWrap'
import { ArticleView } from '../Views/Article' import { ArticleView } from '../Views/Article'
import type { PageProps } from '../types' import type { PageProps } from '../types'
import { loadShoutsBy, useArticlesStore } from '../../stores/zine/articles' import { loadShout, useArticlesStore } from '../../stores/zine/articles'
import { createMemo, onMount, Show } from 'solid-js' import { createMemo, onMount, Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
@ -32,7 +32,7 @@ export const ArticlePage = (props: PageProps) => {
const articleValue = articleEntities()[slug()] const articleValue = articleEntities()[slug()]
if (!articleValue || !articleValue.body) { if (!articleValue || !articleValue.body) {
await loadShoutsBy({ by: { slug: slug() }, limit: 1, offset: 0 }) await loadShout(slug())
} }
}) })

View File

@ -2,7 +2,7 @@ import { PageWrap } from '../_shared/PageWrap'
import { AuthorView, PRERENDERED_ARTICLES_COUNT } from '../Views/Author' import { AuthorView, PRERENDERED_ARTICLES_COUNT } from '../Views/Author'
import type { PageProps } from '../types' import type { PageProps } from '../types'
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { loadShoutsBy, resetSortedArticles } from '../../stores/zine/articles' import { loadShouts, resetSortedArticles } from '../../stores/zine/articles'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { loadAuthor } from '../../stores/zine/authors' import { loadAuthor } from '../../stores/zine/authors'
import { Loading } from '../Loading' import { Loading } from '../Loading'
@ -27,7 +27,7 @@ export const AuthorPage = (props: PageProps) => {
return return
} }
await loadShoutsBy({ by: { author: slug() }, limit: PRERENDERED_ARTICLES_COUNT }) await loadShouts({ filters: { author: slug() }, limit: PRERENDERED_ARTICLES_COUNT })
await loadAuthor({ slug: slug() }) await loadAuthor({ slug: slug() })
setIsLoaded(true) setIsLoaded(true)

View File

@ -0,0 +1,3 @@
.mainContent {
padding-top: 100px;
}

View File

@ -2,9 +2,10 @@ import { HomeView, PRERENDERED_ARTICLES_COUNT } from '../Views/Home'
import { PageWrap } from '../_shared/PageWrap' import { PageWrap } from '../_shared/PageWrap'
import type { PageProps } from '../types' import type { PageProps } from '../types'
import { createSignal, onCleanup, onMount, Show } from 'solid-js' import { createSignal, onCleanup, onMount, Show } from 'solid-js'
import { loadShoutsBy, resetSortedArticles } from '../../stores/zine/articles' import { loadShouts, resetSortedArticles } from '../../stores/zine/articles'
import { loadRandomTopics } from '../../stores/zine/topics' import { loadRandomTopics } from '../../stores/zine/topics'
import { Loading } from '../Loading' import { Loading } from '../Loading'
import styles from './HomePage.module.scss'
export const HomePage = (props: PageProps) => { export const HomePage = (props: PageProps) => {
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.shouts) && Boolean(props.randomTopics)) const [isLoaded, setIsLoaded] = createSignal(Boolean(props.shouts) && Boolean(props.randomTopics))
@ -14,7 +15,7 @@ export const HomePage = (props: PageProps) => {
return return
} }
await loadShoutsBy({ by: { visibility: 'public' }, limit: PRERENDERED_ARTICLES_COUNT, offset: 0 }) await loadShouts({ filters: { visibility: 'public' }, limit: PRERENDERED_ARTICLES_COUNT, offset: 0 })
await loadRandomTopics() await loadRandomTopics()
setIsLoaded(true) setIsLoaded(true)
@ -23,7 +24,7 @@ export const HomePage = (props: PageProps) => {
onCleanup(() => resetSortedArticles()) onCleanup(() => resetSortedArticles())
return ( return (
<PageWrap> <PageWrap class={styles.mainContent}>
<Show when={isLoaded()} fallback={<Loading />}> <Show when={isLoaded()} fallback={<Loading />}>
<HomeView randomTopics={props.randomTopics} shouts={props.shouts || []} /> <HomeView randomTopics={props.randomTopics} shouts={props.shouts || []} />
</Show> </Show>

View File

@ -1,14 +1,14 @@
import { PageWrap } from '../_shared/PageWrap' import { PageWrap } from '../_shared/PageWrap'
import type { PageProps } from '../types' import type { PageProps } from '../types'
import { createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js' import { createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
import { loadShoutsBy, resetSortedArticles } from '../../stores/zine/articles' import { loadShouts, resetSortedArticles } from '../../stores/zine/articles'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { LayoutType, useLayoutsStore } from '../../stores/zine/layouts' import { LayoutType, useLayoutsStore } from '../../stores/zine/layouts'
import { Loading } from '../Loading' import { Loading } from '../Loading'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { splitToPages } from '../../utils/splitToPages' import { splitToPages } from '../../utils/splitToPages'
import clsx from 'clsx' import { clsx } from 'clsx'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { Row3 } from '../Feed/Row3' import { Row3 } from '../Feed/Row3'
import { Row2 } from '../Feed/Row2' import { Row2 } from '../Feed/Row2'
@ -33,7 +33,8 @@ export const LayoutShoutsPage = (props: PageProps) => {
const loadMoreLayout = async (kind: LayoutType) => { const loadMoreLayout = async (kind: LayoutType) => {
saveScrollPosition() saveScrollPosition()
const { hasMore } = await loadLayoutShoutsBy({ const { hasMore } = await loadLayoutShoutsBy({
by: { layout: kind }, // filters: { layout: kind },
limit: LOAD_MORE_PAGE_SIZE, limit: LOAD_MORE_PAGE_SIZE,
offset: sortedArticles().length offset: sortedArticles().length
}) })
@ -62,7 +63,7 @@ export const LayoutShoutsPage = (props: PageProps) => {
onMount(async () => { onMount(async () => {
if (!isLoaded()) { if (!isLoaded()) {
await loadShoutsBy({ by: { layout: layout() }, limit: PRERENDERED_ARTICLES_COUNT, offset: 0 }) await loadShouts({ filters: { layout: layout() }, limit: PRERENDERED_ARTICLES_COUNT, offset: 0 })
} }
}) })

View File

@ -2,8 +2,8 @@ import { PageWrap } from '../_shared/PageWrap'
import { SearchView } from '../Views/Search' import { SearchView } from '../Views/Search'
import type { PageProps } from '../types' import type { PageProps } from '../types'
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { loadShoutsBy, resetSortedArticles } from '../../stores/zine/articles'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { loadShouts, resetSortedArticles } from '../../stores/zine/articles'
import { Loading } from '../Loading' import { Loading } from '../Loading'
export const SearchPage = (props: PageProps) => { export const SearchPage = (props: PageProps) => {
@ -26,7 +26,7 @@ export const SearchPage = (props: PageProps) => {
return return
} }
await loadShoutsBy({ by: { title: q(), body: q() }, limit: 50, offset: 0 }) await loadShouts({ filters: { title: q(), body: q() }, limit: 50, offset: 0 })
setIsLoaded(true) setIsLoaded(true)
}) })

View File

@ -2,7 +2,7 @@ import { PageWrap } from '../_shared/PageWrap'
import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../Views/Topic' import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../Views/Topic'
import type { PageProps } from '../types' import type { PageProps } from '../types'
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { loadShoutsBy, resetSortedArticles } from '../../stores/zine/articles' import { loadShouts, resetSortedArticles } from '../../stores/zine/articles'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { loadTopic } from '../../stores/zine/topics' import { loadTopic } from '../../stores/zine/topics'
import { Loading } from '../Loading' import { Loading } from '../Loading'
@ -27,7 +27,7 @@ export const TopicPage = (props: PageProps) => {
return return
} }
await loadShoutsBy({ by: { topics: [slug()] }, limit: PRERENDERED_ARTICLES_COUNT, offset: 0 }) await loadShouts({ filters: { topic: slug() }, limit: PRERENDERED_ARTICLES_COUNT, offset: 0 })
await loadTopic({ slug: slug() }) await loadTopic({ slug: slug() })
setIsLoaded(true) setIsLoaded(true)

View File

@ -40,10 +40,7 @@ export const DogmaPage = () => {
<li> <li>
<b>Всегда исправляем ошибки, если мы их допустили.</b> <b>Всегда исправляем ошибки, если мы их допустили.</b>
Никто не безгрешен, иногда и мы ошибаемся. Заметили ошибку - отправьте{' '} Никто не безгрешен, иногда и мы ошибаемся. Заметили ошибку - отправьте{' '}
<a href="/about/guide#editing" target="_self"> <a href="/about/guide#editing">ремарку</a> автору или напишите нам на{' '}
ремарку
</a>{' '}
автору или напишите нам на{' '}
<a href="mailto:welcome@discours.io" target="_blank"> <a href="mailto:welcome@discours.io" target="_blank">
welcome@discours.io welcome@discours.io
</a> </a>

View File

@ -67,28 +67,28 @@ export const GuidePage = () => {
<p> <p>
Дискурс&nbsp;&mdash; независимый журнал о&nbsp;культуре, науке, искусстве и&nbsp;обществе Дискурс&nbsp;&mdash; независимый журнал о&nbsp;культуре, науке, искусстве и&nbsp;обществе
с&nbsp;<a href="/about/manifest">открытой редакцией</a>. У&nbsp;нас нет главного редактора, с&nbsp;
инвестора и&nbsp;вообще никого, кто&nbsp;бы принимал единоличные решения. Вместо традиционных <a href="/about/manifest">открытой редакцией</a>. У&nbsp;нас нет главного редактора, инвестора
иерархий Дискурс основан на&nbsp;принципах прямой демократии: в&nbsp;нашем горизонтальном и&nbsp;вообще никого, кто&nbsp;бы принимал единоличные решения. Вместо традиционных иерархий
сообществе все редакционные вопросы решаются открытым голосованием авторов журнала. Вот как Дискурс основан на&nbsp;принципах прямой демократии: в&nbsp;нашем горизонтальном сообществе
это работает. все редакционные вопросы решаются открытым голосованием авторов журнала. Вот как это работает.
</p> </p>
<h3 id="how-it-works">Как устроен сайт Дискурса</h3> <h3 id="how-it-works">Как устроен сайт Дискурса</h3>
<p>Дискурс состоит из&nbsp;четырех основных разделов:</p> <p>Дискурс состоит из&nbsp;четырех основных разделов:</p>
<ul> <ul>
<li> <li>
<p> <p>
<a href="/topics">Темы</a>&nbsp;&mdash; у&nbsp;нас публикуются исследования, обзоры, эссе, <a href="/topics">Темы</a>
интервью, репортажи, аналитика и&nbsp;другие материалы о&nbsp;культуре, науке, искусстве &nbsp;&mdash; у&nbsp;нас публикуются исследования, обзоры, эссе, интервью, репортажи,
и&nbsp;обществе. аналитика и&nbsp;другие материалы о&nbsp;культуре, науке, искусстве и&nbsp;обществе.
</p> </p>
</li> </li>
<li> <li>
<p> <p>
<a href="/topic/art">Искусство</a>&nbsp;&mdash; здесь, например, представлены <a href="/topic/art">Искусство</a>
художественные произведения: литература, живопись, музыка, фотографии, видео. Этот раздел &nbsp;&mdash; здесь, например, представлены художественные произведения: литература,
помогает прозвучать новому искусству, которое создают российские художники, писатели, живопись, музыка, фотографии, видео. Этот раздел помогает прозвучать новому искусству,
режиссёры и&nbsp;музыканты. которое создают российские художники, писатели, режиссёры и&nbsp;музыканты.
</p> </p>
</li> </li>
{/* {/*
@ -118,14 +118,16 @@ export const GuidePage = () => {
&mdash;&nbsp;ключевым словам, которые располагаются в&nbsp;конце материалов и&nbsp;связывают &mdash;&nbsp;ключевым словам, которые располагаются в&nbsp;конце материалов и&nbsp;связывают
материалы по&nbsp;жанрам (например, материалы по&nbsp;жанрам (например,
<a href="/topic/interview">интервью</a>, <a href="/topic/reportage">репортажи</a>,{' '} <a href="/topic/interview">интервью</a>, <a href="/topic/reportage">репортажи</a>,{' '}
<a href="/topic/essay">эссе</a>, <a href="/topic/likbez">ликбезы</a>), по&nbsp;тематике ( <a href="/topic/essay">эссе</a>, <a href="/topic/likbez">ликбезы</a>
<a href="/topic/cinema">кино</a>, <a href="/topic/philosophy">философия</a>,{' '} ), по&nbsp;тематике (<a href="/topic/cinema">кино</a>,{' '}
<a href="/topic/history">история</a>, <a href="/topic/absurdism">абсурдизм</a>,{' '} <a href="/topic/philosophy">философия</a>, <a href="/topic/history">история</a>,{' '}
<a href="/topic/sex">секс</a> и&nbsp;т.д.) или в&nbsp;серии (как &laquo; <a href="/topic/absurdism">абсурдизм</a>, <a href="/topic/sex">секс</a> и&nbsp;т.д.) или
<a href="/topic/zakony-mira">Законы мира</a>&raquo; или &laquo; в&nbsp;серии (как &laquo;
<a href="/topic/za-liniey-mannergeyma">За&nbsp;линией Маннергейма</a>&raquo;). Темы объединяют <a href="/topic/zakony-mira">Законы мира</a>
сотни публикаций, помогают ориентироваться в&nbsp;журнале и&nbsp;следить за&nbsp;интересными &raquo; или &laquo;
материалами. <a href="/topic/za-liniey-mannergeyma">За&nbsp;линией Маннергейма</a>
&raquo;). Темы объединяют сотни публикаций, помогают ориентироваться в&nbsp;журнале
и&nbsp;следить за&nbsp;интересными материалами.
</p> </p>
<section> <section>

View File

@ -91,10 +91,11 @@ export const ManifestPage = () => {
<p> <p>
Редакция Дискурса открыта для всех: у&nbsp;нас нет цензуры, запретных тем Редакция Дискурса открыта для всех: у&nbsp;нас нет цензуры, запретных тем
и&nbsp;идеологических рамок. Каждый может <a href="/create">прислать материал</a>{' '} и&nbsp;идеологических рамок. Каждый может <a href="/create">прислать материал</a>{' '}
в&nbsp;журнал и&nbsp;<a href="/about/guide">присоединиться к&nbsp;редакции</a>. Предоставляя в&nbsp;журнал и&nbsp;
трибуну для независимой журналистики и&nbsp;художественных проектов, мы&nbsp;помогаем людям <a href="/about/guide">присоединиться к&nbsp;редакции</a>. Предоставляя трибуну для
рассказывать свои истории так, чтобы они были услышаны. Мы&nbsp;убеждены: чем больше голосов независимой журналистики и&nbsp;художественных проектов, мы&nbsp;помогаем людям рассказывать
будет звучать на&nbsp;Дискурсе, тем громче в&nbsp;полифонии мнений будет слышна истина. свои истории так, чтобы они были услышаны. Мы&nbsp;убеждены: чем больше голосов будет звучать
на&nbsp;Дискурсе, тем громче в&nbsp;полифонии мнений будет слышна истина.
</p> </p>
<h2 class="h2" id="participation"> <h2 class="h2" id="participation">

View File

@ -269,7 +269,7 @@ export const TermsOfUsePage = () => {
<a href="mailto:welcome@discours.io" target="_blank"> <a href="mailto:welcome@discours.io" target="_blank">
welcome@discours.io welcome@discours.io
</a>{' '} </a>{' '}
или через форму <a href="/feedback-idea">&laquo;предложить идею&raquo;</a>. или через форму <a href="/connect">&laquo;предложить идею&raquo;</a>.
</p> </p>
</div> </div>
</div> </div>

View File

@ -76,11 +76,7 @@ export const ThanksPage = () => {
признательны всем, кто нас поддерживает. Ваши пожертвования&nbsp;&mdash; финансовый фундамент признательны всем, кто нас поддерживает. Ваши пожертвования&nbsp;&mdash; финансовый фундамент
журнала. Благодаря вам мы&nbsp;развиваем платформу качественной журналистики, которая помогает журнала. Благодаря вам мы&nbsp;развиваем платформу качественной журналистики, которая помогает
самым разным авторам быть услышанными. Стать нашим меценатом и&nbsp;подписаться самым разным авторам быть услышанными. Стать нашим меценатом и&nbsp;подписаться
на&nbsp;ежемесячную поддержку проекта можно{' '} на&nbsp;ежемесячную поддержку проекта можно <a href="/about/help">здесь</a>.
<a href="/about/help" target="_self">
здесь
</a>
.
</p> </p>
</div> </div>
</div> </div>

View File

@ -28,6 +28,7 @@
@include font-size(2.2rem); @include font-size(2.2rem);
margin-bottom: 1.2rem; margin-bottom: 1.2rem;
margin-top: 0.5rem !important;
} }
.topicAvatar { .topicAvatar {
@ -104,3 +105,13 @@
text-align: right; text-align: right;
} }
} }
.topicCompact {
.topicTitle {
@include font-size(1.7rem);
}
}
.buttonCompact {
margin-top: 0.6rem;
}

View File

@ -21,6 +21,7 @@ interface TopicProps {
additionalClass?: string additionalClass?: string
isTopicInRow?: boolean isTopicInRow?: boolean
iconButton?: boolean iconButton?: boolean
showPublications?: boolean
} }
export const TopicCard = (props: TopicProps) => { export const TopicCard = (props: TopicProps) => {
@ -47,6 +48,7 @@ export const TopicCard = (props: TopicProps) => {
class={styles.topic} class={styles.topic}
classList={{ classList={{
row: !props.compact && !props.subscribeButtonBottom, row: !props.compact && !props.subscribeButtonBottom,
[styles.topicCompact]: props.compact,
[styles.topicInRow]: props.isTopicInRow [styles.topicInRow]: props.isTopicInRow
}} }}
> >
@ -58,7 +60,7 @@ export const TopicCard = (props: TopicProps) => {
</Show> </Show>
<Show when={props.topic.pic}> <Show when={props.topic.pic}>
<div class={styles.topicAvatar}> <div class={styles.topicAvatar}>
<a href={props.topic.slug}> <a href={`/topic/${props.topic.slug}`}>
<img src={props.topic.pic} alt={props.topic.title} /> <img src={props.topic.pic} alt={props.topic.title} />
</a> </a>
</div> </div>
@ -75,7 +77,7 @@ export const TopicCard = (props: TopicProps) => {
<Show when={props.topic?.stat}> <Show when={props.topic?.stat}>
<div class={styles.topicDetails}> <div class={styles.topicDetails}>
<Show when={!props.compact}> <Show when={props.showPublications}>
<span class={styles.topicDetailsItem} classList={{ compact: props.compact }}> <span class={styles.topicDetailsItem} classList={{ compact: props.compact }}>
{props.topic.stat?.shouts + {props.topic.stat?.shouts +
' ' + ' ' +
@ -85,6 +87,8 @@ export const TopicCard = (props: TopicProps) => {
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'] locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
)} )}
</span> </span>
</Show>
<Show when={!props.compact}>
<span class={styles.topicDetailsItem} classList={{ compact: props.compact }}> <span class={styles.topicDetailsItem} classList={{ compact: props.compact }}>
{props.topic.stat?.authors + {props.topic.stat?.authors +
' ' + ' ' +
@ -116,15 +120,6 @@ export const TopicCard = (props: TopicProps) => {
{/* </span>*/} {/* </span>*/}
{/*</Show>*/} {/*</Show>*/}
</Show> </Show>
{/*
<span class='topic-details__item'>
{subscribers().toString() + ' ' + t('follower') + plural(
subscribers(),
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
)}
</span>
*/}
</div> </div>
</Show> </Show>
</div> </div>
@ -135,16 +130,26 @@ export const TopicCard = (props: TopicProps) => {
<Show <Show
when={subscribed()} when={subscribed()}
fallback={ fallback={
<button onClick={() => subscribe(true)} class="button--light button--subscribe-topic"> <button
onClick={() => subscribe(true)}
class="button--light button--subscribe-topic"
classList={{
[styles.buttonCompact]: props.compact
}}
>
<Show when={props.iconButton}>+</Show> <Show when={props.iconButton}>+</Show>
<Show when={!props.iconButton}>{t('Follow')}</Show> <Show when={!props.iconButton}>{t('Follow')}</Show>
</button> </button>
} }
> >
<button onClick={() => subscribe(false)} class="button--light button--subscribe-topic"> <button
onClick={() => subscribe(false)}
class="button--light button--subscribe-topic"
classList={{
[styles.buttonCompact]: props.compact
}}
>
<Show when={props.iconButton}>-</Show> <Show when={props.iconButton}>-</Show>
<Show when={!props.iconButton}>{t('Unfollow')}</Show> <Show when={!props.iconButton}>{t('Unfollow')}</Show>
</button> </button>
</Show> </Show>

View File

@ -1,7 +1,7 @@
.topicHeader { .topicHeader {
@include font-size(1.7rem); @include font-size(1.7rem);
padding-top: 5.8rem; padding-top: 2.8rem;
text-align: center; text-align: center;
h1 { h1 {

View File

@ -4,10 +4,11 @@ import { AuthorCard } from '../Author/Card'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors' import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors'
import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import styles from '../../styles/AllTopics.module.scss' import styles from '../../styles/AllTopics.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { locale } from '../../stores/ui'
type AllAuthorsPageSearchParams = { type AllAuthorsPageSearchParams = {
by: '' | 'name' | 'shouts' | 'rating' by: '' | 'name' | 'shouts' | 'rating'
@ -35,18 +36,10 @@ export const AllAuthorsView = (props: Props) => {
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => { const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
return sortedAuthors().reduce((acc, author) => { return sortedAuthors().reduce((acc, author) => {
if (!author.name) { let letter = author.name.trim().split(' ').pop().at(0).toUpperCase()
// name === null for new users if (!/[А-я]/i.test(letter) && locale() === 'ru') letter = '@'
return acc if (!acc[letter]) acc[letter] = []
}
const letter = author.name[0].toUpperCase()
if (!acc[letter]) {
acc[letter] = []
}
acc[letter].push(author) acc[letter].push(author)
return acc return acc
}, {} as { [letter: string]: Author[] }) }, {} as { [letter: string]: Author[] })
}) })
@ -70,19 +63,13 @@ export const AllAuthorsView = (props: Props) => {
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}> <ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
<li classList={{ selected: searchParams().by === 'shouts' }}> <li classList={{ selected: searchParams().by === 'shouts' }}>
<a href="/authors?by=shouts" onClick={handleClientRouteLinkClick}> <a href="/authors?by=shouts">{t('By shouts')}</a>
{t('By shouts')}
</a>
</li> </li>
<li classList={{ selected: searchParams().by === 'rating' }}> <li classList={{ selected: searchParams().by === 'rating' }}>
<a href="/authors?by=rating" onClick={handleClientRouteLinkClick}> <a href="/authors?by=rating">{t('By rating')}</a>
{t('By rating')}
</a>
</li> </li>
<li classList={{ selected: !searchParams().by || searchParams().by === 'name' }}> <li classList={{ selected: !searchParams().by || searchParams().by === 'name' }}>
<a href="/authors" onClick={handleClientRouteLinkClick}> <a href="/authors">{t('By alphabet')}</a>
{t('By alphabet')}
</a>
</li> </li>
<li class="view-switcher__search"> <li class="view-switcher__search">
<a href="/authors/search"> <a href="/authors/search">
@ -109,6 +96,7 @@ export const AllAuthorsView = (props: Props) => {
subscribed={subscribed(author.slug)} subscribed={subscribed(author.slug)}
noSocialButtons={true} noSocialButtons={true}
isAuthorsList={true} isAuthorsList={true}
truncateBio={true}
/> />
)} )}
</For> </For>
@ -118,7 +106,7 @@ export const AllAuthorsView = (props: Props) => {
<div class="row"> <div class="row">
<div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10')}> <div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10')}>
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}> <button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
{t('More')} {t('Load more')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -3,11 +3,13 @@ import type { Topic } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics' import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { TopicCard } from '../Topic/Card' import { TopicCard } from '../Topic/Card'
import styles from '../../styles/AllTopics.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { locale } from '../../stores/ui'
import { translit } from '../../utils/ru2en'
import styles from '../../styles/AllTopics.module.scss'
type AllTopicsPageSearchParams = { type AllTopicsPageSearchParams = {
by: 'shouts' | 'authors' | 'title' | '' by: 'shouts' | 'authors' | 'title' | ''
@ -37,13 +39,10 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => { const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => {
return sortedTopics().reduce((acc, topic) => { return sortedTopics().reduce((acc, topic) => {
const letter = topic.title[0].toUpperCase() let letter = topic.title[0].toUpperCase()
if (!acc[letter]) { if (!/[А-я]/i.test(letter) && locale() === 'ru') letter = '#'
acc[letter] = [] if (!acc[letter]) acc[letter] = []
}
acc[letter].push(topic) acc[letter].push(topic)
return acc return acc
}, {} as { [letter: string]: Topic[] }) }, {} as { [letter: string]: Topic[] })
}) })
@ -57,70 +56,78 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
const subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || '')) const subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || ''))
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
let searchEl: HTMLInputElement
const [searchResults, setSearchResults] = createSignal<Topic[]>([])
// eslint-disable-next-line sonarjs/cognitive-complexity
const searchTopics = () => {
/* very stupid search algorithm with no deps */
let q = searchEl.value.toLowerCase()
if (q.length > 0) {
console.debug(q)
setSearchResults([])
return ( if (locale() === 'ru') q = translit(q, 'ru')
<div class={clsx(styles.allTopicsPage, 'container')}> const ttt: Topic[] = []
<Show when={sortedTopics().length > 0}> sortedTopics().forEach((topic) => {
<div class="shift-content"> let flag = false
topic.slug.split('-').forEach((w) => {
if (w.startsWith(q)) flag = true
})
if (!flag) {
let wrds: string = topic.title.toLowerCase()
if (locale() === 'ru') wrds = translit(wrds, 'ru')
wrds.split(' ').forEach((w: string) => {
if (w.startsWith(q)) flag = true
})
}
if (flag && !ttt.includes(topic)) ttt.push(topic)
})
setSearchResults((sr: Topic[]) => [...sr, ...ttt])
changeSearchParam('by', '')
}
}
const AllTopicsHead = () => (
<div class="row"> <div class="row">
<div class={clsx(styles.pageHeader, 'col-lg-10 col-xl-9')}> <div class={clsx(styles.pageHeader, 'col-lg-10 col-xl-9')}>
<h1>{t('Topics')}</h1> <h1>{t('Topics')}</h1>
<p>{t('Subscribe what you like to tune your personal feed')}</p> <p>{t('Subscribe what you like to tune your personal feed')}</p>
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}> <ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
<li classList={{ selected: searchParams().by === 'shouts' || !searchParams().by }}> <li classList={{ selected: searchParams().by === 'shouts' }}>
<a href="/topics?by=shouts" onClick={handleClientRouteLinkClick}> <a href="/topics?by=shouts">{t('By shouts')}</a>
{t('By shouts')}
</a>
</li> </li>
<li classList={{ selected: searchParams().by === 'authors' }}> <li classList={{ selected: searchParams().by === 'authors' }}>
<a href="/topics?by=authors" onClick={handleClientRouteLinkClick}> <a href="/topics?by=authors">{t('By authors')}</a>
{t('By authors')}
</a>
</li> </li>
<li classList={{ selected: searchParams().by === 'title' }}> <li classList={{ selected: searchParams().by === 'title' }}>
<a <a href="/topics?by=title">{t('By alphabet')}</a>
href="/topics?by=title"
onClick={(ev) => {
// just an example
ev.preventDefault()
changeSearchParam('by', 'title')
}}
>
{t('By alphabet')}
</a>
</li> </li>
<li class="view-switcher__search"> <li class="search-switcher">
<a href="/topic/search">
<Icon name="search" /> <Icon name="search" />
{t('Search topic')} <input
</a> class="search-input"
ref={searchEl}
onChange={searchTopics}
onInput={searchTopics}
onFocus={() => (searchEl.innerHTML = '')}
placeholder={t('Search')}
/>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
)
return (
<div class={clsx(styles.allTopicsPage, 'container')}>
<AllTopicsHead />
<Show <div class="shift-content">
when={searchParams().by === 'title'} <Show when={sortedTopics().length > 0 || searchResults().length > 0}>
fallback={() => ( <Show when={searchParams().by === 'title'}>
<>
<For each={sortedTopics().slice(0, limit())}>
{(topic) => (
<TopicCard topic={topic} compact={false} subscribed={subscribed(topic.slug)} />
)}
</For>
<Show when={sortedTopics().length > limit()}>
<div class="row">
<div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10')}>
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
{t('More')}
</button>
</div>
</div>
</Show>
</>
)}
>
<For each={sortedKeys()}> <For each={sortedKeys()}>
{(letter) => ( {(letter) => (
<div class={clsx(styles.group, 'group')}> <div class={clsx(styles.group, 'group')}>
@ -146,8 +153,57 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
)} )}
</For> </For>
</Show> </Show>
<Show when={searchResults().length > 1}>
<For each={searchResults().slice(0, limit())}>
{(topic) => (
<TopicCard
topic={topic}
compact={false}
subscribed={subscribed(topic.slug)}
showPublications={true}
/>
)}
</For>
</Show>
<Show when={searchParams().by === 'authors'}>
<For each={sortedTopics().slice(0, limit())}>
{(topic) => (
<TopicCard
topic={topic}
compact={false}
subscribed={subscribed(topic.slug)}
showPublications={true}
/>
)}
</For>
</Show>
<Show when={searchParams().by === 'shouts'}>
<For each={sortedTopics().slice(0, limit())}>
{(topic) => (
<TopicCard
topic={topic}
compact={false}
subscribed={subscribed(topic.slug)}
showPublications={true}
/>
)}
</For>
</Show>
<Show when={sortedTopics().length > limit()}>
<div class="row">
<div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10')}>
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
{t('Load more')}
</button>
</div>
</div> </div>
</Show> </Show>
</Show>
</div>
</div> </div>
) )
} }

View File

@ -5,7 +5,7 @@ import { Row3 } from '../Feed/Row3'
import { AuthorFull } from '../Author/Full' import { AuthorFull } from '../Author/Full'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { loadShoutsBy, useArticlesStore } from '../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
@ -18,7 +18,7 @@ type AuthorProps = {
shouts: Shout[] shouts: Shout[]
author: Author author: Author
authorSlug: string authorSlug: string
// FIXME author topics fro server // FIXME author topics from server
// topics: Topic[] // topics: Topic[]
} }
@ -42,8 +42,8 @@ export const AuthorView = (props: AuthorProps) => {
const loadMore = async () => { const loadMore = async () => {
saveScrollPosition() saveScrollPosition()
const { hasMore } = await loadShoutsBy({ const { hasMore } = await loadShouts({
by: { author: author().slug }, filters: { author: author().slug },
limit: LOAD_MORE_PAGE_SIZE, limit: LOAD_MORE_PAGE_SIZE,
offset: sortedArticles().length offset: sortedArticles().length
}) })

View File

@ -1,11 +1,11 @@
import { Editor } from '../EditorNew/Editor' import { Editor } from '../EditorNew/Editor'
import { ClientContainer } from '../_shared/ClientContainer' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
export const CreateView = () => { export const CreateView = () => {
return ( return (
<ClientContainer> <ShowOnlyOnClient>
<Editor /> <Editor />
</ClientContainer> </ShowOnlyOnClient>
) )
} }

View File

@ -8,7 +8,7 @@ import { AuthorCard } from '../Author/Card'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { FeedSidebar } from '../Feed/Sidebar' import { FeedSidebar } from '../Feed/Sidebar'
import CommentCard from '../Article/Comment' import CommentCard from '../Article/Comment'
import { useArticlesStore } from '../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { useReactionsStore } from '../../stores/zine/reactions' import { useReactionsStore } from '../../stores/zine/reactions'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
@ -28,7 +28,7 @@ export const FEED_PAGE_SIZE = 20
export const FeedView = () => { export const FeedView = () => {
// state // state
const { sortedArticles, loadShoutsBy } = useArticlesStore() const { sortedArticles } = useArticlesStore()
const { sortedReactions: topComments, loadReactionsBy } = useReactionsStore({}) const { sortedReactions: topComments, loadReactionsBy } = useReactionsStore({})
const { sortedAuthors } = useAuthorsStore() const { sortedAuthors } = useAuthorsStore()
const { topTopics } = useTopicsStore() const { topTopics } = useTopicsStore()
@ -37,8 +37,8 @@ export const FeedView = () => {
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const loadMore = async () => { const loadMore = async () => {
const { hasMore } = await loadShoutsBy({ const { hasMore } = await loadShouts({
by: { visibility: 'community' }, filters: { visibility: 'community' },
limit: FEED_PAGE_SIZE, limit: FEED_PAGE_SIZE,
offset: sortedArticles().length offset: sortedArticles().length
}) })
@ -57,7 +57,7 @@ export const FeedView = () => {
// load recent editing shouts ( visibility = authors ) // load recent editing shouts ( visibility = authors )
const userslug = session().user.slug const userslug = session().user.slug
await loadShoutsBy({ by: { author: userslug, visibility: 'authors' }, limit: 15, offset: 0 }) await loadShouts({ filters: { author: userslug, visibility: 'authors' }, limit: 15, offset: 0 })
const collaborativeShouts = sortedArticles().filter((s: Shout, n: number, arr: Shout[]) => { const collaborativeShouts = sortedArticles().filter((s: Shout, n: number, arr: Shout[]) => {
if (s.visibility !== 'authors') { if (s.visibility !== 'authors') {
arr.splice(n, 1) arr.splice(n, 1)
@ -84,13 +84,13 @@ export const FeedView = () => {
</li> </li>
</Show> </Show>
<li> <li>
<a href="?by=views">{t('Most read')}</a> <a href="/feed/?by=views">{t('Most read')}</a>
</li> </li>
<li> <li>
<a href="?by=rating">{t('Top rated')}</a> <a href="/feed/?by=rating">{t('Top rated')}</a>
</li> </li>
<li> <li>
<a href="?by=comments">{t('Most commented')}</a> <a href="/feed/?by=comments">{t('Most commented')}</a>
</li> </li>
</ul> </ul>
@ -101,7 +101,7 @@ export const FeedView = () => {
<div class={stylesBeside.besideColumnTitle}> <div class={stylesBeside.besideColumnTitle}>
<h4>{t('Popular authors')}</h4> <h4>{t('Popular authors')}</h4>
<a href="/user/list"> <a href="/authors">
{t('All authors')} {t('All authors')}
<Icon name="arrow-right" /> <Icon name="arrow-right" />
</a> </a>

View File

@ -1,6 +1,5 @@
import '../../styles/FeedSettings.scss' import '../../styles/FeedSettings.scss'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { handleClientRouteLinkClick } from '../../stores/router'
// type FeedSettingsSearchParams = { // type FeedSettingsSearchParams = {
// by: '' | 'topics' | 'authors' | 'reacted' // by: '' | 'topics' | 'authors' | 'reacted'
@ -13,9 +12,7 @@ export const FeedSettingsView = (_props) => {
<ul class="view-switcher"> <ul class="view-switcher">
<li class="selected"> <li class="selected">
<a href="?by=topics" onClick={handleClientRouteLinkClick}> <a href="?by=topics">{t('topics')}</a>
{t('topics')}
</a>
</li> </li>
{/*<li> {/*<li>
<a href="?by=collections" onClick={() => setBy('collections')}> <a href="?by=collections" onClick={() => setBy('collections')}>
@ -23,14 +20,10 @@ export const FeedSettingsView = (_props) => {
</a> </a>
</li>*/} </li>*/}
<li> <li>
<a href="?by=authors" onClick={handleClientRouteLinkClick}> <a href="?by=authors">{t('authors')}</a>
{t('authors')}
</a>
</li> </li>
<li> <li>
<a href="?by=reacted" onClick={handleClientRouteLinkClick}> <a href="?by=reacted">{t('reactions')}</a>
{t('reactions')}
</a>
</li> </li>
</ul> </ul>

View File

@ -11,10 +11,9 @@ import RowShort from '../Feed/RowShort'
import Slider from '../Feed/Slider' import Slider from '../Feed/Slider'
import Group from '../Feed/Group' import Group from '../Feed/Group'
import type { Shout, Topic } from '../../graphql/types.gen' import type { Shout, Topic } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { loadShoutsBy, useArticlesStore } from '../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { useTopAuthorsStore } from '../../stores/zine/topAuthors' import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
import { locale } from '../../stores/ui' import { locale } from '../../stores/ui'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
@ -48,8 +47,7 @@ export const HomeView = (props: HomeProps) => {
onMount(async () => { onMount(async () => {
if (sortedArticles().length < PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) { if (sortedArticles().length < PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) {
const { hasMore } = await loadShoutsBy({ const { hasMore } = await loadShouts({
by: {},
limit: CLIENT_LOAD_ARTICLES_COUNT, limit: CLIENT_LOAD_ARTICLES_COUNT,
offset: sortedArticles().length offset: sortedArticles().length
}) })
@ -69,14 +67,7 @@ export const HomeView = (props: HomeProps) => {
return ( return (
<Show when={Boolean(selectedRandomLayout)}> <Show when={Boolean(selectedRandomLayout)}>
<Group <Group articles={articlesByLayout()[selectedRandomLayout]} header={''} />
articles={articlesByLayout()[selectedRandomLayout]}
header={
<div class="layout-icon">
<Icon name={selectedRandomLayout} />
</div>
}
/>
</Show> </Show>
) )
}) })
@ -84,8 +75,8 @@ export const HomeView = (props: HomeProps) => {
const loadMore = async () => { const loadMore = async () => {
saveScrollPosition() saveScrollPosition()
const { hasMore } = await loadShoutsBy({ const { hasMore } = await loadShouts({
by: { visibility: 'public' }, filters: { visibility: 'public' },
limit: LOAD_MORE_PAGE_SIZE, limit: LOAD_MORE_PAGE_SIZE,
offset: sortedArticles().length offset: sortedArticles().length
}) })

View File

@ -3,8 +3,9 @@ import '../../styles/Search.scss'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from '../Feed/Card' import { ArticleCard } from '../Feed/Card'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useArticlesStore, loadShoutsBy } from '../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { useRouter } from '../../stores/router'
type SearchPageSearchParams = { type SearchPageSearchParams = {
by: '' | 'relevance' | 'rating' by: '' | 'relevance' | 'rating'
@ -15,31 +16,49 @@ type Props = {
results: Shout[] results: Shout[]
} }
const LOAD_MORE_PAGE_SIZE = 50
export const SearchView = (props: Props) => { export const SearchView = (props: Props) => {
const { sortedArticles } = useArticlesStore({ shouts: props.results }) const { sortedArticles } = useArticlesStore({ shouts: props.results })
const [getQuery, setQuery] = createSignal(props.query) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [query, setQuery] = createSignal(props.query)
const [offset, setOffset] = createSignal(0)
const { searchParams } = useRouter<SearchPageSearchParams>() const { searchParams, handleClientRouteLinkClick } = useRouter<SearchPageSearchParams>()
let searchEl: HTMLInputElement
const handleQueryChange = (ev) => { const handleQueryChange = (_ev) => {
setQuery(ev.target.value) setQuery(searchEl.value)
} }
const handleSubmit = (_ev) => { const loadMore = async () => {
// TODO page saveScrollPosition()
// TODO sort const { hasMore } = await loadShouts({
loadShoutsBy({ by: { title: getQuery(), body: getQuery() }, limit: 50 }) filters: {
title: query(),
body: query()
},
offset: offset(),
limit: LOAD_MORE_PAGE_SIZE
})
setIsLoadMoreButtonVisible(hasMore)
setOffset(offset() + LOAD_MORE_PAGE_SIZE)
restoreScrollPosition()
} }
return ( return (
<div class="search-page wide-container"> <div class="search-page wide-container">
<form action="/search" class="search-form row"> <form action="/search" class="search-form row">
<div class="col-sm-9"> <div class="col-sm-9">
{/*FIXME t*/} <input
<input type="search" name="q" onChange={handleQueryChange} placeholder="Введите текст..." /> type="search"
name="q"
ref={searchEl}
onInput={handleQueryChange}
placeholder={t('Enter text') + '...'}
/>
</div> </div>
<div class="col-sm-3"> <div class="col-sm-3">
<button class="button" type="submit" onClick={handleSubmit}> <button class="button" type="submit" onClick={loadMore}>
{t('Search')} {t('Search')}
</button> </button>
</div> </div>
@ -87,9 +106,13 @@ export const SearchView = (props: Props) => {
</div> </div>
</div> </div>
<h3>{t('Topics')}</h3> <Show when={isLoadMoreButtonVisible()}>
<p class="load-more-container">
<h3>{t('Authors')}</h3> <button class="button" onClick={loadMore}>
{t('Load more')}
</button>
</p>
</Show>
</Show> </Show>
</div> </div>
) )

View File

@ -8,7 +8,7 @@ import { FullTopic } from '../Topic/Full'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { loadShoutsBy, useArticlesStore } from '../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages' import { splitToPages } from '../../utils/splitToPages'
@ -44,8 +44,8 @@ export const TopicView = (props: TopicProps) => {
const loadMore = async () => { const loadMore = async () => {
saveScrollPosition() saveScrollPosition()
const { hasMore } = await loadShoutsBy({ const { hasMore } = await loadShouts({
by: { topic: topic().slug }, filters: { topic: topic().slug },
limit: LOAD_MORE_PAGE_SIZE, limit: LOAD_MORE_PAGE_SIZE,
offset: sortedArticles().length offset: sortedArticles().length
}) })

View File

@ -1,12 +0,0 @@
import type { JSX } from 'solid-js'
import { createSignal, onMount, Show } from 'solid-js'
// show children only on client side
// usage of isServer causing hydration errors
export const ClientContainer = (props: { children: JSX.Element }) => {
const [isMounted, setIsMounted] = createSignal(false)
onMount(() => setIsMounted(true))
return <Show when={isMounted()}>{props.children}</Show>
}

View File

@ -4,12 +4,14 @@ import { Footer } from '../Discours/Footer'
import '../../styles/app.scss' import '../../styles/app.scss'
import { Show } from 'solid-js' import { Show } from 'solid-js'
import { clsx } from 'clsx'
type PageWrapProps = { type PageWrapProps = {
headerTitle?: string headerTitle?: string
children: JSX.Element children: JSX.Element
isHeaderFixed?: boolean isHeaderFixed?: boolean
hideFooter?: boolean hideFooter?: boolean
class?: string
} }
export const PageWrap = (props: PageWrapProps) => { export const PageWrap = (props: PageWrapProps) => {
@ -18,7 +20,10 @@ export const PageWrap = (props: PageWrapProps) => {
return ( return (
<> <>
<Header title={props.headerTitle} isHeaderFixed={isHeaderFixed} /> <Header title={props.headerTitle} isHeaderFixed={isHeaderFixed} />
<main class="main-content" classList={{ 'main-content--no-padding': !isHeaderFixed }}> <main
class={clsx('main-content', props.class)}
classList={{ 'main-content--no-padding': !isHeaderFixed }}
>
{props.children} {props.children}
</main> </main>
<Show when={props.hideFooter !== true}> <Show when={props.hideFooter !== true}>

View File

@ -0,0 +1,12 @@
import type { JSX } from 'solid-js'
import { createSignal, onMount, Show } from 'solid-js'
const [isClient, setIsClient] = createSignal(false)
// show children only on client side
// usage of isServer causing hydration errors
export const ShowOnlyOnClient = (props: { children: JSX.Element }) => {
onMount(() => setIsClient(true))
return <Show when={isClient()}>{props.children}</Show>
}

View File

@ -1,8 +1,8 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
query LoadShoutsByQuery($by: ShoutsBy, $limit: Int!, $offset: Int!) { query LoadShoutsQuery($options: LoadShoutsOptions) {
loadShoutsBy(by: $by, limit: $limit, offset: $offset) { loadShouts(options: $options) {
_id: slug _id: slug
title title
subtitle subtitle

View File

@ -0,0 +1,42 @@
import { gql } from '@urql/core'
export default gql`
query LoadShoutQuery($slug: String!) {
loadShout(slug: $slug) {
_id: slug
title
subtitle
slug
layout
cover
body
# community
mainTopic
topics {
title
body
slug
stat {
_id: shouts
shouts
authors
followers
}
}
authors {
_id: slug
name
slug
userpic
}
createdAt
publishedAt
stat {
_id: viewed
viewed
reacted
rating
}
}
}
`

View File

@ -12,11 +12,11 @@ export default gql`
links links
createdAt createdAt
lastSeen lastSeen
ratings { # ratings {
_id: rater # _id: rater
rater # rater
value # value
} # }
} }
} }
` `

View File

@ -8,15 +8,15 @@ export default gql`
name name
bio bio
userpic userpic
communities # communities
links links
createdAt # createdAt
lastSeen lastSeen
ratings { # ratings {
_id: rater # _id: rater
rater # rater
value # value
} # }
} }
} }
` `

View File

@ -1,7 +1,7 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
query LoadReactionsByQuery($by: ReactionsBy, $limit: Int!, $offset: Int!) { query LoadReactionsByQuery($by: ReactionBy!, $limit: Int!, $offset: Int!) {
loadReactionsBy(by: $by, limit: $limit, offset: $offset) { loadReactionsBy(by: $by, limit: $limit, offset: $offset) {
id id
createdBy { createdBy {

View File

@ -80,6 +80,14 @@ export type ChatMember = {
userpic?: Maybe<Scalars['String']> userpic?: Maybe<Scalars['String']>
} }
export type ChatUser = {
id: Scalars['Int']
lastSeen?: Maybe<Scalars['DateTime']>
name: Scalars['String']
slug: Scalars['String']
userpic?: Maybe<Scalars['String']>
}
export type Collab = { export type Collab = {
authors: Array<Maybe<Scalars['String']>> authors: Array<Maybe<Scalars['String']>>
body?: Maybe<Scalars['String']> body?: Maybe<Scalars['String']>
@ -116,6 +124,25 @@ export enum FollowingEntity {
Topic = 'TOPIC' Topic = 'TOPIC'
} }
export type LoadShoutsFilters = {
author?: InputMaybe<Scalars['String']>
body?: InputMaybe<Scalars['String']>
days?: InputMaybe<Scalars['Int']>
layout?: InputMaybe<Scalars['String']>
reacted?: InputMaybe<Scalars['Boolean']>
title?: InputMaybe<Scalars['String']>
topic?: InputMaybe<Scalars['String']>
visibility?: InputMaybe<Scalars['String']>
}
export type LoadShoutsOptions = {
filters?: InputMaybe<LoadShoutsFilters>
limit: Scalars['Int']
offset?: InputMaybe<Scalars['Int']>
order_by?: InputMaybe<Scalars['String']>
order_by_desc?: InputMaybe<Scalars['Boolean']>
}
export type Message = { export type Message = {
author: Scalars['String'] author: Scalars['String']
body: Scalars['String'] body: Scalars['String']
@ -183,7 +210,7 @@ export type MutationCreateChatArgs = {
export type MutationCreateMessageArgs = { export type MutationCreateMessageArgs = {
body: Scalars['String'] body: Scalars['String']
chatId: Scalars['String'] chat: Scalars['String']
replyTo?: InputMaybe<Scalars['String']> replyTo?: InputMaybe<Scalars['String']>
} }
@ -317,15 +344,17 @@ export type ProfileInput = {
export type Query = { export type Query = {
authorsAll: Array<Maybe<Author>> authorsAll: Array<Maybe<Author>>
getAuthor: User chatUsersAll: Array<Maybe<ChatUser>>
getAuthor?: Maybe<User>
getCollabs: Array<Maybe<Collab>> getCollabs: Array<Maybe<Collab>>
getTopic: Topic getTopic?: Maybe<Topic>
isEmailUsed: Scalars['Boolean'] isEmailUsed: Scalars['Boolean']
loadAuthorsBy: Array<Maybe<Author>> loadAuthorsBy: Array<Maybe<Author>>
loadChats: Result loadChats: Result
loadMessagesBy: Result loadMessagesBy: Result
loadReactionsBy: Array<Maybe<Reaction>> loadReactionsBy: Array<Maybe<Reaction>>
loadShoutsBy: Array<Maybe<Shout>> loadShout?: Maybe<Shout>
loadShouts: Array<Maybe<Shout>>
markdownBody: Scalars['String'] markdownBody: Scalars['String']
searchUsers: Result searchUsers: Result
signIn: AuthResult signIn: AuthResult
@ -352,8 +381,8 @@ export type QueryIsEmailUsedArgs = {
} }
export type QueryLoadAuthorsByArgs = { export type QueryLoadAuthorsByArgs = {
limit?: InputMaybe<Scalars['Int']>
by?: InputMaybe<AuthorsBy> by?: InputMaybe<AuthorsBy>
limit?: InputMaybe<Scalars['Int']>
offset?: InputMaybe<Scalars['Int']> offset?: InputMaybe<Scalars['Int']>
} }
@ -363,21 +392,23 @@ export type QueryLoadChatsArgs = {
} }
export type QueryLoadMessagesByArgs = { export type QueryLoadMessagesByArgs = {
limit?: InputMaybe<Scalars['Int']>
by: MessagesBy by: MessagesBy
limit?: InputMaybe<Scalars['Int']>
offset?: InputMaybe<Scalars['Int']> offset?: InputMaybe<Scalars['Int']>
} }
export type QueryLoadReactionsByArgs = { export type QueryLoadReactionsByArgs = {
offset?: InputMaybe<Scalars['Int']>
by: ReactionBy by: ReactionBy
limit?: InputMaybe<Scalars['Int']> limit?: InputMaybe<Scalars['Int']>
offset?: InputMaybe<Scalars['Int']>
} }
export type QueryLoadShoutsByArgs = { export type QueryLoadShoutArgs = {
limit?: InputMaybe<Scalars['Int']> slug: Scalars['String']
by?: InputMaybe<ShoutsBy> }
offset?: InputMaybe<Scalars['Int']>
export type QueryLoadShoutsArgs = {
options?: InputMaybe<LoadShoutsOptions>
} }
export type QueryMarkdownBodyArgs = { export type QueryMarkdownBodyArgs = {
@ -563,12 +594,12 @@ export type ShoutInput = {
visibleForUsers?: InputMaybe<Array<InputMaybe<Scalars['String']>>> visibleForUsers?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
} }
export type ShoutsBy = { export type ShoutsFilterBy = {
author?: InputMaybe<Scalars['String']> author?: InputMaybe<Scalars['String']>
authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
body?: InputMaybe<Scalars['String']> body?: InputMaybe<Scalars['String']>
days?: InputMaybe<Scalars['Int']> days?: InputMaybe<Scalars['Int']>
layout?: InputMaybe<Scalars['String']> layout?: InputMaybe<Scalars['String']>
order?: InputMaybe<Scalars['String']>
slug?: InputMaybe<Scalars['String']> slug?: InputMaybe<Scalars['String']>
stat?: InputMaybe<Scalars['String']> stat?: InputMaybe<Scalars['String']>
title?: InputMaybe<Scalars['String']> title?: InputMaybe<Scalars['String']>

View File

@ -4,6 +4,7 @@
"All": "Все", "All": "Все",
"All posts": "Все публикации", "All posts": "Все публикации",
"All topics": "Все темы", "All topics": "Все темы",
"All authors": "Все авторы",
"Authors": "Авторы", "Authors": "Авторы",
"Back to mainpage": "Вернуться на главную", "Back to mainpage": "Вернуться на главную",
"Become an author": "Стать автором", "Become an author": "Стать автором",
@ -175,5 +176,6 @@
"Video": "Видео", "Video": "Видео",
"Literature": "Литература", "Literature": "Литература",
"We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или", "We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или",
"register": "зарегистрируйтесь" "register": "зарегистрируйтесь",
"Enter text": "Введите текст"
} }

View File

@ -9,7 +9,7 @@ if (slug.endsWith('.map')) {
return Astro.redirect('/404') return Astro.redirect('/404')
} }
const article = await apiClient.loadShoutsBy({ by: { slug }, limit: 1}) const article = await apiClient.getShout(slug)
if (!article) { if (!article) {
return Astro.redirect('/404') return Astro.redirect('/404')
} }
@ -21,5 +21,5 @@ Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate'
--- ---
<Prerendered> <Prerendered>
<Root article={article.at(0)} client:load /> <Root article={article} client:load />
</Prerendered> </Prerendered>

View File

@ -6,8 +6,8 @@ import { initRouter } from '../../../stores/router'
import { PRERENDERED_ARTICLES_COUNT } from '../../../components/Views/Author' import { PRERENDERED_ARTICLES_COUNT } from '../../../components/Views/Author'
const slug = Astro.params.slug.toString() const slug = Astro.params.slug.toString()
const shouts = await apiClient.loadShoutsBy({ by: { authors: [slug] } , limit: PRERENDERED_ARTICLES_COUNT }) const shouts = await apiClient.getShouts({ filters: { author: slug }, limit: PRERENDERED_ARTICLES_COUNT })
const author = await apiClient.loadAuthorsBy({ by: { slug } }) const author = await apiClient.getAuthorsBy({ by: { slug } })
const { pathname, search } = Astro.url const { pathname, search } = Astro.url
initRouter(pathname, search) initRouter(pathname, search)

View File

@ -4,13 +4,12 @@ import Prerendered from '../../main.astro'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import { initRouter } from '../../stores/router' import { initRouter } from '../../stores/router'
import type { LayoutType } from '../../stores/zine/layouts' import type { LayoutType } from '../../stores/zine/layouts'
import { Layout } from '../../components/EditorExample/components/Layout'
const layout = (Astro.params.layout?.toString() || 'article') as LayoutType const layout = (Astro.params.layout?.toString() || 'article') as LayoutType
if (!layout || layout.endsWith('.map')) { if (!layout || layout.endsWith('.map')) {
return Astro.redirect('/404') return Astro.redirect('/404')
} }
const shouts = await apiClient.loadShoutsBy({ by: { layout } }) const shouts = await apiClient.getShouts({ filters: { layout }, limit: 50 })
const { pathname, search } = Astro.url const { pathname, search } = Astro.url
initRouter(pathname, search) initRouter(pathname, search)
--- ---

View File

@ -1,7 +1,6 @@
--- ---
import Prerendered from '../main.astro' import Prerendered from '../main.astro'
import { Root } from '../components/Root' import { Root } from '../components/Root'
import { apiClient } from '../utils/apiClient'
import { initRouter } from '../stores/router' import { initRouter } from '../stores/router'
const { pathname, search } = Astro.url const { pathname, search } = Astro.url

View File

@ -6,8 +6,8 @@ import { initRouter } from '../stores/router'
import { PRERENDERED_ARTICLES_COUNT } from '../components/Views/Home' import { PRERENDERED_ARTICLES_COUNT } from '../components/Views/Home'
const randomTopics = await apiClient.getRandomTopics({ amount: 12 }) const randomTopics = await apiClient.getRandomTopics({ amount: 12 })
const articles = await apiClient.loadShoutsBy( const articles = await apiClient.getShouts(
{ by: { visibility: "public" }, limit: PRERENDERED_ARTICLES_COUNT, offset: 0 }) { filters: { visibility: "public" }, limit: PRERENDERED_ARTICLES_COUNT })
const { pathname, search } = Astro.url const { pathname, search } = Astro.url
initRouter(pathname, search) initRouter(pathname, search)

View File

@ -6,7 +6,7 @@ import { initRouter } from '../stores/router'
const params: URLSearchParams = Astro.url.searchParams const params: URLSearchParams = Astro.url.searchParams
const q = params.get('q') const q = params.get('q')
const searchResults = await apiClient.loadShoutsBy({ by: { title: q, body: q }, limit: 50 }) const searchResults = await apiClient.getShouts({ filters: { title: q, body: q }, limit: 50 })
const { pathname, search } = Astro.url const { pathname, search } = Astro.url
initRouter(pathname, search) initRouter(pathname, search)

View File

@ -5,7 +5,7 @@ import { apiClient } from '../../utils/apiClient'
import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Topic' import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Topic'
const slug = Astro.params.slug?.toString() || '' const slug = Astro.params.slug?.toString() || ''
const shouts = await apiClient.loadShoutsBy({ by: { topics: [slug] }, limit: PRERENDERED_ARTICLES_COUNT }) const shouts = await apiClient.getShouts({ filters: { topic: slug }, limit: PRERENDERED_ARTICLES_COUNT })
const topic = await apiClient.getTopic({ slug }) const topic = await apiClient.getTopic({ slug })
import { initRouter } from '../../stores/router' import { initRouter } from '../../stores/router'

View File

@ -63,7 +63,7 @@ const routerStore = createRouter<Routes>(
export const router = routerStore export const router = routerStore
export const handleClientRouteLinkClick = (event) => { const handleClientRouteLinkClick = (event) => {
const link = event.target.closest('a') const link = event.target.closest('a')
if ( if (
link && link &&
@ -96,6 +96,10 @@ export const initRouter = (pathname: string, search: string) => {
routerStore.open(pathname) routerStore.open(pathname)
const params = Object.fromEntries(new URLSearchParams(search)) const params = Object.fromEntries(new URLSearchParams(search))
searchParamsStore.open(params) searchParamsStore.open(params)
if (!isServer) {
document.addEventListener('click', handleClientRouteLinkClick)
}
} }
if (!isServer) { if (!isServer) {
@ -125,6 +129,7 @@ export const useRouter = <TSearchParams extends Record<string, string> = Record<
return { return {
page, page,
searchParams, searchParams,
changeSearchParam changeSearchParam,
handleClientRouteLinkClick
} }
} }

View File

@ -1,4 +1,4 @@
import type { Author, Shout, ShoutInput, ShoutsBy, Topic } from '../../graphql/types.gen' import type { Author, Shout, ShoutInput, Topic, LoadShoutsOptions } from '../../graphql/types.gen'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import { addAuthorsByTopic } from './authors' import { addAuthorsByTopic } from './authors'
import { addTopicsByAuthor } from './topics' import { addTopicsByAuthor } from './topics'
@ -123,22 +123,18 @@ const addSortedArticles = (articles: Shout[]) => {
setSortedArticles((prevSortedArticles) => [...prevSortedArticles, ...articles]) setSortedArticles((prevSortedArticles) => [...prevSortedArticles, ...articles])
} }
export const loadShoutsBy = async ({ export const loadShout = async (slug: string): Promise<void> => {
by, const newArticle = await apiClient.getShout(slug)
limit, addArticles([newArticle])
offset = 0 }
}: {
by: ShoutsBy export const loadShouts = async (options: LoadShoutsOptions): Promise<{ hasMore: boolean }> => {
limit: number const newArticles = await apiClient.getShouts({
offset?: number ...options,
}): Promise<{ hasMore: boolean }> => { limit: options.limit + 1
const newArticles = await apiClient.loadShoutsBy({
by,
limit: limit + 1,
offset
}) })
const hasMore = newArticles.length === limit + 1 const hasMore = newArticles.length === options.limit + 1
if (hasMore) { if (hasMore) {
newArticles.splice(-1) newArticles.splice(-1)
@ -176,7 +172,6 @@ export const useArticlesStore = (initialState: InitialState = {}) => {
return { return {
articleEntities, articleEntities,
sortedArticles, sortedArticles,
loadShoutsBy,
articlesByAuthor, articlesByAuthor,
articlesByLayout, articlesByLayout,
articlesByTopic, articlesByTopic,

View File

@ -38,7 +38,7 @@ const sortedAuthors = createLazyMemo(() => {
}) })
const addAuthors = (authors: Author[]) => { const addAuthors = (authors: Author[]) => {
const newAuthorEntities = authors.reduce((acc, author) => { const newAuthorEntities = authors.filter(Boolean).reduce((acc, author) => {
acc[author.slug] = author acc[author.slug] = author
return acc return acc
}, {} as Record<string, Author>) }, {} as Record<string, Author>)

View File

@ -1,6 +1,5 @@
import type { Shout, ShoutsBy } from '../../graphql/types.gen' import type { Shout, LoadShoutsOptions } from '../../graphql/types.gen'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import { useArticlesStore } from './articles'
import { createSignal } from 'solid-js' import { createSignal } from 'solid-js'
export type LayoutType = 'article' | 'audio' | 'video' | 'image' | 'literature' export type LayoutType = 'article' | 'audio' | 'video' | 'image' | 'literature'
@ -22,27 +21,18 @@ export const resetSortedLayoutShouts = () => {
setSortedLayoutShouts(new Map()) setSortedLayoutShouts(new Map())
} }
export const loadLayoutShoutsBy = async ({ export const loadLayoutShoutsBy = async (options: LoadShoutsOptions): Promise<{ hasMore: boolean }> => {
by, const newLayoutShouts = await apiClient.getShouts({
limit, ...options,
offset limit: options.limit + 1
}: {
by: ShoutsBy
limit?: number
offset?: number
}): Promise<{ hasMore: boolean }> => {
const newLayoutShouts = await apiClient.loadShoutsBy({
by,
limit: limit + 1,
offset
}) })
const hasMore = newLayoutShouts.length === limit + 1 const hasMore = newLayoutShouts.length === options.limit + 1
if (hasMore) { if (hasMore) {
newLayoutShouts.splice(-1) newLayoutShouts.splice(-1)
} }
addLayoutShouts(by.layout as LayoutType, newLayoutShouts) addLayoutShouts(options.filters.layout as LayoutType, newLayoutShouts)
return { hasMore } return { hasMore }
} }

View File

@ -16,7 +16,7 @@ export const loadReactionsBy = async ({
limit?: number limit?: number
offset?: number offset?: number
}): Promise<{ hasMore: boolean }> => { }): Promise<{ hasMore: boolean }> => {
const data = await apiClient.loadReactionsBy({ by, limit: limit + 1, offset }) const data = await apiClient.getReactionsBy({ by, limit: limit + 1, offset })
const hasMore = data.length === limit + 1 const hasMore = data.length === limit + 1
if (hasMore) data.splice(-1) if (hasMore) data.splice(-1)
// TODO: const [data, provider] = roomConnect(articleSlug, username, "reactions") // TODO: const [data, provider] = roomConnect(articleSlug, username, "reactions")

View File

@ -53,7 +53,7 @@ const topTopics = createMemo(() => {
}) })
const addTopics = (...args: Topic[][]) => { const addTopics = (...args: Topic[][]) => {
const allTopics = args.flatMap((topics) => topics || []) const allTopics = args.flatMap((topics) => (topics || []).filter(Boolean))
const newTopicEntities = allTopics.reduce((acc, topic) => { const newTopicEntities = allTopics.reduce((acc, topic) => {
acc[topic.slug] = topic acc[topic.slug] = topic

View File

@ -27,6 +27,11 @@
.container { .container {
width: auto; width: auto;
.search-input {
display: inline-block;
width: 100px !important;
}
} }
} }
@ -39,7 +44,7 @@
text-align: center; text-align: center;
.loadMoreButton { .loadMoreButton {
padding: 0.6em 5em; padding: 0.6em 3em;
width: 100%; width: 100%;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {

View File

@ -2,6 +2,7 @@
@import 'globals'; @import 'globals';
@import 'bootstrap/scss/functions'; @import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables'; @import 'bootstrap/scss/variables';
@import 'bootstrap/scss/maps';
@import 'bootstrap/scss/vendor/rfs'; @import 'bootstrap/scss/vendor/rfs';
@import 'bootstrap/scss/mixins/breakpoints'; @import 'bootstrap/scss/mixins/breakpoints';
@import 'bootstrap/scss/mixins/grid'; @import 'bootstrap/scss/mixins/grid';

View File

@ -232,7 +232,6 @@ button {
font-weight: 400; font-weight: 400;
height: auto; height: auto;
margin-top: 0.6rem;
padding: 0.6rem 1.2rem 0.6rem 1rem; padding: 0.6rem 1.2rem 0.6rem 1rem;
} }
@ -641,7 +640,7 @@ astro-island {
.main-content { .main-content {
flex: 1 100%; flex: 1 100%;
min-height: 300px; min-height: 300px;
padding-top: 100px; padding-top: 120px;
position: relative; position: relative;
} }

View File

@ -1,4 +1,11 @@
import type { FollowingEntity, AuthResult, ShoutInput, Topic, Author } from '../graphql/types.gen' import type {
FollowingEntity,
AuthResult,
ShoutInput,
Topic,
Author,
LoadShoutsOptions
} from '../graphql/types.gen'
import { publicGraphQLClient } from '../graphql/publicGraphQLClient' import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient' import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient'
import topicsAll from '../graphql/query/topics-all' import topicsAll from '../graphql/query/topics-all'
@ -26,8 +33,7 @@ import reactionsLoadBy from '../graphql/query/reactions-load-by'
import { REACTIONS_AMOUNT_PER_PAGE } from '../stores/zine/reactions' import { REACTIONS_AMOUNT_PER_PAGE } from '../stores/zine/reactions'
import authorsLoadBy from '../graphql/query/authors-load-by' import authorsLoadBy from '../graphql/query/authors-load-by'
import shoutsLoadBy from '../graphql/query/articles-load-by' import shoutsLoadBy from '../graphql/query/articles-load-by'
import shoutLoad from '../graphql/query/articles-load'
const FEED_SIZE = 50
type ApiErrorCode = type ApiErrorCode =
| 'unknown' | 'unknown'
@ -195,6 +201,7 @@ export const apiClient = {
}, },
getAuthor: async ({ slug }: { slug: string }): Promise<Author> => { getAuthor: async ({ slug }: { slug: string }): Promise<Author> => {
const response = await publicGraphQLClient.query(authorBySlug, { slug }).toPromise() const response = await publicGraphQLClient.query(authorBySlug, { slug }).toPromise()
console.error('getAuthor', response)
return response.data.getAuthor return response.data.getAuthor
}, },
getTopic: async ({ slug }: { slug: string }): Promise<Topic> => { getTopic: async ({ slug }: { slug: string }): Promise<Topic> => {
@ -230,20 +237,33 @@ export const apiClient = {
return response.data.deleteReaction return response.data.deleteReaction
}, },
// LOAD BY getAuthorsBy: async ({ by, limit = 50, offset = 0 }) => {
loadAuthorsBy: async ({ by, limit = 50, offset = 0 }) => {
const resp = await publicGraphQLClient.query(authorsLoadBy, { by, limit, offset }).toPromise() const resp = await publicGraphQLClient.query(authorsLoadBy, { by, limit, offset }).toPromise()
console.debug(resp) console.debug(resp)
return resp.data.loadShoutsBy return resp.data.loadAuthorsBy
}, },
loadShoutsBy: async ({ by, limit = 50, offset = 0 }) => { getShout: async (slug: string) => {
const resp = await publicGraphQLClient.query(shoutsLoadBy, { by, limit, offset }).toPromise() const resp = await publicGraphQLClient
console.debug(resp) .query(shoutLoad, {
return resp.data.loadShoutsBy slug
})
.toPromise()
return resp.data.loadShout
}, },
loadReactionsBy: async ({ by, limit = REACTIONS_AMOUNT_PER_PAGE, offset = 0 }) => { getShouts: async (options: LoadShoutsOptions) => {
const resp = await publicGraphQLClient
.query(shoutsLoadBy, {
options
})
.toPromise()
// console.debug(resp)
return resp.data.loadShouts
},
getReactionsBy: async ({ by, limit = REACTIONS_AMOUNT_PER_PAGE, offset = 0 }) => {
const resp = await publicGraphQLClient.query(reactionsLoadBy, { by, limit, offset }).toPromise() const resp = await publicGraphQLClient.query(reactionsLoadBy, { by, limit, offset }).toPromise()
console.log('resactions response', resp)
return resp.data.loadReactionsBy return resp.data.loadReactionsBy
}, },

View File

@ -1,7 +1,6 @@
export const isDev = import.meta.env.MODE === 'development' export const isDev = import.meta.env.MODE === 'development'
//export const apiBaseUrl = 'https://v2.discours.io' // export const apiBaseUrl = 'https://v2.discours.io'
// export const apiBaseUrl = 'https://testapi.discours.io'
// export const apiBaseUrl = 'https://newapi.discours.io' // export const apiBaseUrl = 'https://newapi.discours.io'
// testapi.discours.io // testapi.discours.io
export const apiBaseUrl = 'http://localhost:8080' export const apiBaseUrl = 'http://localhost:8080'

View File

@ -41,9 +41,9 @@
node-fetch "^2.6.1" node-fetch "^2.6.1"
"@astrojs/compiler@^0.27.0 || ^0.28.0 || ^0.29.0", "@astrojs/compiler@^0.29.15", "@astrojs/compiler@^0.29.3": "@astrojs/compiler@^0.27.0 || ^0.28.0 || ^0.29.0", "@astrojs/compiler@^0.29.15", "@astrojs/compiler@^0.29.3":
version "0.29.16" version "0.29.17"
resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.29.16.tgz#453d7f4da6c2d0935743f4b7075141f619ac0a05" resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.29.17.tgz#5c65e18fd5dde9620dcc1794a858609b66408215"
integrity sha512-1CCf+dktc8IQCdmsNNSIor3rcJE5OIirFnFtQWp1VUxqCacefqRRlsl9lH7JcKKpRvz1taL43yHYJP8dxNfVww== integrity sha512-6ZbRGVunUMHxROD9Cleqkrfrj/kM9o43nLVwycdxCexCB5G372evy2ZM46LhaG/k5B5yC0PByNHTaGny0ll3iQ==
"@astrojs/language-server@^0.28.3": "@astrojs/language-server@^0.28.3":
version "0.28.3" version "0.28.3"
@ -138,12 +138,13 @@
which-pm-runs "^1.1.0" which-pm-runs "^1.1.0"
"@astrojs/vercel@^2.3.3": "@astrojs/vercel@^2.3.3":
version "2.3.3" version "2.3.4"
resolved "https://registry.yarnpkg.com/@astrojs/vercel/-/vercel-2.3.3.tgz#354aebd3e504d57d9e7794a7e8c5229885d92928" resolved "https://registry.yarnpkg.com/@astrojs/vercel/-/vercel-2.3.4.tgz#79e3658786bebfa57d1c0efdec1b4db35ee3f4c4"
integrity sha512-gdYf98Oii8MEfRHyX6Uwsbvx/rjimZV75qPSMbcGPFUCteshENFvtUAk5jMJvJNrDh/Cxjt4akTd4/llbvWBeQ== integrity sha512-1mrPdlb68Y+DPtDpOfuRlene9F2t+wICXDLoG+bII9ryURxTYCReV5JQ1uwlKQ3yizQLIKIzm0rs5yp+K5FWbw==
dependencies: dependencies:
"@astrojs/webapi" "^1.1.1" "@astrojs/webapi" "^1.1.1"
"@vercel/nft" "^0.22.1" "@vercel/nft" "^0.22.1"
fast-glob "^3.2.11"
"@astrojs/webapi@^1.1.1": "@astrojs/webapi@^1.1.1":
version "1.1.1" version "1.1.1"
@ -1765,9 +1766,9 @@
integrity sha512-1eZA1/HYOhmlZ9LrrGot+LUi/ypO2NXqfB+9F1WY98dGNDMz9pG9k+X7kg2YDJTUHDGbzY7WrsBRyAE8LurE7Q== integrity sha512-1eZA1/HYOhmlZ9LrrGot+LUi/ypO2NXqfB+9F1WY98dGNDMz9pG9k+X7kg2YDJTUHDGbzY7WrsBRyAE8LurE7Q==
"@solid-primitives/resize-observer@^2.0.5": "@solid-primitives/resize-observer@^2.0.5":
version "2.0.6" version "2.0.7"
resolved "https://registry.yarnpkg.com/@solid-primitives/resize-observer/-/resize-observer-2.0.6.tgz#2086c92d3a5f82512ecbc47fceff02eac272bd2c" resolved "https://registry.yarnpkg.com/@solid-primitives/resize-observer/-/resize-observer-2.0.7.tgz#0f909ed58d5fd7ec59b2fee15ddafdd28fdce4c8"
integrity sha512-PbYmBFJBx1/WcrTZepcr6fABOrUP6CeXxehy2AKPCJInX3LKQ/elHLsM1g6KwVbvqpZ0aQ3a/3I7sRYk6BSrpw== integrity sha512-/RtCTs61ACdsCKJodNTgnKA05CI09dkg7usAb5jg14L6mzwTNWWdZbXtbYsUlD+kh1/1j+BKxp6VtkbpgJE5yQ==
dependencies: dependencies:
"@solid-primitives/event-listener" "^2.2.4" "@solid-primitives/event-listener" "^2.2.4"
"@solid-primitives/rootless" "^1.2.1" "@solid-primitives/rootless" "^1.2.1"
@ -2579,9 +2580,9 @@ astro-eslint-parser@^0.9.0:
espree "^9.0.0" espree "^9.0.0"
astro@^1.6.8: astro@^1.6.8:
version "1.6.8" version "1.6.9"
resolved "https://registry.yarnpkg.com/astro/-/astro-1.6.8.tgz#46ab77d8e968088faf8bcc2e77d2856cb1fe0bdd" resolved "https://registry.yarnpkg.com/astro/-/astro-1.6.9.tgz#08d7aed72168f8f45fc46e3ac47dd1a8ac0e2bbc"
integrity sha512-+kOj8s2fguCFCim9he6fl9iugIHrmAl7BmfNXdTdC9zU30VYV162HF5eRJyMlY5hGuDn3GvAoaNSzCMnybVsFQ== integrity sha512-KXFKXobe8MIYl4gduUPLcAazMz+thox6N1pOv3F3QMbJS5rMRXkWloVK/6XebBO7p3DYkOfOGB4qA9ijTc4ftA==
dependencies: dependencies:
"@astrojs/compiler" "^0.29.15" "@astrojs/compiler" "^0.29.15"
"@astrojs/language-server" "^0.28.3" "@astrojs/language-server" "^0.28.3"
@ -2640,8 +2641,8 @@ astro@^1.6.8:
typescript "*" typescript "*"
unist-util-visit "^4.1.0" unist-util-visit "^4.1.0"
vfile "^5.3.2" vfile "^5.3.2"
vite "~3.2.1" vite "~3.2.4"
vitefu "^0.2.0" vitefu "^0.2.1"
yargs-parser "^21.0.1" yargs-parser "^21.0.1"
zod "^3.17.3" zod "^3.17.3"
@ -2875,10 +2876,10 @@ boolean@^3.0.1:
resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b"
integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
bootstrap@5.1.3: bootstrap@5.2.2:
version "5.1.3" version "5.2.2"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.3.tgz#ba081b0c130f810fa70900acbc1c6d3c28fa8f34" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.2.tgz#834e053eed584a65e244d8aa112a6959f56e27a0"
integrity sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q== integrity sha512-dEtzMTV71n6Fhmbg4fYJzQsw1N29hJKO1js5ackCgIpDcGid2ETMGC6zwSYw09v05Y+oRdQ9loC54zB1La3hHQ==
boxen@^6.2.1: boxen@^6.2.1:
version "6.2.1" version "6.2.1"
@ -9703,7 +9704,7 @@ vfile@^5.0.0, vfile@^5.3.2:
unist-util-stringify-position "^3.0.0" unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0" vfile-message "^3.0.0"
vite@^3.2.4, vite@~3.2.1: vite@^3.2.4, vite@~3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.4.tgz#d8c7892dd4268064e04fffbe7d866207dd24166e" resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.4.tgz#d8c7892dd4268064e04fffbe7d866207dd24166e"
integrity sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw== integrity sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==
@ -9715,7 +9716,7 @@ vite@^3.2.4, vite@~3.2.1:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
vitefu@^0.2.0: vitefu@^0.2.0, vitefu@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.1.tgz#9dcd78737c77b366206706dac2403a751903907d" resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.1.tgz#9dcd78737c77b366206706dac2403a751903907d"
integrity sha512-clkvXTAeUf+XQKm3bhWUhT4pye+3acm6YCTGaWhxxIvZZ/QjnA3JA8Zud+z/mO5y5XYvJJhevs5Sjkv/FI8nRw== integrity sha512-clkvXTAeUf+XQKm3bhWUhT4pye+3acm6YCTGaWhxxIvZZ/QjnA3JA8Zud+z/mO5y5XYvJJhevs5Sjkv/FI8nRw==