postmerge-fixes
This commit is contained in:
commit
843912b95d
|
@ -1,5 +1,5 @@
|
|||
import './Comment.scss'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { AuthorCard } from '../Author/Card'
|
||||
import { Show, createMemo } from 'solid-js'
|
||||
import { clsx } from 'clsx'
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { capitalize } from '../../utils'
|
||||
import './Full.scss'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import ArticleComment from './Comment'
|
||||
import { AuthorCard } from '../Author/Card'
|
||||
import { createMemo, For, onMount, Show } from 'solid-js'
|
||||
import type { Author, Reaction, Shout } from '../../graphql/types.gen'
|
||||
import { t } from '../../utils/intl'
|
||||
import { showModal } from '../../stores/ui'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { incrementView } from '../../stores/zine/articles'
|
||||
import MD from './MD'
|
||||
import { SharePopup } from './SharePopup'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
const MAX_COMMENT_LEVEL = 6
|
||||
|
||||
|
@ -38,7 +38,7 @@ const formatDate = (date: Date) => {
|
|||
}
|
||||
|
||||
export const FullArticle = (props: ArticleProps) => {
|
||||
const { session } = useAuthStore()
|
||||
const { session } = useSession()
|
||||
|
||||
onMount(() => {
|
||||
incrementView({ articleSlug: props.article.slug })
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Icon } from '../Nav/Icon'
|
||||
import styles from '../Nav/Popup.module.scss'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { t } from '../../utils/intl'
|
||||
import { Popup, PopupProps } from '../Nav/Popup'
|
||||
|
||||
import styles from '../_shared/Popup.module.scss'
|
||||
import type { PopupProps } from '../_shared/Popup'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
|
||||
type SharePopupProps = Omit<PopupProps, 'children'>
|
||||
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
.authorDetails {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
//padding-right: 1.2rem;
|
||||
width: max-content;
|
||||
|
||||
// padding-right: 1.2rem;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import type { Author } from '../../graphql/types.gen'
|
||||
import Userpic from './Userpic'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import styles from './Card.module.scss'
|
||||
import { createMemo, For, Show } from 'solid-js'
|
||||
import { translit } from '../../utils/ru2en'
|
||||
import { t } from '../../utils/intl'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { locale } from '../../stores/ui'
|
||||
import { follow, unfollow } from '../../stores/zine/common'
|
||||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
interface AuthorCardProps {
|
||||
compact?: boolean
|
||||
|
@ -23,7 +23,7 @@ interface AuthorCardProps {
|
|||
}
|
||||
|
||||
export const AuthorCard = (props: AuthorCardProps) => {
|
||||
const { session } = useAuthStore()
|
||||
const { session } = useSession()
|
||||
|
||||
const subscribed = createMemo<boolean>(
|
||||
() => session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createMemo, For } from 'solid-js'
|
||||
import styles from './Footer.module.scss'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import Subscribe from './Subscribe'
|
||||
import { t } from '../../utils/intl'
|
||||
import { locale } from '../../stores/ui'
|
||||
|
|
|
@ -6,7 +6,7 @@ import { AuthorCard } from '../Author/Card'
|
|||
import { TopicCard } from '../Topic/Card'
|
||||
import style from './Beside.module.scss'
|
||||
import type { Author, Shout, Topic, User } from '../../graphql/types.gen'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { t } from '../../utils/intl'
|
||||
|
||||
interface BesideProps {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { createMemo, For, Show } from 'solid-js'
|
|||
import type { Shout } from '../../graphql/types.gen'
|
||||
import { capitalize } from '../../utils'
|
||||
import { translit } from '../../utils/ru2en'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import styles from './Card.module.scss'
|
||||
import { locale } from '../../stores/ui'
|
||||
import { handleClientRouteLinkClick } from '../../stores/router'
|
||||
|
|
|
@ -8,7 +8,7 @@ const x = [
|
|||
['8', '4']
|
||||
]
|
||||
|
||||
export const Row2 = (props: { articles: Shout[] }) => {
|
||||
export const Row2 = (props: { articles: Shout[]; isEqual?: boolean }) => {
|
||||
const [y, setY] = createSignal(0)
|
||||
|
||||
createComputed(() => setY(Math.floor(Math.random() * x.length)))
|
||||
|
@ -20,8 +20,11 @@ export const Row2 = (props: { articles: Shout[] }) => {
|
|||
{(a, i) => {
|
||||
return (
|
||||
<Show when={!!a}>
|
||||
<div class={`col-md-${x[y()][i()]}`}>
|
||||
<ArticleCard article={a} settings={{ isWithCover: x[y()][i()] === '8' }} />
|
||||
<div class={`col-md-${props.isEqual ? '6' : x[y()][i()]}`}>
|
||||
<ArticleCard
|
||||
article={a}
|
||||
settings={{ isWithCover: props.isEqual || x[y()][i()] === '8' }}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { For } from 'solid-js'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
import { t } from '../../utils/intl'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import { useArticlesStore } from '../../stores/zine/articles'
|
||||
import { seen } from '../../stores/zine/seen'
|
||||
import { useSeenStore } from '../../stores/zine/seen'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
type FeedSidebarProps = {
|
||||
authors: Author[]
|
||||
}
|
||||
|
||||
export const FeedSidebar = (props: FeedSidebarProps) => {
|
||||
const getSeen = seen
|
||||
const { session } = useAuthStore()
|
||||
const { seen } = useSeenStore()
|
||||
const { session } = useSession()
|
||||
const { authorEntities } = useAuthorsStore({ authors: props.authors })
|
||||
const { articlesByTopic } = useArticlesStore()
|
||||
const { topicEntities } = useTopicsStore()
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
.swiper-slide {
|
||||
height: 0 !important;
|
||||
min-height: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
.cards-with-cover & {
|
||||
height: 0 !important;
|
||||
padding-top: 100%;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
|
@ -11,6 +13,7 @@
|
|||
@include media-breakpoint-up(md) {
|
||||
padding-top: 35% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider-arrow-prev,
|
||||
|
|
|
@ -7,11 +7,13 @@ import 'swiper/scss/pagination'
|
|||
import './Slider.scss'
|
||||
import type { Shout } from '../../graphql/types.gen'
|
||||
import { createEffect, createMemo, createSignal, Show, For } from 'solid-js'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
interface SliderProps {
|
||||
title?: string
|
||||
articles: Shout[]
|
||||
slidesPerView?: number
|
||||
isCardsWithCover?: boolean
|
||||
}
|
||||
|
||||
export default (props: SliderProps) => {
|
||||
|
@ -19,12 +21,14 @@ export default (props: SliderProps) => {
|
|||
let pagEl: HTMLDivElement | undefined
|
||||
let nextEl: HTMLDivElement | undefined
|
||||
let prevEl: HTMLDivElement | undefined
|
||||
|
||||
const isCardsWithCover = typeof props.isCardsWithCover === 'boolean' ? props.isCardsWithCover : true
|
||||
|
||||
const opts: SwiperOptions = {
|
||||
roundLengths: true,
|
||||
loop: true,
|
||||
centeredSlides: true,
|
||||
slidesPerView: 1,
|
||||
spaceBetween: 8,
|
||||
modules: [Navigation, Pagination],
|
||||
speed: 500,
|
||||
navigation: { nextEl, prevEl },
|
||||
|
@ -35,7 +39,12 @@ export default (props: SliderProps) => {
|
|||
},
|
||||
breakpoints: {
|
||||
768: {
|
||||
slidesPerView: 1.66666
|
||||
slidesPerView: props.slidesPerView > 0 ? props.slidesPerView : 1.66666,
|
||||
spaceBetween: isCardsWithCover ? 8 : 26
|
||||
},
|
||||
992: {
|
||||
slidesPerView: props.slidesPerView > 0 ? props.slidesPerView : 1.66666,
|
||||
spaceBetween: isCardsWithCover ? 8 : 52
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,12 +58,13 @@ export default (props: SliderProps) => {
|
|||
}
|
||||
})
|
||||
const articles = createMemo(() => props.articles)
|
||||
|
||||
return (
|
||||
<div class="floor floor--important">
|
||||
<div class="wide-container row">
|
||||
<h2 class="col-12">{props.title}</h2>
|
||||
<Show when={!!articles()}>
|
||||
<div class="swiper" ref={el}>
|
||||
<div class="swiper" classList={{ 'cards-with-cover': isCardsWithCover }} ref={el}>
|
||||
<div class="swiper-wrapper">
|
||||
<For each={articles()}>
|
||||
{(a: Shout) => (
|
||||
|
@ -63,7 +73,7 @@ export default (props: SliderProps) => {
|
|||
settings={{
|
||||
additionalClass: 'swiper-slide',
|
||||
isFloorImportant: true,
|
||||
isWithCover: true,
|
||||
isWithCover: isCardsWithCover,
|
||||
nodate: true
|
||||
}}
|
||||
/>
|
||||
|
@ -71,10 +81,10 @@ export default (props: SliderProps) => {
|
|||
</For>
|
||||
</div>
|
||||
<div class="slider-arrow-next" ref={nextEl} onClick={() => swiper()?.slideNext()}>
|
||||
<Icon name="slider-arrow" />
|
||||
<Icon name="slider-arrow" class={'icon'} />
|
||||
</div>
|
||||
<div class="slider-arrow-prev" ref={prevEl} onClick={() => swiper()?.slidePrev()}>
|
||||
<Icon name="slider-arrow" />
|
||||
<Icon name="slider-arrow" class={'icon'} />
|
||||
</div>
|
||||
<div class="slider-pagination" ref={pagEl} />
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import styles from './Search.module.scss'
|
||||
import { createSignal } from 'solid-js'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
type Props = {
|
||||
placeholder: string
|
||||
|
|
|
@ -2,16 +2,20 @@ import styles from './AuthModal.module.scss'
|
|||
import { clsx } from 'clsx'
|
||||
import { t } from '../../../utils/intl'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { createMemo, onMount, Show } from 'solid-js'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import { confirmEmail, useAuthStore } from '../../../stores/auth'
|
||||
|
||||
type ConfirmEmailSearchParams = {
|
||||
token: string
|
||||
}
|
||||
import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
||||
import { handleClientRouteLinkClick, useRouter } from '../../../stores/router'
|
||||
import type { ConfirmEmailSearchParams } from './types'
|
||||
import { ApiError } from '../../../utils/apiClient'
|
||||
import { useSession } from '../../../context/session'
|
||||
|
||||
export const EmailConfirm = () => {
|
||||
const { session } = useAuthStore()
|
||||
const {
|
||||
session,
|
||||
actions: { confirmEmail }
|
||||
} = useSession()
|
||||
|
||||
const [isTokenExpired, setIsTokenExpired] = createSignal(false)
|
||||
const [isTokenInvalid, setIsTokenInvalid] = createSignal(false)
|
||||
|
||||
const confirmedEmail = createMemo(() => session()?.user?.email || '')
|
||||
|
||||
|
@ -22,23 +26,54 @@ export const EmailConfirm = () => {
|
|||
try {
|
||||
await confirmEmail(token)
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
if (error.code === 'token_expired') {
|
||||
setIsTokenExpired(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (error.code === 'token_invalid') {
|
||||
setIsTokenInvalid(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log(error)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class={styles.title}>{t('Hooray! Welcome!')}</div>
|
||||
{/* TODO: texts */}
|
||||
<Show when={isTokenExpired()}>
|
||||
<div class={styles.title}>Ссылка больше не действительна</div>
|
||||
<div class={styles.text}>
|
||||
<a href="/?modal=auth&mode=login" class={styles.sendLink} onClick={handleClientRouteLinkClick}>
|
||||
{/*TODO: temp solution, should be send link again, but we don't have email here*/}
|
||||
Вход
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={isTokenInvalid()}>
|
||||
<div class={styles.title}>Неправильная ссылка</div>
|
||||
<div class={styles.text}>
|
||||
<a href="/?modal=auth&mode=login" class={styles.sendLink} onClick={handleClientRouteLinkClick}>
|
||||
{/*TODO: temp solution, should be send link again, but we don't have email here*/}
|
||||
Вход
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={Boolean(confirmedEmail())}>
|
||||
<div class={styles.title}>{t('Hooray! Welcome!')}</div>
|
||||
<div class={styles.text}>
|
||||
{t("You've confirmed email")} {confirmedEmail()}
|
||||
</div>
|
||||
</Show>
|
||||
<div>
|
||||
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
|
||||
{t('Go to main page')}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,8 +6,9 @@ import { useRouter } from '../../../stores/router'
|
|||
import { email, setEmail } from './sharedLogic'
|
||||
import type { AuthModalSearchParams } from './types'
|
||||
import { isValidEmail } from './validators'
|
||||
import { signSendLink } from '../../../stores/auth'
|
||||
import { locale } from '../../../stores/ui'
|
||||
import { ApiError } from '../../../utils/apiClient'
|
||||
import { signSendLink } from '../../../stores/auth'
|
||||
|
||||
type FormFields = {
|
||||
email: string
|
||||
|
@ -26,11 +27,13 @@ export const ForgotPasswordForm = () => {
|
|||
const [submitError, setSubmitError] = createSignal('')
|
||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||
const [isUserNotFount, setIsUserNotFound] = createSignal(false)
|
||||
|
||||
const handleSubmit = async (event: Event) => {
|
||||
event.preventDefault()
|
||||
|
||||
setSubmitError('')
|
||||
setIsUserNotFound(false)
|
||||
|
||||
const newValidationErrors: ValidationErrors = {}
|
||||
|
||||
|
@ -51,10 +54,12 @@ export const ForgotPasswordForm = () => {
|
|||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
const result = await signSendLink({ email: email(), lang: locale() })
|
||||
if (result.error) setSubmitError(result.error)
|
||||
else setSended(true)
|
||||
await signSendLink({ email: email(), lang: locale() })
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.code === 'user_not_found') {
|
||||
setIsUserNotFound(true)
|
||||
return
|
||||
}
|
||||
setSubmitError(error.message)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
|
@ -77,6 +82,21 @@ export const ForgotPasswordForm = () => {
|
|||
</ul>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={isUserNotFount()}>
|
||||
<div class={styles.authSubtitle}>
|
||||
{/*TODO: text*/}
|
||||
{t("We can't find you, check email or")}{' '}
|
||||
<a
|
||||
href="#"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
changeSearchParam('mode', 'register')
|
||||
}}
|
||||
>
|
||||
{t('register')}
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={validationErrors().email}>
|
||||
<div class={styles.validationError}>{validationErrors().email}</div>
|
||||
</Show>
|
||||
|
|
|
@ -2,7 +2,6 @@ import { t } from '../../../utils/intl'
|
|||
import styles from './AuthModal.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { SocialProviders } from './SocialProviders'
|
||||
import { signIn, signSendLink } from '../../../stores/auth'
|
||||
import { ApiError } from '../../../utils/apiClient'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import { isValidEmail } from './validators'
|
||||
|
@ -10,6 +9,8 @@ import { email, setEmail } from './sharedLogic'
|
|||
import { useRouter } from '../../../stores/router'
|
||||
import type { AuthModalSearchParams } from './types'
|
||||
import { hideModal, locale } from '../../../stores/ui'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { signSendLink } from '../../../stores/auth'
|
||||
|
||||
type FormFields = {
|
||||
email: string
|
||||
|
@ -26,6 +27,10 @@ export const LoginForm = () => {
|
|||
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
|
||||
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
||||
|
||||
const {
|
||||
actions: { signIn }
|
||||
} = useSession()
|
||||
|
||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||
|
||||
const [password, setPassword] = createSignal('')
|
||||
|
@ -53,6 +58,7 @@ export const LoginForm = () => {
|
|||
event.preventDefault()
|
||||
|
||||
setIsLinkSent(false)
|
||||
setIsEmailNotConfirmed(false)
|
||||
setSubmitError('')
|
||||
|
||||
const newValidationErrors: ValidationErrors = {}
|
||||
|
|
|
@ -4,13 +4,14 @@ import { t } from '../../../utils/intl'
|
|||
import styles from './AuthModal.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { SocialProviders } from './SocialProviders'
|
||||
import { checkEmail, register, useAuthStore } from '../../../stores/auth'
|
||||
import { isValidEmail } from './validators'
|
||||
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'
|
||||
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
|
||||
import { register } from '../../../stores/auth'
|
||||
|
||||
type FormFields = {
|
||||
name: string
|
||||
|
@ -23,7 +24,7 @@ type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
|
|||
export const RegisterForm = () => {
|
||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||
|
||||
const { emailChecks } = useAuthStore()
|
||||
const { emailChecks } = useEmailChecks()
|
||||
|
||||
const [submitError, setSubmitError] = createSignal('')
|
||||
const [name, setName] = createSignal('')
|
||||
|
@ -60,11 +61,14 @@ export const RegisterForm = () => {
|
|||
|
||||
const newValidationErrors: ValidationErrors = {}
|
||||
|
||||
if (!name()) {
|
||||
const clearName = name().trim()
|
||||
const clearEmail = email().trim()
|
||||
|
||||
if (!clearName) {
|
||||
newValidationErrors.name = t('Please enter a name to sign your comments and publication')
|
||||
}
|
||||
|
||||
if (!email()) {
|
||||
if (!clearEmail) {
|
||||
newValidationErrors.email = t('Please enter email')
|
||||
} else if (!isValidEmail(email())) {
|
||||
newValidationErrors.email = t('Invalid email')
|
||||
|
@ -76,7 +80,7 @@ export const RegisterForm = () => {
|
|||
|
||||
setValidationErrors(newValidationErrors)
|
||||
|
||||
const emailCheckResult = await checkEmail(email())
|
||||
const emailCheckResult = await checkEmail(clearEmail)
|
||||
|
||||
const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult
|
||||
|
||||
|
@ -88,8 +92,8 @@ export const RegisterForm = () => {
|
|||
|
||||
try {
|
||||
await register({
|
||||
name: name(),
|
||||
email: email(),
|
||||
name: clearName,
|
||||
email: clearEmail,
|
||||
password: password()
|
||||
})
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from '../../../utils/intl'
|
||||
import { Icon } from '../Icon'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
|
||||
import styles from './SocialProviders.module.scss'
|
||||
|
|
|
@ -39,7 +39,7 @@ export const AuthModal = () => {
|
|||
class={clsx('row', styles.view)}
|
||||
classList={{ [styles.signUp]: mode() === 'register' || mode() === 'confirm-email' }}
|
||||
>
|
||||
<div class={clsx('col-sm-6', 'd-md-none', styles.authImage)}>
|
||||
<div class={clsx('col-md-6 d-none d-md-block', styles.authImage)}>
|
||||
<div
|
||||
class={styles.authImageText}
|
||||
classList={{ [styles.hidden]: mode() !== 'register' && mode() !== 'confirm-email' }}
|
||||
|
@ -68,7 +68,7 @@ export const AuthModal = () => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class={clsx('col-sm-6', styles.auth)}>
|
||||
<div class={clsx('col-md-6', styles.auth)}>
|
||||
<Dynamic component={AUTH_MODAL_MODES[mode()]} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,3 +3,7 @@ export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-pas
|
|||
export type AuthModalSearchParams = {
|
||||
mode: AuthModalMode
|
||||
}
|
||||
|
||||
export type ConfirmEmailSearchParams = {
|
||||
token: string
|
||||
}
|
||||
|
|
|
@ -71,12 +71,16 @@
|
|||
align-items: center;
|
||||
display: inline-flex;
|
||||
height: 56px;
|
||||
padding: 0 $container-padding-x 0 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
transition: height 0.2s;
|
||||
text-align: center;
|
||||
z-index: 9;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding: 0 6rem 0 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
height: 70px;
|
||||
}
|
||||
|
@ -114,6 +118,7 @@
|
|||
|
||||
.usernav {
|
||||
display: inline-flex;
|
||||
font-weight: 500;
|
||||
padding-right: 0;
|
||||
position: relative;
|
||||
width: auto;
|
||||
|
@ -141,6 +146,7 @@
|
|||
|
||||
.mainNavigation {
|
||||
display: inline-flex;
|
||||
font-weight: 500;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
opacity: 1;
|
||||
|
@ -204,10 +210,14 @@
|
|||
box-sizing: content-box;
|
||||
display: inline-flex;
|
||||
float: right;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-left: divide($container-padding-x, 2);
|
||||
width: 2.2rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding-left: divide($container-padding-x, 2);
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
display: none;
|
||||
}
|
||||
|
@ -318,12 +328,16 @@
|
|||
.articleControls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 2rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.control {
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
|
@ -344,7 +358,11 @@
|
|||
}
|
||||
|
||||
.control + .control {
|
||||
margin-left: 1.6rem;
|
||||
margin-left: 1.2rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
|
@ -390,12 +408,20 @@
|
|||
display: flex;
|
||||
height: 2.4em;
|
||||
justify-content: center;
|
||||
margin-left: divide($container-padding-x, 2);
|
||||
margin-left: divide($container-padding-x, 4);
|
||||
position: relative;
|
||||
transition: margin-left 0.3s;
|
||||
width: 2.4em;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-left: 1.2rem;
|
||||
@include media-breakpoint-down(sm) {
|
||||
margin-left: 0.4rem !important;
|
||||
}
|
||||
|
||||
.headerScrolledTop &,
|
||||
.headerScrolledBottom & {
|
||||
border-color: transparent;
|
||||
margin-left: 0;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.circlewrap {
|
||||
|
@ -459,7 +485,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.userControlItemWritePost {
|
||||
.userControlItemVerbose {
|
||||
@include media-breakpoint-up(lg) {
|
||||
width: auto;
|
||||
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
import { For, Show, createSignal, createMemo, createEffect, onMount, onCleanup } from 'solid-js'
|
||||
import Notifications from './Notifications'
|
||||
import { Icon } from './Icon'
|
||||
import { For, Show, createSignal, createEffect, onMount, onCleanup } from 'solid-js'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { Modal } from './Modal'
|
||||
import { AuthModal } from './AuthModal'
|
||||
import { t } from '../../utils/intl'
|
||||
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { useModalStore } from '../../stores/ui'
|
||||
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
||||
import styles from './Header.module.scss'
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import { getLogger } from '../../utils/logger'
|
||||
import { clsx } from 'clsx'
|
||||
import { HeaderAuth } from './HeaderAuth'
|
||||
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' },
|
||||
|
@ -34,19 +27,15 @@ export const Header = (props: Props) => {
|
|||
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
|
||||
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()
|
||||
const { modal } = useModalStore()
|
||||
|
||||
const { page } = useRouter()
|
||||
|
||||
// methods
|
||||
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
||||
|
||||
const toggleFixed = () => setFixed((oldFixed) => !oldFixed)
|
||||
// effects
|
||||
|
||||
|
@ -69,20 +58,6 @@ export const Header = (props: Props) => {
|
|||
}
|
||||
})
|
||||
|
||||
// derived
|
||||
const authorized = createMemo(() => session()?.user?.slug)
|
||||
|
||||
const handleBellIconClick = (event: Event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!authorized()) {
|
||||
showModal('auth')
|
||||
return
|
||||
}
|
||||
|
||||
toggleWarnings()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let scrollTop = window.scrollY
|
||||
|
||||
|
@ -146,64 +121,7 @@ export const Header = (props: Props) => {
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class={styles.usernav}>
|
||||
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemWritePost)}>
|
||||
<a href="/create" onClick={handleClientRouteLinkClick}>
|
||||
<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} />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Show when={visibleWarnings()}>
|
||||
<div class={clsx(styles.userControlItem, 'notifications')}>
|
||||
<Notifications />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={authorized()}
|
||||
fallback={
|
||||
<div class={clsx(styles.userControlItem, 'loginbtn')}>
|
||||
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
||||
<Icon name="user-anonymous" />
|
||||
</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>
|
||||
<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>
|
||||
<HeaderAuth setIsProfilePopupVisible={setIsProfilePopupVisible} />
|
||||
<Show when={props.title}>
|
||||
<div class={styles.articleControls}>
|
||||
<SharePopup
|
||||
|
@ -224,7 +142,6 @@ export const Header = (props: Props) => {
|
|||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class={styles.burgerContainer}>
|
||||
<div class={styles.burger} classList={{ fixed: fixed() }} onClick={toggleFixed}>
|
||||
<div />
|
||||
|
|
107
src/components/Nav/HeaderAuth.tsx
Normal file
107
src/components/Nav/HeaderAuth.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
import styles from './Header.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||
import { t } from '../../utils/intl'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { createSignal, onMount, Show } from 'solid-js'
|
||||
import Notifications from './Notifications'
|
||||
import { ProfilePopup } from './ProfilePopup'
|
||||
import Userpic from '../Author/Userpic'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
import { showModal, useWarningsStore } from '../../stores/ui'
|
||||
import { ClientContainer } from '../_shared/ClientContainer'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
type HeaderAuthProps = {
|
||||
setIsProfilePopupVisible: (value: boolean) => void
|
||||
}
|
||||
|
||||
export const HeaderAuth = (props: HeaderAuthProps) => {
|
||||
const { page } = useRouter()
|
||||
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
||||
const { warnings } = useWarningsStore()
|
||||
|
||||
const { session, isAuthenticated } = useSession()
|
||||
|
||||
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
||||
|
||||
const handleBellIconClick = (event: Event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!isAuthenticated()) {
|
||||
showModal('auth')
|
||||
return
|
||||
}
|
||||
|
||||
toggleWarnings()
|
||||
}
|
||||
|
||||
return (
|
||||
<ClientContainer>
|
||||
<Show when={!session.loading}>
|
||||
<div class={styles.usernav}>
|
||||
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||
<a href="/create" onClick={handleClientRouteLinkClick}>
|
||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||
<Icon name="pencil" class={styles.icon} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Show when={isAuthenticated()}>
|
||||
<div class={styles.userControlItem}>
|
||||
<a href="#" onClick={handleBellIconClick}>
|
||||
<div>
|
||||
<Icon name="bell-white" counter={isAuthenticated() ? warnings().length : 1} />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={visibleWarnings()}>
|
||||
<div class={clsx(styles.userControlItem, 'notifications')}>
|
||||
<Notifications />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={isAuthenticated()}
|
||||
fallback={
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
||||
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
||||
<span class={styles.textLabel}>{t('Enter')}</span>
|
||||
<Icon name="user-anonymous" class={styles.icon} />
|
||||
</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>
|
||||
<ProfilePopup
|
||||
onVisibilityChange={(isVisible) => {
|
||||
props.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>
|
||||
</div>
|
||||
</Show>
|
||||
</ClientContainer>
|
||||
)
|
||||
}
|
|
@ -2,16 +2,19 @@ import { AuthorCard } from '../Author/Card'
|
|||
import type { Author } from '../../graphql/types.gen'
|
||||
import { t } from '../../utils/intl'
|
||||
import { hideModal } from '../../stores/ui'
|
||||
import { useAuthStore, signOut } from '../../stores/auth'
|
||||
import { createMemo, For } from 'solid-js'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
const quit = () => {
|
||||
export const ProfileModal = () => {
|
||||
const {
|
||||
session,
|
||||
actions: { signOut }
|
||||
} = useSession()
|
||||
|
||||
const quit = () => {
|
||||
signOut()
|
||||
hideModal()
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const { session } = useAuthStore()
|
||||
}
|
||||
|
||||
const author = createMemo<Author>(() => {
|
||||
const a: Author = {
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import { Popup, PopupProps } from './Popup'
|
||||
import { signOut, useAuthStore } from '../../stores/auth'
|
||||
import styles from './Popup.module.scss'
|
||||
import { useSession } from '../../context/session'
|
||||
import type { PopupProps } from '../_shared/Popup'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
import styles from '../_shared/Popup.module.scss'
|
||||
|
||||
type ProfilePopupProps = Omit<PopupProps, 'children'>
|
||||
|
||||
export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||
const { session } = useAuthStore()
|
||||
const {
|
||||
session,
|
||||
actions: { signOut }
|
||||
} = useSession()
|
||||
|
||||
return (
|
||||
<Popup {...props} horizontalAnchor="right">
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { For, Show } from 'solid-js'
|
||||
import type { Topic } from '../../graphql/types.gen'
|
||||
import { Icon } from './Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import './Topics.scss'
|
||||
import { t } from '../../utils/intl'
|
||||
import { locale } from '../../stores/ui'
|
||||
import { handleClientRouteLinkClick } from '../../stores/router'
|
||||
|
||||
export const NavTopics = (props: { topics: Topic[] }) => {
|
||||
const tag = (topic: Topic) =>
|
||||
|
@ -17,7 +18,7 @@ export const NavTopics = (props: { topics: Topic[] }) => {
|
|||
<For each={props.topics}>
|
||||
{(topic) => (
|
||||
<li class="item">
|
||||
<a href={`/topic/${topic.slug}`}>
|
||||
<a href={`/topic/${topic.slug}`} onClick={handleClientRouteLinkClick}>
|
||||
<span>#{tag(topic)}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -1,27 +1,14 @@
|
|||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
import { AllAuthorsView } from '../Views/AllAuthors'
|
||||
import type { PageProps } from '../types'
|
||||
import { createSignal, onMount, Show } from 'solid-js'
|
||||
import { loadAllAuthors } from '../../stores/zine/authors'
|
||||
import { Loading } from '../Loading'
|
||||
import { ClientContainer } from '../_shared/ClientContainer'
|
||||
|
||||
export const AllAuthorsPage = (props: PageProps) => {
|
||||
const [isLoaded, setIsLoaded] = createSignal<boolean>(Boolean(props.allAuthors))
|
||||
|
||||
onMount(async () => {
|
||||
if (isLoaded()) {
|
||||
return
|
||||
}
|
||||
|
||||
await loadAllAuthors()
|
||||
setIsLoaded(true)
|
||||
})
|
||||
|
||||
return (
|
||||
<PageWrap>
|
||||
<Show when={isLoaded()} fallback={<Loading />}>
|
||||
<ClientContainer>
|
||||
<AllAuthorsView authors={props.allAuthors} />
|
||||
</Show>
|
||||
</ClientContainer>
|
||||
</PageWrap>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,27 +1,14 @@
|
|||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
import { AllTopicsView } from '../Views/AllTopics'
|
||||
import type { PageProps } from '../types'
|
||||
import { createSignal, onMount, Show } from 'solid-js'
|
||||
import { loadAllTopics } from '../../stores/zine/topics'
|
||||
import { Loading } from '../Loading'
|
||||
import { ClientContainer } from '../_shared/ClientContainer'
|
||||
|
||||
export const AllTopicsPage = (props: PageProps) => {
|
||||
const [isLoaded, setIsLoaded] = createSignal<boolean>(Boolean(props.allTopics))
|
||||
|
||||
onMount(async () => {
|
||||
if (isLoaded()) {
|
||||
return
|
||||
}
|
||||
|
||||
await loadAllTopics()
|
||||
setIsLoaded(true)
|
||||
})
|
||||
|
||||
return (
|
||||
<PageWrap>
|
||||
<Show when={isLoaded()} fallback={<Loading />}>
|
||||
<ClientContainer>
|
||||
<AllTopicsView topics={props.allTopics} />
|
||||
</Show>
|
||||
</ClientContainer>
|
||||
</PageWrap>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
import { ArticleView } from '../Views/Article'
|
||||
import type { PageProps } from '../types'
|
||||
import { loadArticle, useArticlesStore } from '../../stores/zine/articles'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
import { AuthorView, PRERENDERED_ARTICLES_COUNT } from '../Views/Author'
|
||||
import type { PageProps } from '../types'
|
||||
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
|
||||
export const ConnectPage = () => {
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { lazy, Suspense } from 'solid-js'
|
||||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
import { Loading } from '../Loading'
|
||||
|
||||
const CreateView = lazy(() => import('../Views/Create'))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
import { FeedView } from '../Views/Feed'
|
||||
import { onCleanup } from 'solid-js'
|
||||
import { resetSortedArticles } from '../../stores/zine/articles'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { FourOuFourView } from '../Views/FourOuFour'
|
||||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
|
||||
export const FourOuFourPage = () => {
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { HomeView, PRERENDERED_ARTICLES_COUNT } from '../Views/Home'
|
||||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
import type { PageProps } from '../types'
|
||||
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { loadPublishedArticles, resetSortedArticles } from '../../stores/zine/articles'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
import type { PageProps } from '../types'
|
||||
import { createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { resetSortedArticles } from '../../stores/zine/articles'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
import { SearchView } from '../Views/Search'
|
||||
import type { PageProps } from '../types'
|
||||
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../Wraps/PageWrap'
|
||||
import { PageWrap } from '../_shared/PageWrap'
|
||||
import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../Views/Topic'
|
||||
import type { PageProps } from '../types'
|
||||
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||
|
@ -8,7 +8,7 @@ import { loadTopic } from '../../stores/zine/topics'
|
|||
import { Loading } from '../Loading'
|
||||
|
||||
export const TopicPage = (props: PageProps) => {
|
||||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.shouts) && Boolean(props.author))
|
||||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.shouts) && Boolean(props.topic))
|
||||
|
||||
const slug = createMemo(() => {
|
||||
const { page: getPage } = useRouter()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../../Wraps/PageWrap'
|
||||
import { PageWrap } from '../../_shared/PageWrap'
|
||||
import { t } from '../../../utils/intl'
|
||||
|
||||
export const DiscussionRulesPage = () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../../Wraps/PageWrap'
|
||||
import { PageWrap } from '../../_shared/PageWrap'
|
||||
|
||||
// const title = t('Dogma')
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createSignal, Show } from 'solid-js'
|
||||
import { PageWrap } from '../../Wraps/PageWrap'
|
||||
import { PageWrap } from '../../_shared/PageWrap'
|
||||
import { t } from '../../../utils/intl'
|
||||
import { Icon } from '../../Nav/Icon'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
export const GuidePage = () => {
|
||||
const title = t('How it works')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createSignal, Show } from 'solid-js'
|
||||
import { PageWrap } from '../../Wraps/PageWrap'
|
||||
import { PageWrap } from '../../_shared/PageWrap'
|
||||
import { Donate } from '../../Discours/Donate'
|
||||
import { Icon } from '../../Nav/Icon'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
// const title = t('Support us')
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { createSignal, Show } from 'solid-js'
|
||||
import { PageWrap } from '../../Wraps/PageWrap'
|
||||
import { PageWrap } from '../../_shared/PageWrap'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
import { Feedback } from '../../Discours/Feedback'
|
||||
import Subscribe from '../../Discours/Subscribe'
|
||||
import Opener from '../../Nav/Opener'
|
||||
import { Icon } from '../../Nav/Icon'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
// title={t('Manifest')}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../../Wraps/PageWrap'
|
||||
import { PageWrap } from '../../_shared/PageWrap'
|
||||
import { t } from '../../../utils/intl'
|
||||
|
||||
// const title = t('Partners')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../../Wraps/PageWrap'
|
||||
import { PageWrap } from '../../_shared/PageWrap'
|
||||
import { t } from '../../../utils/intl'
|
||||
|
||||
export const PrinciplesPage = () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../../Wraps/PageWrap'
|
||||
import { PageWrap } from '../../_shared/PageWrap'
|
||||
import { t } from '../../../utils/intl'
|
||||
|
||||
// title={t('Projects')}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createSignal, Show } from 'solid-js'
|
||||
import { PageWrap } from '../../Wraps/PageWrap'
|
||||
import { Icon } from '../../Nav/Icon'
|
||||
import { PageWrap } from '../../_shared/PageWrap'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
// const title = t('Terms of use')
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageWrap } from '../../Wraps/PageWrap'
|
||||
import { PageWrap } from '../../_shared/PageWrap'
|
||||
import { t } from '../../../utils/intl'
|
||||
|
||||
export const ThanksPage = () => {
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
// import 'solid-devtools'
|
||||
|
||||
import { MODALS, setLocale, showModal } from '../stores/ui'
|
||||
import { Component, createEffect, createMemo, onMount } from 'solid-js'
|
||||
import { Component, createEffect, createMemo } 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'
|
||||
import type { PageProps, RootSearchParams } from './types'
|
||||
|
||||
import { HomePage } from './Pages/HomePage'
|
||||
import { AllTopicsPage } from './Pages/AllTopicsPage'
|
||||
|
@ -30,19 +29,12 @@ import { TermsOfUsePage } from './Pages/about/TermsOfUsePage'
|
|||
import { ThanksPage } from './Pages/about/ThanksPage'
|
||||
import { CreatePage } from './Pages/CreatePage'
|
||||
import { ConnectPage } from './Pages/ConnectPage'
|
||||
import { renewSession } from '../stores/auth'
|
||||
import { LayoutShoutsPage } from './Pages/LayoutShoutsPage'
|
||||
import { SessionProvider } from '../context/session'
|
||||
|
||||
// TODO: lazy load
|
||||
// const SomePage = lazy(() => import('./Pages/SomePage'))
|
||||
|
||||
const log = getLogger('root')
|
||||
|
||||
type RootSearchParams = {
|
||||
modal: string
|
||||
lang: string
|
||||
}
|
||||
|
||||
const pagesMap: Record<keyof Routes, Component<PageProps>> = {
|
||||
expo: LayoutShoutsPage,
|
||||
connect: ConnectPage,
|
||||
|
@ -77,10 +69,6 @@ export const Root = (props: PageProps) => {
|
|||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
renewSession()
|
||||
})
|
||||
|
||||
const pageComponent = createMemo(() => {
|
||||
const result = pagesMap[page().route]
|
||||
|
||||
|
@ -99,5 +87,9 @@ export const Root = (props: PageProps) => {
|
|||
})
|
||||
}
|
||||
|
||||
return <Dynamic component={pageComponent()} {...props} />
|
||||
return (
|
||||
<SessionProvider>
|
||||
<Dynamic component={pageComponent()} {...props} />
|
||||
</SessionProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@ import type { Topic } from '../../graphql/types.gen'
|
|||
import { FollowingEntity } from '../../graphql/types.gen'
|
||||
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'
|
||||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
const log = getLogger('TopicCard')
|
||||
|
||||
|
@ -24,7 +24,7 @@ interface TopicProps {
|
|||
}
|
||||
|
||||
export const TopicCard = (props: TopicProps) => {
|
||||
const { session } = useAuthStore()
|
||||
const { session } = useSession()
|
||||
|
||||
const subscribed = createMemo(() => {
|
||||
if (!session()?.user?.slug || !session()?.news?.topics) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Topic } from '../../graphql/types.gen'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import './FloorHeader.scss'
|
||||
import { t } from '../../utils/intl'
|
||||
|
||||
|
|
|
@ -2,17 +2,17 @@ import { createMemo, Show } from 'solid-js'
|
|||
import type { Topic } from '../../graphql/types.gen'
|
||||
import { FollowingEntity } from '../../graphql/types.gen'
|
||||
import styles from './Full.module.scss'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { follow, unfollow } from '../../stores/zine/common'
|
||||
import { t } from '../../utils/intl'
|
||||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
type Props = {
|
||||
topic: Topic
|
||||
}
|
||||
|
||||
export const FullTopic = (props: Props) => {
|
||||
const { session } = useAuthStore()
|
||||
const { session } = useSession()
|
||||
|
||||
const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug))
|
||||
return (
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { createEffect, createMemo, For, Show } from 'solid-js'
|
||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
import { AuthorCard } from '../Author/Card'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { t } from '../../utils/intl'
|
||||
import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors'
|
||||
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import styles from '../../styles/AllTopics.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
type AllAuthorsPageSearchParams = {
|
||||
by: '' | 'name' | 'shouts' | 'rating'
|
||||
|
@ -17,10 +17,13 @@ type Props = {
|
|||
authors: Author[]
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const AllAuthorsView = (props: Props) => {
|
||||
const { sortedAuthors } = useAuthorsStore({ authors: props.authors })
|
||||
const [limit, setLimit] = createSignal(PAGE_SIZE)
|
||||
|
||||
const { session } = useAuthStore()
|
||||
const { session } = useSession()
|
||||
|
||||
createEffect(() => {
|
||||
setAuthorsSort(searchParams().by || 'shouts')
|
||||
|
@ -54,7 +57,7 @@ export const AllAuthorsView = (props: Props) => {
|
|||
return keys
|
||||
})
|
||||
|
||||
// log.debug(getSearchParams())
|
||||
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.allTopicsPage, 'container')}>
|
||||
|
@ -95,7 +98,7 @@ export const AllAuthorsView = (props: Props) => {
|
|||
when={!searchParams().by || searchParams().by === 'name'}
|
||||
fallback={() => (
|
||||
<div class={styles.stats}>
|
||||
<For each={sortedAuthors()}>
|
||||
<For each={sortedAuthors().slice(0, limit())}>
|
||||
{(author) => (
|
||||
<AuthorCard
|
||||
author={author}
|
||||
|
@ -107,6 +110,13 @@ export const AllAuthorsView = (props: Props) => {
|
|||
/>
|
||||
)}
|
||||
</For>
|
||||
<Show when={sortedAuthors().length > limit()}>
|
||||
<div class={styles.loadMoreContainer}>
|
||||
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
|
||||
{t('More')}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { createEffect, createMemo, For, Show } from 'solid-js'
|
||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||
import type { Topic } from '../../graphql/types.gen'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { t } from '../../utils/intl'
|
||||
import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
|
||||
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||
import { TopicCard } from '../Topic/Card'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import styles from '../../styles/AllTopics.module.scss'
|
||||
import cardStyles from '../Topic/Card.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
type AllTopicsPageSearchParams = {
|
||||
by: 'shouts' | 'authors' | 'title' | ''
|
||||
|
@ -18,18 +17,22 @@ type AllTopicsViewProps = {
|
|||
topics: Topic[]
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||
const { searchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
|
||||
const [limit, setLimit] = createSignal(PAGE_SIZE)
|
||||
|
||||
const { sortedTopics } = useTopicsStore({
|
||||
topics: props.topics,
|
||||
sortBy: searchParams().by || 'shouts'
|
||||
})
|
||||
|
||||
const { session } = useAuthStore()
|
||||
const { session } = useSession()
|
||||
|
||||
createEffect(() => {
|
||||
setTopicsSort(searchParams().by || 'shouts')
|
||||
setLimit(PAGE_SIZE)
|
||||
})
|
||||
|
||||
const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => {
|
||||
|
@ -53,6 +56,8 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
|||
|
||||
const subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || ''))
|
||||
|
||||
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.allTopicsPage, 'container')}>
|
||||
<Show when={sortedTopics().length > 0}>
|
||||
|
@ -102,9 +107,20 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
|||
<Show
|
||||
when={searchParams().by === 'title'}
|
||||
fallback={() => (
|
||||
<For each={sortedTopics()}>
|
||||
{(topic) => <TopicCard topic={topic} compact={false} subscribed={subscribed(topic.slug)} />}
|
||||
<>
|
||||
<For each={sortedTopics().slice(0, limit())}>
|
||||
{(topic) => (
|
||||
<TopicCard topic={topic} compact={false} subscribed={subscribed(topic.slug)} />
|
||||
)}
|
||||
</For>
|
||||
<Show when={sortedTopics().length > limit()}>
|
||||
<div class={styles.loadMoreContainer}>
|
||||
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
|
||||
{t('More')}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<For each={sortedKeys()}>
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import { Show, onMount, createSignal } from 'solid-js'
|
||||
import { Editor } from '../EditorNew/Editor'
|
||||
import { ClientContainer } from '../_shared/ClientContainer'
|
||||
|
||||
export const CreateView = () => {
|
||||
// don't render anything on server
|
||||
// usage of isServer causing hydration errors
|
||||
const [isMounted, setIsMounted] = createSignal(false)
|
||||
|
||||
onMount(() => setIsMounted(true))
|
||||
|
||||
return (
|
||||
<Show when={isMounted()}>
|
||||
<ClientContainer>
|
||||
<Editor />
|
||||
</Show>
|
||||
</ClientContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
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 { Icon } from '../_shared/Icon'
|
||||
import { byCreated, sortBy } from '../../utils/sortby'
|
||||
import { TopicCard } from '../Topic/Card'
|
||||
import { ArticleCard } from '../Feed/Card'
|
||||
import { AuthorCard } from '../Author/Card'
|
||||
import { t } from '../../utils/intl'
|
||||
import { FeedSidebar } from '../Feed/Sidebar'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import CommentCard from '../Article/Comment'
|
||||
import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles'
|
||||
import { useReactionsStore } from '../../stores/zine/reactions'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
// const AUTHORSHIP_REACTIONS = [
|
||||
// ReactionKind.Accept,
|
||||
|
@ -32,7 +32,7 @@ export const FeedView = () => {
|
|||
const { sortedAuthors } = useAuthorsStore()
|
||||
const { topTopics } = useTopicsStore()
|
||||
const { topAuthors } = useTopAuthorsStore()
|
||||
const { session } = useAuthStore()
|
||||
const { session } = useSession()
|
||||
|
||||
const topReactions = createMemo(() => sortBy(reactions(), byCreated))
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from '../../utils/intl'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import styles from '../../styles/FourOuFour.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import RowShort from '../Feed/RowShort'
|
|||
import Slider from '../Feed/Slider'
|
||||
import Group from '../Feed/Group'
|
||||
import type { Shout, Topic } from '../../graphql/types.gen'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { t } from '../../utils/intl'
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { For, createSignal, Show, onMount, createEffect } from 'solid-js'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
import { AuthorCard } from '../Author/Card'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { Loading } from '../Loading'
|
||||
import DialogCard from '../Inbox/DialogCard'
|
||||
import Search from '../Inbox/Search'
|
||||
|
|
|
@ -8,7 +8,7 @@ import { FullTopic } from '../Topic/Full'
|
|||
import { t } from '../../utils/intl'
|
||||
import { useRouter } from '../../stores/router'
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/articles'
|
||||
import { loadTopicArticles, useArticlesStore } from '../../stores/zine/articles'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||
import { splitToPages } from '../../utils/splitToPages'
|
||||
|
@ -26,7 +26,7 @@ interface TopicProps {
|
|||
topicSlug: string
|
||||
}
|
||||
|
||||
export const PRERENDERED_ARTICLES_COUNT = 21
|
||||
export const PRERENDERED_ARTICLES_COUNT = 28
|
||||
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
|
||||
|
||||
export const TopicView = (props: TopicProps) => {
|
||||
|
@ -44,7 +44,8 @@ export const TopicView = (props: TopicProps) => {
|
|||
const loadMore = async () => {
|
||||
saveScrollPosition()
|
||||
|
||||
const { hasMore } = await loadPublishedArticles({
|
||||
const { hasMore } = await loadTopicArticles({
|
||||
topicSlug: topic().slug,
|
||||
limit: LOAD_MORE_PAGE_SIZE,
|
||||
offset: sortedArticles().length
|
||||
})
|
||||
|
@ -112,7 +113,7 @@ export const TopicView = (props: TopicProps) => {
|
|||
</div>
|
||||
|
||||
<Row1 article={sortedArticles()[0]} />
|
||||
<Row2 articles={sortedArticles().slice(1, 3)} />
|
||||
<Row2 articles={sortedArticles().slice(1, 3)} isEqual={true} />
|
||||
|
||||
<Beside
|
||||
title={t('Topic is supported by')}
|
||||
|
@ -130,13 +131,18 @@ export const TopicView = (props: TopicProps) => {
|
|||
wrapper={'top-article'}
|
||||
/>
|
||||
|
||||
<Show when={sortedArticles().length > 5}>
|
||||
<Row3 articles={sortedArticles().slice(13, 16)} />
|
||||
<Row2 articles={sortedArticles().slice(16, 18)} />
|
||||
<Row3 articles={sortedArticles().slice(18, 21)} />
|
||||
<Row3 articles={sortedArticles().slice(21, 24)} />
|
||||
<Row3 articles={sortedArticles().slice(24, 27)} />
|
||||
</Show>
|
||||
<Row2 articles={sortedArticles().slice(13, 15)} isEqual={true} />
|
||||
<Row1 article={sortedArticles()[15]} />
|
||||
|
||||
<Slider
|
||||
title={title()}
|
||||
articles={sortedArticles().slice(16, 22)}
|
||||
slidesPerView={3}
|
||||
isCardsWithCover={false}
|
||||
/>
|
||||
|
||||
<Row3 articles={sortedArticles().slice(23, 26)} />
|
||||
<Row2 articles={sortedArticles().slice(26, 28)} />
|
||||
|
||||
<For each={pages()}>
|
||||
{(page) => (
|
||||
|
|
12
src/components/_shared/ClientContainer.tsx
Normal file
12
src/components/_shared/ClientContainer.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import type { JSX } from 'solid-js'
|
||||
import { createSignal, onMount, Show } from 'solid-js'
|
||||
|
||||
// show children only on client side
|
||||
// usage of isServer causing hydration errors
|
||||
export const ClientContainer = (props: { children: JSX.Element }) => {
|
||||
const [isMounted, setIsMounted] = createSignal(false)
|
||||
|
||||
onMount(() => setIsMounted(true))
|
||||
|
||||
return <Show when={isMounted()}>{props.children}</Show>
|
||||
}
|
|
@ -8,17 +8,19 @@ img {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.notifications-counter {
|
||||
background-color: red;
|
||||
border-radius: 1rem;
|
||||
.notificationsCounter {
|
||||
background-color: #d00820;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 2em;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
height: 1.5em;
|
||||
line-height: 1.5em;
|
||||
height: 1.6em;
|
||||
left: 1.1em;
|
||||
line-height: 1.25em;
|
||||
padding: 0 0.25em;
|
||||
position: absolute;
|
||||
right: -0.5rem;
|
||||
text-align: center;
|
||||
top: -0.5rem;
|
||||
width: 1.5em;
|
||||
min-width: 1.5em;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { mergeProps, Show } from 'solid-js'
|
||||
import type { JSX } from 'solid-js'
|
||||
import { clsx } from 'clsx'
|
||||
import './Icon.css'
|
||||
import styles from './Icon.module.scss'
|
||||
|
||||
type IconProps = {
|
||||
class?: string
|
||||
|
@ -16,10 +16,10 @@ export const Icon = (passedProps: IconProps) => {
|
|||
const props = mergeProps({ title: '', counter: 0 }, passedProps)
|
||||
|
||||
return (
|
||||
<div class={clsx('icon', props.class)} style={props.style}>
|
||||
<div class={clsx('icon', styles.icon, props.class)} style={props.style}>
|
||||
<img src={`/icons/${props.name}.svg`} alt={props.title ?? props.name} class={props.iconClassName} />
|
||||
<Show when={props.counter}>
|
||||
<div class="notifications-counter">{props.counter}</div>
|
||||
<div class={styles.notificationsCounter}>{props.counter}</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
|
@ -17,3 +17,8 @@ export type PageProps = {
|
|||
searchResults?: Shout[]
|
||||
chats?: Chat[]
|
||||
}
|
||||
|
||||
export type RootSearchParams = {
|
||||
modal: string
|
||||
lang: string
|
||||
}
|
||||
|
|
81
src/context/session.tsx
Normal file
81
src/context/session.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import type { Accessor, InitializedResource, JSX } from 'solid-js'
|
||||
import { createContext, createMemo, createResource, onMount, useContext } from 'solid-js'
|
||||
import type { AuthResult } from '../graphql/types.gen'
|
||||
import { apiClient } from '../utils/apiClient'
|
||||
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
||||
|
||||
type SessionContextType = {
|
||||
session: InitializedResource<AuthResult>
|
||||
isAuthenticated: Accessor<boolean>
|
||||
actions: {
|
||||
refreshSession: () => AuthResult | Promise<AuthResult>
|
||||
signIn: ({ email, password }: { email: string; password: string }) => Promise<void>
|
||||
signOut: () => Promise<void>
|
||||
confirmEmail: (token: string) => Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
const SessionContext = createContext<SessionContextType>()
|
||||
|
||||
const refreshSession = async (): Promise<AuthResult> => {
|
||||
try {
|
||||
const authResult = await apiClient.getSession()
|
||||
if (!authResult) {
|
||||
return null
|
||||
}
|
||||
setToken(authResult.token)
|
||||
return authResult
|
||||
} catch (error) {
|
||||
console.error('renewSession error:', error)
|
||||
resetToken()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function useSession() {
|
||||
return useContext(SessionContext)
|
||||
}
|
||||
|
||||
export const SessionProvider = (props: { children: JSX.Element }) => {
|
||||
const [session, { refetch: refetchRefreshSession, mutate }] = createResource<AuthResult>(refreshSession, {
|
||||
ssrLoadFrom: 'initial',
|
||||
initialValue: null
|
||||
})
|
||||
|
||||
const isAuthenticated = createMemo(() => Boolean(session()?.user?.slug))
|
||||
|
||||
const signIn = async ({ email, password }: { email: string; password: string }) => {
|
||||
const authResult = await apiClient.authLogin({ email, password })
|
||||
mutate(authResult)
|
||||
setToken(authResult.token)
|
||||
console.debug('signed in')
|
||||
}
|
||||
|
||||
const signOut = async () => {
|
||||
// TODO: call backend to revoke token
|
||||
mutate(null)
|
||||
resetToken()
|
||||
console.debug('signed out')
|
||||
}
|
||||
|
||||
const confirmEmail = async (token: string) => {
|
||||
const authResult = await apiClient.confirmEmail({ token })
|
||||
mutate(authResult)
|
||||
setToken(authResult.token)
|
||||
}
|
||||
|
||||
const actions = {
|
||||
refreshSession: refetchRefreshSession,
|
||||
signIn,
|
||||
signOut,
|
||||
confirmEmail
|
||||
}
|
||||
|
||||
const value: SessionContextType = { session, isAuthenticated, actions }
|
||||
|
||||
onMount(() => {
|
||||
refetchRefreshSession()
|
||||
})
|
||||
|
||||
return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider>
|
||||
}
|
|
@ -10,6 +10,10 @@ if (isDev) {
|
|||
exchanges.unshift(devtoolsExchange)
|
||||
}
|
||||
|
||||
export const getToken = (): string => {
|
||||
return localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export const setToken = (token: string) => {
|
||||
localStorage.setItem(TOKEN_LOCAL_STORAGE_KEY, token)
|
||||
}
|
||||
|
@ -27,7 +31,6 @@ const options: ClientOptions = {
|
|||
// меняем через setToken, например при получении значения с сервера
|
||||
// скорее всего придумаем что-нибудь получше со временем
|
||||
const token = localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY)
|
||||
|
||||
const headers = { Auth: token }
|
||||
return { headers }
|
||||
},
|
||||
|
|
22
src/graphql/query/author-by-slug.ts
Normal file
22
src/graphql/query/author-by-slug.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
query GetAuthorBySlugQuery($slug: String!) {
|
||||
getAuthor(slug: $slug) {
|
||||
_id: slug
|
||||
slug
|
||||
name
|
||||
bio
|
||||
userpic
|
||||
communities
|
||||
links
|
||||
createdAt
|
||||
lastSeen
|
||||
ratings {
|
||||
_id: rater
|
||||
rater
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
|
@ -8,14 +8,11 @@ export default gql`
|
|||
name
|
||||
bio
|
||||
userpic
|
||||
communities
|
||||
links
|
||||
createdAt
|
||||
lastSeen
|
||||
ratings {
|
||||
_id: rater
|
||||
rater
|
||||
value
|
||||
stat {
|
||||
followers
|
||||
followings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
22
src/graphql/query/topic-by-slug.ts
Normal file
22
src/graphql/query/topic-by-slug.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
query TopicBySlugQuery($slug: String!) {
|
||||
getTopic(slug: $slug) {
|
||||
title
|
||||
body
|
||||
slug
|
||||
pic
|
||||
parents
|
||||
children
|
||||
# community
|
||||
stat {
|
||||
_id: shouts
|
||||
shouts
|
||||
authors
|
||||
# viewed
|
||||
followers
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
|
@ -173,5 +173,7 @@
|
|||
"Artworks": "Артворки",
|
||||
"Audio": "Аудио",
|
||||
"Video": "Видео",
|
||||
"Literature": "Литература"
|
||||
"Literature": "Литература",
|
||||
"We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или",
|
||||
"register": "зарегистрируйтесь"
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { PRERENDERED_ARTICLES_COUNT } from '../../../components/Views/Author'
|
|||
|
||||
const slug = Astro.params.slug.toString()
|
||||
const shouts = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT })
|
||||
const author = shouts[0].authors.find((a) => a.slug === slug)
|
||||
const author = await apiClient.getAuthor({ slug })
|
||||
|
||||
const { pathname, search } = Astro.url
|
||||
initRouter(pathname, search)
|
||||
|
|
|
@ -6,7 +6,7 @@ import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Topic'
|
|||
|
||||
const slug = Astro.params.slug?.toString() || ''
|
||||
const shouts = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT })
|
||||
const topic = shouts[0].topics.find(({ slug: topicSlug }) => topicSlug === slug)
|
||||
const topic = await apiClient.getTopic({ slug })
|
||||
|
||||
import { initRouter } from '../../stores/router'
|
||||
|
||||
|
|
|
@ -1,45 +1,4 @@
|
|||
import type { AuthResult } from '../graphql/types.gen'
|
||||
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
||||
import { apiClient } from '../utils/apiClient'
|
||||
import { createSignal } from 'solid-js'
|
||||
|
||||
const [session, setSession] = createSignal<AuthResult | null>(null)
|
||||
|
||||
export const signIn = async (params) => {
|
||||
const authResult = await apiClient.authLogin(params)
|
||||
setSession(authResult)
|
||||
setToken(authResult.token)
|
||||
console.debug('signed in')
|
||||
}
|
||||
export const signOut = async () => {
|
||||
const result = await apiClient.authSignOut()
|
||||
if (result.error) {
|
||||
console.error('[auth] sign out error', result.error)
|
||||
} else {
|
||||
setSession(null)
|
||||
resetToken()
|
||||
console.debug('signed out')
|
||||
}
|
||||
}
|
||||
|
||||
export const [emailChecks, setEmailChecks] = createSignal<{ [email: string]: boolean }>({})
|
||||
|
||||
export const checkEmail = async (email: string): Promise<boolean> => {
|
||||
if (emailChecks()[email]) {
|
||||
return true
|
||||
}
|
||||
|
||||
const checkResult = await apiClient.authCheckEmail({ email })
|
||||
|
||||
if (checkResult) {
|
||||
setEmailChecks((oldEmailChecks) => ({ ...oldEmailChecks, [email]: true }))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const [resetCode, setResetCode] = createSignal('')
|
||||
|
||||
export const register = async ({
|
||||
name,
|
||||
|
@ -60,19 +19,3 @@ export const register = async ({
|
|||
export const signSendLink = async ({ email, lang }: { email: string; lang: string }) => {
|
||||
return await apiClient.authSendLink({ email, lang })
|
||||
}
|
||||
|
||||
export const renewSession = async () => {
|
||||
const authResult = await apiClient.getSession() // token in header
|
||||
setToken(authResult.token)
|
||||
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 }
|
||||
}
|
||||
|
|
23
src/stores/emailChecks.ts
Normal file
23
src/stores/emailChecks.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { apiClient } from '../utils/apiClient'
|
||||
import { createSignal } from 'solid-js'
|
||||
|
||||
const [emailChecks, setEmailChecks] = createSignal<{ [email: string]: boolean }>({})
|
||||
|
||||
export const checkEmail = async (email: string): Promise<boolean> => {
|
||||
if (emailChecks()[email]) {
|
||||
return true
|
||||
}
|
||||
|
||||
const checkResult = await apiClient.authCheckEmail({ email })
|
||||
|
||||
if (checkResult) {
|
||||
setEmailChecks((oldEmailChecks) => ({ ...oldEmailChecks, [email]: true }))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const useEmailChecks = () => {
|
||||
return { emailChecks }
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import { createSignal } from 'solid-js'
|
||||
import { useRouter } from './router'
|
||||
import type { AuthModalSearchParams, ConfirmEmailSearchParams } from '../components/Nav/AuthModal/types'
|
||||
import type { RootSearchParams } from '../components/types'
|
||||
|
||||
export const [locale, setLocale] = createSignal('ru')
|
||||
export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'thank' | 'donate'
|
||||
|
@ -24,10 +26,22 @@ const [modal, setModal] = createSignal<ModalType | null>(null)
|
|||
const [warnings, setWarnings] = createSignal<Warning[]>([])
|
||||
|
||||
export const showModal = (modalType: ModalType) => setModal(modalType)
|
||||
|
||||
// TODO: find a better solution
|
||||
export const hideModal = () => {
|
||||
const { changeSearchParam } = useRouter()
|
||||
changeSearchParam('modal', null, true)
|
||||
const { searchParams, changeSearchParam } = useRouter<
|
||||
AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams
|
||||
>()
|
||||
|
||||
if (searchParams().modal === 'auth') {
|
||||
if (searchParams().mode === 'confirm-email') {
|
||||
changeSearchParam('token', null, true)
|
||||
}
|
||||
changeSearchParam('mode', null, true)
|
||||
}
|
||||
|
||||
changeSearchParam('modal', null, true)
|
||||
|
||||
setModal(null)
|
||||
}
|
||||
|
||||
|
|
|
@ -52,9 +52,7 @@ const addAuthors = (authors: Author[]) => {
|
|||
}
|
||||
|
||||
export const loadAuthor = async ({ slug }: { slug: string }): Promise<void> => {
|
||||
// TODO:
|
||||
const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: 1 })
|
||||
const author = articles[0].authors.find((a) => a.slug === slug)
|
||||
const author = await apiClient.getAuthor({ slug })
|
||||
addAuthors([author])
|
||||
}
|
||||
|
||||
|
|
|
@ -3,3 +3,13 @@ import { createStorageSignal } from '@solid-primitives/storage'
|
|||
// local stored seen marks by shout's slug
|
||||
export const [seen, setSeen] = createStorageSignal<{ [slug: string]: Date }>('seen', {})
|
||||
export const addSeen = (slug) => setSeen({ ...seen(), [slug]: Date.now() })
|
||||
|
||||
export const useSeenStore = (initialData: { [slug: string]: Date } = {}) => {
|
||||
setSeen({ ...seen(), ...initialData })
|
||||
|
||||
return {
|
||||
seen,
|
||||
setSeen,
|
||||
addSeen
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,9 +100,7 @@ export const loadRandomTopics = async (): Promise<void> => {
|
|||
}
|
||||
|
||||
export const loadTopic = async ({ slug }: { slug: string }): Promise<void> => {
|
||||
// TODO:
|
||||
const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: 1 })
|
||||
const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug)
|
||||
const topic = await apiClient.getTopic({ slug })
|
||||
addTopics([topic])
|
||||
}
|
||||
|
||||
|
|
|
@ -35,3 +35,12 @@
|
|||
.stats {
|
||||
margin-top: 2.4rem;
|
||||
}
|
||||
|
||||
.loadMoreContainer {
|
||||
margin-top: 48px;
|
||||
text-align: center;
|
||||
|
||||
.loadMoreButton {
|
||||
padding: 0.6em 1.5em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -663,7 +663,7 @@ astro-island {
|
|||
width: auto;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
//padding: 0 $container-padding-x * 0.5;
|
||||
// padding: 0 $container-padding-x * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -690,11 +690,11 @@ astro-island {
|
|||
|
||||
.shift-content {
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-left: 127px;
|
||||
margin-left: 161px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-left: 201px;
|
||||
margin-left: 235px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import type { Reaction, Shout, FollowingEntity, AuthResult, ShoutInput } from '../graphql/types.gen'
|
||||
import type {
|
||||
Reaction,
|
||||
Shout,
|
||||
FollowingEntity,
|
||||
AuthResult,
|
||||
ShoutInput,
|
||||
Topic,
|
||||
Author
|
||||
} from '../graphql/types.gen'
|
||||
import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
|
||||
import { privateGraphQLClient } from '../graphql/privateGraphQLClient'
|
||||
import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient'
|
||||
import articleBySlug from '../graphql/query/article-by-slug'
|
||||
import articlesRecentAll from '../graphql/query/articles-recent-all'
|
||||
import articlesRecentPublished from '../graphql/query/articles-recent-published'
|
||||
|
@ -25,7 +33,6 @@ import authorsAll from '../graphql/query/authors-all'
|
|||
import reactionCreate from '../graphql/mutation/reaction-create'
|
||||
import reactionDestroy from '../graphql/mutation/reaction-destroy'
|
||||
import reactionUpdate from '../graphql/mutation/reaction-update'
|
||||
import authorsBySlugs from '../graphql/query/authors-by-slugs'
|
||||
import incrementView from '../graphql/mutation/increment-view'
|
||||
import createArticle from '../graphql/mutation/article-create'
|
||||
import myChats from '../graphql/query/im-chats'
|
||||
|
@ -34,10 +41,18 @@ import getRecentByLayout from '../graphql/query/layout-recent'
|
|||
import getTopByLayout from '../graphql/query/layout-top'
|
||||
import getTopMonthByLayout from '../graphql/query/layout-top-month'
|
||||
import type { LayoutType } from '../stores/zine/layouts'
|
||||
import topicBySlug from '../graphql/query/topic-by-slug'
|
||||
import authorBySlug from '../graphql/query/author-by-slug'
|
||||
|
||||
const FEED_SIZE = 50
|
||||
|
||||
type ApiErrorCode = 'unknown' | 'email_not_confirmed' | 'user_not_found' | 'user_already_exists'
|
||||
type ApiErrorCode =
|
||||
| 'unknown'
|
||||
| 'email_not_confirmed'
|
||||
| 'user_not_found'
|
||||
| 'user_already_exists'
|
||||
| 'token_expired'
|
||||
| 'token_invalid'
|
||||
|
||||
export class ApiError extends Error {
|
||||
code: ApiErrorCode
|
||||
|
@ -49,7 +64,7 @@ export class ApiError extends Error {
|
|||
}
|
||||
|
||||
export const apiClient = {
|
||||
authLogin: async ({ email, password }): Promise<AuthResult> => {
|
||||
authLogin: async ({ email, password }: { email: string; password: string }): Promise<AuthResult> => {
|
||||
const response = await publicGraphQLClient.query(authLoginQuery, { email, password }).toPromise()
|
||||
// console.debug('[api-client] authLogin', { response })
|
||||
if (response.error) {
|
||||
|
@ -103,13 +118,34 @@ export const apiClient = {
|
|||
authSendLink: async ({ email, lang }) => {
|
||||
// send link with code on email
|
||||
const response = await publicGraphQLClient.mutation(authSendLinkMutation, { email, lang }).toPromise()
|
||||
|
||||
if (response.error) {
|
||||
if (response.error.message === '[GraphQL] User not found') {
|
||||
throw new ApiError('user_not_found', response.error.message)
|
||||
}
|
||||
|
||||
throw new ApiError('unknown', response.error.message)
|
||||
}
|
||||
|
||||
if (response.data.sendLink.error) {
|
||||
throw new ApiError('unknown', response.data.sendLink.message)
|
||||
}
|
||||
|
||||
return response.data.sendLink
|
||||
},
|
||||
confirmEmail: async ({ token }: { token: string }) => {
|
||||
// confirm email with code from link
|
||||
const response = await publicGraphQLClient.mutation(authConfirmEmailMutation, { token }).toPromise()
|
||||
|
||||
if (response.error) {
|
||||
// TODO: better error communication
|
||||
if (response.error.message === '[GraphQL] check token lifetime') {
|
||||
throw new ApiError('token_expired', response.error.message)
|
||||
}
|
||||
|
||||
if (response.error.message === '[GraphQL] token is not valid') {
|
||||
throw new ApiError('token_invalid', response.error.message)
|
||||
}
|
||||
|
||||
throw new ApiError('unknown', response.error.message)
|
||||
}
|
||||
|
||||
|
@ -244,11 +280,14 @@ export const apiClient = {
|
|||
},
|
||||
|
||||
getSession: async (): Promise<AuthResult> => {
|
||||
if (!getToken()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// renew session with auth token in header (!)
|
||||
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()
|
||||
|
||||
if (response.error) {
|
||||
// TODO
|
||||
throw new ApiError('unknown', response.error.message)
|
||||
}
|
||||
|
||||
|
@ -281,9 +320,13 @@ export const apiClient = {
|
|||
}
|
||||
return response.data.authorsAll
|
||||
},
|
||||
getAuthor: async ({ slug }: { slug: string }) => {
|
||||
const response = await publicGraphQLClient.query(authorsBySlugs, { slugs: [slug] }).toPromise()
|
||||
return response.data.getUsersBySlugs
|
||||
getAuthor: async ({ slug }: { slug: string }): Promise<Author> => {
|
||||
const response = await publicGraphQLClient.query(authorBySlug, { slug }).toPromise()
|
||||
return response.data.getAuthor
|
||||
},
|
||||
getTopic: async ({ slug }: { slug: string }): Promise<Topic> => {
|
||||
const response = await publicGraphQLClient.query(topicBySlug, { slug }).toPromise()
|
||||
return response.data.getTopic
|
||||
},
|
||||
getArticle: async ({ slug }: { slug: string }): Promise<Shout> => {
|
||||
const response = await publicGraphQLClient.query(articleBySlug, { slug }).toPromise()
|
||||
|
@ -311,10 +354,6 @@ export const apiClient = {
|
|||
|
||||
return response.data.reactionsForShouts
|
||||
},
|
||||
getAuthorsBySlugs: async ({ slugs }) => {
|
||||
const response = await publicGraphQLClient.query(authorsBySlugs, { slugs }).toPromise()
|
||||
return response.data.getUsersBySlugs
|
||||
},
|
||||
createArticle: async ({ article }: { article: ShoutInput }) => {
|
||||
const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise()
|
||||
console.debug('createArticle response:', response)
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export const isDev = import.meta.env.VERCEL_ENV !== 'production'
|
||||
export const apiBaseUrl = import.meta.env.API_URL
|
||||
export const apiBaseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080'
|
||||
|
|
Loading…
Reference in New Issue
Block a user