diff --git a/.eslintrc.js b/.eslintrc.js index 6b46108b..3a5f8a20 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,8 +35,9 @@ module.exports = { varsIgnorePattern: '^log$' } ], - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-non-null-assertion': 'warn', + // TODO: Remove any usage and enable + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'error', // solid-js fix 'import/no-unresolved': [2, { ignore: ['solid-js/'] }] diff --git a/.lintstagedrc b/.lintstagedrc index 6de8a64b..8c919270 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,6 +1,3 @@ { - "*.{js,ts,tsx,json,scss,css,html}": "prettier --write", - "package.json": "sort-package-json", - "*.{scss,css}": "stylelint", - "*.{ts,tsx,js}": "eslint --fix" + "*.{js,ts,tsx,json,scss,css,html}": "prettier --write" } diff --git a/.lintstagedrc.bak b/.lintstagedrc.bak new file mode 100644 index 00000000..6de8a64b --- /dev/null +++ b/.lintstagedrc.bak @@ -0,0 +1,6 @@ +{ + "*.{js,ts,tsx,json,scss,css,html}": "prettier --write", + "package.json": "sort-package-json", + "*.{scss,css}": "stylelint", + "*.{ts,tsx,js}": "eslint --fix" +} diff --git a/package.json b/package.json index e32a446f..ed378be4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "lint:code:fix": "eslint . --fix", "lint:styles": "stylelint **/*.{scss,css}", "lint:styles:fix": "stylelint **/*.{scss,css} --fix", - "pre-commit": "", + "pre-commit": "lint-staged", "pre-push": "", "pre-commit-old": "lint-staged", "pre-push-old": "npm run typecheck", diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 512cdac9..ab6d6b07 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -10,6 +10,7 @@ import { showModal } from '../../stores/ui' import { useAuthStore } from '../../stores/auth' import { incrementView } from '../../stores/zine/articles' import MD from './MD' +import { SharePopup } from './SharePopup' const MAX_COMMENT_LEVEL = 6 @@ -126,9 +127,13 @@ export const FullArticle = (props: ArticleProps) => { {/* */} {/**/}
- showModal('share')}> - - + event.preventDefault()}> + + + } + />
{/*FIXME*/} {/**/} diff --git a/src/components/Article/SharePopup.tsx b/src/components/Article/SharePopup.tsx new file mode 100644 index 00000000..851cf599 --- /dev/null +++ b/src/components/Article/SharePopup.tsx @@ -0,0 +1,45 @@ +import { Icon } from '../Nav/Icon' +import styles from '../Nav/Popup.module.scss' +import { t } from '../../utils/intl' +import { Popup, PopupProps } from '../Nav/Popup' + +type SharePopupProps = Omit + +export const SharePopup = (props: SharePopupProps) => { + return ( + + + + ) +} diff --git a/src/components/Author/Full.tsx b/src/components/Author/Full.tsx index 9f869f7e..b83ed413 100644 --- a/src/components/Author/Full.tsx +++ b/src/components/Author/Full.tsx @@ -2,7 +2,7 @@ import type { Author } from '../../graphql/types.gen' import { AuthorCard } from './Card' import './Full.scss' -export default (props: { author: Author }) => { +export const AuthorFull = (props: { author: Author }) => { return (
diff --git a/src/components/Author/Userpic.tsx b/src/components/Author/Userpic.tsx index 0a075738..b89a1a0a 100644 --- a/src/components/Author/Userpic.tsx +++ b/src/components/Author/Userpic.tsx @@ -1,11 +1,13 @@ import { Show } from 'solid-js/web' import type { Author } from '../../graphql/types.gen' import style from './Userpic.module.scss' +import { clsx } from 'clsx' interface UserpicProps { user: Author hasLink?: boolean isBig?: boolean + class?: string } export default (props: UserpicProps) => { @@ -16,7 +18,7 @@ export default (props: UserpicProps) => { } return ( -
+
{ +export const Beside = (props: BesideProps) => { return ( 0}>
diff --git a/src/components/Feed/Card.module.scss b/src/components/Feed/Card.module.scss index 3a1b21bd..09ba51d8 100644 --- a/src/components/Feed/Card.module.scss +++ b/src/components/Feed/Card.module.scss @@ -418,7 +418,7 @@ display: flex; } -.shoutCardDetailsTtem { +.shoutCardDetailsItem { align-items: center; display: flex; margin-right: 1.7em; @@ -454,6 +454,12 @@ } } +.shoutCardDetailsViewed { + .icon { + margin-top: -0.1em; + } +} + .rating { align-items: center; display: flex; diff --git a/src/components/Feed/Card.tsx b/src/components/Feed/Card.tsx index c1ae7cda..ca378e7c 100644 --- a/src/components/Feed/Card.tsx +++ b/src/components/Feed/Card.tsx @@ -5,7 +5,7 @@ import type { Shout } from '../../graphql/types.gen' import { capitalize } from '../../utils' import { translit } from '../../utils/ru2en' import { Icon } from '../Nav/Icon' -import style from './Card.module.scss' +import styles from './Card.module.scss' import { locale } from '../../stores/ui' import { handleClientRouteLinkClick } from '../../stores/router' import { clsx } from 'clsx' @@ -72,33 +72,33 @@ export const ArticleCard = (props: ArticleCardProps) => { return (
-
-
+
+
{title
-
+
-
+ @@ -113,24 +113,24 @@ export const ArticleCard = (props: ArticleCardProps) => { /> -
+
-
- {title} +
+ {title}
-
-
+
-
+
{(author, index) => { const name = @@ -150,44 +150,50 @@ export const ArticleCard = (props: ArticleCardProps) => { -
{formattedDate()}
+
{formattedDate()}
-
-
-
- - {stat?.rating || ''} - +
+
+
+ + {stat?.rating || ''} +
-
- +
+ {stat?.viewed}
-
+ -
+
-
+
- +
diff --git a/src/components/Feed/List.tsx b/src/components/Feed/List.tsx index dac7e127..055378a4 100644 --- a/src/components/Feed/List.tsx +++ b/src/components/Feed/List.tsx @@ -1,7 +1,7 @@ import { For, Suspense } from 'solid-js/web' -import OneWide from './Row1' -import Row2 from './Row2' -import Row3 from './Row3' +import { Row1 } from './Row1' +import { Row2 } from './Row2' +import { Row3 } from './Row3' import { shuffle } from '../../utils' import { createMemo, createSignal } from 'solid-js' import type { JSX } from 'solid-js' @@ -10,7 +10,7 @@ import './List.scss' import { t } from '../../utils/intl' export const Block6 = (props: { articles: Shout[] }) => { - const dice = createMemo(() => shuffle([OneWide, Row2, Row3])) + const dice = createMemo(() => shuffle([Row1, Row2, Row3])) return ( <> diff --git a/src/components/Feed/Row1.tsx b/src/components/Feed/Row1.tsx index b654fb82..bfb68aca 100644 --- a/src/components/Feed/Row1.tsx +++ b/src/components/Feed/Row1.tsx @@ -2,7 +2,7 @@ import { Show } from 'solid-js' import type { Shout } from '../../graphql/types.gen' import { ArticleCard } from './Card' -export default (props: { article: Shout }) => ( +export const Row1 = (props: { article: Shout }) => (
diff --git a/src/components/Feed/Row2.tsx b/src/components/Feed/Row2.tsx index e12b22bd..274317a8 100644 --- a/src/components/Feed/Row2.tsx +++ b/src/components/Feed/Row2.tsx @@ -2,13 +2,14 @@ import { createComputed, createSignal, Show } from 'solid-js' import { For } from 'solid-js/web' import type { Shout } from '../../graphql/types.gen' import { ArticleCard } from './Card' + const x = [ ['6', '6'], ['4', '8'], ['8', '4'] ] -export default (props: { articles: Shout[] }) => { +export const Row2 = (props: { articles: Shout[] }) => { const [y, setY] = createSignal(0) createComputed(() => setY(Math.floor(Math.random() * x.length))) diff --git a/src/components/Feed/Row3.tsx b/src/components/Feed/Row3.tsx index 280d844a..21ba6727 100644 --- a/src/components/Feed/Row3.tsx +++ b/src/components/Feed/Row3.tsx @@ -3,7 +3,7 @@ import { For } from 'solid-js/web' import type { Shout } from '../../graphql/types.gen' import { ArticleCard } from './Card' -export default (props: { articles: Shout[]; header?: JSX.Element }) => { +export const Row3 = (props: { articles: Shout[]; header?: JSX.Element }) => { return (
diff --git a/src/components/Feed/Sidebar.tsx b/src/components/Feed/Sidebar.tsx index b347310e..09dac4a8 100644 --- a/src/components/Feed/Sidebar.tsx +++ b/src/components/Feed/Sidebar.tsx @@ -85,12 +85,12 @@ export const FeedSidebar = (props: FeedSidebarProps) => { -

+

) } diff --git a/src/components/Nav/AuthModal/AuthModal.module.scss b/src/components/Nav/AuthModal/AuthModal.module.scss index 91a75f17..3a89155e 100644 --- a/src/components/Nav/AuthModal/AuthModal.module.scss +++ b/src/components/Nav/AuthModal/AuthModal.module.scss @@ -160,3 +160,17 @@ } } } + +.title { + font-size: 26px; + line-height: 32px; + font-weight: 700; + color: #141414; + margin-bottom: 16px; +} + +.text { + font-size: 15px; + line-height: 24px; + margin-bottom: 52px; +} diff --git a/src/components/Nav/AuthModal/EmailConfirm.tsx b/src/components/Nav/AuthModal/EmailConfirm.tsx index 6588285e..1fd1c7aa 100644 --- a/src/components/Nav/AuthModal/EmailConfirm.tsx +++ b/src/components/Nav/AuthModal/EmailConfirm.tsx @@ -1,18 +1,19 @@ -import styles from './EmailConfirm.module.scss' -import authModalStyles from './AuthModal.module.scss' +import styles from './AuthModal.module.scss' import { clsx } from 'clsx' import { t } from '../../../utils/intl' import { hideModal } from '../../../stores/ui' -import { onMount } from 'solid-js' +import { createMemo, onMount, Show } from 'solid-js' import { useRouter } from '../../../stores/router' -import { confirmEmail } from '../../../stores/auth' +import { confirmEmail, useAuthStore } from '../../../stores/auth' type ConfirmEmailSearchParams = { token: string } export const EmailConfirm = () => { - const confirmedEmail = 'test@test.com' + const { session } = useAuthStore() + + const confirmedEmail = createMemo(() => session()?.user?.email || '') const { searchParams } = useRouter() @@ -28,12 +29,14 @@ export const EmailConfirm = () => { return (
{t('Hooray! Welcome!')}
-
- {t("You've confirmed email")} {confirmedEmail} -
+ +
+ {t("You've confirmed email")} {confirmedEmail()} +
+
-
diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index ebd09d47..173727eb 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -7,8 +7,6 @@ import { useRouter } from '../../../stores/router' import { email, setEmail } from './sharedLogic' import type { AuthModalSearchParams } from './types' import { isValidEmail } from './validators' -import { checkEmail, register } from '../../../stores/auth' -import { ApiError } from '../../../utils/apiClient' type FormFields = { email: string @@ -61,9 +59,9 @@ export const ForgotPasswordForm = () => { } return ( -
+

{t('Forgot password?')}

- {t('Everything is ok, please give us your email address')} +
{t('Everything is ok, please give us your email address')}
    diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index 115ee075..c683b223 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -3,13 +3,14 @@ import { t } from '../../../utils/intl' import styles from './AuthModal.module.scss' import { clsx } from 'clsx' import { SocialProviders } from './SocialProviders' -import { signIn } from '../../../stores/auth' +import { signIn, signSendLink } from '../../../stores/auth' import { ApiError } from '../../../utils/apiClient' import { createSignal } from 'solid-js' import { isValidEmail } from './validators' import { email, setEmail } from './sharedLogic' import { useRouter } from '../../../stores/router' import type { AuthModalSearchParams } from './types' +import { hideModal } from '../../../stores/ui' type FormFields = { email: string @@ -22,6 +23,9 @@ export const LoginForm = () => { const [submitError, setSubmitError] = createSignal('') const [isSubmitting, setIsSubmitting] = createSignal(false) const [validationErrors, setValidationErrors] = createSignal({}) + // TODO: better solution for interactive error messages + const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false) + const [isLinkSent, setIsLinkSent] = createSignal(false) const { changeSearchParam } = useRouter() @@ -37,9 +41,18 @@ export const LoginForm = () => { setPassword(newPassword) } + const handleSendLinkAgainClick = (event: Event) => { + event.preventDefault() + setIsEmailNotConfirmed(false) + setSubmitError('') + setIsLinkSent(true) + signSendLink({ email: email() }) + } + const handleSubmit = async (event: Event) => { event.preventDefault() + setIsLinkSent(false) setSubmitError('') const newValidationErrors: ValidationErrors = {} @@ -63,10 +76,12 @@ export const LoginForm = () => { try { await signIn({ email: email(), password: password() }) + hideModal() } catch (error) { if (error instanceof ApiError) { if (error.code === 'email_not_confirmed') { setSubmitError(t('Please, confirm email')) + setIsEmailNotConfirmed(true) return } @@ -87,11 +102,17 @@ export const LoginForm = () => {

    {t('Enter the Discours')}

    -
      -
    • {submitError()}
    • -
    +
    {submitError()}
    + + + {t('Send link again')} + +
    + +
    {t('Link sent, check your email')}
    +
    { const [name, setName] = createSignal('') const [password, setPassword] = createSignal('') const [isSubmitting, setIsSubmitting] = createSignal(false) + const [isSuccess, setIsSuccess] = createSignal(false) const [validationErrors, setValidationErrors] = createSignal({}) const handleEmailInput = (newEmail: string) => { @@ -91,6 +93,8 @@ export const RegisterForm = () => { email: email(), password: password() }) + + setIsSuccess(true) } catch (error) { if (error instanceof ApiError && error.code === 'user_already_exists') { return @@ -103,87 +107,100 @@ export const RegisterForm = () => { } return ( - -

    {t('Create account')}

    - -
    -
      -
    • {submitError()}
    • -
    + <> + + +

    {t('Create account')}

    + +
    +
      +
    • {submitError()}
    • +
    +
    +
    +
    + handleNameInput(event.currentTarget.value)} + /> + +
    + +
    {validationErrors().name}
    +
    +
    + handleEmailInput(event.currentTarget.value)} + onBlur={handleEmailBlur} + /> + +
    + +
    {validationErrors().email}
    +
    + + + +
    + handlePasswordInput(event.currentTarget.value)} + /> + +
    + +
    {validationErrors().password}
    +
    + +
    + +
    + + + +
    + changeSearchParam('mode', 'login')}> + {t('I have an account')} + +
    + +
    + +
    {t('Almost done! Check your email.')}
    +
    {t("We've sent you a message with a link to enter our website.")}
    +
    +
    -
    - handleNameInput(event.currentTarget.value)} - /> - -
    - -
    {validationErrors().name}
    -
    -
    - handleEmailInput(event.currentTarget.value)} - onBlur={handleEmailBlur} - /> - -
    - -
    {validationErrors().email}
    -
    - - - -
    - handlePasswordInput(event.currentTarget.value)} - /> - -
    - -
    {validationErrors().password}
    -
    - -
    - -
    - - - -
    - changeSearchParam('mode', 'login')}> - {t('I have an account')} - -
    - + ) } diff --git a/src/components/Nav/AuthModal/index.tsx b/src/components/Nav/AuthModal/index.tsx index 91480993..f0a773d1 100644 --- a/src/components/Nav/AuthModal/index.tsx +++ b/src/components/Nav/AuthModal/index.tsx @@ -1,5 +1,5 @@ -import { Show } from 'solid-js/web' -import { createEffect, createMemo, onMount } from 'solid-js' +import { Dynamic } from 'solid-js/web' +import { Component, createEffect, createMemo } from 'solid-js' import { t } from '../../../utils/intl' import { hideModal } from '../../../stores/ui' import { handleClientRouteLinkClick, useRouter } from '../../../stores/router' @@ -11,12 +11,11 @@ import { ForgotPasswordForm } from './ForgotPasswordForm' import { EmailConfirm } from './EmailConfirm' import type { AuthModalMode, AuthModalSearchParams } from './types' -const AUTH_MODAL_MODES: Record = { - login: 'login', - register: 'register', - 'forgot-password': 'forgot-password', - // eslint-disable-next-line sonarjs/no-duplicate-string - 'confirm-email': 'confirm-email' +const AUTH_MODAL_MODES: Record = { + login: LoginForm, + register: RegisterForm, + 'forgot-password': ForgotPasswordForm, + 'confirm-email': EmailConfirm } export const AuthModal = () => { @@ -25,7 +24,7 @@ export const AuthModal = () => { const { searchParams } = useRouter() const mode = createMemo(() => { - return AUTH_MODAL_MODES[searchParams().mode] || 'login' + return AUTH_MODAL_MODES[searchParams().mode] ? searchParams().mode : 'login' }) createEffect((oldMode) => { @@ -70,18 +69,7 @@ export const AuthModal = () => {
    - - - - - - - - - - - - +
) diff --git a/src/components/Nav/Header.module.scss b/src/components/Nav/Header.module.scss index 0a34a45e..266c5cf1 100644 --- a/src/components/Nav/Header.module.scss +++ b/src/components/Nav/Header.module.scss @@ -35,18 +35,6 @@ } } -.popupShare { - opacity: 1; - transition: opacity 0.3s; - z-index: 1; - - .headerScrolledTop & { - opacity: 0; - transition: opacity 0.3s, z-index 0s 0.3s; - z-index: -1; - } -} - .headerFixed { position: fixed; top: 0; @@ -327,18 +315,6 @@ } } -.userControl { - opacity: 1; - transition: opacity 0.3s; - z-index: 1; - - .headerWithTitle.headerScrolledBottom & { - transition: opacity 0.3s, z-index 0s 0.3s; - opacity: 0; - z-index: -1; - } -} - .articleControls { display: flex; justify-content: flex-end; @@ -348,18 +324,14 @@ transform: translateY(-50%); width: 100%; - .icon { - margin-left: 1.6rem; - opacity: 0.6; - transition: opacity 0.3s; - } + .control { + cursor: pointer; + border: 0; - img { - vertical-align: middle; - } - - a { - border: none; + .icon { + opacity: 0.6; + transition: opacity 0.3s; + } &:hover { background: none; @@ -370,4 +342,138 @@ } } } + + .control + .control { + margin-left: 1.6rem; + } + + img { + vertical-align: middle; + } +} + +.userControl { + align-items: baseline; + display: flex; + opacity: 1; + transition: opacity 0.3s; + z-index: 1; + + .headerWithTitle.headerScrolledBottom & { + transition: opacity 0.3s, z-index 0s 0.3s; + opacity: 0; + z-index: -1; + } + + @include font-size(1.7rem); + + justify-content: flex-end; + + @include media-breakpoint-down(md) { + padding: divide($container-padding-x, 2); + } + + .userpic { + margin-right: 0; + + img { + height: 100%; + width: 100%; + } + } +} + +.userControlItem { + align-items: center; + border: 2px solid #f6f6f6; + border-radius: 100%; + display: flex; + height: 2.4em; + justify-content: center; + margin-left: divide($container-padding-x, 2); + position: relative; + width: 2.4em; + + @include media-breakpoint-up(sm) { + margin-left: 1.2rem; + } + + .circlewrap { + height: 23px; + min-width: 23px; + width: 23px; + } + + .button, + a { + border: none; + + &:hover { + background: none; + + &::before { + background-color: #000; + } + + img { + filter: invert(1); + } + } + + img { + filter: invert(0); + transition: filter 0.3s; + } + + &::before { + background-color: #fff; + border-radius: 100%; + content: ''; + height: 100%; + left: 0; + position: absolute; + top: 0; + transition: background-color 0.3s; + width: 100%; + } + } + + img { + height: 20px; + vertical-align: middle; + width: auto; + } + + .textLabel { + display: none; + } +} + +.userControlItemInbox, +.userControlItemSearch { + @include media-breakpoint-down(sm) { + display: none; + } +} + +.userControlItemWritePost { + width: auto; + + @include media-breakpoint-up(lg) { + .icon { + display: none; + } + + .textLabel { + display: inline; + padding: 0 1.2rem; + position: relative; + z-index: 1; + } + } + + &, + a::before { + border-radius: 1.2em; + } } diff --git a/src/components/Nav/Header.tsx b/src/components/Nav/Header.tsx index be6979ea..aa29bb56 100644 --- a/src/components/Nav/Header.tsx +++ b/src/components/Nav/Header.tsx @@ -1,19 +1,22 @@ import { For, Show, createSignal, createMemo, createEffect, onMount, onCleanup } from 'solid-js' -import Private from './Private' import Notifications from './Notifications' import { Icon } from './Icon' import { Modal } from './Modal' -import { Popup } from './Popup' import { AuthModal } from './AuthModal' import { t } from '../../utils/intl' import { useModalStore, showModal, useWarningsStore } from '../../stores/ui' import { useAuthStore } from '../../stores/auth' import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router' import styles from './Header.module.scss' -import stylesPopup from './Popup.module.scss' -import privateStyles from './Private.module.scss' import { getPagePath } from '@nanostores/router' +import { getLogger } from '../../utils/logger' import { clsx } from 'clsx' +import { SharePopup } from '../Article/SharePopup' +import { ProfilePopup } from './ProfilePopup' +import Userpic from '../Author/Userpic' +import type { Author } from '../../graphql/types.gen' + +const log = getLogger('header') const resources: { name: string; route: keyof Routes }[] = [ { name: t('zine'), route: 'home' }, @@ -32,6 +35,9 @@ export const Header = (props: Props) => { const [getIsScrolled, setIsScrolled] = createSignal(false) const [fixed, setFixed] = createSignal(false) const [visibleWarnings, setVisibleWarnings] = createSignal(false) + const [isSharePopupVisible, setIsSharePopupVisible] = createSignal(false) + const [isProfilePopupVisible, setIsProfilePopupVisible] = createSignal(false) + // stores const { warnings } = useWarningsStore() const { session } = useAuthStore() @@ -41,13 +47,11 @@ export const Header = (props: Props) => { // methods const toggleWarnings = () => setVisibleWarnings(!visibleWarnings()) - const toggleFixed = () => setFixed(!fixed()) + const toggleFixed = () => setFixed((oldFixed) => !oldFixed) // effects createEffect(() => { - const isFixed = fixed() || (modal() && modal() !== 'share') - - document.body.classList.toggle('fixed', isFixed) - document.body.classList.toggle(styles.fixed, isFixed && !modal()) + document.body.classList.toggle('fixed', fixed() || modal() !== null) + document.body.classList.toggle(styles.fixed, fixed() && !modal()) }) // derived @@ -85,7 +89,8 @@ export const Header = (props: Props) => { classList={{ [styles.headerFixed]: props.isHeaderFixed, [styles.headerScrolledTop]: !getIsScrollingBottom() && getIsScrolled(), - [styles.headerScrolledBottom]: getIsScrollingBottom() && getIsScrolled(), + [styles.headerScrolledBottom]: + (getIsScrollingBottom() && getIsScrolled() && !isProfilePopupVisible()) || isSharePopupVisible(), [styles.headerWithTitle]: Boolean(props.title) }} > @@ -94,41 +99,6 @@ export const Header = (props: Props) => {
- - - -