dev
This commit is contained in:
parent
cdcf9afcc2
commit
090a8f2633
|
@ -23,7 +23,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
// 'plugin:@typescript-eslint/recommended-requiring-type-checking', // 23-01-2024 681 problems
|
// 'plugin:@typescript-eslint/recommended-requiring-type-checking', // 30-01-2024 699 problems
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
@ -40,12 +40,12 @@ module.exports = {
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
mocha: true,
|
// mocha: true,
|
||||||
},
|
},
|
||||||
globals: {},
|
globals: {},
|
||||||
rules: {
|
rules: {
|
||||||
// Solid
|
// Solid
|
||||||
'solid/reactivity': 'off', // too many 'should be used within JSX'
|
'solid/reactivity': 'off',
|
||||||
'solid/no-innerhtml': 'off',
|
'solid/no-innerhtml': 'off',
|
||||||
|
|
||||||
/** Unicorn **/
|
/** Unicorn **/
|
||||||
|
@ -65,6 +65,7 @@ module.exports = {
|
||||||
'unicorn/no-array-callback-reference': 'warn',
|
'unicorn/no-array-callback-reference': 'warn',
|
||||||
'unicorn/no-array-method-this-argument': 'warn',
|
'unicorn/no-array-method-this-argument': 'warn',
|
||||||
'unicorn/no-for-loop': 'off',
|
'unicorn/no-for-loop': 'off',
|
||||||
|
'unicorn/prefer-switch': 'warn',
|
||||||
|
|
||||||
'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }],
|
'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }],
|
||||||
'sonarjs/prefer-immediate-return': 'warn',
|
'sonarjs/prefer-immediate-return': 'warn',
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -16,3 +16,4 @@ stats.html
|
||||||
*.scss.d.ts
|
*.scss.d.ts
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
.jj
|
||||||
|
|
|
@ -149,7 +149,7 @@
|
||||||
"typograf": "7.1.0",
|
"typograf": "7.1.0",
|
||||||
"uniqolor": "1.1.0",
|
"uniqolor": "1.1.0",
|
||||||
"vike": "0.4.148",
|
"vike": "0.4.148",
|
||||||
"vite": "4.5.1",
|
"vite": "4.5.2",
|
||||||
"vite-plugin-mkcert": "1.16.0",
|
"vite-plugin-mkcert": "1.16.0",
|
||||||
"vite-plugin-sass-dts": "1.3.11",
|
"vite-plugin-sass-dts": "1.3.11",
|
||||||
"vite-plugin-solid": "2.7.2",
|
"vite-plugin-solid": "2.7.2",
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Dynamic } from 'solid-js/web'
|
||||||
import { ConfirmProvider } from '../context/confirm'
|
import { ConfirmProvider } from '../context/confirm'
|
||||||
import { ConnectProvider } from '../context/connect'
|
import { ConnectProvider } from '../context/connect'
|
||||||
import { EditorProvider } from '../context/editor'
|
import { EditorProvider } from '../context/editor'
|
||||||
|
import { FollowingProvider } from '../context/following'
|
||||||
import { InboxProvider } from '../context/inbox'
|
import { InboxProvider } from '../context/inbox'
|
||||||
import { LocalizeProvider } from '../context/localize'
|
import { LocalizeProvider } from '../context/localize'
|
||||||
import { MediaQueryProvider } from '../context/mediaQuery'
|
import { MediaQueryProvider } from '../context/mediaQuery'
|
||||||
|
@ -90,7 +91,7 @@ type Props = PageProps & { is404: boolean }
|
||||||
|
|
||||||
export const App = (props: Props) => {
|
export const App = (props: Props) => {
|
||||||
const { page, searchParams } = useRouter<RootSearchParams>()
|
const { page, searchParams } = useRouter<RootSearchParams>()
|
||||||
let is404 = props.is404
|
const is404 = createMemo(() => props.is404)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!searchParams().m) {
|
if (!searchParams().m) {
|
||||||
|
@ -106,8 +107,7 @@ export const App = (props: Props) => {
|
||||||
const pageComponent = createMemo(() => {
|
const pageComponent = createMemo(() => {
|
||||||
const result = pagesMap[page()?.route || 'home']
|
const result = pagesMap[page()?.route || 'home']
|
||||||
|
|
||||||
if (is404 || !result || page()?.path === '/404') {
|
if (is404() || !result || page()?.path === '/404') {
|
||||||
is404 = false
|
|
||||||
return FourOuFourPage
|
return FourOuFourPage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,6 +122,7 @@ export const App = (props: Props) => {
|
||||||
<SnackbarProvider>
|
<SnackbarProvider>
|
||||||
<ConfirmProvider>
|
<ConfirmProvider>
|
||||||
<SessionProvider onStateChangeCallback={console.log}>
|
<SessionProvider onStateChangeCallback={console.log}>
|
||||||
|
<FollowingProvider>
|
||||||
<ConnectProvider>
|
<ConnectProvider>
|
||||||
<NotificationsProvider>
|
<NotificationsProvider>
|
||||||
<EditorProvider>
|
<EditorProvider>
|
||||||
|
@ -131,6 +132,7 @@ export const App = (props: Props) => {
|
||||||
</EditorProvider>
|
</EditorProvider>
|
||||||
</NotificationsProvider>
|
</NotificationsProvider>
|
||||||
</ConnectProvider>
|
</ConnectProvider>
|
||||||
|
</FollowingProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</ConfirmProvider>
|
</ConfirmProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
|
|
|
@ -2,13 +2,12 @@ import { openPage } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createEffect, createMemo, createSignal, Match, Show, Switch } from 'solid-js'
|
import { createEffect, createMemo, createSignal, Match, Show, Switch } from 'solid-js'
|
||||||
|
|
||||||
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useMediaQuery } from '../../../context/mediaQuery'
|
import { useMediaQuery } from '../../../context/mediaQuery'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
|
import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { follow, unfollow } from '../../../stores/zine/common'
|
|
||||||
// import { capitalize } from '../../../utils/capitalize'
|
|
||||||
import { isCyrillic } from '../../../utils/cyrillic'
|
import { isCyrillic } from '../../../utils/cyrillic'
|
||||||
import { translit } from '../../../utils/ru2en'
|
import { translit } from '../../../utils/ru2en'
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
|
@ -33,7 +32,7 @@ type Props = {
|
||||||
export const AuthorBadge = (props: Props) => {
|
export const AuthorBadge = (props: Props) => {
|
||||||
const { mediaMatches } = useMediaQuery()
|
const { mediaMatches } = useMediaQuery()
|
||||||
const [isMobileView, setIsMobileView] = createSignal(false)
|
const [isMobileView, setIsMobileView] = createSignal(false)
|
||||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
const [followed, setFollowed] = createSignal(false)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setIsMobileView(!mediaMatches.sm)
|
setIsMobileView(!mediaMatches.sm)
|
||||||
|
@ -41,33 +40,14 @@ export const AuthorBadge = (props: Props) => {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
author,
|
author,
|
||||||
subscriptions,
|
actions: { requireAuthentication },
|
||||||
actions: { loadSubscriptions, requireAuthentication },
|
|
||||||
} = useSession()
|
} = useSession()
|
||||||
|
const { setFollowing } = useFollowing()
|
||||||
const { changeSearchParams } = useRouter()
|
const { changeSearchParams } = useRouter()
|
||||||
const { t, formatDate, lang } = useLocalize()
|
const { t, formatDate, lang } = useLocalize()
|
||||||
const subscribed = createMemo(() => {
|
|
||||||
const sss = subscriptions()
|
|
||||||
return sss?.authors.some((a: Author) => a?.slug === props.author.slug)
|
|
||||||
})
|
|
||||||
|
|
||||||
const subscribe = async (really = true) => {
|
|
||||||
setIsSubscribing(true)
|
|
||||||
|
|
||||||
await (really
|
|
||||||
? follow({ what: FollowingEntity.Author, slug: props.author.slug })
|
|
||||||
: unfollow({ what: FollowingEntity.Author, slug: props.author.slug }))
|
|
||||||
|
|
||||||
await loadSubscriptions()
|
|
||||||
setIsSubscribing(false)
|
|
||||||
}
|
|
||||||
const handleSubscribe = (really: boolean) => {
|
|
||||||
requireAuthentication(() => {
|
|
||||||
subscribe(really)
|
|
||||||
}, 'subscribe')
|
|
||||||
}
|
|
||||||
|
|
||||||
const initChat = () => {
|
const initChat = () => {
|
||||||
|
// eslint-disable-next-line solid/reactivity
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
openPage(router, `inbox`)
|
openPage(router, `inbox`)
|
||||||
changeSearchParams({
|
changeSearchParams({
|
||||||
|
@ -88,6 +68,14 @@ export const AuthorBadge = (props: Props) => {
|
||||||
return props.author.name
|
return props.author.name
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleFollowClick = () => {
|
||||||
|
const value = !followed()
|
||||||
|
requireAuthentication(() => {
|
||||||
|
setFollowed(value)
|
||||||
|
setFollowing(FollowingEntity.Author, props.author.slug, value)
|
||||||
|
}, 'subscribe')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.AuthorBadge, { [styles.nameOnly]: props.nameOnly })}>
|
<div class={clsx(styles.AuthorBadge, { [styles.nameOnly]: props.nameOnly })}>
|
||||||
<div class={styles.basicInfo}>
|
<div class={styles.basicInfo}>
|
||||||
|
@ -135,37 +123,24 @@ export const AuthorBadge = (props: Props) => {
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
<Show
|
<Show
|
||||||
when={!props.minimizeSubscribeButton}
|
when={!props.minimizeSubscribeButton}
|
||||||
fallback={
|
fallback={<CheckButton text={t('Follow')} checked={followed()} onClick={handleFollowClick} />}
|
||||||
<CheckButton
|
|
||||||
text={t('Follow')}
|
|
||||||
checked={subscribed()}
|
|
||||||
onClick={() => handleSubscribe(!subscribed())}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={subscribed()}
|
when={followed()}
|
||||||
fallback={
|
fallback={
|
||||||
<Button
|
<Button
|
||||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||||
size="S"
|
size="S"
|
||||||
value={
|
value={
|
||||||
<Show
|
<Show when={props.iconButtons} fallback={t('Subscribe')}>
|
||||||
when={props.iconButtons}
|
|
||||||
fallback={
|
|
||||||
<Show when={isSubscribing()} fallback={t('Subscribe')}>
|
|
||||||
{t('subscribing...')}
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon name="author-subscribe" class={stylesButton.icon} />
|
<Icon name="author-subscribe" class={stylesButton.icon} />
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
onClick={() => handleSubscribe(true)}
|
onClick={handleFollowClick}
|
||||||
isSubscribeButton={true}
|
isSubscribeButton={true}
|
||||||
class={clsx(styles.actionButton, {
|
class={clsx(styles.actionButton, {
|
||||||
[styles.iconed]: props.iconButtons,
|
[styles.iconed]: props.iconButtons,
|
||||||
[stylesButton.subscribed]: subscribed(),
|
[stylesButton.subscribed]: followed(),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -186,11 +161,11 @@ export const AuthorBadge = (props: Props) => {
|
||||||
<Icon name="author-unsubscribe" class={stylesButton.icon} />
|
<Icon name="author-unsubscribe" class={stylesButton.icon} />
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
onClick={() => handleSubscribe(false)}
|
onClick={handleFollowClick}
|
||||||
isSubscribeButton={true}
|
isSubscribeButton={true}
|
||||||
class={clsx(styles.actionButton, {
|
class={clsx(styles.actionButton, {
|
||||||
[styles.iconed]: props.iconButtons,
|
[styles.iconed]: props.iconButtons,
|
||||||
[stylesButton.subscribed]: subscribed(),
|
[stylesButton.subscribed]: followed(),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import type { Author } from '../../../graphql/schema/core.gen'
|
import type { Author, Community } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { openPage, redirectPage } from '@nanostores/router'
|
import { openPage, redirectPage } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, For, onMount, Show } 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 { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
|
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { SubscriptionFilter } from '../../../pages/types'
|
import { SubscriptionFilter } from '../../../pages/types'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { follow, unfollow } from '../../../stores/zine/common'
|
|
||||||
import { isCyrillic } from '../../../utils/cyrillic'
|
import { isCyrillic } from '../../../utils/cyrillic'
|
||||||
import { isAuthor } from '../../../utils/isAuthor'
|
import { isAuthor } from '../../../utils/isAuthor'
|
||||||
import { translit } from '../../../utils/ru2en'
|
import { translit } from '../../../utils/ru2en'
|
||||||
|
@ -33,32 +33,14 @@ export const AuthorCard = (props: Props) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const {
|
const {
|
||||||
author,
|
author,
|
||||||
subscriptions,
|
|
||||||
isSessionLoaded,
|
isSessionLoaded,
|
||||||
actions: { loadSubscriptions, requireAuthentication },
|
actions: { requireAuthentication },
|
||||||
} = useSession()
|
} = useSession()
|
||||||
|
const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([])
|
||||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
|
||||||
const [following, setFollowing] = createSignal<Array<Author | Topic>>(props.following)
|
|
||||||
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
||||||
|
|
||||||
const subscribed = createMemo<boolean>(() =>
|
|
||||||
subscriptions().authors.some((a: Author) => a?.slug === props.author.slug),
|
|
||||||
)
|
|
||||||
|
|
||||||
const subscribe = async (really = true) => {
|
|
||||||
setIsSubscribing(true)
|
|
||||||
|
|
||||||
await (really
|
|
||||||
? follow({ what: FollowingEntity.Author, slug: props.author.slug })
|
|
||||||
: unfollow({ what: FollowingEntity.Author, slug: props.author.slug }))
|
|
||||||
|
|
||||||
await loadSubscriptions()
|
|
||||||
setIsSubscribing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
|
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
|
||||||
|
const [followed, setFollowed] = createSignal()
|
||||||
|
const { setFollowing } = useFollowing()
|
||||||
const name = createMemo(() => {
|
const name = createMemo(() => {
|
||||||
if (lang() !== 'ru' && isCyrillic(props.author.name)) {
|
if (lang() !== 'ru' && isCyrillic(props.author.name)) {
|
||||||
if (props.author.name === 'Дискурс') {
|
if (props.author.name === 'Дискурс') {
|
||||||
|
@ -71,9 +53,12 @@ export const AuthorCard = (props: Props) => {
|
||||||
return props.author.name
|
return props.author.name
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMount(() => setAuthorSubs(props.following))
|
||||||
|
|
||||||
// TODO: reimplement AuthorCard
|
// TODO: reimplement AuthorCard
|
||||||
const { changeSearchParams } = useRouter()
|
const { changeSearchParams } = useRouter()
|
||||||
const initChat = () => {
|
const initChat = () => {
|
||||||
|
// eslint-disable-next-line solid/reactivity
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
openPage(router, `inbox`)
|
openPage(router, `inbox`)
|
||||||
changeSearchParams({
|
changeSearchParams({
|
||||||
|
@ -82,30 +67,30 @@ export const AuthorCard = (props: Props) => {
|
||||||
}, 'discussions')
|
}, 'discussions')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubscribe = () => {
|
|
||||||
requireAuthentication(() => {
|
|
||||||
subscribe(!subscribed())
|
|
||||||
}, 'subscribe')
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.following) {
|
if (props.following) {
|
||||||
if (subscriptionFilter() === 'users') {
|
if (subscriptionFilter() === 'authors') {
|
||||||
setFollowing(props.following.filter((s) => 'name' in s))
|
setAuthorSubs(props.following.filter((s) => 'name' in s))
|
||||||
} else if (subscriptionFilter() === 'topics') {
|
} else if (subscriptionFilter() === 'topics') {
|
||||||
setFollowing(props.following.filter((s) => 'title' in s))
|
setAuthorSubs(props.following.filter((s) => 'title' in s))
|
||||||
|
} else if (subscriptionFilter() === 'communities') {
|
||||||
|
setAuthorSubs(props.following.filter((s) => 'title' in s))
|
||||||
} else {
|
} else {
|
||||||
setFollowing(props.following)
|
setAuthorSubs(props.following)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const followButtonText = createMemo(() => {
|
const handleFollowClick = () => {
|
||||||
if (isSubscribing()) {
|
const value = !followed()
|
||||||
return t('subscribing...')
|
requireAuthentication(() => {
|
||||||
|
setFollowed(value)
|
||||||
|
setFollowing(FollowingEntity.Author, props.author.slug, value)
|
||||||
|
}, 'subscribe')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscribed()) {
|
const followButtonText = createMemo(() => {
|
||||||
|
if (followed()) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
||||||
|
@ -214,11 +199,11 @@ export const AuthorCard = (props: Props) => {
|
||||||
fallback={
|
fallback={
|
||||||
<div class={styles.authorActions}>
|
<div class={styles.authorActions}>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubscribe}
|
onClick={handleFollowClick}
|
||||||
value={followButtonText()}
|
value={followButtonText()}
|
||||||
isSubscribeButton={true}
|
isSubscribeButton={true}
|
||||||
class={clsx({
|
class={clsx({
|
||||||
[stylesButton.subscribed]: subscribed(),
|
[stylesButton.subscribed]: followed(),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
@ -279,8 +264,8 @@ export const AuthorCard = (props: Props) => {
|
||||||
</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() === 'users' })}>
|
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'authors' })}>
|
||||||
<button type="button" onClick={() => setSubscriptionFilter('users')}>
|
<button type="button" onClick={() => setSubscriptionFilter('authors')}>
|
||||||
{t('Authors')}
|
{t('Authors')}
|
||||||
</button>
|
</button>
|
||||||
<span class="view-switcher__counter">
|
<span class="view-switcher__counter">
|
||||||
|
@ -300,7 +285,7 @@ export const AuthorCard = (props: Props) => {
|
||||||
<div class={styles.listWrapper}>
|
<div class={styles.listWrapper}>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-24">
|
<div class="col-24">
|
||||||
<For each={following()}>
|
<For each={authorSubs()}>
|
||||||
{(subscription) =>
|
{(subscription) =>
|
||||||
isAuthor(subscription) ? (
|
isAuthor(subscription) ? (
|
||||||
<AuthorBadge author={subscription} />
|
<AuthorBadge author={subscription} />
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
import type { Shout } from '../../graphql/schema/core.gen'
|
import type { Shout } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { createComputed, createSignal, Show, For } from 'solid-js'
|
import { createSignal, createEffect, For, Show } from 'solid-js'
|
||||||
|
|
||||||
import { ArticleCard } from './ArticleCard'
|
import { ArticleCard } from './ArticleCard'
|
||||||
import { ArticleCardProps } from './ArticleCard/ArticleCard'
|
|
||||||
|
|
||||||
const x = [
|
const columnSizes = ['col-md-12', 'col-md-8', 'col-md-16']
|
||||||
['12', '12'],
|
|
||||||
['8', '16'],
|
|
||||||
['16', '8'],
|
|
||||||
]
|
|
||||||
|
|
||||||
export const Row2 = (props: {
|
export const Row2 = (props: {
|
||||||
articles: Shout[]
|
articles: Shout[]
|
||||||
|
@ -18,10 +13,10 @@ export const Row2 = (props: {
|
||||||
noAuthorLink?: boolean
|
noAuthorLink?: boolean
|
||||||
noauthor?: boolean
|
noauthor?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const [y, setY] = createSignal(0)
|
const [columnIndex, setColumnIndex] = createSignal(0)
|
||||||
|
|
||||||
// FIXME: random can break hydration
|
// Update column index on component mount
|
||||||
createComputed(() => setY(Math.floor(Math.random() * x.length)))
|
createEffect(() => setColumnIndex(Math.floor(Math.random() * columnSizes.length)))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.articles && props.articles.length > 0}>
|
<Show when={props.articles && props.articles.length > 0}>
|
||||||
|
@ -29,31 +24,16 @@ export const Row2 = (props: {
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<For each={props.articles}>
|
<For each={props.articles}>
|
||||||
{(a, i) => {
|
{(article, _idx) => {
|
||||||
// FIXME: refactor this, too ugly now
|
const className = columnSizes[props.isEqual ? 0 : columnIndex() % columnSizes.length]
|
||||||
const className = `col-md-${props.isEqual ? '12' : x[y()][i()]}`
|
const big = className === 'col-md-12' ? 'M' : 'L'
|
||||||
let desktopCoverSize: ArticleCardProps['desktopCoverSize']
|
const desktopCoverSize = className === 'col-md-8' ? 'S' : big
|
||||||
|
|
||||||
switch (className) {
|
|
||||||
case 'col-md-8': {
|
|
||||||
desktopCoverSize = 'S'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'col-md-12': {
|
|
||||||
desktopCoverSize = 'M'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
desktopCoverSize = 'L'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={className}>
|
<div class={className}>
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
article={a}
|
article={article}
|
||||||
settings={{
|
settings={{
|
||||||
isWithCover: props.isEqual || x[y()][i()] === '16',
|
isWithCover: props.isEqual || className === 'col-md-16',
|
||||||
nodate: props.isEqual || props.nodate,
|
nodate: props.isEqual || props.nodate,
|
||||||
noAuthorLink: props.noAuthorLink,
|
noAuthorLink: props.noAuthorLink,
|
||||||
noauthor: props.noauthor,
|
noauthor: props.noauthor,
|
||||||
|
|
|
@ -2,8 +2,9 @@ import { getPagePath } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createSignal, For, Show } from 'solid-js'
|
import { createSignal, For, Show } from 'solid-js'
|
||||||
|
|
||||||
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { Author } from '../../../graphql/schema/core.gen'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { useArticlesStore } from '../../../stores/zine/articles'
|
import { useArticlesStore } from '../../../stores/zine/articles'
|
||||||
import { useSeenStore } from '../../../stores/zine/seen'
|
import { useSeenStore } from '../../../stores/zine/seen'
|
||||||
|
@ -15,7 +16,7 @@ import styles from './Sidebar.module.scss'
|
||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { seen } = useSeenStore()
|
const { seen } = useSeenStore()
|
||||||
const { subscriptions } = useSession()
|
const { subscriptions } = useFollowing()
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const { articlesByTopic } = useArticlesStore()
|
const { articlesByTopic } = useArticlesStore()
|
||||||
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
|
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
|
||||||
|
@ -27,7 +28,6 @@ export const Sidebar = () => {
|
||||||
const checkAuthorIsSeen = (authorSlug: string) => {
|
const checkAuthorIsSeen = (authorSlug: string) => {
|
||||||
return Boolean(seen()[authorSlug])
|
return Boolean(seen()[authorSlug])
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.sidebar}>
|
<div class={styles.sidebar}>
|
||||||
<ul class={styles.feedFilters}>
|
<ul class={styles.feedFilters}>
|
||||||
|
@ -111,7 +111,7 @@ export const Sidebar = () => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Show when={subscriptions().authors.length > 0 || subscriptions().topics.length > 0}>
|
<Show when={subscriptions.authors.length > 0 || subscriptions.topics.length > 0}>
|
||||||
<h4
|
<h4
|
||||||
classList={{ [styles.opened]: isSubscriptionsVisible() }}
|
classList={{ [styles.opened]: isSubscriptionsVisible() }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -122,22 +122,19 @@ export const Sidebar = () => {
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}>
|
<ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}>
|
||||||
<For each={subscriptions().authors}>
|
<For each={subscriptions.authors}>
|
||||||
{(author) => (
|
{(a: Author) => (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={`/author/${a.slug}`} classList={{ [styles.unread]: checkAuthorIsSeen(a.slug) }}>
|
||||||
href={`/author/${author.slug}`}
|
|
||||||
classList={{ [styles.unread]: checkAuthorIsSeen(author.slug) }}
|
|
||||||
>
|
|
||||||
<div class={styles.sidebarItemName}>
|
<div class={styles.sidebarItemName}>
|
||||||
<Userpic name={author.name} userpic={author.pic} size="XS" class={styles.userpic} />
|
<Userpic name={a.name} userpic={a.pic} size="XS" class={styles.userpic} />
|
||||||
<div class={styles.sidebarItemNameLabel}>{author.name}</div>
|
<div class={styles.sidebarItemNameLabel}>{a.name}</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<For each={subscriptions().topics}>
|
<For each={subscriptions.topics}>
|
||||||
{(topic) => (
|
{(topic) => (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -148,8 +148,10 @@ export const Header = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
if (window.location.pathname === '/' || window.location.pathname === '') {
|
||||||
const topics = await apiClient.getRandomTopics({ amount: RANDOM_TOPICS_COUNT })
|
const topics = await apiClient.getRandomTopics({ amount: RANDOM_TOPICS_COUNT })
|
||||||
setRandomTopics(topics)
|
setRandomTopics(topics)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleToggleMenuByLink = (event: MouseEvent, route: keyof typeof ROUTES) => {
|
const handleToggleMenuByLink = (event: MouseEvent, route: keyof typeof ROUTES) => {
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const ProfileSettings = () => {
|
||||||
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
|
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
|
||||||
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
||||||
const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false)
|
const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false)
|
||||||
const [userpicFile, setUserpicFile] = createSignal<any | null>(null)
|
const [userpicFile, setUserpicFile] = createSignal(null)
|
||||||
const [uploadError, setUploadError] = createSignal(false)
|
const [uploadError, setUploadError] = createSignal(false)
|
||||||
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
|
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
|
||||||
const [hostname, setHostname] = createSignal<string | null>(null)
|
const [hostname, setHostname] = createSignal<string | null>(null)
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import type { Topic } from '../../graphql/schema/core.gen'
|
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createMemo, createSignal, Show } from 'solid-js'
|
import { createMemo, createSignal, Show } 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 { FollowingEntity } from '../../graphql/schema/core.gen'
|
import { FollowingEntity, type Topic } from '../../graphql/schema/core.gen'
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
|
||||||
import { capitalize } from '../../utils/capitalize'
|
import { capitalize } from '../../utils/capitalize'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
import { CheckButton } from '../_shared/CheckButton'
|
import { CheckButton } from '../_shared/CheckButton'
|
||||||
|
@ -36,32 +34,21 @@ interface TopicProps {
|
||||||
|
|
||||||
export const TopicCard = (props: TopicProps) => {
|
export const TopicCard = (props: TopicProps) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
|
const title = createMemo(() =>
|
||||||
|
capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''),
|
||||||
|
)
|
||||||
const {
|
const {
|
||||||
subscriptions,
|
author,
|
||||||
isSessionLoaded,
|
actions: { requireAuthentication },
|
||||||
actions: { loadSubscriptions, requireAuthentication },
|
|
||||||
} = useSession()
|
} = useSession()
|
||||||
|
const { setFollowing, loading: subLoading } = useFollowing()
|
||||||
|
const [followed, setFollowed] = createSignal()
|
||||||
|
|
||||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
const handleFollowClick = () => {
|
||||||
|
const value = !followed()
|
||||||
const subscribed = createMemo(() => {
|
|
||||||
return subscriptions().topics.some((topic) => topic.slug === props.topic.slug)
|
|
||||||
})
|
|
||||||
|
|
||||||
const subscribe = async (really = true) => {
|
|
||||||
setIsSubscribing(true)
|
|
||||||
|
|
||||||
await (really
|
|
||||||
? follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
|
||||||
: unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }))
|
|
||||||
|
|
||||||
await loadSubscriptions()
|
|
||||||
setIsSubscribing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubscribe = () => {
|
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
subscribe(!subscribed())
|
setFollowed(value)
|
||||||
|
setFollowing(FollowingEntity.Topic, props.topic.slug, value)
|
||||||
}, 'subscribe')
|
}, 'subscribe')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,12 +56,12 @@ export const TopicCard = (props: TopicProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={props.iconButton}>
|
<Show when={props.iconButton}>
|
||||||
<Show when={subscribed()} fallback="+">
|
<Show when={followed()} fallback="+">
|
||||||
<Icon name="check-subscribed" />
|
<Icon name="check-subscribed" />
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!props.iconButton}>
|
<Show when={!props.iconButton}>
|
||||||
<Show when={subscribed()} fallback={t('Follow')}>
|
<Show when={followed()} fallback={t('Follow')}>
|
||||||
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
|
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
|
||||||
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -83,10 +70,6 @@ export const TopicCard = (props: TopicProps) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = createMemo(() =>
|
|
||||||
capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.topicContainer}>
|
<div class={styles.topicContainer}>
|
||||||
<div
|
<div
|
||||||
|
@ -141,24 +124,28 @@ export const TopicCard = (props: TopicProps) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ShowOnlyOnClient>
|
<ShowOnlyOnClient>
|
||||||
<Show when={isSessionLoaded()}>
|
<Show when={author()}>
|
||||||
<Show
|
<Show
|
||||||
when={!props.minimizeSubscribeButton}
|
when={!props.minimizeSubscribeButton}
|
||||||
fallback={
|
fallback={
|
||||||
<CheckButton text={t('Follow')} checked={subscribed()} onClick={handleSubscribe} />
|
<CheckButton
|
||||||
|
text={t('Follow')}
|
||||||
|
checked={Boolean(followed())}
|
||||||
|
onClick={handleFollowClick}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
size="M"
|
size="M"
|
||||||
value={subscribeValue()}
|
value={subscribeValue()}
|
||||||
onClick={handleSubscribe}
|
onClick={handleFollowClick}
|
||||||
isSubscribeButton={true}
|
isSubscribeButton={true}
|
||||||
class={clsx(styles.actionButton, {
|
class={clsx(styles.actionButton, {
|
||||||
[styles.isSubscribing]: isSubscribing(),
|
[styles.isSubscribing]: subLoading(),
|
||||||
[stylesButton.subscribed]: subscribed(),
|
[stylesButton.subscribed]: followed(),
|
||||||
})}
|
})}
|
||||||
disabled={isSubscribing()}
|
// disabled={subLoading()}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import type { Topic } from '../../graphql/schema/core.gen'
|
import type { Topic } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createMemo, Show } from 'solid-js'
|
import { createEffect, createSignal, Show } 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 { FollowingEntity } from '../../graphql/schema/core.gen'
|
import { FollowingEntity } from '../../graphql/schema/core.gen'
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
|
|
||||||
import styles from './Full.module.scss'
|
import styles from './Full.module.scss'
|
||||||
|
@ -16,23 +16,26 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FullTopic = (props: Props) => {
|
export const FullTopic = (props: Props) => {
|
||||||
const {
|
|
||||||
subscriptions,
|
|
||||||
actions: { requireAuthentication, loadSubscriptions },
|
|
||||||
} = useSession()
|
|
||||||
|
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
const { subscriptions, setFollowing } = useFollowing()
|
||||||
|
const {
|
||||||
|
actions: { requireAuthentication },
|
||||||
|
} = useSession()
|
||||||
|
const [followed, setFollowed] = createSignal()
|
||||||
|
|
||||||
const subscribed = createMemo(() =>
|
createEffect(() => {
|
||||||
subscriptions().topics.some((topic) => topic.slug === props.topic?.slug),
|
const subs = subscriptions
|
||||||
)
|
if (subs?.topics.length !== 0) {
|
||||||
|
const items = subs.topics || []
|
||||||
|
setFollowed(items.some((x: Topic) => x?.slug === props.topic?.slug))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const handleSubscribe = (really: boolean) => {
|
const handleFollowClick = (_ev) => {
|
||||||
requireAuthentication(async () => {
|
const really = !followed()
|
||||||
await (really
|
setFollowed(really)
|
||||||
? follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
requireAuthentication(() => {
|
||||||
: unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }))
|
setFollowing(FollowingEntity.Topic, props.topic.slug, really)
|
||||||
loadSubscriptions()
|
|
||||||
}, 'follow')
|
}, 'follow')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,16 +44,11 @@ export const FullTopic = (props: Props) => {
|
||||||
<h1>#{props.topic?.title}</h1>
|
<h1>#{props.topic?.title}</h1>
|
||||||
<p>{props.topic?.body}</p>
|
<p>{props.topic?.body}</p>
|
||||||
<div class={clsx(styles.topicActions)}>
|
<div class={clsx(styles.topicActions)}>
|
||||||
<Show when={!subscribed()}>
|
|
||||||
<Button variant="primary" onClick={() => handleSubscribe(true)} value={t('Follow the topic')} />
|
|
||||||
</Show>
|
|
||||||
<Show when={subscribed()}>
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => handleSubscribe(false)}
|
onClick={handleFollowClick}
|
||||||
value={t('Unfollow the topic')}
|
value={followed() ? t('Unfollow the topic') : t('Follow the topic')}
|
||||||
/>
|
/>
|
||||||
</Show>
|
|
||||||
<a class={styles.write} href={`/create/?topicId=${props.topic?.id}`}>
|
<a class={styles.write} href={`/create/?topicId=${props.topic?.id}`}>
|
||||||
{t('Write about the topic')}
|
{t('Write about the topic')}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
|
import { createEffect, createSignal, Show } from 'solid-js'
|
||||||
|
|
||||||
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useMediaQuery } from '../../../context/mediaQuery'
|
import { useMediaQuery } from '../../../context/mediaQuery'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
|
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { follow, unfollow } from '../../../stores/zine/common'
|
|
||||||
import { capitalize } from '../../../utils/capitalize'
|
import { capitalize } from '../../../utils/capitalize'
|
||||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
import { CheckButton } from '../../_shared/CheckButton'
|
import { CheckButton } from '../../_shared/CheckButton'
|
||||||
|
|
||||||
import styles from './TopicBadge.module.scss'
|
import styles from './TopicBadge.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
minimizeSubscribeButton?: boolean
|
minimizeSubscribeButton?: boolean
|
||||||
|
@ -21,29 +22,23 @@ export const TopicBadge = (props: Props) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const { mediaMatches } = useMediaQuery()
|
const { mediaMatches } = useMediaQuery()
|
||||||
const [isMobileView, setIsMobileView] = createSignal(false)
|
const [isMobileView, setIsMobileView] = createSignal(false)
|
||||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
const {
|
||||||
|
actions: { requireAuthentication },
|
||||||
|
} = useSession()
|
||||||
|
const { setFollowing, loading: subLoading } = useFollowing()
|
||||||
|
const [followed, setFollowed] = createSignal()
|
||||||
|
|
||||||
|
const handleFollowClick = () => {
|
||||||
|
const value = !followed()
|
||||||
|
requireAuthentication(() => {
|
||||||
|
setFollowed(value)
|
||||||
|
setFollowing(FollowingEntity.Topic, props.topic.slug, value)
|
||||||
|
}, 'subscribe')
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setIsMobileView(!mediaMatches.sm)
|
setIsMobileView(!mediaMatches.sm)
|
||||||
})
|
})
|
||||||
const {
|
|
||||||
subscriptions,
|
|
||||||
actions: { loadSubscriptions },
|
|
||||||
} = useSession()
|
|
||||||
|
|
||||||
const subscribed = createMemo(() =>
|
|
||||||
subscriptions().topics.some((topic) => topic.slug === props.topic.slug),
|
|
||||||
)
|
|
||||||
|
|
||||||
const subscribe = async (really = true) => {
|
|
||||||
setIsSubscribing(true)
|
|
||||||
|
|
||||||
await (really
|
|
||||||
? follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
|
||||||
: unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }))
|
|
||||||
|
|
||||||
await loadSubscriptions()
|
|
||||||
setIsSubscribing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = () =>
|
const title = () =>
|
||||||
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
|
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
|
||||||
|
@ -82,23 +77,23 @@ export const TopicBadge = (props: Props) => {
|
||||||
<Show
|
<Show
|
||||||
when={!props.minimizeSubscribeButton}
|
when={!props.minimizeSubscribeButton}
|
||||||
fallback={
|
fallback={
|
||||||
<CheckButton text={t('Follow')} checked={subscribed()} onClick={() => subscribe(!subscribed)} />
|
<CheckButton text={t('Follow')} checked={Boolean(followed())} onClick={handleFollowClick} />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={subscribed()}
|
when={followed()}
|
||||||
fallback={
|
fallback={
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="S"
|
size="S"
|
||||||
value={isSubscribing() ? t('subscribing...') : t('Subscribe')}
|
value={subLoading() ? t('subscribing...') : t('Subscribe')}
|
||||||
onClick={() => subscribe(true)}
|
onClick={handleFollowClick}
|
||||||
class={styles.subscribeButton}
|
class={styles.subscribeButton}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => subscribe(false)}
|
onClick={handleFollowClick}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
size="S"
|
size="S"
|
||||||
value={t('Following')}
|
value={t('Following')}
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { Meta } from '@solidjs/meta'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||||
|
|
||||||
|
import { useFollowing } from '../../context/following'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useSession } from '../../context/session'
|
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
|
import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { capitalize } from '../../utils/capitalize'
|
import { capitalize } from '../../utils/capitalize'
|
||||||
|
@ -41,7 +41,7 @@ export const AllTopicsView = (props: Props) => {
|
||||||
sortBy: searchParams().by || 'shouts',
|
sortBy: searchParams().by || 'shouts',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { subscriptions } = useSession()
|
const { subscriptions } = useFollowing()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!searchParams().by) {
|
if (!searchParams().by) {
|
||||||
|
@ -76,7 +76,7 @@ export const AllTopicsView = (props: Props) => {
|
||||||
return keys
|
return keys
|
||||||
})
|
})
|
||||||
|
|
||||||
const subscribed = (topicSlug: string) => subscriptions().topics.some((topic) => topic.slug === topicSlug)
|
const subscribed = (topicSlug: string) => subscriptions.topics.some((topic) => topic.slug === topicSlug)
|
||||||
|
|
||||||
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
||||||
const [searchQuery, setSearchQuery] = createSignal('')
|
const [searchQuery, setSearchQuery] = createSignal('')
|
||||||
|
@ -191,7 +191,7 @@ export const AllTopicsView = (props: Props) => {
|
||||||
{(topic) => (
|
{(topic) => (
|
||||||
<>
|
<>
|
||||||
<TopicCard
|
<TopicCard
|
||||||
topic={topic as Topic}
|
topic={topic}
|
||||||
compact={false}
|
compact={false}
|
||||||
subscribed={subscribed(topic.slug)}
|
subscribed={subscribed(topic.slug)}
|
||||||
showPublications={true}
|
showPublications={true}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { Author, Shout, Topic } from '../../../graphql/schema/core.gen'
|
import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { Meta } from '@solidjs/meta'
|
import { Meta } from '@solidjs/meta'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect } from 'solid-js'
|
import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect, on } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { apiClient } from '../../../graphql/client/core'
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
|
@ -13,6 +13,7 @@ import { loadAuthor, useAuthorsStore } from '../../../stores/zine/authors'
|
||||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
import { getDescription } from '../../../utils/meta'
|
import { getDescription } from '../../../utils/meta'
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
|
||||||
|
import { byCreated } from '../../../utils/sortby'
|
||||||
import { splitToPages } from '../../../utils/splitToPages'
|
import { splitToPages } from '../../../utils/splitToPages'
|
||||||
import { Loading } from '../../_shared/Loading'
|
import { Loading } from '../../_shared/Loading'
|
||||||
import { Comment } from '../../Article/Comment'
|
import { Comment } from '../../Article/Comment'
|
||||||
|
@ -44,21 +45,32 @@ export const AuthorView = (props: Props) => {
|
||||||
const [followers, setFollowers] = createSignal<Author[]>([])
|
const [followers, setFollowers] = createSignal<Author[]>([])
|
||||||
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
|
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
|
||||||
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
|
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
|
||||||
const author = createMemo(() => authorEntities()[props.authorSlug])
|
|
||||||
|
|
||||||
createEffect(async () => {
|
// current author
|
||||||
|
const [author, setAuthor] = createSignal<Author>()
|
||||||
|
createEffect(() => {
|
||||||
|
try {
|
||||||
|
const a = authorEntities()[props.authorSlug]
|
||||||
|
setAuthor(a)
|
||||||
|
} catch (error) {
|
||||||
|
console.debug(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
if (author() && author().id && !author().stat) {
|
if (author() && author().id && !author().stat) {
|
||||||
const a = await loadAuthor({ slug: '', author_id: author().id })
|
const a = loadAuthor({ slug: '', author_id: author().id })
|
||||||
console.debug(`[AuthorView] loaded author:`, a)
|
console.debug(`[AuthorView] loaded author:`, a)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const bioContainerRef: { current: HTMLDivElement } = { current: null }
|
const bioContainerRef: { current: HTMLDivElement } = { current: null }
|
||||||
const bioWrapperRef: { current: HTMLDivElement } = { current: null }
|
const bioWrapperRef: { current: HTMLDivElement } = { current: null }
|
||||||
|
|
||||||
const fetchSubscriptions = async (): Promise<{ authors: Author[]; topics: Topic[] }> => {
|
const fetchSubscriptions = async (): Promise<{ authors: Author[]; topics: Topic[] }> => {
|
||||||
try {
|
try {
|
||||||
const [getAuthors, getTopics] = await Promise.all([
|
const [getAuthors, getTopics] = await Promise.all([
|
||||||
apiClient.getAuthorFollowingUsers({ slug: props.authorSlug }),
|
apiClient.getAuthorFollowingAuthors({ slug: props.authorSlug }),
|
||||||
apiClient.getAuthorFollowingTopics({ slug: props.authorSlug }),
|
apiClient.getAuthorFollowingTopics({ slug: props.authorSlug }),
|
||||||
])
|
])
|
||||||
const authors = getAuthors
|
const authors = getAuthors
|
||||||
|
@ -76,32 +88,27 @@ export const AuthorView = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
const fetchData = async () => {
|
||||||
checkBioHeight()
|
const slug = author()?.slug || props.authorSlug
|
||||||
|
if (slug && getPage().route === 'authorComments' && author()) {
|
||||||
// pagination
|
|
||||||
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
|
||||||
await loadMore()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(async () => {
|
|
||||||
const slug = author()?.slug
|
|
||||||
if (slug) {
|
|
||||||
console.debug('[AuthorView] load subscriptions')
|
|
||||||
try {
|
try {
|
||||||
const { authors, topics } = await fetchSubscriptions()
|
const { authors, topics } = await fetchSubscriptions()
|
||||||
setFollowing([...(authors || []), ...(topics || [])])
|
setFollowing([...(authors || []), ...(topics || [])])
|
||||||
const userSubscribers = await apiClient.getAuthorFollowers({ slug })
|
const flwrs = await apiClient.getAuthorFollowers({ slug })
|
||||||
setFollowers(userSubscribers || [])
|
setFollowers(flwrs || [])
|
||||||
|
console.info('[components.Author] following data loaded')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AuthorView] error:', error)
|
console.error('[components.Author] fetch error', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
document.title = author()?.name
|
if (author()) {
|
||||||
|
console.info('[components.Author] profile data loaded')
|
||||||
|
document.title = author().name
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
|
@ -115,42 +122,66 @@ export const AuthorView = (props: Props) => {
|
||||||
restoreScrollPosition()
|
restoreScrollPosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
checkBioHeight()
|
||||||
|
|
||||||
|
// pagination
|
||||||
|
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
||||||
|
fetchData()
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const pages = createMemo<Shout[][]>(() =>
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE),
|
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE),
|
||||||
)
|
)
|
||||||
|
|
||||||
const [commented, setCommented] = createSignal([])
|
const fetchComments = async (commenter: Author) => {
|
||||||
|
|
||||||
createEffect(async () => {
|
|
||||||
if (getPage().route === 'authorComments' && props.author) {
|
|
||||||
try {
|
|
||||||
const data = await apiClient.getReactionsBy({
|
const data = await apiClient.getReactionsBy({
|
||||||
by: { comment: true, created_by: props.author.id },
|
by: { comment: true, created_by: commenter.id },
|
||||||
})
|
})
|
||||||
|
console.debug(`[components.Author] fetched ${data.length} comments`)
|
||||||
setCommented(data)
|
setCommented(data)
|
||||||
} catch (error) {
|
|
||||||
console.error('[getReactionsBy comment]', error)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const ogImage = props.author?.pic
|
const [commented, setCommented] = createSignal<Reaction[]>([])
|
||||||
? getImageUrl(props.author.pic, { width: 1200 })
|
createEffect(
|
||||||
: getImageUrl('production/image/logo_image.png')
|
on(
|
||||||
const description = getDescription(props.author?.bio)
|
author,
|
||||||
const ogTitle = props.author?.name
|
(a: Author) => {
|
||||||
|
if (getPage().route === 'authorComments') {
|
||||||
|
console.debug('[components.Author] routed to comments')
|
||||||
|
try {
|
||||||
|
if (a) fetchComments(a)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[components.Author] fetch error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const ogImage = createMemo(() =>
|
||||||
|
author()?.pic
|
||||||
|
? getImageUrl(author()?.pic, { width: 1200 })
|
||||||
|
: getImageUrl('production/image/logo_image.png'),
|
||||||
|
)
|
||||||
|
const description = createMemo(() => getDescription(author()?.bio))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.authorPage}>
|
<div class={styles.authorPage}>
|
||||||
<Meta name="descprition" content={description} />
|
<Show when={author()}>
|
||||||
|
<Meta name="descprition" content={description()} />
|
||||||
<Meta name="og:type" content="profile" />
|
<Meta name="og:type" content="profile" />
|
||||||
<Meta name="og:title" content={ogTitle} />
|
<Meta name="og:title" content={author().name} />
|
||||||
<Meta name="og:image" content={ogImage} />
|
<Meta name="og:image" content={ogImage()} />
|
||||||
<Meta name="og:description" content={description} />
|
<Meta name="og:description" content={description()} />
|
||||||
<Meta name="twitter:card" content="summary_large_image" />
|
<Meta name="twitter:card" content="summary_large_image" />
|
||||||
<Meta name="twitter:title" content={ogTitle} />
|
<Meta name="twitter:title" content={author().name} />
|
||||||
<Meta name="twitter:description" content={description} />
|
<Meta name="twitter:description" content={description()} />
|
||||||
<Meta name="twitter:image" content={ogImage} />
|
<Meta name="twitter:image" content={ogImage()} />
|
||||||
|
</Show>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<Show when={author()} fallback={<Loading />}>
|
<Show when={author()} fallback={<Loading />}>
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -28,9 +28,10 @@ export const ProfileSubscriptions = () => {
|
||||||
|
|
||||||
const fetchSubscriptions = async () => {
|
const fetchSubscriptions = async () => {
|
||||||
try {
|
try {
|
||||||
|
const slug = author()?.slug
|
||||||
const [getAuthors, getTopics] = await Promise.all([
|
const [getAuthors, getTopics] = await Promise.all([
|
||||||
apiClient.getAuthorFollowingUsers({ slug: author()?.slug }),
|
apiClient.getAuthorFollowingAuthors({ slug }),
|
||||||
apiClient.getAuthorFollowingTopics({ slug: author()?.slug }),
|
apiClient.getAuthorFollowingTopics({ slug }),
|
||||||
])
|
])
|
||||||
setFollowing([...getAuthors, ...getTopics])
|
setFollowing([...getAuthors, ...getTopics])
|
||||||
setFiltered([...getAuthors, ...getTopics])
|
setFiltered([...getAuthors, ...getTopics])
|
||||||
|
@ -42,7 +43,7 @@ export const ProfileSubscriptions = () => {
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (following()) {
|
if (following()) {
|
||||||
if (subscriptionFilter() === 'users') {
|
if (subscriptionFilter() === 'authors') {
|
||||||
setFiltered(following().filter((s) => 'name' in s))
|
setFiltered(following().filter((s) => 'name' in s))
|
||||||
} else if (subscriptionFilter() === 'topics') {
|
} else if (subscriptionFilter() === 'topics') {
|
||||||
setFiltered(following().filter((s) => 'title' in s))
|
setFiltered(following().filter((s) => 'title' in s))
|
||||||
|
@ -80,8 +81,8 @@ export const ProfileSubscriptions = () => {
|
||||||
{t('All')}
|
{t('All')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'users' })}>
|
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'authors' })}>
|
||||||
<button type="button" onClick={() => setSubscriptionFilter('users')}>
|
<button type="button" onClick={() => setSubscriptionFilter('authors')}>
|
||||||
{t('Authors')}
|
{t('Authors')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { redirectPage } from '@nanostores/router'
|
import { redirectPage } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createEffect, createSignal, lazy, onMount, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, lazy, onMount, Show } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
||||||
|
@ -16,7 +16,6 @@ import { Icon } from '../../_shared/Icon'
|
||||||
import { Image } from '../../_shared/Image'
|
import { Image } from '../../_shared/Image'
|
||||||
import { TopicSelect, UploadModalContent } from '../../Editor'
|
import { TopicSelect, UploadModalContent } from '../../Editor'
|
||||||
import { Modal } from '../../Nav/Modal'
|
import { Modal } from '../../Nav/Modal'
|
||||||
import { EMPTY_TOPIC } from '../Edit'
|
|
||||||
|
|
||||||
import styles from './PublishSettings.module.scss'
|
import styles from './PublishSettings.module.scss'
|
||||||
import stylesBeside from '../../Feed/Beside.module.scss'
|
import stylesBeside from '../../Feed/Beside.module.scss'
|
||||||
|
@ -36,21 +35,26 @@ const shorten = (str: string, maxLen: number) => {
|
||||||
return `${result}...`
|
return `${result}...`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMPTY_TOPIC: Topic = {
|
||||||
|
id: -1,
|
||||||
|
slug: '',
|
||||||
|
}
|
||||||
|
const emptyConfig = {
|
||||||
|
coverImageUrl: '',
|
||||||
|
mainTopic: EMPTY_TOPIC,
|
||||||
|
slug: '',
|
||||||
|
title: '',
|
||||||
|
subtitle: '',
|
||||||
|
description: '',
|
||||||
|
selectedTopics: [],
|
||||||
|
}
|
||||||
|
|
||||||
export const PublishSettings = (props: Props) => {
|
export const PublishSettings = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { author } = useSession()
|
const { author } = useSession()
|
||||||
const { sortedTopics } = useTopicsStore()
|
const { sortedTopics } = useTopicsStore()
|
||||||
|
|
||||||
const [topics, setTopics] = createSignal<Topic[]>(sortedTopics())
|
const [topics, setTopics] = createSignal<Topic[]>(sortedTopics())
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await loadAllTopics()
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
setTopics(sortedTopics())
|
|
||||||
})
|
|
||||||
|
|
||||||
const composeDescription = () => {
|
const composeDescription = () => {
|
||||||
if (!props.form.description) {
|
if (!props.form.description) {
|
||||||
const cleanFootnotes = props.form.body.replaceAll(/<footnote data-value=".*?">.*?<\/footnote>/g, '')
|
const cleanFootnotes = props.form.body.replaceAll(/<footnote data-value=".*?">.*?<\/footnote>/g, '')
|
||||||
|
@ -60,23 +64,32 @@ export const PublishSettings = (props: Props) => {
|
||||||
return props.form.description
|
return props.form.description
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialData: Partial<ShoutForm> = {
|
const initialData = createMemo(() => {
|
||||||
coverImageUrl: props.form.coverImageUrl,
|
return {
|
||||||
mainTopic: props.form.mainTopic || EMPTY_TOPIC,
|
coverImageUrl: props.form?.coverImageUrl,
|
||||||
slug: props.form.slug,
|
mainTopic: props.form?.mainTopic || EMPTY_TOPIC,
|
||||||
title: props.form.title,
|
slug: props.form?.slug,
|
||||||
subtitle: props.form.subtitle,
|
title: props.form?.title,
|
||||||
|
subtitle: props.form?.subtitle,
|
||||||
description: composeDescription(),
|
description: composeDescription(),
|
||||||
selectedTopics: [],
|
selectedTopics: [],
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const [settingsForm, setSettingsForm] = createStore(emptyConfig)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setSettingsForm(initialData())
|
||||||
|
loadAllTopics()
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => setTopics(sortedTopics()))
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formErrors,
|
formErrors,
|
||||||
actions: { setForm, setFormErrors, saveShout, publishShout },
|
actions: { setForm, setFormErrors, saveShout, publishShout },
|
||||||
} = useEditorContext()
|
} = useEditorContext()
|
||||||
|
|
||||||
const [settingsForm, setSettingsForm] = createStore(initialData)
|
|
||||||
|
|
||||||
const handleUploadModalContentCloseSetCover = (image: UploadedFile) => {
|
const handleUploadModalContentCloseSetCover = (image: UploadedFile) => {
|
||||||
hideModal()
|
hideModal()
|
||||||
setSettingsForm('coverImageUrl', image.url)
|
setSettingsForm('coverImageUrl', image.url)
|
||||||
|
@ -110,7 +123,7 @@ export const PublishSettings = (props: Props) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const handleCancelClick = () => {
|
const handleCancelClick = () => {
|
||||||
setSettingsForm(initialData)
|
setSettingsForm(initialData())
|
||||||
handleBackClick()
|
handleBackClick()
|
||||||
}
|
}
|
||||||
const handlePublishSubmit = () => {
|
const handlePublishSubmit = () => {
|
||||||
|
@ -149,9 +162,9 @@ export const PublishSettings = (props: Props) => {
|
||||||
[styles.hasImage]: settingsForm.coverImageUrl,
|
[styles.hasImage]: settingsForm.coverImageUrl,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Show when={settingsForm.coverImageUrl ?? initialData.coverImageUrl}>
|
<Show when={settingsForm.coverImageUrl ?? initialData().coverImageUrl}>
|
||||||
<div class={styles.shoutCardCover}>
|
<div class={styles.shoutCardCover}>
|
||||||
<Image src={settingsForm.coverImageUrl} alt={initialData.title} width={800} />
|
<Image src={settingsForm.coverImageUrl} alt={initialData().title} width={800} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={styles.text}>
|
<div class={styles.text}>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { 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, createMemo, onMount, createSignal } from 'solid-js'
|
import { For, Show, createMemo, onMount, createSignal, createEffect } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
|
@ -39,23 +39,27 @@ const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
|
||||||
export const TopicView = (props: Props) => {
|
export const TopicView = (props: Props) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const { searchParams, changeSearchParams } = useRouter<TopicsPageSearchParams>()
|
const { searchParams, changeSearchParams } = useRouter<TopicsPageSearchParams>()
|
||||||
|
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
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 topic = createMemo(() =>
|
const [topic, setTopic] = createSignal<Topic>()
|
||||||
props.topic?.slug in topicEntities() ? topicEntities()[props.topic.slug] : props.topic,
|
createEffect(() => {
|
||||||
)
|
const topics = topicEntities()
|
||||||
const title = () =>
|
if (props.topicSlug && !topic() && topics) {
|
||||||
|
setTopic(topics[props.topicSlug])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const title = createMemo(
|
||||||
|
() =>
|
||||||
`#${capitalize(
|
`#${capitalize(
|
||||||
lang() === 'en' ? topic()?.slug.replace(/-/, ' ') : topic()?.title || topic()?.slug.replace(/-/, ' '),
|
lang() === 'en'
|
||||||
|
? topic()?.slug.replace(/-/, ' ')
|
||||||
|
: topic()?.title || topic()?.slug.replace(/-/, ' '),
|
||||||
true,
|
true,
|
||||||
)}`
|
)}`,
|
||||||
onMount(() => (document.title = title()))
|
)
|
||||||
|
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
saveScrollPosition()
|
saveScrollPosition()
|
||||||
|
|
127
src/context/following.tsx
Normal file
127
src/context/following.tsx
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import { createEffect, createSignal, createContext, Accessor, useContext, JSX, onMount } from 'solid-js'
|
||||||
|
import { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
|
import { apiClient } from '../graphql/client/core'
|
||||||
|
import { Author, Community, FollowingEntity, Topic } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
|
import { useSession } from './session'
|
||||||
|
|
||||||
|
type SubscriptionsData = {
|
||||||
|
topics?: Topic[]
|
||||||
|
authors?: Author[]
|
||||||
|
communities?: Community[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FollowingContextType {
|
||||||
|
loading: Accessor<boolean>
|
||||||
|
subscriptions: SubscriptionsData
|
||||||
|
setSubscriptions: (subscriptions: SubscriptionsData) => void
|
||||||
|
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
||||||
|
loadSubscriptions: () => void
|
||||||
|
follow: (what: FollowingEntity, slug: string) => Promise<void>
|
||||||
|
unfollow: (what: FollowingEntity, slug: string) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const FollowingContext = createContext<FollowingContextType>()
|
||||||
|
|
||||||
|
export function useFollowing() {
|
||||||
|
return useContext(FollowingContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_SUBSCRIPTIONS = {
|
||||||
|
topics: [],
|
||||||
|
authors: [],
|
||||||
|
communities: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
|
const [loading, setLoading] = createSignal<boolean>(false)
|
||||||
|
const [subscriptions, setSubscriptions] = createStore<SubscriptionsData>(EMPTY_SUBSCRIPTIONS)
|
||||||
|
const { author } = useSession()
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
if (apiClient.private) {
|
||||||
|
console.debug('[context.following] fetching subs data...')
|
||||||
|
const result = await apiClient.getMySubscriptions()
|
||||||
|
setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
|
||||||
|
console.info('[context.following] subs:', subscriptions)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.info('[context.following] cannot get subs', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const follow = async (what: FollowingEntity, slug: string) => {
|
||||||
|
if (!author()) return
|
||||||
|
try {
|
||||||
|
await apiClient.follow({ what, slug })
|
||||||
|
setSubscriptions((prevSubscriptions) => {
|
||||||
|
const updatedSubs = { ...prevSubscriptions }
|
||||||
|
if (!updatedSubs[what]) updatedSubs[what] = []
|
||||||
|
const exists = updatedSubs[what]?.some((entity) => entity.slug === slug)
|
||||||
|
if (!exists) updatedSubs[what].push(slug)
|
||||||
|
return updatedSubs
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unfollow = async (what: FollowingEntity, slug: string) => {
|
||||||
|
if (!author()) return
|
||||||
|
try {
|
||||||
|
await apiClient.unfollow({ what, slug })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadData = (_a?: Author) => {
|
||||||
|
// console.debug('[context.following] current subs:', subscriptions)
|
||||||
|
if (!subscriptions?.authors?.length && !subscriptions?.topics?.length) {
|
||||||
|
// && subscriptions.communites?.length
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createEffect(() => {
|
||||||
|
if (author()) loadData()
|
||||||
|
})
|
||||||
|
onMount(loadData)
|
||||||
|
|
||||||
|
const setFollowing = (what: FollowingEntity, slug: string, value = true) => {
|
||||||
|
setSubscriptions((prevSubscriptions) => {
|
||||||
|
const updatedSubs = { ...prevSubscriptions }
|
||||||
|
if (!updatedSubs[what]) updatedSubs[what] = []
|
||||||
|
if (value) {
|
||||||
|
const exists = updatedSubs[what]?.some((entity) => entity.slug === slug)
|
||||||
|
if (!exists) updatedSubs[what].push(slug)
|
||||||
|
} else {
|
||||||
|
updatedSubs[what] = (updatedSubs[what] || []).filter((x) => x !== slug)
|
||||||
|
}
|
||||||
|
return updatedSubs
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
;(value ? follow : unfollow)(what, slug)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: FollowingContextType = {
|
||||||
|
loading,
|
||||||
|
subscriptions,
|
||||||
|
setSubscriptions,
|
||||||
|
setFollowing,
|
||||||
|
loadSubscriptions: fetchData,
|
||||||
|
follow,
|
||||||
|
unfollow,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FollowingContext.Provider value={value}>{props.children}</FollowingContext.Provider>
|
||||||
|
}
|
|
@ -56,7 +56,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
const createReaction = async (input: ReactionInput): Promise<void> => {
|
const createReaction = async (input: ReactionInput): Promise<void> => {
|
||||||
const reaction = await apiClient.createReaction(input)
|
const reaction = await apiClient.createReaction(input)
|
||||||
|
if (!reaction) return
|
||||||
const changes = {
|
const changes = {
|
||||||
[reaction.id]: reaction,
|
[reaction.id]: reaction,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { AuthModalSource } from '../components/Nav/AuthModal/types'
|
import type { AuthModalSource } from '../components/Nav/AuthModal/types'
|
||||||
import type { Author, Result } from '../graphql/schema/core.gen'
|
import type { Author } from '../graphql/schema/core.gen'
|
||||||
import type { Accessor, JSX, Resource } from 'solid-js'
|
import type { Accessor, JSX, Resource } from 'solid-js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -41,19 +41,17 @@ const defaultConfig: ConfigType = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionContextType = {
|
export type SessionContextType = {
|
||||||
config: ConfigType
|
config: Accessor<ConfigType>
|
||||||
session: Resource<AuthToken>
|
session: Resource<AuthToken>
|
||||||
author: Resource<Author | null>
|
author: Resource<Author | null>
|
||||||
authError: Accessor<string>
|
authError: Accessor<string>
|
||||||
isSessionLoaded: Accessor<boolean>
|
isSessionLoaded: Accessor<boolean>
|
||||||
subscriptions: Accessor<Result>
|
|
||||||
isAuthenticated: Accessor<boolean>
|
isAuthenticated: Accessor<boolean>
|
||||||
actions: {
|
actions: {
|
||||||
loadSession: () => AuthToken | Promise<AuthToken>
|
loadSession: () => AuthToken | Promise<AuthToken>
|
||||||
setSession: (token: AuthToken | null) => void // setSession
|
setSession: (token: AuthToken | null) => void // setSession
|
||||||
loadAuthor: (info?: unknown) => Author | Promise<Author>
|
loadAuthor: (info?: unknown) => Author | Promise<Author>
|
||||||
setAuthor: (a: Author) => void
|
setAuthor: (a: Author) => void
|
||||||
loadSubscriptions: () => Promise<void>
|
|
||||||
requireAuthentication: (
|
requireAuthentication: (
|
||||||
callback: (() => Promise<void>) | (() => void),
|
callback: (() => Promise<void>) | (() => void),
|
||||||
modalSource: AuthModalSource,
|
modalSource: AuthModalSource,
|
||||||
|
@ -77,11 +75,6 @@ export function useSession() {
|
||||||
return useContext(SessionContext)
|
return useContext(SessionContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_SUBSCRIPTIONS = {
|
|
||||||
topics: [],
|
|
||||||
authors: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SessionProvider = (props: {
|
export const SessionProvider = (props: {
|
||||||
onStateChangeCallback(state: AuthToken): unknown
|
onStateChangeCallback(state: AuthToken): unknown
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
|
@ -91,12 +84,12 @@ export const SessionProvider = (props: {
|
||||||
actions: { showSnackbar },
|
actions: { showSnackbar },
|
||||||
} = useSnackbar()
|
} = useSnackbar()
|
||||||
const { searchParams, changeSearchParams } = useRouter()
|
const { searchParams, changeSearchParams } = useRouter()
|
||||||
const [configuration, setConfig] = createSignal<ConfigType>(defaultConfig)
|
const [config, setConfig] = createSignal<ConfigType>(defaultConfig)
|
||||||
const authorizer = createMemo(() => new Authorizer(configuration()))
|
const authorizer = createMemo(() => new Authorizer(config()))
|
||||||
const [oauthState, setOauthState] = createSignal<string>()
|
const [oauthState, setOauthState] = createSignal<string>()
|
||||||
|
|
||||||
// handle callback's redirect_uri
|
// handle callback's redirect_uri
|
||||||
createEffect(async () => {
|
createEffect(() => {
|
||||||
// oauth
|
// oauth
|
||||||
const state = searchParams()?.state
|
const state = searchParams()?.state
|
||||||
if (state) {
|
if (state) {
|
||||||
|
@ -120,12 +113,13 @@ export const SessionProvider = (props: {
|
||||||
})
|
})
|
||||||
|
|
||||||
// load
|
// load
|
||||||
let minuteLater
|
let minuteLater: NodeJS.Timeout | null
|
||||||
|
|
||||||
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
|
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
|
||||||
const [authError, setAuthError] = createSignal('')
|
const [authError, setAuthError] = createSignal('')
|
||||||
const [session, { refetch: loadSession, mutate: setSession }] = createResource<AuthToken>(
|
|
||||||
async () => {
|
// Function to load session data
|
||||||
|
const sessionData = async () => {
|
||||||
try {
|
try {
|
||||||
const s = await authorizer().getSession()
|
const s = await authorizer().getSession()
|
||||||
console.info('[context.session] loading session', s)
|
console.info('[context.session] loading session', s)
|
||||||
|
@ -151,12 +145,12 @@ export const SessionProvider = (props: {
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{
|
|
||||||
|
const [session, { refetch: loadSession, mutate: setSession }] = createResource<AuthToken>(sessionData, {
|
||||||
ssrLoadFrom: 'initial',
|
ssrLoadFrom: 'initial',
|
||||||
initialValue: null,
|
initialValue: null,
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const checkSessionIsExpired = () => {
|
const checkSessionIsExpired = () => {
|
||||||
const expires_at_data = localStorage.getItem('expires_at')
|
const expires_at_data = localStorage.getItem('expires_at')
|
||||||
|
@ -177,26 +171,17 @@ export const SessionProvider = (props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
onCleanup(() => clearTimeout(minuteLater))
|
onCleanup(() => clearTimeout(minuteLater))
|
||||||
|
const authorData = async () => {
|
||||||
const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource<Author | null>(
|
|
||||||
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
|
||||||
},
|
}
|
||||||
{
|
const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource<Author | null>(authorData, {
|
||||||
ssrLoadFrom: 'initial',
|
ssrLoadFrom: 'initial',
|
||||||
initialValue: null,
|
initialValue: null,
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const [subscriptions, setSubscriptions] = createSignal<Result>(EMPTY_SUBSCRIPTIONS)
|
|
||||||
const loadSubscriptions = async (): Promise<void> => {
|
|
||||||
const result = await apiClient.getMySubscriptions()
|
|
||||||
setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
|
|
||||||
}
|
|
||||||
|
|
||||||
// when session is loaded
|
// when session is loaded
|
||||||
createEffect(async () => {
|
createEffect(() => {
|
||||||
if (session()) {
|
if (session()) {
|
||||||
const token = session()?.access_token
|
const token = session()?.access_token
|
||||||
if (token) {
|
if (token) {
|
||||||
|
@ -206,23 +191,24 @@ export const SessionProvider = (props: {
|
||||||
notifierClient.connect(token)
|
notifierClient.connect(token)
|
||||||
inboxClient.connect(token)
|
inboxClient.connect(token)
|
||||||
}
|
}
|
||||||
if (!author()) {
|
if (!author()) loadAuthor()
|
||||||
const a = await loadAuthor()
|
|
||||||
if (a) {
|
|
||||||
await loadSubscriptions()
|
|
||||||
addAuthors([a])
|
|
||||||
} else {
|
|
||||||
reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setIsSessionLoaded(true)
|
setIsSessionLoaded(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// when author is loaded
|
||||||
|
createEffect(() => {
|
||||||
|
if (author()) {
|
||||||
|
addAuthors([author()])
|
||||||
|
} else {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setIsSessionLoaded(true)
|
setIsSessionLoaded(true)
|
||||||
setSubscriptions(EMPTY_SUBSCRIPTIONS)
|
|
||||||
setSession(null)
|
setSession(null)
|
||||||
setAuthor(null)
|
setAuthor(null)
|
||||||
}
|
}
|
||||||
|
@ -252,10 +238,10 @@ export const SessionProvider = (props: {
|
||||||
)
|
)
|
||||||
|
|
||||||
const [authCallback, setAuthCallback] = createSignal<() => void>(() => {})
|
const [authCallback, setAuthCallback] = createSignal<() => void>(() => {})
|
||||||
const requireAuthentication = async (callback: () => void, modalSource: AuthModalSource) => {
|
const requireAuthentication = (callback: () => void, modalSource: AuthModalSource) => {
|
||||||
setAuthCallback((_cb) => callback)
|
setAuthCallback((_cb) => callback)
|
||||||
if (!session()) {
|
if (!session()) {
|
||||||
await loadSession()
|
loadSession()
|
||||||
if (!session()) {
|
if (!session()) {
|
||||||
showModal('auth', modalSource)
|
showModal('auth', modalSource)
|
||||||
}
|
}
|
||||||
|
@ -323,7 +309,6 @@ export const SessionProvider = (props: {
|
||||||
const isAuthenticated = createMemo(() => Boolean(author()))
|
const isAuthenticated = createMemo(() => Boolean(author()))
|
||||||
const actions = {
|
const actions = {
|
||||||
loadSession,
|
loadSession,
|
||||||
loadSubscriptions,
|
|
||||||
requireAuthentication,
|
requireAuthentication,
|
||||||
signUp,
|
signUp,
|
||||||
signIn,
|
signIn,
|
||||||
|
@ -339,9 +324,8 @@ export const SessionProvider = (props: {
|
||||||
}
|
}
|
||||||
const value: SessionContextType = {
|
const value: SessionContextType = {
|
||||||
authError,
|
authError,
|
||||||
config: configuration(),
|
config,
|
||||||
session,
|
session,
|
||||||
subscriptions,
|
|
||||||
isSessionLoaded,
|
isSessionLoaded,
|
||||||
author,
|
author,
|
||||||
actions,
|
actions,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
||||||
QueryLoad_Authors_ByArgs,
|
QueryLoad_Authors_ByArgs,
|
||||||
QueryLoad_Shouts_SearchArgs,
|
QueryLoad_Shouts_SearchArgs,
|
||||||
QueryLoad_Shouts_Random_TopArgs,
|
QueryLoad_Shouts_Random_TopArgs,
|
||||||
|
Community,
|
||||||
} from '../schema/core.gen'
|
} from '../schema/core.gen'
|
||||||
|
|
||||||
import { createGraphQLClient } from '../createGraphQLClient'
|
import { createGraphQLClient } from '../createGraphQLClient'
|
||||||
|
@ -37,20 +38,24 @@ import authorBy from '../query/core/author-by'
|
||||||
import authorFollowers from '../query/core/author-followers'
|
import authorFollowers from '../query/core/author-followers'
|
||||||
import authorId from '../query/core/author-id'
|
import authorId from '../query/core/author-id'
|
||||||
import authorsAll from '../query/core/authors-all'
|
import authorsAll from '../query/core/authors-all'
|
||||||
import authorFollowed from '../query/core/authors-followed-by'
|
|
||||||
import authorsLoadBy from '../query/core/authors-load-by'
|
import authorsLoadBy from '../query/core/authors-load-by'
|
||||||
import mySubscriptions from '../query/core/my-followed'
|
import mySubscriptions from '../query/core/my-followed'
|
||||||
import reactionsLoadBy from '../query/core/reactions-load-by'
|
import reactionsLoadBy from '../query/core/reactions-load-by'
|
||||||
import topicBySlug from '../query/core/topic-by-slug'
|
import topicBySlug from '../query/core/topic-by-slug'
|
||||||
import topicsAll from '../query/core/topics-all'
|
import topicsAll from '../query/core/topics-all'
|
||||||
import userFollowedTopics from '../query/core/topics-by-author'
|
import authorFollowedAuthors from '../query/core/authors-followed-by'
|
||||||
|
import authorFollowedTopics from '../query/core/topics-followed-by'
|
||||||
|
import authorFollowedCommunities from '../query/core/communities-followed-by'
|
||||||
import topicsRandomQuery from '../query/core/topics-random'
|
import topicsRandomQuery from '../query/core/topics-random'
|
||||||
|
|
||||||
const publicGraphQLClient = createGraphQLClient('core')
|
const publicGraphQLClient = createGraphQLClient('core')
|
||||||
|
|
||||||
export const apiClient = {
|
export const apiClient = {
|
||||||
private: null,
|
private: null,
|
||||||
connect: (token: string) => (apiClient.private = createGraphQLClient('core', token)), // NOTE: use it after token appears
|
connect: (token: string) => {
|
||||||
|
// NOTE: use it after token appears
|
||||||
|
apiClient.private = createGraphQLClient('core', token)
|
||||||
|
},
|
||||||
|
|
||||||
getRandomTopShouts: async (params: QueryLoad_Shouts_Random_TopArgs) => {
|
getRandomTopShouts: async (params: QueryLoad_Shouts_Random_TopArgs) => {
|
||||||
const response = await publicGraphQLClient.query(loadShoutsTopRandom, params).toPromise()
|
const response = await publicGraphQLClient.query(loadShoutsTopRandom, params).toPromise()
|
||||||
|
@ -119,14 +124,18 @@ export const apiClient = {
|
||||||
const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise()
|
const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise()
|
||||||
return response.data.get_author_followers
|
return response.data.get_author_followers
|
||||||
},
|
},
|
||||||
getAuthorFollowingUsers: async ({ slug }: { slug: string }): Promise<Author[]> => {
|
getAuthorFollowingAuthors: async ({ slug }: { slug: string }): Promise<Author[]> => {
|
||||||
const response = await publicGraphQLClient.query(authorFollowed, { slug }).toPromise()
|
const response = await publicGraphQLClient.query(authorFollowedAuthors, { slug }).toPromise()
|
||||||
return response.data.get_author_followed
|
return response.data.get_author_followed
|
||||||
},
|
},
|
||||||
getAuthorFollowingTopics: async ({ slug }: { slug: string }): Promise<Topic[]> => {
|
getAuthorFollowingTopics: async ({ slug }: { slug: string }): Promise<Topic[]> => {
|
||||||
const response = await publicGraphQLClient.query(userFollowedTopics, { slug }).toPromise()
|
const response = await publicGraphQLClient.query(authorFollowedTopics, { slug }).toPromise()
|
||||||
return response.data.get_topics_by_author
|
return response.data.get_topics_by_author
|
||||||
},
|
},
|
||||||
|
getAuthorFollowingCommunities: async ({ slug }: { slug: string }): Promise<Community[]> => {
|
||||||
|
const response = await publicGraphQLClient.query(authorFollowedCommunities, { slug }).toPromise()
|
||||||
|
return response.data.get_communities_by_author
|
||||||
|
},
|
||||||
updateProfile: async (input: ProfileInput) => {
|
updateProfile: async (input: ProfileInput) => {
|
||||||
const response = await apiClient.private.mutation(updateProfile, { profile: input }).toPromise()
|
const response = await apiClient.private.mutation(updateProfile, { profile: input }).toPromise()
|
||||||
return response.data.update_profile
|
return response.data.update_profile
|
||||||
|
@ -200,7 +209,7 @@ export const apiClient = {
|
||||||
const resp = await publicGraphQLClient.query(shoutsLoadBy, { options }).toPromise()
|
const resp = await publicGraphQLClient.query(shoutsLoadBy, { options }).toPromise()
|
||||||
if (resp.error) console.error(resp)
|
if (resp.error) console.error(resp)
|
||||||
|
|
||||||
return resp.data.load_shouts_by
|
return resp.data?.load_shouts_by
|
||||||
},
|
},
|
||||||
|
|
||||||
getShoutsSearch: async ({ text, limit, offset }: QueryLoad_Shouts_SearchArgs) => {
|
getShoutsSearch: async ({ text, limit, offset }: QueryLoad_Shouts_SearchArgs) => {
|
||||||
|
|
|
@ -8,7 +8,6 @@ export default gql`
|
||||||
id
|
id
|
||||||
body
|
body
|
||||||
kind
|
kind
|
||||||
range
|
|
||||||
created_at
|
created_at
|
||||||
reply_to
|
reply_to
|
||||||
stat {
|
stat {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query AuthorsAllQuery($by: AuthorsBy, $limit: Int, $offset: Int) {
|
query AuthorsAllQuery($by: AuthorsBy!, $limit: Int, $offset: Int) {
|
||||||
load_authors_by(by: $by, limit: $limit, offset: $offset) {
|
load_authors_by(by: $by, limit: $limit, offset: $offset) {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
|
|
17
src/graphql/query/core/communities-followed-by.ts
Normal file
17
src/graphql/query/core/communities-followed-by.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
|
export default gql`
|
||||||
|
query LoadCommunitiesFollowedBy($slug: String, $user: String, $author_id: Int) {
|
||||||
|
get_communities_by_author(slug: $slug, user: $user, author_id: $author_id) {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
title
|
||||||
|
pic
|
||||||
|
stat {
|
||||||
|
shouts
|
||||||
|
followers
|
||||||
|
authors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -1,7 +1,7 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query UserFollowingTopicsQuery($slug: String, $user: String, $author_id: Int) {
|
query LoadTopicsFollowedBy($slug: String, $user: String, $author_id: Int) {
|
||||||
get_topics_by_author(slug: $slug, user: $user, author_id: $author_id) {
|
get_topics_by_author(slug: $slug, user: $user, author_id: $author_id) {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
|
@ -50,4 +50,4 @@ export type UploadedFile = {
|
||||||
originalFilename?: string
|
originalFilename?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SubscriptionFilter = 'all' | 'users' | 'topics'
|
export type SubscriptionFilter = 'all' | 'authors' | 'topics' | 'communities'
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { apiClient } from '../../graphql/client/core'
|
|
||||||
import { FollowingEntity } from '../../graphql/schema/core.gen'
|
|
||||||
|
|
||||||
export const follow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
|
||||||
await apiClient.follow({ what, slug })
|
|
||||||
}
|
|
||||||
export const unfollow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
|
||||||
await apiClient.unfollow({ what, slug })
|
|
||||||
}
|
|
|
@ -15,7 +15,7 @@ export const getImageUrl = (
|
||||||
src: string,
|
src: string,
|
||||||
options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {},
|
options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {},
|
||||||
) => {
|
) => {
|
||||||
const filename = src.split('/').pop()
|
const filename = src?.split('/').pop()
|
||||||
const isAudio = src.toLowerCase().split('.').pop() in ['wav', 'mp3', 'ogg', 'aif', 'flac']
|
const isAudio = src.toLowerCase().split('.').pop() in ['wav', 'mp3', 'ogg', 'aif', 'flac']
|
||||||
const base = isAudio ? cdnUrl : `${thumborUrl}/unsafe/`
|
const base = isAudio ? cdnUrl : `${thumborUrl}/unsafe/`
|
||||||
const sizeUrlPart = isAudio ? '' : getSizeUrlPart(options)
|
const sizeUrlPart = isAudio ? '' : getSizeUrlPart(options)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { onCleanup, onMount } from 'solid-js'
|
import { onCleanup, onMount } from 'solid-js'
|
||||||
|
|
||||||
export const useEscKeyDownHandler = (onEscKeyDown: () => void) => {
|
export const useEscKeyDownHandler = (onEscKeyDown: (ev) => void) => {
|
||||||
const keydownHandler = (e: KeyboardEvent) => {
|
const keydownHandler = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
onEscKeyDown()
|
onEscKeyDown(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user