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