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
This commit is contained in:
Arkadzi Rakouski 2023-06-14 20:19:30 +03:00 committed by GitHub
parent 06c3740db2
commit 778e8e8c43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 189 additions and 64 deletions

19
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "Ошибка",

View File

@ -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 {

View File

@ -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) => {
requireAuthentication(() => {
// TODO: implement bookmark clicked
ev.preventDefault()
}, 'bookmark')
}
const body = createMemo(() => props.article.body)
@ -222,12 +228,6 @@ export const FullArticle = (props: ArticleProps) => {
<ShoutRatingControl shout={props.article} class={styles.ratingControl} />
</div>
<Show when={props.article.stat?.viewed}>
<div class={clsx(styles.shoutStatsItem)}>
<Icon name="eye" class={clsx(styles.icon, styles.iconEye)} />
{props.article.stat?.viewed}
</div>
</Show>
<Popover content={t('Comment')}>
{(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef} onClick={scrollToComments}>
@ -280,6 +280,12 @@ export const FullArticle = (props: ArticleProps) => {
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalDataItem)}>
{formattedDate()}
</div>
<Show when={props.article.stat?.viewed}>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}>
<Icon name="eye" class={clsx(styles.icon, styles.iconEye)} />
{props.article.stat?.viewed}
</div>
</Show>
</div>
</div>
<div class={styles.help}>

View File

@ -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,6 +60,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
}
const handleRatingChange = async (isUpvote: boolean) => {
requireAuthentication(async () => {
if (isUpvoted()) {
await deleteShoutReaction(ReactionKind.Like)
} else if (isDownvoted()) {
@ -72,6 +76,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
loadReactionsBy({
by: { shout: props.shout.slug }
})
}, 'vote')
}
return (

View File

@ -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 = () => {
requireAuthentication(() => {
openPage(router, `inbox`)
changeSearchParam('initChat', `${props.author.id}`)
}, 'discussions')
}
const handleSubscribe = () => {
requireAuthentication(() => {
subscribe(true)
}, 'subscribe')
}
return (
<div
class={clsx(styles.author, props.class)}
@ -133,7 +143,7 @@ export const AuthorCard = (props: AuthorCardProps) => {
when={subscribed()}
fallback={
<button
onClick={() => subscribe(true)}
onClick={handleSubscribe}
class={clsx('button', styles.button)}
classList={{
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,

View File

@ -30,7 +30,7 @@ export const Beside = (props: BesideProps) => {
<Show when={!!props.beside?.slug && props.values?.length > 0}>
<div class="floor floor--9">
<div class="wide-container">
<div class="row">
<div class="row justify-content-between">
<Show when={!!props.values}>
<div
class={clsx(
@ -102,7 +102,7 @@ export const Beside = (props: BesideProps) => {
</ul>
</div>
</Show>
<div class={clsx('col-md-16', styles.shoutCardContainer)}>
<div class={clsx('col-md-14', styles.shoutCardContainer)}>
<ArticleCard
article={props.beside}
settings={{ isBigTitle: true, isBeside: true, nodate: props.nodate }}

View File

@ -10,6 +10,7 @@ import { hideModal } from '../../../stores/ui'
import { useSession } from '../../../context/session'
import { signSendLink } from '../../../stores/auth'
import { isValidEmail } from '../../../utils/validators'
import { generateModalTitleFromSource } from '../../../utils/custom-i18n'
import { useSnackbar } from '../../../context/snackbar'
import { useLocalize } from '../../../context/localize'
@ -117,7 +118,7 @@ export const LoginForm = () => {
return (
<form onSubmit={handleSubmit} class={styles.authForm}>
<div>
<h4>{t('Enter the Discours')}</h4>
<h4>{generateModalTitleFromSource('login')}</h4>
<Show when={submitError()}>
<div class={styles.authInfo}>
<div class={styles.warn}>{submitError()}</div>

View File

@ -12,6 +12,7 @@ import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
import { register } from '../../../stores/auth'
import { useLocalize } from '../../../context/localize'
import { isValidEmail } from '../../../utils/validators'
import { generateModalTitleFromSource } from '../../../utils/custom-i18n'
type FormFields = {
name: string
@ -136,7 +137,7 @@ export const RegisterForm = () => {
<Show when={!isSuccess()}>
<form onSubmit={handleSubmit} class={styles.authForm}>
<div>
<h4>{t('Create account')}</h4>
<h4>{generateModalTitleFromSource('register')}</h4>
<Show when={submitError()}>
<div class={styles.authInfo}>
<ul>

View File

@ -5,6 +5,7 @@ import { useRouter } from '../../../stores/router'
import { clsx } from 'clsx'
import styles from './AuthModal.module.scss'
import { LoginForm } from './LoginForm'
import { isMobile } from '../../../utils/media-query'
import { RegisterForm } from './RegisterForm'
import { ForgotPasswordForm } from './ForgotPasswordForm'
import { EmailConfirm } from './EmailConfirm'
@ -28,7 +29,7 @@ export const AuthModal = () => {
})
createEffect((oldMode) => {
if (oldMode !== mode()) {
if (oldMode !== mode() && !isMobile()) {
rootRef?.querySelector('input')?.focus()
}
}, null)

View File

@ -1,7 +1,9 @@
export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-password'
export type AuthModalSource = 'discussions' | 'vote' | 'subscribe' | 'bookmark' | 'follow'
export type AuthModalSearchParams = {
mode: AuthModalMode
source?: AuthModalSource
}
export type ConfirmEmailSearchParams = {

View File

@ -31,7 +31,7 @@ export const TopicCard = (props: TopicProps) => {
const {
session,
isSessionLoaded,
actions: { loadSession }
actions: { loadSession, requireAuthentication }
} = useSession()
const [isSubscribing, setIsSubscribing] = createSignal(false)
@ -55,6 +55,12 @@ export const TopicCard = (props: TopicProps) => {
setIsSubscribing(false)
}
const handleSubscribe = () => {
requireAuthentication(() => {
subscribe(!subscribed())
}, 'subscribe')
}
return (
<div class={styles.topicContainer}>
<div
@ -100,7 +106,7 @@ export const TopicCard = (props: TopicProps) => {
<ShowOnlyOnClient>
<Show when={isSessionLoaded()}>
<button
onClick={() => subscribe(!subscribed())}
onClick={handleSubscribe}
class="button--light button--subscribe-topic"
classList={{
[styles.isSubscribing]: isSubscribing(),

View File

@ -13,27 +13,35 @@ type Props = {
}
export const FullTopic = (props: Props) => {
const { session } = useSession()
const {
session,
actions: { requireAuthentication }
} = useSession()
const { t } = useLocalize()
const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug))
const handleSubscribe = (isFollowed: boolean) => {
requireAuthentication(() => {
if (isFollowed) {
unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })
} else {
follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
}
}, 'follow')
}
return (
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
<h1>#{props.topic.title}</h1>
<p>{props.topic.body}</p>
<div class={clsx(styles.topicActions)}>
<Show when={!subscribed()}>
<button
onClick={() => follow({ what: FollowingEntity.Topic, slug: props.topic.slug })}
class="button"
>
<button onClick={() => handleSubscribe(false)} class="button">
{t('Follow the topic')}
</button>
</Show>
<Show when={subscribed()}>
<button
onClick={() => unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })}
class="button"
>
<button onClick={() => handleSubscribe(true)} class="button">
{t('Unfollow the topic')}
</button>
</Show>

View File

@ -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<AuthResult>
@ -13,6 +15,10 @@ type SessionContextType = {
isAuthenticated: Accessor<boolean>
actions: {
loadSession: () => AuthResult | Promise<AuthResult>
requireAuthentication: (
callback: (() => Promise<void>) | (() => void),
modalSource: AuthModalSource
) => void
signIn: ({ email, password }: { email: string; password: string }) => Promise<void>
signOut: () => Promise<void>
confirmEmail: (token: string) => Promise<void>
@ -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

View File

@ -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<ModalType | null>(null)
const [warnings, setWarnings] = createSignal<Warning[]>([])
export const showModal = (modalType: ModalType) => setModal(modalType)
// TODO: find a better solution
export const hideModal = () => {
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 = () => {
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)
}

16
src/utils/custom-i18n.ts Normal file
View File

@ -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<AuthModalSearchParams>()
const { t } = useLocalize()
const { source } = searchParams()
let title = modalType === 'login' ? 'Enter the Discours' : 'Create account'
if (source) title = `${title} from ${source}`
return t(title)
}

3
src/utils/media-query.ts Normal file
View File

@ -0,0 +1,3 @@
import { createMediaQuery } from '@solid-primitives/media'
export const isMobile = createMediaQuery('(max-width: 767px)')