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:
commit
6e21d0640b
11
README.md
11
README.md
|
@ -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.
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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",
|
||||||
|
|
10965
pnpm-lock.yaml
10965
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -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} />
|
||||||
|
|
||||||
</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} />
|
||||||
|
|
||||||
</Show>
|
</Show>
|
||||||
<span class={styles.buttonLabel}>{t('Unfollow')}</span>
|
<span class={styles.buttonLabel}>{t('Unfollow')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
3
src/components/Pages/HomePage.module.scss
Normal file
3
src/components/Pages/HomePage.module.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.mainContent {
|
||||||
|
padding-top: 100px;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -67,28 +67,28 @@ export const GuidePage = () => {
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Дискурс — независимый журнал о культуре, науке, искусстве и обществе
|
Дискурс — независимый журнал о культуре, науке, искусстве и обществе
|
||||||
с <a href="/about/manifest">открытой редакцией</a>. У нас нет главного редактора,
|
с
|
||||||
инвестора и вообще никого, кто бы принимал единоличные решения. Вместо традиционных
|
<a href="/about/manifest">открытой редакцией</a>. У нас нет главного редактора, инвестора
|
||||||
иерархий Дискурс основан на принципах прямой демократии: в нашем горизонтальном
|
и вообще никого, кто бы принимал единоличные решения. Вместо традиционных иерархий
|
||||||
сообществе все редакционные вопросы решаются открытым голосованием авторов журнала. Вот как
|
Дискурс основан на принципах прямой демократии: в нашем горизонтальном сообществе
|
||||||
это работает.
|
все редакционные вопросы решаются открытым голосованием авторов журнала. Вот как это работает.
|
||||||
</p>
|
</p>
|
||||||
<h3 id="how-it-works">Как устроен сайт Дискурса</h3>
|
<h3 id="how-it-works">Как устроен сайт Дискурса</h3>
|
||||||
<p>Дискурс состоит из четырех основных разделов:</p>
|
<p>Дискурс состоит из четырех основных разделов:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<a href="/topics">Темы</a> — у нас публикуются исследования, обзоры, эссе,
|
<a href="/topics">Темы</a>
|
||||||
интервью, репортажи, аналитика и другие материалы о культуре, науке, искусстве
|
— у нас публикуются исследования, обзоры, эссе, интервью, репортажи,
|
||||||
и обществе.
|
аналитика и другие материалы о культуре, науке, искусстве и обществе.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<a href="/topic/art">Искусство</a> — здесь, например, представлены
|
<a href="/topic/art">Искусство</a>
|
||||||
художественные произведения: литература, живопись, музыка, фотографии, видео. Этот раздел
|
— здесь, например, представлены художественные произведения: литература,
|
||||||
помогает прозвучать новому искусству, которое создают российские художники, писатели,
|
живопись, музыка, фотографии, видео. Этот раздел помогает прозвучать новому искусству,
|
||||||
режиссёры и музыканты.
|
которое создают российские художники, писатели, режиссёры и музыканты.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
{/*
|
{/*
|
||||||
|
@ -118,14 +118,16 @@ export const GuidePage = () => {
|
||||||
— ключевым словам, которые располагаются в конце материалов и связывают
|
— ключевым словам, которые располагаются в конце материалов и связывают
|
||||||
материалы по жанрам (например,
|
материалы по жанрам (например,
|
||||||
<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>), по тематике (
|
<a href="/topic/essay">эссе</a>, <a href="/topic/likbez">ликбезы</a>
|
||||||
<a href="/topic/cinema">кино</a>, <a href="/topic/philosophy">философия</a>,{' '}
|
), по тематике (<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> и т.д.) или в серии (как «
|
<a href="/topic/absurdism">абсурдизм</a>, <a href="/topic/sex">секс</a> и т.д.) или
|
||||||
<a href="/topic/zakony-mira">Законы мира</a>» или «
|
в серии (как «
|
||||||
<a href="/topic/za-liniey-mannergeyma">За линией Маннергейма</a>»). Темы объединяют
|
<a href="/topic/zakony-mira">Законы мира</a>
|
||||||
сотни публикаций, помогают ориентироваться в журнале и следить за интересными
|
» или «
|
||||||
материалами.
|
<a href="/topic/za-liniey-mannergeyma">За линией Маннергейма</a>
|
||||||
|
»). Темы объединяют сотни публикаций, помогают ориентироваться в журнале
|
||||||
|
и следить за интересными материалами.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
|
@ -91,10 +91,11 @@ export const ManifestPage = () => {
|
||||||
<p>
|
<p>
|
||||||
Редакция Дискурса открыта для всех: у нас нет цензуры, запретных тем
|
Редакция Дискурса открыта для всех: у нас нет цензуры, запретных тем
|
||||||
и идеологических рамок. Каждый может <a href="/create">прислать материал</a>{' '}
|
и идеологических рамок. Каждый может <a href="/create">прислать материал</a>{' '}
|
||||||
в журнал и <a href="/about/guide">присоединиться к редакции</a>. Предоставляя
|
в журнал и
|
||||||
трибуну для независимой журналистики и художественных проектов, мы помогаем людям
|
<a href="/about/guide">присоединиться к редакции</a>. Предоставляя трибуну для
|
||||||
рассказывать свои истории так, чтобы они были услышаны. Мы убеждены: чем больше голосов
|
независимой журналистики и художественных проектов, мы помогаем людям рассказывать
|
||||||
будет звучать на Дискурсе, тем громче в полифонии мнений будет слышна истина.
|
свои истории так, чтобы они были услышаны. Мы убеждены: чем больше голосов будет звучать
|
||||||
|
на Дискурсе, тем громче в полифонии мнений будет слышна истина.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="h2" id="participation">
|
<h2 class="h2" id="participation">
|
||||||
|
|
|
@ -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">«предложить идею»</a>.
|
или через форму <a href="/connect">«предложить идею»</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -76,11 +76,7 @@ export const ThanksPage = () => {
|
||||||
признательны всем, кто нас поддерживает. Ваши пожертвования — финансовый фундамент
|
признательны всем, кто нас поддерживает. Ваши пожертвования — финансовый фундамент
|
||||||
журнала. Благодаря вам мы развиваем платформу качественной журналистики, которая помогает
|
журнала. Благодаря вам мы развиваем платформу качественной журналистики, которая помогает
|
||||||
самым разным авторам быть услышанными. Стать нашим меценатом и подписаться
|
самым разным авторам быть услышанными. Стать нашим меценатом и подписаться
|
||||||
на ежемесячную поддержку проекта можно{' '}
|
на ежемесячную поддержку проекта можно <a href="/about/help">здесь</a>.
|
||||||
<a href="/about/help" target="_self">
|
|
||||||
здесь
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
|
||||||
}
|
|
|
@ -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}>
|
||||||
|
|
12
src/components/_shared/ShowOnlyOnClient.tsx
Normal file
12
src/components/_shared/ShowOnlyOnClient.tsx
Normal 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>
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
42
src/graphql/query/articles-load.ts
Normal file
42
src/graphql/query/articles-load.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -12,11 +12,11 @@ export default gql`
|
||||||
links
|
links
|
||||||
createdAt
|
createdAt
|
||||||
lastSeen
|
lastSeen
|
||||||
ratings {
|
# ratings {
|
||||||
_id: rater
|
# _id: rater
|
||||||
rater
|
# rater
|
||||||
value
|
# value
|
||||||
}
|
# }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -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
|
||||||
}
|
# }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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']>
|
||||||
|
|
|
@ -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": "Введите текст"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
---
|
---
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>)
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
41
yarn.lock
41
yarn.lock
|
@ -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==
|
||||||
|
|
Loading…
Reference in New Issue
Block a user