diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 7c097558..32f06587 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 6dfe2bbd..fc28e27e 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/Nav/Header/Header.module.scss b/src/components/Nav/Header/Header.module.scss index 018adc78..660a2245 100644 --- a/src/components/Nav/Header/Header.module.scss +++ b/src/components/Nav/Header/Header.module.scss @@ -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..2b6b1ea3 100644 --- a/src/components/Nav/HeaderAuth.tsx +++ b/src/components/Nav/HeaderAuth.tsx @@ -17,6 +17,8 @@ import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { ProfilePopup } from './ProfilePopup' import { useSnackbar } from '../../context/snackbar' +import { Popup } from '../_shared/Popup' +import { VotersList } from '../_shared/VotersList' import styles from './Header/Header.module.scss' type Props = { @@ -51,7 +53,7 @@ export const HeaderAuth = (props: Props) => { const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings') const isNotificationsVisible = createMemo(() => isAuthenticated() && !isEditorPage()) const isSaveButtonVisible = createMemo(() => isAuthenticated() && isEditorPage()) - const isCreatePostButtonVisible = createMemo(() => isAuthenticated() && !isEditorPage()) + const isCreatePostButtonVisible = createMemo(() => !isEditorPage()) const isAuthenticatedControlsVisible = createMemo( () => isAuthenticated() && session()?.user?.email_verified, ) @@ -65,6 +67,7 @@ export const HeaderAuth = (props: Props) => { } const [width, setWidth] = createSignal(0) + const [editorMode, setEditorMode] = createSignal(t('Editing')) onMount(() => { const handleResize = () => setWidth(window.innerWidth) @@ -106,7 +109,7 @@ export const HeaderAuth = (props: Props) => {
- +
{t('Create post')} @@ -117,7 +120,7 @@ export const HeaderAuth = (props: Props) => { - diff --git a/src/components/Nav/Snackbar.module.scss b/src/components/Nav/Snackbar.module.scss index a0fb8e64..9af5719b 100644 --- a/src/components/Nav/Snackbar.module.scss +++ b/src/components/Nav/Snackbar.module.scss @@ -1,5 +1,4 @@ .snackbar { - min-height: 2px; background-color: var(--default-color); color: #fff; font-size: 2rem; diff --git a/src/components/ProfileSettings/ProfileSettings.tsx b/src/components/ProfileSettings/ProfileSettings.tsx index 70389ccb..90a23b33 100644 --- a/src/components/ProfileSettings/ProfileSettings.tsx +++ b/src/components/ProfileSettings/ProfileSettings.tsx @@ -1,7 +1,18 @@ import { createFileUploader } from '@solid-primitives/upload' import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' -import { For, Match, Show, Switch, createEffect, createSignal, lazy, onCleanup, onMount } from 'solid-js' +import { + For, + Match, + Show, + Switch, + createEffect, + createSignal, + lazy, + on, + onCleanup, + onMount, +} from 'solid-js' import { createStore } from 'solid-js/store' import { useConfirm } from '../../context/confirm' @@ -33,6 +44,7 @@ export const ProfileSettings = () => { const { t } = useLocalize() const [prevForm, setPrevForm] = createStore({}) const [isFormInitialized, setIsFormInitialized] = createSignal(false) + const [isSaving, setIsSaving] = createSignal(false) const [social, setSocial] = createSignal([]) const [addLinkForm, setAddLinkForm] = createSignal(false) const [incorrectUrl, setIncorrectUrl] = createSignal(false) @@ -70,16 +82,20 @@ export const ProfileSettings = () => { const handleSubmit = async (event: Event) => { event.preventDefault() + setIsSaving(true) if (nameInputRef.current.value.length === 0) { setNameError(t('Required')) nameInputRef.current.focus() + setIsSaving(false) return } if (slugInputRef.current.value.length === 0) { setSlugError(t('Required')) slugInputRef.current.focus() + setIsSaving(false) return } + try { await submit(form) setPrevForm(clone(form)) @@ -91,6 +107,8 @@ export const ProfileSettings = () => { return } showSnackbar({ type: 'error', body: t('Error') }) + } finally { + setIsSaving(false) } await loadAuthor() // renews author's profile @@ -149,12 +167,15 @@ export const ProfileSettings = () => { onCleanup(() => window.removeEventListener('beforeunload', handleBeforeUnload)) }) - createEffect(() => { - if (!deepEqual(form, prevForm)) { - setIsFloatingPanelVisible(true) - } - }) - + createEffect( + on( + () => deepEqual(form, prevForm), + () => { + setIsFloatingPanelVisible(!deepEqual(form, prevForm)) + }, + { defer: true }, + ), + ) const handleDeleteSocialLink = (link) => { updateFormField('links', link, true) } @@ -359,7 +380,12 @@ export const ProfileSettings = () => { } onClick={handleCancel} /> -
diff --git a/src/components/Topic/Full.tsx b/src/components/Topic/Full.tsx index 492a9113..5e22aed1 100644 --- a/src/components/Topic/Full.tsx +++ b/src/components/Topic/Full.tsx @@ -40,7 +40,7 @@ export const FullTopic = (props: Props) => { return (

#{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 e2de6c1f..719f7b48 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -29,8 +29,6 @@ import stylesArticle from '../../Article/Article.module.scss' import styles from './Author.module.scss' type Props = { - shouts: Shout[] - author: Author authorSlug: string } export const PRERENDERED_ARTICLES_COUNT = 12 @@ -38,7 +36,7 @@ const LOAD_MORE_PAGE_SIZE = 9 export const AuthorView = (props: Props) => { const { t } = useLocalize() - const { subscriptions, followers } = useFollowing() + const { subscriptions, followers, loadSubscriptions } = useFollowing() const { session } = useSession() const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) const { authorEntities } = useAuthorsStore({ authors: [props.author] }) @@ -210,10 +208,10 @@ export const AuthorView = (props: Props) => {
- +
{t('All posts rating')} - +
diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index eddd12a4..82e43dfe 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -24,34 +24,31 @@ type Props = { layout: LayoutType } -export const PRERENDERED_ARTICLES_COUNT = 37 -const LOAD_MORE_PAGE_SIZE = 11 +export const PRERENDERED_ARTICLES_COUNT = 36 +const LOAD_MORE_PAGE_SIZE = 12 export const Expo = (props: Props) => { const [isLoaded, setIsLoaded] = createSignal(Boolean(props.shouts)) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) - const [randomTopArticles, setRandomTopArticles] = createSignal([]) - const [randomTopMonthArticles, setRandomTopMonthArticles] = createSignal([]) + const [favoriteTopArticles, setFavoriteTopArticles] = createSignal([]) + const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal([]) const { t } = useLocalize() - // const { sortedArticles } = useArticlesStore({ - // shouts: isLoaded() ? props.shouts : [], - // }) const { sortedArticles } = useArticlesStore({ - shouts: props.shouts || [], + shouts: isLoaded() ? props.shouts : [], layout: props.layout, }) const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => { - const filters = { featured: true, ...additionalFilters } + const filters = { ...additionalFilters } if (!filters.layouts) filters.layouts = [] if (props.layout) { filters.layouts.push(props.layout) } else { - filters.layouts.push('article') + filters.layouts.push('audio', 'video', 'image', 'literature') } return filters @@ -80,13 +77,12 @@ export const Expo = (props: Props) => { const loadRandomTopArticles = async () => { const options: LoadShoutsOptions = { - filters: getLoadShoutsFilters(), + filters: { ...getLoadShoutsFilters(), featured: true }, limit: 10, random_limit: 100, } - const result = await apiClient.getRandomTopShouts({ options }) - setRandomTopArticles(result) + setFavoriteTopArticles(result) } const loadRandomTopMonthArticles = async () => { @@ -94,19 +90,15 @@ export const Expo = (props: Props) => { const after = getUnixtime(new Date(now.setMonth(now.getMonth() - 1))) const options: LoadShoutsOptions = { - filters: getLoadShoutsFilters({ after }), + filters: { ...getLoadShoutsFilters({ after }), reacted: true }, limit: 10, random_limit: 10, } const result = await apiClient.getRandomTopShouts({ options }) - setRandomTopMonthArticles(result) + setReactedTopMonthArticles(result) } - const pages = createMemo(() => - splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE), - ) - onMount(() => { if (isLoaded()) { return @@ -130,8 +122,8 @@ export const Expo = (props: Props) => { () => props.layout, () => { resetSortedArticles() - setRandomTopArticles([]) - setRandomTopMonthArticles([]) + setFavoriteTopArticles([]) + setReactedTopMonthArticles([]) loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE) loadRandomTopArticles() loadRandomTopMonthArticles() @@ -202,7 +194,7 @@ export const Expo = (props: Props) => {
- + {(shout) => (
{
)}
- 0} keyed={true}> - + 0} keyed={true}> + - + {(shout) => (
{
)}
- 0} keyed={true}> - + 0} keyed={true}> + - - {(page) => ( - - {(shout) => ( -
- -
- )} -
+ + {(shout) => ( +
+ +
)}
diff --git a/src/components/Views/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/context/following.tsx b/src/context/following.tsx index 3371a010..6200c681 100644 --- a/src/context/following.tsx +++ b/src/context/following.tsx @@ -2,14 +2,14 @@ import { Accessor, JSX, createContext, createEffect, createSignal, useContext } import { createStore } from 'solid-js/store' import { apiClient } from '../graphql/client/core' -import { Author, AuthorFollows, FollowingEntity } from '../graphql/schema/core.gen' +import { Author, AuthorFollowsResult, FollowingEntity } from '../graphql/schema/core.gen' import { useSession } from './session' interface FollowingContextType { loading: Accessor followers: Accessor> - subscriptions: AuthorFollows + subscriptions: AuthorFollowsResult setSubscriptions: (subscriptions: AuthorFollows) => void setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void loadSubscriptions: () => void @@ -24,7 +24,7 @@ export function useFollowing() { return useContext(FollowingContext) } -const EMPTY_SUBSCRIPTIONS: AuthorFollows = { +const EMPTY_SUBSCRIPTIONS: AuthorFollowsResult = { topics: [], authors: [], communities: [], 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/styles/app.scss b/src/styles/app.scss index db3a68bc..7afcb070 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -622,6 +622,10 @@ figure { margin-bottom: 0.6em; white-space: nowrap; + @include media-breakpoint-up(md) { + margin-right: 2.4rem; + } + .link { border-bottom: none; }