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 86a191f6..2690613b 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 8a7d26c8..13ba2de4 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/Article.module.scss b/src/components/Article/Article.module.scss index ad85c0a3..55b8dd95 100644 --- a/src/components/Article/Article.module.scss +++ b/src/components/Article/Article.module.scss @@ -22,6 +22,7 @@ img { .articleContent { img:not([data-disable-lightbox='true']) { cursor: zoom-in; + width: 100%; } } diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index f855f170..bc7fd403 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) => { @@ -329,7 +330,7 @@ export const FullArticle = (props: Props) => { width: 1200, }) - const description = getDescription(props.article.description || body()) + const description = getDescription(props.article.description || body() || media()[0]?.body) const ogTitle = props.article.title const keywords = getKeywords(props.article) const shareUrl = getShareUrl({ pathname: `/${props.article.slug}` }) diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx index ed13e10d..65e14027 100644 --- a/src/components/Author/AuthorCard/AuthorCard.tsx +++ b/src/components/Author/AuthorCard/AuthorCard.tsx @@ -135,7 +135,9 @@ export const AuthorCard = (props: Props) => { )}
- {t('SubscriberWithCount', { count: props.followers.length ?? 0 })} + {t('SubscriberWithCount', { + count: props.followers.length ?? 0, + })}
@@ -170,7 +172,9 @@ export const AuthorCard = (props: Props) => { }}
- {t('SubscriptionWithCount', { count: props?.following.length ?? 0 })} + {t('SubscriptionWithCount', { + count: props?.following.length ?? 0, + })}
@@ -236,7 +240,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={ {props.following.length} -
  • +
  • @@ -278,7 +292,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..7f439b94 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..9d405a9b 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) } @@ -174,7 +195,7 @@ export const ProfileSettings = () => {

    {t('Profile settings')}

    {t('Here you can customize your profile the way you want.')}

    -
    +

    {t('Userpic')}

    {
    updateFormField('name', event.currentTarget.value)} value={form.name} ref={(el) => (nameInputRef.current = el)} /> - +
    { type="text" name="user-address" id="user-address" + data-lpignore="true" + autocomplete="one-time-code2" onInput={(event) => updateFormField('slug', event.currentTarget.value)} value={form.slug} ref={(el) => (slugInputRef.current = el)} @@ -359,7 +384,12 @@ export const ProfileSettings = () => { } onClick={handleCancel} /> -
    diff --git a/src/components/TableOfContents/TableOfContents.tsx b/src/components/TableOfContents/TableOfContents.tsx index f7000e20..322aa925 100644 --- a/src/components/TableOfContents/TableOfContents.tsx +++ b/src/components/TableOfContents/TableOfContents.tsx @@ -17,7 +17,7 @@ interface Props { const isInViewport = (el: Element): boolean => { const rect = el.getBoundingClientRect() - return rect.top <= DEFAULT_HEADER_OFFSET + return rect.top <= DEFAULT_HEADER_OFFSET + 24 // default offset + 1.5em (default header margin-top) } const scrollToHeader = (element) => { window.scrollTo({ 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 (

    #{props.topic?.title}

    -

    {props.topic?.body}

    +

    } > -
    {props.topic.body}
    +
    diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index e0fd82e8..df63b945 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, on, o 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,41 +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 { 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]) + } } }) @@ -72,15 +77,17 @@ export const AuthorView = (props: Props) => { const fetchData = async (author: Author) => { try { - const [subscriptionsResult, followersResult] = await Promise.all([ - apiClient.getAuthorFollows({ author_id: author.id }), - apiClient.getAuthorFollowers({ slug: author.slug }), + 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) } @@ -104,10 +111,10 @@ export const AuthorView = (props: Props) => { } onMount(() => { - if (!modal) { - hideModal() - } + if (!modal) hideModal() checkBioHeight() + fetchData(props.authorSlug) + // pagination if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) { loadMore() @@ -120,7 +127,7 @@ export const AuthorView = (props: Props) => { const fetchComments = async (commenter: Author) => { const data = await apiClient.getReactionsBy({ - by: { comment: false, created_by: commenter.id }, + by: { comment: true, created_by: commenter.id }, }) setCommented(data) } @@ -160,31 +167,53 @@ export const AuthorView = (props: Props) => { }> <>
    - +
      -
    • - +
    • + {t('Publications')} {author().stat.shouts}
    • -
    • - +
    • + {t('Comments')} {author().stat.comments}
    • -
    • +
    • checkBioHeight()} - href={getPagePath(router, 'authorAbout', { slug: props.authorSlug })} + href={getPagePath(router, 'authorAbout', { + slug: props.authorSlug, + })} > {t('Profile')} 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/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx index aeee25b9..e457cafc 100644 --- a/src/components/Views/Feed/Feed.tsx +++ b/src/components/Views/Feed/Feed.tsx @@ -54,6 +54,13 @@ type FeedSearchParams = { visibility: VisibilityMode } +type Props = { + loadShouts: (options: LoadShoutsOptions) => Promise<{ + hasMore: boolean + newShouts: Shout[] + }> +} + const getFromDate = (period: FeedPeriod): number => { const now = new Date() let d: Date = now @@ -74,18 +81,10 @@ const getFromDate = (period: FeedPeriod): number => { return Math.floor(d.getTime() / 1000) } -type Props = { - loadShouts: (options: LoadShoutsOptions) => Promise<{ - hasMore: boolean - newShouts: Shout[] - }> -} - export const FeedView = (props: Props) => { const { t } = useLocalize() const monthPeriod: PeriodItem = { value: 'month', title: t('This month') } - const visibilityAll = { value: 'featured', title: t('All') } const periods: PeriodItem[] = [ { value: 'week', title: t('This week') }, @@ -121,7 +120,7 @@ export const FeedView = (props: Props) => { const currentVisibility = createMemo(() => { const visibility = visibilities.find((v) => v.value === searchParams().visibility) if (!visibility) { - return visibilityAll + return visibilities[0] } return visibility }) @@ -172,6 +171,7 @@ export const FeedView = (props: Props) => { } const visibilityMode = searchParams().visibility + if (visibilityMode === 'all') { options.filters = { ...options.filters } } else if (visibilityMode) { @@ -185,6 +185,7 @@ export const FeedView = (props: Props) => { const period = searchParams().period || 'month' options.filters = { after: getFromDate(period) } } + return props.loadShouts(options) } 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 6618efa9..4cafd050 100644 --- a/src/components/_shared/Icon/Icon.module.scss +++ b/src/components/_shared/Icon/Icon.module.scss @@ -10,18 +10,23 @@ } .notificationsCounter { - background-color: #d00820; - border: 2px solid #fff; - border-radius: 2em; + align-items: center; + background-color: #E84500; + border-radius: 0.8rem; color: #fff; - font-size: 1rem; + display: flex; + font-size: 1.2rem; font-weight: 700; - height: 1.6em; - left: 1.1em; - line-height: 1.25em; + height: 2.2rem; + justify-content: center; + left: 1.6rem; + min-width: 2.2rem; padding: 0 0.25em; position: absolute; text-align: center; top: -0.5rem; - min-width: 1.5em; + + @include media-breakpoint-up(md) { + left: 1.8rem; + } } 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 - +
      {slide.title}
      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 2e2d1a44..a6b07a03 100644 --- a/src/context/following.tsx +++ b/src/context/following.tsx @@ -2,7 +2,7 @@ import { Accessor, JSX, createContext, createEffect, createMemo, createSignal, u import { createStore } from 'solid-js/store' import { apiClient } from '../graphql/client/core' -import { Author, AuthorFollows, Community, FollowingEntity, Topic } from '../graphql/schema/core.gen' +import { Author, AuthorFollowsResult, FollowingEntity } from '../graphql/schema/core.gen' import { useSession } from './session' @@ -16,8 +16,9 @@ type SubscribeAction = { slug: string; type: 'subscribe' | 'unsubscribe' } 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 @@ -32,7 +33,7 @@ export function useFollowing() { return useContext(FollowingContext) } -const EMPTY_SUBSCRIPTIONS: AuthorFollows = { +const EMPTY_SUBSCRIPTIONS: AuthorFollowsResult = { topics: [], authors: [], communities: [], @@ -40,9 +41,9 @@ const EMPTY_SUBSCRIPTIONS: AuthorFollows = { export const FollowingProvider = (props: { children: JSX.Element }) => { const [loading, setLoading] = createSignal(false) - const [subscriptions, setSubscriptions] = createStore(EMPTY_SUBSCRIPTIONS) - const { session, author } = useSession() - const [subscribeInAction, setSubscribeInAction] = createSignal() + const [followers, setFollowers] = createSignal>([]) + const [subscriptions, setSubscriptions] = createStore(EMPTY_SUBSCRIPTIONS) + const { author, session } = useSession() const fetchData = async () => { setLoading(true) @@ -94,8 +95,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) + } } }) @@ -125,6 +135,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { subscriptions, setSubscriptions, setFollowing, + followers, loadSubscriptions: fetchData, follow, unfollow, diff --git a/src/context/session.tsx b/src/context/session.tsx index 73659a6c..3a189cc1 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -1,5 +1,5 @@ import type { Accessor, JSX, Resource } from 'solid-js' -import type { AuthModalSource } from '../components/Nav/AuthModal/types' +import type { AuthModalSearchParams, AuthModalSource } from '../components/Nav/AuthModal/types' import type { Author } from '../graphql/schema/core.gen' import { @@ -29,7 +29,6 @@ import { import { inboxClient } from '../graphql/client/chat' import { apiClient } from '../graphql/client/core' -import { notifierClient } from '../graphql/client/notifier' import { useRouter } from '../stores/router' import { showModal } from '../stores/ui' import { addAuthors } from '../stores/zine/authors' @@ -136,6 +135,7 @@ export const SessionProvider = (props: { const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) const [authError, setAuthError] = createSignal('') + const { clearSearchParams } = useRouter() // Function to load session data const sessionData = async () => { @@ -143,7 +143,7 @@ export const SessionProvider = (props: { const s: ApiResponse = await authorizer().getSession() if (s?.data) { console.info('[context.session] loading session', s) - + clearSearchParams() // Set session expiration time in local storage const expires_at = new Date(Date.now() + s.data.expires_in * 1000) localStorage.setItem('expires_at', `${expires_at.getTime()}`) @@ -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 }, ), ) @@ -368,6 +379,7 @@ export const SessionProvider = (props: { } const isAuthenticated = createMemo(() => Boolean(author())) + const actions = { loadSession, requireAuthentication, diff --git a/src/graphql/client/core.ts b/src/graphql/client/core.ts index e7d8708e..e80f2e6d 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/pages/profile/Settings.module.scss b/src/pages/profile/Settings.module.scss index 898b2496..9bd4906c 100644 --- a/src/pages/profile/Settings.module.scss +++ b/src/pages/profile/Settings.module.scss @@ -320,3 +320,14 @@ h5 { margin-bottom: 0; } } + +// disable last pass extention + +div[data-lastpass-icon-root="true"] { + opacity: 0 !important; +} + +div[data-lastpass-infield="true"] { + opacity: 0 !important; +} + 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..0bb124a4 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -588,6 +588,7 @@ figure { display: block; max-height: 90vh; margin: auto; + width: 100%; } } @@ -622,6 +623,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 7d88cd51..e13872cc 100644 --- a/src/utils/getImageUrl.ts +++ b/src/utils/getImageUrl.ts @@ -1,26 +1,41 @@ import { cdnUrl, thumborUrl } from './config' -const getSizeUrlPart = (options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {}) => { - const widthString = options.width ? options.width.toString() : '' - const heightString = options.height ? options.height.toString() : '' +const URL_CONFIG = { + cdnUrl: cdnUrl, + thumborUrl: `${thumborUrl}/unsafe/`, + audioSubfolder: 'audio', + imageSubfolder: 'image', + productionFolder: 'production/', +} - if (!(widthString || heightString) || options.noSizeUrlPart) { - return '' - } +const AUDIO_EXTENSIONS = new Set(['wav', 'mp3', 'ogg', 'aif', 'flac']) - return `${widthString}x${heightString}/` +const isAudioFile = (filename: string): boolean => { + const extension = filename.split('.').pop()?.toLowerCase() + return AUDIO_EXTENSIONS.has(extension ?? '') +} +const getLastSegment = (url: string): string => url.toLowerCase().split('/').pop() || '' + +const buildSizePart = (width?: number, height?: number, includeSize = true): string => { + if (!includeSize) return '' + const widthPart = width ? width.toString() : '' + const heightPart = height ? height.toString() : '' + return widthPart || heightPart ? `${widthPart}x${heightPart}/` : '' } export const getImageUrl = ( src: string, options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {}, -) => { - const filename = src?.split('/').pop() - const isAudio = src.toLowerCase().split('.').pop() in ['wav', 'mp3', 'ogg', 'aif', 'flac'] - const base = isAudio ? cdnUrl : `${thumborUrl}/unsafe/` - const sizeUrlPart = isAudio ? '' : getSizeUrlPart(options) +): string => { + if (!src.includes('discours.io') && src.includes('http')) { + return src + } + const filename = getLastSegment(src) + const base = isAudioFile(filename) ? URL_CONFIG.cdnUrl : URL_CONFIG.thumborUrl + const suffix = options.noSizeUrlPart ? '' : buildSizePart(options.width, options.height) + const subfolder = isAudioFile(filename) ? URL_CONFIG.audioSubfolder : URL_CONFIG.imageSubfolder - return `${base}${sizeUrlPart}production/${isAudio ? 'audio' : 'image'}/${filename}` + return `${base}${suffix}${URL_CONFIG.productionFolder}${subfolder}/${filename}` } export const getOpenGraphImageUrl = ( @@ -32,17 +47,16 @@ export const getOpenGraphImageUrl = ( width?: number height?: number }, -) => { - const sizeUrlPart = getSizeUrlPart(options) - +): string => { + const sizeUrlPart = buildSizePart(options.width, options.height) const filtersPart = `filters:discourstext('${encodeURIComponent(options.topic)}','${encodeURIComponent( options.author, )}','${encodeURIComponent(options.title)}')/` - if (src.startsWith(thumborUrl)) { - const thumborKey = src.replace(`${thumborUrl}/unsafe`, '') - return `${thumborUrl}/unsafe/${sizeUrlPart}${filtersPart}${thumborKey}` + if (src.startsWith(URL_CONFIG.thumborUrl)) { + const thumborKey = src.replace(URL_CONFIG.thumborUrl, '') + return `${URL_CONFIG.thumborUrl}${sizeUrlPart}${filtersPart}${thumborKey}` } - return `${thumborUrl}/unsafe/${sizeUrlPart}${filtersPart}${src}` + return `${URL_CONFIG.thumborUrl}${sizeUrlPart}${filtersPart}${src}` }