Merge branch 'dev' into hotfix/correct-following-status
This commit is contained in:
commit
e38d3b39b7
2
.github/workflows/node-ci.yml
vendored
2
.github/workflows/node-ci.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
run: npm run typecheck
|
run: npm run typecheck
|
||||||
|
|
||||||
- name: Lint with Biome
|
- name: Lint with Biome
|
||||||
run: npx biome ci .
|
run: npm run check:code
|
||||||
|
|
||||||
- name: Lint styles
|
- name: Lint styles
|
||||||
run: npm run lint:styles
|
run: npm run lint:styles
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
||||||
"files": {
|
"files": {
|
||||||
"include": ["*.tsx", "*.ts", "*.js", "*.json"],
|
"include": ["*.tsx", "*.ts", "*.js", "*.json"],
|
||||||
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.d.ts"]
|
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.gen.ts", "*.d.ts"]
|
||||||
},
|
},
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"defaultBranch": "dev",
|
"defaultBranch": "dev",
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
"hygen": "HYGEN_TMPLS=gen hygen",
|
"hygen": "HYGEN_TMPLS=gen hygen",
|
||||||
"postinstall": "npm run codegen && npx patch-package",
|
"postinstall": "npm run codegen && npx patch-package",
|
||||||
"check:code": "npx @biomejs/biome check src --log-kind=compact --verbose",
|
"check:code": "npx @biomejs/biome check src --log-kind=compact --verbose",
|
||||||
"check:code:fix": "npx @biomejs/biome check src --log-kind=compact --verbose --apply-unsafe",
|
"check:code:fix": "npx @biomejs/biome check src --log-kind=compact",
|
||||||
"lint": "npm run lint:code && stylelint **/*.{scss,css}",
|
"lint": "npm run lint:code && stylelint **/*.{scss,css}",
|
||||||
"lint:code": "npx @biomejs/biome lint src --log-kind=compact --verbose",
|
"lint:code": "npx @biomejs/biome lint src --log-kind=compact --verbose",
|
||||||
"lint:code:fix": "npx @biomejs/biome lint src --apply-unsafe --log-kind=compact --verbose",
|
"lint:code:fix": "npx @biomejs/biome lint src --apply-unsafe --log-kind=compact --verbose",
|
||||||
|
|
|
@ -83,6 +83,7 @@
|
||||||
"Coming soon": "Coming soon",
|
"Coming soon": "Coming soon",
|
||||||
"Comment successfully deleted": "Comment successfully deleted",
|
"Comment successfully deleted": "Comment successfully deleted",
|
||||||
"Commentator": "Commentator",
|
"Commentator": "Commentator",
|
||||||
|
"Commenting": "Commenting",
|
||||||
"Comments": "Comments",
|
"Comments": "Comments",
|
||||||
"CommentsWithCount": "{count, plural, =0 {{count} comments} one {{count} comment} few {{count} comments} other {{count} comments}}",
|
"CommentsWithCount": "{count, plural, =0 {{count} comments} one {{count} comment} few {{count} comments} other {{count} comments}}",
|
||||||
"Communities": "Communities",
|
"Communities": "Communities",
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
"Comment successfully deleted": "Комментарий успешно удален",
|
"Comment successfully deleted": "Комментарий успешно удален",
|
||||||
"Comment": "Комментировать",
|
"Comment": "Комментировать",
|
||||||
"Commentator": "Комментатор",
|
"Commentator": "Комментатор",
|
||||||
|
"Commenting": "Комментирование",
|
||||||
"Comments": "Комментарии",
|
"Comments": "Комментарии",
|
||||||
"CommentsWithCount": "{count, plural, =0 {{count} комментариев} one {{count} комментарий} few {{count} комментария} other {{count} комментариев}}",
|
"CommentsWithCount": "{count, plural, =0 {{count} комментариев} one {{count} комментарий} few {{count} комментария} other {{count} комментариев}}",
|
||||||
"Communities": "Сообщества",
|
"Communities": "Сообщества",
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Disallow: /
|
||||||
|
|
|
@ -22,6 +22,7 @@ img {
|
||||||
.articleContent {
|
.articleContent {
|
||||||
img:not([data-disable-lightbox='true']) {
|
img:not([data-disable-lightbox='true']) {
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ type IframeSize = {
|
||||||
export type ArticlePageSearchParams = {
|
export type ArticlePageSearchParams = {
|
||||||
scrollTo: 'comments'
|
scrollTo: 'comments'
|
||||||
commentId: string
|
commentId: string
|
||||||
|
slide?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollTo = (el: HTMLElement) => {
|
const scrollTo = (el: HTMLElement) => {
|
||||||
|
@ -329,7 +330,7 @@ export const FullArticle = (props: Props) => {
|
||||||
width: 1200,
|
width: 1200,
|
||||||
})
|
})
|
||||||
|
|
||||||
const description = getDescription(props.article.description || body())
|
const description = getDescription(props.article.description || body() || media()[0]?.body)
|
||||||
const ogTitle = props.article.title
|
const ogTitle = props.article.title
|
||||||
const keywords = getKeywords(props.article)
|
const keywords = getKeywords(props.article)
|
||||||
const shareUrl = getShareUrl({ pathname: `/${props.article.slug}` })
|
const shareUrl = getShareUrl({ pathname: `/${props.article.slug}` })
|
||||||
|
|
|
@ -135,7 +135,9 @@ export const AuthorCard = (props: Props) => {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<div class={styles.subscribersCounter}>
|
<div class={styles.subscribersCounter}>
|
||||||
{t('SubscriberWithCount', { count: props.followers.length ?? 0 })}
|
{t('SubscriberWithCount', {
|
||||||
|
count: props.followers.length ?? 0,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -170,7 +172,9 @@ export const AuthorCard = (props: Props) => {
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
<div class={styles.subscribersCounter}>
|
<div class={styles.subscribersCounter}>
|
||||||
{t('SubscriptionWithCount', { count: props?.following.length ?? 0 })}
|
{t('SubscriptionWithCount', {
|
||||||
|
count: props?.following.length ?? 0,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -236,7 +240,9 @@ export const AuthorCard = (props: Props) => {
|
||||||
title={props.author.name}
|
title={props.author.name}
|
||||||
description={props.author.bio}
|
description={props.author.bio}
|
||||||
imageUrl={props.author.pic}
|
imageUrl={props.author.pic}
|
||||||
shareUrl={getShareUrl({ pathname: `/author/${props.author.slug}` })}
|
shareUrl={getShareUrl({
|
||||||
|
pathname: `/author/${props.author.slug}`,
|
||||||
|
})}
|
||||||
trigger={<Button variant="secondary" value={t('Share')} />}
|
trigger={<Button variant="secondary" value={t('Share')} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -264,13 +270,21 @@ export const AuthorCard = (props: Props) => {
|
||||||
<>
|
<>
|
||||||
<h2>{t('Subscriptions')}</h2>
|
<h2>{t('Subscriptions')}</h2>
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'all' })}>
|
<li
|
||||||
|
class={clsx({
|
||||||
|
'view-switcher__item--selected': subscriptionFilter() === 'all',
|
||||||
|
})}
|
||||||
|
>
|
||||||
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
||||||
{t('All')}
|
{t('All')}
|
||||||
</button>
|
</button>
|
||||||
<span class="view-switcher__counter">{props.following.length}</span>
|
<span class="view-switcher__counter">{props.following.length}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'authors' })}>
|
<li
|
||||||
|
class={clsx({
|
||||||
|
'view-switcher__item--selected': subscriptionFilter() === 'authors',
|
||||||
|
})}
|
||||||
|
>
|
||||||
<button type="button" onClick={() => setSubscriptionFilter('authors')}>
|
<button type="button" onClick={() => setSubscriptionFilter('authors')}>
|
||||||
{t('Authors')}
|
{t('Authors')}
|
||||||
</button>
|
</button>
|
||||||
|
@ -278,7 +292,11 @@ export const AuthorCard = (props: Props) => {
|
||||||
{props.following.filter((s) => 'name' in s).length}
|
{props.following.filter((s) => 'name' in s).length}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'topics' })}>
|
<li
|
||||||
|
class={clsx({
|
||||||
|
'view-switcher__item--selected': subscriptionFilter() === 'topics',
|
||||||
|
})}
|
||||||
|
>
|
||||||
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
||||||
{t('Topics')}
|
{t('Topics')}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
z-index: 10003;
|
z-index: 10003;
|
||||||
|
|
||||||
.wide-container {
|
.wide-container {
|
||||||
background: #fff;
|
background: var(--background-color);
|
||||||
|
|
||||||
@include media-breakpoint-down(lg) {
|
@include media-breakpoint-down(lg) {
|
||||||
padding: 0 divide($container-padding-x, 2);
|
padding: 0 divide($container-padding-x, 2);
|
||||||
|
@ -114,6 +114,11 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainNavigationWrapper {
|
.mainNavigationWrapper {
|
||||||
|
@ -192,15 +197,8 @@
|
||||||
padding: divide($container-padding-x, 2) !important;
|
padding: divide($container-padding-x, 2) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
|
||||||
span,
|
|
||||||
button {
|
|
||||||
padding: 0 0.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.view-switcher) {
|
:global(.view-switcher) {
|
||||||
margin: 0 -0.5rem;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -299,9 +297,6 @@
|
||||||
.burgerContainer {
|
.burgerContainer {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
padding-left: 0;
|
|
||||||
|
|
||||||
// float: right;
|
|
||||||
|
|
||||||
@include media-breakpoint-up(sm) {
|
@include media-breakpoint-up(sm) {
|
||||||
padding-left: divide($container-padding-x, 2);
|
padding-left: divide($container-padding-x, 2);
|
||||||
|
@ -430,12 +425,15 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@include media-breakpoint-up(xl) {
|
@include media-breakpoint-up(xl) {
|
||||||
right: 2rem;
|
right: 9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control {
|
.control {
|
||||||
cursor: pointer;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 3.2rem;
|
||||||
|
margin: 0 0.6rem;
|
||||||
|
width: 3.2rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: none;
|
background: none;
|
||||||
|
@ -451,11 +449,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.control + .control {
|
.control + .control {
|
||||||
margin-left: 1.2rem;
|
margin: 0 0.6rem;
|
||||||
|
|
||||||
@include media-breakpoint-up(sm) {
|
|
||||||
margin-left: 2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@ -497,10 +491,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settingsControlContainer {
|
||||||
|
margin-left: 1rem !important;
|
||||||
|
margin-right: 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
.settingsControl {
|
.settingsControl {
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
padding: 0.8rem !important;
|
|
||||||
min-width: 4rem !important;
|
min-width: 4rem !important;
|
||||||
|
padding: 0.8rem !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--background-color-invert);
|
background: var(--background-color-invert);
|
||||||
|
@ -516,12 +515,18 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 2.4em;
|
height: 2.8rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: 0.3rem;
|
margin: 0 0.4rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: margin-left 0.3s;
|
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) {
|
@include media-breakpoint-down(sm) {
|
||||||
margin-left: 0.4rem !important;
|
margin-left: 0.4rem !important;
|
||||||
|
@ -543,12 +548,13 @@
|
||||||
a:link {
|
a:link {
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: auto;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: none !important;
|
background: none;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: none;
|
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,
|
.userControlItemInbox,
|
||||||
.userControlItemSearch {
|
.userControlItemSearch {
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
|
@ -579,7 +599,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.userControlItemVerbose {
|
.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 {
|
&:first-child {
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
|
@ -590,6 +619,7 @@
|
||||||
|
|
||||||
@include media-breakpoint-up(xl) {
|
@include media-breakpoint-up(xl) {
|
||||||
background: none;
|
background: none;
|
||||||
|
margin-left: 0.8rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
@ -611,10 +641,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(xl) {
|
@include media-breakpoint-up(xl) {
|
||||||
margin-left: 0.5em !important;
|
margin-left: 3rem !important;
|
||||||
margin-right: 0.5em;
|
margin-right: 0;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: none !important;
|
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 {
|
button {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
@ -636,27 +701,6 @@
|
||||||
a::before {
|
a::before {
|
||||||
display: none;
|
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 {
|
.subnavigation {
|
||||||
|
@ -746,3 +790,65 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0.15em;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -14,10 +14,9 @@ import { Icon } from '../_shared/Icon'
|
||||||
import { Popover } from '../_shared/Popover'
|
import { Popover } from '../_shared/Popover'
|
||||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||||
|
|
||||||
import { ProfilePopup } from './ProfilePopup'
|
import { Popup } from '../_shared/Popup'
|
||||||
|
|
||||||
import { useSnackbar } from '../../context/snackbar'
|
|
||||||
import styles from './Header/Header.module.scss'
|
import styles from './Header/Header.module.scss'
|
||||||
|
import { ProfilePopup } from './ProfilePopup'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setIsProfilePopupVisible: (value: boolean) => void
|
setIsProfilePopupVisible: (value: boolean) => void
|
||||||
|
@ -51,7 +50,7 @@ export const HeaderAuth = (props: Props) => {
|
||||||
const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings')
|
const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings')
|
||||||
const isNotificationsVisible = createMemo(() => isAuthenticated() && !isEditorPage())
|
const isNotificationsVisible = createMemo(() => isAuthenticated() && !isEditorPage())
|
||||||
const isSaveButtonVisible = createMemo(() => isAuthenticated() && isEditorPage())
|
const isSaveButtonVisible = createMemo(() => isAuthenticated() && isEditorPage())
|
||||||
const isCreatePostButtonVisible = createMemo(() => isAuthenticated() && !isEditorPage())
|
const isCreatePostButtonVisible = createMemo(() => !isEditorPage())
|
||||||
const isAuthenticatedControlsVisible = createMemo(
|
const isAuthenticatedControlsVisible = createMemo(
|
||||||
() => isAuthenticated() && session()?.user?.email_verified,
|
() => isAuthenticated() && session()?.user?.email_verified,
|
||||||
)
|
)
|
||||||
|
@ -65,6 +64,7 @@ export const HeaderAuth = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [width, setWidth] = createSignal(0)
|
const [width, setWidth] = createSignal(0)
|
||||||
|
const [editorMode, setEditorMode] = createSignal(t('Editing'))
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const handleResize = () => setWidth(window.innerWidth)
|
const handleResize = () => setWidth(window.innerWidth)
|
||||||
|
@ -106,7 +106,7 @@ export const HeaderAuth = (props: Props) => {
|
||||||
<Show when={isSessionLoaded()} keyed={true}>
|
<Show when={isSessionLoaded()} keyed={true}>
|
||||||
<div class={clsx('col-auto col-lg-7', styles.usernav)}>
|
<div class={clsx('col-auto col-lg-7', styles.usernav)}>
|
||||||
<div class={styles.userControl}>
|
<div class={styles.userControl}>
|
||||||
<Show when={isCreatePostButtonVisible()}>
|
<Show when={isCreatePostButtonVisible() && isAuthenticated()}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
<a href={getPagePath(router, 'create')}>
|
<a href={getPagePath(router, 'create')}>
|
||||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||||
|
@ -117,7 +117,7 @@ export const HeaderAuth = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!isSaveButtonVisible()}>
|
<Show when={!isSaveButtonVisible()}>
|
||||||
<div class={styles.userControlItem}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemSearch)}>
|
||||||
<a href="?m=search">
|
<a href="?m=search">
|
||||||
<Icon name="search" class={styles.icon} />
|
<Icon name="search" class={styles.icon} />
|
||||||
<Icon name="search" class={clsx(styles.icon, styles.iconHover)} />
|
<Icon name="search" class={clsx(styles.icon, styles.iconHover)} />
|
||||||
|
@ -143,13 +143,47 @@ export const HeaderAuth = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={isSaveButtonVisible()}>
|
<Show when={isSaveButtonVisible()}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
<Popup
|
||||||
{renderIconedButton({
|
trigger={
|
||||||
value: t('Save'),
|
<span class={styles.editorModePopupOpener}>
|
||||||
icon: 'save',
|
<Icon name="swiper-r-arr" class={styles.editorModePopupOpenerIcon} />
|
||||||
action: handleSaveButtonClick,
|
{editorMode()}
|
||||||
})}
|
</span>
|
||||||
</div>
|
}
|
||||||
|
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)}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
{renderIconedButton({
|
{renderIconedButton({
|
||||||
|
@ -159,12 +193,18 @@ export const HeaderAuth = (props: Props) => {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
<div
|
||||||
|
class={clsx(
|
||||||
|
styles.userControlItem,
|
||||||
|
styles.settingsControlContainer,
|
||||||
|
styles.userControlItemVerbose,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Popover content={t('Settings')}>
|
<Popover content={t('Settings')}>
|
||||||
{(ref) => (
|
{(ref) => (
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
value={<Icon name="burger" />}
|
value={<Icon name="ellipsis" />}
|
||||||
variant={'light'}
|
variant={'light'}
|
||||||
onClick={handleBurgerButtonClick}
|
onClick={handleBurgerButtonClick}
|
||||||
class={styles.settingsControl}
|
class={styles.settingsControl}
|
||||||
|
@ -173,16 +213,29 @@ export const HeaderAuth = (props: Props) => {
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</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
|
<Show
|
||||||
when={isAuthenticatedControlsVisible()}
|
when={isAuthenticatedControlsVisible()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
<Show when={!isAuthenticated()}>
|
||||||
<a href="?m=auth&mode=login">
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
||||||
<span class={styles.textLabel}>{t('Enter')}</span>
|
<a href="?m=auth&mode=login">
|
||||||
<Icon name="key" class={styles.icon} />
|
<span class={styles.textLabel}>{t('Enter')}</span>
|
||||||
{/*<Icon name="user-default" class={clsx(styles.icon, styles.iconHover)} />*/}
|
<Icon name="key" class={styles.icon} />
|
||||||
</a>
|
{/*<Icon name="user-default" class={clsx(styles.icon, styles.iconHover)} />*/}
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show when={!isSaveButtonVisible()}>
|
<Show when={!isSaveButtonVisible()}>
|
||||||
|
@ -195,28 +248,31 @@ export const HeaderAuth = (props: Props) => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</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>
|
</Show>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</ShowOnlyOnClient>
|
</ShowOnlyOnClient>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
.snackbar {
|
.snackbar {
|
||||||
min-height: 2px;
|
|
||||||
background-color: var(--default-color);
|
background-color: var(--default-color);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
import { createFileUploader } from '@solid-primitives/upload'
|
import { createFileUploader } from '@solid-primitives/upload'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import deepEqual from 'fast-deep-equal'
|
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 { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
import { useConfirm } from '../../context/confirm'
|
import { useConfirm } from '../../context/confirm'
|
||||||
|
@ -33,6 +44,7 @@ export const ProfileSettings = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const [prevForm, setPrevForm] = createStore({})
|
const [prevForm, setPrevForm] = createStore({})
|
||||||
const [isFormInitialized, setIsFormInitialized] = createSignal(false)
|
const [isFormInitialized, setIsFormInitialized] = createSignal(false)
|
||||||
|
const [isSaving, setIsSaving] = createSignal(false)
|
||||||
const [social, setSocial] = createSignal([])
|
const [social, setSocial] = createSignal([])
|
||||||
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
|
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
|
||||||
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
||||||
|
@ -70,16 +82,20 @@ export const ProfileSettings = () => {
|
||||||
|
|
||||||
const handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
setIsSaving(true)
|
||||||
if (nameInputRef.current.value.length === 0) {
|
if (nameInputRef.current.value.length === 0) {
|
||||||
setNameError(t('Required'))
|
setNameError(t('Required'))
|
||||||
nameInputRef.current.focus()
|
nameInputRef.current.focus()
|
||||||
|
setIsSaving(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (slugInputRef.current.value.length === 0) {
|
if (slugInputRef.current.value.length === 0) {
|
||||||
setSlugError(t('Required'))
|
setSlugError(t('Required'))
|
||||||
slugInputRef.current.focus()
|
slugInputRef.current.focus()
|
||||||
|
setIsSaving(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await submit(form)
|
await submit(form)
|
||||||
setPrevForm(clone(form))
|
setPrevForm(clone(form))
|
||||||
|
@ -91,6 +107,8 @@ export const ProfileSettings = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
showSnackbar({ type: 'error', body: t('Error') })
|
showSnackbar({ type: 'error', body: t('Error') })
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadAuthor() // renews author's profile
|
await loadAuthor() // renews author's profile
|
||||||
|
@ -149,12 +167,15 @@ export const ProfileSettings = () => {
|
||||||
onCleanup(() => window.removeEventListener('beforeunload', handleBeforeUnload))
|
onCleanup(() => window.removeEventListener('beforeunload', handleBeforeUnload))
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(
|
||||||
if (!deepEqual(form, prevForm)) {
|
on(
|
||||||
setIsFloatingPanelVisible(true)
|
() => deepEqual(form, prevForm),
|
||||||
}
|
() => {
|
||||||
})
|
setIsFloatingPanelVisible(!deepEqual(form, prevForm))
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
const handleDeleteSocialLink = (link) => {
|
const handleDeleteSocialLink = (link) => {
|
||||||
updateFormField('links', link, true)
|
updateFormField('links', link, true)
|
||||||
}
|
}
|
||||||
|
@ -174,7 +195,7 @@ export const ProfileSettings = () => {
|
||||||
<div class="col-md-20 col-lg-18 col-xl-16">
|
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||||
<h1>{t('Profile settings')}</h1>
|
<h1>{t('Profile settings')}</h1>
|
||||||
<p class="description">{t('Here you can customize your profile the way you want.')}</p>
|
<p class="description">{t('Here you can customize your profile the way you want.')}</p>
|
||||||
<form enctype="multipart/form-data">
|
<form enctype="multipart/form-data" autocomplete="off">
|
||||||
<h4>{t('Userpic')}</h4>
|
<h4>{t('Userpic')}</h4>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<div
|
<div
|
||||||
|
@ -241,14 +262,16 @@ export const ProfileSettings = () => {
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="username"
|
name="nameOfUser"
|
||||||
id="username"
|
id="nameOfUser"
|
||||||
|
data-lpignore="true"
|
||||||
|
autocomplete="one-time-code"
|
||||||
placeholder={t('Name')}
|
placeholder={t('Name')}
|
||||||
onInput={(event) => updateFormField('name', event.currentTarget.value)}
|
onInput={(event) => updateFormField('name', event.currentTarget.value)}
|
||||||
value={form.name}
|
value={form.name}
|
||||||
ref={(el) => (nameInputRef.current = el)}
|
ref={(el) => (nameInputRef.current = el)}
|
||||||
/>
|
/>
|
||||||
<label for="username">{t('Name')}</label>
|
<label for="nameOfUser">{t('Name')}</label>
|
||||||
<Show when={nameError()}>
|
<Show when={nameError()}>
|
||||||
<div
|
<div
|
||||||
style={{ position: 'absolute', 'margin-top': '-4px' }}
|
style={{ position: 'absolute', 'margin-top': '-4px' }}
|
||||||
|
@ -268,6 +291,8 @@ export const ProfileSettings = () => {
|
||||||
type="text"
|
type="text"
|
||||||
name="user-address"
|
name="user-address"
|
||||||
id="user-address"
|
id="user-address"
|
||||||
|
data-lpignore="true"
|
||||||
|
autocomplete="one-time-code2"
|
||||||
onInput={(event) => updateFormField('slug', event.currentTarget.value)}
|
onInput={(event) => updateFormField('slug', event.currentTarget.value)}
|
||||||
value={form.slug}
|
value={form.slug}
|
||||||
ref={(el) => (slugInputRef.current = el)}
|
ref={(el) => (slugInputRef.current = el)}
|
||||||
|
@ -359,7 +384,12 @@ export const ProfileSettings = () => {
|
||||||
}
|
}
|
||||||
onClick={handleCancel}
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,7 +17,7 @@ interface Props {
|
||||||
|
|
||||||
const isInViewport = (el: Element): boolean => {
|
const isInViewport = (el: Element): boolean => {
|
||||||
const rect = el.getBoundingClientRect()
|
const rect = el.getBoundingClientRect()
|
||||||
return rect.top <= DEFAULT_HEADER_OFFSET
|
return rect.top <= DEFAULT_HEADER_OFFSET + 24 // default offset + 1.5em (default header margin-top)
|
||||||
}
|
}
|
||||||
const scrollToHeader = (element) => {
|
const scrollToHeader = (element) => {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const FullTopic = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
|
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
|
||||||
<h1>#{props.topic?.title}</h1>
|
<h1>#{props.topic?.title}</h1>
|
||||||
<p>{props.topic?.body}</p>
|
<p innerHTML={props.topic?.body} />
|
||||||
<div class={clsx(styles.topicActions)}>
|
<div class={clsx(styles.topicActions)}>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|
|
@ -72,7 +72,7 @@ export const TopicBadge = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class={clsx('text-truncate', styles.description)}>{props.topic.body}</div>
|
<div innerHTML={props.topic.body} class={clsx('text-truncate', styles.description)} />
|
||||||
</Show>
|
</Show>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, o
|
||||||
|
|
||||||
import { useFollowing } from '../../../context/following'
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { useSession } from '../../../context/session'
|
||||||
import { apiClient } from '../../../graphql/client/core'
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
||||||
|
@ -29,41 +30,45 @@ import stylesArticle from '../../Article/Article.module.scss'
|
||||||
import styles from './Author.module.scss'
|
import styles from './Author.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shouts: Shout[]
|
|
||||||
author: Author
|
|
||||||
authorSlug: string
|
authorSlug: string
|
||||||
|
shouts?: Shout[]
|
||||||
|
author?: Author
|
||||||
}
|
}
|
||||||
export const PRERENDERED_ARTICLES_COUNT = 12
|
export const PRERENDERED_ARTICLES_COUNT = 12
|
||||||
const LOAD_MORE_PAGE_SIZE = 9
|
const LOAD_MORE_PAGE_SIZE = 9
|
||||||
|
|
||||||
export const AuthorView = (props: Props) => {
|
export const AuthorView = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
const { subscriptions, followers: myFollowers, loadSubscriptions } = useFollowing()
|
||||||
|
const { session } = useSession()
|
||||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||||
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
||||||
const { page: getPage, searchParams } = useRouter()
|
const { page: getPage, searchParams } = useRouter()
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
||||||
const [followers, setFollowers] = createSignal<Author[]>([])
|
const [author, setAuthor] = createSignal<Author>()
|
||||||
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
|
const [followers, setFollowers] = createSignal([])
|
||||||
|
const [following, setFollowing] = createSignal<Array<Author | Topic>>([]) // flat AuthorFollowsResult
|
||||||
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
|
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
|
||||||
const [commented, setCommented] = createSignal<Reaction[]>()
|
const [commented, setCommented] = createSignal<Reaction[]>()
|
||||||
const modal = MODALS[searchParams().m]
|
const modal = MODALS[searchParams().m]
|
||||||
|
|
||||||
// current author
|
const [sessionChecked, setSessionChecked] = createSignal(false)
|
||||||
const [author, setAuthor] = createSignal<Author>()
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
try {
|
if (
|
||||||
const a = authorEntities()[props.authorSlug]
|
!sessionChecked() &&
|
||||||
setAuthor(a)
|
props.authorSlug &&
|
||||||
} catch (error) {
|
session()?.user?.app_data?.profile?.slug === props.authorSlug
|
||||||
console.debug(error)
|
) {
|
||||||
}
|
setSessionChecked(true)
|
||||||
})
|
const appdata = session()?.user.app_data
|
||||||
|
if (appdata) {
|
||||||
createEffect(async () => {
|
console.info('preloaded my own profile')
|
||||||
if (author()?.id && !author().stat) {
|
const { authors, profile, topics } = appdata
|
||||||
const a = await loadAuthor({ slug: '', author_id: author().id })
|
setFollowers(myFollowers)
|
||||||
console.debug('[AuthorView] loaded author:', a)
|
setAuthor(profile)
|
||||||
|
setFollowing([...authors, ...topics])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -72,15 +77,17 @@ export const AuthorView = (props: Props) => {
|
||||||
|
|
||||||
const fetchData = async (author: Author) => {
|
const fetchData = async (author: Author) => {
|
||||||
try {
|
try {
|
||||||
const [subscriptionsResult, followersResult] = await Promise.all([
|
const [subscriptionsResult, followersResult, authorResult] = await Promise.all([
|
||||||
apiClient.getAuthorFollows({ author_id: author.id }),
|
apiClient.getAuthorFollows({ slug }),
|
||||||
apiClient.getAuthorFollowers({ slug: author.slug }),
|
apiClient.getAuthorFollowers({ slug }),
|
||||||
|
loadAuthor({ slug }),
|
||||||
])
|
])
|
||||||
const { authors, topics } = subscriptionsResult
|
const { authors, topics } = subscriptionsResult
|
||||||
|
setAuthor(authorResult)
|
||||||
setFollowing([...(authors || []), ...(topics || [])])
|
setFollowing([...(authors || []), ...(topics || [])])
|
||||||
setFollowers(followersResult || [])
|
setFollowers(followersResult || [])
|
||||||
|
|
||||||
console.info('[components.Author] following data loaded')
|
console.info('[components.Author] data loaded')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[components.Author] fetch error', error)
|
console.error('[components.Author] fetch error', error)
|
||||||
}
|
}
|
||||||
|
@ -104,10 +111,10 @@ export const AuthorView = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!modal) {
|
if (!modal) hideModal()
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
checkBioHeight()
|
checkBioHeight()
|
||||||
|
fetchData(props.authorSlug)
|
||||||
|
|
||||||
// pagination
|
// pagination
|
||||||
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
||||||
loadMore()
|
loadMore()
|
||||||
|
@ -120,7 +127,7 @@ export const AuthorView = (props: Props) => {
|
||||||
|
|
||||||
const fetchComments = async (commenter: Author) => {
|
const fetchComments = async (commenter: Author) => {
|
||||||
const data = await apiClient.getReactionsBy({
|
const data = await apiClient.getReactionsBy({
|
||||||
by: { comment: false, created_by: commenter.id },
|
by: { comment: true, created_by: commenter.id },
|
||||||
})
|
})
|
||||||
setCommented(data)
|
setCommented(data)
|
||||||
}
|
}
|
||||||
|
@ -160,31 +167,53 @@ export const AuthorView = (props: Props) => {
|
||||||
<Show when={author()} fallback={<Loading />}>
|
<Show when={author()} fallback={<Loading />}>
|
||||||
<>
|
<>
|
||||||
<div class={styles.authorHeader}>
|
<div class={styles.authorHeader}>
|
||||||
<AuthorCard author={author()} followers={followers()} following={following()} />
|
<AuthorCard author={author()} followers={followers() || []} following={following() || []} />
|
||||||
</div>
|
</div>
|
||||||
<div class={clsx(styles.groupControls, 'row')}>
|
<div class={clsx(styles.groupControls, 'row')}>
|
||||||
<div class="col-md-16">
|
<div class="col-md-16">
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li classList={{ 'view-switcher__item--selected': getPage().route === 'author' }}>
|
<li
|
||||||
<a href={getPagePath(router, 'author', { slug: props.authorSlug })}>
|
classList={{
|
||||||
|
'view-switcher__item--selected': getPage().route === 'author',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={getPagePath(router, 'author', {
|
||||||
|
slug: props.authorSlug,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{t('Publications')}
|
{t('Publications')}
|
||||||
</a>
|
</a>
|
||||||
<Show when={author().stat}>
|
<Show when={author().stat}>
|
||||||
<span class="view-switcher__counter">{author().stat.shouts}</span>
|
<span class="view-switcher__counter">{author().stat.shouts}</span>
|
||||||
</Show>
|
</Show>
|
||||||
</li>
|
</li>
|
||||||
<li classList={{ 'view-switcher__item--selected': getPage().route === 'authorComments' }}>
|
<li
|
||||||
<a href={getPagePath(router, 'authorComments', { slug: props.authorSlug })}>
|
classList={{
|
||||||
|
'view-switcher__item--selected': getPage().route === 'authorComments',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={getPagePath(router, 'authorComments', {
|
||||||
|
slug: props.authorSlug,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{t('Comments')}
|
{t('Comments')}
|
||||||
</a>
|
</a>
|
||||||
<Show when={author().stat}>
|
<Show when={author().stat}>
|
||||||
<span class="view-switcher__counter">{author().stat.comments}</span>
|
<span class="view-switcher__counter">{author().stat.comments}</span>
|
||||||
</Show>
|
</Show>
|
||||||
</li>
|
</li>
|
||||||
<li classList={{ 'view-switcher__item--selected': getPage().route === 'authorAbout' }}>
|
<li
|
||||||
|
classList={{
|
||||||
|
'view-switcher__item--selected': getPage().route === 'authorAbout',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
onClick={() => checkBioHeight()}
|
onClick={() => checkBioHeight()}
|
||||||
href={getPagePath(router, 'authorAbout', { slug: props.authorSlug })}
|
href={getPagePath(router, 'authorAbout', {
|
||||||
|
slug: props.authorSlug,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{t('Profile')}
|
{t('Profile')}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -24,34 +24,31 @@ type Props = {
|
||||||
layout: LayoutType
|
layout: LayoutType
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PRERENDERED_ARTICLES_COUNT = 37
|
export const PRERENDERED_ARTICLES_COUNT = 36
|
||||||
const LOAD_MORE_PAGE_SIZE = 11
|
const LOAD_MORE_PAGE_SIZE = 12
|
||||||
|
|
||||||
export const Expo = (props: Props) => {
|
export const Expo = (props: Props) => {
|
||||||
const [isLoaded, setIsLoaded] = createSignal<boolean>(Boolean(props.shouts))
|
const [isLoaded, setIsLoaded] = createSignal<boolean>(Boolean(props.shouts))
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
const [randomTopArticles, setRandomTopArticles] = createSignal<Shout[]>([])
|
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
|
||||||
const [randomTopMonthArticles, setRandomTopMonthArticles] = createSignal<Shout[]>([])
|
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
||||||
|
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
// const { sortedArticles } = useArticlesStore({
|
|
||||||
// shouts: isLoaded() ? props.shouts : [],
|
|
||||||
// })
|
|
||||||
const { sortedArticles } = useArticlesStore({
|
const { sortedArticles } = useArticlesStore({
|
||||||
shouts: props.shouts || [],
|
shouts: isLoaded() ? props.shouts : [],
|
||||||
layout: props.layout,
|
layout: props.layout,
|
||||||
})
|
})
|
||||||
|
|
||||||
const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => {
|
const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => {
|
||||||
const filters = { featured: true, ...additionalFilters }
|
const filters = { ...additionalFilters }
|
||||||
|
|
||||||
if (!filters.layouts) filters.layouts = []
|
if (!filters.layouts) filters.layouts = []
|
||||||
if (props.layout) {
|
if (props.layout) {
|
||||||
filters.layouts.push(props.layout)
|
filters.layouts.push(props.layout)
|
||||||
} else {
|
} else {
|
||||||
filters.layouts.push('article')
|
filters.layouts.push('audio', 'video', 'image', 'literature')
|
||||||
}
|
}
|
||||||
|
|
||||||
return filters
|
return filters
|
||||||
|
@ -80,13 +77,12 @@ export const Expo = (props: Props) => {
|
||||||
|
|
||||||
const loadRandomTopArticles = async () => {
|
const loadRandomTopArticles = async () => {
|
||||||
const options: LoadShoutsOptions = {
|
const options: LoadShoutsOptions = {
|
||||||
filters: getLoadShoutsFilters(),
|
filters: { ...getLoadShoutsFilters(), featured: true },
|
||||||
limit: 10,
|
limit: 10,
|
||||||
random_limit: 100,
|
random_limit: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await apiClient.getRandomTopShouts({ options })
|
const result = await apiClient.getRandomTopShouts({ options })
|
||||||
setRandomTopArticles(result)
|
setFavoriteTopArticles(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadRandomTopMonthArticles = async () => {
|
const loadRandomTopMonthArticles = async () => {
|
||||||
|
@ -94,19 +90,15 @@ export const Expo = (props: Props) => {
|
||||||
const after = getUnixtime(new Date(now.setMonth(now.getMonth() - 1)))
|
const after = getUnixtime(new Date(now.setMonth(now.getMonth() - 1)))
|
||||||
|
|
||||||
const options: LoadShoutsOptions = {
|
const options: LoadShoutsOptions = {
|
||||||
filters: getLoadShoutsFilters({ after }),
|
filters: { ...getLoadShoutsFilters({ after }), reacted: true },
|
||||||
limit: 10,
|
limit: 10,
|
||||||
random_limit: 10,
|
random_limit: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await apiClient.getRandomTopShouts({ options })
|
const result = await apiClient.getRandomTopShouts({ options })
|
||||||
setRandomTopMonthArticles(result)
|
setReactedTopMonthArticles(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pages = createMemo<Shout[][]>(() =>
|
|
||||||
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE),
|
|
||||||
)
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (isLoaded()) {
|
if (isLoaded()) {
|
||||||
return
|
return
|
||||||
|
@ -130,8 +122,8 @@ export const Expo = (props: Props) => {
|
||||||
() => props.layout,
|
() => props.layout,
|
||||||
() => {
|
() => {
|
||||||
resetSortedArticles()
|
resetSortedArticles()
|
||||||
setRandomTopArticles([])
|
setFavoriteTopArticles([])
|
||||||
setRandomTopMonthArticles([])
|
setReactedTopMonthArticles([])
|
||||||
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
|
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
|
||||||
loadRandomTopArticles()
|
loadRandomTopArticles()
|
||||||
loadRandomTopMonthArticles()
|
loadRandomTopMonthArticles()
|
||||||
|
@ -202,7 +194,7 @@ export const Expo = (props: Props) => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<For each={sortedArticles().slice(0, PRERENDERED_ARTICLES_COUNT / 2)}>
|
<For each={sortedArticles().slice(0, LOAD_MORE_PAGE_SIZE)}>
|
||||||
{(shout) => (
|
{(shout) => (
|
||||||
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
|
@ -214,10 +206,10 @@ export const Expo = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Show when={randomTopMonthArticles()?.length > 0} keyed={true}>
|
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
|
||||||
<ArticleCardSwiper title={t('Top month articles')} slides={randomTopMonthArticles()} />
|
<ArticleCardSwiper title={t('Top month articles')} slides={reactedTopMonthArticles()} />
|
||||||
</Show>
|
</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) => (
|
{(shout) => (
|
||||||
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
|
@ -229,23 +221,19 @@ export const Expo = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Show when={randomTopArticles()?.length > 0} keyed={true}>
|
<Show when={favoriteTopArticles()?.length > 0} keyed={true}>
|
||||||
<ArticleCardSwiper title={t('Favorite')} slides={randomTopArticles()} />
|
<ArticleCardSwiper title={t('Favorite')} slides={favoriteTopArticles()} />
|
||||||
</Show>
|
</Show>
|
||||||
<For each={pages()}>
|
<For each={sortedArticles().slice(LOAD_MORE_PAGE_SIZE * 2)}>
|
||||||
{(page) => (
|
{(shout) => (
|
||||||
<For each={page}>
|
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
||||||
{(shout) => (
|
<ArticleCard
|
||||||
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
|
article={shout}
|
||||||
<ArticleCard
|
settings={{ nodate: true, nosubtitle: true, noAuthorLink: true }}
|
||||||
article={shout}
|
desktopCoverSize="XS"
|
||||||
settings={{ nodate: true, nosubtitle: true, noAuthorLink: true }}
|
withAspectRatio={true}
|
||||||
desktopCoverSize="XS"
|
/>
|
||||||
withAspectRatio={true}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -54,6 +54,13 @@ type FeedSearchParams = {
|
||||||
visibility: VisibilityMode
|
visibility: VisibilityMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loadShouts: (options: LoadShoutsOptions) => Promise<{
|
||||||
|
hasMore: boolean
|
||||||
|
newShouts: Shout[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
const getFromDate = (period: FeedPeriod): number => {
|
const getFromDate = (period: FeedPeriod): number => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
let d: Date = now
|
let d: Date = now
|
||||||
|
@ -74,18 +81,10 @@ const getFromDate = (period: FeedPeriod): number => {
|
||||||
return Math.floor(d.getTime() / 1000)
|
return Math.floor(d.getTime() / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
|
||||||
loadShouts: (options: LoadShoutsOptions) => Promise<{
|
|
||||||
hasMore: boolean
|
|
||||||
newShouts: Shout[]
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FeedView = (props: Props) => {
|
export const FeedView = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const monthPeriod: PeriodItem = { value: 'month', title: t('This month') }
|
const monthPeriod: PeriodItem = { value: 'month', title: t('This month') }
|
||||||
const visibilityAll = { value: 'featured', title: t('All') }
|
|
||||||
|
|
||||||
const periods: PeriodItem[] = [
|
const periods: PeriodItem[] = [
|
||||||
{ value: 'week', title: t('This week') },
|
{ value: 'week', title: t('This week') },
|
||||||
|
@ -121,7 +120,7 @@ export const FeedView = (props: Props) => {
|
||||||
const currentVisibility = createMemo(() => {
|
const currentVisibility = createMemo(() => {
|
||||||
const visibility = visibilities.find((v) => v.value === searchParams().visibility)
|
const visibility = visibilities.find((v) => v.value === searchParams().visibility)
|
||||||
if (!visibility) {
|
if (!visibility) {
|
||||||
return visibilityAll
|
return visibilities[0]
|
||||||
}
|
}
|
||||||
return visibility
|
return visibility
|
||||||
})
|
})
|
||||||
|
@ -172,6 +171,7 @@ export const FeedView = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibilityMode = searchParams().visibility
|
const visibilityMode = searchParams().visibility
|
||||||
|
|
||||||
if (visibilityMode === 'all') {
|
if (visibilityMode === 'all') {
|
||||||
options.filters = { ...options.filters }
|
options.filters = { ...options.filters }
|
||||||
} else if (visibilityMode) {
|
} else if (visibilityMode) {
|
||||||
|
@ -185,6 +185,7 @@ export const FeedView = (props: Props) => {
|
||||||
const period = searchParams().period || 'month'
|
const period = searchParams().period || 'month'
|
||||||
options.filters = { after: getFromDate(period) }
|
options.filters = { after: getFromDate(period) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.loadShouts(options)
|
return props.loadShouts(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createSignal, onMount } from 'solid-js'
|
import { For, Show, createEffect, createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { apiClient } from '../../../graphql/client/core'
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
|
@ -20,41 +21,32 @@ import stylesSettings from '../../../styles/FeedSettings.module.scss'
|
||||||
|
|
||||||
export const ProfileSubscriptions = () => {
|
export const ProfileSubscriptions = () => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const { author } = useSession()
|
const { author, session } = useSession()
|
||||||
|
const { subscriptions } = useFollowing()
|
||||||
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
|
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
|
||||||
const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([])
|
const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([])
|
||||||
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
||||||
const [searchQuery, setSearchQuery] = createSignal('')
|
const [searchQuery, setSearchQuery] = createSignal('')
|
||||||
|
|
||||||
const fetchSubscriptions = async () => {
|
|
||||||
try {
|
|
||||||
const slug = author()?.slug
|
|
||||||
const authorFollows = await apiClient.getAuthorFollows({ slug })
|
|
||||||
setFollowing([...authorFollows['authors']])
|
|
||||||
setFiltered([...authorFollows['authors'], ...authorFollows['topics']])
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[fetchSubscriptions] :', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (following()) {
|
const { authors, topics } = subscriptions
|
||||||
|
if (authors || topics) {
|
||||||
|
const fdata = [...(authors || []), ...(topics || [])]
|
||||||
|
setFollowing(fdata)
|
||||||
if (subscriptionFilter() === 'authors') {
|
if (subscriptionFilter() === 'authors') {
|
||||||
setFiltered(following().filter((s) => 'name' in s))
|
setFiltered(fdata.filter((s) => 'name' in s))
|
||||||
} else if (subscriptionFilter() === 'topics') {
|
} else if (subscriptionFilter() === 'topics') {
|
||||||
setFiltered(following().filter((s) => 'title' in s))
|
setFiltered(fdata.filter((s) => 'title' in s))
|
||||||
} else {
|
} else {
|
||||||
setFiltered(following())
|
setFiltered(fdata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (searchQuery()) {
|
|
||||||
setFiltered(dummyFilter(following(), searchQuery(), lang()))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(async () => {
|
createEffect(() => {
|
||||||
await fetchSubscriptions()
|
if (searchQuery()) {
|
||||||
|
setFiltered(dummyFilter(following(), searchQuery(), lang()))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -73,17 +65,29 @@ export const ProfileSubscriptions = () => {
|
||||||
<p class="description">{t('Here you can manage all your Discours subscriptions')}</p>
|
<p class="description">{t('Here you can manage all your Discours subscriptions')}</p>
|
||||||
<Show when={following()} fallback={<Loading />}>
|
<Show when={following()} fallback={<Loading />}>
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'all' })}>
|
<li
|
||||||
|
class={clsx({
|
||||||
|
'view-switcher__item--selected': subscriptionFilter() === 'all',
|
||||||
|
})}
|
||||||
|
>
|
||||||
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
||||||
{t('All')}
|
{t('All')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'authors' })}>
|
<li
|
||||||
|
class={clsx({
|
||||||
|
'view-switcher__item--selected': subscriptionFilter() === 'authors',
|
||||||
|
})}
|
||||||
|
>
|
||||||
<button type="button" onClick={() => setSubscriptionFilter('authors')}>
|
<button type="button" onClick={() => setSubscriptionFilter('authors')}>
|
||||||
{t('Authors')}
|
{t('Authors')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'topics' })}>
|
<li
|
||||||
|
class={clsx({
|
||||||
|
'view-switcher__item--selected': subscriptionFilter() === 'topics',
|
||||||
|
})}
|
||||||
|
>
|
||||||
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
||||||
{t('Topics')}
|
{t('Topics')}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -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 { Meta } from '@solidjs/meta'
|
||||||
import { clsx } from 'clsx'
|
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 { useLocalize } from '../../context/localize'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
|
@ -21,7 +21,9 @@ import { Row3 } from '../Feed/Row3'
|
||||||
import { FullTopic } from '../Topic/Full'
|
import { FullTopic } from '../Topic/Full'
|
||||||
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
|
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
|
||||||
|
|
||||||
|
import { apiClient } from '../../graphql/client/core'
|
||||||
import styles from '../../styles/Topic.module.scss'
|
import styles from '../../styles/Topic.module.scss'
|
||||||
|
import { getUnixtime } from '../../utils/getServerDate'
|
||||||
|
|
||||||
type TopicsPageSearchParams = {
|
type TopicsPageSearchParams = {
|
||||||
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
|
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
|
||||||
|
@ -43,14 +45,56 @@ export const TopicView = (props: Props) => {
|
||||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||||
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
||||||
const { authorsByTopic } = useAuthorsStore()
|
const { authorsByTopic } = useAuthorsStore()
|
||||||
|
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
|
||||||
|
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
||||||
|
|
||||||
const [topic, setTopic] = createSignal<Topic>()
|
const [topic, setTopic] = createSignal<Topic>()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const topics = topicEntities()
|
const topics = topicEntities()
|
||||||
if (props.topicSlug && !topic() && topics) {
|
if (props.topicSlug && !topic() && topics) {
|
||||||
setTopic(topics[props.topicSlug])
|
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(
|
const title = createMemo(
|
||||||
() =>
|
() =>
|
||||||
`#${capitalize(
|
`#${capitalize(
|
||||||
|
@ -75,6 +119,7 @@ export const TopicView = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
loadRandom()
|
||||||
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
||||||
loadMore()
|
loadMore()
|
||||||
}
|
}
|
||||||
|
@ -170,9 +215,9 @@ export const TopicView = (props: Props) => {
|
||||||
beside={sortedArticles()[4]}
|
beside={sortedArticles()[4]}
|
||||||
wrapper={'author'}
|
wrapper={'author'}
|
||||||
/>
|
/>
|
||||||
|
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
|
||||||
<ArticleCardSwiper title={title()} slides={sortedArticles().slice(5, 11)} />
|
<ArticleCardSwiper title={t('Top month articles')} slides={reactedTopMonthArticles()} />
|
||||||
|
</Show>
|
||||||
<Beside
|
<Beside
|
||||||
beside={sortedArticles()[12]}
|
beside={sortedArticles()[12]}
|
||||||
title={t('Top viewed')}
|
title={t('Top viewed')}
|
||||||
|
@ -183,8 +228,10 @@ export const TopicView = (props: Props) => {
|
||||||
<Row2 articles={sortedArticles().slice(13, 15)} isEqual={true} />
|
<Row2 articles={sortedArticles().slice(13, 15)} isEqual={true} />
|
||||||
<Row1 article={sortedArticles()[15]} />
|
<Row1 article={sortedArticles()[15]} />
|
||||||
|
|
||||||
|
<Show when={favoriteTopArticles()?.length > 0} keyed={true}>
|
||||||
|
<ArticleCardSwiper title={t('Favorite')} slides={favoriteTopArticles()} />
|
||||||
|
</Show>
|
||||||
<Show when={sortedArticles().length > 15}>
|
<Show when={sortedArticles().length > 15}>
|
||||||
<ArticleCardSwiper slides={sortedArticles().slice(16, 22)} />
|
|
||||||
<Row3 articles={sortedArticles().slice(23, 26)} />
|
<Row3 articles={sortedArticles().slice(23, 26)} />
|
||||||
<Row2 articles={sortedArticles().slice(26, 28)} />
|
<Row2 articles={sortedArticles().slice(26, 28)} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -10,18 +10,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.notificationsCounter {
|
.notificationsCounter {
|
||||||
background-color: #d00820;
|
align-items: center;
|
||||||
border: 2px solid #fff;
|
background-color: #E84500;
|
||||||
border-radius: 2em;
|
border-radius: 0.8rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 1rem;
|
display: flex;
|
||||||
|
font-size: 1.2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
height: 1.6em;
|
height: 2.2rem;
|
||||||
left: 1.1em;
|
justify-content: center;
|
||||||
line-height: 1.25em;
|
left: 1.6rem;
|
||||||
|
min-width: 2.2rem;
|
||||||
padding: 0 0.25em;
|
padding: 0 0.25em;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
top: -0.5rem;
|
top: -0.5rem;
|
||||||
min-width: 1.5em;
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
left: 1.8rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
|
import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||||
import SwiperCore from 'swiper'
|
import SwiperCore from 'swiper'
|
||||||
import { Manipulation, Navigation, Pagination } from 'swiper/modules'
|
import { HashNavigation, Manipulation, Navigation, Pagination } from 'swiper/modules'
|
||||||
import { throttle } from 'throttle-debounce'
|
import { throttle } from 'throttle-debounce'
|
||||||
|
|
||||||
import { MediaItem } from '../../../pages/types'
|
import { MediaItem } from '../../../pages/types'
|
||||||
|
@ -12,6 +12,8 @@ import { Lightbox } from '../Lightbox'
|
||||||
|
|
||||||
import { SwiperRef } from './swiper'
|
import { SwiperRef } from './swiper'
|
||||||
|
|
||||||
|
import { useRouter } from '../../../stores/router'
|
||||||
|
import { ArticlePageSearchParams } from '../../Article/FullArticle'
|
||||||
import styles from './Swiper.module.scss'
|
import styles from './Swiper.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -31,10 +33,13 @@ export const ImageSwiper = (props: Props) => {
|
||||||
const [slideIndex, setSlideIndex] = createSignal(0)
|
const [slideIndex, setSlideIndex] = createSignal(0)
|
||||||
const [isMobileView, setIsMobileView] = createSignal(false)
|
const [isMobileView, setIsMobileView] = createSignal(false)
|
||||||
const [selectedImage, setSelectedImage] = createSignal('')
|
const [selectedImage, setSelectedImage] = createSignal('')
|
||||||
|
const { searchParams, changeSearchParams } = useRouter<ArticlePageSearchParams>()
|
||||||
|
|
||||||
const handleSlideChange = () => {
|
const handleSlideChange = () => {
|
||||||
thumbSwipeRef.current.swiper.slideTo(mainSwipeRef.current.swiper.activeIndex)
|
const activeIndex = mainSwipeRef.current.swiper.activeIndex
|
||||||
setSlideIndex(mainSwipeRef.current.swiper.activeIndex)
|
thumbSwipeRef.current.swiper.slideTo(activeIndex)
|
||||||
|
setSlideIndex(activeIndex)
|
||||||
|
changeSearchParams({ slide: `${activeIndex + 1}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
|
@ -51,8 +56,19 @@ export const ImageSwiper = (props: Props) => {
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const { register } = await import('swiper/element/bundle')
|
const { register } = await import('swiper/element/bundle')
|
||||||
register()
|
register()
|
||||||
SwiperCore.use([Pagination, Navigation, Manipulation])
|
SwiperCore.use([Pagination, Navigation, Manipulation, HashNavigation])
|
||||||
mainSwipeRef.current?.swiper?.on('slideChange', handleSlideChange)
|
while (!mainSwipeRef.current || !mainSwipeRef.current.swiper) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10)) // wait 10 ms
|
||||||
|
}
|
||||||
|
mainSwipeRef.current.swiper.on('slideChange', handleSlideChange)
|
||||||
|
const initialSlide = parseInt(searchParams().slide) - 1
|
||||||
|
if (initialSlide && !Number.isNaN(initialSlide) && initialSlide < props.images.length) {
|
||||||
|
mainSwipeRef.current.swiper.slideTo(initialSlide, 0)
|
||||||
|
} else {
|
||||||
|
changeSearchParams({ slide: '1' })
|
||||||
|
}
|
||||||
|
|
||||||
|
mainSwipeRef.current.swiper.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -103,6 +119,9 @@ export const ImageSwiper = (props: Props) => {
|
||||||
watch-slides-visibility={true}
|
watch-slides-visibility={true}
|
||||||
direction={'horizontal'}
|
direction={'horizontal'}
|
||||||
slides-per-group-auto={true}
|
slides-per-group-auto={true}
|
||||||
|
hash-navigation={{
|
||||||
|
watchState: true,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<For each={props.images}>
|
<For each={props.images}>
|
||||||
{(slide, index) => (
|
{(slide, index) => (
|
||||||
|
@ -149,7 +168,7 @@ export const ImageSwiper = (props: Props) => {
|
||||||
{(slide, index) => (
|
{(slide, index) => (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<swiper-slide lazy="true" virtual-index={index()}>
|
<swiper-slide lazy="true" virtual-index={index()} data-hash={index() + 1}>
|
||||||
<div class={styles.image} onClick={handleImageClick}>
|
<div class={styles.image} onClick={handleImageClick}>
|
||||||
<Image src={slide.url} alt={slide.title} width={800} />
|
<Image src={slide.url} alt={slide.title} width={800} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -135,9 +135,13 @@
|
||||||
.counter {
|
.counter {
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
top: 477px;
|
||||||
|
}
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
top: 477px;
|
top: 276px;
|
||||||
right: 0;
|
right: 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0.2rem 0.8rem;
|
padding: 0.2rem 0.8rem;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Accessor, JSX, createContext, createEffect, createMemo, createSignal, u
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { Author, AuthorFollows, Community, FollowingEntity, Topic } from '../graphql/schema/core.gen'
|
import { Author, AuthorFollowsResult, FollowingEntity } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { useSession } from './session'
|
import { useSession } from './session'
|
||||||
|
|
||||||
|
@ -16,8 +16,9 @@ type SubscribeAction = { slug: string; type: 'subscribe' | 'unsubscribe' }
|
||||||
|
|
||||||
interface FollowingContextType {
|
interface FollowingContextType {
|
||||||
loading: Accessor<boolean>
|
loading: Accessor<boolean>
|
||||||
subscriptions: AuthorFollows
|
followers: Accessor<Array<Author>>
|
||||||
setSubscriptions: (subscriptions: AuthorFollows) => void
|
subscriptions: AuthorFollowsResult
|
||||||
|
setSubscriptions: (subscriptions: AuthorFollowsResult) => void
|
||||||
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
||||||
loadSubscriptions: () => void
|
loadSubscriptions: () => void
|
||||||
follow: (what: FollowingEntity, slug: string) => Promise<void>
|
follow: (what: FollowingEntity, slug: string) => Promise<void>
|
||||||
|
@ -32,7 +33,7 @@ export function useFollowing() {
|
||||||
return useContext(FollowingContext)
|
return useContext(FollowingContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_SUBSCRIPTIONS: AuthorFollows = {
|
const EMPTY_SUBSCRIPTIONS: AuthorFollowsResult = {
|
||||||
topics: [],
|
topics: [],
|
||||||
authors: [],
|
authors: [],
|
||||||
communities: [],
|
communities: [],
|
||||||
|
@ -40,9 +41,9 @@ const EMPTY_SUBSCRIPTIONS: AuthorFollows = {
|
||||||
|
|
||||||
export const FollowingProvider = (props: { children: JSX.Element }) => {
|
export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
const [loading, setLoading] = createSignal<boolean>(false)
|
const [loading, setLoading] = createSignal<boolean>(false)
|
||||||
const [subscriptions, setSubscriptions] = createStore<SubscriptionsData>(EMPTY_SUBSCRIPTIONS)
|
const [followers, setFollowers] = createSignal<Array<Author>>([])
|
||||||
const { session, author } = useSession()
|
const [subscriptions, setSubscriptions] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS)
|
||||||
const [subscribeInAction, setSubscribeInAction] = createSignal<SubscribeAction>()
|
const { author, session } = useSession()
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
@ -94,8 +95,17 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (author()) {
|
if (author()) {
|
||||||
console.debug('[context.following] author update detect')
|
try {
|
||||||
fetchData()
|
const appdata = session()?.user.app_data
|
||||||
|
if (appdata) {
|
||||||
|
const { authors, followers, topics } = appdata
|
||||||
|
setSubscriptions({ authors, topics })
|
||||||
|
setFollowers(followers)
|
||||||
|
if (!authors) fetchData()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -125,6 +135,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
subscriptions,
|
subscriptions,
|
||||||
setSubscriptions,
|
setSubscriptions,
|
||||||
setFollowing,
|
setFollowing,
|
||||||
|
followers,
|
||||||
loadSubscriptions: fetchData,
|
loadSubscriptions: fetchData,
|
||||||
follow,
|
follow,
|
||||||
unfollow,
|
unfollow,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Accessor, JSX, Resource } from 'solid-js'
|
import type { Accessor, JSX, Resource } from 'solid-js'
|
||||||
import type { AuthModalSource } from '../components/Nav/AuthModal/types'
|
import type { AuthModalSearchParams, AuthModalSource } from '../components/Nav/AuthModal/types'
|
||||||
import type { Author } from '../graphql/schema/core.gen'
|
import type { Author } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -29,7 +29,6 @@ import {
|
||||||
|
|
||||||
import { inboxClient } from '../graphql/client/chat'
|
import { inboxClient } from '../graphql/client/chat'
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { notifierClient } from '../graphql/client/notifier'
|
|
||||||
import { useRouter } from '../stores/router'
|
import { useRouter } from '../stores/router'
|
||||||
import { showModal } from '../stores/ui'
|
import { showModal } from '../stores/ui'
|
||||||
import { addAuthors } from '../stores/zine/authors'
|
import { addAuthors } from '../stores/zine/authors'
|
||||||
|
@ -136,6 +135,7 @@ export const SessionProvider = (props: {
|
||||||
|
|
||||||
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
|
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
|
||||||
const [authError, setAuthError] = createSignal('')
|
const [authError, setAuthError] = createSignal('')
|
||||||
|
const { clearSearchParams } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
// Function to load session data
|
// Function to load session data
|
||||||
const sessionData = async () => {
|
const sessionData = async () => {
|
||||||
|
@ -143,7 +143,7 @@ export const SessionProvider = (props: {
|
||||||
const s: ApiResponse<AuthToken> = await authorizer().getSession()
|
const s: ApiResponse<AuthToken> = await authorizer().getSession()
|
||||||
if (s?.data) {
|
if (s?.data) {
|
||||||
console.info('[context.session] loading session', s)
|
console.info('[context.session] loading session', s)
|
||||||
|
clearSearchParams()
|
||||||
// Set session expiration time in local storage
|
// Set session expiration time in local storage
|
||||||
const expires_at = new Date(Date.now() + s.data.expires_in * 1000)
|
const expires_at = new Date(Date.now() + s.data.expires_in * 1000)
|
||||||
localStorage.setItem('expires_at', `${expires_at.getTime()}`)
|
localStorage.setItem('expires_at', `${expires_at.getTime()}`)
|
||||||
|
@ -199,6 +199,7 @@ export const SessionProvider = (props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
onCleanup(() => clearTimeout(minuteLater))
|
onCleanup(() => clearTimeout(minuteLater))
|
||||||
|
|
||||||
const authorData = async () => {
|
const authorData = async () => {
|
||||||
const u = session()?.user
|
const u = session()?.user
|
||||||
return u ? (await apiClient.getAuthorId({ user: u.id.trim() })) || null : null
|
return u ? (await apiClient.getAuthorId({ user: u.id.trim() })) || null : null
|
||||||
|
@ -217,7 +218,18 @@ export const SessionProvider = (props: {
|
||||||
apiClient.connect(token)
|
apiClient.connect(token)
|
||||||
inboxClient.connect(token)
|
inboxClient.connect(token)
|
||||||
}
|
}
|
||||||
if (!author()) loadAuthor()
|
|
||||||
|
try {
|
||||||
|
const appdata = session()?.user.app_data
|
||||||
|
if (appdata) {
|
||||||
|
const { profile } = appdata
|
||||||
|
setAuthor(profile)
|
||||||
|
addAuthors([profile])
|
||||||
|
if (!profile) loadAuthor()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
setIsSessionLoaded(true)
|
setIsSessionLoaded(true)
|
||||||
}
|
}
|
||||||
|
@ -263,7 +275,6 @@ export const SessionProvider = (props: {
|
||||||
() => {
|
() => {
|
||||||
props.onStateChangeCallback(session())
|
props.onStateChangeCallback(session())
|
||||||
},
|
},
|
||||||
{ defer: true },
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -368,6 +379,7 @@ export const SessionProvider = (props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAuthenticated = createMemo(() => Boolean(author()))
|
const isAuthenticated = createMemo(() => Boolean(author()))
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
loadSession,
|
loadSession,
|
||||||
requireAuthentication,
|
requireAuthentication,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type {
|
import type {
|
||||||
Author,
|
Author,
|
||||||
AuthorFollows,
|
AuthorFollowsResult,
|
||||||
CommonResult,
|
CommonResult,
|
||||||
FollowingEntity,
|
FollowingEntity,
|
||||||
LoadShoutsOptions,
|
LoadShoutsOptions,
|
||||||
|
@ -134,7 +134,7 @@ export const apiClient = {
|
||||||
slug?: string
|
slug?: string
|
||||||
author_id?: number
|
author_id?: number
|
||||||
user?: string
|
user?: string
|
||||||
}): Promise<AuthorFollows> => {
|
}): Promise<AuthorFollowsResult> => {
|
||||||
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
|
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
|
||||||
return response.data.get_author_follows
|
return response.data.get_author_follows
|
||||||
},
|
},
|
||||||
|
|
|
@ -56,17 +56,11 @@ export const AuthorPage = (props: PageProps) => {
|
||||||
|
|
||||||
onCleanup(() => resetSortedArticles())
|
onCleanup(() => resetSortedArticles())
|
||||||
|
|
||||||
const usePrerenderedData = props.author?.slug === slug()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title={props.seo?.title || t('Discours')}>
|
<PageLayout title={props.seo?.title || t('Discours')}>
|
||||||
<ReactionsProvider>
|
<ReactionsProvider>
|
||||||
<Show when={isLoaded()} fallback={<Loading />}>
|
<Show when={isLoaded()} fallback={<Loading />}>
|
||||||
<AuthorView
|
<AuthorView authorSlug={slug()} />
|
||||||
author={usePrerenderedData ? props.author : null}
|
|
||||||
shouts={usePrerenderedData ? props.authorShouts : null}
|
|
||||||
authorSlug={slug()}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
</ReactionsProvider>
|
</ReactionsProvider>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|
|
@ -320,3 +320,14 @@ h5 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// disable last pass extention
|
||||||
|
|
||||||
|
div[data-lastpass-icon-root="true"] {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[data-lastpass-infield="true"] {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -114,8 +114,8 @@ const handleClientRouteLinkClick = async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.hash) {
|
if (url.hash) {
|
||||||
scrollToHash(url.hash)
|
// scrollToHash(url.hash)
|
||||||
return
|
// return
|
||||||
}
|
}
|
||||||
|
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { createLazyMemo } from '@solid-primitives/memo'
|
import { createLazyMemo } from '@solid-primitives/memo'
|
||||||
import { createSignal } from 'solid-js'
|
import { createEffect, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { apiClient } from '../../graphql/client/core'
|
import { apiClient } from '../../graphql/client/core'
|
||||||
import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen'
|
import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen'
|
||||||
|
|
|
@ -588,6 +588,7 @@ figure {
|
||||||
display: block;
|
display: block;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -622,6 +623,10 @@ figure {
|
||||||
margin-bottom: 0.6em;
|
margin-bottom: 0.6em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
margin-right: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,41 @@
|
||||||
import { cdnUrl, thumborUrl } from './config'
|
import { cdnUrl, thumborUrl } from './config'
|
||||||
|
|
||||||
const getSizeUrlPart = (options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {}) => {
|
const URL_CONFIG = {
|
||||||
const widthString = options.width ? options.width.toString() : ''
|
cdnUrl: cdnUrl,
|
||||||
const heightString = options.height ? options.height.toString() : ''
|
thumborUrl: `${thumborUrl}/unsafe/`,
|
||||||
|
audioSubfolder: 'audio',
|
||||||
|
imageSubfolder: 'image',
|
||||||
|
productionFolder: 'production/',
|
||||||
|
}
|
||||||
|
|
||||||
if (!(widthString || heightString) || options.noSizeUrlPart) {
|
const AUDIO_EXTENSIONS = new Set(['wav', 'mp3', 'ogg', 'aif', 'flac'])
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${widthString}x${heightString}/`
|
const isAudioFile = (filename: string): boolean => {
|
||||||
|
const extension = filename.split('.').pop()?.toLowerCase()
|
||||||
|
return AUDIO_EXTENSIONS.has(extension ?? '')
|
||||||
|
}
|
||||||
|
const getLastSegment = (url: string): string => url.toLowerCase().split('/').pop() || ''
|
||||||
|
|
||||||
|
const buildSizePart = (width?: number, height?: number, includeSize = true): string => {
|
||||||
|
if (!includeSize) return ''
|
||||||
|
const widthPart = width ? width.toString() : ''
|
||||||
|
const heightPart = height ? height.toString() : ''
|
||||||
|
return widthPart || heightPart ? `${widthPart}x${heightPart}/` : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getImageUrl = (
|
export const getImageUrl = (
|
||||||
src: string,
|
src: string,
|
||||||
options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {},
|
options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {},
|
||||||
) => {
|
): string => {
|
||||||
const filename = src?.split('/').pop()
|
if (!src.includes('discours.io') && src.includes('http')) {
|
||||||
const isAudio = src.toLowerCase().split('.').pop() in ['wav', 'mp3', 'ogg', 'aif', 'flac']
|
return src
|
||||||
const base = isAudio ? cdnUrl : `${thumborUrl}/unsafe/`
|
}
|
||||||
const sizeUrlPart = isAudio ? '' : getSizeUrlPart(options)
|
const filename = getLastSegment(src)
|
||||||
|
const base = isAudioFile(filename) ? URL_CONFIG.cdnUrl : URL_CONFIG.thumborUrl
|
||||||
|
const suffix = options.noSizeUrlPart ? '' : buildSizePart(options.width, options.height)
|
||||||
|
const subfolder = isAudioFile(filename) ? URL_CONFIG.audioSubfolder : URL_CONFIG.imageSubfolder
|
||||||
|
|
||||||
return `${base}${sizeUrlPart}production/${isAudio ? 'audio' : 'image'}/${filename}`
|
return `${base}${suffix}${URL_CONFIG.productionFolder}${subfolder}/${filename}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOpenGraphImageUrl = (
|
export const getOpenGraphImageUrl = (
|
||||||
|
@ -32,17 +47,16 @@ export const getOpenGraphImageUrl = (
|
||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
},
|
},
|
||||||
) => {
|
): string => {
|
||||||
const sizeUrlPart = getSizeUrlPart(options)
|
const sizeUrlPart = buildSizePart(options.width, options.height)
|
||||||
|
|
||||||
const filtersPart = `filters:discourstext('${encodeURIComponent(options.topic)}','${encodeURIComponent(
|
const filtersPart = `filters:discourstext('${encodeURIComponent(options.topic)}','${encodeURIComponent(
|
||||||
options.author,
|
options.author,
|
||||||
)}','${encodeURIComponent(options.title)}')/`
|
)}','${encodeURIComponent(options.title)}')/`
|
||||||
|
|
||||||
if (src.startsWith(thumborUrl)) {
|
if (src.startsWith(URL_CONFIG.thumborUrl)) {
|
||||||
const thumborKey = src.replace(`${thumborUrl}/unsafe`, '')
|
const thumborKey = src.replace(URL_CONFIG.thumborUrl, '')
|
||||||
return `${thumborUrl}/unsafe/${sizeUrlPart}${filtersPart}${thumborKey}`
|
return `${URL_CONFIG.thumborUrl}${sizeUrlPart}${filtersPart}${thumborKey}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${thumborUrl}/unsafe/${sizeUrlPart}${filtersPart}${src}`
|
return `${URL_CONFIG.thumborUrl}${sizeUrlPart}${filtersPart}${src}`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user