Merge remote-tracking branch 'origin/dev' into editor
# Conflicts: # src/components/Feed/Row3.tsx # src/components/Nav/AuthModal/EmailConfirm.tsx # src/components/Nav/AuthModal/ForgotPasswordForm.tsx # src/components/Nav/AuthModal/LoginForm.tsx # src/components/Nav/AuthModal/RegisterForm.tsx # src/components/Nav/AuthModal/index.tsx # src/components/Nav/Header.tsx # src/components/Nav/Modal.tsx # src/components/Nav/Popup.tsx # src/components/Nav/Private.tsx # src/components/Root.tsx # src/components/Topic/Card.tsx # src/components/Views/AllAuthors.tsx # src/components/Views/Author.tsx # src/components/Views/Home.tsx # src/components/Views/Topic.tsx # src/graphql/mutation/auth-confirm-email.ts # src/locales/ru.json # src/pages/welcome.astro # src/stores/auth.ts # src/stores/ui.ts # src/stores/zine/authors.ts # src/utils/apiClient.ts # src/utils/config.ts
This commit is contained in:
commit
5bcef1d1e2
|
@ -35,8 +35,9 @@ module.exports = {
|
||||||
varsIgnorePattern: '^log$'
|
varsIgnorePattern: '^log$'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'@typescript-eslint/no-explicit-any': 'warn',
|
// TODO: Remove any usage and enable
|
||||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||||
|
|
||||||
// solid-js fix
|
// solid-js fix
|
||||||
'import/no-unresolved': [2, { ignore: ['solid-js/'] }]
|
'import/no-unresolved': [2, { ignore: ['solid-js/'] }]
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
{
|
{
|
||||||
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write",
|
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write"
|
||||||
"package.json": "sort-package-json",
|
|
||||||
"*.{scss,css}": "stylelint",
|
|
||||||
"*.{ts,tsx,js}": "eslint --fix"
|
|
||||||
}
|
}
|
||||||
|
|
6
.lintstagedrc.bak
Normal file
6
.lintstagedrc.bak
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write",
|
||||||
|
"package.json": "sort-package-json",
|
||||||
|
"*.{scss,css}": "stylelint",
|
||||||
|
"*.{ts,tsx,js}": "eslint --fix"
|
||||||
|
}
|
|
@ -17,7 +17,7 @@
|
||||||
"lint:code:fix": "eslint . --fix",
|
"lint:code:fix": "eslint . --fix",
|
||||||
"lint:styles": "stylelint **/*.{scss,css}",
|
"lint:styles": "stylelint **/*.{scss,css}",
|
||||||
"lint:styles:fix": "stylelint **/*.{scss,css} --fix",
|
"lint:styles:fix": "stylelint **/*.{scss,css} --fix",
|
||||||
"pre-commit": "",
|
"pre-commit": "lint-staged",
|
||||||
"pre-push": "",
|
"pre-push": "",
|
||||||
"pre-commit-old": "lint-staged",
|
"pre-commit-old": "lint-staged",
|
||||||
"pre-push-old": "npm run typecheck",
|
"pre-push-old": "npm run typecheck",
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { showModal } from '../../stores/ui'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { incrementView } from '../../stores/zine/articles'
|
import { incrementView } from '../../stores/zine/articles'
|
||||||
import MD from './MD'
|
import MD from './MD'
|
||||||
|
import { SharePopup } from './SharePopup'
|
||||||
|
|
||||||
const MAX_COMMENT_LEVEL = 6
|
const MAX_COMMENT_LEVEL = 6
|
||||||
|
|
||||||
|
@ -126,9 +127,13 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
{/* </a>*/}
|
{/* </a>*/}
|
||||||
{/*</div>*/}
|
{/*</div>*/}
|
||||||
<div class="shout-stats__item">
|
<div class="shout-stats__item">
|
||||||
<a href="#share" onClick={() => showModal('share')}>
|
<SharePopup
|
||||||
|
trigger={
|
||||||
|
<a href="#" onClick={(event) => event.preventDefault()}>
|
||||||
<Icon name="share" />
|
<Icon name="share" />
|
||||||
</a>
|
</a>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/*FIXME*/}
|
{/*FIXME*/}
|
||||||
{/*<Show when={canEdit()}>*/}
|
{/*<Show when={canEdit()}>*/}
|
||||||
|
|
45
src/components/Article/SharePopup.tsx
Normal file
45
src/components/Article/SharePopup.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { Icon } from '../Nav/Icon'
|
||||||
|
import styles from '../Nav/Popup.module.scss'
|
||||||
|
import { t } from '../../utils/intl'
|
||||||
|
import { Popup, PopupProps } from '../Nav/Popup'
|
||||||
|
|
||||||
|
type SharePopupProps = Omit<PopupProps, 'children'>
|
||||||
|
|
||||||
|
export const SharePopup = (props: SharePopupProps) => {
|
||||||
|
return (
|
||||||
|
<Popup {...props}>
|
||||||
|
<ul class="nodash">
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<Icon name="vk-white" class={styles.icon} />
|
||||||
|
VK
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<Icon name="facebook-white" class={styles.icon} />
|
||||||
|
Facebook
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<Icon name="twitter-white" class={styles.icon} />
|
||||||
|
Twitter
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<Icon name="telegram-white" class={styles.icon} />
|
||||||
|
Telegram
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<Icon name="link-white" class={styles.icon} />
|
||||||
|
{t('Copy link')}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popup>
|
||||||
|
)
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import type { Author } from '../../graphql/types.gen'
|
||||||
import { AuthorCard } from './Card'
|
import { AuthorCard } from './Card'
|
||||||
import './Full.scss'
|
import './Full.scss'
|
||||||
|
|
||||||
export default (props: { author: Author }) => {
|
export const AuthorFull = (props: { author: Author }) => {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { Show } from 'solid-js/web'
|
import { Show } from 'solid-js/web'
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import style from './Userpic.module.scss'
|
import style from './Userpic.module.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
interface UserpicProps {
|
interface UserpicProps {
|
||||||
user: Author
|
user: Author
|
||||||
hasLink?: boolean
|
hasLink?: boolean
|
||||||
isBig?: boolean
|
isBig?: boolean
|
||||||
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props: UserpicProps) => {
|
export default (props: UserpicProps) => {
|
||||||
|
@ -16,7 +18,7 @@ export default (props: UserpicProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={style.circlewrap} classList={{ [style.big]: props.isBig }}>
|
<div class={clsx(style.circlewrap, props.class)} classList={{ [style.big]: props.isBig }}>
|
||||||
<Show when={props.hasLink}>
|
<Show when={props.hasLink}>
|
||||||
<a href={`/author/${props.user.slug}`}>
|
<a href={`/author/${props.user.slug}`}>
|
||||||
<Show
|
<Show
|
||||||
|
|
|
@ -21,7 +21,7 @@ interface BesideProps {
|
||||||
iconButton?: boolean
|
iconButton?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props: BesideProps) => {
|
export const Beside = (props: BesideProps) => {
|
||||||
return (
|
return (
|
||||||
<Show when={!!props.beside?.slug && props.values?.length > 0}>
|
<Show when={!!props.beside?.slug && props.values?.length > 0}>
|
||||||
<div class="floor floor--9">
|
<div class="floor floor--9">
|
||||||
|
|
|
@ -418,7 +418,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shoutCardDetailsTtem {
|
.shoutCardDetailsItem {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-right: 1.7em;
|
margin-right: 1.7em;
|
||||||
|
@ -454,6 +454,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shoutCardDetailsViewed {
|
||||||
|
.icon {
|
||||||
|
margin-top: -0.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.rating {
|
.rating {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type { Shout } from '../../graphql/types.gen'
|
||||||
import { capitalize } from '../../utils'
|
import { capitalize } from '../../utils'
|
||||||
import { translit } from '../../utils/ru2en'
|
import { translit } from '../../utils/ru2en'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import style 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 { handleClientRouteLinkClick } from '../../stores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
@ -72,33 +72,33 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
class={clsx(style.shoutCard, `${props.settings?.additionalClass || ''}`)}
|
class={clsx(styles.shoutCard, `${props.settings?.additionalClass || ''}`)}
|
||||||
classList={{
|
classList={{
|
||||||
[style.shoutCardShort]: props.settings?.isShort,
|
[styles.shoutCardShort]: props.settings?.isShort,
|
||||||
[style.shoutCardPhotoBottom]: props.settings?.noimage && props.settings?.photoBottom,
|
[styles.shoutCardPhotoBottom]: props.settings?.noimage && props.settings?.photoBottom,
|
||||||
[style.shoutCardFeed]: props.settings?.isFeedMode,
|
[styles.shoutCardFeed]: props.settings?.isFeedMode,
|
||||||
[style.shoutCardFloorImportant]: props.settings?.isFloorImportant,
|
[styles.shoutCardFloorImportant]: props.settings?.isFloorImportant,
|
||||||
[style.shoutCardWithCover]: props.settings?.isWithCover,
|
[styles.shoutCardWithCover]: props.settings?.isWithCover,
|
||||||
[style.shoutCardBigTitle]: props.settings?.isBigTitle,
|
[styles.shoutCardBigTitle]: props.settings?.isBigTitle,
|
||||||
[style.shoutCardVertical]: props.settings?.isVertical,
|
[styles.shoutCardVertical]: props.settings?.isVertical,
|
||||||
[style.shoutCardWithBorder]: props.settings?.withBorder,
|
[styles.shoutCardWithBorder]: props.settings?.withBorder,
|
||||||
[style.shoutCardCompact]: props.settings?.isCompact,
|
[styles.shoutCardCompact]: props.settings?.isCompact,
|
||||||
[style.shoutCardSingle]: props.settings?.isSingle
|
[styles.shoutCardSingle]: props.settings?.isSingle
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={!props.settings?.noimage && cover}>
|
<Show when={!props.settings?.noimage && cover}>
|
||||||
<div class={style.shoutCardCoverContainer}>
|
<div class={styles.shoutCardCoverContainer}>
|
||||||
<div class={style.shoutCardCover}>
|
<div class={styles.shoutCardCover}>
|
||||||
<img src={cover || ''} alt={title || ''} loading="lazy" />
|
<img src={cover || ''} alt={title || ''} loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class={style.shoutCardContent}>
|
<div class={styles.shoutCardContent}>
|
||||||
<Show when={layout && layout !== 'article' && !(props.settings?.noicon || props.settings?.noimage)}>
|
<Show when={layout && layout !== 'article' && !(props.settings?.noicon || props.settings?.noimage)}>
|
||||||
<div class={style.shoutCardType}>
|
<div class={styles.shoutCardType}>
|
||||||
<a href={`/topic/${mainTopic.slug}`}>
|
<a href={`/topic/${mainTopic.slug}`}>
|
||||||
<Icon name={layout} class={style.icon} />
|
<Icon name={layout} class={styles.icon} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -113,24 +113,24 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class={style.shoutCardTitlesContainer}>
|
<div class={styles.shoutCardTitlesContainer}>
|
||||||
<a href={`/${slug || ''}`} onClick={handleClientRouteLinkClick}>
|
<a href={`/${slug || ''}`} onClick={handleClientRouteLinkClick}>
|
||||||
<div class={style.shoutCardTitle}>
|
<div class={styles.shoutCardTitle}>
|
||||||
<span class={style.shoutCardLinkContainer}>{title}</span>
|
<span class={styles.shoutCardLinkContainer}>{title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!props.settings?.nosubtitle && subtitle}>
|
<Show when={!props.settings?.nosubtitle && subtitle}>
|
||||||
<div class={style.shoutCardSubtitle}>
|
<div class={styles.shoutCardSubtitle}>
|
||||||
<span class={style.shoutCardLinkContainer}>{subtitle}</span>
|
<span class={styles.shoutCardLinkContainer}>{subtitle}</span>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!props.settings?.noauthor || !props.settings?.nodate}>
|
<Show when={!props.settings?.noauthor || !props.settings?.nodate}>
|
||||||
<div class={style.shoutDetails}>
|
<div class={styles.shoutDetails}>
|
||||||
<Show when={!props.settings?.noauthor}>
|
<Show when={!props.settings?.noauthor}>
|
||||||
<div class={style.shoutAuthor}>
|
<div class={styles.shoutAuthor}>
|
||||||
<For each={authors}>
|
<For each={authors}>
|
||||||
{(author, index) => {
|
{(author, index) => {
|
||||||
const name =
|
const name =
|
||||||
|
@ -150,44 +150,50 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!props.settings?.nodate}>
|
<Show when={!props.settings?.nodate}>
|
||||||
<div class={style.shoutDate}>{formattedDate()}</div>
|
<div class={styles.shoutDate}>{formattedDate()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.settings?.isFeedMode}>
|
<Show when={props.settings?.isFeedMode}>
|
||||||
<section class={style.shoutCardDetails}>
|
<section class={styles.shoutCardDetails}>
|
||||||
<div class={style.shoutCardDetailsContent}>
|
<div class={styles.shoutCardDetailsContent}>
|
||||||
<div class={clsx(style.shoutCardDetailsItem, 'rating')}>
|
<div class={clsx(styles.shoutCardDetailsItem, styles.rating)}>
|
||||||
<button class="rating__control">−</button>
|
<button class={styles.ratingControl}>−</button>
|
||||||
<span class="rating__value">{stat?.rating || ''}</span>
|
<span class={styles.ratingValue}>{stat?.rating || ''}</span>
|
||||||
<button class="rating__control">+</button>
|
<button class={styles.ratingControl}>+</button>
|
||||||
</div>
|
</div>
|
||||||
<div class={clsx(style.shoutCardDetailsItem, style.shoutCardComments)}>
|
<div
|
||||||
<Icon name="eye" class={style.icon} />
|
class={clsx(
|
||||||
|
styles.shoutCardDetailsItem,
|
||||||
|
styles.shoutCardDetailsViewed,
|
||||||
|
styles.shoutCardComments
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon name="eye" class={styles.icon} />
|
||||||
{stat?.viewed}
|
{stat?.viewed}
|
||||||
</div>
|
</div>
|
||||||
<div class={clsx(style.shoutCardDetailsTtem, style.shoutCardComments)}>
|
<div class={clsx(styles.shoutCardDetailsItem, styles.shoutCardComments)}>
|
||||||
<a href={`/${slug + '#comments' || ''}`}>
|
<a href={`/${slug + '#comments' || ''}`}>
|
||||||
<Icon name="comment" class={style.icon} />
|
<Icon name="comment" class={styles.icon} />
|
||||||
{stat?.commented || ''}
|
{stat?.commented || ''}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={style.shoutCardDetailsItem}>
|
<div class={styles.shoutCardDetailsItem}>
|
||||||
<button>
|
<button>
|
||||||
<Icon name="bookmark" class={style.icon} />
|
<Icon name="bookmark" class={styles.icon} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={style.shoutCardDetailsItem}>
|
<div class={styles.shoutCardDetailsItem}>
|
||||||
<button>
|
<button>
|
||||||
<Icon name="ellipsis" class={style.icon} />
|
<Icon name="ellipsis" class={styles.icon} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button--light shout-card__edit-control">{t('Collaborate')}</button>
|
<button class={clsx('button--light', styles.shoutCardEditControl)}>{t('Collaborate')}</button>
|
||||||
</section>
|
</section>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { For, Suspense } from 'solid-js/web'
|
import { For, Suspense } from 'solid-js/web'
|
||||||
import OneWide from './Row1'
|
import { Row1 } from './Row1'
|
||||||
import Row2 from './Row2'
|
import { Row2 } from './Row2'
|
||||||
import Row3 from './Row3'
|
import { Row3 } from './Row3'
|
||||||
import { shuffle } from '../../utils'
|
import { shuffle } from '../../utils'
|
||||||
import { createMemo, createSignal } from 'solid-js'
|
import { createMemo, createSignal } from 'solid-js'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
|
@ -10,7 +10,7 @@ import './List.scss'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
|
|
||||||
export const Block6 = (props: { articles: Shout[] }) => {
|
export const Block6 = (props: { articles: Shout[] }) => {
|
||||||
const dice = createMemo(() => shuffle([OneWide, Row2, Row3]))
|
const dice = createMemo(() => shuffle([Row1, Row2, Row3]))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Show } from 'solid-js'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
|
|
||||||
export default (props: { article: Shout }) => (
|
export const Row1 = (props: { article: Shout }) => (
|
||||||
<Show when={!!props.article}>
|
<Show when={!!props.article}>
|
||||||
<div class="floor floor--one-article">
|
<div class="floor floor--one-article">
|
||||||
<div class="wide-container row">
|
<div class="wide-container row">
|
||||||
|
|
|
@ -2,13 +2,14 @@ import { createComputed, createSignal, Show } from 'solid-js'
|
||||||
import { For } from 'solid-js/web'
|
import { For } from 'solid-js/web'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
|
|
||||||
const x = [
|
const x = [
|
||||||
['6', '6'],
|
['6', '6'],
|
||||||
['4', '8'],
|
['4', '8'],
|
||||||
['8', '4']
|
['8', '4']
|
||||||
]
|
]
|
||||||
|
|
||||||
export default (props: { articles: Shout[] }) => {
|
export const Row2 = (props: { articles: Shout[] }) => {
|
||||||
const [y, setY] = createSignal(0)
|
const [y, setY] = createSignal(0)
|
||||||
|
|
||||||
createComputed(() => setY(Math.floor(Math.random() * x.length)))
|
createComputed(() => setY(Math.floor(Math.random() * x.length)))
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { For } from 'solid-js/web'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
|
|
||||||
export default (props: { articles: Shout[]; header?: JSX.Element }) => {
|
export const Row3 = (props: { articles: Shout[]; header?: JSX.Element }) => {
|
||||||
return (
|
return (
|
||||||
<div class="floor">
|
<div class="floor">
|
||||||
<div class="wide-container row">
|
<div class="wide-container row">
|
||||||
|
|
|
@ -85,12 +85,12 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
|
||||||
</For>
|
</For>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p class="settings">
|
<div class="settings">
|
||||||
<a href="/feed/settings">
|
<a href="/feed/settings">
|
||||||
<strong>{t('Feed settings')}</strong>
|
<strong>{t('Feed settings')}</strong>
|
||||||
</a>
|
|
||||||
<Icon name="settings" />
|
<Icon name="settings" />
|
||||||
</p>
|
</a>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,3 +160,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #141414;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 24px;
|
||||||
|
margin-bottom: 52px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import styles from './EmailConfirm.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
import authModalStyles from './AuthModal.module.scss'
|
|
||||||
import { clsx } from 'clsx'
|
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 { onMount } from 'solid-js'
|
import { createMemo, onMount, Show } from 'solid-js'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import { confirmEmail } from '../../../stores/auth'
|
import { confirmEmail, useAuthStore } from '../../../stores/auth'
|
||||||
|
|
||||||
type ConfirmEmailSearchParams = {
|
type ConfirmEmailSearchParams = {
|
||||||
token: string
|
token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmailConfirm = () => {
|
export const EmailConfirm = () => {
|
||||||
const confirmedEmail = 'test@test.com'
|
const { session } = useAuthStore()
|
||||||
|
|
||||||
|
const confirmedEmail = createMemo(() => session()?.user?.email || '')
|
||||||
|
|
||||||
const { searchParams } = useRouter<ConfirmEmailSearchParams>()
|
const { searchParams } = useRouter<ConfirmEmailSearchParams>()
|
||||||
|
|
||||||
|
@ -28,12 +29,14 @@ export const EmailConfirm = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class={styles.title}>{t('Hooray! Welcome!')}</div>
|
<div class={styles.title}>{t('Hooray! Welcome!')}</div>
|
||||||
|
<Show when={Boolean(confirmedEmail())}>
|
||||||
<div class={styles.text}>
|
<div class={styles.text}>
|
||||||
{t("You've confirmed email")} {confirmedEmail}
|
{t("You've confirmed email")} {confirmedEmail()}
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
<div>
|
<div>
|
||||||
<button class={clsx('button', authModalStyles.submitButton)} onClick={() => hideModal()}>
|
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
|
||||||
Перейти на главную
|
{t('Go to main page')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,8 +7,6 @@ import { useRouter } from '../../../stores/router'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
import { isValidEmail } from './validators'
|
import { isValidEmail } from './validators'
|
||||||
import { checkEmail, register } from '../../../stores/auth'
|
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
|
||||||
|
|
||||||
type FormFields = {
|
type FormFields = {
|
||||||
email: string
|
email: string
|
||||||
|
@ -61,9 +59,9 @@ export const ForgotPasswordForm = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form>
|
<form onSubmit={handleSubmit}>
|
||||||
<h4>{t('Forgot password?')}</h4>
|
<h4>{t('Forgot password?')}</h4>
|
||||||
{t('Everything is ok, please give us your email address')}
|
<div class={styles.authSubtitle}>{t('Everything is ok, please give us your email address')}</div>
|
||||||
<Show when={submitError()}>
|
<Show when={submitError()}>
|
||||||
<div class={styles.authInfo}>
|
<div class={styles.authInfo}>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
@ -3,13 +3,14 @@ import { t } from '../../../utils/intl'
|
||||||
import styles from './AuthModal.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { SocialProviders } from './SocialProviders'
|
import { SocialProviders } from './SocialProviders'
|
||||||
import { signIn } from '../../../stores/auth'
|
import { signIn, signSendLink } from '../../../stores/auth'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
import { isValidEmail } from './validators'
|
import { isValidEmail } from './validators'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
import { hideModal } from '../../../stores/ui'
|
||||||
|
|
||||||
type FormFields = {
|
type FormFields = {
|
||||||
email: string
|
email: string
|
||||||
|
@ -22,6 +23,9 @@ export const LoginForm = () => {
|
||||||
const [submitError, setSubmitError] = createSignal('')
|
const [submitError, setSubmitError] = createSignal('')
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
// TODO: better solution for interactive error messages
|
||||||
|
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
|
||||||
|
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
||||||
|
|
||||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
|
@ -37,9 +41,18 @@ export const LoginForm = () => {
|
||||||
setPassword(newPassword)
|
setPassword(newPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSendLinkAgainClick = (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setIsEmailNotConfirmed(false)
|
||||||
|
setSubmitError('')
|
||||||
|
setIsLinkSent(true)
|
||||||
|
signSendLink({ email: email() })
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
setIsLinkSent(false)
|
||||||
setSubmitError('')
|
setSubmitError('')
|
||||||
|
|
||||||
const newValidationErrors: ValidationErrors = {}
|
const newValidationErrors: ValidationErrors = {}
|
||||||
|
@ -63,10 +76,12 @@ export const LoginForm = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await signIn({ email: email(), password: password() })
|
await signIn({ email: email(), password: password() })
|
||||||
|
hideModal()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ApiError) {
|
if (error instanceof ApiError) {
|
||||||
if (error.code === 'email_not_confirmed') {
|
if (error.code === 'email_not_confirmed') {
|
||||||
setSubmitError(t('Please, confirm email'))
|
setSubmitError(t('Please, confirm email'))
|
||||||
|
setIsEmailNotConfirmed(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,11 +102,17 @@ export const LoginForm = () => {
|
||||||
<h4>{t('Enter the Discours')}</h4>
|
<h4>{t('Enter the Discours')}</h4>
|
||||||
<Show when={submitError()}>
|
<Show when={submitError()}>
|
||||||
<div class={styles.authInfo}>
|
<div class={styles.authInfo}>
|
||||||
<ul>
|
<div class={styles.warn}>{submitError()}</div>
|
||||||
<li class={styles.warn}>{submitError()}</li>
|
<Show when={isEmailNotConfirmed()}>
|
||||||
</ul>
|
<a href="#" class={styles.sendLink} onClick={handleSendLinkAgainClick}>
|
||||||
|
{t('Send link again')}
|
||||||
|
</a>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={isLinkSent()}>
|
||||||
|
<div class={styles.authInfo}>{t('Link sent, check your email')}</div>
|
||||||
|
</Show>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { ApiError } from '../../../utils/apiClient'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
import { hideModal } from '../../../stores/ui'
|
||||||
|
|
||||||
type FormFields = {
|
type FormFields = {
|
||||||
name: string
|
name: string
|
||||||
|
@ -29,6 +30,7 @@ export const RegisterForm = () => {
|
||||||
const [name, setName] = createSignal('')
|
const [name, setName] = createSignal('')
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
|
const [isSuccess, setIsSuccess] = createSignal(false)
|
||||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
|
||||||
const handleEmailInput = (newEmail: string) => {
|
const handleEmailInput = (newEmail: string) => {
|
||||||
|
@ -91,6 +93,8 @@ export const RegisterForm = () => {
|
||||||
email: email(),
|
email: email(),
|
||||||
password: password()
|
password: password()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setIsSuccess(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ApiError && error.code === 'user_already_exists') {
|
if (error instanceof ApiError && error.code === 'user_already_exists') {
|
||||||
return
|
return
|
||||||
|
@ -103,6 +107,8 @@ export const RegisterForm = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Show when={!isSuccess()}>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<h4>{t('Create account')}</h4>
|
<h4>{t('Create account')}</h4>
|
||||||
<Show when={submitError()}>
|
<Show when={submitError()}>
|
||||||
|
@ -185,5 +191,16 @@ export const RegisterForm = () => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</Show>
|
||||||
|
<Show when={isSuccess()}>
|
||||||
|
<div class={styles.title}>{t('Almost done! Check your email.')}</div>
|
||||||
|
<div class={styles.text}>{t("We've sent you a message with a link to enter our website.")}</div>
|
||||||
|
<div>
|
||||||
|
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
|
||||||
|
{t('Back to main page')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Show } from 'solid-js/web'
|
import { Dynamic } from 'solid-js/web'
|
||||||
import { createEffect, createMemo, onMount } 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 { handleClientRouteLinkClick, useRouter } from '../../../stores/router'
|
||||||
|
@ -11,12 +11,11 @@ import { ForgotPasswordForm } from './ForgotPasswordForm'
|
||||||
import { EmailConfirm } from './EmailConfirm'
|
import { EmailConfirm } from './EmailConfirm'
|
||||||
import type { AuthModalMode, AuthModalSearchParams } from './types'
|
import type { AuthModalMode, AuthModalSearchParams } from './types'
|
||||||
|
|
||||||
const AUTH_MODAL_MODES: Record<AuthModalMode, AuthModalMode> = {
|
const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = {
|
||||||
login: 'login',
|
login: LoginForm,
|
||||||
register: 'register',
|
register: RegisterForm,
|
||||||
'forgot-password': 'forgot-password',
|
'forgot-password': ForgotPasswordForm,
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
'confirm-email': EmailConfirm
|
||||||
'confirm-email': 'confirm-email'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthModal = () => {
|
export const AuthModal = () => {
|
||||||
|
@ -25,7 +24,7 @@ export const AuthModal = () => {
|
||||||
const { searchParams } = useRouter<AuthModalSearchParams>()
|
const { searchParams } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
const mode = createMemo<AuthModalMode>(() => {
|
const mode = createMemo<AuthModalMode>(() => {
|
||||||
return AUTH_MODAL_MODES[searchParams().mode] || 'login'
|
return AUTH_MODAL_MODES[searchParams().mode] ? searchParams().mode : 'login'
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect((oldMode) => {
|
createEffect((oldMode) => {
|
||||||
|
@ -70,18 +69,7 @@ export const AuthModal = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={clsx('col-sm-6', styles.auth)}>
|
<div class={clsx('col-sm-6', styles.auth)}>
|
||||||
<Show when={mode() === 'login'}>
|
<Dynamic component={AUTH_MODAL_MODES[mode()]} />
|
||||||
<LoginForm />
|
|
||||||
</Show>
|
|
||||||
<Show when={mode() === 'register'}>
|
|
||||||
<RegisterForm />
|
|
||||||
</Show>
|
|
||||||
<Show when={mode() === 'forgot-password'}>
|
|
||||||
<ForgotPasswordForm />
|
|
||||||
</Show>
|
|
||||||
<Show when={mode() === 'confirm-email'}>
|
|
||||||
<EmailConfirm />
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,18 +35,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupShare {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.headerScrolledTop & {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s, z-index 0s 0.3s;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerFixed {
|
.headerFixed {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -327,18 +315,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.userControl {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.headerWithTitle.headerScrolledBottom & {
|
|
||||||
transition: opacity 0.3s, z-index 0s 0.3s;
|
|
||||||
opacity: 0;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.articleControls {
|
.articleControls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
@ -348,19 +324,15 @@
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
.control {
|
||||||
|
cursor: pointer;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-left: 1.6rem;
|
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: none;
|
background: none;
|
||||||
|
|
||||||
|
@ -370,4 +342,138 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control + .control {
|
||||||
|
margin-left: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userControl {
|
||||||
|
align-items: baseline;
|
||||||
|
display: flex;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.headerWithTitle.headerScrolledBottom & {
|
||||||
|
transition: opacity 0.3s, z-index 0s 0.3s;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include font-size(1.7rem);
|
||||||
|
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
padding: divide($container-padding-x, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userpic {
|
||||||
|
margin-right: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userControlItem {
|
||||||
|
align-items: center;
|
||||||
|
border: 2px solid #f6f6f6;
|
||||||
|
border-radius: 100%;
|
||||||
|
display: flex;
|
||||||
|
height: 2.4em;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: divide($container-padding-x, 2);
|
||||||
|
position: relative;
|
||||||
|
width: 2.4em;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
margin-left: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circlewrap {
|
||||||
|
height: 23px;
|
||||||
|
min-width: 23px;
|
||||||
|
width: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
a {
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
filter: invert(0);
|
||||||
|
transition: filter 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 100%;
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userControlItemInbox,
|
||||||
|
.userControlItemSearch {
|
||||||
|
@include media-breakpoint-down(sm) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userControlItemWritePost {
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
.icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLabel {
|
||||||
|
display: inline;
|
||||||
|
padding: 0 1.2rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&,
|
||||||
|
a::before {
|
||||||
|
border-radius: 1.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
import { For, Show, createSignal, createMemo, createEffect, onMount, onCleanup } from 'solid-js'
|
import { For, Show, createSignal, createMemo, createEffect, onMount, onCleanup } from 'solid-js'
|
||||||
import Private from './Private'
|
|
||||||
import Notifications from './Notifications'
|
import Notifications from './Notifications'
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
import { Modal } from './Modal'
|
import { Modal } from './Modal'
|
||||||
import { Popup } from './Popup'
|
|
||||||
import { AuthModal } from './AuthModal'
|
import { AuthModal } from './AuthModal'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
|
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
||||||
import styles from './Header.module.scss'
|
import styles from './Header.module.scss'
|
||||||
import stylesPopup from './Popup.module.scss'
|
|
||||||
import privateStyles from './Private.module.scss'
|
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
|
import { getLogger } from '../../utils/logger'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { SharePopup } from '../Article/SharePopup'
|
||||||
|
import { ProfilePopup } from './ProfilePopup'
|
||||||
|
import Userpic from '../Author/Userpic'
|
||||||
|
import type { Author } from '../../graphql/types.gen'
|
||||||
|
|
||||||
|
const log = getLogger('header')
|
||||||
|
|
||||||
const resources: { name: string; route: keyof Routes }[] = [
|
const resources: { name: string; route: keyof Routes }[] = [
|
||||||
{ name: t('zine'), route: 'home' },
|
{ name: t('zine'), route: 'home' },
|
||||||
|
@ -32,6 +35,9 @@ export const Header = (props: Props) => {
|
||||||
const [getIsScrolled, setIsScrolled] = createSignal(false)
|
const [getIsScrolled, setIsScrolled] = createSignal(false)
|
||||||
const [fixed, setFixed] = createSignal(false)
|
const [fixed, setFixed] = createSignal(false)
|
||||||
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
||||||
|
const [isSharePopupVisible, setIsSharePopupVisible] = createSignal(false)
|
||||||
|
const [isProfilePopupVisible, setIsProfilePopupVisible] = createSignal(false)
|
||||||
|
|
||||||
// stores
|
// stores
|
||||||
const { warnings } = useWarningsStore()
|
const { warnings } = useWarningsStore()
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuthStore()
|
||||||
|
@ -41,13 +47,11 @@ export const Header = (props: Props) => {
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
||||||
const toggleFixed = () => setFixed(!fixed())
|
const toggleFixed = () => setFixed((oldFixed) => !oldFixed)
|
||||||
// effects
|
// effects
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const isFixed = fixed() || (modal() && modal() !== 'share')
|
document.body.classList.toggle('fixed', fixed() || modal() !== null)
|
||||||
|
document.body.classList.toggle(styles.fixed, fixed() && !modal())
|
||||||
document.body.classList.toggle('fixed', isFixed)
|
|
||||||
document.body.classList.toggle(styles.fixed, isFixed && !modal())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// derived
|
// derived
|
||||||
|
@ -85,7 +89,8 @@ export const Header = (props: Props) => {
|
||||||
classList={{
|
classList={{
|
||||||
[styles.headerFixed]: props.isHeaderFixed,
|
[styles.headerFixed]: props.isHeaderFixed,
|
||||||
[styles.headerScrolledTop]: !getIsScrollingBottom() && getIsScrolled(),
|
[styles.headerScrolledTop]: !getIsScrollingBottom() && getIsScrolled(),
|
||||||
[styles.headerScrolledBottom]: getIsScrollingBottom() && getIsScrolled(),
|
[styles.headerScrolledBottom]:
|
||||||
|
(getIsScrollingBottom() && getIsScrolled() && !isProfilePopupVisible()) || isSharePopupVisible(),
|
||||||
[styles.headerWithTitle]: Boolean(props.title)
|
[styles.headerWithTitle]: Boolean(props.title)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -94,41 +99,6 @@ export const Header = (props: Props) => {
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div class={clsx(styles.mainHeaderInner, 'wide-container')}>
|
<div class={clsx(styles.mainHeaderInner, 'wide-container')}>
|
||||||
<Popup name="share" class={clsx(styles.popupShare, stylesPopup.popupShare)}>
|
|
||||||
<ul class="nodash">
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<Icon name="vk-white" class={stylesPopup.icon} />
|
|
||||||
VK
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<Icon name="facebook-white" class={stylesPopup.icon} />
|
|
||||||
Facebook
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<Icon name="twitter-white" class={stylesPopup.icon} />
|
|
||||||
Twitter
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<Icon name="telegram-white" class={stylesPopup.icon} />
|
|
||||||
Telegram
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<Icon name="link-white" class={stylesPopup.icon} />
|
|
||||||
{t('Copy link')}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Popup>
|
|
||||||
|
|
||||||
<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')} onClick={handleClientRouteLinkClick}>
|
||||||
|
@ -162,8 +132,15 @@ export const Header = (props: Props) => {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.usernav}>
|
<div class={styles.usernav}>
|
||||||
<div class={clsx(privateStyles.userControl, styles.userControl, 'col')}>
|
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
||||||
<div class={privateStyles.userControlItem}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemWritePost)}>
|
||||||
|
<a href="/create">
|
||||||
|
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||||
|
<Icon name="pencil" class={styles.icon} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class={styles.userControlItem}>
|
||||||
<a href="#" onClick={handleBellIconClick}>
|
<a href="#" onClick={handleBellIconClick}>
|
||||||
<div>
|
<div>
|
||||||
<Icon name="bell-white" counter={authorized() ? warnings().length : 1} />
|
<Icon name="bell-white" counter={authorized() ? warnings().length : 1} />
|
||||||
|
@ -172,7 +149,7 @@ export const Header = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={visibleWarnings()}>
|
<Show when={visibleWarnings()}>
|
||||||
<div class={clsx(privateStyles.userControlItem, 'notifications')}>
|
<div class={clsx(styles.userControlItem, 'notifications')}>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -180,31 +157,56 @@ export const Header = (props: Props) => {
|
||||||
<Show
|
<Show
|
||||||
when={authorized()}
|
when={authorized()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={clsx(privateStyles.userControlItem, 'loginbtn')}>
|
<div class={clsx(styles.userControlItem, 'loginbtn')}>
|
||||||
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
||||||
<Icon name="user-anonymous" />
|
<Icon name="user-anonymous" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Private />
|
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
||||||
|
<a href="/inbox">
|
||||||
|
{/*FIXME: replace with route*/}
|
||||||
|
<div classList={{ entered: page().path === '/inbox' }}>
|
||||||
|
<Icon name="inbox-white" counter={session()?.news?.unread || 0} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ProfilePopup
|
||||||
|
onVisibilityChange={(isVisible) => {
|
||||||
|
setIsProfilePopupVisible(isVisible)
|
||||||
|
}}
|
||||||
|
containerCssClass={styles.control}
|
||||||
|
trigger={
|
||||||
|
<div class={styles.userControlItem}>
|
||||||
|
<button class={styles.button}>
|
||||||
|
<div classList={{ entered: page().path === `/${session().user?.slug}` }}>
|
||||||
|
<Userpic user={session().user as Author} class={styles.userpic} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.title}>
|
<Show when={props.title}>
|
||||||
<div class={styles.articleControls}>
|
<div class={styles.articleControls}>
|
||||||
<button
|
<SharePopup
|
||||||
onClick={() => {
|
onVisibilityChange={(isVisible) => {
|
||||||
// FIXME: Popup
|
setIsSharePopupVisible(isVisible)
|
||||||
showModal('share')
|
|
||||||
}}
|
}}
|
||||||
>
|
containerCssClass={styles.control}
|
||||||
<Icon name="share-outline" class={styles.icon} />
|
trigger={<Icon name="share-outline" class={styles.icon} />}
|
||||||
</button>
|
/>
|
||||||
<a href="#comments">
|
<a href="#comments" class={styles.control}>
|
||||||
<Icon name="comments-outline" class={styles.icon} />
|
<Icon name="comments-outline" class={styles.icon} />
|
||||||
</a>
|
</a>
|
||||||
|
<a href="#" class={styles.control} onClick={(event) => event.preventDefault()}>
|
||||||
<Icon name="pencil-outline" class={styles.icon} />
|
<Icon name="pencil-outline" class={styles.icon} />
|
||||||
|
</a>
|
||||||
|
<a href="#" class={styles.control} onClick={(event) => event.preventDefault()}>
|
||||||
<Icon name="bookmark" class={styles.icon} />
|
<Icon name="bookmark" class={styles.icon} />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
|
import { getLogger } from '../../utils/logger'
|
||||||
import './Modal.scss'
|
import './Modal.scss'
|
||||||
import { hideModal, useModalStore } from '../../stores/ui'
|
import { hideModal, useModalStore } from '../../stores/ui'
|
||||||
|
|
||||||
|
const log = getLogger('modal')
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
name: string
|
name: string
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
|
@ -31,7 +34,7 @@ export const Modal = (props: ModalProps) => {
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setVisible(modal() === props.name)
|
setVisible(modal() === props.name)
|
||||||
console.debug(`[auth.modal] ${props.name} is ${modal() === props.name ? 'visible' : 'hidden'}`)
|
log.debug(`${props.name} is ${modal() === props.name ? 'visible' : 'hidden'}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,10 +1,25 @@
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.popup {
|
.popup {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 2px solid #000;
|
border: 2px solid #000;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
&.horizontalAnchorCenter {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.horizontalAnchorRight {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@include font-size(1.6rem);
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
padding: 2.4rem 2.4rem 2.4rem 1.6rem;
|
padding: 2.4rem;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
|
@ -14,7 +29,6 @@
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin-bottom: 1.6rem;
|
margin-bottom: 1.6rem;
|
||||||
padding-left: 3.6rem;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
|
@ -24,23 +38,36 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
border: none;
|
border: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
img {
|
||||||
|
filter: invert(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
max-height: 2rem;
|
max-height: 2rem;
|
||||||
max-width: 2rem;
|
max-width: 2rem;
|
||||||
|
transition: filter 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
left: 1.5rem;
|
display: inline-block;
|
||||||
position: absolute;
|
width: 3.6rem;
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupShare {
|
// TODO: animation
|
||||||
right: 1em;
|
// .popup {
|
||||||
top: 4.5rem;
|
// opacity: 1;
|
||||||
}
|
// transition: opacity 0.3s;
|
||||||
|
// z-index: 1;
|
||||||
|
// &.visible {
|
||||||
|
// opacity: 0;
|
||||||
|
// transition: opacity 0.3s, z-index 0s 0.3s;
|
||||||
|
// z-index: -1;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
@ -1,31 +1,61 @@
|
||||||
import { createEffect, createSignal, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, JSX, onCleanup, onMount, Show } from 'solid-js'
|
||||||
import style from './Popup.module.scss'
|
import styles from './Popup.module.scss'
|
||||||
import { hideModal, useModalStore } from '../../stores/ui'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
interface PopupProps {
|
type HorizontalAnchor = 'center' | 'right'
|
||||||
name: string
|
|
||||||
children: any
|
export type PopupProps = {
|
||||||
class?: string
|
containerCssClass?: string
|
||||||
|
trigger: JSX.Element
|
||||||
|
children: JSX.Element
|
||||||
|
onVisibilityChange?: (isVisible) => void
|
||||||
|
horizontalAnchor?: HorizontalAnchor
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Popup = (props: PopupProps) => {
|
export const Popup = (props: PopupProps) => {
|
||||||
const { modal } = useModalStore()
|
const [isVisible, setIsVisible] = createSignal(false)
|
||||||
|
const horizontalAnchor: HorizontalAnchor = props.horizontalAnchor || 'center'
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.onVisibilityChange) {
|
||||||
|
props.onVisibilityChange(isVisible())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let container: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent & { target: Element }) => {
|
||||||
|
if (!isVisible()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target === container || container?.contains(event.target)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('keydown', (e: KeyboardEvent) => {
|
document.addEventListener('click', handleClickOutside, { capture: true })
|
||||||
if (e.key === 'Escape') hideModal()
|
onCleanup(() => document.removeEventListener('click', handleClickOutside, { capture: true }))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const [visible, setVisible] = createSignal(false)
|
const toggle = () => setIsVisible((oldVisible) => !oldVisible)
|
||||||
createEffect(() => {
|
|
||||||
setVisible(modal() === props.name)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={visible()}>
|
<span class={clsx(styles.container, props.containerCssClass)} ref={container}>
|
||||||
<div class={clsx(style.popup, props.class)}>{props.children}</div>
|
<span onClick={toggle}>{props.trigger}</span>
|
||||||
|
<Show when={isVisible()}>
|
||||||
|
<div
|
||||||
|
class={clsx(styles.popup, {
|
||||||
|
[styles.horizontalAnchorCenter]: horizontalAnchor === 'center',
|
||||||
|
[styles.horizontalAnchorRight]: horizontalAnchor === 'right'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
.userControl {
|
|
||||||
align-items: baseline;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
@include font-size(1.7rem);
|
|
||||||
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
@include media-breakpoint-down(md) {
|
|
||||||
padding: divide($container-padding-x, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.circlewrap {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.userControlItem {
|
|
||||||
align-items: center;
|
|
||||||
border: 2px solid #f6f6f6;
|
|
||||||
border-radius: 100%;
|
|
||||||
display: flex;
|
|
||||||
height: 2.4em;
|
|
||||||
justify-content: center;
|
|
||||||
margin-left: divide($container-padding-x, 2);
|
|
||||||
position: relative;
|
|
||||||
width: 2.4em;
|
|
||||||
|
|
||||||
@include media-breakpoint-up(sm) {
|
|
||||||
margin-left: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circlewrap {
|
|
||||||
height: 23px;
|
|
||||||
min-width: 23px;
|
|
||||||
width: 23px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: none;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
filter: invert(0);
|
|
||||||
transition: filter 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 100%;
|
|
||||||
content: '';
|
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 20px;
|
|
||||||
vertical-align: middle;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.userControlItemWritePost {
|
|
||||||
@include media-breakpoint-up(lg) {
|
|
||||||
.icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textLabel {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.userControlItemInbox,
|
|
||||||
.userControlItemSearch {
|
|
||||||
@include media-breakpoint-down(sm) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
import type { Author } from '../../graphql/types.gen'
|
|
||||||
import Userpic from '../Author/Userpic'
|
|
||||||
import { Icon } from './Icon'
|
|
||||||
import styles from './Private.module.scss'
|
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import { useRouter } from '../../stores/router'
|
|
||||||
import { clsx } from 'clsx'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const { session } = useAuthStore()
|
|
||||||
const { page } = useRouter()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={clsx(styles.userControl, 'col')}>
|
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemWritePost)}>
|
|
||||||
<a href="/create">
|
|
||||||
<span class={styles.textLabel}>опубликовать материал</span>
|
|
||||||
<Icon name="pencil" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
|
||||||
<a href="/inbox">
|
|
||||||
{/*FIXME: replace with route*/}
|
|
||||||
<div classList={{ entered: page().path === '/inbox' }}>
|
|
||||||
<Icon name="inbox-white" counter={session()?.news?.unread || 0} />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class={styles.userControlItem}>
|
|
||||||
<a href={`/${session().user?.slug}`}>
|
|
||||||
{/*FIXME: replace with route*/}
|
|
||||||
<div classList={{ entered: page().path === `/${session().user?.slug}` }}>
|
|
||||||
<Userpic user={session().user as Author} />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
44
src/components/Nav/ProfilePopup.tsx
Normal file
44
src/components/Nav/ProfilePopup.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Popup, PopupProps } from './Popup'
|
||||||
|
import { signOut, useAuthStore } from '../../stores/auth'
|
||||||
|
|
||||||
|
type ProfilePopupProps = Omit<PopupProps, 'children'>
|
||||||
|
|
||||||
|
export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
|
const { session } = useAuthStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popup {...props} horizontalAnchor="right">
|
||||||
|
<ul class="nodash">
|
||||||
|
<li>
|
||||||
|
<a href={`/${session().user?.slug}`}>Профиль</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">Черновики</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">Подписки</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">Комментарии</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">Закладки</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">Настройки</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
signOut()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выйти из аккаунта
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popup>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { MainLayout } from '../Layouts/MainLayout'
|
import { MainLayout } from '../Layouts/MainLayout'
|
||||||
import { AuthorView } 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 { loadArticlesForAuthors, resetSortedArticles } from '../../stores/zine/articles'
|
import { loadAuthorArticles, 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 loadArticlesForAuthors({ authorSlugs: [slug()] })
|
await loadAuthorArticles({ authorSlug: slug(), limit: PRERENDERED_ARTICLES_COUNT })
|
||||||
await loadAuthor({ slug: slug() })
|
await loadAuthor({ slug: slug() })
|
||||||
|
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
|
|
|
@ -1,30 +1,14 @@
|
||||||
import { MainLayout } from '../Layouts/MainLayout'
|
import { MainLayout } from '../Layouts/MainLayout'
|
||||||
import { FeedView } from '../Views/Feed'
|
import { FeedView } from '../Views/Feed'
|
||||||
import type { PageProps } from '../types'
|
import { onCleanup } from 'solid-js'
|
||||||
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { resetSortedArticles } from '../../stores/zine/articles'
|
||||||
import { loadRecentArticles, resetSortedArticles } from '../../stores/zine/articles'
|
|
||||||
import { Loading } from '../Loading'
|
|
||||||
|
|
||||||
export const FeedPage = (props: PageProps) => {
|
|
||||||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.feedArticles))
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (isLoaded()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadRecentArticles({ limit: 50, offset: 0 })
|
|
||||||
|
|
||||||
setIsLoaded(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
export const FeedPage = () => {
|
||||||
onCleanup(() => resetSortedArticles())
|
onCleanup(() => resetSortedArticles())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<Show when={isLoaded()} fallback={<Loading />}>
|
<FeedView />
|
||||||
<FeedView articles={props.feedArticles} />
|
|
||||||
</Show>
|
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { HomeView } from '../Views/Home'
|
import { HomeView, PRERENDERED_ARTICLES_COUNT } from '../Views/Home'
|
||||||
import { MainLayout } from '../Layouts/MainLayout'
|
import { MainLayout } from '../Layouts/MainLayout'
|
||||||
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'
|
||||||
|
@ -14,7 +14,7 @@ export const HomePage = (props: PageProps) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadPublishedArticles({ limit: 5, offset: 0 })
|
await loadPublishedArticles({ limit: PRERENDERED_ARTICLES_COUNT, offset: 0 })
|
||||||
await loadRandomTopics()
|
await loadRandomTopics()
|
||||||
|
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { MainLayout } from '../Layouts/MainLayout'
|
import { MainLayout } from '../Layouts/MainLayout'
|
||||||
import { 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 { loadArticlesForTopics, resetSortedArticles } from '../../stores/zine/articles'
|
import { loadTopicArticles, 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 loadArticlesForTopics({ topicSlugs: [slug()] })
|
await loadTopicArticles({ topicSlug: slug(), limit: PRERENDERED_ARTICLES_COUNT, offset: 0 })
|
||||||
await loadTopic({ slug: slug() })
|
await loadTopic({ slug: slug() })
|
||||||
|
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
// FIXME: breaks on vercel, research
|
// FIXME: breaks on vercel, research
|
||||||
// import 'solid-devtools'
|
// import 'solid-devtools'
|
||||||
|
|
||||||
import { hideModal, MODALS, setLocale, showModal } from '../stores/ui'
|
import { MODALS, setLocale, showModal } from '../stores/ui'
|
||||||
import { Component, createEffect, createMemo } from 'solid-js'
|
import { Component, createEffect, createMemo, onMount } from 'solid-js'
|
||||||
import { Routes, useRouter } from '../stores/router'
|
import { Routes, useRouter } from '../stores/router'
|
||||||
import { Dynamic, isServer } from 'solid-js/web'
|
import { Dynamic, isServer } from 'solid-js/web'
|
||||||
|
import { getLogger } from '../utils/logger'
|
||||||
|
|
||||||
import type { PageProps } from './types'
|
import type { PageProps } from './types'
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ import { ProjectsPage } from './Pages/about/ProjectsPage'
|
||||||
import { TermsOfUsePage } from './Pages/about/TermsOfUsePage'
|
import { TermsOfUsePage } from './Pages/about/TermsOfUsePage'
|
||||||
import { ThanksPage } from './Pages/about/ThanksPage'
|
import { ThanksPage } from './Pages/about/ThanksPage'
|
||||||
import { CreatePage } from './Pages/CreatePage'
|
import { CreatePage } from './Pages/CreatePage'
|
||||||
|
import { renewSession } from '../stores/auth'
|
||||||
|
|
||||||
// TODO: lazy load
|
// TODO: lazy load
|
||||||
// const HomePage = lazy(() => import('./Pages/HomePage'))
|
// const HomePage = lazy(() => import('./Pages/HomePage'))
|
||||||
|
@ -47,6 +49,8 @@ import { CreatePage } from './Pages/CreatePage'
|
||||||
// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
|
// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
|
||||||
// const CreatePage = lazy(() => import('./Pages/about/CreatePage'))
|
// const CreatePage = lazy(() => import('./Pages/about/CreatePage'))
|
||||||
|
|
||||||
|
const log = getLogger('root')
|
||||||
|
|
||||||
type RootSearchParams = {
|
type RootSearchParams = {
|
||||||
modal: string
|
modal: string
|
||||||
lang: string
|
lang: string
|
||||||
|
@ -82,6 +86,10 @@ export const Root = (props: PageProps) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
renewSession()
|
||||||
|
})
|
||||||
|
|
||||||
const pageComponent = createMemo(() => {
|
const pageComponent = createMemo(() => {
|
||||||
const result = pagesMap[page().route]
|
const result = pagesMap[page().route]
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,10 @@ import { t } from '../../utils/intl'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
import { follow, unfollow } from '../../stores/zine/common'
|
||||||
|
import { getLogger } from '../../utils/logger'
|
||||||
|
|
||||||
|
const log = getLogger('TopicCard')
|
||||||
|
|
||||||
interface TopicProps {
|
interface TopicProps {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import { Show, createMemo } from 'solid-js'
|
import { Show, createMemo, createSignal, For, onMount } from 'solid-js'
|
||||||
import type { Author, Shout } from '../../graphql/types.gen'
|
import type { Author, Shout } from '../../graphql/types.gen'
|
||||||
import Row2 from '../Feed/Row2'
|
import { Row2 } from '../Feed/Row2'
|
||||||
import Row3 from '../Feed/Row3'
|
import { Row3 } from '../Feed/Row3'
|
||||||
// import Beside from '../Feed/Beside'
|
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 { useArticlesStore } from '../../stores/zine/articles'
|
import { loadAuthorArticles, useArticlesStore } from '../../stores/zine/articles'
|
||||||
|
|
||||||
import '../../styles/Topic.scss'
|
import '../../styles/Topic.scss'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
import { useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import Beside from '../Feed/Beside'
|
import { Beside } from '../Feed/Beside'
|
||||||
|
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||||
|
import { splitToPages } from '../../utils/splitToPages'
|
||||||
|
|
||||||
// TODO: load reactions on client
|
// TODO: load reactions on client
|
||||||
type AuthorProps = {
|
type AuthorProps = {
|
||||||
|
@ -26,16 +27,37 @@ type AuthorPageSearchParams = {
|
||||||
by: '' | 'viewed' | 'rating' | 'commented' | 'recent'
|
by: '' | 'viewed' | 'rating' | 'commented' | 'recent'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PRERENDERED_ARTICLES_COUNT = 12
|
||||||
|
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
|
||||||
|
|
||||||
export const AuthorView = (props: AuthorProps) => {
|
export const AuthorView = (props: AuthorProps) => {
|
||||||
const { sortedArticles } = useArticlesStore({
|
const { sortedArticles } = useArticlesStore({
|
||||||
sortedArticles: props.authorArticles
|
sortedArticles: props.authorArticles
|
||||||
})
|
})
|
||||||
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
||||||
const { topicsByAuthor } = useTopicsStore()
|
const { topicsByAuthor } = useTopicsStore()
|
||||||
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
const author = createMemo(() => authorEntities()[props.authorSlug])
|
const author = createMemo(() => authorEntities()[props.authorSlug])
|
||||||
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
||||||
|
|
||||||
|
const loadMore = async () => {
|
||||||
|
saveScrollPosition()
|
||||||
|
const { hasMore } = await loadAuthorArticles({
|
||||||
|
authorSlug: author().slug,
|
||||||
|
limit: LOAD_MORE_PAGE_SIZE,
|
||||||
|
offset: sortedArticles().length
|
||||||
|
})
|
||||||
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
|
restoreScrollPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
const m = searchParams().by
|
const m = searchParams().by
|
||||||
if (m === 'viewed') return t('Top viewed')
|
if (m === 'viewed') return t('Top viewed')
|
||||||
|
@ -44,6 +66,10 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
return t('Top recent')
|
return t('Top recent')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
|
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="container author-page">
|
<div class="container author-page">
|
||||||
<Show when={author()} fallback={<div class="center">{t('Loading')}</div>}>
|
<Show when={author()} fallback={<div class="center">{t('Loading')}</div>}>
|
||||||
|
@ -83,8 +109,8 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="col-12">{title()}</h3>
|
<h3 class="col-12">{title()}</h3>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<Show when={sortedArticles().length > 0}>
|
|
||||||
<Beside
|
<Beside
|
||||||
title={t('Topics which supported by author')}
|
title={t('Topics which supported by author')}
|
||||||
values={topicsByAuthor()[author().slug].slice(0, 5)}
|
values={topicsByAuthor()[author().slug].slice(0, 5)}
|
||||||
|
@ -96,18 +122,26 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
iconButton={true}
|
iconButton={true}
|
||||||
/>
|
/>
|
||||||
<Row3 articles={sortedArticles().slice(1, 4)} />
|
<Row3 articles={sortedArticles().slice(1, 4)} />
|
||||||
|
|
||||||
<Show when={sortedArticles().length > 4}>
|
|
||||||
<Row2 articles={sortedArticles().slice(4, 6)} />
|
<Row2 articles={sortedArticles().slice(4, 6)} />
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={sortedArticles().length > 6}>
|
|
||||||
<Row3 articles={sortedArticles().slice(6, 9)} />
|
<Row3 articles={sortedArticles().slice(6, 9)} />
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={sortedArticles().length > 9}>
|
|
||||||
<Row3 articles={sortedArticles().slice(9, 12)} />
|
<Row3 articles={sortedArticles().slice(9, 12)} />
|
||||||
</Show>
|
|
||||||
|
<For each={pages()}>
|
||||||
|
{(page) => (
|
||||||
|
<>
|
||||||
|
<Row3 articles={page.slice(0, 3)} />
|
||||||
|
<Row3 articles={page.slice(3, 6)} />
|
||||||
|
<Row3 articles={page.slice(6, 9)} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
|
<p class="load-more-container">
|
||||||
|
<button class="button" onClick={loadMore}>
|
||||||
|
{t('Load more')}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createMemo, For, Show } from 'solid-js'
|
import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
|
||||||
import type { Shout, Reaction } from '../../graphql/types.gen'
|
|
||||||
import '../../styles/Feed.scss'
|
import '../../styles/Feed.scss'
|
||||||
|
import stylesBeside from '../../components/Feed/Beside.module.scss'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { byCreated, sortBy } from '../../utils/sortby'
|
import { byCreated, sortBy } from '../../utils/sortby'
|
||||||
import { TopicCard } from '../Topic/Card'
|
import { TopicCard } from '../Topic/Card'
|
||||||
|
@ -16,11 +16,6 @@ import { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
import { useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
||||||
|
|
||||||
interface FeedProps {
|
|
||||||
articles: Shout[]
|
|
||||||
reactions?: Reaction[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// const AUTHORSHIP_REACTIONS = [
|
// const AUTHORSHIP_REACTIONS = [
|
||||||
// ReactionKind.Accept,
|
// ReactionKind.Accept,
|
||||||
// ReactionKind.Reject,
|
// ReactionKind.Reject,
|
||||||
|
@ -28,9 +23,11 @@ interface FeedProps {
|
||||||
// ReactionKind.Ask
|
// ReactionKind.Ask
|
||||||
// ]
|
// ]
|
||||||
|
|
||||||
export const FeedView = (props: FeedProps) => {
|
export const FEED_PAGE_SIZE = 20
|
||||||
|
|
||||||
|
export const FeedView = () => {
|
||||||
// state
|
// state
|
||||||
const { sortedArticles } = useArticlesStore({ sortedArticles: props.articles })
|
const { sortedArticles } = useArticlesStore()
|
||||||
const reactions = useReactionsStore()
|
const reactions = useReactionsStore()
|
||||||
const { sortedAuthors } = useAuthorsStore()
|
const { sortedAuthors } = useAuthorsStore()
|
||||||
const { topTopics } = useTopicsStore()
|
const { topTopics } = useTopicsStore()
|
||||||
|
@ -39,6 +36,8 @@ export const FeedView = (props: FeedProps) => {
|
||||||
|
|
||||||
const topReactions = createMemo(() => sortBy(reactions(), byCreated))
|
const topReactions = createMemo(() => sortBy(reactions(), byCreated))
|
||||||
|
|
||||||
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
// const expectingFocus = createMemo<Shout[]>(() => {
|
// const expectingFocus = createMemo<Shout[]>(() => {
|
||||||
// // 1 co-author notifications needs
|
// // 1 co-author notifications needs
|
||||||
// // TODO: list of articles where you are co-author
|
// // TODO: list of articles where you are co-author
|
||||||
|
@ -52,13 +51,15 @@ export const FeedView = (props: FeedProps) => {
|
||||||
// return []
|
// return []
|
||||||
// })
|
// })
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
const loadMore = async () => {
|
||||||
const loadMore = () => {
|
const { hasMore } = await loadRecentArticles({ limit: FEED_PAGE_SIZE, offset: sortedArticles().length })
|
||||||
// const limit = props.limit || 50
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
// const offset = props.offset || 0
|
|
||||||
// FIXME
|
|
||||||
loadRecentArticles({ limit: 50, offset: 0 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadMore()
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="container feed">
|
<div class="container feed">
|
||||||
|
@ -90,7 +91,7 @@ export const FeedView = (props: FeedProps) => {
|
||||||
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<div class="beside-column-title">
|
<div class={stylesBeside.besideColumnTitle}>
|
||||||
<h4>{t('Popular authors')}</h4>
|
<h4>{t('Popular authors')}</h4>
|
||||||
<a href="/user/list">
|
<a href="/user/list">
|
||||||
{t('All authors')}
|
{t('All authors')}
|
||||||
|
@ -98,7 +99,7 @@ export const FeedView = (props: FeedProps) => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="beside-column">
|
<ul class={stylesBeside.besideColumn}>
|
||||||
<For each={topAuthors().slice(0, 5)}>
|
<For each={topAuthors().slice(0, 5)}>
|
||||||
{(author) => (
|
{(author) => (
|
||||||
<li>
|
<li>
|
||||||
|
@ -112,10 +113,6 @@ export const FeedView = (props: FeedProps) => {
|
||||||
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<p class="load-more-container">
|
|
||||||
<button class="button">{t('Load more')}</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="col-md-3">
|
<aside class="col-md-3">
|
||||||
|
@ -135,12 +132,13 @@ export const FeedView = (props: FeedProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
<p class="load-more-container">
|
<p class="load-more-container">
|
||||||
<button class="button" onClick={loadMore}>
|
<button class="button" onClick={loadMore}>
|
||||||
{t('Load more')}
|
{t('Load more')}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { createMemo, For, onMount, Show } from 'solid-js'
|
import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
|
||||||
import Banner from '../Discours/Banner'
|
import Banner from '../Discours/Banner'
|
||||||
import { NavTopics } from '../Nav/Topics'
|
import { NavTopics } from '../Nav/Topics'
|
||||||
import { Row5 } from '../Feed/Row5'
|
import { Row5 } from '../Feed/Row5'
|
||||||
import Row3 from '../Feed/Row3'
|
import { Row3 } from '../Feed/Row3'
|
||||||
import Row2 from '../Feed/Row2'
|
import { Row2 } from '../Feed/Row2'
|
||||||
import Row1 from '../Feed/Row1'
|
import { Row1 } from '../Feed/Row1'
|
||||||
import Hero from '../Discours/Hero'
|
import Hero from '../Discours/Hero'
|
||||||
import Beside from '../Feed/Beside'
|
import { Beside } from '../Feed/Beside'
|
||||||
import RowShort from '../Feed/RowShort'
|
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'
|
||||||
|
@ -23,12 +23,14 @@ import {
|
||||||
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'
|
||||||
|
import { splitToPages } from '../../utils/splitToPages'
|
||||||
|
|
||||||
type HomeProps = {
|
type HomeProps = {
|
||||||
randomTopics: Topic[]
|
randomTopics: Topic[]
|
||||||
recentPublishedArticles: Shout[]
|
recentPublishedArticles: Shout[]
|
||||||
}
|
}
|
||||||
const PRERENDERED_ARTICLES_COUNT = 5
|
|
||||||
|
export const PRERENDERED_ARTICLES_COUNT = 5
|
||||||
const CLIENT_LOAD_ARTICLES_COUNT = 29
|
const CLIENT_LOAD_ARTICLES_COUNT = 29
|
||||||
const LOAD_MORE_PAGE_SIZE = 16 // Row1 + Row3 + Row2 + Beside (3 + 1) + Row1 + Row 2 + Row3
|
const LOAD_MORE_PAGE_SIZE = 16 // Row1 + Row3 + Row2 + Beside (3 + 1) + Row1 + Row 2 + Row3
|
||||||
|
|
||||||
|
@ -46,14 +48,20 @@ export const HomeView = (props: HomeProps) => {
|
||||||
const { randomTopics, topTopics } = useTopicsStore({
|
const { randomTopics, topTopics } = useTopicsStore({
|
||||||
randomTopics: props.randomTopics
|
randomTopics: props.randomTopics
|
||||||
})
|
})
|
||||||
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
const { topAuthors } = useTopAuthorsStore()
|
const { topAuthors } = useTopAuthorsStore()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
loadTopArticles()
|
loadTopArticles()
|
||||||
loadTopMonthArticles()
|
loadTopMonthArticles()
|
||||||
if (sortedArticles().length < PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) {
|
if (sortedArticles().length < PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) {
|
||||||
loadPublishedArticles({ limit: CLIENT_LOAD_ARTICLES_COUNT, offset: sortedArticles().length })
|
const { hasMore } = await loadPublishedArticles({
|
||||||
|
limit: CLIENT_LOAD_ARTICLES_COUNT,
|
||||||
|
offset: sortedArticles().length
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -82,22 +90,23 @@ export const HomeView = (props: HomeProps) => {
|
||||||
|
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
saveScrollPosition()
|
saveScrollPosition()
|
||||||
await loadPublishedArticles({ limit: LOAD_MORE_PAGE_SIZE, offset: sortedArticles().length })
|
|
||||||
|
const { hasMore } = await loadPublishedArticles({
|
||||||
|
limit: LOAD_MORE_PAGE_SIZE,
|
||||||
|
offset: sortedArticles().length
|
||||||
|
})
|
||||||
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
|
|
||||||
restoreScrollPosition()
|
restoreScrollPosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
const pages = createMemo<Shout[][]>(() => {
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
return sortedArticles()
|
splitToPages(
|
||||||
.slice(PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT)
|
sortedArticles(),
|
||||||
.reduce((acc, article, index) => {
|
PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT,
|
||||||
if (index % LOAD_MORE_PAGE_SIZE === 0) {
|
LOAD_MORE_PAGE_SIZE
|
||||||
acc.push([])
|
)
|
||||||
}
|
)
|
||||||
|
|
||||||
acc[acc.length - 1].push(article)
|
|
||||||
return acc
|
|
||||||
}, [] as Shout[][])
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={locale() && sortedArticles().length > 0}>
|
<Show when={locale() && sortedArticles().length > 0}>
|
||||||
|
@ -170,11 +179,13 @@ export const HomeView = (props: HomeProps) => {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
<p class="load-more-container">
|
<p class="load-more-container">
|
||||||
<button class="button" onClick={loadMore}>
|
<button class="button" onClick={loadMore}>
|
||||||
{t('Load more')}
|
{t('Load more')}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { For, Show, createMemo } from 'solid-js'
|
import { For, Show, createMemo, onMount, createSignal } from 'solid-js'
|
||||||
import type { Shout, Topic } from '../../graphql/types.gen'
|
import type { Shout, Topic } from '../../graphql/types.gen'
|
||||||
import Row3 from '../Feed/Row3'
|
import { Row3 } from '../Feed/Row3'
|
||||||
import Row2 from '../Feed/Row2'
|
import { Row2 } from '../Feed/Row2'
|
||||||
import Beside from '../Feed/Beside'
|
import { Beside } from '../Feed/Beside'
|
||||||
import { ArticleCard } from '../Feed/Card'
|
import { ArticleCard } from '../Feed/Card'
|
||||||
import '../../styles/Topic.scss'
|
import '../../styles/Topic.scss'
|
||||||
import { FullTopic } from '../Topic/Full'
|
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 { useArticlesStore } from '../../stores/zine/articles'
|
import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/articles'
|
||||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
|
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||||
|
import { splitToPages } from '../../utils/splitToPages'
|
||||||
|
|
||||||
type TopicsPageSearchParams = {
|
type TopicsPageSearchParams = {
|
||||||
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
|
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
|
||||||
|
@ -22,9 +24,14 @@ interface TopicProps {
|
||||||
topicSlug: string
|
topicSlug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PRERENDERED_ARTICLES_COUNT = 21
|
||||||
|
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
|
||||||
|
|
||||||
export const TopicView = (props: TopicProps) => {
|
export const TopicView = (props: TopicProps) => {
|
||||||
const { searchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
|
const { searchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
|
||||||
|
|
||||||
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
|
const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
|
||||||
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
||||||
|
|
||||||
|
@ -32,6 +39,24 @@ export const TopicView = (props: TopicProps) => {
|
||||||
|
|
||||||
const topic = createMemo(() => topicEntities()[props.topicSlug])
|
const topic = createMemo(() => topicEntities()[props.topicSlug])
|
||||||
|
|
||||||
|
const loadMore = async () => {
|
||||||
|
saveScrollPosition()
|
||||||
|
|
||||||
|
const { hasMore } = await loadPublishedArticles({
|
||||||
|
limit: LOAD_MORE_PAGE_SIZE,
|
||||||
|
offset: sortedArticles().length
|
||||||
|
})
|
||||||
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
|
|
||||||
|
restoreScrollPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
const m = searchParams().by
|
const m = searchParams().by
|
||||||
if (m === 'viewed') return t('Top viewed')
|
if (m === 'viewed') return t('Top viewed')
|
||||||
|
@ -40,6 +65,10 @@ export const TopicView = (props: TopicProps) => {
|
||||||
return t('Top recent')
|
return t('Top recent')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
|
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="topic-page container">
|
<div class="topic-page container">
|
||||||
<Show when={topic()}>
|
<Show when={topic()}>
|
||||||
|
@ -110,6 +139,24 @@ export const TopicView = (props: TopicProps) => {
|
||||||
<Row3 articles={sortedArticles().slice(15, 18)} />
|
<Row3 articles={sortedArticles().slice(15, 18)} />
|
||||||
<Row3 articles={sortedArticles().slice(18, 21)} />
|
<Row3 articles={sortedArticles().slice(18, 21)} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<For each={pages()}>
|
||||||
|
{(page) => (
|
||||||
|
<>
|
||||||
|
<Row3 articles={page.slice(0, 3)} />
|
||||||
|
<Row3 articles={page.slice(3, 6)} />
|
||||||
|
<Row3 articles={page.slice(6, 9)} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
|
<p class="load-more-container">
|
||||||
|
<button class="button" onClick={loadMore}>
|
||||||
|
{t('Load more')}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,6 @@ export type PageProps = {
|
||||||
authorArticles?: Shout[]
|
authorArticles?: Shout[]
|
||||||
topicArticles?: Shout[]
|
topicArticles?: Shout[]
|
||||||
homeArticles?: Shout[]
|
homeArticles?: Shout[]
|
||||||
feedArticles?: Shout[]
|
|
||||||
author?: Author
|
author?: Author
|
||||||
allAuthors?: Author[]
|
allAuthors?: Author[]
|
||||||
topic?: Topic
|
topic?: Topic
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation ConfirmEmailMutation($code: String!) {
|
mutation ConfirmEmailMutation($token: String!) {
|
||||||
confirmEmail(code: $code) {
|
confirmEmail(token: $token) {
|
||||||
error
|
error
|
||||||
token
|
token
|
||||||
user {
|
user {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query SendLinkQuery($email: String!) {
|
mutation SendLinkQuery($email: String!) {
|
||||||
sendLink(email: $email) {
|
sendLink(email: $email) {
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,5 +158,12 @@
|
||||||
"Hooray! Welcome!": "Ура! Добро пожаловать!",
|
"Hooray! Welcome!": "Ура! Добро пожаловать!",
|
||||||
"You've confirmed email": "Вы подтвердили почту",
|
"You've confirmed email": "Вы подтвердили почту",
|
||||||
"This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы",
|
"This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы",
|
||||||
"enter": "войдите"
|
"enter": "войдите",
|
||||||
|
"Go to main page": "Перейти на главную",
|
||||||
|
"Back to main page": "Вернуться на главную",
|
||||||
|
"Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.",
|
||||||
|
"We've sent you a message with a link to enter our website.": "Мы выслали вам письмо с ссылкой на почту. Перейдите по ссылке в письме, чтобы войти на сайт.",
|
||||||
|
"Send link again": "Прислать ссылку ещё раз",
|
||||||
|
"Link sent, check your email": "Ссылка отправлена, проверьте почту",
|
||||||
|
"Create post": "Создать публикацию"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@ import { Root } from '../../../components/Root'
|
||||||
import Zine from '../../../layouts/zine.astro'
|
import Zine from '../../../layouts/zine.astro'
|
||||||
import { apiClient } from '../../../utils/apiClient'
|
import { apiClient } from '../../../utils/apiClient'
|
||||||
import { initRouter } from '../../../stores/router'
|
import { initRouter } from '../../../stores/router'
|
||||||
|
import { PRERENDERED_ARTICLES_COUNT } from '../../../components/Views/Author'
|
||||||
|
|
||||||
const slug = Astro.params.slug.toString()
|
const slug = Astro.params.slug.toString()
|
||||||
const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: 50 })
|
const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT })
|
||||||
const author = articles[0].authors.find((a) => a.slug === slug)
|
const author = articles[0].authors.find((a) => a.slug === slug)
|
||||||
|
|
||||||
const { pathname, search } = Astro.url
|
const { pathname, search } = Astro.url
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
---
|
---
|
||||||
import { Root } from '../../components/Root'
|
import { Root } from '../../components/Root'
|
||||||
import Zine from '../../layouts/zine.astro'
|
import Zine from '../../layouts/zine.astro'
|
||||||
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
|
||||||
initRouter(pathname, search)
|
initRouter(pathname, search)
|
||||||
|
|
||||||
const articles = await apiClient.getRecentArticles({ limit: 50 })
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Zine>
|
<Zine>
|
||||||
<Root feedArticles={articles} client:load />
|
<Root client:load />
|
||||||
</Zine>
|
</Zine>
|
||||||
|
|
|
@ -3,14 +3,14 @@ import Zine from '../layouts/zine.astro'
|
||||||
import { Root } from '../components/Root'
|
import { Root } from '../components/Root'
|
||||||
import { apiClient } from '../utils/apiClient'
|
import { apiClient } from '../utils/apiClient'
|
||||||
import { initRouter } from '../stores/router'
|
import { initRouter } from '../stores/router'
|
||||||
|
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.getRecentPublishedArticles({ limit: 5 })
|
const articles = await apiClient.getRecentPublishedArticles({ limit: PRERENDERED_ARTICLES_COUNT })
|
||||||
|
|
||||||
const { pathname, search } = Astro.url
|
const { pathname, search } = Astro.url
|
||||||
initRouter(pathname, search)
|
initRouter(pathname, search)
|
||||||
|
|
||||||
|
|
||||||
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
|
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
import { Root } from '../../components/Root'
|
import { Root } from '../../components/Root'
|
||||||
import Zine from '../../layouts/zine.astro'
|
import Zine from '../../layouts/zine.astro'
|
||||||
import { apiClient } from '../../utils/apiClient'
|
import { apiClient } from '../../utils/apiClient'
|
||||||
|
import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Topic'
|
||||||
|
|
||||||
const slug = Astro.params.slug?.toString() || ''
|
const slug = Astro.params.slug?.toString() || ''
|
||||||
const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: 50 })
|
const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT })
|
||||||
const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug)
|
const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug)
|
||||||
|
|
||||||
import { initRouter } from '../../stores/router'
|
import { initRouter } from '../../stores/router'
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
---
|
---
|
||||||
return Astro.redirect('/?modal=auth&mode=welcome')
|
return Astro.redirect('/?modal=auth&mode=register')
|
||||||
---
|
---
|
||||||
|
|
|
@ -11,7 +11,6 @@ export const signIn = async (params) => {
|
||||||
setToken(authResult.token)
|
setToken(authResult.token)
|
||||||
console.debug('signed in')
|
console.debug('signed in')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signOut = () => {
|
export const signOut = () => {
|
||||||
// TODO: call backend to revoke token
|
// TODO: call backend to revoke token
|
||||||
setSession(null)
|
setSession(null)
|
||||||
|
@ -54,9 +53,8 @@ export const register = async ({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signSendLink = async (params) => {
|
export const signSendLink = async ({ email }: { email: string }) => {
|
||||||
await apiClient.authSendLink(params) // { email }
|
await apiClient.authSendLink({ email })
|
||||||
resetToken()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const renewSession = async () => {
|
export const renewSession = async () => {
|
||||||
|
@ -71,6 +69,12 @@ export const confirmEmail = async (token: string) => {
|
||||||
setSession(authResult)
|
setSession(authResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const confirmEmail = async (token: string) => {
|
||||||
|
const authResult = await apiClient.confirmEmail({ token })
|
||||||
|
setToken(authResult.token)
|
||||||
|
setSession(authResult)
|
||||||
|
}
|
||||||
|
|
||||||
export const useAuthStore = () => {
|
export const useAuthStore = () => {
|
||||||
return { session, emailChecks }
|
return { session, emailChecks }
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useRouter } from './router'
|
||||||
|
|
||||||
//export const locale = persistentAtom<string>('locale', 'ru')
|
//export const locale = persistentAtom<string>('locale', 'ru')
|
||||||
export const [locale, setLocale] = createSignal('ru')
|
export const [locale, setLocale] = createSignal('ru')
|
||||||
export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'share' | 'thank' | 'donate'
|
export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'thank' | 'donate'
|
||||||
type WarnKind = 'error' | 'warn' | 'info'
|
type WarnKind = 'error' | 'warn' | 'info'
|
||||||
|
|
||||||
export interface Warning {
|
export interface Warning {
|
||||||
|
@ -17,7 +17,6 @@ export const MODALS: Record<ModalType, ModalType> = {
|
||||||
auth: 'auth',
|
auth: 'auth',
|
||||||
subscribe: 'subscribe',
|
subscribe: 'subscribe',
|
||||||
feedback: 'feedback',
|
feedback: 'feedback',
|
||||||
share: 'share',
|
|
||||||
thank: 'thank',
|
thank: 'thank',
|
||||||
donate: 'donate'
|
donate: 'donate'
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,40 +123,109 @@ const addSortedArticles = (articles: Shout[]) => {
|
||||||
setSortedArticles((prevSortedArticles) => [...prevSortedArticles, ...articles])
|
setSortedArticles((prevSortedArticles) => [...prevSortedArticles, ...articles])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadFeed = async ({
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
}: {
|
||||||
|
limit: number
|
||||||
|
offset?: number
|
||||||
|
}): Promise<{ hasMore: boolean }> => {
|
||||||
|
// TODO: load actual feed
|
||||||
|
return await loadRecentArticles({ limit, offset })
|
||||||
|
}
|
||||||
|
|
||||||
export const loadRecentArticles = async ({
|
export const loadRecentArticles = async ({
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset
|
||||||
}: {
|
}: {
|
||||||
limit?: number
|
limit: number
|
||||||
offset?: number
|
offset?: number
|
||||||
}): Promise<void> => {
|
}): Promise<{ hasMore: boolean }> => {
|
||||||
const newArticles = await apiClient.getRecentArticles({ limit, offset })
|
const newArticles = await apiClient.getRecentArticles({ limit: limit + 1, offset })
|
||||||
|
const hasMore = newArticles.length === limit + 1
|
||||||
|
|
||||||
|
if (hasMore) {
|
||||||
|
newArticles.splice(-1)
|
||||||
|
}
|
||||||
|
|
||||||
addArticles(newArticles)
|
addArticles(newArticles)
|
||||||
addSortedArticles(newArticles)
|
addSortedArticles(newArticles)
|
||||||
|
|
||||||
|
return { hasMore }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadPublishedArticles = async ({
|
export const loadPublishedArticles = async ({
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset = 0
|
||||||
}: {
|
}: {
|
||||||
limit?: number
|
limit: number
|
||||||
offset?: number
|
offset?: number
|
||||||
}): Promise<void> => {
|
}): Promise<{ hasMore: boolean }> => {
|
||||||
const newArticles = await apiClient.getPublishedArticles({ limit, offset })
|
const newArticles = await apiClient.getPublishedArticles({ limit: limit + 1, offset })
|
||||||
|
const hasMore = newArticles.length === limit + 1
|
||||||
|
|
||||||
|
if (hasMore) {
|
||||||
|
newArticles.splice(-1)
|
||||||
|
}
|
||||||
|
|
||||||
addArticles(newArticles)
|
addArticles(newArticles)
|
||||||
addSortedArticles(newArticles)
|
addSortedArticles(newArticles)
|
||||||
|
|
||||||
|
return { hasMore }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadArticlesForAuthors = async ({ authorSlugs }: { authorSlugs: string[] }): Promise<void> => {
|
export const loadAuthorArticles = async ({
|
||||||
const articles = await apiClient.getArticlesForAuthors({ authorSlugs, limit: 50 })
|
authorSlug,
|
||||||
addArticles(articles)
|
limit,
|
||||||
setSortedArticles(articles)
|
offset = 0
|
||||||
|
}: {
|
||||||
|
authorSlug: string
|
||||||
|
limit: number
|
||||||
|
offset?: number
|
||||||
|
}): Promise<{ hasMore: boolean }> => {
|
||||||
|
const newArticles = await apiClient.getArticlesForAuthors({
|
||||||
|
authorSlugs: [authorSlug],
|
||||||
|
limit: limit + 1,
|
||||||
|
offset
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasMore = newArticles.length === limit + 1
|
||||||
|
|
||||||
|
if (hasMore) {
|
||||||
|
newArticles.splice(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
addArticles(newArticles)
|
||||||
|
addSortedArticles(newArticles)
|
||||||
|
|
||||||
|
return { hasMore }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadArticlesForTopics = async ({ topicSlugs }: { topicSlugs: string[] }): Promise<void> => {
|
export const loadTopicArticles = async ({
|
||||||
const articles = await apiClient.getArticlesForTopics({ topicSlugs, limit: 50 })
|
topicSlug,
|
||||||
addArticles(articles)
|
limit,
|
||||||
setSortedArticles(articles)
|
offset
|
||||||
|
}: {
|
||||||
|
topicSlug: string
|
||||||
|
limit: number
|
||||||
|
offset: number
|
||||||
|
}): Promise<{ hasMore: boolean }> => {
|
||||||
|
const newArticles = await apiClient.getArticlesForTopics({
|
||||||
|
topicSlugs: [topicSlug],
|
||||||
|
limit: limit + 1,
|
||||||
|
offset
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasMore = newArticles.length === limit + 1
|
||||||
|
|
||||||
|
if (hasMore) {
|
||||||
|
newArticles.splice(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
addArticles(newArticles)
|
||||||
|
addSortedArticles(newArticles)
|
||||||
|
|
||||||
|
return { hasMore }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resetSortedArticles = () => {
|
export const resetSortedArticles = () => {
|
||||||
|
|
|
@ -120,11 +120,25 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
img {
|
||||||
|
transition: filter 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
margin-right: 0.3em;
|
margin-right: 0.3em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
img {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
margin-left: 0.3em;
|
||||||
|
vertical-align: middle;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,14 +98,12 @@ export const apiClient = {
|
||||||
},
|
},
|
||||||
authSendLink: async ({ email }) => {
|
authSendLink: async ({ email }) => {
|
||||||
// send link with code on email
|
// send link with code on email
|
||||||
const response = await publicGraphQLClient.query(authSendLinkMutation, { email }).toPromise()
|
const response = await publicGraphQLClient.mutation(authSendLinkMutation, { email }).toPromise()
|
||||||
return response.data.reset
|
return response.data.reset
|
||||||
},
|
},
|
||||||
confirmEmail: async ({ token }: { token: string }) => {
|
confirmEmail: async ({ token }: { token: string }) => {
|
||||||
// confirm email with code from link
|
// confirm email with code from link
|
||||||
const response = await publicGraphQLClient
|
const response = await publicGraphQLClient.mutation(authConfirmEmailMutation, { token }).toPromise()
|
||||||
.mutation(authConfirmEmailMutation, { code: token })
|
|
||||||
.toPromise()
|
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
throw new ApiError('unknown', response.error.message)
|
throw new ApiError('unknown', response.error.message)
|
||||||
|
@ -183,7 +181,7 @@ export const apiClient = {
|
||||||
},
|
},
|
||||||
getArticlesForTopics: async ({
|
getArticlesForTopics: async ({
|
||||||
topicSlugs,
|
topicSlugs,
|
||||||
limit = FEED_SIZE,
|
limit,
|
||||||
offset = 0
|
offset = 0
|
||||||
}: {
|
}: {
|
||||||
topicSlugs: string[]
|
topicSlugs: string[]
|
||||||
|
@ -206,7 +204,7 @@ export const apiClient = {
|
||||||
},
|
},
|
||||||
getArticlesForAuthors: async ({
|
getArticlesForAuthors: async ({
|
||||||
authorSlugs,
|
authorSlugs,
|
||||||
limit = FEED_SIZE,
|
limit,
|
||||||
offset = 0
|
offset = 0
|
||||||
}: {
|
}: {
|
||||||
authorSlugs: string[]
|
authorSlugs: string[]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const isDev = import.meta.env.MODE === 'development'
|
export const isDev = import.meta.env.MODE === 'development'
|
||||||
|
|
||||||
export const apiBaseUrl = 'https://newapi.discours.io'
|
export const apiBaseUrl = 'https://newapi.discours.io'
|
||||||
// export const apiBaseUrl = 'http://localhost:8000'
|
// export const apiBaseUrl = 'http://localhost:8080'
|
||||||
|
|
10
src/utils/splitToPages.ts
Normal file
10
src/utils/splitToPages.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export function splitToPages<T>(arr: T[], startIndex: number, pageSize: number): T[][] {
|
||||||
|
return arr.slice(startIndex).reduce((acc, article, index) => {
|
||||||
|
if (index % pageSize === 0) {
|
||||||
|
acc.push([])
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[acc.length - 1].push(article)
|
||||||
|
return acc
|
||||||
|
}, [] as T[][])
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user