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-config-stylelint": "18.0.0",
"eslint-import-resolver-typescript": "3.5.5", "eslint-import-resolver-typescript": "3.5.5",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-jest": "27.2.1",
"eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-solid": "0.12.1", "eslint-plugin-solid": "0.12.1",
@ -148,7 +149,7 @@
"uniqolor": "1.1.0", "uniqolor": "1.1.0",
"unique-names-generator": "4.7.1", "unique-names-generator": "4.7.1",
"uuid": "9.0.0", "uuid": "9.0.0",
"vite": "4.3.5", "vite": "4.3.9",
"vite-plugin-sass-dts": "1.3.5", "vite-plugin-sass-dts": "1.3.5",
"vite-plugin-solid": "2.7.0", "vite-plugin-solid": "2.7.0",
"vite-plugin-ssr": "0.4.123", "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", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz",
"integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==",
"dev": true, "dev": true,
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/utils": "^5.10.0" "@typescript-eslint/utils": "^5.10.0"
}, },
@ -19958,9 +19957,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.3.5", "version": "4.3.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
"integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.17.5", "esbuild": "^0.17.5",
@ -27778,8 +27777,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz",
"integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==",
"dev": true, "dev": true,
"optional": true,
"peer": true,
"requires": { "requires": {
"@typescript-eslint/utils": "^5.10.0" "@typescript-eslint/utils": "^5.10.0"
} }
@ -35451,9 +35448,9 @@
"dev": true "dev": true
}, },
"vite": { "vite": {
"version": "4.3.5", "version": "4.3.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
"integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
"dev": true, "dev": true,
"requires": { "requires": {
"esbuild": "^0.17.5", "esbuild": "^0.17.5",

View File

@ -109,6 +109,7 @@
"eslint-config-stylelint": "18.0.0", "eslint-config-stylelint": "18.0.0",
"eslint-import-resolver-typescript": "3.5.5", "eslint-import-resolver-typescript": "3.5.5",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-jest": "27.2.1",
"eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-solid": "0.12.1", "eslint-plugin-solid": "0.12.1",
@ -168,7 +169,7 @@
"uniqolor": "1.1.0", "uniqolor": "1.1.0",
"unique-names-generator": "4.7.1", "unique-names-generator": "4.7.1",
"uuid": "9.0.0", "uuid": "9.0.0",
"vite": "4.3.5", "vite": "4.3.9",
"vite-plugin-sass-dts": "1.3.5", "vite-plugin-sass-dts": "1.3.5",
"vite-plugin-solid": "2.7.0", "vite-plugin-solid": "2.7.0",
"vite-plugin-ssr": "0.4.123", "vite-plugin-ssr": "0.4.123",

View File

@ -56,6 +56,11 @@
"Create Chat": "Create Chat", "Create Chat": "Create Chat",
"Create Group": "Create a group", "Create Group": "Create a group",
"Create account": "Create an account", "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", "Create post": "Create post",
"Date of Birth": "Date of Birth", "Date of Birth": "Date of Birth",
"Delete": "Delete", "Delete": "Delete",
@ -74,6 +79,11 @@
"Enter URL address": "Enter URL address", "Enter URL address": "Enter URL address",
"Enter text": "Enter text", "Enter text": "Enter text",
"Enter the Discours": "Enter the Discours", "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 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", "Enter your new password": "Enter your new password",
"Error": "Error", "Error": "Error",

View File

@ -58,6 +58,11 @@
"Create Chat": "Создать чат", "Create Chat": "Создать чат",
"Create Group": "Создать группу", "Create Group": "Создать группу",
"Create account": "Создать аккаунт", "Create account": "Создать аккаунт",
"Create account from subscribe": "Создайте аккаунт для подписки на новые публикации",
"Create account from discussions": "Создайте аккаунт для участия в дискуссиях",
"Create account from vote": "Создайте аккаунт, чтобы голосовать",
"Create account from bookmark": "Создайте аккаунт, чтобы добавить в закладки",
"Create account from follow": "Создайте аккаунт, чтобы подписаться",
"Create post": "Создать публикацию", "Create post": "Создать публикацию",
"Date of Birth": "Дата рождения", "Date of Birth": "Дата рождения",
"Delete": "Удалить", "Delete": "Удалить",
@ -77,6 +82,11 @@
"Enter URL address": "Введите адрес ссылки", "Enter URL address": "Введите адрес ссылки",
"Enter text": "Введите текст", "Enter text": "Введите текст",
"Enter the Discours": "Войти в Дискурс", "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 the code or click the link from email to confirm": "Введите код из письма или пройдите по ссылке в письме для подтверждения регистрации",
"Enter your new password": "Введите новый пароль", "Enter your new password": "Введите новый пароль",
"Error": "Ошибка", "Error": "Ошибка",

View File

@ -237,6 +237,7 @@ img {
margin-right: 0; margin-right: 0;
margin-left: auto; margin-left: auto;
white-space: nowrap; white-space: nowrap;
cursor: default;
.icon { .icon {
opacity: 0.4; opacity: 0.4;
@ -248,11 +249,18 @@ img {
} }
} }
.shoutStatsItemViews {
margin-left: 2rem;
margin-right: 0;
cursor: default;
}
.shoutStatsItemAdditionalDataItem { .shoutStatsItemAdditionalDataItem {
font-weight: normal; font-weight: normal;
display: inline-block; display: inline-block;
margin-left: 2rem; margin-left: 2rem;
margin-right: 0; margin-right: 0;
cursor: default;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
&:first-child { &:first-child {

View File

@ -62,7 +62,11 @@ const MediaView = (props: { media: MediaItem; kind: Shout['layout'] }) => {
export const FullArticle = (props: ArticleProps) => { export const FullArticle = (props: ArticleProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { user, isAuthenticated } = useSession() const {
user,
isAuthenticated,
actions: { requireAuthentication }
} = useSession()
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt))) 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) const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
// eslint-disable-next-line unicorn/consistent-function-scoping
const handleBookmarkButtonClick = (ev) => { const handleBookmarkButtonClick = (ev) => {
// TODO: implement bookmark clicked requireAuthentication(() => {
ev.preventDefault() // TODO: implement bookmark clicked
ev.preventDefault()
}, 'bookmark')
} }
const body = createMemo(() => props.article.body) const body = createMemo(() => props.article.body)
@ -222,12 +228,6 @@ export const FullArticle = (props: ArticleProps) => {
<ShoutRatingControl shout={props.article} class={styles.ratingControl} /> <ShoutRatingControl shout={props.article} class={styles.ratingControl} />
</div> </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')}> <Popover content={t('Comment')}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef} onClick={scrollToComments}> <div class={styles.shoutStatsItem} ref={triggerRef} onClick={scrollToComments}>
@ -280,6 +280,12 @@ export const FullArticle = (props: ArticleProps) => {
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalDataItem)}> <div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalDataItem)}>
{formattedDate()} {formattedDate()}
</div> </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> </div>
<div class={styles.help}> <div class={styles.help}>

View File

@ -16,7 +16,10 @@ interface ShoutRatingControlProps {
export const ShoutRatingControl = (props: ShoutRatingControlProps) => { export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { user } = useSession() const {
user,
actions: { requireAuthentication }
} = useSession()
const { const {
reactionEntities, reactionEntities,
@ -57,21 +60,23 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
} }
const handleRatingChange = async (isUpvote: boolean) => { const handleRatingChange = async (isUpvote: boolean) => {
if (isUpvoted()) { requireAuthentication(async () => {
await deleteShoutReaction(ReactionKind.Like) if (isUpvoted()) {
} else if (isDownvoted()) { await deleteShoutReaction(ReactionKind.Like)
await deleteShoutReaction(ReactionKind.Dislike) } else if (isDownvoted()) {
} else { await deleteShoutReaction(ReactionKind.Dislike)
await createReaction({ } else {
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, await createReaction({
shout: props.shout.id kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
}) shout: props.shout.id
} })
}
loadShout(props.shout.slug) loadShout(props.shout.slug)
loadReactionsBy({ loadReactionsBy({
by: { shout: props.shout.slug } by: { shout: props.shout.slug }
}) })
}, 'vote')
} }
return ( return (

View File

@ -13,6 +13,7 @@ import { FollowingEntity } from '../../graphql/types.gen'
import { router, useRouter } from '../../stores/router' import { router, useRouter } from '../../stores/router'
import { openPage } from '@nanostores/router' import { openPage } from '@nanostores/router'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { init } from 'i18next'
interface AuthorCardProps { interface AuthorCardProps {
caption?: string caption?: string
@ -40,7 +41,7 @@ export const AuthorCard = (props: AuthorCardProps) => {
const { const {
session, session,
isSessionLoaded, isSessionLoaded,
actions: { loadSession } actions: { loadSession, requireAuthentication }
} = useSession() } = useSession()
const [isSubscribing, setIsSubscribing] = createSignal(false) const [isSubscribing, setIsSubscribing] = createSignal(false)
@ -77,9 +78,18 @@ export const AuthorCard = (props: AuthorCardProps) => {
// TODO: reimplement AuthorCard // TODO: reimplement AuthorCard
const { changeSearchParam } = useRouter() const { changeSearchParam } = useRouter()
const initChat = () => { const initChat = () => {
openPage(router, `inbox`) requireAuthentication(() => {
changeSearchParam('initChat', `${props.author.id}`) openPage(router, `inbox`)
changeSearchParam('initChat', `${props.author.id}`)
}, 'discussions')
} }
const handleSubscribe = () => {
requireAuthentication(() => {
subscribe(true)
}, 'subscribe')
}
return ( return (
<div <div
class={clsx(styles.author, props.class)} class={clsx(styles.author, props.class)}
@ -133,7 +143,7 @@ export const AuthorCard = (props: AuthorCardProps) => {
when={subscribed()} when={subscribed()}
fallback={ fallback={
<button <button
onClick={() => subscribe(true)} onClick={handleSubscribe}
class={clsx('button', styles.button)} class={clsx('button', styles.button)}
classList={{ classList={{
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton, [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}> <Show when={!!props.beside?.slug && props.values?.length > 0}>
<div class="floor floor--9"> <div class="floor floor--9">
<div class="wide-container"> <div class="wide-container">
<div class="row"> <div class="row justify-content-between">
<Show when={!!props.values}> <Show when={!!props.values}>
<div <div
class={clsx( class={clsx(
@ -102,7 +102,7 @@ export const Beside = (props: BesideProps) => {
</ul> </ul>
</div> </div>
</Show> </Show>
<div class={clsx('col-md-16', styles.shoutCardContainer)}> <div class={clsx('col-md-14', styles.shoutCardContainer)}>
<ArticleCard <ArticleCard
article={props.beside} article={props.beside}
settings={{ isBigTitle: true, isBeside: true, nodate: props.nodate }} 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 { useSession } from '../../../context/session'
import { signSendLink } from '../../../stores/auth' import { signSendLink } from '../../../stores/auth'
import { isValidEmail } from '../../../utils/validators' import { isValidEmail } from '../../../utils/validators'
import { generateModalTitleFromSource } from '../../../utils/custom-i18n'
import { useSnackbar } from '../../../context/snackbar' import { useSnackbar } from '../../../context/snackbar'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -117,7 +118,7 @@ export const LoginForm = () => {
return ( return (
<form onSubmit={handleSubmit} class={styles.authForm}> <form onSubmit={handleSubmit} class={styles.authForm}>
<div> <div>
<h4>{t('Enter the Discours')}</h4> <h4>{generateModalTitleFromSource('login')}</h4>
<Show when={submitError()}> <Show when={submitError()}>
<div class={styles.authInfo}> <div class={styles.authInfo}>
<div class={styles.warn}>{submitError()}</div> <div class={styles.warn}>{submitError()}</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -13,27 +13,35 @@ type Props = {
} }
export const FullTopic = (props: Props) => { export const FullTopic = (props: Props) => {
const { session } = useSession() const {
session,
actions: { requireAuthentication }
} = useSession()
const { t } = useLocalize() const { t } = useLocalize()
const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug)) 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 ( return (
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}> <div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
<h1>#{props.topic.title}</h1> <h1>#{props.topic.title}</h1>
<p>{props.topic.body}</p> <p>{props.topic.body}</p>
<div class={clsx(styles.topicActions)}> <div class={clsx(styles.topicActions)}>
<Show when={!subscribed()}> <Show when={!subscribed()}>
<button <button onClick={() => handleSubscribe(false)} class="button">
onClick={() => follow({ what: FollowingEntity.Topic, slug: props.topic.slug })}
class="button"
>
{t('Follow the topic')} {t('Follow the topic')}
</button> </button>
</Show> </Show>
<Show when={subscribed()}> <Show when={subscribed()}>
<button <button onClick={() => handleSubscribe(true)} class="button">
onClick={() => unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })}
class="button"
>
{t('Unfollow the topic')} {t('Unfollow the topic')}
</button> </button>
</Show> </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 { createContext, createMemo, createResource, createSignal, onMount, useContext } from 'solid-js'
import type { AuthResult, User } from '../graphql/types.gen' import type { AuthResult, User } from '../graphql/types.gen'
import { apiClient } from '../utils/apiClient' import { apiClient } from '../utils/apiClient'
import { resetToken, setToken } from '../graphql/privateGraphQLClient' import { resetToken, setToken } from '../graphql/privateGraphQLClient'
import { useSnackbar } from './snackbar' import { useSnackbar } from './snackbar'
import { useLocalize } from './localize' import { useLocalize } from './localize'
import { showModal } from '../stores/ui'
import type { AuthModalSource } from '../components/Nav/AuthModal/types'
type SessionContextType = { type SessionContextType = {
session: Resource<AuthResult> session: Resource<AuthResult>
@ -13,6 +15,10 @@ type SessionContextType = {
isAuthenticated: Accessor<boolean> isAuthenticated: Accessor<boolean>
actions: { actions: {
loadSession: () => AuthResult | Promise<AuthResult> loadSession: () => AuthResult | Promise<AuthResult>
requireAuthentication: (
callback: (() => Promise<void>) | (() => void),
modalSource: AuthModalSource
) => void
signIn: ({ email, password }: { email: string; password: string }) => Promise<void> signIn: ({ email, password }: { email: string; password: string }) => Promise<void>
signOut: () => Promise<void> signOut: () => Promise<void>
confirmEmail: (token: string) => Promise<void> confirmEmail: (token: string) => Promise<void>
@ -65,6 +71,28 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
console.debug('signed in') 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 () => { const signOut = async () => {
// TODO: call backend to revoke token // TODO: call backend to revoke token
mutate(null) mutate(null)
@ -80,6 +108,7 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
const actions = { const actions = {
loadSession, loadSession,
requireAuthentication,
signIn, signIn,
signOut, signOut,
confirmEmail confirmEmail

View File

@ -1,6 +1,10 @@
import { createSignal } from 'solid-js' import { createSignal } from 'solid-js'
import { useRouter } from './router' 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' import type { RootSearchParams } from '../pages/types'
export type ModalType = export type ModalType =
@ -36,14 +40,20 @@ const [modal, setModal] = createSignal<ModalType | null>(null)
const [warnings, setWarnings] = createSignal<Warning[]>([]) const [warnings, setWarnings] = createSignal<Warning[]>([])
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 // TODO: find a better solution
export const hideModal = () => { export const hideModal = () => {
const { searchParams, changeSearchParam } = useRouter<
AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams
>()
if (searchParams().modal === 'auth') { if (searchParams().modal === 'auth') {
if (searchParams().mode === 'confirm-email') { if (searchParams().mode === 'confirm-email') {
changeSearchParam('token', null, true) changeSearchParam('token', null, true)
@ -52,6 +62,7 @@ export const hideModal = () => {
} }
changeSearchParam('modal', null, true) changeSearchParam('modal', null, true)
changeSearchParam('source', null, true)
setModal(null) 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)')