From 778e8e8c43f57b397012296d14efcf08fdc27138 Mon Sep 17 00:00:00 2001 From: Arkadzi Rakouski Date: Wed, 14 Jun 2023 20:19:30 +0300 Subject: [PATCH] Implement auth modal calling on required actions & fix minor ui bugs. (#108) * fix views & date layout * handle auth modal calling on required actions & fix minor ui bugs * fix eslint errors * refactor auth modals for non sessioned & implement callback triggering after sign in * refactor handlers with requireAuth --- package-lock.json | 19 +++++----- package.json | 3 +- public/locales/en/translation.json | 10 ++++++ public/locales/ru/translation.json | 10 ++++++ src/components/Article/Article.module.scss | 8 +++++ src/components/Article/FullArticle.tsx | 26 ++++++++------ src/components/Article/ShoutRatingControl.tsx | 35 +++++++++++-------- src/components/Author/AuthorCard.tsx | 18 +++++++--- src/components/Feed/Beside.tsx | 4 +-- src/components/Nav/AuthModal/LoginForm.tsx | 3 +- src/components/Nav/AuthModal/RegisterForm.tsx | 3 +- src/components/Nav/AuthModal/index.tsx | 3 +- src/components/Nav/AuthModal/types.ts | 2 ++ src/components/Topic/Card.tsx | 10 ++++-- src/components/Topic/Full.tsx | 26 +++++++++----- src/context/session.tsx | 31 +++++++++++++++- src/stores/ui.ts | 23 ++++++++---- src/utils/custom-i18n.ts | 16 +++++++++ src/utils/media-query.ts | 3 ++ 19 files changed, 189 insertions(+), 64 deletions(-) create mode 100644 src/utils/custom-i18n.ts create mode 100644 src/utils/media-query.ts diff --git a/package-lock.json b/package-lock.json index 726c46b3..e98981fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,6 +89,7 @@ "eslint-config-stylelint": "18.0.0", "eslint-import-resolver-typescript": "3.5.5", "eslint-plugin-import": "2.27.5", + "eslint-plugin-jest": "27.2.1", "eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-promise": "6.1.1", "eslint-plugin-solid": "0.12.1", @@ -148,7 +149,7 @@ "uniqolor": "1.1.0", "unique-names-generator": "4.7.1", "uuid": "9.0.0", - "vite": "4.3.5", + "vite": "4.3.9", "vite-plugin-sass-dts": "1.3.5", "vite-plugin-solid": "2.7.0", "vite-plugin-ssr": "0.4.123", @@ -9447,8 +9448,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz", "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "@typescript-eslint/utils": "^5.10.0" }, @@ -19958,9 +19957,9 @@ } }, "node_modules/vite": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", - "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", + "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", "dev": true, "dependencies": { "esbuild": "^0.17.5", @@ -27778,8 +27777,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz", "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", "dev": true, - "optional": true, - "peer": true, "requires": { "@typescript-eslint/utils": "^5.10.0" } @@ -35451,9 +35448,9 @@ "dev": true }, "vite": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", - "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", + "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", "dev": true, "requires": { "esbuild": "^0.17.5", diff --git a/package.json b/package.json index b94cd67c..78f2dd31 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "eslint-config-stylelint": "18.0.0", "eslint-import-resolver-typescript": "3.5.5", "eslint-plugin-import": "2.27.5", + "eslint-plugin-jest": "27.2.1", "eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-promise": "6.1.1", "eslint-plugin-solid": "0.12.1", @@ -168,7 +169,7 @@ "uniqolor": "1.1.0", "unique-names-generator": "4.7.1", "uuid": "9.0.0", - "vite": "4.3.5", + "vite": "4.3.9", "vite-plugin-sass-dts": "1.3.5", "vite-plugin-solid": "2.7.0", "vite-plugin-ssr": "0.4.123", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 0e089f5c..b4743edd 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -56,6 +56,11 @@ "Create Chat": "Create Chat", "Create Group": "Create a group", "Create account": "Create an account", + "Create account from subscribe": "Create an account to subscribe to new publications", + "Create account from discussions": "Create an account to participate in discussions", + "Create account from vote": "Create an account to vote", + "Create account from bookmark": "Create an account to add to your bookmarks", + "Create account from follow": "Create an account to subscribe", "Create post": "Create post", "Date of Birth": "Date of Birth", "Delete": "Delete", @@ -74,6 +79,11 @@ "Enter URL address": "Enter URL address", "Enter text": "Enter text", "Enter the Discours": "Enter the Discours", + "Enter the Discours from subscribe": "Sign in to subscribe to new publications", + "Enter the Discours from discussions": "Sign in to participate in the discussions", + "Enter the Discours from vote": "Sign in to vote", + "Enter the Discours from bookmark": "Sign in to add to bookmarks", + "Enter the Discours from follow": "Sign in to follow", "Enter the code or click the link from email to confirm": "Enter the code from the email or follow the link in the email to confirm registration", "Enter your new password": "Enter your new password", "Error": "Error", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 9797ff67..c17a94f1 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -58,6 +58,11 @@ "Create Chat": "Создать чат", "Create Group": "Создать группу", "Create account": "Создать аккаунт", + "Create account from subscribe": "Создайте аккаунт для подписки на новые публикации", + "Create account from discussions": "Создайте аккаунт для участия в дискуссиях", + "Create account from vote": "Создайте аккаунт, чтобы голосовать", + "Create account from bookmark": "Создайте аккаунт, чтобы добавить в закладки", + "Create account from follow": "Создайте аккаунт, чтобы подписаться", "Create post": "Создать публикацию", "Date of Birth": "Дата рождения", "Delete": "Удалить", @@ -77,6 +82,11 @@ "Enter URL address": "Введите адрес ссылки", "Enter text": "Введите текст", "Enter the Discours": "Войти в Дискурс", + "Enter the Discours from subscribe": "Войдите для подписки на новые публикации", + "Enter the Discours from discussions": "Войдите для участия в дискуссиях", + "Enter the Discours from vote": "Войдите, чтобы голосовать", + "Enter the Discours from bookmark": "Войдите, чтобы добавить в закладки", + "Enter the Discours from follow": "Войдите, чтобы подписаться", "Enter the code or click the link from email to confirm": "Введите код из письма или пройдите по ссылке в письме для подтверждения регистрации", "Enter your new password": "Введите новый пароль", "Error": "Ошибка", diff --git a/src/components/Article/Article.module.scss b/src/components/Article/Article.module.scss index 7ee1567c..c0cbb7ed 100644 --- a/src/components/Article/Article.module.scss +++ b/src/components/Article/Article.module.scss @@ -237,6 +237,7 @@ img { margin-right: 0; margin-left: auto; white-space: nowrap; + cursor: default; .icon { opacity: 0.4; @@ -248,11 +249,18 @@ img { } } +.shoutStatsItemViews { + margin-left: 2rem; + margin-right: 0; + cursor: default; +} + .shoutStatsItemAdditionalDataItem { font-weight: normal; display: inline-block; margin-left: 2rem; margin-right: 0; + cursor: default; @include media-breakpoint-down(sm) { &:first-child { diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 79162e17..2dfa9599 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -62,7 +62,11 @@ const MediaView = (props: { media: MediaItem; kind: Shout['layout'] }) => { export const FullArticle = (props: ArticleProps) => { const { t } = useLocalize() - const { user, isAuthenticated } = useSession() + const { + user, + isAuthenticated, + actions: { requireAuthentication } + } = useSession() const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt))) @@ -81,10 +85,12 @@ export const FullArticle = (props: ArticleProps) => { }) const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug) - // eslint-disable-next-line unicorn/consistent-function-scoping + const handleBookmarkButtonClick = (ev) => { - // TODO: implement bookmark clicked - ev.preventDefault() + requireAuthentication(() => { + // TODO: implement bookmark clicked + ev.preventDefault() + }, 'bookmark') } const body = createMemo(() => props.article.body) @@ -222,12 +228,6 @@ export const FullArticle = (props: ArticleProps) => { - -
- - {props.article.stat?.viewed} -
-
{(triggerRef: (el) => void) => (
@@ -280,6 +280,12 @@ export const FullArticle = (props: ArticleProps) => {
{formattedDate()}
+ +
+ + {props.article.stat?.viewed} +
+
diff --git a/src/components/Article/ShoutRatingControl.tsx b/src/components/Article/ShoutRatingControl.tsx index e38d54ba..982d40d9 100644 --- a/src/components/Article/ShoutRatingControl.tsx +++ b/src/components/Article/ShoutRatingControl.tsx @@ -16,7 +16,10 @@ interface ShoutRatingControlProps { export const ShoutRatingControl = (props: ShoutRatingControlProps) => { const { t } = useLocalize() - const { user } = useSession() + const { + user, + actions: { requireAuthentication } + } = useSession() const { reactionEntities, @@ -57,21 +60,23 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => { } const handleRatingChange = async (isUpvote: boolean) => { - if (isUpvoted()) { - await deleteShoutReaction(ReactionKind.Like) - } else if (isDownvoted()) { - await deleteShoutReaction(ReactionKind.Dislike) - } else { - await createReaction({ - kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, - shout: props.shout.id - }) - } + requireAuthentication(async () => { + if (isUpvoted()) { + await deleteShoutReaction(ReactionKind.Like) + } else if (isDownvoted()) { + await deleteShoutReaction(ReactionKind.Dislike) + } else { + await createReaction({ + kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, + shout: props.shout.id + }) + } - loadShout(props.shout.slug) - loadReactionsBy({ - by: { shout: props.shout.slug } - }) + loadShout(props.shout.slug) + loadReactionsBy({ + by: { shout: props.shout.slug } + }) + }, 'vote') } return ( diff --git a/src/components/Author/AuthorCard.tsx b/src/components/Author/AuthorCard.tsx index 5bc802bd..ed4133fa 100644 --- a/src/components/Author/AuthorCard.tsx +++ b/src/components/Author/AuthorCard.tsx @@ -13,6 +13,7 @@ import { FollowingEntity } from '../../graphql/types.gen' import { router, useRouter } from '../../stores/router' import { openPage } from '@nanostores/router' import { useLocalize } from '../../context/localize' +import { init } from 'i18next' interface AuthorCardProps { caption?: string @@ -40,7 +41,7 @@ export const AuthorCard = (props: AuthorCardProps) => { const { session, isSessionLoaded, - actions: { loadSession } + actions: { loadSession, requireAuthentication } } = useSession() const [isSubscribing, setIsSubscribing] = createSignal(false) @@ -77,9 +78,18 @@ export const AuthorCard = (props: AuthorCardProps) => { // TODO: reimplement AuthorCard const { changeSearchParam } = useRouter() const initChat = () => { - openPage(router, `inbox`) - changeSearchParam('initChat', `${props.author.id}`) + requireAuthentication(() => { + openPage(router, `inbox`) + changeSearchParam('initChat', `${props.author.id}`) + }, 'discussions') } + + const handleSubscribe = () => { + requireAuthentication(() => { + subscribe(true) + }, 'subscribe') + } + return (
{ when={subscribed()} fallback={ - diff --git a/src/context/session.tsx b/src/context/session.tsx index 4ed38048..c0258387 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -1,10 +1,12 @@ -import type { Accessor, JSX, Resource } from 'solid-js' +import { Accessor, JSX, Resource, createEffect } from 'solid-js' import { createContext, createMemo, createResource, createSignal, onMount, useContext } from 'solid-js' import type { AuthResult, User } from '../graphql/types.gen' import { apiClient } from '../utils/apiClient' import { resetToken, setToken } from '../graphql/privateGraphQLClient' import { useSnackbar } from './snackbar' import { useLocalize } from './localize' +import { showModal } from '../stores/ui' +import type { AuthModalSource } from '../components/Nav/AuthModal/types' type SessionContextType = { session: Resource @@ -13,6 +15,10 @@ type SessionContextType = { isAuthenticated: Accessor actions: { loadSession: () => AuthResult | Promise + requireAuthentication: ( + callback: (() => Promise) | (() => void), + modalSource: AuthModalSource + ) => void signIn: ({ email, password }: { email: string; password: string }) => Promise signOut: () => Promise confirmEmail: (token: string) => Promise @@ -65,6 +71,28 @@ export const SessionProvider = (props: { children: JSX.Element }) => { console.debug('signed in') } + const [isAuthWithCallback, setIsAuthWithCallback] = createSignal(null) + + const requireAuthentication = (callback: () => void, modalSource: AuthModalSource) => { + setIsAuthWithCallback(() => callback) + + if (!isAuthenticated()) { + showModal('auth', modalSource) + } + } + + createEffect(async () => { + if (isAuthWithCallback()) { + const sessionProof = await session() + + if (sessionProof) { + await isAuthWithCallback()() + + setIsAuthWithCallback(null) + } + } + }) + const signOut = async () => { // TODO: call backend to revoke token mutate(null) @@ -80,6 +108,7 @@ export const SessionProvider = (props: { children: JSX.Element }) => { const actions = { loadSession, + requireAuthentication, signIn, signOut, confirmEmail diff --git a/src/stores/ui.ts b/src/stores/ui.ts index 3bf13675..17e891aa 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -1,6 +1,10 @@ import { createSignal } from 'solid-js' import { useRouter } from './router' -import type { AuthModalSearchParams, ConfirmEmailSearchParams } from '../components/Nav/AuthModal/types' +import type { + AuthModalSearchParams, + AuthModalSource, + ConfirmEmailSearchParams +} from '../components/Nav/AuthModal/types' import type { RootSearchParams } from '../pages/types' export type ModalType = @@ -36,14 +40,20 @@ const [modal, setModal] = createSignal(null) const [warnings, setWarnings] = createSignal([]) -export const showModal = (modalType: ModalType) => setModal(modalType) +const { searchParams, changeSearchParam } = useRouter< + AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams +>() + +export const showModal = (modalType: ModalType, modalSource?: AuthModalSource) => { + if (modalSource) { + changeSearchParam('source', modalSource) + } + + setModal(modalType) +} // TODO: find a better solution export const hideModal = () => { - const { searchParams, changeSearchParam } = useRouter< - AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams - >() - if (searchParams().modal === 'auth') { if (searchParams().mode === 'confirm-email') { changeSearchParam('token', null, true) @@ -52,6 +62,7 @@ export const hideModal = () => { } changeSearchParam('modal', null, true) + changeSearchParam('source', null, true) setModal(null) } diff --git a/src/utils/custom-i18n.ts b/src/utils/custom-i18n.ts new file mode 100644 index 00000000..ba900f7c --- /dev/null +++ b/src/utils/custom-i18n.ts @@ -0,0 +1,16 @@ +import { useRouter } from '../stores/router' +import type { AuthModalSearchParams } from '../components/Nav/AuthModal/types' +import { useLocalize } from '../context/localize' + +export const generateModalTitleFromSource = (modalType: 'login' | 'register') => { + const { searchParams } = useRouter() + const { t } = useLocalize() + + const { source } = searchParams() + + let title = modalType === 'login' ? 'Enter the Discours' : 'Create account' + + if (source) title = `${title} from ${source}` + + return t(title) +} diff --git a/src/utils/media-query.ts b/src/utils/media-query.ts new file mode 100644 index 00000000..0fb977a2 --- /dev/null +++ b/src/utils/media-query.ts @@ -0,0 +1,3 @@ +import { createMediaQuery } from '@solid-primitives/media' + +export const isMobile = createMediaQuery('(max-width: 767px)')