Merge branch 'dev' into feature/session-upgrade
This commit is contained in:
commit
adae4c0144
|
@ -83,6 +83,7 @@
|
|||
"Coming soon": "Coming soon",
|
||||
"Comment successfully deleted": "Comment successfully deleted",
|
||||
"Commentator": "Commentator",
|
||||
"Commenting": "Commenting",
|
||||
"Comments": "Comments",
|
||||
"CommentsWithCount": "{count, plural, =0 {{count} comments} one {{count} comment} few {{count} comments} other {{count} comments}}",
|
||||
"Communities": "Communities",
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
"Comment successfully deleted": "Комментарий успешно удален",
|
||||
"Comment": "Комментировать",
|
||||
"Commentator": "Комментатор",
|
||||
"Commenting": "Комментирование",
|
||||
"Comments": "Комментарии",
|
||||
"CommentsWithCount": "{count, plural, =0 {{count} комментариев} one {{count} комментарий} few {{count} комментария} other {{count} комментариев}}",
|
||||
"Communities": "Сообщества",
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /
|
||||
|
|
|
@ -114,6 +114,11 @@
|
|||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.control {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.mainNavigationWrapper {
|
||||
|
@ -192,15 +197,8 @@
|
|||
padding: divide($container-padding-x, 2) !important;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
span,
|
||||
button {
|
||||
padding: 0 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.view-switcher) {
|
||||
margin: 0 -0.5rem;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -299,9 +297,6 @@
|
|||
.burgerContainer {
|
||||
box-sizing: content-box;
|
||||
display: inline-flex;
|
||||
padding-left: 0;
|
||||
|
||||
// float: right;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding-left: divide($container-padding-x, 2);
|
||||
|
@ -430,12 +425,15 @@
|
|||
width: 100%;
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
right: 2rem;
|
||||
right: 9rem;
|
||||
}
|
||||
|
||||
.control {
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
height: 3.2rem;
|
||||
margin: 0 0.6rem;
|
||||
width: 3.2rem;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
|
@ -451,11 +449,7 @@
|
|||
}
|
||||
|
||||
.control + .control {
|
||||
margin-left: 1.2rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
margin: 0 0.6rem;
|
||||
}
|
||||
|
||||
img {
|
||||
|
@ -497,10 +491,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.settingsControlContainer {
|
||||
margin-left: 1rem !important;
|
||||
margin-right: 2rem !important;
|
||||
}
|
||||
|
||||
.settingsControl {
|
||||
border-radius: 100%;
|
||||
padding: 0.8rem !important;
|
||||
min-width: 4rem !important;
|
||||
padding: 0.8rem !important;
|
||||
|
||||
&:hover {
|
||||
background: var(--background-color-invert);
|
||||
|
@ -516,12 +515,18 @@
|
|||
align-items: center;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
height: 2.4em;
|
||||
height: 2.8rem;
|
||||
justify-content: center;
|
||||
margin-left: 0.3rem;
|
||||
margin: 0 0.4rem;
|
||||
position: relative;
|
||||
transition: margin-left 0.3s;
|
||||
width: 2.4em;
|
||||
width: 2.8rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
height: 3.2rem;
|
||||
margin: 0 0.7rem;
|
||||
width: 3.2rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
margin-left: 0.4rem !important;
|
||||
|
@ -543,12 +548,13 @@
|
|||
a:link {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: none !important;
|
||||
background: none;
|
||||
|
||||
.icon {
|
||||
display: none;
|
||||
|
@ -571,6 +577,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.userControlItemSearch {
|
||||
margin: 0 1rem 0 2.2rem;
|
||||
}
|
||||
|
||||
.userControlItemUserpic {
|
||||
height: 3.2rem;
|
||||
width: 3.2rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.userControlItemInbox,
|
||||
.userControlItemSearch {
|
||||
@include media-breakpoint-down(sm) {
|
||||
|
@ -579,7 +599,16 @@
|
|||
}
|
||||
|
||||
.userControlItemVerbose {
|
||||
margin-left: 0.9em !important;
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
height: 3.2rem;
|
||||
margin-left: 1rem !important;
|
||||
width: 3.2rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0 !important;
|
||||
|
@ -590,6 +619,7 @@
|
|||
|
||||
@include media-breakpoint-up(xl) {
|
||||
background: none;
|
||||
margin-left: 0.8rem !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
@ -611,10 +641,14 @@
|
|||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
margin-left: 0.5em !important;
|
||||
margin-right: 0.5em;
|
||||
margin-left: 3rem !important;
|
||||
margin-right: 0;
|
||||
width: auto;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: none !important;
|
||||
}
|
||||
|
@ -629,6 +663,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
a:link,
|
||||
a:visited,
|
||||
button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
border-radius: 2rem;
|
||||
box-shadow: inset 0 0 0 2px #000;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--link-hover-background);
|
||||
|
||||
&,
|
||||
.textLabel {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.iconHover {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
@ -636,27 +701,6 @@
|
|||
a::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
button:hover {
|
||||
.icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.iconHover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.textLabel {
|
||||
color: var(--link-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
.textLabel {
|
||||
background-color: var(--link-hover-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subnavigation {
|
||||
|
@ -746,3 +790,65 @@
|
|||
position: relative;
|
||||
top: 0.15em;
|
||||
}
|
||||
|
||||
.editorPopup {
|
||||
border: 1px solid rgb(0 0 0 / 15%) !important;
|
||||
border-radius: 1.6rem;
|
||||
line-height: 1.3;
|
||||
min-width: 28rem;
|
||||
padding: 1.6rem !important;
|
||||
}
|
||||
|
||||
.editorModePopupOpener {
|
||||
display: inline-block;
|
||||
margin-right: 2rem;
|
||||
position: relative;
|
||||
text-align: right;
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.editorModePopupOpenerIcon {
|
||||
height: 2rem;
|
||||
left: 100%;
|
||||
margin-left: 0.2em;
|
||||
top: 0;
|
||||
transform: rotate(90deg);
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.editorModesList {
|
||||
li {
|
||||
cursor: pointer;
|
||||
margin-bottom: 1.6rem;
|
||||
padding-left: 3rem !important;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.editorModesSelected {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.editorModeTitle {
|
||||
color: #000;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.editorModeDescription {
|
||||
color: #696969;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.editorModeIcon {
|
||||
height: 2.4rem;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: -0.2em;
|
||||
width: 2.4rem;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
|||
import { ProfilePopup } from './ProfilePopup'
|
||||
|
||||
import { useSnackbar } from '../../context/snackbar'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
import { VotersList } from '../_shared/VotersList'
|
||||
import styles from './Header/Header.module.scss'
|
||||
|
||||
type Props = {
|
||||
|
@ -51,7 +53,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings')
|
||||
const isNotificationsVisible = createMemo(() => isAuthenticated() && !isEditorPage())
|
||||
const isSaveButtonVisible = createMemo(() => isAuthenticated() && isEditorPage())
|
||||
const isCreatePostButtonVisible = createMemo(() => isAuthenticated() && !isEditorPage())
|
||||
const isCreatePostButtonVisible = createMemo(() => !isEditorPage())
|
||||
const isAuthenticatedControlsVisible = createMemo(
|
||||
() => isAuthenticated() && session()?.user?.email_verified,
|
||||
)
|
||||
|
@ -65,6 +67,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
}
|
||||
|
||||
const [width, setWidth] = createSignal(0)
|
||||
const [editorMode, setEditorMode] = createSignal(t('Editing'))
|
||||
|
||||
onMount(() => {
|
||||
const handleResize = () => setWidth(window.innerWidth)
|
||||
|
@ -106,7 +109,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
<Show when={isSessionLoaded()} keyed={true}>
|
||||
<div class={clsx('col-auto col-lg-7', styles.usernav)}>
|
||||
<div class={styles.userControl}>
|
||||
<Show when={isCreatePostButtonVisible()}>
|
||||
<Show when={isCreatePostButtonVisible() && isAuthenticated()}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||
<a href={getPagePath(router, 'create')}>
|
||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||
|
@ -117,7 +120,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
</Show>
|
||||
|
||||
<Show when={!isSaveButtonVisible()}>
|
||||
<div class={styles.userControlItem}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemSearch)}>
|
||||
<a href="?m=search">
|
||||
<Icon name="search" class={styles.icon} />
|
||||
<Icon name="search" class={clsx(styles.icon, styles.iconHover)} />
|
||||
|
@ -143,13 +146,47 @@ export const HeaderAuth = (props: Props) => {
|
|||
</Show>
|
||||
|
||||
<Show when={isSaveButtonVisible()}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||
{renderIconedButton({
|
||||
value: t('Save'),
|
||||
icon: 'save',
|
||||
action: handleSaveButtonClick,
|
||||
})}
|
||||
</div>
|
||||
<Popup
|
||||
trigger={
|
||||
<span class={styles.editorModePopupOpener}>
|
||||
<Icon name="swiper-r-arr" class={styles.editorModePopupOpenerIcon} />
|
||||
{editorMode()}
|
||||
</span>
|
||||
}
|
||||
variant="bordered"
|
||||
popupCssClass={styles.editorPopup}
|
||||
>
|
||||
<ul class={clsx('nodash', styles.editorModesList)}>
|
||||
<li
|
||||
class={clsx({ [styles.editorModesSelected]: editorMode() === t('Preview') })}
|
||||
onClick={() => setEditorMode(t('Preview'))}
|
||||
>
|
||||
<Icon name="eye" class={styles.editorModeIcon} />
|
||||
<div class={styles.editorModeTitle}>{t('Preview')}</div>
|
||||
<div class={styles.editorModeDescription}>
|
||||
Посмотрите, как материал будет выглядеть при публикации
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({ [styles.editorModesSelected]: editorMode() === t('Editing') })}
|
||||
onClick={() => setEditorMode(t('Editing'))}
|
||||
>
|
||||
<Icon name="pencil-outline" class={styles.editorModeIcon} />
|
||||
<div class={styles.editorModeTitle}>{t('Editing')}</div>
|
||||
<div class={styles.editorModeDescription}>Изменяйте текст напрямую в редакторе</div>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({ [styles.editorModesSelected]: editorMode() === t('Commenting') })}
|
||||
onClick={() => setEditorMode(t('Commenting'))}
|
||||
>
|
||||
<Icon name="comment" class={styles.editorModeIcon} />
|
||||
<div class={styles.editorModeTitle}>{t('Commenting')}</div>
|
||||
<div class={styles.editorModeDescription}>
|
||||
Предлагайте правки и комментарии, чтобы сделать материал лучше
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</Popup>
|
||||
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||
{renderIconedButton({
|
||||
|
@ -159,12 +196,18 @@ export const HeaderAuth = (props: Props) => {
|
|||
})}
|
||||
</div>
|
||||
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||
<div
|
||||
class={clsx(
|
||||
styles.userControlItem,
|
||||
styles.settingsControlContainer,
|
||||
styles.userControlItemVerbose,
|
||||
)}
|
||||
>
|
||||
<Popover content={t('Settings')}>
|
||||
{(ref) => (
|
||||
<Button
|
||||
ref={ref}
|
||||
value={<Icon name="burger" />}
|
||||
value={<Icon name="ellipsis" />}
|
||||
variant={'light'}
|
||||
onClick={handleBurgerButtonClick}
|
||||
class={styles.settingsControl}
|
||||
|
@ -173,6 +216,17 @@ export const HeaderAuth = (props: Props) => {
|
|||
</Popover>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={isCreatePostButtonVisible() && !isAuthenticated()}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||
<a href={getPagePath(router, 'create')}>
|
||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||
<Icon name="pencil" class={styles.icon} />
|
||||
<Icon name="pencil" class={clsx(styles.icon, styles.iconHover)} />
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={isAuthenticatedControlsVisible()}
|
||||
fallback={
|
||||
|
@ -195,28 +249,31 @@ export const HeaderAuth = (props: Props) => {
|
|||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
<ProfilePopup
|
||||
onVisibilityChange={(isVisible) => {
|
||||
props.setIsProfilePopupVisible(isVisible)
|
||||
}}
|
||||
containerCssClass={styles.control}
|
||||
trigger={
|
||||
<div class={styles.userControlItem}>
|
||||
<button class={styles.button}>
|
||||
<div classList={{ entered: page().path === `/${author()?.slug}` }}>
|
||||
<Userpic
|
||||
size={'M'}
|
||||
name={author()?.name}
|
||||
userpic={author()?.pic}
|
||||
class={styles.userpic}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={isAuthenticated()}>
|
||||
<ProfilePopup
|
||||
onVisibilityChange={(isVisible) => {
|
||||
props.setIsProfilePopupVisible(isVisible)
|
||||
}}
|
||||
containerCssClass={styles.control}
|
||||
trigger={
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemUserpic)}>
|
||||
<button class={styles.button}>
|
||||
<div classList={{ entered: page().path === `/${author()?.slug}` }}>
|
||||
<Userpic
|
||||
size={'L'}
|
||||
name={author()?.name}
|
||||
userpic={author()?.pic}
|
||||
class={styles.userpic}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</ShowOnlyOnClient>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.snackbar {
|
||||
min-height: 2px;
|
||||
background-color: var(--default-color);
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
import { createFileUploader } from '@solid-primitives/upload'
|
||||
import { clsx } from 'clsx'
|
||||
import deepEqual from 'fast-deep-equal'
|
||||
import { For, Match, Show, Switch, createEffect, createSignal, lazy, onCleanup, onMount } from 'solid-js'
|
||||
import {
|
||||
For,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
createEffect,
|
||||
createSignal,
|
||||
lazy,
|
||||
on,
|
||||
onCleanup,
|
||||
onMount,
|
||||
} from 'solid-js'
|
||||
import { createStore } from 'solid-js/store'
|
||||
|
||||
import { useConfirm } from '../../context/confirm'
|
||||
|
@ -33,6 +44,7 @@ export const ProfileSettings = () => {
|
|||
const { t } = useLocalize()
|
||||
const [prevForm, setPrevForm] = createStore({})
|
||||
const [isFormInitialized, setIsFormInitialized] = createSignal(false)
|
||||
const [isSaving, setIsSaving] = createSignal(false)
|
||||
const [social, setSocial] = createSignal([])
|
||||
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
|
||||
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
||||
|
@ -70,16 +82,20 @@ export const ProfileSettings = () => {
|
|||
|
||||
const handleSubmit = async (event: Event) => {
|
||||
event.preventDefault()
|
||||
setIsSaving(true)
|
||||
if (nameInputRef.current.value.length === 0) {
|
||||
setNameError(t('Required'))
|
||||
nameInputRef.current.focus()
|
||||
setIsSaving(false)
|
||||
return
|
||||
}
|
||||
if (slugInputRef.current.value.length === 0) {
|
||||
setSlugError(t('Required'))
|
||||
slugInputRef.current.focus()
|
||||
setIsSaving(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await submit(form)
|
||||
setPrevForm(clone(form))
|
||||
|
@ -91,6 +107,8 @@ export const ProfileSettings = () => {
|
|||
return
|
||||
}
|
||||
showSnackbar({ type: 'error', body: t('Error') })
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
|
||||
await loadAuthor() // renews author's profile
|
||||
|
@ -149,12 +167,15 @@ export const ProfileSettings = () => {
|
|||
onCleanup(() => window.removeEventListener('beforeunload', handleBeforeUnload))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!deepEqual(form, prevForm)) {
|
||||
setIsFloatingPanelVisible(true)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => deepEqual(form, prevForm),
|
||||
() => {
|
||||
setIsFloatingPanelVisible(!deepEqual(form, prevForm))
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
const handleDeleteSocialLink = (link) => {
|
||||
updateFormField('links', link, true)
|
||||
}
|
||||
|
@ -359,7 +380,12 @@ export const ProfileSettings = () => {
|
|||
}
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
<Button onClick={handleSubmit} variant="primary" value={t('Save settings')} />
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="primary"
|
||||
disabled={isSaving()}
|
||||
value={isSaving() ? t('Saving...') : t('Save settings')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -40,7 +40,7 @@ export const FullTopic = (props: Props) => {
|
|||
return (
|
||||
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
|
||||
<h1>#{props.topic?.title}</h1>
|
||||
<p>{props.topic?.body}</p>
|
||||
<p innerHTML={props.topic?.body} />
|
||||
<div class={clsx(styles.topicActions)}>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
|
|
@ -79,7 +79,7 @@ export const TopicBadge = (props: Props) => {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<div class={clsx('text-truncate', styles.description)}>{props.topic.body}</div>
|
||||
<div innerHTML={props.topic.body} class={clsx('text-truncate', styles.description)} />
|
||||
</Show>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -29,8 +29,6 @@ import stylesArticle from '../../Article/Article.module.scss'
|
|||
import styles from './Author.module.scss'
|
||||
|
||||
type Props = {
|
||||
shouts: Shout[]
|
||||
author: Author
|
||||
authorSlug: string
|
||||
}
|
||||
export const PRERENDERED_ARTICLES_COUNT = 12
|
||||
|
@ -38,7 +36,7 @@ const LOAD_MORE_PAGE_SIZE = 9
|
|||
|
||||
export const AuthorView = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const { subscriptions, followers } = useFollowing()
|
||||
const { subscriptions, followers, loadSubscriptions } = useFollowing()
|
||||
const { session } = useSession()
|
||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
||||
|
@ -210,10 +208,10 @@ export const AuthorView = (props: Props) => {
|
|||
</ul>
|
||||
</div>
|
||||
<div class={clsx('col-md-8', styles.additionalControls)}>
|
||||
<Show when={props.author?.stat?.rating || props.author?.stat?.rating === 0}>
|
||||
<Show when={author()?.stat?.rating || author()?.stat?.rating === 0}>
|
||||
<div class={styles.ratingContainer}>
|
||||
{t('All posts rating')}
|
||||
<AuthorShoutsRating author={props.author} class={styles.ratingControl} />
|
||||
<AuthorShoutsRating author={author()} class={styles.ratingControl} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -24,34 +24,31 @@ type Props = {
|
|||
layout: LayoutType
|
||||
}
|
||||
|
||||
export const PRERENDERED_ARTICLES_COUNT = 37
|
||||
const LOAD_MORE_PAGE_SIZE = 11
|
||||
export const PRERENDERED_ARTICLES_COUNT = 36
|
||||
const LOAD_MORE_PAGE_SIZE = 12
|
||||
|
||||
export const Expo = (props: Props) => {
|
||||
const [isLoaded, setIsLoaded] = createSignal<boolean>(Boolean(props.shouts))
|
||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||
|
||||
const [randomTopArticles, setRandomTopArticles] = createSignal<Shout[]>([])
|
||||
const [randomTopMonthArticles, setRandomTopMonthArticles] = createSignal<Shout[]>([])
|
||||
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
|
||||
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
||||
|
||||
const { t } = useLocalize()
|
||||
|
||||
// const { sortedArticles } = useArticlesStore({
|
||||
// shouts: isLoaded() ? props.shouts : [],
|
||||
// })
|
||||
const { sortedArticles } = useArticlesStore({
|
||||
shouts: props.shouts || [],
|
||||
shouts: isLoaded() ? props.shouts : [],
|
||||
layout: props.layout,
|
||||
})
|
||||
|
||||
const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => {
|
||||
const filters = { featured: true, ...additionalFilters }
|
||||
const filters = { ...additionalFilters }
|
||||
|
||||
if (!filters.layouts) filters.layouts = []
|
||||
if (props.layout) {
|
||||
filters.layouts.push(props.layout)
|
||||
} else {
|
||||
filters.layouts.push('article')
|
||||
filters.layouts.push('audio', 'video', 'image', 'literature')
|
||||
}
|
||||
|
||||
return filters
|
||||
|
@ -80,13 +77,12 @@ export const Expo = (props: Props) => {
|
|||
|
||||
const loadRandomTopArticles = async () => {
|
||||
const options: LoadShoutsOptions = {
|
||||
filters: getLoadShoutsFilters(),
|
||||
filters: { ...getLoadShoutsFilters(), featured: true },
|
||||
limit: 10,
|
||||
random_limit: 100,
|
||||
}
|
||||
|
||||
const result = await apiClient.getRandomTopShouts({ options })
|
||||
setRandomTopArticles(result)
|
||||
setFavoriteTopArticles(result)
|
||||
}
|
||||
|
||||
const loadRandomTopMonthArticles = async () => {
|
||||
|
@ -94,19 +90,15 @@ export const Expo = (props: Props) => {
|
|||
const after = getUnixtime(new Date(now.setMonth(now.getMonth() - 1)))
|
||||
|
||||
const options: LoadShoutsOptions = {
|
||||
filters: getLoadShoutsFilters({ after }),
|
||||
filters: { ...getLoadShoutsFilters({ after }), reacted: true },
|
||||
limit: 10,
|
||||
random_limit: 10,
|
||||
}
|
||||
|
||||
const result = await apiClient.getRandomTopShouts({ options })
|
||||
setRandomTopMonthArticles(result)
|
||||
setReactedTopMonthArticles(result)
|
||||
}
|
||||
|
||||
const pages = createMemo<Shout[][]>(() =>
|
||||
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE),
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
if (isLoaded()) {
|
||||
return
|
||||
|
@ -130,8 +122,8 @@ export const Expo = (props: Props) => {
|
|||
() => props.layout,
|
||||
() => {
|
||||
resetSortedArticles()
|
||||
setRandomTopArticles([])
|
||||
setRandomTopMonthArticles([])
|
||||
setFavoriteTopArticles([])
|
||||
setReactedTopMonthArticles([])
|
||||
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
|
||||
loadRandomTopArticles()
|
||||
loadRandomTopMonthArticles()
|
||||
|
@ -202,7 +194,7 @@ export const Expo = (props: Props) => {
|
|||
</li>
|
||||
</ul>
|
||||
<div class="row">
|
||||
<For each={sortedArticles().slice(0, PRERENDERED_ARTICLES_COUNT / 2)}>
|
||||
<For each={sortedArticles().slice(0, LOAD_MORE_PAGE_SIZE)}>
|
||||
{(shout) => (
|
||||
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
||||
<ArticleCard
|
||||
|
@ -214,10 +206,10 @@ export const Expo = (props: Props) => {
|
|||
</div>
|
||||
)}
|
||||
</For>
|
||||
<Show when={randomTopMonthArticles()?.length > 0} keyed={true}>
|
||||
<ArticleCardSwiper title={t('Top month articles')} slides={randomTopMonthArticles()} />
|
||||
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
|
||||
<ArticleCardSwiper title={t('Top month articles')} slides={reactedTopMonthArticles()} />
|
||||
</Show>
|
||||
<For each={sortedArticles().slice(PRERENDERED_ARTICLES_COUNT / 2, PRERENDERED_ARTICLES_COUNT)}>
|
||||
<For each={sortedArticles().slice(LOAD_MORE_PAGE_SIZE, LOAD_MORE_PAGE_SIZE * 2)}>
|
||||
{(shout) => (
|
||||
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
||||
<ArticleCard
|
||||
|
@ -229,23 +221,19 @@ export const Expo = (props: Props) => {
|
|||
</div>
|
||||
)}
|
||||
</For>
|
||||
<Show when={randomTopArticles()?.length > 0} keyed={true}>
|
||||
<ArticleCardSwiper title={t('Favorite')} slides={randomTopArticles()} />
|
||||
<Show when={favoriteTopArticles()?.length > 0} keyed={true}>
|
||||
<ArticleCardSwiper title={t('Favorite')} slides={favoriteTopArticles()} />
|
||||
</Show>
|
||||
<For each={pages()}>
|
||||
{(page) => (
|
||||
<For each={page}>
|
||||
{(shout) => (
|
||||
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
||||
<ArticleCard
|
||||
article={shout}
|
||||
settings={{ nodate: true, nosubtitle: true, noAuthorLink: true }}
|
||||
desktopCoverSize="XS"
|
||||
withAspectRatio={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<For each={sortedArticles().slice(LOAD_MORE_PAGE_SIZE * 2)}>
|
||||
{(shout) => (
|
||||
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
||||
<ArticleCard
|
||||
article={shout}
|
||||
settings={{ nodate: true, nosubtitle: true, noAuthorLink: true }}
|
||||
desktopCoverSize="XS"
|
||||
withAspectRatio={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { Shout, Topic } from '../../graphql/schema/core.gen'
|
||||
import { LoadShoutsOptions, Shout, Topic } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { Meta } from '@solidjs/meta'
|
||||
import { clsx } from 'clsx'
|
||||
import { For, Show, createEffect, createMemo, createSignal, onMount } from 'solid-js'
|
||||
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useRouter } from '../../stores/router'
|
||||
|
@ -21,7 +21,9 @@ import { Row3 } from '../Feed/Row3'
|
|||
import { FullTopic } from '../Topic/Full'
|
||||
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
|
||||
|
||||
import { apiClient } from '../../graphql/client/core'
|
||||
import styles from '../../styles/Topic.module.scss'
|
||||
import { getUnixtime } from '../../utils/getServerDate'
|
||||
|
||||
type TopicsPageSearchParams = {
|
||||
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
|
||||
|
@ -43,14 +45,56 @@ export const TopicView = (props: Props) => {
|
|||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
||||
const { authorsByTopic } = useAuthorsStore()
|
||||
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
|
||||
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
||||
|
||||
const [topic, setTopic] = createSignal<Topic>()
|
||||
|
||||
createEffect(() => {
|
||||
const topics = topicEntities()
|
||||
if (props.topicSlug && !topic() && topics) {
|
||||
setTopic(topics[props.topicSlug])
|
||||
}
|
||||
})
|
||||
|
||||
const loadFavoriteTopArticles = async (topic: string) => {
|
||||
const options: LoadShoutsOptions = {
|
||||
filters: { featured: true, topic: topic },
|
||||
limit: 10,
|
||||
random_limit: 100,
|
||||
}
|
||||
const result = await apiClient.getRandomTopShouts({ options })
|
||||
setFavoriteTopArticles(result)
|
||||
}
|
||||
|
||||
const loadReactedTopMonthArticles = async (topic: string) => {
|
||||
const now = new Date()
|
||||
const after = getUnixtime(new Date(now.setMonth(now.getMonth() - 1)))
|
||||
|
||||
const options: LoadShoutsOptions = {
|
||||
filters: { after: after, featured: true, topic: topic },
|
||||
limit: 10,
|
||||
random_limit: 10,
|
||||
}
|
||||
|
||||
const result = await apiClient.getRandomTopShouts({ options })
|
||||
|
||||
setReactedTopMonthArticles(result)
|
||||
}
|
||||
|
||||
const loadRandom = () => {
|
||||
loadFavoriteTopArticles(topic()?.slug)
|
||||
loadReactedTopMonthArticles(topic()?.slug)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => topic(),
|
||||
() => loadRandom(),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const title = createMemo(
|
||||
() =>
|
||||
`#${capitalize(
|
||||
|
@ -75,6 +119,7 @@ export const TopicView = (props: Props) => {
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
loadRandom()
|
||||
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
||||
loadMore()
|
||||
}
|
||||
|
@ -170,9 +215,9 @@ export const TopicView = (props: Props) => {
|
|||
beside={sortedArticles()[4]}
|
||||
wrapper={'author'}
|
||||
/>
|
||||
|
||||
<ArticleCardSwiper title={title()} slides={sortedArticles().slice(5, 11)} />
|
||||
|
||||
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
|
||||
<ArticleCardSwiper title={t('Top month articles')} slides={reactedTopMonthArticles()} />
|
||||
</Show>
|
||||
<Beside
|
||||
beside={sortedArticles()[12]}
|
||||
title={t('Top viewed')}
|
||||
|
@ -183,8 +228,10 @@ export const TopicView = (props: Props) => {
|
|||
<Row2 articles={sortedArticles().slice(13, 15)} isEqual={true} />
|
||||
<Row1 article={sortedArticles()[15]} />
|
||||
|
||||
<Show when={favoriteTopArticles()?.length > 0} keyed={true}>
|
||||
<ArticleCardSwiper title={t('Favorite')} slides={favoriteTopArticles()} />
|
||||
</Show>
|
||||
<Show when={sortedArticles().length > 15}>
|
||||
<ArticleCardSwiper slides={sortedArticles().slice(16, 22)} />
|
||||
<Row3 articles={sortedArticles().slice(23, 26)} />
|
||||
<Row2 articles={sortedArticles().slice(26, 28)} />
|
||||
</Show>
|
||||
|
|
|
@ -10,18 +10,23 @@
|
|||
}
|
||||
|
||||
.notificationsCounter {
|
||||
background-color: #d00820;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 2em;
|
||||
align-items: center;
|
||||
background-color: #E84500;
|
||||
border-radius: 0.8rem;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
height: 1.6em;
|
||||
left: 1.1em;
|
||||
line-height: 1.25em;
|
||||
height: 2.2rem;
|
||||
justify-content: center;
|
||||
left: 1.6rem;
|
||||
min-width: 2.2rem;
|
||||
padding: 0 0.25em;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: -0.5rem;
|
||||
min-width: 1.5em;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
left: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,14 @@ import { Accessor, JSX, createContext, createEffect, createSignal, useContext }
|
|||
import { createStore } from 'solid-js/store'
|
||||
|
||||
import { apiClient } from '../graphql/client/core'
|
||||
import { Author, AuthorFollows, FollowingEntity } from '../graphql/schema/core.gen'
|
||||
import { Author, AuthorFollowsResult, FollowingEntity } from '../graphql/schema/core.gen'
|
||||
|
||||
import { useSession } from './session'
|
||||
|
||||
interface FollowingContextType {
|
||||
loading: Accessor<boolean>
|
||||
followers: Accessor<Array<Author>>
|
||||
subscriptions: AuthorFollows
|
||||
subscriptions: AuthorFollowsResult
|
||||
setSubscriptions: (subscriptions: AuthorFollows) => void
|
||||
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
||||
loadSubscriptions: () => void
|
||||
|
@ -24,7 +24,7 @@ export function useFollowing() {
|
|||
return useContext(FollowingContext)
|
||||
}
|
||||
|
||||
const EMPTY_SUBSCRIPTIONS: AuthorFollows = {
|
||||
const EMPTY_SUBSCRIPTIONS: AuthorFollowsResult = {
|
||||
topics: [],
|
||||
authors: [],
|
||||
communities: [],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type {
|
||||
Author,
|
||||
AuthorFollows,
|
||||
AuthorFollowsResult,
|
||||
CommonResult,
|
||||
FollowingEntity,
|
||||
LoadShoutsOptions,
|
||||
|
@ -134,7 +134,7 @@ export const apiClient = {
|
|||
slug?: string
|
||||
author_id?: number
|
||||
user?: string
|
||||
}): Promise<AuthorFollows> => {
|
||||
}): Promise<AuthorFollowsResult> => {
|
||||
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
|
||||
return response.data.get_author_follows
|
||||
},
|
||||
|
|
|
@ -56,17 +56,11 @@ export const AuthorPage = (props: PageProps) => {
|
|||
|
||||
onCleanup(() => resetSortedArticles())
|
||||
|
||||
const usePrerenderedData = props.author?.slug === slug()
|
||||
|
||||
return (
|
||||
<PageLayout title={props.seo?.title || t('Discours')}>
|
||||
<ReactionsProvider>
|
||||
<Show when={isLoaded()} fallback={<Loading />}>
|
||||
<AuthorView
|
||||
author={usePrerenderedData ? props.author : null}
|
||||
shouts={usePrerenderedData ? props.authorShouts : null}
|
||||
authorSlug={slug()}
|
||||
/>
|
||||
<AuthorView authorSlug={slug()} />
|
||||
</Show>
|
||||
</ReactionsProvider>
|
||||
</PageLayout>
|
||||
|
|
|
@ -622,6 +622,10 @@ figure {
|
|||
margin-bottom: 0.6em;
|
||||
white-space: nowrap;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-right: 2.4rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user