diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml
index 2d2d4bc8..1220b122 100644
--- a/.github/workflows/node-ci.yml
+++ b/.github/workflows/node-ci.yml
@@ -16,7 +16,7 @@ jobs:
run: npm run typecheck
- name: Lint with Biome
- run: npx biome ci .
+ run: npm run check:code
- name: Lint styles
run: npm run lint:styles
diff --git a/biome.json b/biome.json
index ba7464a5..ca4262bb 100644
--- a/biome.json
+++ b/biome.json
@@ -2,7 +2,7 @@
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"files": {
"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": {
"defaultBranch": "dev",
diff --git a/package.json b/package.json
index 5e79d11f..1da95480 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"hygen": "HYGEN_TMPLS=gen hygen",
"postinstall": "npm run codegen && npx patch-package",
"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:code": "npx @biomejs/biome lint src --log-kind=compact --verbose",
"lint:code:fix": "npx @biomejs/biome lint src --apply-unsafe --log-kind=compact --verbose",
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 3249be2a..394eaacc 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -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",
diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json
index d4e5bfcd..16e402fa 100644
--- a/public/locales/ru/translation.json
+++ b/public/locales/ru/translation.json
@@ -87,6 +87,7 @@
"Comment successfully deleted": "Комментарий успешно удален",
"Comment": "Комментировать",
"Commentator": "Комментатор",
+ "Commenting": "Комментирование",
"Comments": "Комментарии",
"CommentsWithCount": "{count, plural, =0 {{count} комментариев} one {{count} комментарий} few {{count} комментария} other {{count} комментариев}}",
"Communities": "Сообщества",
diff --git a/public/robots.txt b/public/robots.txt
index c2a49f4f..1f53798b 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -1,2 +1,2 @@
User-agent: *
-Allow: /
+Disallow: /
diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx
index ef5a4b80..aee81066 100644
--- a/src/components/Article/FullArticle.tsx
+++ b/src/components/Article/FullArticle.tsx
@@ -54,6 +54,7 @@ type IframeSize = {
export type ArticlePageSearchParams = {
scrollTo: 'comments'
commentId: string
+ slide?: string
}
const scrollTo = (el: HTMLElement) => {
diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx
index 75f23639..5fedd3e5 100644
--- a/src/components/Author/AuthorCard/AuthorCard.tsx
+++ b/src/components/Author/AuthorCard/AuthorCard.tsx
@@ -134,7 +134,9 @@ export const AuthorCard = (props: Props) => {
)}
- {t('SubscriberWithCount', { count: props.followers.length ?? 0 })}
+ {t('SubscriberWithCount', {
+ count: props.followers.length ?? 0,
+ })}
@@ -169,7 +171,9 @@ export const AuthorCard = (props: Props) => {
}}
- {t('SubscriptionWithCount', { count: props?.following.length ?? 0 })}
+ {t('SubscriptionWithCount', {
+ count: props?.following.length ?? 0,
+ })}
@@ -234,7 +238,9 @@ export const AuthorCard = (props: Props) => {
title={props.author.name}
description={props.author.bio}
imageUrl={props.author.pic}
- shareUrl={getShareUrl({ pathname: `/author/${props.author.slug}` })}
+ shareUrl={getShareUrl({
+ pathname: `/author/${props.author.slug}`,
+ })}
trigger={}
/>
@@ -270,13 +276,21 @@ export const AuthorCard = (props: Props) => {
<>
{t('Subscriptions')}
- -
+
-
{props.following.length}
- -
+
-
@@ -284,7 +298,11 @@ export const AuthorCard = (props: Props) => {
{props.following.filter((s) => 'name' in s).length}
- -
+
-
diff --git a/src/components/Nav/Header/Header.module.scss b/src/components/Nav/Header/Header.module.scss
index 018adc78..010a8164 100644
--- a/src/components/Nav/Header/Header.module.scss
+++ b/src/components/Nav/Header/Header.module.scss
@@ -8,7 +8,7 @@
z-index: 10003;
.wide-container {
- background: #fff;
+ background: var(--background-color);
@include media-breakpoint-down(lg) {
padding: 0 divide($container-padding-x, 2);
@@ -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;
+}
diff --git a/src/components/Nav/HeaderAuth.tsx b/src/components/Nav/HeaderAuth.tsx
index 31a0acc6..8ddff9df 100644
--- a/src/components/Nav/HeaderAuth.tsx
+++ b/src/components/Nav/HeaderAuth.tsx
@@ -14,10 +14,9 @@ import { Icon } from '../_shared/Icon'
import { Popover } from '../_shared/Popover'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
-import { ProfilePopup } from './ProfilePopup'
-
-import { useSnackbar } from '../../context/snackbar'
+import { Popup } from '../_shared/Popup'
import styles from './Header/Header.module.scss'
+import { ProfilePopup } from './ProfilePopup'
type Props = {
setIsProfilePopupVisible: (value: boolean) => void
@@ -51,7 +50,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 +64,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 +106,7 @@ export const HeaderAuth = (props: Props) => {
-
+
{t('Create post')}
@@ -117,7 +117,7 @@ export const HeaderAuth = (props: Props) => {
-
+
+
+
+ {
+ props.setIsProfilePopupVisible(isVisible)
+ }}
+ containerCssClass={styles.control}
+ trigger={
+
+ }
+ />
+
diff --git a/src/components/Nav/Snackbar.module.scss b/src/components/Nav/Snackbar.module.scss
index a0fb8e64..9af5719b 100644
--- a/src/components/Nav/Snackbar.module.scss
+++ b/src/components/Nav/Snackbar.module.scss
@@ -1,5 +1,4 @@
.snackbar {
- min-height: 2px;
background-color: var(--default-color);
color: #fff;
font-size: 2rem;
diff --git a/src/components/ProfileSettings/ProfileSettings.tsx b/src/components/ProfileSettings/ProfileSettings.tsx
index 70389ccb..90a23b33 100644
--- a/src/components/ProfileSettings/ProfileSettings.tsx
+++ b/src/components/ProfileSettings/ProfileSettings.tsx
@@ -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(false)
const [incorrectUrl, setIncorrectUrl] = createSignal(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}
/>
-
+
diff --git a/src/components/Topic/Full.tsx b/src/components/Topic/Full.tsx
index 492a9113..5e22aed1 100644
--- a/src/components/Topic/Full.tsx
+++ b/src/components/Topic/Full.tsx
@@ -40,7 +40,7 @@ export const FullTopic = (props: Props) => {
return (
diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx
index 06d5ac1e..4ddb5649 100644
--- a/src/components/Views/Author/Author.tsx
+++ b/src/components/Views/Author/Author.tsx
@@ -7,6 +7,7 @@ import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onMou
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize'
+import { useSession } from '../../../context/session'
import { apiClient } from '../../../graphql/client/core'
import { router, useRouter } from '../../../stores/router'
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
@@ -29,42 +30,45 @@ import stylesArticle from '../../Article/Article.module.scss'
import styles from './Author.module.scss'
type Props = {
- shouts: Shout[]
- author: Author
authorSlug: string
+ shouts?: Shout[]
+ author?: Author
}
export const PRERENDERED_ARTICLES_COUNT = 12
const LOAD_MORE_PAGE_SIZE = 9
export const AuthorView = (props: Props) => {
const { t } = useLocalize()
- const { loadSubscriptions } = useFollowing()
+ const { subscriptions, followers: myFollowers, loadSubscriptions } = useFollowing()
+ const { session } = useSession()
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
const { page: getPage, searchParams } = useRouter()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
- const [followers, setFollowers] = createSignal([])
- const [following, setFollowing] = createSignal>([])
+ const [author, setAuthor] = createSignal()
+ const [followers, setFollowers] = createSignal([])
+ const [following, setFollowing] = createSignal>([]) // flat AuthorFollowsResult
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
const [commented, setCommented] = createSignal()
const modal = MODALS[searchParams().m]
- // current author
- const [author, setAuthor] = createSignal()
+ const [sessionChecked, setSessionChecked] = createSignal(false)
createEffect(() => {
- try {
- const a = authorEntities()[props.authorSlug]
- setAuthor(a)
- } catch (error) {
- console.debug(error)
- }
- })
-
- createEffect(async () => {
- if (author()?.id && !author().stat) {
- const a = await loadAuthor({ slug: '', author_id: author().id })
- console.debug('[AuthorView] loaded author:', a)
+ if (
+ !sessionChecked() &&
+ props.authorSlug &&
+ session()?.user?.app_data?.profile?.slug === props.authorSlug
+ ) {
+ setSessionChecked(true)
+ const appdata = session()?.user.app_data
+ if (appdata) {
+ console.info('preloaded my own profile')
+ const { authors, profile, topics } = appdata
+ setFollowers(myFollowers)
+ setAuthor(profile)
+ setFollowing([...authors, ...topics])
+ }
}
})
@@ -73,16 +77,17 @@ export const AuthorView = (props: Props) => {
const fetchData = async (slug) => {
try {
- const [subscriptionsResult, followersResult] = await Promise.all([
+ const [subscriptionsResult, followersResult, authorResult] = await Promise.all([
apiClient.getAuthorFollows({ slug }),
apiClient.getAuthorFollowers({ slug }),
+ loadAuthor({ slug }),
])
-
const { authors, topics } = subscriptionsResult
+ setAuthor(authorResult)
setFollowing([...(authors || []), ...(topics || [])])
setFollowers(followersResult || [])
- console.info('[components.Author] following data loaded')
+ console.info('[components.Author] data loaded')
} catch (error) {
console.error('[components.Author] fetch error', error)
}
@@ -94,14 +99,6 @@ export const AuthorView = (props: Props) => {
}
}
- onMount(() => {
- fetchData(props.authorSlug)
-
- if (!modal) {
- hideModal()
- }
- })
-
const loadMore = async () => {
saveScrollPosition()
const { hasMore } = await loadShouts({
@@ -114,7 +111,9 @@ export const AuthorView = (props: Props) => {
}
onMount(() => {
+ if (!modal) hideModal()
checkBioHeight()
+ fetchData(props.authorSlug)
// pagination
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
@@ -168,31 +167,53 @@ export const AuthorView = (props: Props) => {
}>
<>
-
+
{t('All posts rating')}
-
+
diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx
index eddd12a4..82e43dfe 100644
--- a/src/components/Views/Expo/Expo.tsx
+++ b/src/components/Views/Expo/Expo.tsx
@@ -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(props.shouts))
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
- const [randomTopArticles, setRandomTopArticles] = createSignal([])
- const [randomTopMonthArticles, setRandomTopMonthArticles] = createSignal([])
+ const [favoriteTopArticles, setFavoriteTopArticles] = createSignal([])
+ const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal([])
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(() =>
- 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) => {
-
+
{(shout) => (
)}
- 0} keyed={true}>
-
+ 0} keyed={true}>
+
-
+
{(shout) => (
)}
- 0} keyed={true}>
-
+ 0} keyed={true}>
+
-
- {(page) => (
-
- {(shout) => (
-
- )}
-
+
+ {(shout) => (
+
)}
diff --git a/src/components/Views/ProfileSubscriptions/ProfileSubscriptions.tsx b/src/components/Views/ProfileSubscriptions/ProfileSubscriptions.tsx
index 6e1b4d8d..51f27325 100644
--- a/src/components/Views/ProfileSubscriptions/ProfileSubscriptions.tsx
+++ b/src/components/Views/ProfileSubscriptions/ProfileSubscriptions.tsx
@@ -1,6 +1,7 @@
import { clsx } from 'clsx'
import { For, Show, createEffect, createSignal, onMount } from 'solid-js'
+import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { apiClient } from '../../../graphql/client/core'
@@ -20,41 +21,32 @@ import stylesSettings from '../../../styles/FeedSettings.module.scss'
export const ProfileSubscriptions = () => {
const { t, lang } = useLocalize()
- const { author } = useSession()
+ const { author, session } = useSession()
+ const { subscriptions } = useFollowing()
const [following, setFollowing] = createSignal>([])
const [filtered, setFiltered] = createSignal>([])
const [subscriptionFilter, setSubscriptionFilter] = createSignal('all')
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(() => {
- if (following()) {
+ const { authors, topics } = subscriptions
+ if (authors || topics) {
+ const fdata = [...(authors || []), ...(topics || [])]
+ setFollowing(fdata)
if (subscriptionFilter() === 'authors') {
- setFiltered(following().filter((s) => 'name' in s))
+ setFiltered(fdata.filter((s) => 'name' in s))
} else if (subscriptionFilter() === 'topics') {
- setFiltered(following().filter((s) => 'title' in s))
+ setFiltered(fdata.filter((s) => 'title' in s))
} else {
- setFiltered(following())
+ setFiltered(fdata)
}
}
- if (searchQuery()) {
- setFiltered(dummyFilter(following(), searchQuery(), lang()))
- }
})
- onMount(async () => {
- await fetchSubscriptions()
+ createEffect(() => {
+ if (searchQuery()) {
+ setFiltered(dummyFilter(following(), searchQuery(), lang()))
+ }
})
return (
@@ -73,17 +65,29 @@ export const ProfileSubscriptions = () => {
{t('Here you can manage all your Discours subscriptions')}
}>
- -
+
-
- -
+
-
- -
+
-
diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx
index 34d2683b..97a107dd 100644
--- a/src/components/Views/Topic.tsx
+++ b/src/components/Views/Topic.tsx
@@ -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([])
+ const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal([])
const [topic, setTopic] = createSignal()
+
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'}
/>
-
-
-
+ 0} keyed={true}>
+
+
{
+ 0} keyed={true}>
+
+
15}>
-
diff --git a/src/components/_shared/Icon/Icon.module.scss b/src/components/_shared/Icon/Icon.module.scss
index 1b7025c9..f197f704 100644
--- a/src/components/_shared/Icon/Icon.module.scss
+++ b/src/components/_shared/Icon/Icon.module.scss
@@ -31,18 +31,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;
+ }
}
diff --git a/src/components/_shared/SolidSwiper/ImageSwiper.tsx b/src/components/_shared/SolidSwiper/ImageSwiper.tsx
index 1de1fe65..674ac701 100644
--- a/src/components/_shared/SolidSwiper/ImageSwiper.tsx
+++ b/src/components/_shared/SolidSwiper/ImageSwiper.tsx
@@ -1,7 +1,7 @@
import { clsx } from 'clsx'
import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
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 { MediaItem } from '../../../pages/types'
@@ -12,6 +12,8 @@ import { Lightbox } from '../Lightbox'
import { SwiperRef } from './swiper'
+import { useRouter } from '../../../stores/router'
+import { ArticlePageSearchParams } from '../../Article/FullArticle'
import styles from './Swiper.module.scss'
type Props = {
@@ -31,10 +33,13 @@ export const ImageSwiper = (props: Props) => {
const [slideIndex, setSlideIndex] = createSignal(0)
const [isMobileView, setIsMobileView] = createSignal(false)
const [selectedImage, setSelectedImage] = createSignal('')
+ const { searchParams, changeSearchParams } = useRouter()
const handleSlideChange = () => {
- thumbSwipeRef.current.swiper.slideTo(mainSwipeRef.current.swiper.activeIndex)
- setSlideIndex(mainSwipeRef.current.swiper.activeIndex)
+ const activeIndex = mainSwipeRef.current.swiper.activeIndex
+ thumbSwipeRef.current.swiper.slideTo(activeIndex)
+ setSlideIndex(activeIndex)
+ changeSearchParams({ slide: `${activeIndex + 1}` })
}
createEffect(
@@ -51,8 +56,19 @@ export const ImageSwiper = (props: Props) => {
onMount(async () => {
const { register } = await import('swiper/element/bundle')
register()
- SwiperCore.use([Pagination, Navigation, Manipulation])
- mainSwipeRef.current?.swiper?.on('slideChange', handleSlideChange)
+ SwiperCore.use([Pagination, Navigation, Manipulation, HashNavigation])
+ 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(() => {
@@ -103,6 +119,9 @@ export const ImageSwiper = (props: Props) => {
watch-slides-visibility={true}
direction={'horizontal'}
slides-per-group-auto={true}
+ hash-navigation={{
+ watchState: true,
+ }}
>
{(slide, index) => (
@@ -149,7 +168,7 @@ export const ImageSwiper = (props: Props) => {
{(slide, index) => (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
-
+
diff --git a/src/components/_shared/SolidSwiper/Swiper.module.scss b/src/components/_shared/SolidSwiper/Swiper.module.scss
index b8e5b990..f3f182e8 100644
--- a/src/components/_shared/SolidSwiper/Swiper.module.scss
+++ b/src/components/_shared/SolidSwiper/Swiper.module.scss
@@ -135,9 +135,13 @@
.counter {
@include font-size(1.2rem);
+ @include media-breakpoint-up(sm) {
+ top: 477px;
+ }
+
position: absolute;
z-index: 2;
- top: 477px;
+ top: 276px;
right: 0;
font-weight: 600;
padding: 0.2rem 0.8rem;
diff --git a/src/context/following.tsx b/src/context/following.tsx
index f3208ca9..fc92565e 100644
--- a/src/context/following.tsx
+++ b/src/context/following.tsx
@@ -2,14 +2,15 @@ import { Accessor, JSX, createContext, createEffect, createSignal, useContext }
import { createStore } from 'solid-js/store'
import { apiClient } from '../graphql/client/core'
-import { AuthorFollows, FollowingEntity } from '../graphql/schema/core.gen'
+import { Author, AuthorFollowsResult, FollowingEntity } from '../graphql/schema/core.gen'
import { useSession } from './session'
interface FollowingContextType {
loading: Accessor
- subscriptions: AuthorFollows
- setSubscriptions: (subscriptions: AuthorFollows) => void
+ followers: Accessor>
+ subscriptions: AuthorFollowsResult
+ setSubscriptions: (subscriptions: AuthorFollowsResult) => void
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
loadSubscriptions: () => void
follow: (what: FollowingEntity, slug: string) => Promise
@@ -23,7 +24,7 @@ export function useFollowing() {
return useContext(FollowingContext)
}
-const EMPTY_SUBSCRIPTIONS: AuthorFollows = {
+const EMPTY_SUBSCRIPTIONS: AuthorFollowsResult = {
topics: [],
authors: [],
communities: [],
@@ -31,7 +32,8 @@ const EMPTY_SUBSCRIPTIONS: AuthorFollows = {
export const FollowingProvider = (props: { children: JSX.Element }) => {
const [loading, setLoading] = createSignal(false)
- const [subscriptions, setSubscriptions] = createStore(EMPTY_SUBSCRIPTIONS)
+ const [followers, setFollowers] = createSignal>([])
+ const [subscriptions, setSubscriptions] = createStore(EMPTY_SUBSCRIPTIONS)
const { author, session } = useSession()
const fetchData = async () => {
@@ -77,8 +79,17 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
createEffect(() => {
if (author()) {
- console.debug('[context.following] author update detect')
- fetchData()
+ try {
+ 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)
+ }
}
})
@@ -116,6 +127,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
setSubscriptions,
isOwnerSubscribed,
setFollowing,
+ followers,
loadSubscriptions: fetchData,
follow,
unfollow,
diff --git a/src/context/session.tsx b/src/context/session.tsx
index 73659a6c..fa068bc8 100644
--- a/src/context/session.tsx
+++ b/src/context/session.tsx
@@ -199,6 +199,7 @@ export const SessionProvider = (props: {
}
onCleanup(() => clearTimeout(minuteLater))
+
const authorData = async () => {
const u = session()?.user
return u ? (await apiClient.getAuthorId({ user: u.id.trim() })) || null : null
@@ -217,7 +218,18 @@ export const SessionProvider = (props: {
apiClient.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)
}
@@ -263,7 +275,6 @@ export const SessionProvider = (props: {
() => {
props.onStateChangeCallback(session())
},
- { defer: true },
),
)
diff --git a/src/graphql/client/core.ts b/src/graphql/client/core.ts
index 62aa24a8..9d8a9953 100644
--- a/src/graphql/client/core.ts
+++ b/src/graphql/client/core.ts
@@ -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 => {
+ }): Promise => {
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
return response.data.get_author_follows
},
diff --git a/src/pages/author.page.tsx b/src/pages/author.page.tsx
index 39c4dc28..e23f92d3 100644
--- a/src/pages/author.page.tsx
+++ b/src/pages/author.page.tsx
@@ -56,17 +56,11 @@ export const AuthorPage = (props: PageProps) => {
onCleanup(() => resetSortedArticles())
- const usePrerenderedData = props.author?.slug === slug()
-
return (
}>
-
+
diff --git a/src/stores/router.ts b/src/stores/router.ts
index be197bd1..8be3b78c 100644
--- a/src/stores/router.ts
+++ b/src/stores/router.ts
@@ -114,8 +114,8 @@ const handleClientRouteLinkClick = async (event) => {
}
if (url.hash) {
- scrollToHash(url.hash)
- return
+ // scrollToHash(url.hash)
+ // return
}
window.scrollTo({
diff --git a/src/stores/zine/authors.ts b/src/stores/zine/authors.ts
index 131b70c6..2253b84e 100644
--- a/src/stores/zine/authors.ts
+++ b/src/stores/zine/authors.ts
@@ -1,5 +1,5 @@
import { createLazyMemo } from '@solid-primitives/memo'
-import { createSignal } from 'solid-js'
+import { createEffect, createSignal } from 'solid-js'
import { apiClient } from '../../graphql/client/core'
import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen'
diff --git a/src/styles/app.scss b/src/styles/app.scss
index db3a68bc..7afcb070 100644
--- a/src/styles/app.scss
+++ b/src/styles/app.scss
@@ -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;
}
diff --git a/src/utils/getImageUrl.ts b/src/utils/getImageUrl.ts
index 7b8b56dc..bc3c9073 100644
--- a/src/utils/getImageUrl.ts
+++ b/src/utils/getImageUrl.ts
@@ -15,17 +15,17 @@ export const getImageUrl = (
src: string,
options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {},
) => {
- if (!src.includes('discours.io') && src.includes('http')) {
- return src
- }
- const filename = src.toLowerCase().split('/').pop()
- const ext = filename.split('.').pop()
- const isAudio = ext in ['wav', 'mp3', 'ogg', 'aif', 'flac']
- const base = isAudio ? cdnUrl : `${thumborUrl}/unsafe/`
- const suffix = isAudio || options.noSizeUrlPart ? '' : getSizeUrlPart(options)
- const subfolder = isAudio ? 'audio' : 'image'
+ if (!src.includes('discours.io') && src.includes('http')) {
+ return src
+ }
+ const filename = src.toLowerCase().split('/').pop()
+ const ext = filename.split('.').pop()
+ const isAudio = ext in ['wav', 'mp3', 'ogg', 'aif', 'flac']
+ const base = isAudio ? cdnUrl : `${thumborUrl}/unsafe/`
+ const suffix = isAudio || options.noSizeUrlPart ? '' : getSizeUrlPart(options)
+ const subfolder = isAudio ? 'audio' : 'image'
- return `${base}${suffix}production/${subfolder}/${filename}`
+ return `${base}${suffix}production/${subfolder}/${filename}`
}
export const getOpenGraphImageUrl = (